3、SystemVerilog断言基础:立即断言、并发断言、断言的应用场景

3.1 断言是什么?我为什么离不开它?

做FPGA验证这些年,我越来越觉得断言是个好东西。说白了,断言就是一段检查代码,它盯着你的信号,看它们是不是符合预期。不符合?立刻报错。

我记得刚入行那会儿,写验证全靠波形。仿真跑完,眼睛盯着波形一帧一帧地看,累得不行。后来有个老工程师跟我说:「你为什么不加断言?」从那以后,我就再也没离开过它。

断言的好处很明显:它能帮你把「检查」这件事自动化。你想想看,一个模块几百个信号,靠肉眼盯,能盯出几个bug?断言一加,仿真跑完,哪里出问题一目了然。

核心观点:断言不是可选项,而是验证的必需品。它让你的验证从「被动看波形」变成「主动抓错误」。

3.2 立即断言:简单粗暴,但好用

立即断言,名字就说明了一切——它立刻检查,立刻出结果。不等待,不延时。

语法很简单:

assert (expression) else $error("错误信息");

举个例子,我检查一个握手信号:

always_ff @(posedge clk) begin
  if (valid_in) begin
    assert (ready) else $fatal("valid_in为高时,ready必须为高!");
  end
end

这段代码什么意思?每次时钟上升沿,如果valid_in拉高,我就检查ready是不是也高。不是?直接报fatal,仿真停掉。

我个人习惯,致命错误用$fatal,普通错误用$error。$fatal会终止仿真,适合那些「一旦发生,后续验证毫无意义」的场景。$error只是打印警告,仿真继续跑。

小技巧:立即断言写在always块里,或者initial块里。它本质上是过程性代码,不是声明性的。

3.3 并发断言:这才是真正的利器

立即断言虽然好用,但有个局限——它只能检查当前时刻的信号值。如果你想检查「一个事件发生后,另一个事件必须在3个时钟内发生」,立即断言就无能为力了。

这时候,并发断言登场。

并发断言基于时钟,可以描述时序关系。它的语法长这样:

property_name: assert property (
  @(posedge clk) 触发条件 |-> 检查条件
);

注意那个 |-> 符号,它叫「蕴含操作符」。左边是触发条件,右边是检查条件。触发条件满足时,检查条件必须在同一个时钟周期成立。

还有一种 |=> ,表示下一个时钟周期检查。

看个实际例子:

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

assert property (p_req_ack)
  else $error("req拉高后,ack未在1~3个时钟内响应");

这段代码检查什么?req拉高后,ack必须在接下来的1到3个时钟周期内拉高。超过3个周期?报错。

我在项目中遇到过一个问题:一个总线协议要求地址有效后,数据必须在2个周期内准备好。我一开始用立即断言,发现根本写不出来。后来换成并发断言,一行代码搞定。

注意:并发断言必须写在module内部,不能写在initial或always块里。它是声明性的,不是过程性的。

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

很多新手问我:「这两个到底有什么区别?我该用哪个?」

我一般这么回答:

  • 检查当前时刻的值 → 用立即断言。比如「valid为高时,data不能为X态」。
  • 检查跨时钟的时序关系 → 用并发断言。比如「req后3个周期内必须有ack」。
  • 检查序列 → 用并发断言。比如「a拉高,然后b拉高,然后c拉高」。

说白了,立即断言是「点检查」,并发断言是「段检查」。

我个人的经验是:能用并发断言的地方,尽量用并发断言。为什么?因为并发断言更灵活,表达能力更强。而且并发断言可以复用——你可以把property定义好,然后在多个地方调用。

特性 立即断言 并发断言
检查时机 立即执行 基于时钟边沿
时序描述 不支持 支持(##、|=>等)
放置位置 always/initial块内 module内部
复用性 低 高(可定义property)
适用场景 简单组合逻辑检查 复杂时序协议检查

3.5 断言的应用场景:我踩过的坑

断言到底用在哪儿?我总结了几类常见场景:

3.5.1 协议检查

这是断言最经典的应用。比如AHB、AXI、APB这些总线协议,都有严格的时序要求。用断言把协议规则写下来,仿真时自动检查。

我曾经在一个项目中,用断言检查AXI的burst传输。结果仿真跑了不到10分钟,断言就抓到了一个地址非对齐的错误。要是靠波形看,估计得盯半天。

3.5.2 状态机检查

状态机是bug重灾区。我习惯在每个状态机后面加一个断言,检查状态是否合法:

assert property (@(posedge clk) 
  !($isunknown(state)) 
) else $error("状态机进入未知状态!");

还有一个经典检查:状态机不能卡死。比如某个状态必须在N个周期内跳转出去,否则报错。

3.5.3 数据完整性检查

数据在模块间传递时,会不会丢?会不会错?用断言可以检查:

assert property (@(posedge clk) 
  $rose(valid) |-> ##1 data == $past(data_dly)
) else $error("数据传递过程中发生变化!");

3.5.4 复位检查

复位信号释放后,所有寄存器应该处于已知状态。我习惯在复位释放后加一个断言:

initial begin
  @(posedge rst_n);  // 等待复位释放
  @(posedge clk);
  assert (!$isunknown({reg1, reg2, reg3}))
    else $error("复位释放后,寄存器仍为X态!");
end

避坑指南:我曾经在复位检查上吃过亏。当时没加断言,结果仿真通过,上板后寄存器全是X态。从那以后,复位检查成了我的必加项。

3.6 知识体系:一张图看懂断言

下面这张图,是我自己整理的断言知识体系。你看一眼,就能明白今天讲了什么:

SystemVerilog断言知识体系 立即断言 并发断言 特点 • 过程性代码(always/initial内) • 检查当前时刻信号值 • 简单直接,适合点检查 特点 • 声明性代码(module内) • 基于时钟边沿检查时序 • 支持序列和蕴含操作 应用场景(两者结合使用) 🔹 协议检查 🔹 状态机检查 🔹 数据完整性检查 🔹 复位检查 立即断言负责「点」,并发断言负责「段」,两者互补,缺一不可

3.7 写在最后

断言这东西,刚开始用会觉得麻烦——「不就是看波形吗?加什么断言?」但等你真正用起来,你会发现它帮你省了多少时间。

我现在的习惯是:写RTL之前,先把断言写好。为什么?因为断言写清楚了,你的设计思路也就理清了。你想想看,如果你连「什么情况下应该报错」都说不清楚,那你的设计能靠谱吗?

嗯,今天就聊到这儿。断言是个大话题,后面我们还会深入讲sequence、property的进阶用法。但今天这些基础,够你上手用了。

记住一句话:没有断言的验证,就像没有安全带的赛车——跑得快,但风险也大。

核心总结:

  • 立即断言:检查当前时刻,写在always/initial里
  • 并发断言:检查时序关系,写在module里
  • 应用场景:协议、状态机、数据完整性、复位检查
  • 个人建议:能写并发断言就别写立即断言,复用性更好

可可精品资料库