ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SystemVerilog随机分支:randcase与randsequence区别

SystemVerilog随机分支:randcase与randsequence区别 去年帮几个同学做 IC 秋招的模拟面试System Verilog 部分我几乎每次都会问同一道题randcase 和 randsequence 有什么区别各自用在哪。结果挺有意思——十个人里有八个能背出randcase 是按权重选分支randsequence 是生成随机序列但往下追问一句那 randsequence 的权重是跟谁比的基本就卡住了。这道题之所以好问是因为它同时考三件事你知不知道这两个语句的随机源在哪、你理不理解权重到底怎么归一化、你有没有真的在项目里用过它们。这篇就把我这些年从写激励、调 seed、到被长尾序列坑到半夜重启仿真的经历一次讲清楚。1. 一道面试题引出的分岔路randcase 选一条路randsequence 长一棵树1.1 两个语句都在解决运行期随机分支这个问题从语言层面看randcase 和 randsequence 都是 System Verilog 里的过程语句只能写在 initial、always、task、function 或者类方法这类过程代码里不能出现在约束块、也不能出现在连续赋值里。它们解决的共同问题是代码执行到某个点时需要按概率做选择。但选择的对象不一样。randcase 是在一组互斥的分支里挑一条走做完就结束了控制流是线性的进来选一条出去。randsequence 是在一组语法规则里做展开它选的不只是下一步做什么还包括后面还接不接东西、接几个最终产出的是一串有先后顺序、甚至带嵌套结构的事件序列。用一句话概括就是randcase 是在十字路口挑一条路走randsequence 是让一棵语法树随机生长然后在叶子上依次打印出动作。我第一次真正把 randsequence 用在项目里是要造一批带嵌套结构的总线激励。当时用了一个 class 加数组递归的方式写了一百多行改一次结构要动三四个地方。后来换成 randsequence二十行就写完了而且结构一眼能看懂。那次的感受很直接当你的随机需求里出现顺序和嵌套两个词的时候才轮到 randsequence 上场。1.2 本质差异randcase 共享一个概率池randsequence 只在单个产生式内部比权重这是我认为最关键、也是面试里最能分出水平的一点。randcase 里所有分支的权重是放在一起比的。写三个分支权重分别是 3、1、6那它们被选中的概率就是 30%、10%、60%因为分母是三个权重之和 10。整个 randcase 就是一个概率池你加一个分支、改一个数字其他分支的概率都会跟着变。randsequence 完全不是这么回事。它的权重只在同一个产生式的多条规则之间生效。比如你写cmd : read : 4 | write : 3 | idle : 3 ;这里的 4:3:3 只在产生式 cmd 被展开这一件事发生的时候有意义概率是 4/10、3/10、3/10。至于 cmd 本身被展开多少次、什么时候被展开是由上层产生式决定的跟这组权重没有任何关系。所以你不能说read 在整条序列里的概率是 40%那是错的——read 在整条序列里的占比取决于 main 里 cmd 出现几次。很多人第一次用 randsequence 觉得概率不对根子就在这里他把 randsequence 当成了 randcase 的语法糖其实两者根本不在同一个抽象层次上。1.3 和 constraint/dist、$urandom_range 的分工边界再往外延一层这两个语句和另外两种常见随机手段的关系也需要理清楚。约束块里的dist是给类成员做分发用的它写在constraint里参与随机求解器的求解过程好处是能被randomize()的失败检测覆盖到坏处是只能在类里用而且是声明式的不能写代码逻辑。$urandom_range是最底层的均匀随机数简单直接但它本身不带权重要实现加权你得自己累加、自己比较。我的分工习惯是这样的事务内部的字段随机交给类加 dist事务之间的顺序和结构随机交给 randsequence运行期临时插个延迟、丢个包、加个背压交给 randcase纯粹需要个均匀随机数就用 $urandom。四者不是替代关系各管一段。1.4 面试被问到这道题我会怎么答如果只给三十秒我会这么答randcase 和 randsequence 都是过程语句随机源都是当前进程。区别在于 randcase 在互斥分支间按权重选一条执行权重是全局归一化的randsequence 是按描述好的语法产生式做随机展开权重只在单个产生式内部生效产出的是一段有顺序、可嵌套的序列。再补一句randsequence 在真实项目里用得少因为它可读性和工具支持都不如用类建模基本就能体现出你是真用过而不是背书。2. randcase 的权重是怎么算出来的求值时机、边界与改写方案2.1 权重表达式、概率公式以及它没有 default 这件事randcase 的语法规则很简单每个分支前面跟一个整型表达式当权重冒号后面是语句task automatic inject_random_latency(); randcase 70 : #10ns; 20 : #50ns; 10 : #100ns; endcase endtask这里有个新手特别容易误解的点权重不需要加起来等于 100也不需要是百分比。上面写 70/20/10 只是因为它凑巧好看。你写成 7/2/1、或者 7000/2000/1000概率完全一样。权重唯一的作用是表达相对比例最终概率是单个权重除以所有权重之和。另一个容易被忽略的点randcase 没有default分支。你没法像 case 那样写一个兜底项。如果你想表达剩下的概率给某个分支只能自己算100 - w_err这种写法就是典型场景。这里立刻引出一个坑——如果w_err是个由覆盖率动态调整的变量它一旦超过 100100 - w_err就变成负数负权重是违规写法不同仿真器的处理完全不同。所以动态权重表达式一定要先做范围夹紧别指望工具帮你兜底。int unsigned w_err; w_err pick_err_weight(); // 由覆盖率或测试阶段决定 w_err (w_err 100) ? 100 : w_err; // 夹紧别省这一步 randcase w_err : gen_err_packet(); 100 - w_err : gen_normal_packet(); endcase权重0的分支永远不会被选中这个特性非常好用——它相当于给每个分支装了一个隐形开关。做定向测试的时候我经常挂一组配置变量在权重上把不关心的场景权重置 0不改代码就能把随机测试临时变成定向测试。但要注意别把所有分支都置 0那会踩到下面说的边界行为。2.2 权重什么时候被求值把副作用写进权重是自找麻烦LRM 的要求是进入 randcase 时先按顺序把所有权重表达式求值一次然后再根据这组值做选择分支语句执行期间不再重新求值。也就是说权重表达式的求值次数是一次与哪个分支被选中无关。结论很直接权重表达式必须是纯的不要有任何副作用。我见过有人把计数器自增写进权重里比如cnt : do_something();本意是想统计各分支被选中的次数结果统计出来的数字全乱——因为每次执行 randcase 都会把所有权重表达式都求值一遍计数器加的是被评估次数而不是被选中次数。想统计分布老老实实在分支体里打点别在权重里搞花活。还有一个作用域上的小陷阱randcase 里不能引用外层分支里声明的局部变量作为内层 randcase 的权重。嵌套 randcase 时权重要么用外层作用域可见的变量要么用常量别在分支内部临时声明再往下一层传。2.3 全零权重、负权重与仿真器行为的差异权重和为零是个边界情况。LRM 并没有给出一个所有工具都统一遵守的强制行为有的仿真器直接报错有的告警之后不执行任何分支有的可能还有别的处理。这种东西不要依赖尤其是别写出我赌它不执行的代码。负权重同理属于违规写法有的工具当 0 处理有的直接编译报错。既然规则里说权重是非负整型表达式那就别越线。工程上更稳妥的做法是加一层断言在随机测试的初期就把配置错误暴露出来function automatic void check_weights(int unsigned w[]); int unsigned sum 0; foreach (w[i]) begin sum w[i]; if (w[i] 0 i 0) $display(warn: branch %0d weight is 0, i); end if (sum 0) uvm_fatal(RANDCASE, all weights are zero, fix the config) endfunction另外提醒一句权重必须是整型。想表达 2.5:1 这种比例只能两边同乘放大成 5:2别塞实数进去。2.4 用 $urandom_range 手工实现一遍理解得更透想确认自己真的理解了 randcase 的语义最好的办法是手写一个等价的实现。下面这段不依赖 randcase纯粹用$urandom_range加累积权重表实现跑出来的分布应该和 randcase 基本一致function automatic int weighted_pick(int unsigned w[]); int unsigned sum 0; int unsigned r, acc; foreach (w[i]) sum w[i]; if (sum 0) return -1; r $urandom_range(0, sum - 1); acc 0; foreach (w[i]) begin acc w[i]; if (r acc) return i; end return w.size() - 1; // 理论上到不了这里 endfunction写一遍你就会发现这个手写版本每次调用消耗一个随机数randcase 在多数仿真器里的实现方式也差不多——它同样是从当前进程的随机流里取一次随机值。这一点直接引出了第 4 节要讲的随机稳定性问题。3. randsequence 的产生式语法从 BNF 式的规则生成序列3.1 产生式、终结符和代码块randsequence 的写法像在写一套语法这一点是刻意的它本来就是为协议报文序列指令流事务结构这类有语法的东西设计的。基本结构是一个语句、一个名字、若干条产生式randsequence (main) main : hdr body tail ; hdr : { $display([%0t] HDR, $time); } ; tail : { $display([%0t] TAIL, $time); } ; body : item : 4 | item item : 1 ; item : { $display([%0t] ITEM, $time); } ; endsequence括号里的main是这条 randsequence 的名字用来做层次化引用和日志定位不是模块名也不是变量名。main、hdr、body、item这些叫产生式名字冒号后面是它的规则多条规则用|分开。{ ... }里的是代码块也是最终的叶子——序列展开到代码块就产出实际动作了。没有代码块只有产生式名字的规则会被继续展开。有一个硬性要求每个非终结产生式最终都必须能推导到代码块。如果你的产生式之间绕成环比如a : b ; b : a ;编译能过但运行时会无限展开直到栈溢出或者直接卡死。这不是概率问题是逻辑问题。我在项目里见过一次类似的写法仿真跑了两分钟没出结果最后发现是某个产生式的所有分支都是递归分支没有任何一条能停下来。3.2:权重与:/交织权重产生式的权重写在规则后面用:cmd : read : 4 | write : 3 | idle : 3 ;语义是相对权重和 randcase 一样的归一化逻辑但作用域只在这一个产生式内。LRM 里还有一种:/写法它的定位偏向交织场景和下面的rand join配合使用。日常生成序列用:完全够用: /我个人的建议是——如果你真的要用先在你手上的仿真器里跑一个小例子确认归一化行为不同工具在这一点上的表现我不敢替你打包票。这里还有个很容易踩的语法坑权重只能挂在产生式的规则上不能挂在一个内联的分组上。像seq : if (a) (x : 3 | y : 7) ;这种写法是不合法的权重的两侧必须是被|分开的完整规则。想要在条件分支里做加权得把加权的那一层单独抽成一个产生式seq : if (depth 3) leaf else branch ; branch : tree : 3 | leaf : 7 ;3.3 if、repeat、case 三个控制结构在产生式里的用法产生式列表里除了产生式名和代码块还能放if-else、repeat和case三种控制结构以及rand join。它们的条件表达式在每次展开到这个位置时重新求值所以可以直接读外部变量这就给了我们控制序列形态的手段。最实用的组合是if加一个深度计数器用来给递归产生式加兜底int unsigned depth 0; initial begin randsequence (main) main : { depth 0; } seq ; seq : if (depth 3) leaf else branch ; branch : tree : 3 | leaf : 7 ; tree : { depth depth 1; } seq { depth depth - 1; } ; leaf : { $display(leaf depth%0d, depth); } ; endsequence end这段的意思是深度没到 3 的时候按 3:7 的概率决定是继续往下长一层还是收尾一旦到了 3无条件走leaf结束。depth的加减必须成对写在tree规则里{ depth depth 1; }放在seq前面、{ depth depth - 1; }放在后面这样递归回溯的时候计数才准。省掉这个计数器长序列的概率虽然低但确实存在跑十万次总会撞上一次特别长的链日志刷屏不说极端情况下还可能把栈吃掉。repeat用来做固定次数的重复比如body : repeat (4) item ;。注意它不是循环执行的而是在展开时把item复制四份展开之后每份独立做随机选择所以四次的结果互不相同——这正是我们想要的。case用得少一些主要是在产生式里根据外部模式变量切换整套子结构比如按mode走不同的帧格式。能用但可读性一般我通常更倾向把不同模式拆成不同产生式用if选。3.4 rand join 的交织语义和它的支持现状rand join是这套语法里最炫也最不建议上生产环境的一个它把多条产生式序列随机交织在一起括号里可以给一个概率表达式用来影响哪条序列先展开、交织的倾向如何。问题在于这是 randsequence 里支持度最参差不齐的特性。有的仿真器支持得很好有的只支持一部分形式有的在嵌套使用时行为微妙。我在一个项目里想用它来模拟两条独立数据流的交织本地跑得挺欢换到另一个仿真器上结果完全对不上最后花了半天定位结论是这东西不适合出现在需要跨工具复现的环境里。后来我改成fork-join加随机延迟或者干脆把交织点显式建模成一个产生式效果一样好可读性还更高。这是我用过一次就再也不想用的特性列出来主要是因为面试偶尔会问到。3.5 返回值与产生式参数能不用就不用产生式可以带返回类型和端口randsequence 也能出现在表达式上下文里让外层拿到一个值。理论上很灵活实
返回列表