第2章:SystemVerilog断言语法:立即断言与并发断言,assert/assume/cover关键字

好,咱们今天聊聊断言最基础的两个分类——立即断言和并发断言。说实话,我刚入行那会儿,总觉得这俩东西差不多,不都是检查条件吗?后来被一个bug狠狠教育了一顿,才明白它们各自有各自的脾气。

2.1 立即断言:简单粗暴的检查器

立即断言,说白了就是一个带标签的if-else。它执行的时候,就像你写代码一样,立刻检查条件。条件成立就通过,不成立就报错。

它的语法长这样:

label: assert (expression) [else $fatal("error msg")];

注意,这里的expression必须是非时序的,也就是不能带##这种延时操作。我习惯把它放在组合逻辑或者initial块里用。

举个例子:

always_comb begin
    valid_check: assert (data_valid && !data_busy) 
        else $error("data_valid和data_busy不能同时为高");
end

嗯,这里要注意:立即断言是过程性语句,必须放在always、initial、function或task里。你不能把它直接扔在module外面。

⚠️ 避坑指南
我曾经在项目中犯过一个低级错误:把立即断言写在always_ff块里,结果它只在时钟沿触发时才检查。但我的信号是组合逻辑变化的,中间状态全漏掉了。后来改成always_comb才解决问题。记住:立即断言的触发时机,取决于它所在的块。

2.2 并发断言:带时间轴的侦探

并发断言就不一样了。它天生跟时钟绑定,可以检查跨越多个时钟周期的行为。你想想看,一个握手协议从请求到应答可能要等好几个周期,这种场景下立即断言就抓瞎了。

并发断言的基本格式:

label: assert property (property_expr) [else action_block];

这里的property_expr可以包含##、|->、s_until等时序操作符。我举个例子,检查一个简单的请求-应答协议:

property req_ack;
    @(posedge clk) req |=> ##[1:3] ack;
endproperty

req_ack_assert: assert property (req_ack)
    else $error("请求后1-3个周期内没有收到应答");

这个断言的意思是:每当req拉高,在接下来的1到3个时钟周期内,ack必须至少拉高一次。你看,这种跨周期的检查,立即断言就做不到了。

💡 个人经验
我建议你把并发断言写在module外面,用bind的方式绑进去。这样既不影响RTL代码的整洁,又能方便地开关断言。我在一个大型SoC项目里就是这么干的,后期调试时爽得不行。

2.3 assert/assume/cover:三兄弟各司其职

这三个关键字,其实是一家人,但分工不同。我画了张图,帮你理清它们的关系:

断言三兄弟:assert / assume / cover assert 必须成立 不成立就报错 验证设计正确性 仿真/形式验证都用 我:这是最常用的 项目里90%都是它 assume 假设成立 约束输入环境 形式验证中常用 仿真中可替代assert 我:形式验证时 assume帮我省了好多事 cover 覆盖统计 检查是否被触发 验证激励完备性 不报错,只统计 我:经常被忽略 但覆盖率全靠它 三者语法完全一样,只是语义不同

说白了,assert是硬约束——你的设计必须满足,不满足就是bug。assume是软约束——我假设输入环境是这样的,形式验证工具会把它当作前提条件。cover是观察者——它只记录这个条件有没有发生过,不报错。

我举个例子你就明白了:

// assert:检查内部逻辑
a_valid_data: assert property (@(posedge clk) valid |-> ##1 data_ready)
    else $error("数据有效后下一拍必须准备好");

// assume:约束输入
a_input_range: assume property (@(posedge clk) addr < 256)
    else $error("地址必须在0-255范围内");

// cover:统计某个状态是否出现
c_fifo_full: cover property (@(posedge clk) fifo_full);
🔑 关键区别
  • assert:验证设计本身是否正确。不满足就是设计有bug。
  • assume:约束验证环境。不满足说明你的测试激励有问题。
  • cover:统计覆盖率。不满足说明你的测试没跑到这个场景。

2.4 实战中的选择策略

我个人习惯这样用:

场景 推荐关键字 原因
检查内部状态机跳转 assert 这是设计逻辑,必须正确
约束外部输入时序 assume 输入环境由验证者控制
检查某个事件是否发生 cover 用于功能覆盖率统计
形式验证中的前提条件 assume 告诉工具:我只关心在这些条件下
仿真中的输入合法性检查 assert 或 assume 看你想要报错还是约束
⚠️ 注意
在仿真中,assume的行为和assert几乎一样——不满足也会报错。但在形式验证中,assume是作为约束条件使用的。所以如果你写的是纯仿真环境,用assert就够了。我见过有人把assume当assert用,结果形式验证时发现工具把assume当成了约束,导致验证范围被错误地缩小了。

2.5 立即断言 vs 并发断言:什么时候用哪个?

这个问题其实很简单,我总结了一个判断标准:

  • 用立即断言:检查的是当前时刻的组合逻辑关系。比如:状态不能同时为IDLE和BUSY。
  • 用并发断言:检查的是跨周期的时序关系。比如:请求后3个周期内必须有应答。

举个例子,你想想看:

// 立即断言:检查当前值
always_comb begin
    state_check: assert (state inside {IDLE, BUSY, DONE})
        else $error("非法状态");
end

// 并发断言:检查时序
property handshake;
    @(posedge clk) req |=> ##[1:5] ack;
endproperty

handshake_check: assert property (handshake)
    else $error("握手超时");

嗯,这里有个小技巧:如果你不确定该用哪个,先问自己「这个检查需要看时钟沿吗?」。需要就看并发,不需要就看立即。我在项目中就是这么快速判断的。

💡 避坑指南
我曾经在一个项目中,用立即断言去检查一个跨周期的握手信号。结果仿真通过,但上板就挂。后来发现,立即断言只在组合逻辑变化时触发,而握手信号的变化发生在时钟沿之后,中间有毛刺。改成并发断言后,问题就解决了。所以记住:跨周期检查,一定要用并发断言。

好了,这一章的内容就这些。记住:立即断言是「现在检查」,并发断言是「跨周期检查」。assert是「必须对」,assume是「假设对」,cover是「有没有对」。搞清楚了这些,你写断言的时候就不会迷茫了。

可可精品资料库