受约束随机验证 CRV:rand、constraint、solve before 与 seed 复现
更新时间:2026-09-14。本文是「集成电路大类 · 芯片设计 · 数字验证」实操序列第二篇。上一篇 SystemVerilog Testbench 实战 里用
$urandom产生了随机数据,但「随机数据」不等于「随机验证」。这一篇讲工业界主力方法——受约束随机验证(CRV,Constrained Random Verification),它是 覆盖率驱动验证 CDV 的前提。
本文要回答的问题
- 纯随机为什么不行,「受约束」约束的到底是什么?
rand/randc、constraint怎么把「一笔合法事务」描述给求解器?- 怎么既随机出合法场景,又定向注入非法/边界场景?
- 随机用例跑挂了,怎么稳定复现?
一、为什么不是定向、也不是纯随机
一个 32 位加法器输入空间是 2⁶⁴;一个带握手、可变长、多字段的协议帧,合法组合更是天文数字。两条极端路线都走不通:
- 纯定向:工程师能想到的场景有限,验证的是「我怀疑会错的地方」,真正的 bug 常藏在你没想到的字段组合里。
- 纯随机:对协议一无所知地乱抛,绝大多数激励非法、被 DUT 在第一关拒绝,永远进不了深层逻辑。
CRV 的思路是:用约束把随机量限定在「协议合法空间」内,让求解器在合法空间里自由组合,撞出人脑想不到的合法边角组合;再用少量定向/非法约束专门打边界和错误处理。 工程师的工作量从「枚举每个场景」变成「描述清楚合法空间 + 写好检查器与覆盖率」。
二、把事务写成类:rand / randc
随机的对象是事务(transaction),用类封装字段,可随机字段加 rand:
class eth_frame;
rand bit [47:0] da, sa;
rand bit [15:0] eth_type;
rand byte payload[];
rand bit [31:0] fcs;
rand bit bad_fcs; // 是否故意造错
function void pre_randomize(); endfunction
endclassrand:每次随机化按约束/分布独立取值,可重复。randc:cyclic 随机,在所有可能值被取遍之前不重复(适合想轮询所有操作码、所有端口号的场景)。- 不加
rand的字段(如配置常量)不参与求解。
三、constraint:描述合法空间
约束块是一组必须同时成立的关系式,仿真器内置约束求解器(constraint solver)求出一组合法解:
class eth_frame;
// ...上面的字段
constraint c_len {
payload.size() inside {[46 : 1500]}; // 以太网最小/最大载荷
}
constraint c_type {
eth_type inside {16'h0800, 16'h0806, 16'h86DD}; // IPv4/ARP/IPv6
}
constraint c_badfcs {
bad_fcs dist {0 := 99, 1 := 1}; // 99% 好帧,1% 故意坏帧
}
constraint c_mac {
da[47:40] inside {8'h01, 8'h??}; // 可放组播/单播分布
}
endclass常用约束手段:
| 写法 | 含义 |
|---|---|
a inside {[lo:hi]} | 区间/集合成员 |
a dist {v:=w} / :/ | 加权分布(:= 每值权重,:/ 组内平分) |
a -> b(蕴含) | a 成立时 b 必须成立 |
if (cond) {...} | 条件约束 |
a == b + 1 | 双向:求解器同时反推 a、b |
solve a before b | 先选 a 再定 b,控制概率,见下 |
约束是双向声明式的:你写 payload.size() inside {[46:1500]},求解器自己决定怎么凑出满足的值,不必关心赋值顺序。这和普通过程代码很不一样。
四、合法/非法分层与 scenario 叠加
直接把「合法」约束写死,就永远测不到错误处理。工业做法是约束分层 + 继承开关:
- 基类约束只保证「能构造成帧」;
- 用子类或约束开关打开非法场景:超短帧、超长帧、bad FCS、未对齐、反压期间发数;
- 用「scenario 序列」约束事务间关系:连续发满后立刻读、背靠背、间隔随机。
class short_frame extends eth_frame;
constraint c_short { payload.size() inside {[1:20]}; bad_fcs == 0; }
endclass
// 运行时关闭某条约束,造特定场景
frame.c_len.constraint_mode(0);
frame.randomize() with { payload.size() == 4; };randomize() with {...} 内联约束用于「这一次」临时加条件;constraint_mode(0) 关约束、rand_mode(0) 把某字段冻结为固定值。定向测试由此成为随机平台上的一个特例,而不是另一套代码。
五、solve...before:不是逻辑需要,是概率需要
约束只限定合法解时,求解器给各解的概率可能不符合验证意图。典型例子:
rand bit a;
rand bit [3:0] b;
constraint c { (a==1) -> (b==5); }逻辑上完全合法,但默认求解可能让 a==1 的概率很低(因为 a=1 时 b 被钉死成一个值,a=0 时 b 有 16 种)。加 solve a before b; 表示先按 1/2 选 a,再在 a 决定的空间里选 b,于是 a=1 概率恢复到 50%。solve...before 不改变合法性,只改变各合法解的分布——这是「某类场景总随机不出来」时的常用修正手段,但滥用会让分布难以推理,应配合覆盖率看效果。
六、随机化失败与结果检查
obj.randomize()有返回值:约束互相矛盾、无解时返回 0(randomization failure)。必须检查返回值,否则字段可能保持旧值,你以为随机了其实没有:
if (!frame.randomize()) `uvm_error("RND", "randomize failed")- 先随机、再做「派生字段」后处理用
post_randomize():例如根据载荷算 FCS,或当bad_fcs时故意算错:
function void post_randomize();
fcs = bad_fcs ? ~calc_crc(da,sa,eth_type,payload)
: calc_crc(da,sa,eth_type,payload);
endfunction七、seed 复现:随机用例必须可重生
「随机测试」最怕挂了却复现不了。SystemVerilog 的随机是伪随机:整个仿真由一个初始 seed 决定,相同 seed + 相同代码 + 相同编译选项 → 逐笔相同序列。因此:
- 每次回归随机分配 seed 并打印/记录(命令行
+ntb_random_seed=或工具选项,仿真器语法略有差异); - 用例失败日志里必须带 seed;
- 复现时固定该 seed 单跑;
- 代码改动后旧 seed 可能不再产生同样序列(随机化顺序变了),属正常,关键是失败当下的环境可追溯。
回归策略通常是:大量随机 seed 广撒网找 bug(每天几百上千个),找到后用固定 seed 转成「可重复的定向回归」永久保留,防止 bug 回流。
八、CRV 检查清单
- [ ] 事务字段语义清晰,
rand/非 rand、二态/四态选择正确; - [ ] 基类约束只描述协议合法性,错误场景用子类/开关注入;
- [ ] 关键分布用
dist显式加权,而不是赌默认概率; - [ ] 每次
randomize()都检查返回值; - [ ] 派生物(CRC/长度/ECC)在
post_randomize计算,坏值可控; - [ ] 随机到的场景由功能覆盖率确认,而不是凭感觉(见下一篇 SVA 与覆盖率);
- [ ] seed 进日志、失败可固定复现、可转永久回归。
相关与延伸
随机激励如何被实时检查协议见 SVA 断言实战,随机「有没有测到」由 代码覆盖率与功能覆盖率 回答,三者合称覆盖率驱动验证 CDV。承载这些随机事务的工业化框架见 UVM 组件与 phase、UVM sequence/driver/scoreboard。
总结
受约束随机验证的本质是「声明合法空间、求解器自由组合」:用类把事务字段标成 rand,用 constraint(inside/dist/蕴含/双向关系)界定协议合法解,用子类继承、with 内联、constraint_mode 分层注入合法与非法/scenario 场景,用 solve...before 只调概率不改合法性,并检查每次 randomize() 返回值、在 post_randomize 里生成 CRC 等派生字段;由于随机是由 seed 决定的伪随机序列,只要回归记录 seed、失败用固定 seed 复现并沉淀为永久定向用例,就能在「广撒网找新 bug」和「防回流」之间闭环——工程师的产出因此从枚举场景升级为描述约束、检查器和覆盖率。