几个月前帮组里一位刚转验证的同事Review代码看到一个激励类里写着这样一行randc bit [7:0] addr;同事很自信地跟我解释“加了c就是更随机一点不会连续出现同样的地址。”听起来好像没什么毛病但后来回归跑了整整一周地址在全覆盖率的统计里始终有两个值没有被踩到。问题就出在他对randc的理解上他把randc当成了“加强版rand”却不知道randc背后有一套完整的轮次调度机制。类似的误会我在代码评审里见过太多次了所以这篇直接把rand和randc拆开讲透包括它们各自的行为差异、底层调度逻辑以及标题里另一个问题——如果项目里不允许用randc怎么用rand加constraint实现同样“一轮内不重复”的效果。这篇文章适合三类人看刚入行芯片验证、正在啃SystemVerilog随机化约束的工程师在UVM环境中写sequence、写寄存器模型但被randc坑过的人以及做覆盖率优化时想通过随机化提高遍历效率的验证同学。1. 骰子与牌堆rand和randc在“随机”这件事上的本质分歧先抛结论rand是“骰子”每次投掷完全独立跟之前出现过什么没有关系randc是“牌堆”一副牌洗完发完才算一轮发过的牌不会重复出现直到整副牌发完才重新洗牌。很多教材会把randc解释成“循环随机”这个说法没有错但很容易被误解成“它只是更容易均匀一点”。实际上randc的行为是严格的不放回抽样对于一个N比特的randc变量它的取值范围是2的N次方个离散值随机化求解器会从这2的N次方个值里每次抽一个抽过的值从本轮候选集合中剔除直到所有值都被抽过一遍下一轮才重新开始。而rand在每次调用randomize()时都是从全集里重新独立抽样允许连续两次抽到完全相同的值。1.1 一次16次随机的对照实验口说无凭。我们在仿真里跑一个小例子class rand_vs_randc; rand bit [7:0] r1; randc bit [7:0] r2; endclass module tb; initial begin rand_vs_randc obj new; repeat (16) begin void(obj.randomize()); $display(rand %0d, randc %0d, obj.r1, obj.r2); end end endmodule这段代码里r1和r2每次一起随机化。r1是普通rand16次打印中你大概率会看到有人重复虽然8位范围下重复概率不算高但理论上是允许且会发生的而r2因为是randc16次打印里必然不存在重复值因为一轮256个值远没有发完。这个实验对验证工程师的核心启发是如果你需要依靠随机化去踩遍一个较大的合法地址空间或命令码集合用randc会比rand更高效如果你只是想模拟真实业务流中独立且可能重复的事件rand反而更贴近物理世界。1.2 两者的适用边界和选择原则我把两个关键字的行为差异整理成一张对照表方便以后写代码前直接拍板对比维度randrandc每次randomize行为独立抽样可能重复本轮内不放回不能重复全值域遍历能力无保证靠大样本逼近有保证一轮内覆盖全部取值范围是否存在隐藏状态无无记忆性有每个实例维护轮次进度一轮周期长度不适用满量程时为2的N次方与constraint结合的稳定性稳定业界主用受约束影响候选集合可能变化典型使用场景大批量随机激励、独立事件模拟枚举遍历、地址全覆盖、命令码轮询选择原则其实就一条看你想模拟的现象是否要求“近期不重样”。比如要随机产生一个寄存器地址去反复读写如果0x00和0xFF都是关键边界你用rand可能要跑很久才能踩到一次边界用randc则保证每一个值都会出现一轮反过来如果模拟总线事务包的长度你希望每次包长是独立的那应该用rand而不是randc因为randc会强行让16个长度的包轮着出现真实场景反而不像。2. randc的“轮”是怎么转的周期、状态与约束边界理解了randc是牌堆还不够真正容易出问题的是“这堆牌什么时候重洗、洗完之后跟上一轮是什么关系、跟其他约束叠在一起时还算不算数”。这一章把randc的内部机制和边界条件讲清楚。SystemVerilog标准规定randc变量的取值范围就是它的满量程比如randc bit [7:0]就是0到255一个完整的轮次必然包含这256个值且本轮内不重复。求解器内部对实现方式没有强制要求——有的仿真器用隐藏计数器有的用排除列表——但标准只约束行为这一点你在不同EDA工具之间切换时不需要担心行为不一致倒是性能会有差异。2.1 new()、轮次进度与对象生命周期这是我在面试和代码评审时最喜欢考的一个点randc的轮次进度是跟对象实例绑定的不是跟类绑定的更不是全局的。每当你对同一个对象连续调用randomize()时randc才会按轮次推进。一旦你用new()重新创建一个对象randc的轮次进度就重新初始化相当于牌堆重新洗好但还没发牌。这个特性对验证环境的设计影响很大。举个例子你在sequence里这样写class my_seq extends uvm_sequence #(my_trans); virtual task body(); repeat (10) begin my_trans req my_trans::type_id::create(req); start_item(req); void(req.randomize()); finish_item(req); end endtask endclass每次循环都create一个新对象每个新对象只randomize()一次就丢掉了。从全局看每个对象的randc都只走了轮次的第一个位置随机化求解器根据种子为每个新对象生成一个“轮次初始值”你这个sequence虽然调用了10次随机化却可能产生大量重复值某些值永远轮不到。这就是开头那个同事遇到的“回归跑一周还有地址没踩到”的典型成因。正确做法是让随机化对象跨多次调用存活class my_seq extends uvm_sequence #(my_trans); virtual task body(); my_trans req my_trans::type_id::create(req); repeat (10) begin start_item(req); void(req.randomize()); finish_item(req); end endtask endclass把create挪到循环外面同一个对象被随机化10次randc才能按轮次推进。这个小改动往往能显著改善地址类激励的全覆盖收敛速度。2.2 和constraint并存的隐藏代价randc不是孤立工作的它几乎总会跟constraint一起出现。最经典的写法是randc bit [7:0] addr; constraint c_addr_valid { addr inside {[0:9]}; }这行代码的意思是候选集合从0到255被约束成了0到9那么一轮周期就从256次变成了10次且这10次里0到9每个值恰好出现一次。这种“集合型约束”inside、关系运算符、算术表达式构造出的合法集合下randc的轮次行为在主流仿真器里是基本可靠的。但有一种情况必须警惕当randc变量和别的rand变量之间有交叉约束时randc的“遍历承诺”很可能被打破。比如randc bit [2:0] cmd; rand bit [2:0] data; constraint c_cross { cmd data; }这个约束把cmd和data绑定在了一起。每轮求解时求解器不仅要满足cmd轮内不重复还要满足cmd比当前data大。当一个候选cmd值跟本轮其它约束冲突时求解器完全有可能跳过它这一轮结束后你会发现某个cmd值从头到尾没出现过。也就是说只要约束不是单纯地对randc变量划定一个静态合法集合而是牵扯到其它随机变量或状态你就不能依赖randc做严格的全遍历。我的建议是让randc变量只跟“静态集合型约束”搭配凡是涉及多个随机变量联动的约束要么改成简单rand然后靠概率覆盖要么干脆用第3章的手写方案把“轮次”主动权拿在自己手里。3. 不用randc如何在constraint里再造一个“无重复循环”标题里那个问题“如何用constraint实现randc”在很多项目里是真实需求。原因五花八门团队代码规范要求所有随机成员必须是rand工具对randc的求解性能太差或者你需要在一个超大值域内做无重复抽样但不想让求解器去维护一个巨型候选集。这里给两个可行方案一个是通用但适用小值域的Used-Queue回填法一个是大值域低内存的LCG步长游走法。3.1 方案一Used-Queue回填法推荐小值域核心思想用一个队列记录本轮已经产生过的值约束中要求新值不能出现在这个队列中当队列长度达到值域规模时说明这一轮已经遍历完下一轮前清空队列、重新开始。class randc_by_constraint; parameter int unsigned N 16; rand bit [3:0] val; local bit [3:0] used_q[$]; local int unsigned used_cnt 0; constraint c_randc_sim { if (used_cnt N) { !(val inside {used_q}); } } function void post_randomize(); if (used_cnt N) begin used_q.delete(); used_cnt 0; end used_q.push_back(val); used_cnt; endfunction endclass逐行解释一下这个实现的心脏部分约束!(val inside {used_q})是主角。inside后面的队列里存的是本轮已经出现过的值!取反后新val必然不在其中。used_cnt N作为轮次判断条件。前N-1次随机化时count一直小于N约束被激活不断排除旧值第N次随机化时count等于N-1used_q里有N-1个值val只能选最后一个从未出现的值。等到第N次随机化结束post_randomize里会先把queue清空、count归零再记录当前值。这样下一轮又从空集开始行为跟randc完全一致。这个方案的好处是逻辑直观、行为完全可控跨轮边界也跟randc一样允许上一轮的最后一个值和本轮第一个值相同因为清空后无约束。缺点同样明显约束里每多一个已用值inside表达式就会多一个成员求解器要把它们全部展开成排除条件。当N超过几千甚至上万时求解时间会迅速恶化。所以我的建议是值域规模在几百以内时放心用这个方案值域超过四位数就要认真考虑性能问题了。3.2 方案二LCG步长游走法适合大值域低内存如果值域很大比如bit [31:0]你不可能用一个队列去存4G个已用值。这时候可以用线性同余发生器LCG的思路在constraint之外手动维护一个步长和当前位置通过数学性质保证一轮内无重复。LCG能实现无重复遍历的原理很简单对于值域N只要步长step和N互质那么从任意起点开始不断执行(idx step) % N会在回到起点前恰好走遍N个值一个不多一个不少。这样我们就不需要记录任何历史值只需要在每一轮结束时重新随机选一个与N互质的步长让下一轮的顺序发生变化。class lcg_randc_sim; local int unsigned N; local int unsigned idx; local int unsigned step; function new(int unsigned n); N n; idx 0; step 1; endfunction function int unsigned next(); int unsigned v; v idx; idx (idx step) % N; if (idx 0) begin do begin step $urandom_range(1, N - 1); end while (gcd(step, N) ! 1); end return v; endfunction function int unsigned gcd(int unsigned a, int unsigned b); while (b ! 0) begin int unsigned t b; b a % b; a t; end return a; endfunction endclass这段代码里有一个细节值得说明do...while重新选step的时机是idx 0也就是步长把这一轮完整走完、准备开启下一轮的时刻。gcd(step, N) ! 1的循环确保新步长与值域互质这是LCG周期等于N的前提条件。如果N是2的幂gcd判断可以简化成“step只取奇数”速度更快。这个方案的优点非常突出内存占用是常数级不管值域是8位还是32位都只存三个整数。缺点是它产生的不是完全随机的排列而是等差数列上按模走出来的顺序连续两个值之间的间隔固定为step。如果被测DUT对输入之间的相关性敏感这种等差排列可能会掩盖某些跨周期交互的bug。所以它更适合“只需要保证不重复、对顺序随机性要求不高”的大范围场景比如大批量写入不同地址做压力测试。3.3 两个方案的实测对比与选择建议我在这两种实现上都跑过一轮覆盖实验结论很清晰方案值域规模建议内存随机性强弱实现复杂度Used-Queue回填法数百以内随值域线性增长强接近原生randc低LCG步长游走法数千到32位全值域常数级中有等差数列相关性中实际选型时我的习惯是先用原生randc原生randc在复杂约束下行为不可控时改用Used-Queue回填法值域超过四位数且约束求解被拖到几秒以上时才切到LCG方案。还有一点不管用哪个手写方案都建议把“已用集合”或“当前步长”这类内部状态做成local避免外部代码不小心改坏。这点在team里多人维护同一个验证环境时尤其重要。4. 实战里那些让randc失效的隐蔽场景前几章把原理讲透了这一章全是真实项目里踩过的坑。如果说第2章是“randc本身的行为”那这一章就是“验证环境的行为如何反过来坑randc”。4.1 生命周期类问题new出来的对象和共享对象前文已经讲过“短生命周期对象会打断randc轮次”。这里再补一个反面场景多个sequence共享同一个transactor对象。假设顶层testbench里有一个公共的地址生成器对象几个sequence并行运行时都对它调用randomize()。两个并行进程同时操作同一个对象的randc状态时会发生类似“两个人同时从一副牌里抽牌”的竞态每个进程拿到的值确实不重复但两个进程会互相消费掉对方的候选值实际观察到的激励序列可能在一轮还没结束时出现“看起来重复”的情况因为进程A拿到的值和进程B上一轮结束时拿到的值在全局时间线上可能相邻。标准没有规定多进程并发对同一对象randc调用的原子性所以遇到这种场景不要依赖仿真器帮你兜底。我的经验是要么给生成器加uvm_resource或semaphore做互斥要么每个sequence持有自己的独立对象不要让公共对象进入并发随机的射程范围。4.2 register model上慎用randc在UVM寄存器模型中如果你把一个寄存器字段定义成randcrandc uvm_reg_field some_field;这个行为很有诱惑力因为你希望写寄存器时每个合法值都轮一遍。但寄存器模型有大量后门操作、镜像更新、predict操作这些路径会直接改写字段值不会走randomize的轮次推进逻辑。一旦寄存器值被外力写成一个不在当前randc候选集合中的值下一次调用randomize时求解器既要满足randc的轮次约束又要保留字段当前值如果字段不在随机域中或者被约束固定住容易出现CONSTRAINT INFEASIBLE或者行为在不同仿真器之间不一致。我的建议寄存器模型字段一律用rand配合一个合法的值列表约束不要用randc。需要遍历合法值的话用uvm_reg_field::randomize()配合外部计数器或者干脆预生成一个枚举数组按索引写入这样日志可追溯也不会被后门写操作干扰。4.3 用randc后覆盖率仍然不动的排查思路有一次我排查一个“randc地址生成器跑了几万次某些地址还是没覆盖到”的case第一反应是约束有问题。排查链路大概是这样的先检查是否存在第2章说的“交叉约束破坏轮次”把randc变量和普通rand变量之间的cross约束逐条注释掉观察覆盖点是否开始前进。这一步能快速定位是不是约束联动导致某些值被求解器跳过。然后检查对象生命周期看被随机化的对象是不是每次循环都new。如果是改成循环外创建再跑一遍覆盖率往往立刻改善。再检查是否在randomize() with {}内联约束里额外夹带了其它随机变量。有些仿真器在in-line constraint引入临时随机变量时会扩大解空间搜索randc的候选调度可能被干扰。我遇到过一次奇怪的现象把in-line约束改写为具名constraint后问题消失之后项目里就定了规矩涉及randc成员的约束一律用具名constraint不用with从句。最后检查代码覆盖率工具的采样方式。如果覆盖率模型对枚举值的bins定义把0到255拆成非法和合法两组而你的randc被约束在合法组内某些“合法却不需要关心”的值也可能因为bins自动生成被误判为覆盖缺口。这种是工具配置问题跟随机化代码无关别在这上面浪费时间。整个排查过程最忌讳的是一上来就怀疑EDA工具有bug。99%的情况下randc失效都是因为约束联动、对象生命周期或者内联约束这三类原因。4.4 一个更可控的替代思路预生成排列数组如果项目对激励顺序的可复现性要求特别高或者你想彻底绕开randc在整个验证环境里引入的隐性状态我强烈推荐一个老派做法在测试平台初始化阶段用$urandom和洗牌算法一次性生成一整轮无重复排列存到队列里序列里按索引依次取用。class perm_gen; local int unsigned arr[$]; local int unsigned idx 0; function void build(int unsigned n); arr.delete(); for (int unsigned i 0; i n; i) arr.push_back(i); for (int unsigned i n - 1; i 0; i--) begin int unsigned j $urandom_range(i); int unsigned tmp arr[i]; arr[i] arr[j]; arr[j] tmp; end endfunction function int unsigned next(); if (idx arr.size()) begin build(arr.size()); idx 0; end return arr[idx]; endfunction endclass这种实现把所有随机性集中在初始化阶段之后每一轮都只是按顺序读数组。好处有三个一是行为完全可预测打印日志时只要记录当前排列数组的种子就能复现整个序列二是随机值之间没有隐含的求解器状态并行环境里更安全三是可以随时在build函数里对排列结果做后处理比如要排除非法地址或者做加权排序比在constraint里绕来绕去容易得多。代价是内存占用和初始化时间。对百万以下量级的值域来说这个代价微乎其微所以我个人在地址遍历、命令码遍历这类场景里越来越倾向于这个方法原生randc反而成了次选。最后补一个我自己的习惯。判断一个随机成员到底该用rand还是randc或者该不该用手写方案我先问自己三个问题候选值域规模是多少这一轮遍历是否允许被其它约束打乱激励序列是否需要精确复现把这三个问题想清楚再回头看代码大多数随机化设计其实都能直接拍板不需要纠结关键字本身。