简介:AXI总线协议规范是一份面向SoC设计、FPGA开发与IC验证学习者及工程师的协议入门与查阅资料,围绕ARM AMBA 3.0中的AXI展开,帮助读者理解高性能、高带宽、低延迟片内总线的设计动机与关键机制。内容涵盖AXI单向通道体系结构、读地址/读数据/写地址/写数据/写响应五个独立通道、Valid-Ready握手、突发传输、非对齐访问、乱序响应及低功耗进出握手,并与AHB作性能对照,延伸到处理器、存储器与DMA控制器之间的典型互连场景。资源包共1个doc文件,约77KB,篇幅精炼,适合快速建立术语与框架。已有429人学习下载,可作为查阅AXI通道时序、整理SoC总线知识或准备相关面试与课程学习的参考资料。
1. 先给一个反直觉的结论:AXI 快,不是因为它跑得频率高
很多人第一次看 AXI 的通道图,第一反应是「线怎么这么多」。五个通道、几十根信号,相比 AHB 那套地址数据复用、读写共享的写法,面积明显更大。按常理,线多功耗高、时序更难收敛,性能应该更差才对。但实际做 SoC 集成的血泪经验恰好相反:在同样工艺、同样时钟频率下,AXI 的吞吐经常能把 AHB 拉开一大截。
原因不在频率,而在于 AXI 允许主设备「说话不算数」——地址发出去之后,不必等数据回来就能继续发下一个请求。这种不等待的能力,把「控制流」和「数据流」在时间上彻底解耦了,才有了 outstanding、乱序、突发这些机制存在的空间。搞懂 AXI,本质上就是搞懂它如何在五条通道上把「谁先动、谁等谁」这件事安排明白。下面从通道结构一路拆到握手规则和 ID 归位。
2. 五通道分离:地址与数据为什么必须分家
2.1 从共享总线到单向通道,省掉的不只是线
早期片内总线把地址、控制、写数据、读数据塞进同一组物理线,靠分时复用来完成一次传输。这种结构的问题不在于慢,而在于每一拍都得明确「当前这组线上跑的是什么」。读操作要先发地址,等 slave 把数据准备好,再反转总线方向把数据送回来,中间还要插入一个 turnaround 周期防止两边同时驱动。时钟频率一高,这个方向切换就成了时序收敛的老大难。
AXI 的做法是把这个复用彻底拆掉:读地址、读数据、写地址、写数据、写响应各走各的线,每一条通道内部只朝一个方向流动。单向的好处是每个方向上的信号只由一个驱动源控制,中间不需要三态门,也不需要方向切换的等待拍。桥接两个时钟域时,你只需要在单向路径上插一级同步器,不用操心「这根线这会儿是谁在驱动」。门数量下来了,延迟也就跟着下来了。
更关键的是,地址通道和数据通道的相位独立之后,slave 可以在收到地址的下一拍就开始处理,而不必等数据到齐。读方向上,master 可以先连发几个读地址,数据什么时候回来是 slave 的事;写方向上,地址先到、数据后到也完全合法。这种「控制先行、数据后随」的流水方式,才是吞吐提升的真正来源。
2.2 五条通道各自的职责与关键信号
把五个通道摊开看,职责边界其实非常清晰,每个通道都有自己的 VALID/READY 对,互不干扰。
| 通道 | 方向 | 关键信号 | 承担的工作 |
|---|---|---|---|
| 写地址 AW | master → slave | AWADDR、AWLEN、AWSIZE、AWBURST、AWID、AWVALID/AWREADY | 告诉 slave 这次写从哪里开始、写多长、怎么递增 |
| 写数据 W | master → slave | WDATA、WSTRB、WLAST、WVALID/WREADY | 逐拍搬运写数据,WSTRB 标记哪些字节有效 |
| 写响应 B | slave → master | BRESP、BID、BVALID/BREADY | 整个 burst 写完后给一个完成回执 |
| 读地址 AR | master → slave | ARADDR、ARLEN、ARSIZE、ARBURST、ARID、ARVALID/ARREADY | 发起一次读请求,参数语义与 AW 对称 |
| 读数据 R | slave → master | RDATA、RRESP、RID、RLAST、RVALID/RREADY | 逐拍返回读数据,RLAST 标记最后一拍 |
写响应通道容易被忽略,但它是写操作能「不等待」的前提。master 把地址和数据都推出去之后就可以去干别的,slave 写完存储体再通过 B 通道回一个 BRESP,告诉 master 这笔写到底成功了没有。读方向没有独立的响应通道,RRESP 直接挂在读数据上一起回来,因为读本来就有一个数据回传的过程,顺路带个状态最省事。
读写的对称性在这里体现得很明显:AW 和 AR 的信号集合几乎一一对应,W 和 R 也类似。这种对称设计让主从接口的实现可以复用同一套状态机骨架,做多层互连时路由逻辑也好写——地址往哪走、数据往哪回,规则是一致的。
2.3 相位分离带来的时序收敛红利
地址和数据分开之后,一条传输不再要求所有信号在同一拍有效。AWVALID 拉高的那一拍,WDATA 可能还在路上;RVALID 回来的那一拍,master 可能还在处理上一笔读。每对 VALID/READY 各自独立握手,意味着每条通道可以有自己的流水深度和延迟预算。
这对后端设计是实打实的减负。综合工具在优化一条单向路径时,只需要考虑一个驱动源到一个负载的延迟;而共享总线那种双向结构,工具得同时兼顾两个方向的时序,往往要在中间插 buffer,面积和功耗都上去了。通道分离之后,每条路径的时序约束更干净,跨时钟域桥接的握手逻辑也更简单——这也是原文反复强调「容易进行时序收敛」的实际含义。
还有一点常被忽视:单向通道让信息流在物理上不会反向,减少了长线之间的耦合和串扰风险。在复杂的片上系统里,总线往往要横跨大半个芯片,信号走向一致对布线友好得多。
3. 握手的两种形态:VALID/READY 与低功耗 CSYSREQ/CSYSACK
3.1 VALID 不能等 READY:依赖方向决定了谁先动
AXI 每条通道都用一对 VALID/READY 做流控,规则看着简单,但里面藏着一条不能破的依赖关系:VALID 一旦拉高,就必须保持到 READY 拉高为止,中间不能撤销;而 READY 可以在 VALID 之前或之后拉高,完全由接收方决定。
这条规则的本质是防死锁。假设允许发送方「先看 READY 再决定要不要拉 VALID」,那发送方等接收方、接收方等发送方,两边互相观望,通道就永远动不了。所以协议强制规定:VALID 只能由发送方根据自己的意愿拉高,不能以 READY 为前提;READY 可以由接收方等 VALID 到了再拉。方向定死,依赖图就是无环的。
用一段断言把这个约束固化下来,是验证阶段最常见的做法:
// 检查:VALID 拉高后,在 READY 到来之前不允许撤销 property p_valid_stable(logic valid, logic ready, logic clk, logic rst_n); @(posedge clk) disable iff (!rst_n) (valid && !ready) |=> valid; endproperty // 五条通道逐一绑定,任何一条违反都说明 master 或 slave 的状态机写错了 a_aw_valid_stable: assert property (p_valid_stable(AWVALID, AWREADY, ACLK, ARESETn)); a_w_valid_stable : assert property (p_valid_stable(WVALID, WREADY, ACLK, ARESETn)); a_ar_valid_stable: assert property (p_valid_stable(ARVALID, ARREADY, ACLK, ARESETn)); a_r_valid_stable : assert property (p_valid_stable(RVALID, RREADY, ACLK, ARESETn)); a_b_valid_stable : assert property (p_valid_stable(BVALID, BREADY, ACLK, ARESETn));这段代码里|=>表示下一拍蕴含,disable iff在复位期间关闭检查。参数 valid 和 ready 是通道对应的握手信号,clk 和 rst_n 是全局时钟复位。断言命中说明有人在 VALID 拉高后、READY 还没来的时候把 VALID 撤了,这在实际波形里表现为「握手看起来完成了,但数据其实没被接收」,是仿真和上板行为不一致的经典来源。
注意,VALID/READY 是逐拍发生的,一个 burst 有多少拍就要握手多少次。WLAST 和 RLAST 只负责标记「这是最后一拍」,不改变握手规则。很多人第一次写 slave 会误以为 WLAST 拉高那拍可以不看 WREADY,结果最后一拍数据被吞掉。
3.2 低功耗握手:CSYSREQ/CSYSACK 是一对状态迁移信号
AXI 在数据通道之外还定义了一套低功耗控制接口,用的是另一套握手语义。核心信号有三个:CACTIVE、CSYSREQ、CSYSACK。CACTIVE 由设备驱动,高电平表示「我还需要时钟」;CSYSREQ 由系统时钟控制模块驱动,拉低表示请求设备进入低功耗状态;CSYSACK 由设备驱动,用来回应这个请求。
时序上分四段:正常状态下 CSYSREQ 和 CSYSACK 都为高;T1 时刻系统拉低 CSYSREQ;T2 时刻设备拉低 CSYSACK,表示已进入低功耗;T3 时刻系统把 CSYSREQ 拉高,表示要求退出;T4 时刻设备把 CSYSACK 拉高,表示已退出。整个过程是电平握手,但语义是状态迁移——一次进入、一次退出,各握手一次,不像数据通道那样每拍都握。
这里最容易理解错的是 CACTIVE 和 CSYSACK 的分工。CSYSACK 只表示「状态迁移收到了」,它不表达设备到底愿不愿意进低功耗;真正表达意愿的是 CACTIVE。设备如果还在忙,就把 CACTIVE 拉高,系统看到这个电平就知道现在不能关时钟,哪怕 CSYSACK 已经按流程拉低也没用。原文说得很直白:CACTIVE 一直拉高就表示当前不接受低功耗请求,判断依据是这个电平,而不是 ACK。
系统层面有两种管理策略。第一种是系统主动轮询,挨个看哪些设备可以进低功耗,把对应的 CACTIVE 拉低、再走 CSYSACK 流程。这种做法耦合性强,系统不知道设备什么时候空闲,只能按时间盲查,效率一般。第二种是设备主动,设备通过 CACTIVE 告诉系统「我现在可以关了」,系统被动接受再启动流程。两种可以混用,取决于具体 IP 的功耗管理粒度。
3.3 两种握手不能混为一谈
把两组信号放在一起对比,差异就出来了:
| 维度 | VALID/READY | CSYSREQ/CSYSACK |
|---|---|---|
| 触发频率 | 每个数据 beat 一次 | 每次进出低功耗一次 |
| 语义 | 这一拍数据是否有效 | 状态机是否完成迁移 |
| 意愿表达 | VALID 即意愿 | CACTIVE 单独表达,ACK 不表达 |
| 撤销规则 | VALID 拉高后不可撤 | 请求可在外设拒绝时保持 |
混用的后果很直接:把 CSYSACK 当成「设备同意」的信号去关时钟,遇到一个 CACTIVE 还拉高的设备,时钟一关设备状态就丢了,下次唤醒直接跑飞。反过来,把 CACTIVE 当成数据通道的 valid 去用,时序上根本对不上。两套握手服务的是两个层次的问题,一个是拍级流控,一个是状态级电源管理,不能互相替代。
4. 突发与乱序:首地址、ID 和 outstanding 如何撑起吞吐
4.1 突发只给首地址:AxLEN/AxSIZE/AxBURST 三个参数怎么算地址
一次 AXI 传输可以携带多拍数据,地址通道只在第一拍给出起始地址,后续每拍的地址由 slave 根据 AxLEN、AxSIZE、AxBURST 三个参数自己推算。AxLEN 是 burst 长度减一,所以 8 拍传输对应 AxLEN=7;AxSIZE 表示每拍传输的字节数,编码是 2 的幂,AxSIZE=2 表示 4 字节;AxBURST 决定地址怎么变,FIXED 表示地址不变、INCR 表示递增、WRAP 表示回绕。
INCR 模式下第 n 拍的地址等于起始地址加上 n 乘以每拍字节数;WRAP 模式下地址在一个固定边界内回绕,边界大小等于传输总字节数,而且起始地址必须按每拍字节数对齐,长度只能是 2、4、8、16 这几种。FIXED 一般给 FIFO 这类外设用,地址不变但每拍数据都往同一个端口灌。
这里有一条硬规则:一个 burst 不能跨越 4KB 地址边界。原因是 slave 通常以 4KB 为粒度做地址译码,跨边界意味着一次传输要落到两个不同的存储体上,路由逻辑没法处理。写 master 的时候如果长度和起始地址没算好,越界是迟早的事。
def burst_addr(start, index, size, burst, length): # size 是每拍字节数(2 的幂),length 是 burst 总拍数 step = 1 << size if burst == "FIXED": return start if burst == "INCR": # 第 index 拍地址,index 从 0 开始 return start + index * step if burst == "WRAP": total = step * length # 回绕窗口大小 base = start & ~(total - 1) # 对齐到窗口起点 offset = (start - base + index * step) % total return base + offset raise ValueError("unknown burst type") # 检查是否跨 4KB 边界:最后一个字节的页号和起始页号必须一致 def cross_4k(start, size, burst, length): if burst == "FIXED": return False last = burst_addr(start, length - 1, size, burst, length) + (1 << size) - 1 return (start >> 12) != (last >> 12)这段代码把三种 burst 的地址推导写清楚了,cross_4k用来在仿真阶段做前置检查。参数 start 是首地址,index 是第几拍,size 是 AxSIZE 编码,length 是实际拍数。实际项目里我会在 master 的驱动层挂一个 assert,只要 cross_4k 返回真就直接报错,比等到 slave 返回 SLVERR 再回头查波形快得多。
非对齐传输的处理也在这套地址规则里。AXI 允许首地址不按传输大小对齐,此时第一拍的低位字节用 WSTRB 屏蔽掉,从第二拍开始地址自然对齐。很多人以为非对齐是「每拍都不对齐」,其实只有首拍需要特殊处理。
4.2 outstanding:主设备为什么可以「不等回包」
outstanding 指的是 master 发出请求后不等待响应、继续发下一个请求的能力。读方向上,master 可以连续发多个 AR,slave 按自己的节奏把数据通过 R 通道送回来;写方向上,master 可以把 AW 和 W 都推出去,然后等 B 通道的响应。这种能力的前提就是前面说的通道分离——地址通道和数据通道各有各的握手,互不阻塞。
outstanding 的深度不是无限的,它取决于两边 FIFO 的容量和 ID 的数量。master 侧要记录每个未完成请求的 ID 和参数,slave 侧要缓存待返回的数据,互连里还要为每个 ID 维护路由状态。深度开太大,面积吃不消;开太小,流水灌不满,吞吐上不去。实际项目里这个值通常是仿真扫出来的,不是拍脑袋定的。
4.3 乱序与 ID:数据回来时怎么知道属于谁
允许 outstanding 之后,必然出现「后发的请求先回来」的情况。AXI 用 ID 来管理这种乱序:每个请求带一个 AxID,slave 返回数据时把同一个 ID 挂在 RID 或 BID 上。规则是同一 ID 的传输必须保序,不同 ID 之间可以乱序。
这条规则的设计意图很清楚。同一个 master 发出的同 ID 请求,往往有先后依赖,比如先写配置寄存器再读状态寄存器,顺序错了结果就没意义;而不同 ID 通常来自不同的 master 或者不同的数据流,彼此独立,谁先回来都行。互连根据 ID 把返回数据路由回对应的 master,slave 根据 ID 决定哪些请求可以插队。
slave 侧实现乱序,常见的思路是给每个 ID 开一个重排缓冲区:请求进来时按 ID 入队,哪个 ID 的数据先准备好就先返回,返回时把 ID 原样带出去。master 侧则要根据 RID 把数据分发到对应的接收队列,不能假设「第一个回来的就是第一个发出去的」。
// slave 侧乱序返回的简化状态:按 ID 维护就绪队列 // 同一 ID 内部用 FIFO 保序,不同 ID 之间由仲裁器决定谁先出 always_ff @(posedge ACLK or negedge ARESETn) begin if (!ARESETn) begin for (int i = 0; i < NUM_IDS; i++) id_valid[i] <= 1'b0; end else begin // 某个 ID 的数据准备好后置位,返回一拍清一位 for (int i = 0; i < NUM_IDS; i++) begin if (data_ready[i] && !id_valid[i]) id_valid[i] <= 1'b1; else if (id_valid[i] && RVALID && RREADY && RID == i) id_valid[i] <= 1'b0; end end end这段逻辑里id_valid每一位对应一个 ID,表示该 ID 是否有待返回的数据;data_ready来自存储体,表示数据已经取到。仲裁器从id_valid里挑一个置位的 ID 输出到 RID,同一 ID 内部再按入队顺序取。参数 NUM_IDS 是 ID 位宽决定的队列数量,实际实现里还要考虑每个 ID 的深度。这段代码省掉了仲裁和 FIFO 细节,但保序和乱序的分界点就在这里。
5. 排查:AXI 联调里最容易翻车的几处
5.1 仿真过了,上板数据错位——VALID 中途被撤销
现象:RTL 仿真跑得好好的,上板之后偶尔读回的数据整体偏移一拍,重跑又不复现。
原因:master 的读数据接收状态机在 RVALID 和 RREADY 同时为高的那拍做了额外判断,某个分支下把 RVALID 当成了「可以商量的信号」,在 RREADY 还没来的时候提前撤了。仿真里 slave 的时序恰好没触发这个分支,上板后时序抖动一进来就露馅。
解决:回到协议规则本身,VALID 拉高后只能等 READY,不能主动撤。把第 3 章那段断言挂到所有通道上,仿真阶段就拦住。上板后用逻辑分析仪抓 RVALID/RREADY/RLAST 三根线,看握手拍数是否等于 burst 长度,对不上就是这类问题。
5.2 读数据错位到下一个 4KB 页——burst 跨边界了
现象:连续读一段内存,前面数据都对,到某个固定偏移之后开始整体错位,偏移量正好是 4096 的整数倍。
原因:master 算 burst 长度时只看了「还剩多少数据」,没看当前地址到 4KB 边界还剩多少空间,导致一个 burst 跨越了页边界。slave 的地址译码在边界处切到了另一个存储体,后半段数据回不来或者回错位置。
解决:在 master 的请求生成逻辑里加一道切分,每次 burst 长度取「剩余长度」和「到 4KB 边界剩余空间」的较小值。第 4 章那个 cross_4k 检查函数就是干这个的,放在驱动层做前置断言,比事后查波形省时间。
5.3 系统偶发挂死——ID 位宽在互连里被截断
现象:单 master 测试全过,多 master 一起跑的时候偶发挂死,复位能恢复,但复现概率很低。
原因:互连模块为了省面积,把 AxID 的高位截掉了几位做路由,两个不同 master 的请求被映射成了同一个 ID。按协议同 ID 必须保序,slave 把后到的请求排在前面的请求后面等,而前面的请求因为路由错误一直没回来,整条链路就卡住了。
解决:互连的 ID 位宽必须覆盖所有上游 master 的 ID 空间,截断等于人为制造假依赖。检查方法是把所有 master 的 ID 全排列跑一遍,看互连输出的 ID 是否唯一。如果面积实在紧张,可以在互连入口给每个 master 的 ID 加一段前缀做区分,而不是简单截位。
5.4 低功耗进不去也出不来——CACTIVE 和 CSYSACK 搞混了
现象:系统想关某个外设的时钟,CSYSREQ 拉低之后 CSYSACK 也拉低了,时钟一关外设就失联,唤醒后状态全乱。
原因:设计者把 CSYSACK 当成了「设备同意进入低功耗」的依据,没看 CACTIVE。实际上 CSYSACK 只表示状态迁移的信号收到了,设备可能还在忙,CACTIVE 仍然是高。时钟控制模块没等 CACTIVE 拉低就关了时钟,设备正在处理的传输直接丢状态。
解决:时钟门控的使能条件必须是「CSYSACK 已拉低且 CACTIVE 已拉低」,两个条件缺一不可。第 3 章讲的两种管理策略里,无论用轮询还是设备主动,CACTIVE 都是最终裁决信号。抓波形时重点看关时钟那一拍 CACTIVE 的电平,这是最容易翻车的地方。
5.5 写响应丢了——WLAST 和 BVALID 的关系理解错
现象:写操作的数据都灌进去了,但 master 一直等不到 B 通道的响应,超时之后报错。
原因:slave 的写响应逻辑写成了「每收到一拍 W 数据就回一个 B」,或者反过来「WLAST 到了就立刻回 B,不管数据有没有收完」。前者会让 master 收到多余的响应,后者在 W 通道还有未完成握手时提前发 B,master 状态机对不上。
解决:B 通道的响应必须在一个 burst 的所有写数据都完成握手之后才能发出,且一个 burst 只回一个 B。判断依据是 WLAST 与 WREADY 同时为高的那一拍,从这拍之后才允许拉 BVALID。验证时把 WLAST、WVALID、WREADY、BVALID 四根线放在一条时间轴上对,响应拍数必须和 burst 数一一对应。
6. 设计取舍:什么时候 AXI 反而不如 AHB
AXI 的性能优势建立在通道数量和并发能力上,代价是面积和验证复杂度。五条通道意味着五套 VALID/READY 状态机,互连里还要为每个 ID 维护路由和重排逻辑,寄存器数量比 AHB 高出一个量级。对于一个只需要低速配置寄存器的小外设,挂 AXI 全功能接口纯属浪费,这也是后续演进里出现精简版接口的原因——只保留基本传输能力,砍掉 outstanding 和乱序,面积能降下来一大截。
选型时我一般看三个维度。第一是并发需求,多 master 同时访问不同 slave、或者单 master 需要流水发请求,AXI 的分离通道才有意义;如果系统里只有一个 master 顺序访问,AHB 那种共享结构反而更省。第二是带宽和延迟预算,高带宽场景下 AXI 的突发和 outstanding 能把流水灌满,低带宽场景这些机制用不上,只剩面积开销。第三是验证成本,AXI 的协议检查项比 AHB 多得多,断言、覆盖率、乱序场景都要单独构造,项目周期紧的时候这笔账要提前算。
验证方法上,除了前面提到的 VALID 稳定性断言和 4KB 边界检查,我还会做两件事:一是把所有 ID 的组合跑一遍乱序注入,确认同 ID 保序、异 ID 可乱序;二是把 outstanding 深度调到边界值,看 FIFO 满和空的时候握手会不会卡死。这两类场景在正常随机激励里很难覆盖到,必须定向构造。
说个自己的教训。早年做一个图像处理 Demo 的片内互连,为了省面积把 ID 位宽砍了一半,单测全过,系统联调时偶发挂死,查了整整两周才定位到是假 ID 冲突。从那以后我每次做互连配置,都强制走一遍 ID 全排列检查和 4KB 边界扫描,不管模块多小。希望这些踩坑记录能帮到你,少走一段弯路。
本文还有配套的精品资源,点击获取