这几个星期一直在折腾 Synopsys AXI VIP 的调优,前两篇把环境搭建和 ID 基础讲了,这篇集中写我在乱序(out-of-order)和延时(delay/latency)配置上踩过的坑和最终沉淀下来的思路。做 AXI 验证的人应该都有体会:协议功能全通之后,真正拖住项目进度的往往不是“错得离谱”的 case,而是那些看似已经通过、把乱序和延时一开就崩的 case。这篇内容比较适合已经有 AXI VIP 基本使用经验、正在做回归压力和协议边界测试的验证工程师,如果你正被 transaction 刷屏、仿真日志越来越大困扰,最后一节可以直接跳到。
1. 为什么乱序和延时值得单独写一章
1.1 功能正确不代表时序健壮
很多团队跑 AXI 验证,第一轮全通后觉得万事大吉,但第二轮把乱序开关一开,挂得莫名其妙。原因其实很常见:DUT 在顺序请求下,buffer、FIFO、仲裁逻辑都没有被真正推到边界,功能是“在宽松时序下正确”,而不是“在乱序和延时压力下依然正确”。
我在实际项目里遇到过一个互联模块,顺序读写全部通过,一旦 master 同时发出多个不同 ID 的读请求,返回数据就出现交叉错位。这个 bug 靠顺序激励是永远复现不出来的,必须通过 VIP 主动制造乱序。
所以我的结论是:AXI 验证的第二个阶段,核心任务就是“用配置代替运气”去制造乱序。不要等着 VIP 随机出来一个乱序,而是通过 ID、outstanding、交织参数把乱序区域明确拉开,让 DUT 的仲裁和返回逻辑在一个可预期但足够极端的压力窗口里工作。
延时也是一样。延时不是简单地在 ready 上卡几拍,它会影响地址通道和数据通道的相对关系,影响 credit 回填时序,甚至会暴露 DUT 内部 buffer 深度不足的问题。把延时和乱序组合起来,才算真正验证了 AXI 接口的时序健壮性。
1.2 AXI 对“完成顺序”的两条硬约束
配置乱序之前,必须先把 AXI 协议里关于顺序的规则想清楚。很多人对乱序的理解是“请求顺序和返回顺序可以随便不一致”,这么理解是不完整的。AXI 其实靠 ID 来区分:
- 相同 ID 的事务,在返回时仍然要保证顺序一致。
- 不同 ID 的事务,之间允许乱序完成。
这两条规则是所有乱序配置的地基。你在 VIP 里启用乱序,本质上是让 VIP 有能力在“不同 ID 之间”打乱返回顺序,但同一个 ID 内部的顺序约束依然有效。如果 DUT 本身对同一 ID 也没有严格保序,那是 DUT 的设计有问题;如果 VIP 错误地让同一 ID 也乱序返回,就是 VIP 配置或者 DUT 对 ID 的解析有误。
更细一层,AXI 还有交织的说法。交织是数据通道层面的行为,一个 master 可以在返回 ID A 的 beats 中间插入 ID B 的 beats,但每个 ID 自己仍然是顺序返回。乱序和交织经常一起出现,但这是两个独立的开关,下面第三节再拆开讲。
2. 先搞定乱序:ID 空间、outstanding 与数据交织配置
2.1 ID 宽度决定了乱序的最大边界
配置乱序的第一步,不是开一个“允许乱序”的开关,而是确认 ID 空间够不够。AXI 的 ID 字段位宽决定了可以同时 outstanding 的不同事务数量上限。如果 ID 是 4 bit,最多 16 种 ID;如果只有 1 bit,就只能区分两个事务,乱序能力天然受限。
我曾经因为图省事,把 VIP 的 ID 位宽配成 8,DUT 实际只支持 4 bit ID。结果 VIP 发出了大量超过 DUT 解析能力的 ID,DUT 内部出现 ID 覆盖的情况,仿真波形看起来就像“VIP 制造了一个 DUT 不可能支持的乱序场景”。这不是 VIP 的问题,是配置没有对齐。
在 Synopsys AXI VIP 的配置类里,一般会看到类似id_width这样的字段。要先查 DUT 的接口位宽,再按实际值去配置,不要为了追求压力而随意放大。
2.2 outstanding 数量和读写 outstanding 的配置
ID 空间只是上限,真正决定同一时刻“有多少事务在飞”的参数是 outstanding。outstanding 数量越多,DUT 需要同时跟踪的 ID 就越多,乱序返回的可能性就越高。
一个我常用的初始配置表:
| 配置维度 | 推荐初始值 | 压力模式 | 风险点 |
|---|---|---|---|
| ID 位宽 | 与 DUT 实际位宽一致 | 扩大到协议上限 | 过宽会掩盖 DUT 在窄 ID 下的复用问题 |
| RD outstanding | 8 | 32~64 | 读缓冲溢出或 R 通道长时间占线 |
| WR outstanding | 8 | 16~32 | 写响应 B 堆积,刷不到 DUT 内部 buffer |
| 读数据交织 | 关闭或 4 | 8~16 | DUT 不支持交织时会立刻产生协议错误 |
| 写数据交织 | 关闭或 4 | 4~8 | 写数据通道插入间隔过密,容易暴露 FIFO 深度问题 |
读和写的 outstanding 最好分开配。很多 DUT 在读通道和写通道各自维护 credit,读写 outstanding 不对称时,压力分布才会更接近真实场景。如果读和写都设成一样大的值,反而容易把问题限定在“对称压力”范围内。
还有一个细节:outstanding 增加以后,DUT 内部跟踪 ID 的表项也需要相应容纳。如果 DUT 表项深度只有 8,你配了 32 outstanding,那么仿真跑到第 9 个事务时就会出现 VIP 侧等待、DUT 侧无法接收的假死现象。遇到这种情况,第一反应不应该是改 DUT,而是确认你的配置是否超出了 DUT 的真实能力。
2.3 读交织和写交织:容易和乱序混淆的另一个开关
我刚接触 AXI VIP 时,以为读数据交织就是乱序,其实不对。乱序改变的是事务完成顺序,交织改变的是数据返回路径上 beats 的排列方式。
举个例子:master 发出 ID=0 和 ID=1 两个读请求,每个请求返回 4 个 beats。乱序情况下,ID=1 的最后 2 个 beats 可能先于 ID=0 的第 1 个 beats 返回;交织情况下,返回序列可能是 ID=0 beat0、ID=1 beat0、ID=0 beat1、ID=1 beat1 这样交替出现。
在 VIP 配置里,一般会有read_data_interleaving和write_data_interleaving之类的开关。这里有个特别需要注意的地方:交织不是所有 DUT 都支持。很多 AXI slave 只支持单 ID 连续返回,如果 VIP 开了读交织而 DUT 不支持,仿真马上会报协议错误。所以配置交织之前,一定要先确认 DUT 的接口文档里是否明确支持数据交织。
如果 DUT 确实支持交织,那么交织深度要小于或等于 outstanding 数量,否则数据通道上无法同时挂起足够的返回序列来填充交织窗口。最好的方式是把交织深度和 outstanding 数量关联起来配置,两者保持倍数或相等关系,比较容易在回归里覆盖到数据通道的交叉压力。
3. 延时注入的三个通道层面与死锁避让
3.1 地址通道、数据通道、响应通道分别在哪里注入延时
AXI 的延时可以放在任何通道的 VALID/READY 握手之前。从功能上看,延时注入的目的是制造握手的等待周期,从而改变请求与响应的相对时序。Synopsys AXI VIP 里,延时一般是面向通道配置的,而不是面向整个事务。
我在配置时会把延时拆成三组来考虑:
第一组是地址通道延时,包括 AW 和 AR。地址通道延时增加后,master 发出请求的节奏变慢,DUT 的地址接收端不会一直处于满负载状态。这个延时适合模拟“master 侧仲裁资源紧张”的场景。
第二组是写数据通道延时,W 通道的 beats 间隔。W 通道延时增大后,DUT 的写数据缓冲可能在一段时间内只收到部分 beats,暴露写数据包处理的完整性检查逻辑。
第三组是响应通道延时,B 和 R。响应通道延时是模拟 DUT 处理完成后返回结果的速度。响应延时增大,直接影响 master 侧下一波操作的发起,因为 master 通常在收到响应后才释放 outstanding credit。
一个常用的配置思路是:地址通道延时设置较小,响应通道延时设置较大,让 DUT 能快速接收请求但慢慢返回结果,这样 outstanding 容易被占满,乱序出现的概率和密度都会提高。
3.2 固定延时 vs 约束随机延时
延时配置有两种:固定值和约束随机。
固定值适合 debug 阶段。当某个乱序场景挂掉后,先把所有延时固定下来,让波形完全可复现,然后逐步调整某一个通道的延时值,观察 DUT 的响应。这个阶段不要开随机,否则复现率和定位成本都会变高。
约束随机适合回归阶段。固定延时只能覆盖固定的时序窗口,约束随机则可以把延时范围铺开。我是这样写约束的:
constraint c_rsp_delay { rsp_delay dist { 0 := 20, [1:4] := 60, [5:16] := 20 }; }这个约束的意思是:20% 概率没有延时,60% 概率延时 1 到 4 拍,20% 概率延时 5 到 16 拍。比起均匀分布在 0 到 16,这样的分布更像是真实系统的行为——大多数时候响应较快,偶发卡顿严重。
延时的随机范围要控制上限。我不建议把延时上限设成 outstanding 数量的好几倍,因为这样会让整个事务的完成时间变得不可控,仿真时间也拉得很长。比如 outstanding 是 16,延时范围 0~10 就足够制造压力了,没必要配到 0~100。
3.3 延时过大时需要考虑的缓冲与死锁风险
延时不是越大越好。延时过大,最直接的影响是 DUT 内部的 buffer 被持续占满,一旦 master 和 slave 两侧都在等待对方释放资源,握手就会进入互相等待状态,表现为仿真卡死。
有一个比较典型的场景:master 发送写地址和写数据后,slave 因为 W 通道延时过大,迟迟没有收完所有 beats,因此不发 B 响应。master 的 outstanding credit 被这个写事务占着,无法发出新的 AW。如果 VIP 配置里同时限制了写事务的最大数量,那么整个写通道就死锁了。
我排查这种死锁时,会先看波形里所有 VALID 和 READY 是否都处于一个“谁都不让谁”的状态,然后去核对 outstanding credit 和 buffer 深度。多数情况下是配置里的 outstanding 数量大于 DUT 实际缓冲深度。
另外一个容易被忽略的是跨通道 mutual wait,比如 slave 的写响应缓冲满,导致 B 通道无法接收新的 B 响应;与此同时 master 因为 outstanding 满,无法发出新的 W 数据。看起来问题是延时造成的,实际却是配置没有对齐 DUT 的缓冲能力。所以每次延时调大,我都会同步检查 DUT 侧能否吞下对应的 outstanding 数量。
4. 实战组合:构造一个“乱序+延时”回归场景
4.1 场景目标与平台配置
用一个实际场景来说明吧。假设我手头有一个 AXI 互联矩阵,需要验证它在一个 master 同时发起多个读和写的情况下,乱序返回是否可靠,以及 slave 响应延时抖动时,master 侧业务是否受影响。
回归场景的目标有三个:一是覆盖不同 ID 的读返回乱序组合;二是覆盖写响应延时和读返回延时混合出现时的通道行为;三是整个回归的运行时间不能比原来慢一倍以上。
平台配置里有两个 agent:一个 AXI master,一个 AXI slave。master 负责发起读写流量,slave 挂在 DUT 的从端,负责接收请求并返回响应。为了让乱序出现得自然,master 必须能同时发出多个不同 ID 的 outstanding 事务;为了让延时可控,slave 的响应延时采用约束随机。
4.2 配置过程和代码注释
下面是我在项目里用的配置框架,为了通用性做了简化。Synopsys AXI VIP 不同版本字段名会有差异,但配置层级和思路是一致的:
// master 侧:控制 outstanding 和交织 axi_mst_cfg m_cfg = axi_mst_cfg::type_id::create("m_cfg"); m_cfg.id_width = 4; m_cfg.max_outstanding = 16; m_cfg.rd_outstanding = 8; m_cfg.wr_outstanding = 8; m_cfg.enable_read_data_interleaving = 1; m_cfg.read_data_interleaving_depth = 4; m_cfg.enable_write_data_interleaving = 0; // slave 侧:控制响应延时 axi_slv_cfg s_cfg = axi_slv_cfg::type_id::create("s_cfg"); s_cfg.r_ready_delay_mode = AXI_DELAY_RANDOM; s_cfg.r_ready_delay_min = 0; s_cfg.r_ready_delay_max = 8; s_cfg.b_ready_delay_mode = AXI_DELAY_RANDOM; s_cfg.b_ready_delay_min = 0; s_cfg.b_ready_delay_max = 4;这段配置的核心是:master 侧把读 outstanding 和读交织打开,让读返回路径上出现多个 ID 交叉返回的机会;slave 侧把 R 通道延时范围设得比 B 通道大,因为我想优先压读乱序场景。如果需要同时压写乱序,可以把wr_outstanding调大,再把写交织打开。
有一个我在调试中反复确认的点:enable_read_data_interleaving打开后,DUT 必须要支持交织。如果 DUT 不支持,应该直接关闭,不要为了“压力大”硬开。这个开关开错了,跑出来全是误报。
4.3 跑完以后看什么
仿真跑完,我不急着看 pass/fail,先看三个地方的波形:
第一个是 ID 的分布。我会检查 outstanding 的读请求确实使用了不同 ID,并且同一 ID 内部的 beats 保持连续。如果同一 ID 内部被打乱,那属于 VIP 配置错误,不是 DUT 问题。
第二个是 R 通道的 beats 组合。重点看不同 ID 的返回数据是否真的是交织或者乱序出现,还是所有返回都排队按顺序走。如果配置了读交织,但波形里从来没有交织,说明约束过弱,需要把随机种子换掉或者调整交织深度。
第三个是 B 通道和 R 通道的相对时序。我要确认在 R 通道高延时期间,写响应 B 通道还能保持正常回填。很多 DUT 在读返回慢时,写通道也跟着卡,这个现象只有在读、写延时并发时才会暴露,单独的延时测试覆盖不到。
5. 关闭 transaction 打印:别让日志拖垮回归
5.1 日志为什么越积越慢
跑过大型回归的人都有体会:一旦 AXI VIP 把 outstanding 和乱序打开,transaction 数量会直线上升。每个 transaction 在发起、完成、返回时都会打印一条信息,日志从几十 MB 涨到几个 GB 是非常快的。
日志变大的直接影响是仿真速度下降。大量的格式化打印会占用 CPU 时间,而且文件写入 I/O 本身比内存操作慢得多。如果每个人打印的信息都是完整 beat 级 dump,回归效率会低到让人抓狂。
我还遇到过一种情况:仿真并没有错,但是因为打印太慢导致 watchdog 超时,最后被误判成失败。这种问题比协议错误还要隐蔽,因为你查波形查不到任何问题,只是超时了。
所以把打印控制纳入 VIP 调优的日常操作,非常有必要。
5.2 用 UVM verbosity 控制配合组件层次过滤
Synopsys AXI VIP 底层走的是 UVM 的 report 机制,因此可以用 UVM 标准的 verbosity 控制来关闭 transaction 打印。
最简单粗暴的方式,是在运行时指定全局 verbosity:
+uvm_set_verbosity="*,_ALL_,""UVM_NONE,main"这样会把所有组件的 verbosity 全部降到 UVM_NONE,任何uvm_info都打印不出来。但全局拉黑有个问题:真正想留的错误和警告也没了。所以更推荐按组件层次过滤。
比如我的环境中 agent 的层次是uvm_test_top.env.mst_agent,只想在这个 agent 范围内关掉 transaction 打印,可以这样写:
+uvm_set_verbosity="uvm_test_top.env.mst_agent,_ALL_,UVM_NONE,main"如果是在 testbench 里用代码控制,可以在 build_phase 里通过uvm_config_db去设置:
uvm_config_db#(uvm_verbosity)::set( null, "uvm_test_top.env.mst_agent.*", "*", UVM_NONE );这里UVM_NONE会让低于它的 verbosity 消息全部关闭。
还有一个更细致的方法是针对消息 ID 过滤。transaction 打印一般都有特定的消息 ID,比如AXI_MST_TXN。你可以在打印代码里找到这些 ID,只关掉这一个 ID 对应的消息,保留其他所有打印:
+uvm_set_verbosity="*,AXI_MST_TXN,UVM_NONE,main"这种方式的好处是保留了其他重要信息,不会在关打印的同时把调试线索也关了。
5.3 更好的做法:隔离而不是全关
我的原则是:回归时不要直接把打印全关,而是把详细 transaction 打印重定向到文件,终端只保留 summary 和 error。
做法很简单,跑仿真时用日志文件输出:
./simv +ntb_random_seed=1234 -l run.log然后在环境里把 transaction 级的 verbosity 降低,只保留每个测试结束时的统计打印,比如总共发了多少个读请求、多少个写请求、平均响应延时是多少。这样日志文件仍然保留了必要的信息,但终端不会被刷屏,仿真速度也不会受到打印数量的影响。
如果你连日志文件都嫌大,可以进一步把UVM_HIGH以上的打印关闭,只留UVM_MEDIUM。transaction 层面的原始打印留给定向 debug 的 case 单独开,回归 case 全部走低 verbosity。
我还会在每个 case 结束前打一行总的事务数量和延时分布统计。这样即使无关打印,回归结束后也能从日志快读判断是否跑到预期压力。
6. 调试乱序问题时我踩过的几个坑
6.1 VIP 和 DUT 对乱序能力的假设不一致
这是最冤的一类问题。VIP 支持 16 个不同 ID 同时 outstanding,但 DUT 只支持 4 个。VIP 把乱序场景造得飞起,DUT 一收到第 5 个不同 ID 就懵了。报错看起来像协议错误,实际是配置和设计能力不匹配。
排查时先核对 ID 位宽,再核对 outstanding 数量,最后核对交织支持。这三项是 DUT 接口时序参数的核心,任何一项对不上,后面的调试都是在浪费时间。
6.2 只做读乱序,没有混入写路径压力
读乱序很容易通过读 outstanding 和读交织配置出来,但很多 bug 出现在读写混合时。DUT 的仲裁器如果对读和写分别维护 credit,那读乱序压力再大,写通道也是空的,发现不了读写互相争抢资源的问题。
我现在通常把读写 outstanding 配成接近的值,然后让 master 同时发起读写事务。仿真时看 R 和 B 通道的返回是否互相影响,一旦发现写响应在读返回高峰期明显变慢,就会继续调大读 outstanding,看 DUT 的写通道是否被彻底饿住。
6.3 延迟配置和时钟域之间被忽视的关系
AXI 多时钟域设计里,延时拍数是相对 VIP 侧时钟的。如果你在 slave 侧配置了固定 8 拍延时,而 slave 时钟频率比 master 快两倍,那么实际延时的时间长度已经变了。在跨时钟验证里,延时的绝对值并不重要,重要的是延时占比是否覆盖到了边界窗口。
我会在配置延时之前,先确认 master 和 slave 的时钟频率比,再决定延时的拍数范围。否则调了很久的延时范围,实际覆盖的时序窗口根本没有变化。
6.4 打印全关后遇到死锁,连现场都看不到
有一次为了跑大回归,我直接把 transaction 打印全部关掉,结果某个随机种子命中了死锁。等到去查日志时,发现只有挂死前的最后一条打印,完全没有 transaction 的流动轨迹,只能靠 trace 重新复现。
从那以后我学乖了:重要回归保留 summary 级打印,debug 单独开交易打印,偶发死锁用固定的种子先复现,再去开详细打印定位。关打印是手段,不是目的,不要为了速度把现场信息也关没了。
最后再分享一个配置心得:乱序和延时的核心,是让 VIP 配置尽量贴近 DUT 的真实能力,再在这个能力边界附近做压力外扩。ID 宽度、outstanding、交织和延时范围,每一项都要有明确的配置依据,不要一味往大调。调参之前先想清楚 DUT 到底会面对什么样的总线流量,比盲目把压力拉满有效得多。