第1章:激励生成与约束随机 — 让验证自己“跑”起来

大家好,我是你们的讲师。今天咱们聊聊验证里最核心的一个话题——激励生成与约束随机

说实话,我刚入行那会儿,写测试用例都是手写一个个固定的激励。比如发一个CAN报文,我就写死ID=0x123,数据=0xAA。后来发现,这种写法根本测不出bug。为什么呢?因为芯片不会按你写的剧本出牌。真正的错误,往往藏在那些你“没想到”的角落里。

所以,SystemVerilog给了我们一把利器——约束随机。说白了,就是让工具帮你生成“合法但不可预测”的激励。你只需要告诉它规则,剩下的交给随机。

1.1 约束随机化基础:从“手写”到“自动”

先看一个最简单的例子。假设我们要生成一个CAN报文,ID范围是0x100到0x1FF。

class CAN_Frame;
    rand bit [10:0] id;      // 11位CAN ID
    rand bit [7:0] data[];   // 数据场

    constraint id_range {
        id inside {[11'h100 : 11'h1FF]};
    }

    constraint data_len {
        data.size() inside {[1 : 8]};
    }
endclass

你看,我只需要声明rand关键字,再写一个constraint块。每次调用randomize(),工具就会自动生成一个符合规则的随机对象。

核心思想:约束随机 = 随机性 + 合法性。随机性保证覆盖率,合法性保证不违反协议。

我个人习惯,在写约束时尽量用insidedist这些操作符。它们比写一堆if-else要简洁得多,而且不容易出错。

1.2 面向车规场景的约束:CAN/LIN/Ethernet

车规验证和普通芯片不一样。你想想看,车上的通信协议都是标准化的——CAN、LIN、Ethernet。每个协议都有严格的时序和格式要求。

CAN协议约束示例

class CAN_Extended_Frame;
    rand bit [28:0] id;      // 29位扩展ID
    rand bit [7:0] dlc;      // 数据长度码

    constraint can_ext_id {
        id inside {[29'h1000 : 29'h1FFF]};
    }

    constraint valid_dlc {
        dlc inside {[0:8]};
    }

    // 避坑:DLC不能超过8,这是CAN标准规定的
    constraint dlc_not_zero {
        dlc != 0;  // 我个人习惯,空报文容易漏掉某些逻辑
    }
endclass

我记得有一次,我在项目中遇到一个bug——某个CAN控制器在DLC=0时,竟然把数据场清零了。嗯,这就是约束没写全的后果。所以我现在写约束,都会把边界值、特殊值都考虑进去。

LIN协议约束示例

class LIN_Frame;
    rand bit [3:0] id;       // 4位LIN ID
    rand bit [7:0] data[8];  // 固定8字节数据

    constraint lin_id_range {
        id inside {[0:15]};
    }

    // LIN的校验和计算
    function bit [7:0] calc_checksum();
        // 实现略
    endfunction
endclass

Ethernet协议约束示例

class Ethernet_Frame;
    rand bit [47:0] dst_mac;
    rand bit [47:0] src_mac;
    rand bit [15:0] ether_type;
    rand bit [7:0] payload[];

    constraint valid_mac {
        dst_mac != 48'hFFFFFFFFFFFF;  // 广播地址单独处理
        src_mac[0] == 0;              // 单播MAC
    }

    constraint payload_len {
        payload.size() inside {[46 : 1500]};
    }
endclass

你可能会问:为什么要写这么多约束?说白了,就是为了让随机生成的报文“看起来像真的”。如果随机出一个MAC地址全是0的报文,那验证还有什么意义?

1.3 功能覆盖率驱动的随机测试生成

光有随机还不够。你得知道“测了什么”和“没测什么”。这就是功能覆盖率的用武之地。

我见过很多团队,随机跑了几万次,覆盖率还是0%。为什么?因为没写覆盖率收集代码。

covergroup CAN_Coverage @(posedge clk);
    coverpoint can_frame.id {
        bins low = {[11'h000 : 11'h0FF]};
        bins mid = {[11'h100 : 11'h1FF]};
        bins high = {[11'h200 : 11'h2FF]};
        bins reserved = default;
    }

    coverpoint can_frame.dlc {
        bins min_dlc = {0};
        bins max_dlc = {8};
        bins typical = {[1:7]};
    }

    cross can_frame.id, can_frame.dlc;  // 交叉覆盖率
endgroup

我的经验:覆盖率不是越高越好。我一般把目标定在95%左右。剩下的5%,往往是那些不可能出现的组合。硬要覆盖,反而浪费时间。

功能覆盖率驱动的流程是这样的:

  1. 先跑一轮随机测试
  2. 收集覆盖率数据
  3. 分析哪些bin没覆盖到
  4. 调整约束,定向生成缺失的激励
  5. 重复直到达标

我曾经在一个项目中,发现某个CAN ID的保留位从来没被覆盖到。后来一查,是约束写死了。改完之后,果然发现了一个隐藏的bug——那个保留位在特定条件下会触发错误中断。

1.4 本章知识体系

下面这张图,是我自己画的。它把本章的核心逻辑串起来了:

约束随机化验证流程 协议规范 约束条件 随机种子 约束随机化引擎 随机激励(CAN/LIN/Ethernet) 覆盖率分析 图:约束随机化验证流程 — 从规范到激励,再到覆盖率反馈

这张图想表达的是:约束随机不是瞎随机。它需要协议规范作为输入,约束条件作为规则,随机种子作为变量。最终生成的激励,还要通过覆盖率分析来反馈,看看有没有遗漏。

注意:随机种子不要用默认值。我建议每次仿真都记录种子号。这样如果发现bug,可以复现。我曾经因为没记种子,一个bug复现了三天...后来老老实实加了$urandom(seed)

1.5 实战建议

最后,给大家几个我自己的实战建议:

  • 约束要分层:协议层约束(如CAN ID范围)写在基类里;场景层约束(如特定测试用例)写在子类里。这样复用性高。
  • 覆盖率要早写:不要等随机跑完了再补覆盖率代码。我习惯在写约束的同时,就把covergroup写好。
  • 随机不等于无序:用dist操作符控制概率分布。比如,让边界值出现的概率更高一些。
  • 调试要留后门:加一个disable_constraint的开关。万一随机出来的激励不符合预期,可以快速排查。

好了,这一章就到这里。记住一句话:约束随机,是让验证从“手工”走向“自动化”的第一步。下一章,我们会深入SystemVerilog的随机化机制,看看randrandc到底有什么区别。


专注资料整理