4、接口协议验证:AXI协议详解、读写事务生成、时序检查、断言(Assertion)编写
AXI协议,说白了就是芯片内部的高速公路。我做了这么多年验证,最怕听到的就是“AXI协议没搞清楚”。为什么?因为它是SoC的命脉,CPU、DMA、DDR控制器全挂在这条总线上。一旦出问题,整个芯片就瘫痪了。
4.1 AXI协议核心要点
AXI协议是AMBA家族里的老大哥。它最牛的地方在于——分离的地址/数据通道。什么意思?就是读地址、读数据、写地址、写数据、写响应,这五条通道各走各的。你想想看,这就像五条高速公路并行,效率能不高吗?
我个人习惯把AXI协议的关键点归纳为三个:
- 握手信号:VALID和READY,缺一不可。VALID表示发送方数据准备好了,READY表示接收方能收了。两者同时为高,事务才算完成。
- 突发传输:一次地址请求,可以传多笔数据。Burst长度从1到256,看你的配置。
- 乱序传输:不同ID的事务可以乱序完成。这是AXI最强大的特性,也是最容易踩坑的地方。
重要提醒:AXI协议里,VALID信号不能依赖READY信号。也就是说,发送方必须无条件地把VALID拉起来,不能等READY来了再拉。这个规则很多人会搞反。
4.2 读写事务生成
验证AXI接口,第一步就是生成各种读写事务。我一般用SystemVerilog的类来封装事务,这样复用性最好。
先看一个写事务的例子:
class axi_write_transaction extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [2:0] burst_type; // 0: FIXED, 1: INCR, 2: WRAP
rand bit [7:0] burst_len; // 实际长度 = burst_len + 1
rand bit [3:0] wstrb; // 写选通信号
rand bit [31:0] data[]; // 数据数组
constraint valid_burst {
burst_len inside {[0:15]}; // 支持1到16笔数据
data.size() == burst_len + 1;
}
constraint aligned_addr {
addr[1:0] == 2'b00; // 地址对齐
}
endclass
读事务类似,只是没有写数据和写选通:
class axi_read_transaction extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [2:0] burst_type;
rand bit [7:0] burst_len;
constraint valid_burst {
burst_len inside {[0:15]};
}
endclass
我在项目中遇到过一个问题:某个DMA控制器只支持INCR模式,但测试用例生成了WRAP模式的事务。结果仿真跑了两天,才发现是约束没写对。所以,约束一定要写严谨,特别是burst_type这种关键字段。
小技巧:生成事务时,建议把边界条件都覆盖到。比如burst_len=0(单笔传输)、burst_len=15(最大长度)、地址对齐和不对齐的情况。这些边界最容易暴露设计bug。
4.3 时序检查
时序检查,说白了就是看信号之间的时间关系对不对。AXI协议对时序有明确要求,比如:
- VALID拉高后,必须保持到READY为高
- READY可以提前拉高,但不能在VALID为低时采样数据
- BVALID必须在WLAST和WVALID都有效之后才能拉高
我一般用SystemVerilog的assertion来做时序检查。举个例子:
// 检查写响应通道的时序
property write_response_timing;
@(posedge aclk)
bvalid |-> ##[0:$] bready;
endproperty
assert_write_response: assert property(write_response_timing)
else $error("写响应超时:BVALID拉高后BREADY未及时响应");
嗯,这里要注意。时序检查不能只做正向的,还要做负向的。什么叫负向?就是检查不应该发生的事情。比如:
// 检查WLAST是否在写数据结束前拉高
property wlast_before_last_data;
@(posedge aclk)
wvalid && wready && wlast |-> wlast_asserted;
endproperty
assert_wlast_timing: assert property(wlast_before_last_data)
else $error("WLAST时序错误:最后一笔数据未完成时WLAST已拉高");
我曾经因为没检查这个,导致一个IP在流片后才发现写数据会多写一笔。那叫一个惨啊,改版费花了十几万。
4.4 断言(Assertion)编写
断言是验证的“眼睛”。没有断言,你根本不知道设计有没有跑偏。我写断言的原则是:宁可错杀一千,不可放过一个。
AXI协议里,我建议至少写以下几类断言:
| 断言类型 | 检查内容 | 示例 |
|---|---|---|
| 协议合规 | 信号时序是否符合AXI规范 | VALID不能依赖READY |
| 数据完整性 | 写数据和读数据是否一致 | WSTRB与WDATA的对应关系 |
| 地址范围 | 访问地址是否在合法范围内 | 地址不能超出Slave的地址空间 |
| ID管理 | 乱序传输的ID是否正确 | 读返回的ID必须与请求的ID一致 |
来看一个完整的断言例子:
// 检查写地址通道的协议合规性
module axi_assertions (
input logic aclk,
input logic aresetn,
input logic awvalid,
input logic awready,
input logic [31:0] awaddr,
input logic [2:0] awburst,
input logic [7:0] awlen
);
// 断言1:AWVALID不能依赖AWREADY
property awvalid_independent;
@(posedge aclk)
disable iff (!aresetn)
awvalid |-> $rose(awvalid);
endproperty
assert_awvalid_indep: assert property(awvalid_independent)
else $error("AWVALID不能依赖AWREADY");
// 断言2:AWLEN不能超过15
property awlen_range;
@(posedge aclk)
disable iff (!aresetn)
awvalid && awready |-> awlen <= 8'd15;
endproperty
assert_awlen_range: assert property(awlen_range)
else $error("AWLEN超过最大值15");
// 断言3:地址对齐检查
property addr_aligned;
@(posedge aclk)
disable iff (!aresetn)
awvalid && awready && (awburst != 2'b00) |-> awaddr[1:0] == 2'b00;
endproperty
assert_addr_aligned: assert property(addr_aligned)
else $error("INCR/WRAP模式的地址必须对齐");
endmodule
避坑指南:我曾经在项目中遇到一个情况——断言写得太多,导致仿真速度慢得离谱。后来发现是某个断言用了$past函数,采样深度设成了100。所以,断言不是越多越好,要精准打击。每个断言都要问自己:这个断言能抓到什么bug?抓不到就删掉。
4.5 实战经验总结
做了这么多年AXI验证,我总结了几条铁律:
- 先写断言,再写测试用例。断言是验证的骨架,测试用例是血肉。骨架立起来了,血肉才能长上去。
- 覆盖率要盯紧。AXI协议的覆盖率点很多,比如burst类型、burst长度、地址对齐、ID乱序等。我一般用UVM的covergroup来收集。
- 随机化要加约束。纯随机生成的事务,大概率是无效的。一定要加合理的约束,让事务贴近实际使用场景。
- 别忘了复位场景。很多AXI的bug都是在复位过程中暴露的。我建议专门写一个复位测试用例,在复位过程中发起事务。
最后说一句,AXI验证没有捷径。老老实实把协议读透,把断言写全,把覆盖率做满。这样流片回来,你才能睡个安稳觉。