1. 从数据怎么进来说起为什么AXI/AXIS和DMA是绕不过去的坎做FPGA开发到一定阶段你会发现一个很尴尬的现实前面二十多篇折腾的都是流水灯、状态机、串口收发、简单的视频时序这些自娱自乐的东西数据量小、速率低随便怎么写都能跑。可一旦你要做真正有用的系统——比如图像采集、高速数据采集、实时信号处理——问题立刻变得尖锐起来数据从哪进来进来之后放哪处理完怎么送出去这三个问题不解决你的FPGA永远只能停留在玩具阶段。我自己刚开始碰这块的时候踩过一个特别典型的坑。当时做一个简单的图像缓存实验摄像头出来的数据想存到片外内存再读出来显示。我天真地以为只要把数据线接上、给个写信号就完事了结果波形抓出来一看数据丢得七零八落偶尔还整个画面撕裂。折腾了整整两天才搞明白问题根本不在我的图像逻辑而在于我完全没理解AXI总线的握手规则也没搞清楚DMA到底在帮我做什么。那次之后我才真正沉下心去啃AXI/AXIS协议和DMA的工作机制。这篇内容就是把我踩过的这些坑、以及后来总结出来的一套可复用的架构思路完整讲清楚。核心围绕三件事AXI和AXIS到底有什么区别、各自用在哪DMA是怎么把数据从一端搬到另一端的DDR缓存架构应该怎么设计才能既跑得动又不丢数据。适合已经能写基本Verilog、跑过简单工程但一碰到大数据量搬运就发怵的朋友。如果你连状态机都还没写利索建议先回去补一补再来因为这里面的东西对时序和握手的理解要求比较高。我尽量不用那种教科书式的讲法而是按照我当初是怎么一步步搞懂的这个顺序来展开。每个概念我都会先讲它解决什么问题再讲它怎么工作最后讲实际写代码时要注意什么。你跟着这个思路走应该能少走不少弯路。2. AXI和AXIS名字像双胞胎用途差得远2.1 先搞清楚这两个协议到底为谁服务很多人第一次看到AXI和AXIS这两个词会懵觉得不就是差一个字母吗能有多大区别。实际上它们的定位完全不同用错了地方会让你痛不欲生。AXIAdvanced eXtensible Interface是一个带地址的存储器映射总线。什么叫存储器映射就是它访问的每一个目标都有一个明确的地址。你读写的时候要给出地址、给出数据、给出各种控制信号它是一套完整的寻址传输机制。它主要用在需要随机访问的场景比如CPU读写DDR、DMA读写DDR、寄存器配置等等。它的特点是通道多读地址、读数据、写地址、写数据、写响应五个独立通道、支持突发传输、支持乱序完成功能强大但协议复杂。AXISAXI4-Stream则是一个不带地址的流式总线。它没有地址概念数据就像水流一样从上游流到下游你只管一个接一个地送接收方一个接一个地收。它只有数据线和几个握手信号TVALID、TREADY、TLAST、TKEEP等协议简单得多。它主要用在数据流处理链路里比如视频像素流、ADC采样流、滤波器输入输出等等。我打个比方你就明白了AXI像是去图书馆借书你得先查书号地址然后管理员去对应书架存储位置把书拿给你AXIS像是传送带东西放上去就一直往前送你不需要知道它从哪个货架来的只管接住就行。2.2 握手规则AXIS简单但简单不等于可以乱来AXIS的握手规则说起来就一句话发送方拉高TVALID表示我有数据了接收方拉高TREADY表示我准备好了两者同时为高的那个时钟周期数据才算真正传过去。听起来简单但实际写代码时最容易出问题的恰恰是这里。我见过太多新手包括当年的我自己写出这样的逻辑TVALID直接跟某个使能信号绑死数据一变TVALID就跟着变。这在仿真里可能看不出问题一上板就丢数据。为什么因为AXIS有一条铁律TVALID一旦拉高在TREADY到来之前绝对不能拉低数据也必须保持不变。你如果让TVALID跟着数据变接收方还没准备好你就把数据换了那传过去的就是错的数据。正确的做法是用一个状态机或者一个valid寄存器来管理。当上游有数据要发时把数据锁存到输出寄存器同时把TVALID置1然后一直等直到检测到TREADY为高才把TVALID清0准备下一拍数据。这样就能保证握手期间数据稳定。// AXIS发送端的典型写法 always (posedge clk) begin if (rst) begin m_axis_tvalid 1b0; end else begin if (data_available !m_axis_tvalid) begin m_axis_tdata data_in; m_axis_tvalid 1b1; end else if (m_axis_tvalid m_axis_tready) begin m_axis_tvalid 1b0; // 握手成功撤销valid end end end接收端也有讲究。TREADY可以一直拉高表示我永远准备好也可以根据内部FIFO的满空状态动态拉高拉低。如果你下游是个FIFO那TREADY就应该接FIFO的未满信号。这里有个细节TREADY拉低之后如果TVALID还是高的数据不会丢只是暂停传输等TREADY再拉高时继续。所以流控是靠TREADY做的不是靠TVALID。2.3 AXI的五个通道和突发传输别被吓到AXI看起来复杂但如果你只是用现成的IP核比如DMA、内存控制器其实不需要手写AXI主机逻辑只需要理解它的行为就行。不过理解它的通道结构对调试非常关键。AXI有五个独立通道读地址通道AR、读数据通道R、写地址通道AW、写数据通道W、写响应通道BRESP。每个通道都是独立的握手也就是说读和写可以同时进行地址和数据也可以流水起来。这种设计是为了最大化吞吐量但也意味着你抓波形的时候要同时看多个通道才能理清一次完整传输。突发传输Burst是AXI的精髓。一次突发可以传输多个数据拍只需要给一个起始地址。突发长度Burst Length可以是1到256拍。突发类型有三种FIXED地址不变用于FIFO式访问、INCR地址递增最常用、WRAP回绕用于Cache Line填充。实际做DDR访问时INCR突发是绝对主力因为DDR的物理特性决定了连续地址访问效率最高。这里有个经验突发长度不要设得太短。如果你每次只传一拍那地址通道的开销会占掉大量带宽实际有效吞吐可能只有理论值的三分之一。一般建议至少设成8或16拍具体看你的数据宽度和DDR的突发要求。但也不能太长太长会导致单次传输延迟增加影响实时性。我一般用16拍兼顾效率和延迟。2.4 什么时候用AXI什么时候用AXIS一条实用判断标准判断标准其实很简单看你的数据有没有地址这个概念。如果你要访问的是DDR里的某个具体位置、某个寄存器、某块内存区域那就用AXI。比如DMA要往DDR的0x10000000地址写数据这就是AXI。如果你的数据是源源不断产生的流不需要关心它存在哪只需要一个接一个处理那就用AXIS。比如摄像头出来的像素流、ADC出来的采样流、滤波器处理完的输出流这些都是AXIS。实际系统里两者经常混用。典型架构是传感器→AXIS→DMA→AXI→DDR读回来的时候是DDR→AXI→DMA→AXIS→处理模块。DMA就是那个翻译官把流式的AXIS数据翻译成带地址的AXI访问或者反过来。理解了这一点整个数据通路的脉络就清晰了。3. DMA那个帮你搬砖的搬运工到底怎么工作3.1 DMA解决的核心痛点CPU不该干搬数据的活在没有DMA的年代数据搬运全靠CPU一条一条指令去读写。比如要把1MB数据从DDR搬到外设CPU得执行几十万次load/store指令期间啥也干不了。这在FPGA系统里更夸张因为FPGA里可能根本没有CPU你得自己写状态机去搬那逻辑复杂度和资源消耗都很吓人。DMADirect Memory Access直接内存访问的思路很直接专门做一个硬件模块来干搬运这件事CPU或者你的控制逻辑只需要告诉它从哪搬、搬到哪、搬多少然后就可以去干别的事了搬完了DMA会给你一个中断或者状态标志。在FPGA里DMA通常是一个IP核一端是AXI主机接口用来访问DDR另一端是AXIS接口用来对接你的数据流逻辑。你配置好源地址、目的地址、传输长度它就开始自动搬运。搬运过程中它自己会处理AXI的握手、突发、地址递增这些细节你完全不用管。3.2 两种典型DMA模式MM2S和S2MMXilinx的AXI DMA IP核这是最常用的一个提供两个方向的通道MM2SMemory-Map to Stream从内存DDR读到流AXIS。方向是DDR→AXI→DMA→AXIS→你的逻辑。典型场景你要处理一段存在DDR里的数据DMA把它读出来变成流送进你的处理模块。S2MMStream to Memory-Map从流写到内存。方向是AXIS→DMA→AXI→DDR。典型场景你的采集模块产生了一堆数据DMA把它写进DDR缓存起来。这两个通道是独立的可以同时工作也就是一边读一边写实现全双工。这在视频处理里特别有用一帧数据从DDR读出来处理同时处理完的上一帧写回DDR。配置DMA的时候有几个关键参数传输长度以字节为单位、源地址、目的地址、以及描述符模式还是简单模式。简单模式就是每次传输你都得重新配置一遍寄存器描述符模式则是你预先在内存里建一个描述符链表DMA自己会去读链表然后依次执行适合需要连续搬运多块数据的场景。刚开始建议用简单模式跑通了再上描述符模式。3.3 描述符模式让DMA自己管自己描述符模式是DMA真正好用的地方。你想象一下视频采集的场景每一帧图像都要存到DDR的不同位置如果每帧都要CPU去配置一次DMA那CPU负担还是很重。描述符模式就是解决这个问题的。你在DDR里开辟一块区域存放描述符Descriptor每个描述符包含下一块数据在哪、多长、搬到哪这些信息。DMA启动后会自己去读第一个描述符执行搬运然后自动读下一个一直循环。你只需要在初始化的时候把描述符链建好之后DMA就自己跑起来了。描述符的结构一般是这样的以Xilinx DMA为例字段含义典型值NXTDESC下一个描述符的地址指向下一个描述符BUFFER_ADDRESS数据缓冲区地址DDR中的物理地址CONTROL控制字段含传输长度、SOF/EOF标志长度标志位STATUS状态字段传输完成状态由DMA回写这里有个坑描述符本身的地址必须对齐Xilinx DMA要求描述符地址按16字节对齐具体看版本不对齐会导致DMA读描述符出错而且错误现象很隐蔽可能只是偶尔丢一帧很难查。我第一次用描述符模式时就因为这个对齐问题折腾了半天。3.4 DMA调试的实战心得先慢后快先小后大DMA调试我总结了一条原则先用最低速率、最小数据量跑通再逐步加压。具体做法是先设一个很小的传输长度比如64字节用最简单的模式抓AXI波形确认握手正常、数据正确。然后逐步加大长度到1KB、4KB、64KB观察有没有丢数据。最后再把速率提上去。如果一上来就搞大长度高速率出了问题你根本不知道是协议理解错了、还是时序没收敛、还是DDR带宽不够。还有一个必查项DMA的中断和状态寄存器。DMA传输完成、出错、空闲这些状态都会反映在寄存器里。我习惯在调试阶段把DMA的Error寄存器读出来打印很多时候错误原因直接写在里面了比如Decode Error表示地址访问越界Slave Error表示从设备返回了错误响应。不看这些寄存器瞎猜效率极低。4. DDR缓存架构数据放得下还要取得快4.1 为什么非要用DDR片上RAM根本不够FPGA片上的Block RAMBRAM容量很有限一颗中端FPGA可能也就几MB。而一帧1080P的RGB图像就是1920×1080×3≈6MB存一帧都费劲更别说存多帧做缓存了。所以只要涉及图像、视频、大批量数据采集DDR几乎是必然选择。DDR的容量大动辄几百MB到几GB、带宽高DDR3/DDR4轻松上GB/s但它的访问特性跟BRAM完全不同。BRAM是真正的随机访问任何地址延迟都一样DDR则是连续地址访问效率极高随机访问效率极低。因为DDR内部是分行Row分列Column的换行需要预充电和激活开销很大。连续访问同一行内的数据Row Hit效率最高。这个特性直接决定了你的缓存架构设计尽量让DMA做连续的大块传输避免频繁的小块随机访问。这也是为什么前面强调AXI突发长度不能太短的原因。4.2 帧缓存架构乒乓缓冲与多缓冲做视频或图像处理最经典的缓存架构是乒乓缓冲Ping-Pong Buffer。原理很简单开两块内存区域一块用来写采集新数据一块用来读处理旧数据写满一块后交换角色。这样读写互不干扰不会出现边写边读导致数据撕裂的问题。乒乓缓冲的地址规划通常是这样的缓冲区起始地址大小用途Buffer 00x10000000一帧大小写第N帧/读第N-1帧Buffer 10x10000000帧大小一帧大小写第N-1帧/读第N帧切换的时机很关键必须在写满一整帧之后、且读端已经读完上一帧之后才能切换。如果读端还没读完你就把缓冲区切走了读出来的就是半新半旧的数据画面会撕裂。所以需要一个握手机制通常用一个状态机来管理写完成和读完成两个标志两个都满足才切换。如果处理链路比较长乒乓可能不够用这时候要上多缓冲比如三缓冲、四缓冲。多缓冲的本质是用空间换时间让写端不用等读端读端也不用等写端各自按自己的节奏跑只要缓冲数量足够吸收两边的速率差就行。代价是占用更多DDR空间以及管理逻辑更复杂。4.3 带宽估算别等跑起来才发现不够DDR带宽是有限的设计阶段就要算清楚不然跑起来发现带宽不够画面卡顿或者丢帧回头改架构代价很大。带宽估算的基本公式是带宽 数据量 × 帧率 × 读写次数。举个例子1080P60fps的RGB888视频单帧数据量 1920 × 1080 × 3 6,220,800字节 ≈ 6MB每秒数据量 6MB × 60 360MB/s如果做乒乓缓冲写一次读一次总带宽需求 360 × 2 720MB/s这还没算上DDR的访问效率损失。实际DDR的有效带宽通常只有理论峰值的60%~80%受刷新、换行、仲裁等影响。所以如果你的DDR理论带宽是1.6GB/s比如16位DDR3-800有效带宽可能只有1GB/s左右720MB/s的需求已经占了七成余量不多了。如果还要同时跑其他访问比如CPU取指令、其他DMA通道很容易就爆了。我的经验是带宽估算出来后至少留50%的余量。也就是说如果算出来需要720MB/s那你的DDR有效带宽最好在1.5GB/s以上。不够的话要么降分辨率/帧率要么换更宽的DDR位宽要么优化访问模式提高效率。4.4 跨时钟域与数据位宽转换两个容易被忽视的细节DDR的时钟域和你的处理逻辑时钟域往往不一样。比如DDR控制器可能跑在400MHz而你的图像处理逻辑跑在150MHz。DMA作为中间桥梁需要处理跨时钟域的问题。好在现成的DMA IP核一般都内置了异步FIFO来处理这个你只需要确保两边的复位和时钟正确连接就行。但数据位宽转换是另一个容易出问题的地方。DDR的AXI接口可能是64位或128位宽而你的AXIS流可能是8位比如灰度图像素或24位RGB。DMA通常支持位宽转换但配置不对会导致数据错位。比如你把24位数据送进一个配置成32位宽的流接口它可能会把数据打包成32位多出来的8位填零或者填垃圾读出来的时候如果没按同样规则解包数据就全乱了。我的建议是在DMA两端明确数据位宽并在文档里写清楚打包规则。如果DMA不支持你需要的位宽转换就在DMA前面或后面加一个位宽转换模块用AXIS的TKEEP信号或者自己写逻辑把数据整理成DMA要求的格式再送进去。5. 把三者串起来一个完整的图像采集缓存系统长什么样5.1 系统框图与数据流现在把前面讲的东西串成一个完整系统。假设我们要做一个图像采集缓存回读显示的系统数据流是这样的采集端图像传感器输出像素数据附带行同步、场同步信号。我们需要一个采集模块把像素数据转成AXIS流TVALID/TREADY/TLAST/TUSER其中TLAST标记一行的结束TUSER标记一帧的开始。写入路径AXIS流进入DMA的S2MM通道DMA把流数据通过AXI写入DDR的帧缓冲区。每写完一帧DMA产生一个中断或更新状态寄存器。读取路径显示端需要数据时配置DMA的MM2S通道从DDR帧缓冲区读出数据通过AXIS流送给显示时序生成模块最终输出到显示器。控制逻辑一个简单的控制状态机负责管理乒乓缓冲的切换、DMA的启动和状态查询、以及异常处理比如DMA出错时重新初始化。这个系统里AXIS负责流式数据AXI负责带地址的DDR访问DMA负责两者之间的转换和搬运DDR负责大容量缓存。四者各司其职缺一不可。5.2 关键配置参数清单实际搭建时以下参数必须明确配置我列一个清单方便你对照模块参数说明DMA S2MM目标地址DDR帧缓冲区物理地址DMA S2MM传输长度一帧的字节数DMA MM2S源地址DDR帧缓冲区物理地址DMA MM2S传输长度一帧的字节数AXI数据位宽匹配DDR控制器位宽AXI突发长度建议8~16AXIS数据位宽匹配像素格式DDR帧缓冲区大小至少2倍帧大小乒乓DDR地址对齐按DMA要求对齐通常4KB或更大地址对齐这一项特别容易忽略。DDR的访问通常要求按突发边界对齐DMA的描述符也有对齐要求。如果地址没对齐轻则效率下降重则直接报错。我一般把帧缓冲区起始地址设在1MB边界上这样基本不会踩对齐的坑。5.3 实测中遇到的三个典型问题与解决问题一画面偶尔撕裂。排查发现是乒乓切换时机不对写端写满一帧后立刻切换但读端还在读上一帧的尾部。解决方法是加一个读完成标志写满且读完成才切换。这个标志可以用一个简单的寄存器实现写端置位读端清零切换时检查。问题二DMA传输偶尔卡死。抓波形发现AXIS的TREADY长时间为低导致DMA一直等。追下去发现是下游FIFO满了但上游还在拼命送而TREADY的生成逻辑有毛刺。解决方法是在TREADY路径上加一级寄存器打拍消除组合逻辑毛刺。这个坑很隐蔽因为仿真时毛刺不一定能复现。问题三带宽不够导致丢帧。一开始用1920×108060fps跑发现每几秒丢一帧。算了一下带宽读写加起来接近DDR有效带宽的90%太满了。后来把帧率降到30fps同时把AXI突发长度从4加大到16有效带宽利用率上去了丢帧消失。这个案例说明带宽估算和突发长度优化有多重要。5.4 给刚上手的朋友几条实在建议第一先用仿真把数据通路跑通。写一个简单的AXIS激励源比如产生递增数据一个简单的AXIS接收端把数据打印出来中间接DMA模型在仿真里确认数据从源到目的完全正确。仿真跑通了再上板能省掉大量调试时间。第二ILA集成逻辑分析仪是你的好朋友。把AXIS的TVALID/TREADY/TDATA、AXI的地址和数据、DMA的状态信号都接到ILA上触发条件设成传输开始或错误发生抓一次波形比盯着代码看半天管用得多。第三不要一上来就追求满带宽。先用低速率跑通功能确认数据正确、逻辑稳定再逐步提高速率。很多问题在低速下不出现高速下才暴露但如果你连低速都没跑通高速下的问题你根本无从下手。第四把DDR的地址规划写下来。哪块区域做什么用、起始地址多少、大小多少、对齐要求是什么全部列清楚。我见过太多项目因为地址冲突或者越界导致诡异bug最后发现是两个人各自规划地址时撞车了。这套AXI/AXISDMADDR的架构说实话第一次搭会觉得东西很多、很杂但一旦跑通一次后面再做类似系统就是复制粘贴改改参数的事。我自己的体会是这块内容属于门槛高但天花板低——入门那一下确实要花点功夫但掌握之后能做的事情一下子就打开了图像、视频、高速采集、实时处理全都建立在这个基础之上。所以别怕麻烦把这一关过了后面的路会宽很多。