4、随机化与约束:随机化概念、rand/randc关键字、约束块、内嵌约束

各位同学,咱们今天聊聊验证里最核心的一个话题——随机化。

说实话,我刚入行那会儿,对随机化是又爱又怕。爱的是它能自动生成海量测试,怕的是它生成的东西完全不可控。后来我才明白,随机化不是乱来,而是「带着镣铐跳舞」。这个镣铐,就是约束。

4.1 随机化概念:为什么我们需要它?

你想想看,如果每个测试用例都要手写,一个模块几百个场景,写到猴年马月?而且人写的测试,很容易陷入思维定势——你总觉得某个边界条件不会出问题,结果偏偏就出在那里。

随机化的意义,就是让工具帮你「瞎蒙」。但这个「瞎蒙」是有方向的。我们告诉工具:地址要在0x1000到0x1FFF之间,数据长度必须是偶数,操作类型只能是读或写。剩下的,让工具自己去组合。

我在项目中遇到过最典型的例子:一个AHB总线桥,手写测试跑了两个月没出问题,随机化一跑,三天就抓到一个地址对齐的bug。嗯,从那以后,我对随机化就彻底服气了。

核心思想:随机化 = 随机生成 + 约束控制。没有约束的随机是灾难,没有随机的约束是死板。

4.2 rand 与 randc 关键字:两种随机模式

SystemVerilog给了我们两个关键字来声明随机变量:randrandc。它们有什么区别?我直接说人话:

  • rand:每次随机都是独立的,可能重复。就像你掷骰子,这次掷出6,下次还可能掷出6。
  • randc:周期随机,一轮内不重复。就像你从一副牌里抽牌,抽过的牌不会马上再出现,直到所有牌抽完才重新洗牌。

我个人习惯:大部分场景用 rand 就够了。但如果你需要遍历所有可能值,比如测试一个4位计数器,用 randc 会更高效。

class Packet;
    rand  bit [7:0] addr;    // 每次独立随机
    randc bit [3:0] id;      // 一轮16个值,不重复
    
    // 约束块稍后讲
    constraint c_addr {
        addr inside {[0:255]};
    }
endclass

initial begin
    Packet p = new();
    repeat(20) begin
        assert(p.randomize());
        $display("addr=%0d, id=%0d", p.addr, p.id);
    end
end

运行这段代码,你会发现 id 在0到15之间循环,而 addr 可能连续出现相同值。这就是两者的区别。

小技巧:randc 做覆盖率的「穷举」非常方便。我曾经用它来验证一个状态机的所有状态跳转,一轮跑完,覆盖率直接100%。

4.3 约束块:给随机化戴上「紧箍咒」

约束块是随机化的灵魂。没有约束,随机变量就是脱缰的野马。约束块用 constraint 关键字定义,里面可以写各种条件。

常见的约束写法有几种:

约束类型 示例 说明
范围约束 addr inside {[0:255]}; 地址在0到255之间
关系约束 len < max_len; 长度小于最大值
条件约束 if (op == READ) addr inside {[0:127]}; 读操作时地址范围缩小
权重约束 dist {0 := 1, 1 := 10}; 值1出现的概率是值0的10倍

这里我要特别提一下 dist 操作符。它用来控制随机值的分布权重。我在项目中遇到过一个问题:某个错误注入场景需要90%的概率产生正常包,10%的概率产生异常包。用 dist 一行就搞定了。

class Transaction;
    rand bit [1:0] pkt_type;
    
    constraint c_type {
        pkt_type dist {
            0 := 90,   // 正常包,权重90
            1 := 5,    // 异常包1,权重5
            2 := 3,    // 异常包2,权重3
            3 := 2     // 异常包3,权重2
        };
    }
endclass

注意:约束块里的条件不能矛盾。比如你同时写了 addr > 100addr < 50,随机化会失败。我曾经调试过一个bug,查了两天才发现是约束冲突导致 randomize() 一直返回0。

4.4 内嵌约束:临时加个「小灶」

有时候,类里定义的约束太死板了。比如你有一个通用的Packet类,大部分测试用默认约束就行。但某个特殊场景,你需要临时改一下约束——这时候内嵌约束就派上用场了。

内嵌约束用 randomize() with 语法,直接在调用时添加额外约束。它不会修改类本身的约束,只是这次随机化临时生效。

class Packet;
    rand bit [7:0] addr;
    rand bit [7:0] data;
    
    constraint c_default {
        addr inside {[0:100]};
        data inside {[0:50]};
    }
endclass

initial begin
    Packet p = new();
    
    // 正常随机化,使用默认约束
    assert(p.randomize());
    
    // 内嵌约束:这次让addr在200到255之间
    assert(p.randomize() with {
        addr inside {[200:255]};
        data == 0;  // 数据固定为0
    });
    
    $display("addr=%0d, data=%0d", p.addr, p.data);
end

内嵌约束的语法很灵活。你可以加新约束,也可以覆盖已有的约束。但要注意:内嵌约束不能删除类里定义的约束,只能增加或收紧。

我的经验:内嵌约束特别适合做「边界测试」。比如你有一个通用的地址约束0-255,但想测试地址为0和255的边界情况。用内嵌约束 addr inside {0, 255}; 就搞定了,不用改类定义。

4.5 避坑指南:随机化常见问题

做验证这么多年,我在随机化上踩过的坑不少。挑几个典型的说说:

  • 约束过紧导致无解:我曾经写过一个约束,要求 addr > 200addr < 100。工具直接报错,我还纳闷了半天。记住:约束必须要有交集。
  • 忘记调用 randomize():新手常犯的错误。定义了rand变量,但没调用 randomize(),变量值永远是0。嗯,这个坑我至少帮别人排查过五次。
  • 随机化失败不处理:randomize() 返回0表示失败。很多人直接忽略返回值,导致后续用未初始化的变量。我建议用 assert(randomize()) 强制检查。
  • randc 变量数量太多:如果你声明了100个 randc 变量,工具需要遍历所有组合,仿真速度会急剧下降。我一般只用 randc 控制关键变量,其他用 rand

好了,这一章的内容就这些。随机化是验证的基石,掌握了它,你就能从「手写测试」的苦海中解脱出来。下一章我们聊聊覆盖率,看看怎么衡量你的随机化到底够不够。