立即断言:从入门到实战
好,咱们今天聊聊立即断言。说实话,很多刚接触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处理就好。
我的习惯:在验证环境里,我会把立即断言和并发断言分开管理。立即断言放在模块内部,检查模块内部的状态;并发断言放在接口或验证组件里,检查模块间的交互。这样分工明确,调试起来也方便。
好了,关于立即断言,咱们就聊这么多。说白了,它就是你在代码里埋下的一个个"地雷"——一旦踩到,立刻爆炸,帮你快速定位问题。下一章咱们聊并发断言,那个就更有意思了。