SystemVerilog 基础

第 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 验证代码生成激励最核心的手段:不是手写一堆具体的测试数据,而是声明"数据应该满足什么规则",让工具自动生成大量满足规则、但每次都不一样的数据。

randrandc:声明可随机化的属性

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
end
  • rand:声明这个属性参与随机化,每次调用 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");
end

constraint 块:给随机化加上规则

不加任何约束时,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_cdata_nonzero_c)之间没有先后顺序关系,求解器会同时满足它们,而不是先满足一个再满足另一个。

更丰富的约束写法

约束表达式可以描述更复杂的规则。加权分布dist 让某些取值出现的概率更高(比如更频繁地测试边界值):

constraint data_dist_c {
  data dist { 8'h00 := 1, [8'h01:8'hFE] :/ 8, 8'hFF := 1 };
}

上面的写法大致是"取到 0x000xFF 各占 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 == WRITEaddr 不受这条约束影响,可以取整个位宽范围内的任意值。

一次性额外约束: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

这会同时给 xy赋随机值,满足 x inside {[0:9]}y > x——和 constraint 块一样是声明式、交给求解器处理的规则,只是作用对象换成了不属于任何类的普通变量。

std:: 前缀是必须的:在类的方法内部,直接写 randomize(...) 永远意味着"调用当前对象自己的 randomize() 方法"。std::randomize(...) 明确表示"使用全局的系统级版本",这样才能在方法内部随机化局部变量(或者类里没有 rand 修饰的成员),而不会被误解为在调用对象自己的 randomize()

打开/关闭随机化:rand_modeconstraint_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
endclass

checksum 不能是 rand——它的值依赖 addrdata 随机化之后的结果,而不是自己独立随机出来的。post_randomize() 正是计算它的合适位置:等它被调用时,randomize() 已经给 addrdata 赋好了新值。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() 完成赋值之后,会自动调用哪个回调方法?常用来计算依赖字段或打印调试信息。(英文小写,不带括号)