B阶段流片准备与考核

恭喜你! 你已经基本上达成B阶段的流片指标了. 接下来你还需要进行一些流片相关的准备工作.

流片前的准备工作

切换到32位ysyxSoC

SoC团队已经提供了32位的流片SoC, 我们也已经把ysyxSoC修改成32位, 以帮助大家在接入流片SoC之前在本地环境中进行测试. 和之前的64位ysyxSoC相比, 32位ysyxSoC主要将总线的数据宽度改为32位, 并去除了AXI数据位宽转换模块.

切换到32位ysyxSoC

如果你在2024/07/26 13:00:00之后克隆ysyxSoC项目, 你无需进行任何操作; 否则, 请进行以下步骤:

  1. 执行以下命令获取新的32位ysyxSoC:
    cd ysyxSoC
    git pull origin master # 你可能需要解决一些代码冲突
    
  2. 将NPC顶层的AXI数据位宽修改为32位, 并去除相关的数据位宽转换代码
  3. 重新使用32位的ysyxSoC环境进行仿真

去除下降沿触发的时钟

上升沿和下降沿混用, 会导致时序收敛更加困难, 增加后端物理实现的难度. 如果你的处理器严重影响SoC整体的时序, 在流片时间节点紧张的情况下, "一生一芯"项目组将会把你的处理器移出该批次的流片名单.

去除下降沿触发的时钟

你需要检查你的代码中是否包含下降沿触发的时钟, 若有, 你需要修改相应模块中的代码去除它们.

去除锁存器

在同步时序逻辑电路中, 所有存储单元的访问都受时钟控制. 但锁存器的写入不受时钟驱动, 时序分析工具难以对其进行分析, 因此一般不在同步时序逻辑电路中使用.

去除锁存器

你需要通过yosys对你的设计进行综合, 然后检查synth_stat.txt中是否包含以下标准单元: DLL_X1, DLL_X2, DLH_X1, DLH_X2. 若有, 说明你的设计中存在锁存器, 你需要修改相应模块中的代码去除它们.

Chisel福利

Chisel生成的Verilog代码描述的都是同步时序逻辑电路, 如果你使用Chisel开发, 你不必担心会生成锁存器.

命名修改

我们会将多个同学的代码集成到同一个SoC中, 如果代码的模块名相同, 会导致EDA工具报告模块重复定义的错误. 为了解决这个问题, 我们要求大家进行以下修改:

  1. 将CPU代码合并到一个.v文件, 文件名为ysyx_8位学号.v, 如ysyx_22040228.v
    • 在Linux上可通过cat命令实现:
      $> cat CPU.v ALU.v regs.v ... > ysyx_22040228.v
      
  2. 将CPU顶层命名修改为ysyx_8位学号,如ysyx_22040228
  3. 为CPU内所有用define定义的变量添加前缀ysyx_8位学号_, 如 `define SIZE 5需要修改为 `define ysyx_22040228_SIZE 5; 如果你使用Chisel, 你不必进行这部分的修改
  4. 为CPU内的所有模块名添加前缀ysyx_8位学号_, 如module ALU修改为module ysyx_22040228_ALU

Chisel福利(2)

如果你使用Chisel开发, 可以通过给Chisel代码添加Annotation来自动添加模块名前缀, 具体方式可参考这个issue中的讨论在新窗口中打开. 如果你使用Verilog/SystemVerilog开发, 目前暂时无法进行模块名前缀的自动添加, 请手动进行添加.

命名修改

根据上述要求, 完成命名修改的工作.

修改后, 通过yosys进行综合, 然后通过如下命令检查是否存在未修改的模块:

grep "^Checking module" yosys.log | sort | uniq | sed -e 's/.* \(.*\)...$/\1/' | grep -v "^ysyx_"

上述命令所输出的模块名即为未修改的模块.

代码静态检查

Verilator可以对verilog进行代码静态检查, 并提示代码中潜在的错误风险之处, 修复这些问题有助于提升代码的正确性. 具体地, 可以通过verilator的--lint-only选项来进行上述检查.

为了让verilator进行尽可能多的检查工作, 可以添加-Wall-Werror选项. verilator的手册在新窗口中打开记录了所有warning的含义, 原则上来说, 你需要尽可能清除它们, 来让你的代码变得更规范, 除了以下两种warning:

  1. DECLFILENAME的warning与代码逻辑无关, 可通过添加-Wno-DECLFILENAME来忽略它.
  2. 对于UNUSED相关的warning, 有的是因为输入端口未使用, 如顶层的io_slave_arvalid, 这部分warning可以忽略; 有的可能是因为你的代码编写不规范, 你需要仔细确认其原因. 如果你决定忽略一个warning, 你需要清楚这可能会引起什么问题, 并对你自己的决定负责.

