3. 断言实战:assert property在UVM中的嵌入、断言覆盖率收集、断言与scoreboard的联动、常见断言编写错误
断言这东西,说白了就是给设计加个「电子警察」。你想想看,仿真跑了几百万个周期,靠人眼去波形里找bug?不现实。断言能帮我们在违规发生的第一时间抓住它,而且精确到哪个周期、哪个信号。
我个人习惯,在项目一开始就把断言框架搭好。别等到验证后期再补,那时候改RTL的成本太高了。好,我们直接进入正题。
3.1 assert property在UVM中的嵌入方式
SystemVerilog的断言有两种:立即断言和并发断言。在UVM里,我们主要用并发断言,也就是assert property。嵌入方式有三种,我一个个说。
方式一:直接在interface里写
这是最常用的做法。把断言写在interface里,跟接口信号绑定在一起。UVM的driver和monitor通过virtual interface来驱动和采样,断言自然就能看到所有信号变化。
interface my_bus_if (input clk, input rst_n);
logic [31:0] addr;
logic valid;
logic ready;
// 断言:valid拉高后,ready必须在3个周期内拉高
property p_valid_to_ready;
@(posedge clk) disable iff (!rst_n)
valid |-> ##[1:3] ready;
endproperty
assert property(p_valid_to_ready)
else $error("valid拉高后ready未在3周期内响应");
endinterface
我在项目中遇到过一个问题:把断言写在module里,结果UVM的virtual interface绑定时出了岔子。后来统一改到interface里,世界清净了。
方式二:在UVM component里用`uvm_assert
UVM也提供了宏来包装断言,比如`uvm_assert。这种方式的好处是能跟UVM的报告机制整合,打印的信息更规范。
class my_monitor extends uvm_monitor;
virtual my_bus_if vif;
task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
`uvm_assert(vif.valid & vif.ready,
"valid和ready同时拉高,传输完成")
end
endtask
endclass
不过说实话,我个人不太喜欢这种方式。因为断言本身是声明式的,写成过程代码反而失去了「自动检查」的味道。你想想看,用assert property是硬件思维,用`uvm_assert是软件思维,混在一起容易乱。
方式三:用bind语句注入
这是最优雅的方式。不改动原有代码,通过bind把断言模块「贴」到设计上。适合在验证后期追加检查点。
bind dut my_assert_module assert_inst (.clk(clk), .rst_n(rst_n), .*);
嗯,这里要注意:bind的模块里不能有initial块,否则仿真器会报错。我吃过这个亏,排查了半天。
3.2 断言覆盖率收集
断言覆盖率,说白了就是「你的断言被触发了多少次」。这个指标很重要。如果某个断言从来没被触发过,有两种可能:要么设计没问题,要么你的测试根本没跑到那个场景。
UVM里收集断言覆盖率,我一般用两种方法。
方法一:利用仿真器的内置功能
大多数仿真器(VCS、Xcelium、Questa)都支持自动收集断言覆盖率。你只需要在仿真命令里加个选项:
# VCS
vcs -assert coverage -cm assert ...
# Questa
vsim -assertcover ...
仿真结束后,会生成覆盖率数据库。用verdi或simvision打开,能看到每个断言被hit了多少次。
方法二:在UVM里手动收集
有时候我们需要在仿真过程中实时知道断言覆盖率。我会在scoreboard里维护一个覆盖率表。
class coverage_collector extends uvm_component;
int assert_hit_count[string];
function void report_phase(uvm_phase phase);
foreach(assert_hit_count[name]) begin
`uvm_info(get_type_name(),
$sformatf("断言 %s 被触发 %0d 次",
name, assert_hit_count[name]),
UVM_MEDIUM)
end
endfunction
endclass
我曾经在一个项目里发现,某个关键断言覆盖率一直是0。查了半天,原来是测试用例的约束写错了,导致那个场景永远跑不到。所以断言覆盖率真的能帮你发现测试的盲区。
3.3 断言与scoreboard的联动
断言和scoreboard,一个抓时序违规,一个抓数据错误。两者配合,才能全面覆盖。
我常用的联动方式是这样的:断言发现违规后,直接通知scoreboard,让scoreboard标记当前事务为「无效」或「异常」。
class my_scoreboard extends uvm_scoreboard;
event assert_fail_event;
task run_phase(uvm_phase phase);
forever begin
@(assert_fail_event);
// 断言失败,标记当前事务
current_tr.status = FAILED;
// 可以在这里记录日志或触发额外检查
end
endtask
function void notify_assert_fail();
-> assert_fail_event;
endfunction
endclass
然后在断言里调用这个函数:
assert property (p_valid_to_ready)
else begin
$error("断言失败");
scoreboard.notify_assert_fail();
end
为什么要联动?因为有时候断言报错了,但scoreboard还在傻傻地比对数据。你想想看,时序都错了,数据比对还有意义吗?直接标记异常,避免误报。
3.4 常见断言编写错误
写断言看着简单,坑其实不少。我列几个最常见的,都是血泪教训。
| 错误类型 | 错误示例 | 正确写法 | 说明 |
|---|---|---|---|
| 忘记disable iff | @(posedge clk) a |-> b |
@(posedge clk) disable iff (!rst_n) a |-> b |
复位期间断言会误报 |
| 时序范围写反 | ##[3:1] |
##[1:3] |
范围必须从小到大 |
| 组合逻辑里用断言 | @(*) a |-> b |
改用@(posedge clk) |
组合逻辑断言容易产生毛刺误报 |
| $past使用不当 | $past(a, 0) |
$past(a, 1) |
$past的第二个参数最小是1 |
$past(a, 2),但没考虑时钟沿对齐问题。结果仿真波形上看a确实满足条件,但断言就是报错。后来发现是$past取的是两个时钟周期前的值,而我的信号是在时钟下降沿变化的。记住,$past默认是相对于采样沿的。
3.5 本章小结
断言这东西,写起来不难,但写好需要经验。我建议你从简单的开始,比如先给接口加上valid-ready握手检查。跑通了再慢慢加复杂的时序约束。
另外,断言覆盖率一定要看。不看覆盖率,你都不知道自己的断言是不是在「自娱自乐」。嗯,今天就到这里,下一章我们聊形式化验证和断言的结合。
- 断言嵌入推荐用interface或bind方式
- 断言覆盖率是测试质量的「照妖镜」
- 断言和scoreboard联动,避免无效比对
- 常见错误:忘记disable iff、时序范围写反、$past参数错误