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验证,我总结了几条铁律:

  1. 先写断言,再写测试用例。断言是验证的骨架,测试用例是血肉。骨架立起来了,血肉才能长上去。
  2. 覆盖率要盯紧。AXI协议的覆盖率点很多,比如burst类型、burst长度、地址对齐、ID乱序等。我一般用UVM的covergroup来收集。
  3. 随机化要加约束。纯随机生成的事务,大概率是无效的。一定要加合理的约束,让事务贴近实际使用场景。
  4. 别忘了复位场景。很多AXI的bug都是在复位过程中暴露的。我建议专门写一个复位测试用例,在复位过程中发起事务。

最后说一句,AXI验证没有捷径。老老实实把协议读透,把断言写全,把覆盖率做满。这样流片回来,你才能睡个安稳觉。