立即断言:从入门到实战

好,咱们今天聊聊立即断言。说实话,很多刚接触SystemVerilog断言的朋友,一上来就被并发断言的各种时序搞晕了。其实,立即断言才是那个最朴实、最直接的家伙。我个人习惯把立即断言叫做"现场断言"——它就像你在代码里安插的一个个检查哨,一旦执行到那里,立刻判断对错。

立即断言语法

立即断言的语法其实特别简单。你看,它长这样:

assert (expression) [else $fatal|$error|$warning|$info("message")]

说白了,就是assert关键字后面跟一个条件表达式。条件为真,万事大吉;条件为假,就执行后面的else分支。嗯,这里要注意,else分支是可选的,但我强烈建议你加上。为什么?因为不加的话,断言失败了你都不知道。

举个例子:

always_ff @(posedge clk) begin
    if (valid) begin
        assert (data != 0) else $error("data should not be zero when valid is high");
    end
end

这段代码的意思是:当valid为高时,data不能为0。如果为0,就报错。简单粗暴,对吧?

立即断言的使用场景

我在项目中遇到过不少场景,用立即断言特别合适。总结下来,主要有这么几类:

  • 输入参数检查:函数或任务的输入参数是否合法
  • 状态机状态检查:状态机是否进入了非法状态
  • 数据一致性检查:比如FIFO的读写指针是否越界
  • 协议基本规则检查:比如握手信号的时序关系

你想想看,这些场景都有一个共同点:它们都是"此时此刻"就要判断的,不需要等几个时钟周期。这就是立即断言的用武之地。

核心区别:立即断言是过程性语句,必须放在过程块(always、initial、function、task)里。它不像并发断言那样可以独立声明。

立即断言示例

好,咱们来看几个实际例子。这些代码都是我实际项目中用过的,稍微改了一下。

示例1:函数参数检查

function automatic bit [7:0] add_one(input bit [7:0] value);
    assert (value < 255) else $fatal("value must be less than 255");
    return value + 1;
endfunction

这个例子很简单。函数add_one要求输入值小于255,否则就报致命错误。我曾经因为没加这个检查,导致仿真跑了一整天,最后发现是某个参数传错了。从那以后,我所有函数的输入参数都加了断言。

示例2:状态机非法状态检查

typedef enum {IDLE, READ, WRITE, DONE} state_t;
state_t current_state, next_state;

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        current_state <= IDLE;
    end else begin
        current_state <= next_state;
    end
end

always_comb begin
    assert (current_state inside {IDLE, READ, WRITE, DONE}) 
        else $error("Illegal state: %0d", current_state);
    
    case (current_state)
        IDLE: next_state = READ;
        READ: next_state = WRITE;
        WRITE: next_state = DONE;
        DONE: next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

这里有个坑,我提醒一下。状态机如果因为某些原因(比如综合后的毛刺)进入了非法状态,没有断言的话,你可能要花好几个小时才能定位到问题。有了这个断言,仿真一停,你就知道是状态机出问题了。

示例3:FIFO指针检查

module fifo #(parameter DEPTH = 16) (
    input clk, rst_n,
    input wr_en, rd_en,
    input [7:0] wr_data,
    output reg [7:0] rd_data,
    output full, empty
);
    reg [3:0] wr_ptr, rd_ptr;
    reg [7:0] mem [0:DEPTH-1];
    
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr <= 0;
            rd_ptr <= 0;
        end else begin
            if (wr_en && !full) begin
                assert (wr_ptr < DEPTH) else $error("wr_ptr overflow");
                mem[wr_ptr] <= wr_data;
                wr_ptr <= wr_ptr + 1;
            end
            if (rd_en && !empty) begin
                assert (rd_ptr < DEPTH) else $error("rd_ptr overflow");
                rd_data <= mem[rd_ptr];
                rd_ptr <= rd_ptr + 1;
            end
        end
    end
    
    assign full = (wr_ptr == rd_ptr - 1);
    assign empty = (wr_ptr == rd_ptr);
endmodule

注意:这里的指针检查其实有点多余,因为正常情况下指针不会越界。但我在项目中遇到过因为复位时序问题导致指针跳变的情况,所以加了这个断言。宁可多查,不可漏查。

立即断言的几个小技巧

做了这么多年验证,我总结了几条关于立即断言的经验:

  • 错误级别要选对$fatal用于不可恢复的错误,$error用于一般错误,$warning用于提醒。别动不动就用$fatal,否则仿真跑一半就停了,你都不知道后面还有没有其他问题。
  • 信息要详细:断言失败时,尽量输出有用的信息,比如变量值、时间戳等。否则你看到"assertion failed"几个字,还得去翻代码。
  • 不要滥用:立即断言适合检查"不应该发生"的情况。对于"可能发生"的情况,用正常的if-else处理就好。

我的习惯:在验证环境里,我会把立即断言和并发断言分开管理。立即断言放在模块内部,检查模块内部的状态;并发断言放在接口或验证组件里,检查模块间的交互。这样分工明确,调试起来也方便。

好了,关于立即断言,咱们就聊这么多。说白了,它就是你在代码里埋下的一个个"地雷"——一旦踩到,立刻爆炸,帮你快速定位问题。下一章咱们聊并发断言,那个就更有意思了。