Chisel福利(3)

Chisel生成的Verilog代码通常是符合规编码规范的, 但也会出现信号和位宽相关的UNUSED warning. 如果你使用Chisel开发, 在这部分工作中你会稍微轻松一些, 但我们仍然建议你仔细核对每个waring.

四值仿真

系统的启动分冷启动和热启动, 其中冷启动是指系统在断电状态下进行上电和复位, 而热启动则是指在上电状态下直接进行复位. 我们知道电路的状态由时序逻辑元件的状态决定, 因此我们期望电路在复位后能进入一个预期的正确状态, 然后从这个状态开始工作. 要实现这一点, 就需要对时序逻辑元件指定复位值.

一个方案是对所有时序逻辑元件进行复位, 保证无论是冷启动还是热启动, 系统都能在复位后进入完全一致的状态. 但对于包含随机存储器的电路来说, 这是无法做到的. 例如, DRAM存储颗粒中的存储阵列没有复位功能, 即使是可以集成在CPU中的SRAM, 其存储阵列也没有复位功能. 这意味着, 在热启动的场景下, 随机存储器中存储阵列的状态不受复位信号的影响, 其中已经存储的数据会被带进下一次复位中. 因此, 电路必须保证, 无论复位后随机存储器中存储什么数据, 电路都应该能正确工作. 换句话说, 电路复位时的行为必须设计成与随机存储器所存储的内容无关.

如果不考虑随机存储器, 剩下的时序逻辑元件就是触发器. 对所有触发器进行复位固然可以使得系统以更大的概率进入正确的状态, 但这也可能会带来额外的延迟和一定的面积开销. 一方面, 布线时需要将复位信号传递到所有触发器中, 因此复位信号的传播延迟也会更高; 另一方面, 触发器的复位功能也需要占用一定的门电路, 这些门电路也会占用一定的面积.

在真实的项目中, 一般是在保证复位后能正确工作的条件下, 仅仅对触发器的一个最小子集进行复位. 至于那些不需要进行复位的触发器, 它们的状态不受复位信号的影响, 因此系统的复位信号到来时, 它们仍然保留之前的状态. 只要一个触发器存储的值不影响电路在复位后的行为, 它就不需要进行复位.

通常来说, 不需要进行复位的触发器主要有两类:

  1. 软件在使用之前可对其初始化的寄存器. 这意味着这些寄存器需要是软件可见的, 换句话说, 它们是由ISA规范定义的. 例如, 对于除了零寄存器以外的通用寄存器, RISC-V规范约定, 复位后其值是未定义的, 因此软件在读出它们之前, 应该对这些通用寄存器进行写入, 确保其中已经存放一个有意义的值. 类似的还有mepc, RISC-V规范同样约定, 复位后其值是未定义的, 因此软件在读出mepc之前, 要么已经发生过一次异常, 使得硬件已经往mepc中写入有意义的值, 要么软件已经通过CSR指令对其进行过写入操作. 事实上, 相关ISA规范将这些寄存器的初值约定为未定义, 本质上是将相关寄存器的初始化从硬件层面移动到软件层面, 从而优化了电路的面积和延迟.
  2. 在数据通路上与控制信号关联的触发器. 通常来说, 类似valid, ready, en等控制信号会用于指示相应的数据信号是否有效, 如果无效, 这些控制信号所连接的下游模块一般不会使用或存储相应的数据信号. 因此, 数据通路上的大部分触发器可以不进行复位, 只要保证电路在产生与其关联的控制信号时, 相应的触发器已经写入了有效的数据即可. 一个例子是流水段寄存器中和存放负载的触发器, 我们只需要对相应的valid寄存器复位即可. 不过具体情况还需要根据你的实现来决定.

对不需要进行复位的触发器进行了复位, 最多是浪费了面积, 增加了复位信号的传播延迟; 但如果没有对需要复位的触发器进行复位, 就可能会使得电路在复位后无法正确工作. 因此, 后者的情况是需要避免的. 之前我们一直使用verilator进行仿真, 它是一个二值仿真工具, 所有信号的值只包含01, 因此所有触发器在复位时都有一个具体的值, 不能很好地检查上述问题.

而在支持四值仿真的RTL仿真器中, 可以通过不定态X来表示一个无复位端触发器的初值. X信号参与门电路运算时, 结果也可能会是X信号, 因此它可以在电路中进行传播. 如果存在一个需要复位但未进行复位的触发器, X信号就会在电路中大范围传播, 使得其他触发器的值也为X, 无法精确表示一个信号的值, 最终使得电路收敛到一个与预期的正确状态不同的另一个状态, 处理器上的程序将无法得到预期的运行结果. 相反, 如果不存在这样的触发器, X信号在电路中的传播就会被控制信号遏制, 并随着有效数据的更新, X信号将会逐渐减少, 最终使得电路收敛到预期的正确状态.

