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定义了方向。这样在顶层连线时,代码会非常清爽。
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,排查了整整一个下午。
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::OKAY和ahb_pkg::OKAY不会冲突 - 不要用
import pkg::*通配导入,除非你确定没有重名 - 如果必须通配,可以用
import pkg::* except pkg::conflict_name
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个时钟周期|->:蕴含(如果左边成立,右边必须成立)|=>:蕴含(左边成立后,下一个时钟周期右边必须成立)and、or、not:逻辑组合$rose(signal):信号上升沿检测$fell(signal):信号下降沿检测$stable(signal):信号保持不变
awvalid && wvalid,结果发现仿真时总是报错。后来排查发现,AXI协议允许写地址和写数据在不同周期有效。所以写断言前,一定要先搞清楚协议规范。
4.3.3 断言在验证环境中的部署
我一般把断言放在三个地方:
- 接口里:检查协议层面的时序约束,比如握手协议
- DUT内部:检查内部状态机的合法性、数据通路的一致性
- 验证环境里:检查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里埋好断言
这样你的环境会非常健壮。嗯,下一章我们开始真正搭建验证环境,到时候这些东西都会用上。