第 12 章 · 共 16 章
随机化与约束:rand、randc 与 constraint 块
学会用 rand/randc 声明可随机化的属性,用 constraint 块描述规则,调用 randomize() 并检查结果,用 $urandom/$urandom_range 和 std::randomize() 在类之外做随机化,并用 rand_mode 与 constraint_mode 在运行时控制随机化范围——这是验证代码生成激励的核心手段。
第 1 章说过,Verilog 几乎没有为验证准备的语言特性,其中就包括内建的随机激励生成;第 10、11 章在此基础上搭好了类和对象的基础。这一章正式讲约束随机化(constrained randomization)——SystemVerilog 验证代码生成激励最核心的手段:不是手写一堆具体的测试数据,而是声明"数据应该满足什么规则",让工具自动生成大量满足规则、但每次都不一样的数据。
rand 与 randc:声明可随机化的属性
class packet;
rand bit [7:0] addr;
randc bit [1:0] priority_level;
bit [7:0] data; // 没有 rand,不会被随机化
function void print();
$display("addr=%0h priority=%0d data=%0h", addr, priority_level, data);
endfunction
endclass
initial begin
packet p = new();
repeat (3) begin
void'(p.randomize());
p.print();
end
endrand:声明这个属性参与随机化,每次调用randomize()都会得到(可能)不同的值。randc(random-cyclic):也会随机取值,但保证在取值范围内的所有值被轮完一遍之前不会重复——适合需要"确保每个可能值都至少覆盖一次"的小范围场景(比如上面 2 位宽的priority_level,只有 4 个可能值)。randc通常只适合位宽很小的场景,位宽大了之后需要维护的"已用值集合"会迅速变得不现实。- 没有
rand/randc修饰的属性(比如data)不会被randomize()触碰。
每个类都自带一个 randomize() 方法(就像自带 new() 一样),调用它会给所有 rand/randc 属性重新赋随机值。它有返回值:成功求解返回 1,如果约束互相矛盾、无法同时满足,返回 0——上面例子里用 void'(...) 显式丢弃了返回值(第 9 章讲过这个写法),但在真实的验证代码里,通常应该检查这个返回值,随机化失败时报错,而不是静默地继续用旧值跑下去:
packet p = new();
if (!p.randomize()) begin
$error("packet randomization failed - check constraint conflicts");
endconstraint 块:给随机化加上规则
不加任何约束时,rand 属性会在它类型的整个取值范围里均匀随机取值。真实场景里往往需要限制范围、排除非法值,这就是 constraint 块的作用:
class packet;
rand bit [7:0] addr;
rand bit [7:0] data;
constraint addr_range_c {
addr inside {[8'h10 : 8'h1F]}; // addr 只能落在 0x10~0x1F 之间
}
constraint data_nonzero_c {
data != 8'h00; // data 不能是 0
}
endclass有一点和普通过程代码的思维方式不同,需要转变一下:constraint 块里的语句不是"按顺序执行的代码",而是描述"最终结果必须满足什么条件"的声明式规则,交给约束求解器(constraint solver)去找一组同时满足所有规则的值。两个 constraint 块(addr_range_c、data_nonzero_c)之间没有先后顺序关系,求解器会同时满足它们,而不是先满足一个再满足另一个。
更丰富的约束写法
约束表达式可以描述更复杂的规则。加权分布用 dist 让某些取值出现的概率更高(比如更频繁地测试边界值):
constraint data_dist_c {
data dist { 8'h00 := 1, [8'h01:8'hFE] :/ 8, 8'hFF := 1 };
}上面的写法大致是"取到 0x00 或 0xFF 各占 1 份权重,剩下 254 个值总共占 8 份权重"——边界值 0x00/0xFF 因此会比其他任意一个值更容易被抽到,这在验证里很有用,因为边界条件往往是 bug 最容易出现的地方。
条件约束用蕴含操作符 -> 表达"如果 A 成立,则 B 也必须成立":
typedef enum { READ, WRITE } op_e;
class packet;
rand op_e op;
rand bit [7:0] addr;
constraint addr_valid_c {
(op == READ) -> (addr < 8'h80); // 只有 READ 操作时才限制 addr 范围
}
endclass这条约束只在 op == READ 时才会限制 addr;如果随机到 op == WRITE,addr 不受这条约束影响,可以取整个位宽范围内的任意值。
一次性额外约束:randomize() with {...}
有时候你不想为了一次性的需求去修改类里声明的约束,可以在调用 randomize() 时用 with 临时叠加约束,只对这一次调用生效:
packet p = new();
void'(p.randomize() with {
addr == 8'h20; // 只在这次调用里,强制 addr 等于 0x20
});with {...} 里的约束会和类里声明的约束一起求解(而不是替换掉它们)——如果这次额外加的约束和类里原有的约束冲突,randomize() 会求解失败,返回 0。
$urandom 与 $urandom_range:不依赖类的随机化
前面所有内容都需要一个带 rand/randc 属性的 class,再调用 randomize()。生成一个结构化的对象(比如一个 packet、一个 transaction)时这是正确的工具,但如果只是想要一个一次性的随机数——比如 initial 块里的一个随机循环次数、一个随机数组下标——为此专门写一个类就太重了。SystemVerilog 提供了两个系统函数,可以直接作用于普通变量,完全不需要类:
bit [31:0] raw;
int delay;
int idx;
raw = $urandom; // 均匀分布在整个 32 位取值范围内
delay = $urandom_range(20, 5); // 均匀分布在 [5, 20] 之间的整数
idx = $urandom_range(7); // 均匀分布在 [0, 7] 之间的整数(minval 默认为 0)$urandom返回一个均匀分布的 32 位随机值(当作有符号还是无符号,取决于赋值给谁)。$urandom_range(maxval, minval = 0)返回一个限制在[minval, maxval]范围内的值——第一个参数是上界,第二个参数(可选,默认为0)是下界。
这两者都不经过约束求解器——没有 constraint 块,没有 dist,没有 inside,就是一个均匀(或限定范围的均匀)随机值。这正是它们的定位:一次性的简单随机值用 $urandom/$urandom_range 又快又轻;而值需要遵守真正的规则、或者属于一个更大的随机化对象时,才需要 rand/randc + constraint 块。
std::randomize():随机化类之外的普通变量
obj.randomize() 只会触碰对象自己声明的 rand/randc 属性——和一个不属于任何类的普通局部变量毫无关系。对于这种情况,SystemVerilog 提供了一个全局版本的 randomize,可以直接作用于普通变量,还能像之前一样用 with {...} 临时叠加约束:
int unsigned x, y;
if (!std::randomize(x, y) with { x inside {[0:9]}; y > x; }) begin
$error("randomization failed");
end这会同时给 x 和 y赋随机值,满足 x inside {[0:9]} 和 y > x——和 constraint 块一样是声明式、交给求解器处理的规则,只是作用对象换成了不属于任何类的普通变量。
std:: 前缀是必须的:在类的方法内部,直接写 randomize(...) 永远意味着"调用当前对象自己的 randomize() 方法"。std::randomize(...) 明确表示"使用全局的系统级版本",这样才能在方法内部随机化局部变量(或者类里没有 rand 修饰的成员),而不会被误解为在调用对象自己的 randomize()。
打开/关闭随机化:rand_mode 与 constraint_mode
有时候某个测试需要临时停止随机化某个属性,或者关掉某一条具体规则,但又不想改动类本身。两个内置方法可以做到这一点,对任何带 rand/randc 属性或命名 constraint 块的对象都适用:
packet p = new();
// 只冻结一个属性;它会保持当前值不变
p.addr.rand_mode(0);
void'(p.randomize()); // addr 不受影响;data 仍然正常随机化
p.addr.rand_mode(1); // 重新打开 addr 的随机化
// 冻结整个对象
p.rand_mode(0);
void'(p.randomize()); // 空操作:addr 和 data 都不会变
p.rand_mode(1); // 重新打开整个对象的随机化
// 关闭某一个命名的 constraint 块,其他约束照常生效
p.addr_range_c.constraint_mode(0);
void'(p.randomize()); // addr 现在可以取完整 8 位范围内的任意值;
// data_nonzero_c 仍然生效
p.addr_range_c.constraint_mode(1); // 重新打开这条约束rand_mode(0/1):可以对整个对象调用(p.rand_mode(0)),也可以对单个属性调用(p.addr.rand_mode(0)),控制打开/关闭随机化。被关闭的属性会保持它上一次的值。constraint_mode(0/1):作用于某个命名的constraint块(p.addr_range_c.constraint_mode(0)),只关闭/打开这一条规则,不影响类里的其他约束。
两者默认都是打开(1)状态,经常搭配使用——比如关掉某一条约束块,让一个测试故意生成一个超出正常范围的值,同时其他规则照常生效。
pre_randomize() 与 post_randomize():挂载到 randomize() 调用上
每个类都隐式自带两个回调方法 pre_randomize() 和 post_randomize(),默认都是空的——randomize() 会在求解约束之前自动调用 pre_randomize(),求解完成之后自动调用 post_randomize()。最常见的用法是重写 post_randomize(),计算一个依赖刚刚随机出来的值的字段:
class packet;
rand bit [7:0] addr;
rand bit [7:0] data;
bit [7:0] checksum; // 不是 rand —— 由 addr/data 派生,而不是直接随机化
constraint addr_range_c { addr inside {[8'h10 : 8'h1F]}; }
constraint data_nonzero_c { data != 8'h00; }
function void post_randomize();
checksum = addr ^ data; // 随机化完成后,立刻计算一个依赖字段
$display("randomized: addr=%0h data=%0h checksum=%0h", addr, data, checksum);
endfunction
endclasschecksum 不能是 rand——它的值依赖 addr 和 data 随机化之后的结果,而不是自己独立随机出来的。post_randomize() 正是计算它的合适位置:等它被调用时,randomize() 已经给 addr 和 data 赋好了新值。pre_randomize() 是对称的另一个钩子,在求解器开始工作之前调用,偶尔用来在新一轮随机化之前重置某些状态。
小结
rand声明可随机化的属性;randc额外保证"轮完一遍所有取值才会重复",适合小范围的场景。- 每个类自带
randomize()方法,调用后重新随机化所有rand/randc属性;返回1表示成功,返回0表示约束无法同时满足,实际代码里应该检查这个返回值。 constraint块是声明式规则,交给约束求解器统一求解,不是按顺序执行的过程代码。dist可以给不同取值分配不同权重;->(蕴含)可以表达条件约束。randomize() with {...}可以为单次调用叠加额外约束,不影响类里原有的约束定义。$urandom/$urandom_range(maxval, minval = 0)生成一个普通的均匀随机值,不涉及类或约束求解器,适合一次性的简单需求。std::randomize(x, y)可以随机化类之外的普通变量,语法上同样支持with {...}约束;std::前缀用来和对象自己的randomize()方法区分开。rand_mode(0/1)(作用于整个对象或单个属性)和constraint_mode(0/1)(作用于某个命名约束块)可以在运行时打开/关闭随机化或某条具体规则,不需要改动类本身。pre_randomize()/post_randomize()是randomize()自动在求解前后调用的回调方法,常用来在随机化之后计算依赖字段或打印调试信息。
rand 和 randc 修饰的属性,在随机化行为上最主要的区别是什么?
一个类里同时声明了两个 constraint 块,分别约束 addr 和 data。关于它们的求解方式,以下哪项正确?
调用哪个方法,会让一个对象根据它声明的 rand/randc 属性和 constraint 规则生成新的随机取值?(英文小写,不带括号)
$urandom_range(20, 5) 会返回什么?
在类的方法内部,为什么要写 std::randomize(x, y) 而不是直接写 randomize(x, y) 来随机化两个局部变量?
先调用 p.addr.rand_mode(0),再调用 p.randomize(),p.addr 会发生什么?
randomize() 完成赋值之后,会自动调用哪个回调方法?常用来计算依赖字段或打印调试信息。(英文小写,不带括号)