AXI总线五通道解耦、outstanding与乱序机制解析 简介这份《AXI总线协议规范》文档面向SoC、FPGA及芯片设计领域的学习者与工程师系统讲解ARM AMBA 3.0框架下AXI高性能、高带宽、低延迟片内总线的核心机制可作为理解总线协议、开展IP互联设计的入门参考。文档围绕读地址与控制、读数据、写地址与控制、写数据、写响应五个独立通道展开说明地址/控制与数据相位分离、突发传输仅需首地址、非对齐访问、乱序响应及outstanding操作等特性并对比AXI与AHB在性能和带宽上的差异。同时文中还介绍低功耗模式进出时的握手协议、CACTIVE/CSYSREQ/CSYSACK信号作用以及处理器、存储器、DMA控制器等典型应用场景便于读者建立从协议概念到系统集成的整体认识。资源包共1个doc文件大小约77KB轻量便携适合快速查阅。目前已有430人学习可供总线协议入门与复习使用。1. AXI 总线的性能账单笔传输其实更慢了把 AHB 和 AXI 的同一笔读操作摊在波形上数拍子AXI 往往更慢地址要握手、数据要握手、写还要多一个响应握手。可一旦系统里挂上几个主设备、几块存储、几个 DMA 控制器AXI 的吞吐能拉开一个量级。AXI 总线协议规范里那些看起来啰嗦的通道和信号本质上是在做一件事——把「等」这件事从关键路径上拆下来挪到别的地方去消化。搞懂这套拆分方式比背信号名有用得多。你在做 IP 集成、看波形、调互联的时候遇到的大部分怪现象根子都在这套设计取舍里。2. 五通道分离地址和数据为什么要分家2.1 通道切分不是分类是把相位解耦AXI 定义了五个通道读地址、读数据、写地址、写数据、写响应。刚接触的时候很容易把它当成「把信号按功能分个组」实际不是。真正的变化是相位关系被拆开了地址和控制信息相对数据可以独立推进。主设备先把地址发出去数据可以晚几个周期再来中间这段空隙不是浪费是可以塞进另一笔事务的。AHB 上地址相位和数据相位是绑死的每一笔传输必须连续走完一个等待周期就把整条总线钉在原地。AXI 把这个绑定解开之后控制流的处理和数据的搬运就能各自流水起来。这种解耦带来的第一个直接好处是时序收敛。地址通道逻辑浅、位宽窄可以往高频推数据通道位宽大、扇出多走线和逻辑都更重频率上不去也没关系靠位宽补。两者分开做约束工具不用在一条路径上同时满足两种互相打架的目标。第二个好处是接口对称。AXI 的主从接口在读写两个方向上是镜像的同一套握手规则复用在五个通道上。做互联的时候你只需要写一份通道模板实例化五遍加不同的位宽和方向参数这比给每种传输单独写一套状态机省太多事了。2.2 单向通道三态总线的坟头草早期共享总线为了省引脚数据线是双向的多个主设备靠仲裁器决定谁驱动。片上系统里这么干代价很大双向信号在综合时要推断出三态门物理实现上要处理使能方向和竞争跨时钟域的时候更麻烦——双向信号没法简单地打一拍你还得同时处理方向翻转的时刻。AXI 的每个通道都是严格单向的信息只朝一个方向流。多主多从的连接靠互联结构里的多路选择器完成不再有双向驱动。单向带来的连锁反应比想象中大。跨时钟域桥接变得极其简单单向信号加一级同步器就能过去不用管方向握手。做寄存器切片register slice也很自然在通道中间插一级触发器把长线打断时序立刻好看。多层互联、片上网络这类结构能落地前提就是通道单向、可以随便切段。对比项AHBAXI数据线方向双向共享每个通道单向读写通道共用一套读写完全独立握手方式从设备单向拉低 HREADYVALID / READY 双向未完成事务基本不支持支持多笔 outstanding返回顺序严格保序允许乱序非对齐访问不支持支持靠字节选通2.3 VALID/READY双向流控替代单向 HREADYAHB 的流控是单向的从设备拉低 HREADY主设备只能等。这套机制简单但把主动权全给了从设备主设备没法表达「我这笔数据还没准备好」。AXI 改成两边各有一个信号发起方用 VALID 表示「我手里的地址或数据有效」接收方用 READY 表示「我能接」。两个信号独立谁也不许等谁。规则里有两条是硬约束违反了仿真不一定报错但上板必炸。第一条VALID 一旦拉高在对应的 READY 到来之前不许撤销也不能改变数据内容。第二条VALID 不允许组合依赖 READY否则两端互等就是死锁。第二条经常被写成「VALID 可以晚于 READY但不能由 READY 组合产生」本质是要求握手路径上不能有组合环。反压就建立在这套双向握手上。下游忙READY 拉低上游的 VALID 挂在那里不动数据原地待命不丢也不重复。上游忙VALID 不拉高下游的 READY 拉高也没用白等。任意一端慢整条链路自动降速不需要额外的流控协议。2.4 从握手到 skid buffer为什么要多一级寄存VALID 不许撤销这条规则在组合逻辑很深的设计里会变成麻烦。假设上游的 VALID 是由一堆条件算出来的中途某个条件变化导致 VALID 掉了而此时 READY 恰好没来这笔事务就丢了。常见的解法是在中间插一个浅缓冲把「已经决定要发」和「下游接不接」这两件事隔开。// 一级 skid buffer保证 VALID 一旦拉高不因下游未 READY 而撤销 module skid_buffer #(parameter W 32) ( input wire clk, input wire rst_n, // 上游侧 input wire s_valid, output wire s_ready, input wire [W-1:0] s_data, // 下游侧 output reg m_valid, input wire m_ready, output reg [W-1:0] m_data ); reg buf_valid; reg [W-1:0] buf_data; // buffer 空的时候才允许接新数据否则会把未送走的数据覆盖 assign s_ready m_ready | ~buf_valid; always (posedge clk or negedge rst_n) begin if (!rst_n) begin m_valid 1b0; m_data {W{1b0}}; buf_valid 1b0; buf_data {W{1b0}}; end else if (s_valid s_ready) begin if (m_valid !m_ready) begin // 下游还没接走旧数据新数据先落 buffer buf_valid 1b1; buf_data s_data; end else begin m_valid 1b1; m_data s_data; end end else if (m_valid m_ready) begin if (buf_valid) begin // 下游接走旧数据buffer 里的顶上 m_data buf_data; buf_valid 1b0; end else begin m_valid 1b0; end end end endmodule这段代码的关键在s_ready m_ready | ~buf_valid这一行。buffer 空的时候上游可以直接穿过去buffer 满了上游必须停。注意它没有用m_ready去组合地影响m_validm_valid只在时钟沿更新握手路径上不存在组合环。为什么一级就够因为 AXI 规定 VALID 不许撤销最坏情况是「下游拒收一拍、上游又送来一拍」此时只需要暂存这一拍。深度为 2 的存储一拍在主路径、一拍在 buffer足以吸收这种背靠背的冲突。再深就是性能优化而不是功能需求了通常出现在跨时钟域或者长线延迟补偿的场景。参数上W要跟通道位宽对齐。数据通道是TDATA的宽度地址通道就是地址加控制信号的打包宽度。实际工程里很少手写这个模块互联 IP 里到处都有但它的存在解释了一个常见现象为什么你在某段链路上看到 VALID 拉高后数据晚了两个周期才出现在下游——中间有切片。3. 突发、outstanding 与乱序ID 是唯一的解药3.1 一次突发只发首地址省下的是地址通道带宽AXI 的突发传输只发首地址后续每个数据 beats 的地址由从设备内部自己递增。这个设计把地址通道的带宽摊销掉了。做一块大块内存拷贝的时候地址通道只在开头忙一拍剩下的时间全是数据通道在跑地址通道可以腾出来服务别的事务。突发有三种类型。FIXED 是地址不变适合 FIFO 这类固定端口的设备INCR 是递增最常用WRAP 是回卷地址涨到边界后绕回起点专门服务缓存行填充这种场景——缓存行是固定长度的从中间地址开始的填充需要绕回去。突发类型地址行为典型用途FIXED每个 beat 地址不变FIFO、外设寄存器INCR每个 beat 递增连续内存读写WRAP涨到边界回卷到起点缓存行填充代价是从设备必须自己维护地址计数器而且要能处理边界。互联结构里通常会给突发加边界约束超出边界的突发必须拆成两笔否则从设备的地址解码逻辑会做得很复杂。这也是为什么有些主设备在发起长突发之前会先检查起始地址和长度避免跨过解码边界被拆开白白损失效率。3.2 outstanding把等待时间叠起来outstanding 的意思是主设备发完地址不等数据回来接着发下一笔。这跟 CPU 的乱序执行是同一个思路等待时间不再串行累加而是重叠起来。假设一次读操作往返要 100 个周期串行发 8 笔就是 800 个周期允许 8 笔 outstanding理论上 100 多加 7 个周期就能全部发完剩下的就是等数据陆续回来。支持 outstanding 的前提是能区分每一笔事务。AXI 用 ID 来干这件事每笔事务在地址通道上带一个 ID数据返回时把同一个 ID 带回来。主设备靠这个 ID 把回来的数据对号入座。ID 的位宽决定了互联结构内部能并行处理多少条独立的事务流位宽越宽互联可以做的并行 bank 越多但面积和路由复杂度也上去了。读写通道独立意味着读写可以同时进行。这一点在做 DMA 的时候特别明显读通道在从内存搬数据写通道同时在往外设送数据两条流互不干扰不需要像共享总线那样分时复用。3.3 乱序返回与 ID 归位从设备或者互联结构内部如果有多个可以并行的存储体不同 ID 的事务可能以不同的速度完成返回顺序就跟发出顺序不一致了。这就是乱序。乱序本身不是问题问题是主设备得知道哪笔数据对应哪次请求。协议给的约束是同一个 ID 的事务必须保序不同 ID 之间可以乱序。这条规则把复杂度分配得很清楚——从设备只要保证单个 ID 内部的顺序主设备只要为不同的用途分配不同的 ID两边都不用做全局排序。import heapq from collections import defaultdict def simulate_returns(bursts, bank_of, latency_of): 模拟从设备侧的乱序返回并检查「同 ID 保序」这条约束。 bursts: [(tid, addr, seq)]按主设备发起顺序排列 bank_of: 地址 - bank 编号不同 bank 可以并行服务 latency_of:bank 编号 - 服务延迟 heap [] for idx, (tid, addr, seq) in enumerate(bursts): bank bank_of(addr) # 简化模型完成时刻 发起时刻 该 bank 的服务延迟 finish idx latency_of(bank) heapq.heappush(heap, (finish, seq, tid)) last_seq defaultdict(lambda: -1) order [] while heap: finish, seq, tid heapq.heappop(heap) order.append((tid, seq)) # 同 ID 的事务如果后发先至就是协议违规 assert seq last_seq[tid], ( fID {tid} 发生乱序seq{seq} 早于 seq{last_seq[tid]} 返回 ) last_seq[tid] seq return order # 三笔同 ID 走同一个 bank两笔不同 ID 走另一个 bank bursts [(0, 0x1000, 1), (1, 0x2000, 2), (0, 0x1004, 3), (1, 0x2004, 4)] bank lambda a: 0 if a 0x2000 else 1 lat {0: 3, 1: 1} for item in simulate_returns(bursts, bank, lat): print(item)跑一遍会看到 ID 1 的两笔先回来ID 0 的两笔后回来但 ID 0 内部仍然是 1 在前 3 在后断言全部通过。把bank函数改成让同 ID 的两笔落到不同 bank断言立刻炸掉——这正是集成时最容易踩的坑互联结构内部做了并行 bank却没检查同一个 ID 的事务会不会被分到不同 bank 上去。参数说明finish是模拟的完成时刻实际硬件里没有这个值这里只是为了造出乱序效果last_seq记录每个 ID 最后一次返回的事务序号断言比较的就是这一项。真实环境里这条检查应该写成断言挂在仿真里一旦违规立刻报出来而不是等到波形上对着看。3.4 非对齐传输与字节选通非对齐指的是传输的起始地址不在数据宽度的整数倍上。AHB 时代这类访问要靠软件拆成两次对齐访问或者由桥接逻辑代劳。AXI 直接支持非对齐靠的是写数据通道上的字节选通信号每个字节位置对应一个选通位从设备只需要把选通为高的字节写进去其余字节保持不变。非对齐的第一拍最麻烦。假设总线宽度 32 位起始地址是 0x1002那么第一拍只有高两个字节有效选通位是4b1100不是全 1。最后一拍如果长度不是整数倍也会出现部分有效。中间各拍才是全选通。这个细节在写从设备的时候必须处理尤其是那种把字节选通直接映射到寄存器写使能的实现漏掉首拍的掩码就会把相邻字段冲掉。好处很实在拷贝一个小结构体的时候不用为了对齐先搬一次再拼一次软件侧的 memcpy 路径能省掉不少分支。代价是从设备的写逻辑要按字节而不是按字处理。4. 低功耗握手CACTIVE 才是真正的准入票4.1 三个信号的职责划分AXI 的低功耗接口是在数据传输协议之上加的一层扩展只用到三个信号针对有低功耗管理能力的设备和没有低功耗管理能力的设备都能用。CACTIVE 是设备发给系统时钟控制模块的意思是「我现在需要时钟」。这个信号的方向很关键——它由设备发起不是系统轮询出来的。CSYSREQ 是系统发给设备的表示「我请求你进入低功耗状态」。CSYSACK 是设备回给系统的应答表示「我收到了这个请求并且状态已经迁移」。三者的角色不能混CSYSACK 只负责说明状态迁移的完整性设备到底能不能关时钟由 CACTIVE 说了算。很多初学的人会以为 CSYSACK 拉低就等于可以关时钟了这个理解是错的。项目文档里说得很清楚在握手中 CACTIVE 可以作为拒绝或者同意的标志如果 CACTIVE 一直拉高就表示当前不接受低功耗请求而不是依赖 ACK 信号来表达这个意思。换句话说ACK 走的是流程CACTIVE 走的是许可。4.2 一次完整的进入/退出时序时刻CSYSREQCSYSACK含义T1 之前高高设备处于正常工作状态T1拉低高系统请求进入低功耗T2低拉低设备应答状态迁移完成T3拉高低系统请求退出低功耗T4高拉高设备应答已退出低功耗T2 和 T4 两个时刻都要注意先后关系CSYSACK 的翻转必须晚于 CSYSREQ 的翻转不能同时发生否则系统侧无法判断这次握手是新的请求还是上一次的残留。实际实现里通常用状态机保证这个次序。4.3 轮询与被动上报两种系统策略的取舍系统层面的低功耗管理有两条路。第一条是系统主动轮询设备定期检查哪些设备空闲空闲就拉低 CACTIVE再走低功耗握手。这条路耦合性强实现简单但效率低——系统并不知道某个设备什么时候会空下来只能按固定节奏查查早了没用查晚了错过窗口。第二条路是设备主动上报。设备自己知道什么时候活干完了把 CACTIVE 拉低系统收到这个信号再决定要不要发起低功耗请求。这条路效率高得多但系统可能因为预测到马上要用这个设备收到信号也不发起请求。所以它是个建议而不是命令最终决定权还在系统侧。两种方式可以混合使用。常见的做法是关键路径上的设备用被动上报边缘设备用轮询系统侧维护一张策略表。文档里特意点出这一点是因为很多设计一上来就只做一种后面发现某些设备根本没法主动上报比如没有内部状态机的纯组合逻辑外设只能返工。// 设备侧低功耗握手CSYSACK 跟随 CSYSREQCACTIVE 表达真实意愿 module pm_handshake ( input wire clk, input wire rst_n, input wire csysreq, // 来自系统时钟控制器 input wire busy, // 设备内部仍有未完成的事务 output reg csysack, output wire cactive ); reg lowp; // 已经进入低功耗状态 // 只要设备还有活干就持续拉高 CACTIVE 表示拒绝 assign cactive busy | ~lowp; always (posedge clk or negedge rst_n) begin if (!rst_n) begin lowp 1b0; csysack 1b1; end else if (!csysreq !lowp) begin lowp 1b1; // T2ACK 晚于 REQ 一拍拉低 csysack 1b0; end else if (csysreq lowp) begin lowp 1b0; // T4ACK 晚于 REQ 一拍拉高 csysack 1b1; end end endmodule这里cactive busy | ~lowp是整个模块的核心。busy为高时无论系统怎么请求CACTIVE 都是高的时钟控制器看到这个信号就不该关时钟。~lowp保证设备在正常状态下始终主张自己需要时钟。系统侧关时钟的条件应该是!cactive !csysack同时成立只满足其中一个都不能关。参数上busy需要覆盖设备所有可能产生事务的路径包括内部 DMA、缓冲刷新、状态机迁移。漏掉任何一条都会出现「CACTIVE 已经拉低但设备还在发请求」的情况时钟一关就挂死。5. 集成时最容易翻车的五处5.1 VALID 拉高后又撤销现象仿真里从设备偶发丢数据重跑一遍又不复现波形上看到 VALID 有毛刺。原因多半是发送侧的 VALID 由组合逻辑产生某个上游条件在中途变化VALID 跟着掉了而此时 READY 还没来。协议明确禁止这种行为。解决办法是把 VALID 打一拍寄存或者在中间插 skid buffer让「决定发送」和「下游接收」彻底解耦。5.2 VALID 与 READY 形成组合环现象仿真跑起来直接死锁波形上一片 X或者两个信号在同一个时刻互相等待。原因是 VALID 的组合逻辑里用到了 READY而 READY 的组合逻辑里又用到了 VALID。跨通道的依赖更容易出问题比如写数据通道的 VALID 依赖写地址通道的 READY。解决办法是把跨通道的依赖打拍或者干脆拆成独立的状态机让每个通道的 VALID 只由本通道的状态决定。5.3 同 ID 事务被下游并行打散现象主设备收到的数据顺序和预期对不上但每个 ID 单独看又没错。原因是互联结构内部做了多 bank 并行同一个 ID 的多笔事务被分到了不同 bank 上返回时自然就乱了。协议要求同 ID 保序这个责任在从设备和互联侧。解决办法是在互联的 bank 分配逻辑里加约束同一 ID 的事务必须落到同一个 bank或者在上层用不同的 ID 区分。5.4 非对齐首拍的字节选通算错现象写寄存器的时候相邻字段被莫名其妙改掉。原因是非对齐传输的第一拍只有部分字节有效如果从设备把字节选通直接当成寄存器写使能而没有按地址低位做掩码就会把不该写的字节写进去。解决办法是在从设备侧显式处理首拍根据起始地址的低位算出有效字节范围与选通信号做与运算再输出。末拍同理长度不是整数倍时要截断。5.5 低功耗握手只看 ACK 不看 CACTIVE现象设备在低功耗模式下被关掉时钟恢复后状态全乱或者干脆起不来。原因是系统侧把 CSYSACK 拉低当成了可以关时钟的条件忽略了 CACTIVE 还拉着。解决办法是把关时钟的条件改成!cactive !csysack并且在设备侧确认所有可能产生事务的路径都汇总进了 CACTIVE 的生成逻辑。这条坑的隐蔽之处在于大部分时候设备确实空闲问题要等到某个特定负载下才暴露。6. 复杂度换并行度AXI 的取舍边界在哪AXI 的设计哲学可以把账算得很直白用五倍的通道数、翻倍的信号量、一套必须严格遵守的握手规则换来的是把串行等待变成并行重叠的能力。这笔账在高端 SoC 上是划算的因为那里瓶颈在带宽不在面积但在一个只有几块慢速外设的控制器里全套 AXI 就是杀鸡用牛刀。常见的做法是主干用完整 AXI边缘挂 AXI4-Lite再往外用 APB 接寄存器三层各司其职。验证上有一件事值得单独提。AXI 的规则大多是「不许做什么」而不是「必须做什么」这类约束最容易在仿真里漏过去。我的习惯是把 VALID 不撤销、同 ID 保序、握手不出现组合环这三条写成断言挂在接口上跑随机激励的时候让断言自己去抓。覆盖率报表好看不代表协议合规断言才是那道兜底。从那以后我每次接一个新 IP不管它标称多符合规范都会先把这三条断言挂上去跑一轮。翻车最惨的一次就是某个第三方模块的写数据通道 VALID 会掉仿真跑了几十万个周期才撞上一次波形上根本看不出来是它的问题最后靠断言定位到具体那一拍。这套规范文档值得放在手边当字典翻遇到波形对不上的时候先去里面找那条被违反的规则比盲猜快得多。希望帮到你。本文还有配套的精品资源点击获取