4. SystemVerilog基础(下):接口(Interface)、包(Package)、断言(Assertion)基础

好,咱们接着往下聊。上一章我们把SV的数据类型、过程块和面向对象撸了一遍。这一章,我重点讲三个在验证环境里天天都要用的东西:接口断言

这三个东西,说白了就是帮你把验证代码写得更干净、更可复用、更智能。我刚开始做验证那会儿,没有接口的概念,所有信号都靠端口一层层传,改一个模块名能改到手抽筋。后来用了接口,嗯,真香。

4.1 接口(Interface)—— 把连线打包

接口是什么?你想想看,以前写Verilog,模块之间通信要一个个端口列出来。如果总线有50根信号,你得写50次。而且一旦信号名变了,所有用到的地方都得改。

接口就是把这些信号打包成一个“插头”。你只需要定义一次,到处都能用。

4.1.1 接口的定义与使用

interface axi_if (input clk, input rst_n);
    logic [31:0] awaddr;
    logic [7:0]  awlen;
    logic        awvalid;
    logic        awready;
    logic [31:0] wdata;
    logic        wvalid;
    logic        wready;
    logic [1:0]  rresp;
    logic [31:0] rdata;
    logic        rvalid;
    logic        rready;

    // 可以定义modport来区分方向
    modport master (
        output awaddr, awlen, awvalid,
        input  awready,
        output wdata, wvalid,
        input  wready,
        input  rresp, rdata, rvalid,
        output rready
    );

    modport slave (
        input  awaddr, awlen, awvalid,
        output awready,
        input  wdata, wvalid,
        output wready,
        output rresp, rdata, rvalid,
        input  rready
    );
endinterface

你看,一个AXI接口,所有信号都在里面。master和slave的modport定义了方向。这样在顶层连线时,代码会非常清爽。

个人习惯:我一般会在接口里把modport定义好,这样谁用谁选。而且我建议把时钟和复位也放在接口里,省得每个模块都要传一遍。

4.1.2 接口在验证环境中的典型用法

在验证环境里,接口通常连接DUT和driver/monitor。举个例子:

// 在testbench顶层
axi_if axi_bus0 (clk, rst_n);

my_dut dut (
    .axi_if_0 (axi_bus0.slave)
);

my_driver drv;
drv.axi_vif = axi_bus0.master;

my_monitor mon;
mon.axi_vif = axi_bus0.master;

这里有个坑,我提醒一下:接口是引用类型。你在class里声明的是virtual interface,否则会报错。我曾经有个同事,忘了加virtual,仿真跑了一天发现数据全是X,排查了整整一个下午。

注意:在class中使用接口时,必须声明为 virtual interface_name。否则你拿到的只是一个拷贝,不是真正的硬件连线。

4.2 包(Package)—— 把公共代码收起来

包的作用,说白了就是代码的收纳盒。你把常用的类型定义、函数、任务、常量都放在包里,谁用谁import。

4.2.1 包的声明与导入

package axi_pkg;
    // 参数定义
    parameter int AXI_ADDR_WIDTH = 32;
    parameter int AXI_DATA_WIDTH = 64;

    // 枚举类型
    typedef enum logic [1:0] {
        OKAY   = 2'b00,
        EXOKAY = 2'b01,
        SLVERR = 2'b10,
        DECERR = 2'b11
    } axi_resp_e;

    // 函数
    function automatic string resp_to_string(axi_resp_e resp);
        case (resp)
            OKAY:   return "OKAY";
            EXOKAY: return "EXOKAY";
            SLVERR: return "SLVERR";
            DECERR: return "DECERR";
        endcase
    endfunction

    // 任务
    task automatic wait_for_ready(
        ref logic ready,
        input int timeout = 1000
    );
        repeat (timeout) @(posedge ready);
    endtask
endpackage

// 使用时
import axi_pkg::*;
axi_resp_e resp;
resp = OKAY;

我个人习惯把包分成几类:类型包(放typedef和参数)、函数包(放通用函数)、配置包(放环境配置参数)。这样找起来方便。

4.2.2 包的作用域与冲突处理

