第1章:立即断言——让Bug无处遁形的第一道防线
各位做验证的同行,大家好。今天我们来聊聊断言验证中最基础、也最实用的一类——立即断言。
我记得刚入行那会儿,带我的老工程师跟我说过一句话:「断言不是用来证明设计是对的,而是用来证明设计是错的。」这句话我一直记到现在。说白了,断言就是我们在代码里埋下的「哨兵」,一旦发现异常情况,立刻拉响警报。
1.1 立即断言语法
立即断言的语法其实非常简单,就一个关键字:assert。它的基本格式长这样:
assert (expression) [else $fatal|$error|$warning|$info("message")];
这里有几个要点:
- expression:你要检查的条件表达式,必须为真(1'b1)才算通过
- else 分支:可选,但强烈建议加上。当断言失败时,执行后面的系统任务
- 系统任务:$fatal(终止仿真)、$error(报错)、$warning(警告)、$info(信息)
核心要点:立即断言是过程性语句,必须放在 always 块、initial 块或 task/function 中执行。它不像并发断言那样有采样时钟的概念,而是「执行到这一行,立刻检查」。
来看一个最简单的例子:
always @(posedge clk) begin
if (data_valid) begin
assert (data_ready) else $error("data_valid asserted but data_ready is low!");
end
end
这段代码的意思是:在每个时钟上升沿,如果 data_valid 拉高,那么 data_ready 必须也为高。否则,仿真器会打印错误信息。
1.2 立即断言的使用场景
我在项目中用过很多次立即断言,总结下来主要有这么几个场景:
| 场景 | 说明 | 我的经验 |
|---|---|---|
| 接口协议检查 | 在握手信号、控制信号之间检查时序关系 | 曾经在AXI总线验证中,用立即断言抓到了一个地址非对齐的Bug |
| 状态机非法状态 | 检查状态机是否进入了未定义状态 | 有一次状态机跳飞了,就是靠断言第一时间发现的 |
| 数据完整性 | 检查数据是否在预期范围内,或者是否满足某种约束 | 比如FIFO空时不能读,满时不能写 |
| 配置参数合法性 | 在仿真开始时检查配置参数是否合理 | 我习惯在initial块里加一堆assert,防止配错参数 |
小技巧:我个人习惯在 initial 块里放一组「启动检查」断言。比如检查复位时长是否足够、时钟频率是否在范围内。这些检查在仿真一开始就执行,能避免跑了大半天才发现配置错了的尴尬。
1.3 立即断言示例
光说不练假把式,我们来看几个实际项目中会用到的例子。
示例1:FIFO空满检查
module fifo_checker (
input logic clk,
input logic rst_n,
input logic wr_en,
input logic rd_en,
input logic full,
input logic empty
);
// 满时不能写
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时不检查
end else begin
assert (!(full && wr_en)) else $error("FIFO full but write asserted!");
end
end
// 空时不能读
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时不检查
end else begin
assert (!(empty && rd_en)) else $error("FIFO empty but read asserted!");
end
end
endmodule
嗯,这里要注意:复位期间我们跳过了检查,因为复位时信号状态不确定,容易误报。
示例2:状态机非法状态检查
typedef enum logic [2:0] {IDLE, READ, WRITE, DONE} state_t;
state_t current_state, next_state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
current_state <= IDLE;
end else begin
current_state <= next_state;
end
end
// 检查当前状态是否合法
always @(current_state) begin
assert (current_state inside {IDLE, READ, WRITE, DONE})
else $fatal("Illegal state detected: %0d", current_state);
end
我曾经在一个项目中遇到过状态机因为综合优化问题跳到了3'b101这个非法值。如果没有这个断言,仿真可能继续跑,但结果全是错的。断言一触发,$fatal直接停仿真,省了我至少两天调试时间。
示例3:参数合法性检查
module config_checker #(
parameter int DATA_WIDTH = 32,
parameter int FIFO_DEPTH = 16
)();
initial begin
// 数据宽度必须是2的幂
assert (DATA_WIDTH % 2 == 0)
else $warning("DATA_WIDTH should be even, got %0d", DATA_WIDTH);
// FIFO深度至少为4
assert (FIFO_DEPTH >= 4)
else $error("FIFO_DEPTH too small: %0d", FIFO_DEPTH);
// 深度必须是2的幂(方便地址译码)
assert ($onehot(FIFO_DEPTH))
else $error("FIFO_DEPTH must be power of 2, got %0d", FIFO_DEPTH);
end
endmodule
避坑指南:我曾经犯过一个错误——在断言里用了 $fatal 但没加条件判断。结果仿真一跑,复位还没结束就触发了断言,整个仿真直接崩了。所以记住:复位期间一定要屏蔽断言检查,或者用 if (!rst_n) 跳过。
知识体系总览
下面这张图总结了本章的核心内容,方便你快速回顾:
最后说一句:立即断言虽然简单,但它是验证工程师最趁手的工具之一。我个人的经验是——能用断言的地方,绝不用if-else打印。断言不仅帮你发现问题,更重要的是它把问题定位到了具体的行和时刻,调试效率能提升好几倍。
好了,这一章就到这里。记住:写断言不是给仿真器看的,是给你自己看的。你埋下的每一个断言,都是在为未来的自己省时间。