iverilog是一个支持四值仿真的RTL仿真器, 我们可以使用它来检查是否存在需要复位但还未进行复位的触发器. 在使用iverilog之前, 你需要先安装它:

apt-get install iverilog

安装后, 可以先根据相关文档在新窗口中打开运行一个简单的示例.

我们的目标是检查NPC能否在iverilog上运行程序, 而且ysyxSoC不参与流片, 因此我们只需要单独仿真NPC, 在其上运行riscv32e-npc的AM程序即可. 不过由于iverilog暂不支持DPI-C, 也不支持通过C++代码来驱动仿真, 因此我们需要对仿真环境进行一些改动:

  • 去掉DiffTest和存储器相关的DPI-C调用.
  • 在AXI接口的SRAM中重新用Verilog实现存储阵列的读写功能, 由于这个SRAM仅在仿真过程中使用, 因此你不必考虑相关代码是否可综合, 只需要采用行为建模的方式来实现即可.
  • 通过$readmemh()对上述存储器进行初始化, 将指定的存储器镜像文件读出存储器中. $readmemh()所支持的文件格式可以通过objcopy命令和参数-O verilog生成, 不过$readmemh()认为存储器的地址从0开始, 这与ELF文件的链接地址不同, 因此你还需要用到参数--adjust-vma, 具体用法可参考man objcopy.
  • 编写一个简单的仿真顶层, 在其中实例化时钟和复位信号, 并驱动NPC的仿真.
  • 由于无法使用DiffTest, 如果仿真结果与预期不符, 你需要通过波形来诊断问题. 生成波形文件的具体方式可以参考上述iverilog文档.

完成上述修改后, 就可以尝试用iverilog进行编译了. 不过iverilog支持的语法比verilator要少, 我们建议你通过-g2012选项来采用较新的语言标准, 但你还是可能会遇到verilator编译通过, 但iverilog编译不通过的情况, 你需要根据报错信息修改你的代码. 特别地, 如果你使用Chisel, 你还需要给firtool添加选项 --lowering-options=disallowLocalVariables,disallowPackedArrays, 来屏蔽一些iverilog不支持的语言特性. 此外, 你可能还会遇到以下报错信息, 该信息可忽略:

sorry: constant selects in always_* processes are not currently supported (all bits will be included).

通过四值仿真检查X信号传播问题

根据上述内容, 在iverilog中仿真NPC并运行microbench和RT-Thread等程序. 如果程序运行失败, 你需要修改相应的RTL代码来解决X信号的传播问题.

网表仿真

由于Verilog本质上是一门事件驱动的建模语言, 可综合的Verilog只是其中一个子集, 因此Verilog仿真器可能会接受一些不可综合的代码. 这并不是Verilog仿真器的bug, 它只是按照Verilog标准手册在新窗口中打开中定义的行为来进行仿真. 显然, 在RTL设计中, 我们并不希望编写出这样的代码.

为了检查项目中是否包含不可综合代码, 一个方案是对综合后的电路进行仿真. 如果项目中包含不可综合代码, 综合后的电路可能与综合前不一致, 从而帮助我们发现相关错误. 因此, 我们可以考虑对yosys的综合结果进行仿真.

综合的步骤是将RTL代码转换成逻辑等价的标准单元, 描述这些标准单元之间的连接关系的文件称为网表. 在网表中, 标准单元是以模块的方式进行实例化的, 因此, 要对网表进行仿真, 就需要提供这些标准单元的模块级实现. 事实上, yosys-sta项目中已经提供了相应的实现, 具体位于yosys-sta/nangate45/sim/cells.v.

网表仿真

用综合后的网表文件和上述标准单元的模块级实现进行仿真, 尝试NPC上运行microbench和RT-Thread等程序. 由于AXI接口以外的存储器不属于流片的范畴, 因此你可以继续采用四值仿真时使用的存储器代码. 此处建议继续采用iverilog进行仿真.

Chisel福利(4)

Chisel生成的Verilog代码都是可综合的, 如果你使用Chisel开发, 网表仿真大概率会直接成功, 但我们仍然建议你不要因此跳过网表仿真.

申请代码调试考核

再次检查你的代码

如果你在上述过程中修改了代码, 请重新进行上述检查, 避免在修改代码的途中再次引入了不符合要求的代码.

考核申请步骤建设中

最近更新时间:
贡献者: Zihao Yu