第二讲:SystemVerilog断言基础

断言这东西,说白了就是给仿真器装上一双「眼睛」。我刚开始做验证那会儿,还不太重视断言,总觉得靠波形抓bug就够了。后来有一次,一个时序违例在波形上藏得特别深,折腾了两天才定位到——从那以后,我再也不敢小看断言了。

2.1 立即断言 vs 并发断言

SystemVerilog里断言分两种:立即断言和并发断言。很多新手容易搞混,我简单说说它们的区别。

2.1.1 立即断言

立即断言,顾名思义,执行到这一行就立刻检查。它跟$display一样,是过程性语句,写在always块或initial块里。

always @(posedge clk) begin
    if (valid) begin
        assert (data != 0) else $error("data不能为0!");
    end
end

嗯,这里要注意:立即断言没有时间概念。它只检查当前时刻的条件是否成立。我在项目中遇到过有人拿立即断言去检查跨时钟域的信号,结果误报一大堆——因为信号还没同步过来呢。

2.1.2 并发断言

并发断言就不一样了。它基于时钟沿,可以描述跨越多个时钟周期的行为。这才是验证大杀器。

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

你看,这个断言说的是:req拉高之后,1到3个时钟周期内ack必须拉高。这种时序关系,用立即断言根本写不了。

核心区别一句话总结:立即断言检查「此刻」,并发断言检查「一段时间内」。

2.2 assert / assume / cover 的区别

这三个关键字长得像,但用途完全不同。我刚开始学的时候也经常搞混,后来用多了才真正理解。

关键字 用途 失败时 典型场景
assert 验证设计行为是否正确 报错 检查协议、时序、数据完整性
assume 约束输入环境 约束违反 形式验证、激励约束
cover 收集覆盖率 不报错 检查某些场景是否被覆盖到

2.2.1 assert — 你的看门狗

assert是最常用的。它检查设计是否做了「该做的事」。比如握手协议、FIFO满空标志、状态机跳转合法性等等。

// 检查:写使能时,不能同时读写同一地址
assert property (
    @(posedge clk) 
    write_en && read_en |-> (write_addr != read_addr)
);

我曾经在一个DMA控制器项目里,靠assert抓到了一个地址越界的bug。那个bug在功能仿真里完全看不出来,但assert直接报了出来——省了我至少一周的调试时间。

2.2.2 assume — 给环境立规矩

assume是用来约束输入环境的。它告诉工具:我的输入信号必须满足某些条件。形式验证里特别常用。

// 假设:输入数据不能为X态
assume property (
    @(posedge clk) 
    !$isunknown(data_in)
);

嗯,这里有个坑:assume在仿真里和assert行为类似,但在形式验证里,assume是约束,assert才是检查目标。我见过有人把assume当assert用,结果形式验证跑出来一堆假反例。

2.2.3 cover — 看看你走了哪些路

cover是用来收集覆盖率的。它不报错,只记录某个事件是否发生过。

// 覆盖:是否出现过背靠背传输
cover property (
    @(posedge clk) 
    valid && ready ##1 valid && ready
);

我个人习惯在写assert的同时,顺手加上cover。这样既能检查正确性,又能知道哪些场景真的被测试到了。你想想看,如果某个断言从来没被触发过,那它等于白写了。

我的经验:一个完整的断言组应该包含assert + cover。assert告诉你「有没有错」,cover告诉你「有没有测到」。

2.3 断言在覆盖率收集中的作用

说到覆盖率,很多人第一反应是代码覆盖率、功能覆盖率。但断言覆盖率其实是个被低估的利器。

2.3.1 断言覆盖率的独特价值

功能覆盖率需要你手动定义covergroup和coverpoint,写起来挺麻烦的。但断言覆盖率不一样——你写一个cover property,工具自动帮你统计这个事件触发了多少次。

// 自动统计:这个断言被触发了多少次
cover property (
    @(posedge clk) 
    fifo_full ##1 fifo_not_full
);

我在项目中经常用断言覆盖率来回答一个问题:「这个边界条件到底测到了没有?」比如FIFO满之后又读走一个数据,这种场景如果cover property显示为0,那说明你的测试用例还不够。

2.3.2 断言覆盖率 vs 功能覆盖率

对比项 断言覆盖率 功能覆盖率
定义方式 cover property covergroup + coverpoint
粒度 事件级(时序序列) 数据级(值的分布)
自动程度 较高 需要手动定义bin
典型用途 协议序列、握手场景 数据范围、状态组合

说白了,断言覆盖率擅长检查「有没有发生过某个序列」,功能覆盖率擅长检查「数据值有没有覆盖到某个范围」。两者互补,缺一不可。

2.3.3 实战中的覆盖率策略

我个人习惯的做法是这样的:

  1. 先写assert:把关键协议、时序约束都写成断言
  2. 顺手加cover:每个assert旁边加一个对应的cover property
  3. 跑完回归看报告:检查哪些cover property没被触发
  4. 补充测试用例:针对没触发的场景,写新的测试

避坑指南:我曾经在一个项目里发现,某个cover property覆盖率是100%,但对应的assert从来没报过错。后来一查,原来那个assert的使能条件写错了,导致它根本没生效。所以,cover property触发率高不代表assert就有效——你得确保assert本身是正确的。

2.4 本章知识体系

下面这张图总结了本章的核心内容,方便你快速回顾:

SystemVerilog断言基础 立即断言 并发断言 断言覆盖率 特点 过程性语句 检查当前时刻 写在always/initial中 特点 基于时钟沿 跨周期检查 property + sequence 特点 cover property 自动统计触发次数 补充功能覆盖率 三个关键字:assert(检查正确性) | assume(约束环境) | cover(收集覆盖率) 核心思想:断言是验证的「眼睛」——看得见时序、抓得住边界、测得到覆盖率

好了,这一章的内容就到这里。断言这东西,刚开始用可能会觉得麻烦,但用顺手了你会发现——它真的是验证效率的倍增器。下一章我们聊聊断言的高级用法,包括序列组合、多时钟域断言这些实战中经常遇到的内容。

一句话记住本章:assert抓bug,assume立规矩,cover看覆盖——三者配合,验证无忧。

可可精品资料库