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,这种场景太常见了。

我的经验:随机生成时,让寄存器有20%-30%的概率重复使用。这样既能覆盖数据依赖,又能保持随机性。

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 关键字,让约束有回退余地。

注意:SystemVerilog的约束求解器不是万能的。约束越复杂,求解时间越长。我建议把约束拆成多个小约束块,而不是写一个巨型约束。

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根本没法定位。

核心思想:随机化不是目的,覆盖bug才是。好的激励生成器,应该像一位经验丰富的测试工程师——知道该测什么,也知道怎么测。

4.5 知识体系总览

下面这张图,是我对激励生成器设计的理解。你可以把它当作一个检查清单:

激励生成器设计 随机指令生成 约束随机化 定向测试用例 指令类型随机(FADD/FMUL/...) 操作数随机(寄存器/立即数) 舍入模式随机 权重分配(dist约束) 边界值偏置 soft约束回退 边界值测试 特殊值组合 异常触发场景 高质量激励 → 高覆盖率 三者缺一不可,互相补充

这张图想表达的是:随机指令生成、约束随机化、定向测试用例,三者是互补关系。没有谁比谁更重要,只有谁更适合当前场景。

好了,关于激励生成器,我就讲这么多。记住一句话:好的激励,是验证成功的一半。剩下的,就看你的DUT争不争气了。

专注资料整理