4. 激励生成器:随机指令生成、约束随机化、定向测试用例
说到激励生成,很多刚入行的朋友会问:不就是往DUT里灌数据吗?其实没那么简单。我做了这么多年验证,最深的体会是——激励的质量,直接决定了你能发现多少bug。
FPU验证尤其特殊。浮点运算的组合空间太大了,光一个加法指令,就有NaN、无穷大、规格化数、非规格化数……各种排列组合。你想想看,如果全靠手写定向用例,写到猴年马月也写不完。所以,随机化是必须的。
4.1 随机指令生成——让CPU自己“编程序”
我的做法很简单:写一个指令序列生成器,让它随机拼接指令。但这里的“随机”不是瞎随机,而是有策略的随机。
举个例子,我们验证FPU,通常需要生成这样的指令流:
// 伪代码示意
class riscv_fpu_instr_sequence;
rand fpu_op_t op; // 操作类型:FADD, FSUB, FMUL, FDIV...
rand reg_t rs1, rs2, rd; // 源寄存器和目标寄存器
rand frm_t rounding; // 舍入模式
rand bit [63:0] operand1, operand2; // 操作数
constraint valid_op {
op inside {FADD, FSUB, FMUL, FDIV, FSQRT, FMADD, FMSUB};
}
constraint valid_regs {
rs1 != rd; // 避免写后读冲突(简化版)
rs2 != rd;
}
endclass
嗯,这里要注意:寄存器约束不能太死。我见过有人把寄存器全约束成不同值,结果覆盖率死活上不去。为什么?因为真实程序里,很多指令会重复使用同一个寄存器。你想想看,fld f1, 0(sp) 然后 fadd f2, f1, f3,这种场景太常见了。
4.2 约束随机化——给随机戴上“镣铐”
纯粹的随机,验证效率其实很低。我刚开始做FPU验证时,跑了一晚上随机,结果发现90%的指令都是普通加法,特殊值一个没碰到。这就是典型的“随机陷阱”。
所以,我们需要约束随机化。说白了,就是告诉随机器:哪些场景你得多生成,哪些场景你偶尔生成就行。
我常用的约束策略:
- 权重分配:给特殊操作数更高的权重。比如NaN占5%,无穷大占5%,零占10%,规格化数占80%。
- 边界值偏置:浮点数的边界值(最大正数、最小正数、最大负数等)必须高频出现。
- 舍入模式轮换:RNE、RTZ、RDN、RUP、RMM五种模式,按比例随机切换。
看一个实际的约束例子:
class fpu_operand_constraint;
rand bit [63:0] op;
// 让特殊值出现的概率更高
constraint special_weight {
op dist {
{64'h7FF0000000000000} :/ 5, // +inf
{64'hFFF0000000000000} :/ 5, // -inf
{64'h7FF8000000000000} :/ 5, // NaN
{64'h0000000000000000} :/ 10, // +0
[64'h0010000000000000 : 64'h7FEFFFFFFFFFFFFF] :/ 70, // 规格化数
[64'h0000000000000001 : 64'h000FFFFFFFFFFFFF] :/ 5 // 非规格化数
};
}
endclass
我曾经踩过一个坑:约束写得太死,导致随机器经常解不出解,仿真跑着跑着就卡死了。后来我学乖了,给每个约束加一个 soft 关键字,让约束有回退余地。
4.3 定向测试用例——该手写时就手写
随机化再强,也替代不了定向用例。为什么?因为有些bug,随机就是撞不到。
我举个例子:FMADD指令(融合乘加)的中间精度问题。IEEE 754规定,FMADD的中间乘积要保持无限精度,最后才舍入。这个逻辑,随机很难覆盖到所有边界情况。这时候,就得手写定向用例。
我一般把定向用例分成几类:
| 类别 | 说明 | 示例 |
|---|---|---|
| 边界值 | 浮点格式的极限值 | 最大正数 + 最小正数 |
| 特殊值 | NaN、无穷大、零的组合 | NaN × 0 + 无穷大 |
| 舍入边界 | 刚好卡在舍入边界的值 | 2.5, 2.5000001 等 |
| 异常触发 | 上溢、下溢、不精确 | 两个最大数相乘 |
| 数据依赖 | 前后指令的数据关联 | 先写后读、先读后写 |
写定向用例时,我习惯用宏来封装:
`define TEST_CASE(name, body) \
task name(); \
$display("Running test: %s", `"name`"); \
body \
endtask
`TEST_CASE(test_fadd_nan,
send_instr(FADD, f1, f2, f3, 64'h7FF8000000000000, 64'h3FF0000000000000);
check_result(f1, 64'h7FF8000000000000);
)
这样写的好处是,每个用例都是一个独立的任务,调试起来特别方便。
4.4 随机与定向的黄金比例
很多人问我:随机和定向,到底各占多少?我的答案是:没有固定比例,但有个原则——定向用例覆盖功能点,随机用例覆盖组合空间。
我一般这样安排:
- 项目前期:定向用例占70%,快速验证基本功能
- 项目中期:随机用例占80%,疯狂压测
- 项目后期:回归测试时,定向+随机混合跑,确保覆盖率达标
另外,我强烈建议把随机种子保存下来。有一次,我发现一个bug只在种子为 0xDEADBEEF 时复现。如果没有保存种子,这种bug根本没法定位。
4.5 知识体系总览
下面这张图,是我对激励生成器设计的理解。你可以把它当作一个检查清单:
这张图想表达的是:随机指令生成、约束随机化、定向测试用例,三者是互补关系。没有谁比谁更重要,只有谁更适合当前场景。
好了,关于激励生成器,我就讲这么多。记住一句话:好的激励,是验证成功的一半。剩下的,就看你的DUT争不争气了。