简介:针对AXI协议验证中的乱序返回与交错读取需求,这套项目代码面向使用AXI slave VIP的验证工程师,重点解决out of order read data和interleaved read data的高级配置问题。资源内提供read_data_reordering_depth、read_data_interleave_size等属性的设置思路,并给出enable_interleave、interleave_pattern及SVT_AXI_MAX_READ_DATA_REORDERING_DEPTH宏定义示例,便于快速搭建多通道并发访问的验证环境。压缩包共3个文件,包含HTML说明文档、inscode工程入口及gitignore配置,包体仅7KB,轻量且便于直接参考。目前已有273人学习/下载,适合熟悉AXI总线协议、希望优化存储或内存控制器验证场景的软件开发与验证人员。
1. AXI VIP 配置 interleaving:为什么一上来就要问深度和边界
AXI VIP 配置 interleaving 这件事,看着是改几个参数,实际上决定了你的验证环境能不能发现总线互联里的数据乱序问题。大多数验证人员默认把 interleaving 当成一个布尔开关,打开就完事,结果仿真跑了几万周期,DUT 里真正会发生的交织场景在环境里根本没被激励过。我在一个总线互联验证项目里就翻过车:A 同学把 ID 宽度从 8 改成 12 之后,仿真在 3 万周期附近直接死锁,排了三天才发现 VIP 默认不交织,读数据永远按发出顺序返回,outstanding 配得再大也只是排队,不是并发。
这篇文章会把 interleaving 背后的协议约束、UVM 环境里的配置方法、参数之间的关联关系一次讲透。你会在第 3 章看到可直接改用的代码,第 4 章是四条常见的翻车现场和对应解法。适合被 VIP 玄学卡过的验证工程师,也适合刚接手 AXI 总线验证、想少走弯路的新手。配置 interleaving 不是让环境变得更复杂,而是让激励更接近硬件真实行为,这一步绕不开。
2. 先分清协议语义再动手:AXI interleaving 改的是数据与响应的顺序
2.1 读数据交织与写数据交织:两条通道的不同约束
AXI 协议里的 interleaving 出现在数据通道和响应通道,地址通道没有这个概念。读数据通道是协议明确允许交织的:主设备可以同时发出多笔读事务,从设备返回读数据时,允许在不同事务的 beat 之间穿插返回。比如事务 A 和事务 B 都是 4 拍突发,合法的返回顺序可以是 A0、B0、B1、A1、A2、B2、A3、B3,只要每个事务内部 beat 序号保持递增,就不算违例。但注意一个硬约束:同一事务 ID 的所有 beat 必须按照 burst 顺序返回,不能把某个 ID 内部的顺序打乱。如果 DUT 把同一 ID 的两笔事务数据交叉返回,那不是合法的交织,属于协议违例。
写通道的情况不太一样。写数据依靠 WSTRB 区分每字节的有效性,所以不同写事务的 WDATA beat 理论上可以穿插发送,但 AXI 规范对写交织的允许程度比读通道保守得多,大多数 VIP 默认也不开启写数据交织。写响应通道则是按事务返回,每个事务一个 WRESP,交织体现在响应返回顺序上,与读数据交织的粒度完全不同。很多初次配置的人把读数据交织和写响应交织混在一起设置,结果 DUT 行为与预期完全对不上。
所以配置 interleaving 前先问自己一句:要验证的 DUT 是读通道支持交织,还是写通道支持交织,还是两者都支持?你需要分别在 VIP 配置里控制读写通道的交织能力。如果开关没分开,后续功能覆盖率的收集和违例上报都会乱掉。这里还有一层容易被忽视的细节:交织只对 INCR 和 WRAP 类型的突发有意义,FIXED 突发因为地址固定,交织后数据顺序的检查逻辑要单独处理,否则 scoreboard 会把合法的交织误判为数据错位。
2.2 与 outstanding、乱序返回的关系:配置前必须搞懂的三组参数
Interleaving 不是孤立存在的开关,它和另外两组参数强耦合。第一组是 outstanding 事务数,指主设备在收到响应之前最多可以发出的未完成事务数量。只有 outstanding 大于 1,交织才有存在的前提。如果 outstanding 配成 1,任意时刻总线上只有一笔事务,数据通道天然不可能交织。许多环境为了简化建模,默认把 outstanding 设成 1,这时候你去打开 interleaving 开关,sequence 产生的事务仍然是顺序返回,白配。
第二组是 ID 深度。AXI 事务依靠 ID 区分乱序和交织,如果 ID 位宽只有 2 位,就算 outstanding 配了 8,从设备最多也只能维护 4 个不同的 ID。VIP 配置里通常有一个并发 ID 数量参数,它决定 driver 能否同时跟踪多笔不同 ID 的事务。这个参数如果小于发起的 outstanding 数量,部分事务会被 VIP 内部排队,不会真正同时出现在总线上,交织场景也就测不出来。这里有一个经验公式方便估算:max_interleaving_depth <= max_outstanding <= 2^id_width。
第三组是数据缓存深度。VIP 内部需要用 RAM 缓存未返回的读数据,当交织允许的 beat 数超过缓存深度,VIP 为了不丢数据,可能强行把某些事务按顺序发送。商用 VIP 的配置项里通常会暴露 data_buffer_depth,它和 max_interleaving_depth 通常是一对。配置原则是 max_interleaving_depth 必须同时小于等于 data_buffer_depth 和 outstanding 数量。把这三组参数用约束写死在配置类里,比逐个字段手填更安全,因为手填最容易出现 depth 大于缓存深度这种配置合法但行为退化的组合。三组参数的关系如下表所示:
| 参数 | 作用 | 配错时的表现 |
|---|---|---|
| outstanding | 控制未决事务数 | 为 1 时交织不可能发生 |
| ID 位宽 | 决定可并行事务数量 | 太小导致 VIP 内部排队 |
| 数据缓存深度 | 暂存未返回的读数据 | 过小退化为顺序返回 |
2.3 VIP 里 interleaving 能力的建模:不是开关而是一组约束
商用的 AXI VIP 和常见的 UVM AXI agent,对 interleaving 的建模方式并不统一,但一般都会提供几个公共配置项:enable_read_interleaving、enable_write_interleaving、max_interleaving_depth。有些实现还提供 interleaving_mode,用来区分随机交织还是固定深度交织。这个 mode 会在第 3 章讲随机化策略时展开,这里先强调一个容易踩的坑:不要只把 enable 开关当成唯一入口。
我之前排查过一个仿真空洞就是典型例子。某开发者把 enable_read_interleaving 设成 1,但 max_interleaving_depth 用的默认值是 0。翻开源码才看到,driver 判断是否交织的条件是 enable 为真且 depth 大于 0。depth 为 0 时每次切换事务最多只能发一个 beat,数据返回顺序被强制频繁打散,反而制造了大量协议违例。这说明 depth 不是单纯描述“最多能交织多少笔”,它还参与决定交织是否实际生效。更深一层,VIP 对 interleaving 的建模本质是把协议里的三条约束翻译成 driver 和 scoreboard 的限制条件。driver 侧控制发出和返回事务的时序,scoreboard 侧根据 ID 和 burst 顺序检查数据完整性。配置时要同时告诉 VIP 允许交织到什么程度、以及检查端应该怎么处理乱序数据。如果只开了前者的开关,后者通常默认是顺序检查模式,交织产生的乱序数据会被误报为违例。这个对应关系非常重要,建议在配置时直接查找 VIP 的 data_check_mode 或 compare_mode 字段,把它设置成支持交织的模式,才算真正配完。
3. AXI VIP interleaving 配置实操:UVM 环境中的最小改动
3.1 创建 VIP 配置对象并打开读通道交织:树的叶子要传到位
在 UVM 环境里配置 AXI VIP 的标准路径是实例化一个配置类,把交织参数填好,再通过 uvm_config_db 设置到环境树上的指定节点。下面是一个最小可用的配置类,把三组参数的约束关系直接写进了 constraint:
class axi_vip_cfg extends uvm_object; `uvm_object_utils(axi_vip_cfg) bit enable_read_interleaving; bit enable_write_interleaving; int unsigned max_interleaving_depth; int unsigned max_outstanding_reqs; int unsigned data_buffer_depth; constraint c_interleave_cfg { max_interleaving_depth inside {[0:16]}; max_outstanding_reqs inside {[1:8]}; enable_read_interleaving == 1 -> max_interleaving_depth > 0; max_interleaving_depth <= data_buffer_depth; max_interleaving_depth <= max_outstanding_reqs; } function new(string name = "axi_vip_cfg"); super.new(name); enable_read_interleaving = 1; enable_write_interleaving = 0; max_interleaving_depth = 4; max_outstanding_reqs = 4; data_buffer_depth = 8; endfunction endclass这里三个约束条件就是第 2 章提到的三组关系。打开读交织时必须保证深度大于 0,否则开了一个实际不生效的开关;交织深度不能超过数据缓存;交织深度不能超过 outstanding 数。把约束写在同一个配置类里,比在 test 里分开赋值更不容易出错。写通道交织我在构造函数里默认关掉了,因为大多数 DUT 的写通道并不支持数据交织,开启反而会产生从设备永远不会响应的合法激励。
然后在 base_test 里把它传到环境树上。这里的关键是路径必须和实际层级一致,用通配符*确实省事,但如果环境里有多个 AXI 实例,通配符会把读交织开关错误传给写侧 agent。我一般写显式路径:
class base_test extends uvm_test; `uvm_component_utils(base_test) axi_vip_cfg m_axi_cfg; function void build_phase(uvm_phase phase); super.build_phase(phase); m_axi_cfg = axi_vip_cfg::type_id::create("m_axi_cfg"); uvm_config_db#(axi_vip_cfg)::set(this, "env.mst_agent.*", "vip_cfg", m_axi_cfg); endfunction endclass这两行配置的路径包含.*,表示在env.mst_agent下所有组件都能 get 到同一份配置。注意 set 的路径是相对当前组件的,test 层级不同,路径就要调整。如果路径配错,VIP 内部组件 get 不到配置,通常会静默使用默认值,表现为交织开关设了但行为完全没变。遇到这种情况,第一步先检查 config_db 的 set 和 get 路径是不是真的对上了,而不要急着怀疑 VIP 本身。
3.2 配置交织深度:max_interleaving_depth 怎么取才不踩协议红线
交织深度是配置里最容易被拍脑袋填的参数。常见做法是打开交织开关后用默认值,或者随手填一个 16,但深度不仅影响行为,还影响仿真时间和覆盖率收集的质量。取值的核心逻辑是让它接近 DUT 的真实能力,同时不超出协议合法范围。VIP 侧本身的限制有三条:缓存深度、outstanding 数、并发 ID 数。这三条之外,还要考虑 DUT 是什么类型的模块。下面给出一个可配置的 agent 从 config_db 获取配置并传递给内部组件的完整示例:
class axi_mst_agent extends uvm_agent; `uvm_component_utils(axi_mst_agent) axi_vip_cfg cfg; axi_mst_driver drv; axi_mst_monitor mon; function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(axi_vip_cfg)::get(this, "", "vip_cfg", cfg)) `uvm_fatal("AXI_CFG_MISS", "axi vip cfg not set to agent") drv = axi_mst_driver::type_id::create("drv", this); mon = axi_mst_monitor::type_id::create("mon", this); endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); drv.interleave_depth = cfg.enable_read_interleaving ? cfg.max_interleaving_depth : 0; mon.check_data_order = cfg.enable_read_interleaving; endfunction endclass这里在 connect_phase 里把深度传给 driver,同时把 monitor 的数据顺序检查模式与交织开关绑定。这样配置类负责约束和校验,agent 负责分发,driver 和 monitor 各管各的职责。后续如果要加交织相关的覆盖率收集,直接在 agent 里再引一次 cfg 就行,不需要动 VIP 内部源码。这种分层处理是调试效率的关键,因为当 bug 出现时,你可以快速定位是配置传错了还是检查端配置错了。
深度取值方面,如果 DUT 是简单的 AXI 到 AHB 桥,通常不支持读数据交织,把深度配成 4 会让 VIP 产生从设备永远不会支持的激励,等于给 testbench 加戏。如果验证的是带深度缓冲的 AXI 互联核,建议从头递进:depth=2 验证基本穿插,depth=4 验证多笔穿插,depth=8 以上配合延迟模型使用。如下表所示,是一个可供参考的取值对照:
| DUT 类型 | 建议 depth | 说明 |
|---|---|---|
| AXI 到 AHB 桥 | 0 或 2 | 桥内缺乏数据缓冲,高深度无意义 |
| 单级互联核 | 2 到 4 | 验证基本穿插 |
| 带缓存的多级互联 | 4 到 8 | 覆盖深缓冲场景 |
| 压力测试模式 | 最大支持值 | 专门跑边界 |
3.3 用 sequence 产生交织事务:让 VIP 真正跑起来的最小示例
配置参数只是搭好了舞台,真正让交织发生的是 sequence。最简单的方式是发起多笔不同 ID 的读事务,由 VIP 内部分配返回顺序;更可控的方式是通过uvm_do_with显式指定每笔事务的关系。这里给出一个最小可跑的 sequence:
class interleave_read_seq extends uvm_sequence #(axi_transaction); `uvm_object_utils(interleave_read_seq) int unsigned num_reqs = 4; task body(); axi_transaction req; for (int i = 0; i < num_reqs; i++) begin req = axi_transaction::type_id::create("req"); start_item(req); if (!req.randomize() with { addr == (i * 32'h100); burst_len == 3; // 4 beats per burst id == i; // use distinct ID outstanding_ok == 1; }) `uvm_fatal("RAND_FAIL", "req randomize failed"); finish_item(req); end endtask endclass核心是id == i生成了 4 笔不同 ID 的读事务,每笔地址间隔固定,且每笔都是 4 拍突发。VIP 驱动在这 4 笔事务发出后,会根据配置里的交织深度决定返回顺序是穿插还是顺序。如果 depth 为 4,它可能在事务 0 的第 0 拍返回后,把事务 1 的 4 个 beat 全部返回,再继续返回事务 0 的后 3 拍,这是一种极端交叠。具体哪一拍切换,由 VIP 内部的仲裁机制决定,你可能无法精确控制每一拍,但可以控制事务的 ID 和突发长度,保证交织候选者存在。
outstanding_ok == 1是这里假设 transaction 里存在的一个字段,用来允许该事务在未收到响应的情况下继续发出下一笔。如果实际 VIP 接口不提供这个字段,要么在 sequence 里用 fork 并发发送,要么在 driver 里预先把多笔事务压入队列。不同 VIP 的 transaction 字段差异很大,但核心一致:只有多笔事务同时处于飞行状态,交织才有机会发生。写完 sequence 后建议先跑一个 1000 周期的小用例,在波形里看读数据通道是否真的有不同 ID 的 beat 穿插。如果完全顺序返回,优先查 outstanding 是否大于 1,再查 ID 位宽是否足够,最后查 depth 有没有成功传到 driver。
4. AXI VIP 配置 interleaving 的常见问题与避坑清单
4.1 现象一:配置了交织,实测 transaction 仍然顺序返回
这是最常遇到的问题。enable_read_interleaving 设成 1,max_interleaving_depth 配了 4,仿真跑完看波形,读数据还是按事务发出顺序依次返回,看不到任何穿插。把 config_db 的打印打开,值也正确,一度怀疑是 VIP 的 bug。
原因排查下来基本落在三个位置。第一,outstanding_reqs 被设成了 1,VIP 每次只放行一笔事务到总线,第二笔在队列里等前一笔完全结束才发出,交织自然不可能发生。第二,sequence 发的多笔事务用了同一个 ID,AXI 约束同一 ID 的数据必须保持顺序返回,VIP 不会产生协议违例,于是顺序返回。第三,config_db 路径不对,set 到了 env 层,agent 内部 get 时字段名不一致,VIP 用的是默认值。
解决方法是做一个诊断:打印 VIP 内部实际生效的配置,而不是打印 test 里 set 进去的配置。很多 VIP 都有 print_config 或者 protocol checker 相关的接口,在 build_phase 结束后调用就能看到。把 outstanding 改到 4,sequence 里的 ID 改成每笔不同,再开打印确认。这三步做完,交织基本就出现了。如果还没有,看 VIP 是否还有额外的门控条件,比如 transaction 里的 no_ordering 标志,这类字段在 sequence 里最容易漏。
4.2 现象二:打开交织后出现大量协议违例,误报还是真错
打开交织后,仿真跑到两百个周期附近报了一堆违例,主要落在地址通道,现象是多个事务地址重叠。第一反应是 VIP 配置有问题,把交织关掉就安静了,于是怀疑交织让 VIP 产生了非法激励。
真实原因往往是 DUT 本来就是按顺序接收地址的,VIP 发出多笔重叠地址的并发事务后,触发了从设备内部的地址仲裁断言。AXI 协议允许不同 ID 的事务并行存在,但 DUT 不一定支持。这种违例不是 VIP 的 bug,而是环境把 VIP 配得比 DUT 更强了。换句话说,VIP 的行为合法,但 DUT 的约束模型不完整,需要靠 sequence 约束来限制。
处理原则是区分违例来源。如果检查来自 AXI 协议规则,比如同一 ID 读数据顺序错乱,那通常是 scoreboard 的检查逻辑没跟上交织模式,需要在 monitor 里关闭顺序检查或改用 per-ID 检查。如果检查来自 DUT 断言,比如地址不允许重叠,应该增加地址约束,让地址间隔大于最大 burst 长度,而不是关闭 VIP 的交织。VIP 配置只是协议允许的上限,sequence 的地址约束才是测试场景的边界,两者要配合调整。直接把交织关掉绕开问题,等于把 DUT 的真实行为也绕过了,后面还会在别的地方爆。
4.3 现象三:随机化后 deadlock,长时间挂起无响应
配置交织后,仿真跑到某处就停住了,波形停在读数据通道上,没有新的数据返回,也没有新的请求发出。这种 deadlock 在加入交织后出现概率明显升高,排查时最容易慌。
典型原因是 outstanding 数量与响应通道的返回能力不匹配。VIP 发出的未完成事务数量超过了从设备能同时处理的 ID 数量,从设备挂起等待某些条件,VIP 因为数据缓存占满不肯释放新的响应,两边互相等。另一种常见原因是 sequence 用阻塞式get_response等待响应,而交织配置让响应顺序与发出顺序不同。sequence 按发出顺序等第一笔事务的响应,但第一笔事务的数据被交织排到了最后返回,前一笔没响应,后一笔没法继续,死锁就出现了。
解决方法是别用阻塞式get_response等待交织事务。sequence 发出所有事务后在 scoreboard 里通过非阻塞方式收集响应,或者用带 ID 过滤的 get_base_response。同时检查数据缓存深度,确保 buffer 大于交织深度加突发长度的和。还有一个实践技巧:把 VIP 的 timeout 机制打开,死锁出现时自动打印所有未完成事务的 ID 和状态,定位速度快很多。看波形在这种场景下效率很低,看 VIP 内部维护的事务队列状态更能说明问题。
4.4 现象四:交织覆盖率 100%,关键边界却完全没碰到
功能覆盖率里定义了交织深度的 bin,从深度 2 到 16 都配了,回归跑完覆盖率报告显示 100%,但代码审查发现深度 16 的路径根本没被真正执行过。这是个典型问题:覆盖率收集点和 VIP 实际行为之间隔了一层。
原因在于覆盖率采样点选错了。常见的写法是 coverpoint 直接采cfg.max_interleaving_depth,配置阶段这个值就是 16,所以覆盖率必然显示被覆盖。但实际仿真里 outstanding 数量只有 4,VIP 真正产生的交织深度最多到 3,深度 8 和 16 的场景一次都没发生过。这种覆盖率没有任何意义,它描述的是配置意图,而不是总线行为。
解决方法是把采样点从配置参数改成总线行为。具体做法是在 monitor 里记录每次返回数据切换 ID 时的 beat 计数,统计连续同一 ID 的 beat 数作为实际交织深度。覆盖率模型采集的是从总线上抓到的实际穿插宽度,而不是配置里的 depth。配置代码里加一个 debug 打印,在每次返回数据切换 ID 时打印当前 beat 序号,就能快速核对覆盖率模型是否采到了真实数据。覆盖率报告的 100% 从来不是终点,关键是确认它真的覆盖到了硬件行为。
5. 用响应回调验证交织边界:让 scoreboard 真正抓住乱序错误
5.1 注册 response handler 并检查 per-ID 顺序
配置交织只是第一步,最终要验证的是乱序行为下数据能否正确拼回。我常用的做法是在 VIP 的响应回调里注册一个 per-ID 检查器,它只关注同一 ID 内的 beat 顺序,不关心不同 ID 之间的返回顺序。这样既能验证交织合法性,又不会被交织现象本身干扰。下面的代码是一个简化了的回调检查类,可以直接套用到自己的 monitor 数据通路里:
class interleave_data_checker extends uvm_component; `uvm_component_utils(interleave_data_checker) int last_beat_idx[int unsigned]; bit seen_first_beat[int unsigned]; function void check_read_data(int unsigned id, int unsigned beat_idx); if (!seen_first_beat.exists(id) || !seen_first_beat[id]) begin seen_first_beat[id] = 1; last_beat_idx[id] = beat_idx; return; end if (beat_idx != last_beat_idx[id] + 1) `uvm_error("DATA_ORDER", $sformatf( "ID %0d beat order broken: got %0d expect %0d", id, beat_idx, last_beat_idx[id] + 1)) last_beat_idx[id] = beat_idx; endfunction endclass回调逻辑核心就一点:每收到一笔读数据,解析出 ID 和 beat index,用 ID 做 key 查表。如果当前 beat index 不等于上次记录的 beat index 加 1,报错。初始化时等每笔事务第一个 beat 到达后才置有效状态,避免把合法交织误报成顺序错误。这个检查器在未开启交织时也能工作,它天然兼容两种模式。对写通道则是另一套逻辑:写响应通道每个 ID 只能有一个未决响应,同一 ID 连续出现两个未决 WRESP 就是违例,实现上用队列保存未决响应即可。
5.2 把交织深度压到最大再放开:回归里的边界扫描
最后建议配置一个专门的压力测试模式,把 max_interleaving_depth 设到 VIP 支持的最大值,outstanding 设到最大,同时把 sequence 的 ID 数量设为 outstanding 的一半。这样每个 ID 都有至少两笔未决事务,交织深度才可能逼近上限。跑通之后再逐级缩小 depth,找出 DUT 在哪个深度开始出现数据错误。那个深度就是 DUT 的真实边界,这个数字值得写进验证报告。
后续回归保留两个模式。一个是随机交织模式,让 depth 在 2 到 8 之间随机,覆盖常规场景;另一个是压力模式,固定最大 depth,每周跑一次长回归。这样既不会让日常回归太慢,又能保证交织边界始终有人照看。我在某个总线互联项目里用这套配置,正是压力模式抓到了一个缓存未命中时数据错位的 bug,而随机模式已经跑了几千次都没暴露。配置 interleaving 说穿了就是让激励符合硬件真实行为,我的习惯是每次改完参数先打印 VIP 实际生效值,再跑 1000 周期的小用例看波形里是否真的出现了不同 ID 的 beat 穿插,确认之后再调 scoreboard 的数据顺序检查。这样后面出了问题,你至少能分清是配置错了还是 DUT 行为错了。希望这份清单能帮到你,少走一些我走过的弯路。
本文还有配套的精品资源,点击获取