第1章:SystemVerilog断言基础:立即断言与并发断言的区别

各位同学,咱们今天聊聊断言。说实话,我刚入行那会儿,对断言这东西也是云里雾里的。总觉得「不就是检查几个信号嘛,用if语句不就行了?」后来被现实狠狠教育了一回——有一次仿真跑了两天,最后发现一个时序问题,if语句根本抓不住。嗯,从那以后,我再也不敢小看断言了。

SystemVerilog里断言分两种:立即断言和并发断言。这俩名字听着挺像,但本质完全不同。你想想看,一个像「保安查岗」,另一个像「监控录像」。咱们一个一个说。

1.1 立即断言:直截了当的检查

立即断言,说白了就是过程语句。它写在always块或者initial块里,跟$display、$monitor是同一类东西。执行到它的时候,立刻检查条件是否成立。

语法很简单:

assert (expression) [else action_block]

看个例子:

always @(posedge clk) begin
    if (valid) begin
        assert (data != 0) 
            else $error("data不能为0,时间=%0t", $time);
    end
end

这里要注意:立即断言是事件驱动的。什么意思?就是它只在valid为高且时钟上升沿的时候才执行。如果valid一直为低,这个断言永远不会被触发。

核心特点:

  • 写在过程块里(always、initial、task、function)
  • 执行到该语句时立即求值
  • 不依赖时钟沿(除非你写在时钟敏感块里)
  • 可以加else分支处理失败情况

我个人习惯,在模块内部做简单的数据完整性检查时,用立即断言特别顺手。比如检查FIFO的空满标志、状态机的非法状态跳转,这些场景用立即断言,代码写起来干净利落。

1.2 并发断言:时序的守护者

并发断言就完全不一样了。它不是过程语句,而是声明式语句。你把它写在模块的任意位置——甚至写在module和endmodule之间就行,不需要放在过程块里。

语法长这样:

property_name: assert property (property_spec) [else action_block];

看个实际例子:

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

a_req_ack: assert property (p_req_ack)
    else $error("req后3个周期内没有收到ack");

这个断言什么意思?它说:在每个时钟上升沿,如果req为高,那么在接下来的1到3个时钟周期内,ack必须为高。注意那个|=>符号,这叫「非重叠蕴含」,表示前件满足后,下一个时钟周期开始检查后件。

我的经验:并发断言最强大的地方在于它能描述时序关系。我在项目中遇到过总线协议检查,用并发断言写握手协议、流水线协议,比用always块写状态机检查要简洁十倍。而且并发断言是采样稳定的——它在时钟的预采样区采样,不会出现竞争问题。

1.3 两者的核心区别

咱们用一张表来对比,这样更清楚:

对比维度 立即断言 并发断言
本质 过程语句 声明式语句
执行方式 执行到即检查 在每个时钟沿自动检查
时序能力 无(需自己写时序逻辑) 有(支持##、|=>、s_until等)
采样稳定性 有竞争风险 预采样区采样,稳定
放置位置 过程块内部 模块任意位置
典型用途 数据完整性、状态检查 协议时序、接口协议

为什么会这样?其实核心在于仿真调度机制。并发断言在仿真器的预采样区(Preponed region)采样,在观察区(Observed region)求值。而立即断言在Active region执行,跟你的RTL代码混在一起。这就导致了并发断言天然抗竞争,而立即断言需要你小心处理。

1.4 知识体系结构

下面这张图,是我自己总结的断言知识体系。你看一眼,心里就有谱了:

SystemVerilog断言知识体系 立即断言 并发断言 过程块内使用(always/initial) 执行到即检查,无时序能力 典型:数据完整性、状态机检查 声明式,模块任意位置 时钟沿采样,支持时序操作 典型:总线协议、接口时序 选择依据:需要检查时序关系吗?

这张图其实就说明白了:你判断用哪种断言,就问自己一个问题——我需要检查时序关系吗?如果需要,用并发断言;如果只是检查某个时刻的数据对不对,用立即断言就够了。

1.5 避坑指南

我曾经踩过的坑:

  • 立即断言写在组合逻辑里:有一次我把立即断言写在组合always块里,结果仿真时断言被反复触发,日志文件直接爆了。记住,立即断言最好写在时序逻辑里,或者至少加一个使能条件。
  • 并发断言忘记指定时钟:并发断言必须有时钟事件,否则仿真器会报错。我见过新手写@(*) req |-> ack,这其实是错的——并发断言不支持@(*)。
  • 混淆蕴含操作符:|->和|=>的区别一定要搞清楚。前者是「重叠蕴含」,前件满足的当前周期检查后件;后者是「非重叠蕴含」,下一个周期开始检查。搞混了,断言结果完全相反。

1.6 什么时候用哪个?

我个人的经验法则:

  • 用立即断言:检查FIFO空时不能读、状态机不能进入非法状态、数据通路中的奇偶校验。这些场景都是「此时此刻」的检查,不需要跨周期。
  • 用并发断言:检查握手协议(req/ack时序)、流水线协议(valid/ready配合)、总线协议(地址/数据/控制信号的时序关系)。这些场景天然需要描述「过几个周期后」的关系。

小技巧:如果你不确定用哪种,先试试并发断言。因为并发断言能覆盖立即断言的功能(通过写@(posedge clk) disable iff(!rst) expr),但反过来不行。而且并发断言在仿真效率上通常更优——仿真器对并发断言有专门的优化。

好了,这一章咱们把立即断言和并发断言的区别讲清楚了。下一章我会深入并发断言的语法细节,包括蕴含操作符、序列、属性的组合使用。到时候咱们再聊。

可可精品资料库