
做SoC验证这些年AXI总线上的死锁问题我前前后后排查过不下十回其中最隐蔽、也最容易被“甩锅”的一类就是写地址通道AW和写数据通道W之间的依赖。刚遇到的时候波形上就是四根线死死拉住AWVALID拉高但AWREADY不拉高WVALID又一直低着。整个仿真像被按了暂停键跑到超时也等不到一个写响应。后来梳理协议才发现这是典型的AXI总线死锁场景AW-W依赖。这里把这类问题的成因、波形特征、仿真复现方法和规避手段都梳理一遍做总线设计或者验证的同学可以直接拿去对照。1. 从一次“仿真挂死”说起AW和W是怎么较上劲的第一次遇到这种问题时我还在调一个自研的DMA模块和某个第三方的NAND控制器之间的通路。功能仿真跑到连续写包场景固定在第几千笔事务上挂死。单步跟进去之后发现DMA发完写地址AW之后一直没收到AWREADY所以它按设计不发送写数据W而NAND控制器内部的数据FIFO已经满了它的AWREADY信号恰恰又是由“数据FIFO是否可写”来生成的——FIFO满了AWREADY就不置位。于是两边各持己见互相等待最终谁也没办法继续推进。1.1 先说清楚AXI写事务的三段式握手要理解这个死锁得先把AXI写事务的基础对齐。一个AXI写事务由三部分组合完成写地址通道AW、写数据通道W、写响应通道B。AW通道有一个握手对——AWVALID和AWREADYW通道也有一个握手对——WVALID和WREADYB通道是BVALID和BREADY。每个通道使用独立的VALID/READY握手协议只有VALID和READY同时拉高的时钟上升沿才算完成一笔传输。关键在于AXI协议并没有强制规定AW和W谁必须先到。协议明确允许W通道的数据“早到”也就是写数据可以比写地址先出现在总线上也允许AW先到甚至允许两者在同一拍同时握手。这种自由度本来是设计用来提升乱序传输效率和降低访问延时的但也给实现埋下了隐患如果master和slave双方各自选择了不同的“隐性顺序约束”就可能把协议留给顺序的自由度变成死锁。我在理解这个握手机制时喜欢把它比作两个人分别从两个门进入同一个房间。地址代表“先遣部队”数据代表“后勤物资”协议没有规定先遣部队必须比后勤物资先进门。但很多master实现里后勤物资非得等先遣部队进了门才动身而有些slave又规定先遣部队必须等后勤物资到了才能开门。那结果就是双双堵在门口谁也进不去。1.2 协议没有强制顺序但实现有隐性顺序我们在写master逻辑时最常见的一种“省事”写法是状态机先发AW然后等待AWREADY拉高握手完成后再进入发送W数据的状态。这种写法完全合法因为AXI协议没有禁止master这样做它只是会限制性能写延时相对较高。很多CPU总线桥、自研DMA控制器、甚至某些VIP的默认行为都是这样。问题出在slave这一侧。从AHB时代转过来做AXI的工程师容易把AXI的地址通道和数据通道理解成“必须按顺序到达”于是slave逻辑里可能写成“AWREADY在WVALID有效且数据FIFO未满时才置位”或者“只有收到W数据后地址逻辑才允许接受新的AW”。这种实现虽然不符合AXI协议建议的“slave应能处理W早于AW”但它确实能工作——只要master那边始终严格按照AW先完成后W再开始的顺序并且系统没有大量outstanding写事务通常测不出问题。于是隐患就埋下了master的“等待AWREADY后再发W”和slave的“等待WVALID后再接受AW”像两扇门互为钥匙一旦slave数据通道因为FIFO满、内部缓冲不足、写校准序列等原因暂时无法收WAWREADY也会被同步拉低。master看到AWREADY一直为低就按设计持续不发W。结果AW通道和W通道同时停在“等待对方”的状态写事务永远无法完成。这就是标题里说的AXI总线死锁场景AW-W依赖。2. 用四个必要条件还原AW-W死锁的根因死锁这个词其实不是硬件总线独有的。数据库里的行锁、多线程编程里的互斥锁都会产生类似问题。操作系统课程里讲死锁有经典的四个必要条件互斥、持有并等待、不可剥夺、循环等待。把这四个条件套到AXI的AW-W依赖上你会发现完全对得上甚至比数据库死锁更好理解。2.1 死锁四条件在AXI场景的映射我们先把四个条件逐一映射到总线握手场景中。互斥条件在AXI上体现为每个通道的握手“资源”在同一时刻只能为一个事务服务。AW通道握手未完成master就不能把这个AW资源释放给下一个写地址W通道握手未完成数据总线资源也被占用无法发送后续写入数据。slave同理它用来接受新AW的入口逻辑如果被当前等待流程占住其他事务也无法进入。持有并等待条件master持有了即将发送的W数据却在等待slave的AWREADYslave持有了“可以接受AW”的能力或地址槽位却因为内部数据FIFO满或者“必须数据先到”的策略在等待master的WVALID。不可剥夺条件AXI握手一旦进入VALID拉高等待READY的状态就不会自动撤销重来。master不会因为等了100拍没等到AWREADY就自行把WVALID拉高slave也不会因为等不到WVALID就把AWREADY拉高。协议层面没有“超时取消”机制除非有外部复位或独立看门狗否则两个通道状态就这么僵住。循环等待条件master等待AWREADYAWREADY取决于slave等待WVALIDWVALID取决于master等待AWREADY。闭环形成。把这四条整理成一张表对照起来更清楚。死锁必要条件数据库/线程死锁举例AXI AW-W依赖场景举例互斥行锁同一时刻只能被一个事务持有AW/W握手资源同一时刻只能服务当前事务持有并等待事务A持有一行锁等待另一行锁master持有W数据等待AW握手完成不可剥夺行锁不会被迫让出VALID拉高后等待READY期间不会撤回循环等待事务A等事务B的锁事务B等事务A的锁master等slave的AWREADYslave等master的WVALIDAXI死锁和数据库死锁在根因模型上是同构的只是在时序细节上完全不同。这也是为什么我建议验证同学在排查总线死锁时可以先画一张谁在等谁的“资源等待图”比干看波形直观得多。2.2 两个最常见的触发拓扑在我实际遇到的案例中AW-W依赖死锁主要集中在两种拓扑形态下。第一种是数据FIFO反压导致AWREADY被连带拉低。slave内部写数据路径上有一个同步FIFO深度可能只有8或者16用来缓冲写入数据。FIFO满后wdata_fifo_full拉高正常情况下应该只反压WREADY让master暂停发送W。但如果slave代码把wdata_fifo_full也用在了AWREADY的生成逻辑上比如写成“只有数据FIFO未满才接受新AW”那么当数据FIFO满时AWREADY就变低。如果此时master又是不等AW握手就不发W的严格顺序模型那么WVALID永远不会拉高数据FIFO永远不会被消费AWREADY也永远不会恢复。死锁成立。第二种是slave要求“先W后AW”才能完成地址数据配对。这类slave多见于简化设计的数据通路。它们可能把地址缓存和数据缓存合在一起只有在收到数据时才知道这笔事务对应的地址于是AWREADY必须等待WVALID到达后再置位。这种设计在协议上虽然不推荐但可以通过一致性测试因为AXI允许W早到。可它一旦碰上严格顺序的master就成了死局的另一半拼图。这两种拓扑的共同点是AW通道的握手完成条件被错误地依赖到了W通道的状态上。而在AXI协议的设计哲学里不同通道之间的控制信号应当尽量解耦反压信号应该只反映本通道fifo的空间而不是跨通道去引用对方的空闲状态。2.3 为什么AXI3和AXI4都留了这个“雷”有人可能会问既然协议允许W早到为什么还会死锁原因是协议写的是一套理想行为实际硅实现却是千差万别。AXI4规范里确实要求slave应能处理写数据早于写地址的情况也要求master不能因为需要接收B响应而阻塞后续AW或W的发送。但规范并没有强制要求每个slave都必须能无限接收写数据也没有规定slave的AWREADY必须与W通道无关。当一个slave的数据缓冲已经填满它自然需要反压。如果它的反压策略恰好在AWREADY上也做了同样的动作而master又不够激进——不支持数据早到或者被自己内部状态机限制了W通道的启动——那协议允许的空间反而变成了死锁的温床。AXI3时代更是如此。AXI3在写数据通道上有一个“写数据交织”的特性允许不同事务的写数据穿插发送这要求slave必须有强大的重排序能力。很多slave为了省事直接退化成“必须严格顺序处理”。这种实现配合上面说的严格顺序master虽然慢但不出错。最怕的就是master和slave各自坚持自己的顺序策略互相不兼容。所以我们常说AXI协议本身没有死锁死锁都是具体实现选型组合出来的。3. 具体场景实操一个可复现的SystemVerilog模型光讲理论不够我把这个死锁场景落成了一个很小的SystemVerilog模型逻辑足够简单放到任何仿真器里跑都能看到卡死。这个模型的价值在于验证同学拿到手后可以直接拿它来演练死锁定位流程设计同学也可以用它验证自己改完后的master/slave是否真的解决了问题。3.1 复现死锁的最小代码片段模型分为两部分master模块和slave模块。master的写状态机严格采用“等待AWREADY后才启动W”的顺序。// master严格顺序写事务模型 module axi_master_strict ( input logic clk, input logic rst_n, output logic awvalid, input logic awready, output logic wvalid, input logic wready, output logic [31:0] wdata ); typedef enum logic [1:0] {IDLE, ADDR_PHASE, DATA_PHASE, DONE} state_t; state_t state; always_ff (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; awvalid 1b0; wvalid 1b0; wdata 32h0; end else begin case (state) IDLE: begin awvalid 1b1; // 发起写地址 state ADDR_PHASE; end ADDR_PHASE: begin if (awvalid awready) begin awvalid 1b0; // AW握手完成 wvalid 1b1; // 现在才允许发数据 wdata 32hA5A5_1234; state DATA_PHASE; end // 若awready一直为0则wvalid保持0卡死根源在这 end DATA_PHASE: begin if (wvalid wready) begin wvalid 1b0; state DONE; end end DONE: state IDLE; endcase end end endmoduleslave模块负责模拟“AWREADY依赖数据FIFO”的行为。这里省略了实际FIFO存储只用一个满标志表示数据FIFO已满并且这个满标志被用来阻塞AWREADY。// slaveAWREADY依赖数据FIFO空满的模型可能死锁 module axi_slave_bad ( input logic clk, input logic rst_n, input logic awvalid, output logic awready, input logic wvalid, output logic wready ); logic data_fifo_full; // 模拟数据FIFO满状态。现实中由数据缓冲计数产生。 // 这里用一个常数1表示“随时保持满”便于复现死锁。 assign data_fifo_full 1b1; // 问题逻辑awready 不仅受地址通道控制还受数据fifo满状态控制 assign awready ~data_fifo_full; // W通道接受能力独立 assign wready 1b1; endmodule把两个模块连起来跑可以看到时序第一个周期开始awvalid拉高由于awready恒为0master停在ADDR_PHASEwvalid一直是0wready虽然拉高但没有任何数据发出。整个模型变成一台“永不前进”的机器仿真器只能靠外部超时中断。这个模型的毛病在于slave用data_fifo_full直接决定awready。真实的slave里如果数据FIFO满了它完全可以用“wready为低”来反压W通道等数据FIFO被消耗后再恢复。但AW通道是独立资源AWREADY不应该受wready或者数据FIFO满的影响它只应该由“地址通道是否有空闲槽位”决定。3.2 修改一行让W不再等AW修复方式其实非常直接。核心原则是保证两个方向中至少有一个能持续前进。简单有效的改动是让master支持数据早到不需要等awready再发wvalid。// master修改版支持W早于AWAW未握手前也可发送W module axi_master_early ( input logic clk, input logic rst_n, output logic awvalid, input logic awready, output logic wvalid, input logic wready, output logic [31:0] wdata ); typedef enum logic [1:0] {IDLE, WAIT_DATA_DONE} state_t; state_t state; logic send_w; always_ff (posedge clk or negedge rst_n) begin if (!rst_n) begin awvalid 1b0; wvalid 1b0; state IDLE; end else begin case (state) IDLE: begin awvalid 1b1; // 地址照发 wvalid 1b1; // 数据也同时发起早于W完成没关系 wdata 32hA5A5_1234; state WAIT_DATA_DONE; end WAIT_DATA_DONE: begin if (wvalid wready) begin wvalid 1b0; // 如果此时awready还未握手我们可以继续等待 // 但wdata已经被slave收走slave数据FIFO会释放空间 end if (awvalid awready) begin awvalid 1b0; end if (~awvalid ~wvalid) state IDLE; end endcase end end endmodule这个版本里master不等AWREADY直接把WVALID拉高并发送数据。slave虽然AWREADY仍然是0但W通道已经收到数据数据FIFO得到释放data_fifo_full被拉低随后awready恢复为高AW握手完成整个事务继续推进。在真实设计中如果数据FIFO本来就是由W通道消费后才释放的那就应该让W先推进。如果master因为某种原因死活不支持数据早到那就要从slave侧改。slave的awready必须改成仅由地址缓存槽位决定例如数据FIFO满时只把wready拉低不影响awready。两个方案选一个甚至两个都做可以极大降低死锁概率。3.3 为什么“数据早到”不是万能药这里需要泼一盆冷水不是所有场景都适合用“W早到”来解决AW-W死锁。若slave的数据接收路径真的没有任何空间比如数据FIFO已满而且只有成对出现的地址数据才能一起被处理那master即使把WVALID拉高slave也不会有WREADY。这时候W通道也会被反压整个系统仍然面临着双向等待的可能性只不过等待的资源从“数据FIFO未满”变成了“数据FIFO必须同时能接收新数据和释放旧数据”。所以设计原则要更本质一点在任意时刻至少应该保证AXI写事务的某个握手方向可以推进。具体到AW和W就是两个通道的READY反压信号不能互相耦合。要么AWREADY永远能独立完成即使W通道停摆地址也能继续走要么WREADY永远能独立完成即使AW通道停摆数据也能继续灌。只要有一个方向能推进最终必然会打破“持有并等待”的僵局。两个方向都互相依赖那才是真正的死锁。我在评审设计时会重点检查每一个AXI slave的awready和wready生成逻辑里有没有引用对方的通道状态。只要awready表达式中出现与wdata_fifo_full、wready、wlast之类W通道相关的信号就会在评审意见里标红。这一条简单粗暴但能挡掉绝大多数AW-W死锁。4. 验证拦截与定位如何快速找到总线死锁死锁在仿真里通常表现为“卡住不动”但仿真本身不会自动报错你得有一个机制在它卡住的时候把它捞出来。如果全芯片仿真规模很大卡好几个小时才发现是AXI死锁这时间成本完全不可控。所以在验证环境里加死锁检测器、断言和超时机制是比手动排查更高优先级的工作。4.1 断言与超时检测最常用的方法是给AXI通道握手加超时断言。AXI的VALID和READY握手通常要求在有限周期内完成。当然因为可能有反压不能断言“一次握手必须在10拍内完成”这会误报。正确思路是如果某一个VALID信号持续拉高超过预设阈值比如10000拍而对应的READY始终没有变成高或者事务整体进度没有推进就报一个deadlock错误。// 超时检测AWVALID持续拉高但长期无AWREADY logic [15:0] awwait_cnt; always_ff (posedge clk or negedge rst_n) begin if (!rst_n) begin awwait_cnt 16h0; end else if (awvalid !awready) begin awwait_cnt awwait_cnt 1b1; if (awwait_cnt 16hFFFF) begin $fatal(1, AXI AW channel stuck deadlock: AWVALID high, AWREADY low, WVALID%0d, WREADY%0d, wvalid, wready); end end else begin awwait_cnt 16h0; end end类似地可以为W通道也加一个。检测到超时之后最好把当前所有相关信号值一起打印出来尤其是AWVALID、AWREADY、WVALID、WREADY、BVALID、BREADY这几个以及slave内部的关键状态比如数据FIFO满标志。有了这些现场打印定位效率会高很多。4.2 波形定位四板斧就算没有超时打印用波形也能快速定位。拿到卡死的波形后按下面四个步骤看。第一步看AWVALID和AWREADY。如果AWVALID一直为高、AWREADY一直为低说明master在等AW握手。第二步看WVALID和WREADY。如果WVALID一直为低说明master根本就没有发起W传输。结合第一步可以确定是master在等待AWREADY。第三步到这里其实已经能锁定“master等AW”的方向。接下来要回答“为什么AWREADY一直为低”。看slave内部的反压信号去追溯awready的生成逻辑。常见的下游信号是wdata_fifo_full、wdata_wr_ptr、甚至可能是wvalid本身。如果发现awready的下降沿跟着wdata_fifo_full拉高问题就找到了。第四步再看WVALID为什么没有拉高——一般会对应到master状态机的ADDR_PHASE等待分支。到这里闭环关系已经清楚。把这四步浓缩成一张速查表方便现场对照。观察项状态特征初步结论AWVALID / AWREADY1/0持续不变master等待AW握手WVALID / WREADY0/1持续不变master未发W数据AWREADY生成逻辑依赖data_fifo_fullslave的AW接受被数据通道阻塞master状态机ADDR_PHASE等待master被自己的顺序策略卡住这四步基本能覆盖90%的AW-W死锁现场。剩下的10%可能是交织事务、多master仲裁、interconnect内部buffer引入的更复杂闭环但排查思路依然是用同样的方法逐级追依赖。4.3 和数据库死锁排查思路的对照既然热搜词里挂了数据库死锁我就顺手提一句共性。MySQL里查看死锁常用SHOW ENGINE INNODB STATUS它会直接打印出死锁事务和等待的锁资源Oracle有dba_blockers、v$lockJava线程死锁用jstack能看出线程栈互相等锁。这些工具本质上都是输出“资源等待图”谁持有什么资源谁在等待什么资源。AXI总线死锁排查也是同样思路。波形就是我们的INNODB STATUSAW和W两个通道就是资源节点。画出等待图很快就能看清循环依赖。区别在于数据库死锁通常有内置检测器等死锁出现后自动回滚事务而AXI总线死锁没有协议级检测器只能靠验证环境或者芯片里的看门狗复位来恢复。所以设计阶段把死锁干掉比事后怎么恢复重要得多。5. 设计与协议层面的避坑清单最后这部分是我个人在实际项目打磨中沉淀下来的几条硬规则每一条都对应过一次或多次死锁教训。有些看着简单但在大型互连系统里跨模块协作时很容易被忽略。5.1 握手层级上的“总是前进”设计原则给所有AXI slave写RTL时我给自己定了一条规矩AWREADY的表达式里不允许出现任何W通道或B通道相关信号WREADY的表达式里不允许出现任何AW通道或B通道相关信号。这句话听上去很绝对但实际执行下来几乎不会影响性能因为AXI本来就是多通道独立设计的。AW通道和W通道的资源分配完全可以分开管理地址FIFO管地址数据FIFO管数据两者各看各的空满互不牵扯。这条规矩的底层逻辑就是“总是前进”任何一个通道的握手只要对应通道自己的资源准备好了就可以独立完成。即使数据FIFO满了只要地址FIFO还有空位AW握手应该照常完成即使地址FIFO满了只要数据FIFO还有空位W握手也应该照常完成。两个通道各自为战整个写事务就能在部分资源不足时继续往前挪。相反如果实现里为了“保持顺序”而强行让一个通道等待另一个通道那就是在制造人工依赖离死锁就不远了。当然这个原则要处理一个细节如果设计确实不允许事务的AW和W错得太远比如地址和数据的配对必须严格对应那么必须保证地址FIFO和数据FIFO的容量足够覆盖两者之间可能的最大偏移。否则可能出现“地址FIFO满但数据FIFO空”的错位问题。但这和死锁是两回事它最多降低吞吐但不会导致总线hang死。5.2 高优先级的Scenario让master支持数据早到在做一个CPU子系统的写请求路径时我通常会在master侧设计一个很小的写数据缓冲。这个缓冲不一定要很深但它能实现一个关键能力当AW通道被slave反压时master仍然可以把W数据推动到总线上让slave的数据侧先消化掉一部分压力。这样即使slave的awready依赖数据FIFO数据一旦被收走fifo_full自然清除awready也会释放。从验证角度看master是否支持数据早到应该作为协议一致性检查的必测项。具体做法是在随机测试里人为持续拉低AWREADY若干拍同时保持WREADY拉高观察master的WVALID是否上升。如果master在AWREADY为低时始终不拉WVALID说明它没有数据早到能力这时候就需要把“AW被反压”和“slave数据FIFO满”这两个条件组合成定向用例确认不会死锁。如果不幸死锁了趁早让设计改成数据早到或者调整slave的反压逻辑不要留到芯片回来再用实时光脚踩坑。5.3 常见问题速查表把我在不同项目中遇到过的AW-W依赖相关现象汇总成表方便大家对照快速定位。场景信号现象根因解决偏方slave数据FIFO满AWREADY0WVALID0slave的awready依赖data_fifo_full修改awready生成只依赖地址FIFOmaster严格顺序AWVALID1WVALID0master在ADDR_PHASE等握手master增加数据早到能力interconnect桥接反压AWVALID1WVALID0桥内部FIFO满桥把W反压带到了AW侧桥内部AW和W缓冲区独立管理两个master竞争slave其中一个master的AW长期等不到仲裁器分配给低优先级master的槽位被W阻塞检查仲裁器是否对不同通道分别仲裁写响应B反压引发连锁AWVALID1WVALID1BREADY0且BVALID0slave的B响应必须等AW和W都完成但AW等待B槽位释放给B通道独立分配响应缓存表格里有一个很容易被忽略的场景写响应B通道间接导致AW-W死锁。比如slave内部的写响应队列满后某些设计会在AWREADY或WREADY上同时加反压用来避免接入了新事务但没有空间返回响应。如果响应队列长期满且master在收到B响应前不允许发新事务那最终可能会出现所有AWBW通道卡住的活锁。排查时不要只盯AW和W把B通道也纳入等待图。5.4 验证环境里做一组“反压风暴”回归最后一个建议是在回归用例里加入一组长反压随机顺序的组合。具体来说我会在UVM环境里通过一个assert_race或者force的方式把slave的AWREADY用一段很长的低电平脉冲控制同时在WREADY上使用随机有效位制造AW通道被压住但W通道可以走的极端场景。如果设计和验证代码的规则没问题这个用例应该能顺利跑完如果跑挂十有八九就是AW-W依赖死锁。跑完这组回归后再补一组反向用例压低WREADY保持AWREADY高让数据通道被压住但地址通道能走。这两组用例能把“两个方向都只能等对方”的死锁条件覆盖比单纯随机激励更有效。我在自己的环境里把这种用例叫“反压风暴”每次集成新IP、新互联桥时必跑一次目前已经替我抓出了三轮真实的AW-W死锁。最后再分享一个习惯在每次流片前的门级仿真阶段我会把总线死锁检测的超时时间调成正常延时上限的三到五倍一旦触发就打印全通道状态快照。在SoC上百个master/slave的真实互联场景中有些死锁只有在特定初始化序列和随机反压组合下才会出现。早一点在仿真中抓到比带着一颗“会间歇性死机”的片子去做bring-up要踏实得多。AXI的AW-W依赖问题看似简单但越是在复杂互联里越需要通过这种系统性的验证手段去防患于未然。