资讯动态

FPGA AXI 总线入门:VALID/READY 握手、背压与 DMA 实战

发布时间:2026/9/18 4:07:52 来源:尧图企业网站定制
1. 从流水灯到总线零基础阶段为什么绕不开 AXI1.1 我带过的人几乎都卡在同一个地方前面十篇写下来计数器和状态机跑通了数码管动态扫描点亮了SPI 读写 Flash 也不再是问题。然后突然有一天你想在 Vivado 的 Block Design 里拖一个 AXI GPIO或者想从 DDR 里读一帧图像出来看看整个人就愣住了自动生成的代码几百行满屏的AWVALID、WSTRB、RRESP注释比代码还多一条线都看不懂。更尴尬的是这时候你连从哪里开始读都不知道。我自己第一次接触 FPGA 的 AXI 总线协议是在做一个图像缓存的小项目。当时的思路很朴素摄像头数据进来写进 DDR再从 DDR 读出来送给显示屏。用了一个厂商的 IP把线连起来综合、下载结果屏幕上什么都没有。仿真是过的波形看上去数据也在动但就是不出图。后来折腾了两个晚上才发现是我自己在读通道上没有正确处理RLAST导致突发传输的边界判断错了读回来的数据整体错位。这件事给我的教训是AXI 不是一堆要背的信号名它是一份数据交接的契约。你不需要把所有信号都记住但必须搞清楚每一次数据交接是怎么被确认的什么时候允许停什么时候不许撤。这篇文章会从零基础的角度把 AXI4、AXI4-Lite、AXI4-Stream 三兄弟的关系拆开把握手、背压、突发、仲裁、跨时钟这些工程里一定会碰到的东西讲透适合刚学完时序逻辑、准备第一次用 IP 或者第一次上板搬数据的人。1.2 AXI 解决的是谁先动手的老问题在 AXI 之前片上总线的主流是 AHB 和 APB 这类共享式总线。共享式总线的麻烦在于所有主设备轮流用同一组地址线和数据线一个时刻只能有一个主设备在说话仲裁器点名谁被点到谁上。这种方式结构简单但一旦系统里出现了 CPU、DMA、视频输入、视频输出这四个都想访问 DDR 的家伙带宽就立刻变成瓶颈。AXI 的核心变化是引入独立的多条通道和基于 VALID/READY 的握手。读写分离地址和数据分离于是地址可以先发出去、数据可以晚几拍跟上从机在处理上一笔数据的时候主机已经在准备下一笔的地址了。这种管道化的设计才是它能撑起高带宽的原因。对比一下更直观总线通道结构是否支持乱序典型场景APB单通道地址数据复用不支持低速寄存器配置AHB单通道读写分时不支持中低速、片上共享AXI4-Lite五通道无突发不支持寄存器访问AXI4五通道支持突发支持靠 IDDDR、DMA、高速数据流AXI4-Stream无地址仅数据流不适用视频、ADC、滤波链1.3 给零基础的学习次序很多教程一上来就贴出 AXI 的完整信号表一百多个信号看两眼就放弃了。我个人更推荐这个次序亲测对新手友好得多第一步只关心五条通道的名字和方向知道有一次写事务需要三次握手、一次读事务需要两次握手别的先不管。第二步用 AXI4-Lite 写一个最小的从机就用寄存器代码不超过一百行。这一步的目的是把握手的手感建立起来知道VALID什么时候能拉什么时候不能撤。第三步打开仿真看波形。把 AW、W、B 三条通道的波形并排放数一数每一拍的先后关系。这一步比读十遍协议文档都管用。第四步再上 AXI4 的突发理解AWLEN、AWSIZE、AWBURST三个参数怎么组合出一次搬运。第五步接 DMA 或者自己写 AXI-Stream 的流水链开始接触背压和跨时钟。提示如果你的开发环境里有 AXI 协议检查器Verification IP一定先挂上。它会直接报出VALID 拉高后撤回这类致命错误比你自己盯波形效率高出一个数量级。2. 五条独立通道AXI4 到底把一次读写拆成了什么2.1 写事务的三段式AW、W、B一次 AXI4 写事务被拆成了三段彼此独立、又可以并行推进的流程。**写地址通道AW**负责告诉从机我要往哪里写。关键信号有AWADDR起始地址、AWLEN突发长度、AWSIZE每次传输的字节数、AWBURST突发类型、AWID事务标识。这一条通道上只有地址和控制信息没有数据。**写数据通道W**负责把数据搬过去。WDATA是数据本体WSTRB是字节选通WLAST标记这次突发的最后一拍。注意这里的设计哲学数据通道是按拍走的不是按字节走的。哪怕你只想写一个字节整个数据总线也要占一拍只是WSTRB上只有对应那位是 1。写响应通道B是从机向主机回话的唯一通道。BRESP表示这次写成功还是失败BID对应事务标识。它只回一次出现在整个突发的最后一拍数据被接收之后。这一条很关键——很多新手以为写完数据就完事了其实从机必须回一个 B主机才能确认这笔事务真正落盘。三条通道的关系可以用一句大白话概括地址和数据谁先到都行但 B 一定在最后。AXI4 明确允许写数据先于写地址出现这在 AXI3 里是不允许的协议做了放宽代价是从机必须能缓存住早到的数据。2.2 读事务为什么只有两条通道读事务只有读地址通道AR和读数据通道R。原因很简单读操作的结果本身就是数据从机把数据顺着 R 通道推回来就等于同时完成了确认。没有单独的读响应通道。R 通道上的关键信号是RDATA、RRESP、RLAST、RID。其中RLAST是最容易出事的一个——它标记本次突发的最后一拍。如果你的从机在算最后一个拍点的时候差了一拍主机就会一直等等到超时反过来多拉一次主机就会把下一次突发的数据误当成这一次的尾巴。读通道还有一条容易忽略的约束RVALID必须在ARVALID和ARREADY同时有效之后才能拉高。也就是说读数据不能先于读地址出现。这和写数据可以抢跑是相反的因为从机根本不知道要读什么就不可能提前准备好数据。2.3 VALID 与 READY一次握手就是一次双向确认AXI 的每一次信息传递都遵循同一个模式发送方把VALID拉高表示我这边的东西准备好了接收方把READY拉高表示我这边能吃下了。两者在同一个时钟上升沿同时为高这一拍的数据才算被成功传输。这个机制看起来简单但它是整个 AXI 协议里最需要反复确认的地方。因为它同时规定了三条铁律发送方的VALID不许等READY。VALID只能由我有没有数据来决定绝不能写成assign VALID READY have_data这种形式。这是协议明令禁止的因为一旦上游靠下游的READY来决定自己要不要说话整条链路上就会形成组合逻辑环时序直接崩。VALID一旦拉高在握手成功前不许撤回。除非复位否则不能因为等太久了就把VALID拉低主机侧会当成协议错误。READY可以晚来也可以一直等。接收方有权利用READY拉低来拖延这是合法的流量控制手段也就是下一节要讲的背压。用生活场景类比一下这就像两个人递东西。递的人把东西举出来VALID接的人把手伸出来READY。举起东西之后不许再缩回去但接的人可以慢一点伸手——只要最后手伸出来这件事就算完成。规则的作用是保证双方对东西到底交没交出去永远有唯一答案。2.4 AWLEN、AWSIZE、AWBURST突发参数怎么算才不出错这三个参数是新手最容易算错的地方我建议把它们的含义写死在心里。AWSIZE决定每一拍传几个字节编码方式是2^AWSIZE。AWSIZE 2b010表示 4 字节AWSIZE 2b011表示 8 字节。注意它和总线物理宽度是两回事64 位总线上完全可以把AWSIZE设成 38 字节也可以设成 24 字节后者就叫窄传输narrow transfer此时WSTRB上只有一半的位是有效的。AWLEN决定这次突发一共几拍注意它是拍数减一。AWLEN 8d15表示 16 拍。AXI4 的AWLEN是 8 位最多 256 拍AXI3 只有 4 位最多 16 拍这也是两代协议最主要的差别之一。AWBURST决定地址怎么变只有三种合法取值编码类型地址变化规律典型用途2b00FIXED地址始终不变读写同一个 FIFO 端口2b01INCR每拍递增2^AWSIZE字节内存搬运最常用2b10WRAP递增到边界后回绕Cache line 填充INCR 是绝对主力99% 的场景都用它。WRAP 只在和 Cache 打交道的场景里出现新手可以先跳过。有一次总字节数要和地址边界一起算。举个具体例子64 位数据总线AWSIZE 38 字节AWLEN 127128 拍起始地址0x1000。那么这次突发覆盖的地址范围是0x1000到0x1000 128 × 8 - 1 0x1400 - 1总共 1024 字节。这里有一条硬性约束必须记住一次突发绝对不能跨越 4KB 边界。原因是 4KB 是内存管理的最小页单位一旦跨页从机就可能在两页之间被换出无法保证原子性。实际工程里跨页的搬运要由写代码的人自己拆成两笔突发协议不会帮你处理。另外还有一个细节AXI4 允许首拍地址不对齐但只有第一拍可以后面的拍必须落到对齐的边界上。这一点在处理图像每行起始地址不是总线宽度整数倍的时候特别有用。3. 背压与 stall握手逻辑最容易翻车的地方3.1 背压到底是从哪来的背压backpressure这个词听着唬人说白了就是下游吃不动了让上游慢点。在 AXI 里它的实现方式就是接收方把READY拉低。背压的来源通常有三个。第一个是FIFO 快满了你的从机内部有个缓冲写进去的数据要等后面的模块慢慢取走一旦水位超过阈值就得让上游停。第二个是从机忙着比如 DDR 控制器正在处理刷新或者仲裁器把带宽给了别的主设备你这一笔就先等等。第三个是下游链路本身拥堵数据从 AXI 转成 Stream再送进一个滤波器滤波器的输出端又堵住了背压会沿着链路层层往上传。这里有一个设计上的关键判断背压该在哪一级被吸收。完全不吸收背压会一路传到 CPU 或者 DMA导致整个系统卡顿到处都堆大 FIFO 去吸收BRAM 资源会被吃光而且延迟变大。我一般的做法是在模块边界放一个深度 16 到 64 的 FIFO关键是加一个**几乎满almost full**信号提前几拍就把READY拉低而不是等真的满了才拉。提前量给多少取决于下游READY撤销之后、上游真正停下之前还可能有几拍数据在路上——如果是纯寄存器打拍的链路提前 2 拍就够如果中间隔了 FIFO就要按最坏情况估。3.2 三条铁律和它们的例外前面提到的那三条握手规则实际写代码的时候经常被违反而且很多违反是仿真能过、上板出错的隐蔽型。这里再细化一下。第一VALID不能依赖READY。违反的典型写法是assign m_axis_tvalid s_axis_tvalid s_axis_tready;看着像在转发实际上制造了组合环综合器会给你警告时序报告也会惨不忍睹。正确的做法是用寄存器打一拍always (posedge clk) m_axis_tvalid s_axis_tvalid;。第二VALID拉高后不能撤回。这条在单模块里好遵守但在多模块拼接时容易出问题。比如中间某个模块做了数据过滤发现这一拍数据不合格就把它丢了结果VALID忽高忽低下游直接乱套。正确的做法是不合格的数据也要把VALID走完只是把TLAST或某个 user 位标记成无效让下游自己决定丢不丢。第三READY可以自由拉低。这条没有例外但有个实践建议READY最好也用寄存器输出而不是用组合逻辑直接算出来。原因是READY的组合路径如果很长会直接变成关键路径。用 FF 打一拍代价是增加一拍延迟收益是时序余量大增非常值。至于例外严格来说协议里只有一处缓冲主机可以在AWVALID之前就拉高WVALID。这不属于违反规则而是协议特意允许的乱序。但从机的设计难度就上来了——它必须先能接住数据再等地址到齐才能决定写到哪。手写从机的时候我建议先用最简单的状态机先等 AW再等 W虽然效率低一点但绝对不会错。等工程稳定了再考虑优化。3.3 一段仿真通过、上板出错的反例这是我踩过的一个真实坑值得完整讲一遍。当时做的是一个 AXI-Stream 的降采样模块输入 4 拍数据只保留 1 拍。我的写法是// 错误示范 always (posedge clk) begin if (s_axis_tvalid s_axis_tready) begin cnt cnt 1; if (cnt 2d3) begin m_axis_tdata s_axis_tdata; m_axis_tvalid 1b1; cnt 2d0; end else begin m_axis_tvalid 1b0; end end end assign m_axis_tlast s_axis_tlast;仿真的行为级模型跑得很顺因为在测试激励里s_axis_tready和m_axis_tready一直是高m_axis_tvalid的抖动没有引发任何问题。上板之后就不一样了下游的 FIFO 偶尔会拉低READY这时候m_axis_tvalid已经在上一拍被拉高可这一拍因为下游没准备好握手没成功我却在下一个s_axis_tvalid到来的时刻把它改掉了。等价于把VALID撤了回来下游看到的是一个幽灵脉冲数据直接错位。修法其实很直接就是加一个待发状态数据一旦准备好就锁存下来VALID拉高后只由READY来清楚不再受输入侧影响。// 修正后 reg m_valid_r; reg [31:0] m_data_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin m_valid_r 1b0; end else if (m_valid_r m_axis_tready) begin m_valid_r 1b0; // 只有握手成功才清 end else if (s_axis_tvalid s_axis_tready cnt 2d3) begin m_data_r s_axis_tdata; m_valid_r 1b1; // 拉高之后不再因为输入空闲而清掉 end end注意这类问题的可怕之处在于它在功能仿真里几乎不暴露只有在下游真的出现背压时才会露头。所以仿真激励一定要人为制造READY的随机拉低别让它一直为高。我自己现在写 testbench默认就会给下游加一个随机停 1 到 5 拍的行为。4. 把 AXI 接进真实工程互连、仲裁与数据搬运4.1 多主多从下的互连与仲裁真实的片上系统不会只有一个主机和一个从机。CPU 要访问 DDRDMA 要访问 DDR视频输入要往 DDR 写视频输出要从 DDR 读还有一堆低速外设挂在 AXI4-Lite 上。这些主设备共享 DDR 这条从设备就必须有互连和仲裁。互连Interconnect干的事是把多对多的连接关系展开成多组点到点把地址解码、位宽转换、协议转换、仲裁都包进去。仲裁器Arbiter则决定同一时刻哪个主机拿到从机的访问权。常见的仲裁策略有几种固定优先级实现最简单但低优先级的主设备在繁忙时可能被饿死**轮询round-robin**公平但高优先级的视频流可能因为等待而出现撕裂基于 QoS 或信用制的方案最灵活主设备在发起事务的时候带上 QoS 值仲裁器按权重分配带宽。工程上的经验是视频输入和视频输出这类实时流一定要给足优先级和带宽最好单独走一条互连。而 CPU 访问这类容忍延迟的事务可以走低优先级通道。我见过一个项目把摄像头和 CPU 挂在同一个低带宽互连上结果 CPU 一跑密集的中断服务画面就开始丢帧查了两周才发现是带宽被抢了。还有一个容易被忽略的点主设备的 outstanding 能力。一个主设备如果不等上一笔读返回就发下一笔读地址就形成了多笔未完成事务outstanding transaction。这个数量决定了它能压多少延迟。DDR 的延迟通常几十到上百个时钟周期如果主设备只允许一笔 outstanding那带宽利用率会被延迟吃得一干二净。这也是为什么 DMA 引擎的设计里outstanding 深度是一个核心参数。4.2 AXI DMA为什么搬数据不该让 CPU 来干回到那个经典需求场景串口一秒钟进来几兆的数据要存到 DDR 里做后续处理。如果让处理器一条一条地读寄存器、写内存每字节都要好几条指令CPU 直接被占满什么别的事都干不了。DMA 的价值就在这里。它本质上是挂在 AXI 上的一个专用搬运引擎通常有两条数据通路MM2SMemory-Mapped to Stream从内存读出来转成流和S2MMStream to Memory-Mapped从流写回内存。处理器只需要配置一次源地址、目的地址、长度然后等中断通知完成中间几兆字节的搬运全由硬件完成。用 AXI UART 16550 配合 DMA 就是一个很典型的组合。串口本身是低速的 AXI4-Lite 从机只能一拍一拍地读写寄存器。要跑高速收发就在串口和内存之间挂 DMADMA 从内存读一块数据转成 AXI-Stream送进串口发送 FIFO接收方向反过来串口把收到的字节转成 StreamDMA 攒够一定长度再一次性写进内存。这样处理器只处理块级别的调度效率提升非常明显。这类设计里有几个参数必须一起算DMA 的一笔搬运长度、Stream 位宽和串口字节流之间的转换比、中断的触发粒度。举个具体的串口波特率 115200每字节 10 位理论吞吐约 11.5 KB/s。这个速率下DMA 一笔搬 1024 字节大约 89 ms 才需要中断一次处理器压力几乎为零。但如果波特率提到 3 Mbps一笔 1024 字节不到 3.5 ms 就传完中断频率就上来了这时候就要考虑把搬运长度加大到 8KB 甚至 16KB。4.3 AXI4-Stream 与 AXI4-Lite 的取舍三兄弟里AXI4-Stream 是最好理解的因为它最简单没有地址只有数据流。核心信号就四个TDATA、TVALID、TREADY、TLAST加上可选的TKEEP、TSTRB、TUSER、TID、TDEST。什么时候用 Stream答案是数据是连续的、顺序的、点对点的时候。视频像素流、ADC 采样流、滤波器链路、编解码器输入输出这些都是天然的顺序流没有任何理由去给每一个像素附带一个地址——地址会白白吃掉带宽。什么时候用 AXI4-Lite寄存器配置。比如你要给滤波器设置系数、给 DMA 设置源地址、读一个状态位。这些操作零散、随机、低频用完整的 AXI4 太浪费资源用 Lite 正好没有突发、没有 ID、数据宽度固定 32 或 64 位逻辑量很小。两者之间转换也很常见。比如 DMA 把内存数据变成 Stream 送进处理链处理完再变回带地址的写事务存回内存。这个转换不要自己手搓厂商的 IP 里通常都有对应的转换模块配置一下就行。自己写的风险主要在TLAST和突发边界的对齐上很容易出现最后一拍少一个的问题。4.4 手写一个 AXI4-Lite 从机寄存器读写的最小实现理解了上面这些一个最小的 AXI4-Lite 从机其实不到一百行。下面这个是我常用的模板两个 32 位寄存器支持WSTRB字节选通状态机简单清晰适合拿来改。module axi_lite_reg #( parameter integer DATA_W 32, parameter integer ADDR_W 5 )( input wire aclk, input wire aresetn, // 写地址通道 input wire [ADDR_W-1:0] awaddr, input wire awvalid, output wire awready, // 写数据通道 input wire [DATA_W-1:0] wdata, input wire [DATA_W/8-1:0] wstrb, input wire wvalid, output wire wready, // 写响应通道 output wire [1:0] bresp, output wire bvalid, input wire bready, // 读地址通道 input wire [ADDR_W-1:0] araddr, input wire arvalid, output wire arready, // 读数据通道 output wire [DATA_W-1:0] rdata, output wire [1:0] rresp, output wire rvalid, input wire rready, // 对外暴露的用户寄存器 output wire [DATA_W-1:0] reg0, output wire [DATA_W-1:0] reg1 ); localparam S_IDLE 2d0; localparam S_WDATA 2d1; localparam S_WRESP 2d2; reg [1:0] wstate; reg [ADDR_W-1:0] awaddr_lat; reg [DATA_W-1:0] slv_reg0, slv_reg1; reg [DATA_W-1:0] wdata_masked; reg rvalid_r; reg [DATA_W-1:0] rdata_r; integer i; // WSTRB 字节选通只覆盖被选中的字节 always (*) begin wdata_masked slv_reg0; for (i 0; i DATA_W/8; i i 1) begin if (wstrb[i]) begin wdata_masked[i*8 : 8] wdata[i*8 : 8]; end end end // 写通道主状态机 always (posedge aclk) begin if (!aresetn) begin wstate S_IDLE; awaddr_lat {ADDR_W{1b0}}; slv_reg0 {DATA_W{1b0}}; slv_reg1 {DATA_W{1b0}}; end else begin case (wstate) S_IDLE: begin if (awvalid) begin awaddr_lat awaddr; wstate S_WDATA; end end S_WDATA: begin if (wvalid) begin case (awaddr_lat[ADDR_W-1:2]) 3d0: slv_reg0 wdata_masked; 3d1: slv_reg1 wdata_masked; default: ; // 访问未定义地址回 OKAY 但不写 endcase wstate S_WRESP; end end S_WRESP: begin if (bready) begin wstate S_IDLE; end end default: wstate S_IDLE; endcase end end assign awready (wstate S_IDLE); assign wready (wstate S_WDATA); assign bvalid (wstate S_WRESP); assign bresp 2b00; // OKAY // 读通道单笔 outstanding always (posedge aclk) begin if (!aresetn) begin rvalid_r 1b0; rdata_r {DATA_W{1b0}}; end else if (arvalid arready) begin rvalid_r 1b1; case (araddr[ADDR_W-1:2]) 3d0: rdata_r slv_reg0; 3d1: rdata_r slv_reg1; default: rdata_r {DATA_W{1b0}}; endcase end else if (rvalid_r rready) begin rvalid_r 1b0; end end assign arready ~rvalid_r; // 上一笔没被取走就先把 AR 停住 assign rvalid rvalid_r; assign rdata rdata_r; assign rresp 2b00; assign reg0 slv_reg0; assign reg1 slv_reg1; endmodule这段代码有几个地方值得单独说。WSTRB的处理必须按字节来不能整体覆盖否则主机想只写一个字节的时候另外三个字节会被写坏。arready用~rvalid_r来产生意思是上一笔读数据还没被取走先别接新的地址这是一种最简单的单笔 outstanding 策略。它牺牲了吞吐但绝对不会乱序非常适合调试阶段。等整个系统跑通了再考虑加读数据 FIFO 支持多笔 outstanding。bresp和rresp硬编码成 OKAY是因为这个从机不做地址合法性判断。真实项目里如果接了多个从机访问越界应该回DECERR从机内部出错应该回SLVERR这样主机侧的驱动才能做正确的错误处理。5. 仿真、上板与性能AXI 工程的三个调试层面5.1 仿真环境怎么搭协议检查器比波形更重要新手搭 AXI 的仿真环境最常见的做法是自己写一个 testbench手动拉AWVALID、WVALID然后看波形对不对。这种方式能跑通功能但它有一个致命缺陷它只能验证你想到的情况。而 AXI 的坑绝大多数都在你没想到的地方。我现在的做法是分两层。第一层用协议检查器它是一组 SystemVerilog 断言会实时检查握手规则、地址边界、突发长度、响应顺序这些硬性约束一旦违反立刻报错并给出时间戳。第二层再用自己写的激励去覆盖功能场景。检查器负责合法性激励负责正确性两者分工明确。激励的设计也要讲究。我一般会覆盖这几类单笔读写、连续突发、下游随机背压、读写交替、边界长度比如 1 拍和 256 拍、窄传输。特别是随机背压一定要加前面那个降采样的坑就是靠它才在仿真阶段暴露出来的。关于 AXI-Stream 的仿真还有一个很实用的技巧在TLAST上挂断言检查每个包的拍数是否和预期一致。很多图像处理的 bug 归根到底就是包长错了一个像素一个像素地看波形太痛苦直接检查拍数效率高得多。5.2 ILA 抓不到数据时的排查链路上了板子出问题就需要 ILA 这个片内逻辑分析仪。但很多人遇到的第一个问题不是数据错而是ILA 根本没抓到东西触发条件一直不满足。这个时候按下面这个链路走基本能定位到问题第一步确认时钟是否真的在跑。ILA 的采样时钟如果没有翻转波形就是一条直线。可以在同一个时钟域里加一个自由运行的计数器看看它在不在动。第二步确认触发条件本身是可达的。如果你把触发条件设成awvalid awready而地址通道从来没被访问过那当然永远不触发。这时候先把触发条件放宽到awvalid本身看看有没有出现。第三步确认信号没有被综合优化掉。这是最隐蔽的一种。你抓的信号如果只参与了调试而没有连到任何实际逻辑综合器会把它优化掉ILA 上就一直是 0。解决办法是在代码里做标记或者在综合选项里禁止对调试信号做优化。第四步确认采样深度够。一笔 256 拍的突发加上前后的握手波形可能要占几百个采样点。如果深度只设了 1024很容易把关键信息挤出去。我通常会把深度设成 4096 或 8192先用大深度定位再缩小。提示ILA 的触发条件最好设计成事件序列而不是单一信号。比如先触发bvalid再往前看wlast这样一次抓取就能看到完整的一笔写事务。5.3 跨时钟域与时序收敛为什么 VALID 常是关键路径只要系统里有不同时钟域AXI 就一定要跨时钟。比如图像输入的像素时钟是 74.25 MHzDDR 控制器的时钟是 300 MHz两边必须转换。这个活不要自己写用现成的 AXI 时钟转换模块它会内部处理好异步 FIFO 和握手的跨域问题。自己手搓的版本几乎一定会在某个极端情况下丢数据。时序方面AXI 里最容易变成关键路径的是VALID和READY。原因是这两个信号在互连里往往要走很长一段组合逻辑——地址解码、仲裁选择、状态判断全都串在一条路径上。缓解手段有几个把READY用寄存器输出切断组合链在互连的关键节点插入流水寄存器把仲裁逻辑提前一个周期算好用选择器直接选。如果时序报告里看到某条VALID路径的 slack 是负的先别急着加流水先看看是不是有VALID依赖READY这种违规写法。改掉之后往往问题就消失了比加流水寄存器管用。5.4 算一笔账位宽、突发长度与实际带宽很多人做完一个 AXI 数据通路发现实测带宽只有理论值的一半就开始怀疑 IP 有问题。其实大部分时候问题出在突发长度太短。来算一笔具体的账。假设数据位宽 64 位时钟 200 MHz突发长度 128 拍AWSIZE为 8 字节。单次突发的数据量是128 × 8 1024字节。数据搬运本身耗时128 拍 × 5 ns 640 ns。再加上地址握手和写响应的开销假设总共 5 拍也就是 25 ns。合计 665 ns。有效带宽 1024 B / 665 ns ≈ 1.54 GB/s。理论上限 8 B × 200 MHz 1.6 GB/s。效率约为96%非常理想。但如果把突发长度降到 8 拍呢数据量变成 64 字节搬运耗时8 × 5 40 ns开销仍然是约 25 ns合计 65 ns。有效带宽 64 B / 65 ns ≈ 0.98 GB/s效率掉到61%。也就是说同样的硬件仅仅因为突发长度短了带宽损失了近四成。突发长度单次数据量有效带宽效率8 拍64 B≈ 0.98 GB/s61%32 拍256 B≈ 1.38 GB/s86%128 拍1024 B≈ 1.54 GB/s96%256 拍2048 B≈ 1.57 GB/s98%这张表说明了一个很实在的结论突发长度比位宽更值得优化。当然突发也不能无限长——受AWLEN最大 256 拍和 4KB 边界两条约束64 位总线上单次突发最多 2048 字节这已经是上限了。还有一个会影响效率的因素是读写切换。DDR 在读写之间切换需要额外的总线周转时间如果 AXI 上读写频繁交替实测带宽会明显低于纯读或纯写的场景。工程上的对策是把读和写分时批处理让 DMA 先集中写完一块再集中读另一块减少切换次数。6. 面试与进阶AXI 那些容易被追问的边角6.1 乱序、写响应提前、窄传输与 4KB 边界如果你以后要面数字设计相关的岗位AXI 的问题是必问的。这里列几个我被问过、也问过别人的高频点。乱序是怎么实现的。答案是靠AWID和ARID。不同的 ID 代表不同的事务流从机可以打乱它们的执行顺序以提升效率但同一 ID 的事务必须保持顺序。互连里通常会给每个主设备分配不同的 ID这样从不同主机来的事务天然可以乱序而同一个主机内部又保持有序。写响应可以提前吗。不可以。BVALID必须在整个突发的最后一拍写数据被接收之后才能拉高。这一条经常被用来验证候选人对协议的理解深度。窄传输和不对齐有什么区别。窄传输是AWSIZE小于总线物理宽度此时WSTRB只在部分字节上有效不对齐是地址没有落在2^AWSIZE的整数倍上AXI4 只允许首发不对齐。两者经常同时出现但概念完全不同。4KB 边界的由来。前面提过是为了兼容内存管理的页单位保证一次突发不会跨越两个物理页。这一条在写 DMA 描述符的时候特别重要如果一个搬运请求的长度加上起始地址会跨页必须拆成两笔。AXI3 和 AXI4 的主要差别。AWLEN从 4 位变成 8 位16 拍变 256 拍去掉了WID因为写数据必须按序增加了 QoS、Region、User 信号允许写数据先于写地址到达。这几条几乎每次面试都会被拎出来问。6.2 和 AHB、APB 放在一起记单纯记 AXI 容易记混放到一起对比反而更清楚。APB 最简单没有流水线一次传输至少两个周期只有地址、数据、读写使能、选通和准备好几个信号专门给低速外设用。AHB 引入了流水线地址阶段和数据阶段错开一拍加了HREADY做等待但依然是单通道共享读写不能同时进行也不支持乱序。AXI 相比它们的本质跃迁是三点通道独立读写可以并行、地址和数据可以解耦、基于 VALID/READY 的通用握手不依赖固定的周期数、支持乱序和 outstanding用 ID 来管理。理解了这三点再去看那些信号名就不再是一盘散沙了。6.3 下一步可以往哪些方向延伸把这一篇的内容跑通之后有几个方向值得继续往前推。一个是从机侧的性能优化把单笔 outstanding 的读通道改成带 FIFO 的多笔 outstanding观察带宽的变化。这个改造会让你真正理解延迟隐藏的原理。一个是自己写一个 AXI-Stream 的处理链从数据源到滤波器再到输出中间塞进几个不同深度的 FIFO用随机背压测试整条链路的稳定性。这一步做完你对流水线的理解会上一个台阶。还有一个方向是把 AXI 和具体的算法结合比如图像处理里的行缓存、滤波器里的系数加载、高速接口里的数据对齐。这些场景的共同点是算法本身不难难的是数据怎么高效地喂进去、怎么及时地取出来——而 AXI 恰好就是回答这个问题的。我自己最大的体会是AXI 这个东西一开始看着复杂是因为信号太多但真正用起来以后会发现核心逻辑其实只有握手两个字。把这套握手搞扎实后面接任何 IP、读写 DDR、搭图像通路都只是同一套规则的重复应用。真正花时间的从来不是协议本身而是踩过的那些仿真能过、上板不行的坑——而这些坑只能靠自己一遍一遍地看波形、加断言、调激励慢慢积累出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价