SV 随机化回调:pre/post_randomize、rand_mode、失败处理与动态加权
更新时间:2026-09-28。在 约束块与 inside/dist 求解 和 随机化进阶约束 里,SystemVerilog 的
randomize()把「合法值怎么选」交给了求解器。但真实用例里,光声明约束远远不够:随机前要先铺上下文、随机后要派生冗余字段、有的字段这次不能动、分布要随用例阶段动态变化。这一篇把随机化调用前后的所有可控环节串成一条流程,讲清每个钩子能做什么、不能做什么。
本文要回答的问题
pre_randomize和post_randomize分别在哪个时机被调用,各自适合干什么?rand_mode(0)关掉一个字段后,它还会被求解吗,值会不会变?randomize()返回 0 意味着什么,失败后对象是「不动」还是「半随机」?- 怎样在不改源码的情况下,运行时把某个 bin 的权重从 10 提到 80?
constraint_mode和rand_mode一个管约束一个管字段,怎么配合?
一、randomize() 的完整生命周期
很多工程师把 tx.randomize() 当成一条原子指令,实际上它是一个三段流程:调用前钩子 → 求解器求解 → 调用后钩子。理解这条流水线,后面所有机制就都有位置了。

上图的关键事实有两条。第一,两个钩子都是 function void,可以被派生类重写,在基类里留好虚函数接口,子类就能在不动基类代码的前提下注入自定义行为。第二,即使求解失败,post_randomize 也会被调用——这意味着你不能在 post 里假设字段都是合法新值,必须先检查状态。
class axi_txn;
rand bit [31:0] addr;
rand bit [3:0] len;
bit [31:0] aligned_addr; // 非 rand,由 post 派生
constraint c_len { len inside {[0:15]}; }
function void pre_randomize();
$display("[%0t] pre: old addr=%h len=%0d", $time, addr, len);
endfunction
function void post_randomize();
aligned_addr = addr & 32'hFFFF_FFFC; // 派生字段
$display("[%0t] post: new addr=%h len=%0d", $time, addr, len);
endfunction
endclass二、pre_randomize:求解前的最后准备
pre_randomize 最常见的三类用途:保存旧值(失败后能比对前后差异)、动态开关约束(按当前环境状态决定哪几条 c_* 生效)、记录日志(追踪每次随机化的上下文)。
它不适合做的事:不要在里面改 rand 字段的值指望被保留——求解器紧接着就会重写它,除非这个字段已经被 rand_mode(0) 关掉。也不要在里面调用 randomize() 自己,那是递归。
一个典型的用法是按用例阶段切换约束组合。假设同一个 transaction 类既要在正常流量里跑、又要在错误注入用例里跑:
class error_capable_txn extends axi_txn;
bit inject_err = 0; // 由 sequence 设置
rand bit parity_err;
constraint c_no_err { parity_err == 0; }
constraint c_some_err{ parity_err dist {0:=7, 1:=3}; }
function void pre_randomize();
// 按开关禁用其中一组,启用另一组
c_no_err.constraint_mode(inject_err ? 0 : 1);
c_some_err.constraint_mode(inject_err ? 1 : 0);
endfunction
endclassconstraint_mode 只能在 pre_randomize 或更外层调用,写在求解器跑完之后毫无意义。它是运行时改约束的唯一正路,比 if...else 包两版 randomize 干净得多。
三、post_randomize:派生字段与一致性修补
求解完成后,post_randomize 是收尾的唯一机会。它该干的事是:派生非 rand 字段(比如 CRC、对齐地址、长度字段的字节数)、修补跨字段一致性(有些关系约束写不出来或写得太复杂,干脆随机主字段、post 里算从字段)、触发覆盖率采样。
class packet;
rand bit [7:0] payload [];
rand bit [3:0] kind;
bit [7:0] crc; // 派生
int unsigned byte_len; // 派生
constraint c_size { payload.size() inside {[4:64]}; }
function void post_randomize();
byte_len = payload.size();
crc = calc_crc(payload); // 调用非 virtual 普通函数
`uvm_info("PKT", $sformatf("kind=%0d len=%0d crc=%h",
kind, byte_len, crc), UVM_HIGH)
endfunction
endclass不要在 post_randomize 里再调用 randomize()。如果求解结果不满意想重试,应该把约束写得更严,或者在外面循环调用 randomize,而不是在钩子里递归——递归一次会触发新的 pre/post,层级混乱后极难调试。
还有一个反模式:在 post 里用 $display 打一大段调试信息但不关 UVM_HIGH,回归跑一晚上日志几百兆,定位问题反而更慢。把钩子当成逻辑代码,不要当打印机的收容所。
四、rand_mode:让某些字段这次别动
rand_mode(0) 把一个 rand 字段临时降为「普通变量」:求解器跳过它,保留原值。rand_mode(1) 恢复。它是按字段名/句柄调的,粒度比关整条约束更细。
典型场景有两个。
第一个,锁定某字段做定向测试。随机大部分字段、但固定 addr=0x1000 验证某个地址边界:
tx.addr.rand_mode(0);
tx.addr = 32'h0000_1000;
tx.randomize(); // addr 不变,其余字段随机第二个,数组里只随机一部分元素。rand bit [7:0] data[16] 想固定前 4 字节为包头:
foreach (tx.data[i])
if (i < 4) tx.data[i].rand_mode(0);rand_mode 的状态会保留在对象里,下一次 randomize() 仍然生效,直到显式恢复。这是常见的「忘了恢复」坑:第一次用例锁了 addr,第二个用例忘了开回来,整个回归里 addr 永远是同一个值,coverage 里的 addr bins 一片红。好习惯是用 rand_mode 的配对写法,用完立即恢复,或者在 sequence 的 pre_body/post_body 里成对调用。

五、randomize() 返回 0:失败了怎么办
randomize() 是有返回值的函数:返回 1 表示求解成功,返回 0 表示约束冲突、无解。两种常见踩坑方式:一是把返回值扔掉,失败也接着用对象;二是以为失败时字段保持「上一次的合法值」,继续往下跑——其实规范未定义,不同仿真器表现可能不同,依赖它就是埋雷。
正确姿势是先判断、再分支:
if (!tx.randomize() with { len > 8; }) begin
`uvm_error("RAND", "inline constraint conflict")
// 回退:去掉 inline 约束再来一次
if (!tx.randomize()) begin
`uvm_fatal("RAND", "base constraints unsolvable")
end
end这个例子里还演示了分两步收紧的模式:先试严格版本,失败就退到基础版本。base 约束(类里写的 constraint c_*)必须保证永远可解,否则没有任何回退余地。
调试失败约束的方法:把所有约束一条条 constraint_mode(0) 关掉,二分定位冲突源。或者先 randomize() with {1};(恒真 inline),能解说明类里约束有矛盾,不能解说明 rand 字段类型/范围本身有问题。

六、动态加权:运行时调整分布
dist 约束的权重在编译期写死,但很多场景要运行时改:用例前半段多打 read,后半段多打 write;压力阶段把 len 拉满;覆盖率关闭阶段把稀有 bin 权重拉高。两条路。
第一条,用 constraint_mode 切换多组 dist。预定义几组权重,pre_randomize 里按当前阶段启用对应组。这是干净做法,推荐优先用。
第二条,rand 字段充当权重。把权重本身声明为 rand int w_read;,约束里 kind dist {READ:=w_read, WRITE:=w_write}。求解时权重参与求解,但通常你希望它固定——配合 rand_mode(0) 锁权重,或把权重改成非 rand 字段、在 sequence 里直接赋值。第二条路灵活但容易绕晕,第一组方案能覆盖 90% 场景。
class phase_aware_txn extends axi_txn;
typedef enum {PHASE_READ, PHASE_WRITE, PHASE_MIX} phase_t;
phase_t phase = PHASE_MIX;
rand bit is_write;
constraint c_read_heavy { is_write dist {0:=9, 1:=1}; }
constraint c_write_heavy { is_write dist {0:=1, 1:=9}; }
constraint c_balanced { is_write dist {0:=5, 1:=5}; }
function void pre_randomize();
c_read_heavy.constraint_mode (phase == PHASE_READ);
c_write_heavy.constraint_mode(phase == PHASE_WRITE);
c_balanced.constraint_mode (phase == PHASE_MIX);
endfunction
endclasssequence 里只需在阶段切换时给 tx.phase 赋新值,下次 randomize() 自动按新权重求解。

七、组合实战:一次完整的随机化调用
把前面所有机制串到一个 transaction 类里:
class full_txn extends uvm_sequence_item;
typedef enum {NORMAL, ERR_INJECT, STRESS} mode_t;
mode_t mode = NORMAL;
rand bit [31:0] addr;
rand bit [3:0] len;
rand bit wr;
rand bit parity_err;
bit [31:0] aligned;
bit [7:0] crc;
constraint c_addr { addr inside {[32'h0:32'hFFFF]}; }
constraint c_len { len inside {[1:16]}; }
constraint c_normal { parity_err == 0; }
constraint c_err { parity_err dist {0:=7,1:=3}; }
constraint c_rd { wr dist {0:=8,1:=2}; }
constraint c_wr { wr dist {0:=2,1:=8}; }
constraint c_mix { wr dist {0:=5,1:=5}; }
function void pre_randomize();
// 按模式启用约束组合
c_normal.constraint_mode(mode != ERR_INJECT);
c_err .constraint_mode(mode == ERR_INJECT);
c_rd .constraint_mode(mode == NORMAL);
c_wr .constraint_mode(mode == STRESS);
c_mix .constraint_mode(mode == ERR_INJECT);
endfunction
function void post_randomize();
aligned = addr & ~32'h3;
crc = $countones(addr ^ len);
endfunction
endclass调用侧:
full_txn tx = full_txn::type_id::create("tx");
tx.mode = full_txn::STRESS;
if (!tx.randomize()) `uvm_fatal("RAND", "unsolvable")
// 定向补充:这次固定 addr
tx.addr.rand_mode(0);
tx.addr = 32'h100;
if (!tx.randomize()) `uvm_fatal("RAND", "unsolvable")
tx.addr.rand_mode(1); // 别忘了恢复整段流程里 pre_randomize 负责按模式摆好约束阵形,求解器填值,post_randomize 算派生字段,失败路径有明确的 fatal 兜底,rand_mode 的开关成对出现。这就是 SV 随机化回调机制在真实项目里的标准用法。
常见坑
- 在
pre_randomize里给 rand 字段赋值,指望求解器尊重——除非先rand_mode(0),否则会被覆盖。 - 在
post_randomize里再调randomize()想重抽,递归层级失控。 rand_mode(0)用完忘了rand_mode(1)恢复,后续用例分布全塌。- 把
randomize()的返回值当空气,失败继续走,对象字段处于未定义状态。 - 在 pre/post 钩子里挂耗时任务(比如 DPI 调外部模型),这两个是 function,不能消耗仿真时间,违规直接编译报错。
- 用
if...else包多份 randomize 调用替代constraint_mode,代码越长越难维护。
这一篇在知识体系里的位置
- 向上承接:约束块与 inside/dist 求解 讲清约束怎么写,本篇回答「写完之后怎么在运行时控制它」。
- 横向相关:随机化进阶约束 覆盖
solve...before、soft、数组约束等高级求解语义。 - 机制对照:随机稳定性与种子管理 讲种子怎么影响结果,本篇讲调用流程里用户能干预的钩子点。
- 落地延伸:sequence 与 driver/scoreboard 协作 演示这些机制在 UVM sequence 里的具体落地。
总结
SV 随机化回调以 pre_randomize → 求解 → post_randomize 三段流程为骨架:pre 里用 constraint_mode 摆阵形、用 rand_mode 冻结字段,post 里算派生字段和收尾;randomize() 返回值必须判定、失败要有回退路径;运行时改分布优先用多组 dist + constraint_mode 切换,把这套流程写在基类钩子里,子类和 sequence 就能用最小代价控制每次随机化的行为。