概述
多周期 RV32I CPU 的功能验证。激励有随机测试(random test)和定向测试(directed test)两种,CPU 每 commit 一条指令都要经过检查;随机指令由 constraint 生成,覆盖率由 covergroup 收集。
Testbench 结构

- 两种激励,二选一:
- 定向测试:汇编程序编译成 hex 和 ELF。
memory_model加载 hex 给 CPU 执行;Spike(RISC-V 官方的指令集模拟器)通过 DPI-C 运行同一个 ELF,CPU 每 commit 一条指令就比对一次。 - 随机测试:
random_tb对每个读请求都回一条新生成的随机指令,写请求直接忽略。随机指令流没有对应的程序,Spike 不参与,由 riscv-formal(开源的 RISC-V 形式验证框架,这里只用它的 monitor)按 ISA 语义检查每条 commit。
- 定向测试:汇编程序编译成 hex 和 ELF。
- RVFI(RISC-V Formal Interface):CPU 每 commit 一条指令,就经这组信号报出 PC、寄存器读写和访存内容,两种比对都从这里取数据。
- 报错即停:比对不一致、RVFI 信号出现 X,或访存协议出错(同时读写、地址为 X、地址不对齐,由
memory_model和random_tb各自检查),仿真立即停止。 - 本项目编写:指令生成器的 constraint、covergroup 和四组定向测试(图中带星号的部分),并把随机测试从 5,000 次访存加长到 20,000 次。
Constraint 设计
RandInst 是一个 SystemVerilog class,每调用一次 randomize() 生成一条合法的 RV32I 指令,覆盖除 FENCE*、ECALL、EBREAK、CSRR* 以外的全部 RV32I 指令。
- 先抽类型:
instr_type是 9 位 one-hot,对应 9 类 opcode。solve instr_type before instr让求解器先抽类型、再填字段,各类的比例由dist决定。权重按每类要覆盖的 bin 数分配:OP 38%、OP-IMM 24%、BRANCH 13%、LOAD 11%、STORE 6%,JALR、JAL、AUIPC、LUI 各 2%。 - 按类型写字段:每类 opcode 一个 implication 约束块(
->),块里先固定 opcode,再限定这一类合法的 funct3 和 funct7:- OP:funct3 是 ADD/SUB 或 SRL/SRA 时,funct7 可取 0000000 或 0100000,其余只能取 0000000。
- OP-IMM:SLLI 的立即数高 7 位只能是 0000000,SRLI/SRAI 是 0000000 或 0100000,其余指令的立即数不限。
- LOAD、STORE、BRANCH:funct3 用
inside列出合法值;JALR 的 funct3 固定为 000;JAL、AUIPC、LUI 只固定 opcode。
- funct3 先解:默认情况下,求解器在所有合法解里均匀抽取。按上面 OP-IMM 的约束,非移位指令的立即数高 7 位有 128 种取法,SLLI、SRLI、SRAI 只有一两种,移位指令很少被抽到。为此加了一个镜像变量
funct3(OP 和 OP-IMM 的 funct3 字段都约束成等于它),用solve funct3 before instr先把 8 个 funct3 等概率抽定。 - 访存对齐:CPU 不支持非对齐访存。有效地址是 rs1 的值加立即数,而 rs1 的值要到运行时才知道,约束管不到。因此 lh、lhu、lw、sh、sw 的 rs1 固定为 x0,地址就等于立即数,对齐由约束保证;lb、lbu、sb 天然对齐,基址保持随机。
Functional coverage
instr_cg 是 RandInst 里的 covergroup,每次 randomize() 之后在 post_randomize() 里采样一次,共 7 项:
| 覆盖项 | bin 数 | 检查什么 |
|---|---|---|
| all_opcodes | 9 | 每类 opcode |
| all_funct7 | 2 | funct7 的两个合法值 |
| all_funct3 | 8 | funct3 的 8 个值 |
| all_regs_rs1、all_regs_rs2 | 各 32 | 每个寄存器都当过 rs1、rs2 |
| funct3_cross | 31 | 每条有 funct3 的指令 |
| funct7_cross | 13 | 每条有 funct7 的指令 |
- cross 只留合法组合:
ignore_bins去掉不存在的组合,例如 JAL、LUI、AUIPC 没有 funct3;illegal_bins标出不合法的编码,生成器一旦产生这种指令,采样时直接报错。 - 收集与查看:仿真时 VCS 把覆盖率写进 coverage 数据库,再用 URG 生成 HTML 报告,按 bin 查哪些组合还没打到;
rand_tb跑 500 次randomize()后,用get_coverage()打印每一项的覆盖率。 - 结果:autograder 用参考 covergroup 检查生成器(要求 100%,且不能出现非法指令),再用一组覆盖不全、一组覆盖完整的激励检查 covergroup 本身,两项都通过。
定向测试
四组汇编测试,用例按边界条件设计:
- 算术:移位量取 0、3、20 和 31。只测 16 以下的移位量时,少用一位移位量的实现结果完全一样。同一对操作数(10 和 −1)分别做 slt 和 sltu,有符号和无符号的结果相反。
- 分支:六条分支各测该跳、不该跳和两数相等。分支后面垫一条 nop:不垫的话,跳转目标就是下一条,跳和不跳的 PC 一样,比对不出来。最后一组向后跳,测负偏移。
- 跳转:jal 测向前、向后和偏移 4096 以上的远跳;jalr 测结果最低位清零和负偏移。
- 访存:lb、lbu 测四个字节偏移上的符号扩展和零扩展,lh、lhu、lw 测对齐的偏移;每次 sb、sh 之后整字读回,检查其余字节没被改;最后测负偏移。
修复的 bug
| bug | 错在哪 | 暴露它的用例 |
|---|---|---|
| 移位量只取低 4 位 | 移位指令只用了移位量的低 4 位 | 移位 20、31 位 |
| bge、bgeu 漏了相等 | 比较用的是「大于」,应为「大于等于」 | 两数相等的分支 |
| wmask 默认全 1 | 不写内存的状态也把写掩码拉满 | 任何程序:第一次取指就同时读写 |
| load 地址漏加 rs1 | load 地址只用了立即数 | 基址寄存器不为 0 的 load |
修复后 autograder 的 CPU 测试全部通过。
技术栈
SystemVerilog · Constrained-random · Covergroup · Spike · RVFI · riscv-formal · VCS
Discussion
Loading comments…
Checking sign-in…