3、SystemVerilog断言基础:立即断言与并发断言,assert/assume/cover的使用
各位做验证的朋友,咱们今天聊聊断言。说实话,我刚入行那会儿,觉得断言就是个可有可无的装饰品。直到有一次,一个时序问题在仿真里跑了三天才暴露出来,而如果当时写了断言,第一拍就能抓到。从那以后,我再也不敢小看它了。
SystemVerilog里的断言,说白了就两种:立即断言和并发断言。它们俩就像验证工具箱里的螺丝刀和电钻——各有各的用处,但你不能拿螺丝刀去钻墙,对吧?
3.1 立即断言:简单粗暴,立竿见影
立即断言,我习惯叫它"过程断言"。它写在过程块里(比如always块、initial块),执行到这一行就立刻检查。语法很简单:
assert (expression) else $error("断言失败");
嗯,就这么一行。但你别小看它。我在项目中遇到过一种情况:一个状态机的跳转条件写错了,仿真跑了上万拍才触发。后来我在状态跳转的地方加了个立即断言,每次跳转都检查当前状态是否合法。结果呢?第一轮回归就抓到了三个非法跳转。
核心要点:立即断言是阻塞性的。它执行完这一行,结果就出来了。适合用在组合逻辑、状态机跳转、数据通路的关键节点上。
举个例子:
always_ff @(posedge clk) begin
if (valid) begin
data_q <= data_in;
// 检查数据是否在合法范围内
assert (data_in inside {[0:255]})
else $error("数据越界: %0d", data_in);
end
end
你看,这个断言写在时序逻辑里,每次valid有效时都会检查。如果数据是256,仿真立刻报错,不用等到数据传到下游才发现问题。
3.2 并发断言:时序的守护者
并发断言就不一样了。它不写在过程块里,而是写在模块的声明区。它跟时钟走,可以描述跨多个时钟周期的时序关系。
我个人习惯把并发断言叫做"时序断言"。为什么?因为它天生就是为时序检查设计的。你想想看,一个握手协议,req拉高后几个周期ack必须拉高——这种场景,用立即断言你得写一堆状态机,而并发断言一行搞定:
property p_handshake;
@(posedge clk) req |=> ##[1:3] ack;
endproperty
a_handshake: assert property (p_handshake);
这个断言的逻辑是:如果req拉高,那么1到3个时钟周期后,ack必须拉高。简洁、清晰、可读性强。
我的经验:并发断言一定要写property,然后assert property。不要图省事把表达式直接写在assert里。为什么?因为property可以复用,可以单独调试,还可以被cover收集。
3.3 assert、assume、cover:三兄弟各司其职
这三个关键字,我经常看到有人混用。其实它们分工很明确:
| 关键字 | 用途 | 仿真行为 | 形式验证行为 |
|---|---|---|---|
| assert | 检查设计行为是否正确 | 失败时报错 | 作为证明目标 |
| assume | 约束输入环境 | 失败时报错 | 作为约束条件 |
| cover | 收集覆盖率 | 记录触发次数 | 不适用 |
assert 是老大,负责抓bug。设计该有的行为没出现,它立刻报警。
assume 是老二,负责管输入。我曾经在一个项目中,测试环境给DUT的输入信号总是违反协议,导致仿真跑飞。后来我在接口处加了assume,约束输入必须满足协议。这样一来,测试环境不规范,assume先报错,问题定位快多了。
cover 是老幺,负责记录。它不报错,只告诉你某个事件发生了多少次。我经常用cover来检查:这个状态机有没有进入过所有状态?这个握手协议有没有出现过背压?
注意:assume和assert在仿真里行为一样,都是报错。但在形式验证里,assume是约束,assert是目标。如果你把assume当assert用,形式验证会认为你在约束环境,而不是在检查设计。我曾经吃过这个亏,形式验证跑了三天,结果发现assume写错了,所有结果都是废的。
3.4 实战:一个完整的断言示例
咱们来看一个实际项目中的例子。这是一个AXI-Stream接口的验证:
module axis_checker (
input logic clk,
input logic rst_n,
input logic tvalid,
input logic tready,
input logic tlast,
input logic [7:0] tdata
);
// 1. 并发断言:valid不能在没有ready时拉低
property p_valid_stable;
@(posedge clk) disable iff (!rst_n)
($rose(tvalid) && !tready) |=> tvalid;
endproperty
a_valid_stable: assert property (p_valid_stable)
else $error("valid在未握手时被拉低");
// 2. 并发断言:tlast之后必须跟新的包起始
property p_tlast_to_tvalid;
@(posedge clk) disable iff (!rst_n)
$rose(tlast) |=> ##1 !tvalid;
endproperty
a_tlast_to_tvalid: assert property (p_tlast_to_tvalid)
else $error("tlast后下一拍valid必须为低");
// 3. cover:收集握手成功的次数
cover property (
@(posedge clk) tvalid && tready
);
// 4. 立即断言:检查数据在握手时不能为X
always_ff @(posedge clk) begin
if (tvalid && tready) begin
assert (!$isunknown(tdata))
else $error("握手时数据包含X态");
end
end
endmodule
这个例子涵盖了并发断言、立即断言、cover。你看,每个断言都有明确的职责:有的检查时序,有的检查数据完整性,有的收集覆盖率。
3.5 避坑指南
做了这么多年验证,我总结了几条断言的使用心得:
- 不要过度断言。我曾经在一个模块里写了200多个断言,结果仿真速度慢了30%,而且大部分断言从来没触发过。断言要打在关键路径上,比如协议边界、状态机跳转、数据通路。
- 并发断言一定要写disable iff。复位期间断言应该被禁用,否则复位释放那一拍会误报。我见过太多人忘了写这个,然后花一整天排查假失败。
- cover不要只写一个。我习惯在每个assert旁边配一个cover,记录这个断言被检查了多少次。如果cover计数为0,说明这个断言从来没被触发过——要么是环境没覆盖到,要么是断言写错了。
- 立即断言和并发断言可以混用。时序检查用并发断言,数据完整性检查用立即断言。各取所长。
一句话总结:assert抓bug,assume管输入,cover看覆盖。三者配合,你的验证环境才算完整。
这张图把断言体系梳理得很清楚。你写断言的时候,先想清楚:我要检查什么?是时序关系还是数据值?然后选对工具。别拿立即断言去检查跨周期时序,也别拿并发断言去检查组合逻辑——虽然也能用,但代码会变得很难看。
好了,关于断言的基础就聊这么多。记住:断言不是装饰品,它是验证的守护者。写好了,它能帮你省下几周的调试时间。写不好,它就是个摆设。多练、多用、多总结,你也能成为断言高手。
公众号:蓝海资料掘金营,微信deep3321