如果两个包里定义了同名的类型,怎么办?嗯,这确实是个问题。我建议:

  • 用包名限定:axi_pkg::OKAYahb_pkg::OKAY 不会冲突
  • 不要用 import pkg::* 通配导入,除非你确定没有重名
  • 如果必须通配,可以用 import pkg::* except pkg::conflict_name
小技巧:在验证环境里,我通常只在testbench顶层用通配导入。在class文件里,我会用具体的导入,比如 import axi_pkg::axi_resp_e。这样代码的依赖关系一目了然。

4.3 断言(Assertion)—— 让代码自己检查自己

断言是什么?说白了就是在仿真时自动检查信号是否满足预期。你不用再手动拉波形、肉眼比对。断言会帮你盯着,一旦出问题就报错。

我刚开始做验证时,觉得断言可有可无。直到有一次,一个时序问题在仿真里跑了三天才暴露出来,而如果当时写了断言,第一天就能发现。从那以后,我再也不敢轻视断言了。

4.3.1 立即断言 vs 并发断言

类型 语法 特点 使用场景
立即断言 assert (condition) 在过程块中执行,立即检查 检查函数返回值、状态机状态等
并发断言 assert property (property_expr) 基于时钟沿,持续检查 检查时序关系、协议规范

举个例子,立即断言:

always_ff @(posedge clk) begin
    if (state == READ) begin
        assert (valid == 1) else
            $error("在READ状态时,valid必须为高");
    end
end

并发断言:

// 检查:当valid拉高后,ready必须在3个时钟内拉高
property p_valid_ready;
    @(posedge clk)
    valid |-> ##[1:3] ready;
endproperty

assert property (p_valid_ready)
    else $error("valid拉高后,ready未在3个时钟内响应");

4.3.2 断言中的常用操作符

这里我列几个最常用的,你记一下:

  • ##n:延迟n个时钟周期
  • ##[m:n]:延迟m到n个时钟周期
  • |->:蕴含(如果左边成立,右边必须成立)
  • |=>:蕴含(左边成立后,下一个时钟周期右边必须成立)
  • andornot:逻辑组合
  • $rose(signal):信号上升沿检测
  • $fell(signal):信号下降沿检测
  • $stable(signal):信号保持不变
避坑指南:我曾经写过一个断言,检查写地址和写数据必须同时有效。我用了 awvalid && wvalid,结果发现仿真时总是报错。后来排查发现,AXI协议允许写地址和写数据在不同周期有效。所以写断言前,一定要先搞清楚协议规范。

4.3.3 断言在验证环境中的部署

我一般把断言放在三个地方:

  1. 接口里:检查协议层面的时序约束,比如握手协议
  2. DUT内部:检查内部状态机的合法性、数据通路的一致性
  3. 验证环境里:检查driver/monitor的行为是否正确

举个例子,在接口里写断言:

interface axi_if (input clk, input rst_n);
    // ... 信号定义 ...

    // 断言:awvalid和awready不能同时为高超过1个周期
    property p_aw_handshake;
        @(posedge clk)
        awvalid && awready |-> ##1 !(awvalid && awready);
    endproperty

    assert property (p_aw_handshake)
        else $error("AW握手信号异常");
endinterface

4.4 本章小结

接口、包、断言,这三个东西是SV验证的三大基石。接口帮你简化连线,包帮你管理代码,断言帮你自动检查

我个人建议,在搭建验证环境时:

  • 先把接口定义好,modport分清楚
  • 把公共类型和函数收进包
  • 在接口和DUT里埋好断言

这样你的环境会非常健壮。嗯,下一章我们开始真正搭建验证环境,到时候这些东西都会用上。

SystemVerilog基础(下)知识体系 接口(Interface) 包(Package) 断言(Assertion) 信号打包 + modport方向控制 virtual interface 引用传递 连接DUT与验证组件 类型定义 + 参数常量 函数/任务封装 import作用域管理 立即断言(过程块内) 并发断言(基于时钟) 蕴含操作符 + 时序检查 目标:搭建可复用、自检查的验证环境

专注资料整理