1. AXI总线中的AW-W依赖:死锁场景的完整复盘
做总线验证的人应该都遇到过这种场景:仿真跑到一半,整个testbench卡死不动了,时钟还在跳,但总线事务就是不往前走。波形拉出来一看,AWREADY一直拉不高,WREADY也一直等在那里,两边就这么僵持着。
这种卡死,十有八九就是AXI总线上的死锁。而在所有死锁场景里,AW-W依赖是我在实际项目中见过最多、也最容易踩的一种。
先解释一下什么是AW-W依赖。AXI协议里,写通道由三个独立的子通道组成:写地址通道(AW)、写数据通道(W)、写响应通道(B)。协议规范上,AW和W是相互独立的通道,理论上谁先谁后都可以。但实际设计中,很多slave会把AW通道的握手信号和W通道的数据接收逻辑耦合在一起,比如:AW FIFO满的时候,必须等W通道写入一笔数据才能腾出空间;而master侧又恰好持“必须先完成AW握手才能发送W数据”的逻辑。这时候,master在等slave的AWREADY,slave又在等master的WVALID,一个完美的循环等待就形成了。
这个问题的麻烦之处在于:它不是必现的。同样的设计,跑简单读写测试可能几十个case都过,但一旦跑随机激励或者长时间压力测试,就可能在某一个特定的时序窗口下触发死锁。更糟的是,死锁一旦发生,仿真时间会一直跑,但功能逻辑完全停摆,如果没在验证环境里做超时检测,你可能要等好几个小时才发现case已经卡死了。
所以这篇文章,我想把AW-W依赖这个死锁场景从头到尾拆一遍:协议层面的根因是什么、怎么在验证环境里主动构造这种场景、死锁发生之后怎么定位和排查、以及设计上怎么从源头规避。内容全部来自我实际做AXI验证项目时的真实经历,希望能给你一些参考。
2. AXI写通道的握手规则与死锁的四个必要条件
2.1 AW、W、B三个通道的协议约束
要把死锁讲清楚,得先把AXI写通道的握手规则摆出来。AXI协议里,每个通道都是基于VALID和READY两个信号完成一次握手,二者拉高的那个时钟上升沿,就是一次数据传输完成。
写地址通道:master拉高AWVALID,slave在能接收地址时拉高AWREADY,两者同时为高的拍,地址被采样。
写数据通道:master拉高WVALID,同时在WDATA上放数据,拉高WLAST表示最后一笔;slave拉高WREADY表示可以接收,两者同时为高的拍,数据被采样。
写响应通道:slave在处理完写事务后,拉高BVALID返回响应,同时给出BRESP;master拉高BREADY表示可以接收响应,两者同时为高,事务完成。
这里有一个很多人容易忽略的约束,那就是协议规定了BVALID必须在最后一个写数据(WLAST对应的那笔)被采样之后才能拉高,这条依赖关系是协议强制的,本身合理。但AW和W之间,协议并没有强制谁必须先握手。
说白了,从协议规范的角度讲,master完全可以先发写数据、再发写地址,或者两个同时发。AXI协议甚至支持写数据通道比写地址通道提前一拍甚至几拍到达。所以如果你在处理AW通道和W通道的握手关系时,擅自加了一条“W必须等到AW握手成功后才能发”,这就等于给设计埋了一颗雷。
2.2 死锁的四个必要条件在总线场景中的映射
经典的操作系统课程里,死锁有四个必要条件:互斥、持有并等待、不可剥夺、循环等待。把这套理论映射到AXI总线场景里,你会发现AW-W依赖死锁刚好满足这四个条件:
互斥:总线通道上的握手信号和FIFO资源是排他性占用的。AW FIFO里某个entry被占用时,其他地址请求无法使用这个entry。
持有并等待:slave持有AW FIFO的占用权(因为AW FIFO满了),同时在等待W通道的数据进来,希望腾出空间;而master持有W数据(已经在W通道上valid,等待WREADY),同时在等待AW通道握手完成。
不可剥夺:握手中的信号不能被强行打断。master不能强行把已经拉高的WVALID收回去,slave也不能强行让AW FIFO里的entry无效掉重新分配。
循环等待:master等slave的AWREADY,slave等master的WVALID,形成一个环。
这个映射关系很关键,因为这意味着只要你在设计中看到了上述任何一个条件被打破的可能,死锁就有解。但反过来,如果四个条件全部成立,死锁就是必然的,只是触发时机早晚的问题。
2.3 AW-W依赖死锁的典型触发链路
结合我遇到过的实际场景,AW-W依赖死锁的典型触发链路是这样的:
第一步,master发起一笔写突发(burst),假设是4拍数据。master侧的设计是:先发AW,等AW握手成功之后,再开始发W数据。这一步本身是很多人的默认习惯,协议不禁止,但如果slave侧恰好没处理好,这里就会出问题。
第二步,slave侧AW通道的接收FIFO设计成了容量只有1个entry,当AW FIFO被占用时,AWREADY不会拉高。同时,slave的逻辑在AW FIFO满的时候,要求W通道先写入一笔数据,让FIFO里的AW被消费掉,才会释放AWREADY。
第三步,master发AW,AWVALID拉高,等待AWREADY。slave这边AW FIFO已经满了,AWREADY不拉高,同时slave在等WVALID,但master的WVALID此时还没拉起来——因为master在等AW握手成功。
逻辑上,这已经是一个死锁了。但从波形上看,AWVALID和W数据都卡在那里,两个信号都是低电平,非常“安静”,如果不是有经验,很容易误判成“总线空闲,可能master还没开始发请求”。
在实际项目中,这种死锁不会第一拍就暴露,因为AW FIFO刚开始是空的,前几笔事务能正常完成。但只要master侧再发出第二笔、第三个AW请求,且slave还没来得及消费完第一笔,AW FIFO就满了,死锁的时机就到了。
3. 验证环境里如何主动构造AW-W依赖死锁场景
3.1 为什么随机激励很难覆盖到死锁
说句实话,在真正遇到一次死锁之前,我一直觉得随机约束激励已经完全够用了。AXI VIP本身支持随机化地址对齐、突发长度、突发类型、数据间隔,看起来覆盖面已经很大了。
但随机激励很难精确命中AW-W依赖死锁,原因在于:大多数AXI VIP的master模型在发送写事务时,默认会在AW握手完成后才发W数据,或者AW和W数据之间有较大的间隔。而slave侧如果对AWREADY和WREADY的依赖关系设计得不合理,这种“默认顺序”恰好就被绕过去了——aw和w不会发生重叠等待。
要复现死锁,就得让master在AW未握手成功的情况下,持续拉高WVALID,或者至少让W通道提前到AW握手之前的窗口期。这就需要你在验证环境里主动控制写通道的握手时序,而不是依赖VIP默认的行为。
3.2 用AXI VIP的配置接口强制AW-W交错
目前主流的AXI VIP,比如Synopsys的VC VIP、Cadence的AXI VIP,或者开源的AXI验证IP,基本都支持通过配置来控制写通道之间的时序关系。以SYNOPSYS VC VIP为例,你可以在sequence里通过约束aw_ready和w_ready的依赖关系来构造交错:
// 关键在于让AW和W通道可以并行拉高VALID class axi_write_deadlock_seq extends axi_master_base_seq; `uvm_object_utils(axi_write_deadlock_seq) constraint c_aw_w_overlap { // 允许W通道在AW未完成时就开始发数据 aw_valid_delay == 0; w_valid_delay inside {[0:2]}; // 让AW通道的握手被延迟 aw_ready_delay inside {[10:20]}; } task body(); axi_write_transaction wr_trans; `uvm_do_with(wr_trans, { burst_length == 4; // 其他约束... }) endtask endclass核心思路就是一句话:让W通道的发送不受AW握手的影响,但同时让AW的握手被slave侧拉长。这样AWVALID和WVALID会在某个时间段内同时为高,形成前文说的僵持条件。
如果你的VIP不支持这种细粒度的时序控制,那就需要自己在driver层做处理:在发送AW之后,不等AWREADY拉高,就立刻发起W通道的驱动力。这种写法虽然麻烦,但对场景的控制精度更高。
3.3 用SystemVerilog断言实时检测死锁发生
在验证环境里主动构造死锁很重要,但更重要的是,你要能第一时间发现死锁。仿真卡死本身就是一个“信号”,但在大型testbench里,你可能同时跑了多个master和多个slave,单纯靠肉眼盯波形效率太低。
我建议在总线monitor里加上死锁检测断言,一旦AW和W之间出现循环等待的迹象,立刻报错。下面这段断言是我在一个项目里实际用过的,思路很简单:如果AWVALID拉高后超过N个周期AWREADY没有响应,同时WVALID也拉高且WREADY没有响应,就判定为死锁。
property aw_w_deadlock_checker; @(posedge aclk) disable iff (!arst_n) // 检测条件:AWVALID和WVALID同时为高,但AWREADY和WREADY都持续为低 (awvalid && wvalid && !awready && !wready) |-> ##[1:MAX_WAIT_CYCLES] 1; // 这里MAX_WAIT_CYCLES要根据性能需求设置,通常设置成10~20拍 endproperty assert property (aw_w_deadlock_checker) else $fatal(1, "AXI deadlock detected: AW-W dependency loop!");这个断言的原理是:正常情况下,AXI总线上即使有拥塞,AWREADY或WREADY其中一个总会在有限拍数内拉高,两个同时死等的概率极低。如果持续N拍两个ready都是低,说明大概率是依赖造成了死锁。
不过要注意一点,MAX_WAIT_CYCLES的取值是有讲究的。设太短,总线flush时master不发新事务,断言误报;设太长,死锁发生后仿真还要空跑很久。我做过最稳妥的做法是:这个参数可以配置,正常回归测试用比较小的值(比如32拍),跑性能测试时再调大。
4. 死锁发生后的定位与排查:从波形到根因
4.1 死锁波形的典型特征
构造死锁容易,但如果你是在一个既有的项目里偶然遇到死锁,第一步一定是看波形。死锁波形的典型特征是:
AWVALID和WVALID都拉高,但AWREADY和WREADY都长期为低。B通道上没有任何活动,BVALID一直为低。RESET已经释放,时钟一直正常运行。
这几个条件全部满足,基本可以确定总线上发生了死锁。
但这里有一个特别容易误判的点:如果master还没发起新的写事务,AWVALID本身是低的,WVALID也是低的。这种情况下波形看起来一切正常,你甚至可能以为master还在等待什么外部条件。所以排查死锁时,不要只盯当前一拍,要把时间窗口往回拉,找到AWVALID和WVALID从低拉高的那一个时刻,然后看从那以后发生了什么。
4.2 用波形定位循环等待的两个关键检查点
拿到波形后,我一般按两个关键检查点来定位:
第一个检查点是AWREADY为什么一直为低。顺着AWREADY的生成逻辑往前追,看它依赖了什么信号。常见的情况是:AWREADY拉低是因为AW FIFO满,而FIFO的释放逻辑又依赖W通道写入新数据来消费。这时候,你就找到了“slave在等W”这一环。
第二个检查点是WVALID为什么一直为高但WREADY不响应。这里要分两种情况:如果slave侧WREADY本身能拉高,但master侧WVALID一直没有拉高,说明是master在等AW握手,这是“master等slave”这一环;如果WREADY也拉低,那要去看为什么WREADY拉低——是不是slave内部数据buffer满,而buffer的释放又依赖AW先完成。
把两个检查点拼接起来,如果能形成“A等B、B等A”的闭环,根因就找到了。实际项目里,闭环往往不止两个节点,有时会形成A等B、B等C、C等A的三方循环,排查起来更费时间,但方法是一样的:顺着ready信号逐级向上追。
4.3 一个真实的死锁排查记录
我之前在一款AI加速芯片的验证项目里,遇到过一次很隐蔽的死锁。表面上看,master和slave之间协议交互正常,单笔读写测试全部通过,但跑多核并发访问的场景时,仿真跑到第37个case才卡死。
排查过程很有意思。最初我怀疑是内存一致性协议的问题,因为多个master在访问同一个slave的同一块地址区域。后来拉波形发现,死锁的两个参与者分别是master_0和slave_2,与数据一致性完全无关。
再往下追,slave_2的AW通道FIFO是4深度的,理论容量并不小,但问题是这个FIFO的读指针释放条件写错了——它依赖的是W通道的写指针推进,而W通道的写指针又依赖AXI协议里的WLAST。当master_0发了一个奇数长度的burst(比如长度3)时,WLAST的时序与正常情况下不同,导致FIFO读指针没能及时释放,FIFO被占满了。这个时机刚好和master_0在等AWREADY的窗口重叠,死锁就形成了。
这个case最终修了三行代码:把FIFO的读指针释放条件从“W指针推进”改成“WLAST拉高后的下一拍”,问题彻底解决。但它给验证环境提了一个很强的要求:AXI总线的死锁测试,不能只跑常规的偶数长度突发,奇数长度、非对齐地址、乱序ID这些边界情况,才是最容易踩雷的地方。
5. 从设计源头规避AW-W依赖死锁
5.1 slave侧握手信号的独立性原则
验证环境能发现问题,但真正解决问题,还是要靠设计层面的规则约束。我做AXI slave设计时,有一个很重要的原则:AW、W、B三个通道的握手信号,在逻辑上必须尽量保持独立。
AWREADY的产生,只应该依赖AW FIFO的状态,也就是说,AW FIFO有空间就拉高,AW FIFO满就拉低。不应该把W通道的数据接收情况作为AWREADY的释放条件。
WREADY的产生,只应该依赖W数据FIFO的状态,有空间就拉高,满就拉低。不应该把AW通道的地址接收情况作为WREADY的释放条件。
这个原则听起来很简单,但实际设计里特别容易被绕进去。因为很多设计会把地址和数据打包成同一个描述符(descriptor)存储,地址FIFO满了,数据FIFO就必须暂停接收;反过来,数据FIFO满了,又没法推出新的描述符。这种耦合设计在资源上确实省了一点,但代价是死锁风险。
我的建议是:如果资源允许,AW地址FIFO和W数据FIFO一定要分开设计、独立管理。两个FIFO的满状态互不依赖,AW和W通道的握手才能做到真正的独立。如果资源紧张,必须共用存储,那么在逻辑上要保证:接收AW时不检查W状态,接收W时不检查AW状态,只在FIFO内部做地址和数据的绑定关系管理。
5.2 master侧不要“等待AW握手成功再发W”
master侧同样重要。很多工程师在写master驱动时,习惯性地把写流程写成:先发AWVALID,等待AWREADY,握手成功后,再发WVALID和WDATA。
这个写法在功能上完全正确,大多数时候也不会出问题。但前面说了,如果你这么写,而slave恰好又设计了AW和W的依赖逻辑,死锁就是必然结果。
从协议允许的范围看,master完全可以在AWVALID拉高的同时,就把WVALID拉高,数据跟着放上去。AXI协议的写数据通道并不要求W必须等在AW之后。采用这种“AW和W并行发起”的方式,即使slave侧存在一定的依赖,也不会形成环:master已经发出了W数据,slave只要有能力接收W,就能推进事务;如果slave没有能力接收W,那说明W FIFO满了,这个状态和AW无关,死锁的条件不成立。
当然,有些设计为了做写合并(write merging),会把写数据暂存在master侧的buffer里,等收到来自slave的buffer地址再发送。这种情况下必须仔细分析buffer分配和释放的依赖关系,不能在buffer分配上形成AW和W之间的互相等待。
5.3 用超时计数器做最后防线
无论设计怎么做,验证环节都应该放一道最后防线。我强烈建议在每个slave的接口上加上busy timeout检测,当AWVALID或WVALID拉高之后,如果对应的READY信号在指定周期内一直没有拉高,就自动报错或触发中断。
// 简化示例:AWREADY超时监控 always @(posedge aclk or negedge arst_n) begin if (!arst_n) begin aw_timeout_cnt <= 0; aw_timeout_flag <= 0; end else if (awvalid && !awready) begin if (aw_timeout_cnt >= TIMEOUT_THRESHOLD) aw_timeout_flag <= 1; // 上报超时 else aw_timeout_cnt <= aw_timeout_cnt + 1; end else begin aw_timeout_cnt <= 0; end end这个逻辑不需要太复杂,核心目的是:如果握手超过了预设的阈值,就说明协议层出现了非正常的状态。超时计数器既能用来在验证阶段暴露死锁,也能在芯片实际运行阶段作为硬件自检手段上报异常,道理是通的。
6. 更多AXI死锁场景与通用排查方法论
6.1 从AW-W依赖延伸开来的其他场景
AW-W依赖只是AXI死锁的一个起点。我在项目里还遇到过不少其他类型的死锁,简单列举一下:
W-B依赖死锁。slave在处理完W数据后,产生BVALID响应,但产生BVALID需要master先释放某个资源;同时master又在等BVALID才能继续发W数据。这种死锁不能完全依靠总线协议来分析,需要结合slave内部功能逻辑。
R通道依赖死锁。读数据通道RVALID和RREADY之间的循环等待,在乱序返回数据的master中更容易出现。比如slave同时处理两个读请求,返回顺序与master ID管理方式不匹配时,可能把一个ID的R数据堵住,导致另一个ID的R数据也卡住。
响应顺序依赖死锁。AXI协议允许不同ID的写事务乱序完成,但有些设计会强制要求ID顺序返回。如果master侧管理多个outstanding事务,而slave内部先完成了后发的ID,却在返回B时被强制等前一个ID先返回,就可能在B通道上形成阻塞。
这些场景的共性和AW-W依赖是一样的:多个通道的握手信号之间形成了不该有的依赖关系,最终导致循环等待。所以排查思路也完全通用。
6.2 死锁检测工具与调试方法
除了自己写断言,现在很多验证工具也提供了死锁检测功能。Cadence的XCHECK可以在仿真时自动检测X态传播和死锁循环,Synopsys的VCS也有类似的formal/assertion检查选项。用这些工具的好处是,可以在RTL仿真阶段自动标记出可能存在死锁的代码路径。
但工具的局限在于,它只能抓“已经发生”的死锁,不能预测“可能发生”的死锁。所以我还是建议在验证环境中配套使用以下手段:
- 在总线monitor中维护每个master当前outstanding的事务数量,如果某个master的outstanding数量长时间不为0且不减少,触发告警。
- 在powerful场景测试中,随机化AW和W的延迟、随机化B和R的响应延迟,增加死锁暴露概率。
- 对slave的AW FIFO、W FIFO分别做full状态监测,一旦出现两个FIFO同时full且时间超过阈值,就要重点检查是否有依赖问题。
6.3 常见AXI总线死锁排查速查表
| 异常现象 | 可能原因 | 排查方向 |
|---|---|---|
| AWVALID为高、AWREADY长时间为低 | AW FIFO满,AWREADY释放逻辑依赖W通道 | 检查slave的AWREADY生成逻辑 |
| WVALID为高、WREADY长时间为低 | W FIFO满,或WREADY释放逻辑依赖AW通道 | 检查slave的WREADY生成逻辑 |
| AWVALID和WVALID同时为高,两个ready都不拉高 | AW与W之间形成循环等待 | 按AWREADY和WREADY两个方向逐级追查 |
| BVALID一直为低,B通道无响应 | 写事务未完成,可能与WLAST相关 | 检查BVALID的生成条件是否依赖W通道 |
| RVALID一直为低,读数据无响应 | 读请求未处理或slave状态机卡死 | 检查读地址通道AR的握手和读状态机 |
| 总线上所有事务都暂停,burst不能推进 | 可能是地址和数据路径的死锁 | 重点检查master侧outstanding管理和slave侧ID管理 |
这个表的价值在于,它不只是一个问题清单,更是一个“先看什么、再看什么”的排查路径。总线死锁不会凭空产生,每个信号长期不变化,背后一定有一个释放条件没有被满足的依赖链条,顺着链条找到底,答案就在那里。
7. 最后分享一点实操感受
做AXI总线验证这几年,我最大的体会是:死锁问题不怕难,怕的是没有意识。很多死锁在设计的第一版就已经埋下了,但常规的功能测试根本不会触发,等到芯片流片回来或者系统集成阶段才暴露,代价就是几何级数上升。所以在验证计划阶段把死锁场景作为一类独立的测试项来设计,是性价比极高的投入。
构造AW-W依赖死锁这个场景,我个人的建议路径很简单:先把写通道的三个子通道之间的依赖关系在excel里列成一张矩阵,标明哪些信号可以独立握手、哪些信号存在强制依赖、哪些信号被设计加了不该有的依赖,然后针对每一处“非协议强制的依赖”设计一个专门的定向测试。这个方法费不了多少时间,但能帮你把死锁问题消灭在验证阶段。
另外,波形分析时不要只盯着握手信号,AXI的ID和outstanding信息同样重要。很多死锁和事务完成顺序强相关,ID配错了,或者outstanding深度设小了,都可能造成你意想不到的循环等待。把这些因素纳入考虑,你的排查效率会高很多。