Skip to content

多周期 RV32I 处理器的 constrained-random 验证

多周期 RV32I CPU 的验证:constrained-random 指令生成器、functional coverage,以及经 RVFI 逐条检查的定向汇编测试。

Verif SystemVerilog CPU RISC-V
Contents

概述

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

Testbench 结构

Testbench 总览:定向汇编和随机指令两种激励、多周期 CPU、RVFI 上的三个检查器,以及修复的 4 类 bug

  • 两种激励,二选一:
    • 定向测试:汇编程序编译成 hex 和 ELF。memory_model 加载 hex 给 CPU 执行;Spike(RISC-V 官方的指令集模拟器)通过 DPI-C 运行同一个 ELF,CPU 每 commit 一条指令就比对一次。
    • 随机测试:random_tb 对每个读请求都回一条新生成的随机指令,写请求直接忽略。随机指令流没有对应的程序,Spike 不参与,由 riscv-formal(开源的 RISC-V 形式验证框架,这里只用它的 monitor)按 ISA 语义检查每条 commit。
  • 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_opcodes9每类 opcode
all_funct72funct7 的两个合法值
all_funct38funct3 的 8 个值
all_regs_rs1、all_regs_rs2各 32每个寄存器都当过 rs1、rs2
funct3_cross31每条有 funct3 的指令
funct7_cross13每条有 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 地址漏加 rs1load 地址只用了立即数基址寄存器不为 0 的 load

修复后 autograder 的 CPU 测试全部通过。

技术栈

SystemVerilog · Constrained-random · Covergroup · Spike · RVFI · riscv-formal · VCS

Discussion

Loading comments…

Checking sign-in…