第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(),工具就会自动生成一个符合规则的随机对象。
核心思想:约束随机 = 随机性 + 合法性。随机性保证覆盖率,合法性保证不违反协议。
我个人习惯,在写约束时尽量用inside、dist这些操作符。它们比写一堆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%,往往是那些不可能出现的组合。硬要覆盖,反而浪费时间。
功能覆盖率驱动的流程是这样的:
- 先跑一轮随机测试
- 收集覆盖率数据
- 分析哪些bin没覆盖到
- 调整约束,定向生成缺失的激励
- 重复直到达标
我曾经在一个项目中,发现某个CAN ID的保留位从来没被覆盖到。后来一查,是约束写死了。改完之后,果然发现了一个隐藏的bug——那个保留位在特定条件下会触发错误中断。
1.4 本章知识体系
下面这张图,是我自己画的。它把本章的核心逻辑串起来了:
这张图想表达的是:约束随机不是瞎随机。它需要协议规范作为输入,约束条件作为规则,随机种子作为变量。最终生成的激励,还要通过覆盖率分析来反馈,看看有没有遗漏。
注意:随机种子不要用默认值。我建议每次仿真都记录种子号。这样如果发现bug,可以复现。我曾经因为没记种子,一个bug复现了三天...后来老老实实加了$urandom(seed)。
1.5 实战建议
最后,给大家几个我自己的实战建议:
- 约束要分层:协议层约束(如CAN ID范围)写在基类里;场景层约束(如特定测试用例)写在子类里。这样复用性高。
- 覆盖率要早写:不要等随机跑完了再补覆盖率代码。我习惯在写约束的同时,就把covergroup写好。
- 随机不等于无序:用
dist操作符控制概率分布。比如,让边界值出现的概率更高一些。 - 调试要留后门:加一个
disable_constraint的开关。万一随机出来的激励不符合预期,可以快速排查。
好了,这一章就到这里。记住一句话:约束随机,是让验证从“手工”走向“自动化”的第一步。下一章,我们会深入SystemVerilog的随机化机制,看看rand和randc到底有什么区别。