FPGA数据搬运架构设计:DMA、AXI与DDR缓存实战解析

📅 发布时间:2026/10/12 12:07:58
FPGA数据搬运架构设计:DMA、AXI与DDR缓存实战解析
1. 流量洪峰倒逼出来的架构问题为什么必须认真对待数据搬运1.1 一个具体的采样场景和之前的笨办法前22篇我们把时序约束、FIFO、状态机、串口、SPI这些地基模块一个个搭了起来到了Part.23终于要面对本系列第一个真正意义上的系统级问题数据量变大以后怎么让数据自己从DDR里面跑出来再自己跑回去而不是让逻辑或者CPU去逐字节当搬运工。我经常用这个例子说明问题假设你有一个ADC以250 MSPS的采样率持续输出16 bit采样值换算下来每秒要处理500 MB数据。如果按老思路写一个状态机每次向DDR控制器发一笔写请求等响应再发下一笔这个状态机从头到尾会被地址计算、bank切换、响应等待占满。CPU直接参与搬运更糟中断一来就要进上下文切换数据稍微快点就追不上。结果就是带宽数字看起来挺大实际一测吞吐腰斩波形一多就丢采样点。所谓搬运其实有三个递进层次最底层是CPU一条条读外设寄存器再一条条写DDR中间层是逻辑自己维护地址计数器、自己发AXI突发最上层才是用DMA控制器把源地址、目的地址、传输长度扔给硬件剩下的事情全部自动化。做FPGA越久越会感觉到大部分性能问题不是算力不够而是数据在来的路上堵死了。这也是本篇要解决的问题。1.2 数据搬运的三个层次从人肉搬运到自动化分拣先把这个分层讲透后面看架构图才不会晕。第一层CPU直搬。逻辑把采样数据放进一个状态寄存器CPU读回来再写进DDR。优点是实现简单调试方便缺点是吞吐完全取决于CPU读写速度而且高频率中断会挤占其他任务的执行时间实测带宽通常只有几十MB/s的量级适合低速控制和调试用。第二层FIFO加状态机直写DDR。自己设计一个写控制器维护写地址、写长度FIFO攒够一小段就发一笔AXI突发。这个方案比CPU快得多在小带宽场景下很实用但每笔突发之间地址递增、响应等待都是串行的突发长度和地址连续性一旦安排不好效率照样上不去。第三层DMA控制器。DMA的本质是可编程地址发生器加长度计数器它把搬运动作拆成读和写两条通道读通道负责把数据从源端拿回来写通道负责把数据推到目的端。搬运的过程中主逻辑只需要在开始和结束的时候介入一下。把这个过程类比成物流很合适CPU是人肉跑仓库状态机是手推车DMA是一条能自己认路的自动分拣线。本篇要搭的核心链路就是这套自动分拣线的骨架AXI/AXIS是道路规则DMA是运输车DDR缓存是仓库。三者各司其职数据从源头进来经DMA进仓库再从仓库经DMA出去全程不需要人工介入。1.3 本篇的阅读路线图如果你手里已经有一块带DDR颗粒的开发板而且前面的FIFO和AXI-Lite读写实验都跑通过这篇的内容可以直接落到工程里。我会先在第二部分把AXI和AXIS的最小必要知识讲清楚只讲和搬运强相关的握手、突发、对齐不铺开讲协议全集第三部分讲DMA控制器内部到底在做什么读通道和写通道各自的工作流程第四部分是重头戏讲DDR缓存架构如何分层、缓存深度怎么算、水位线怎么设第五部分给出搭建链路的具体步骤并分享三个我实测踩过的坑。这篇文章不会涉及某个厂商特有的IP细节但思路和调试方法在所有主流FPGA平台上都通用。2. AXI 与 AXIS 协议入门0基础需要掌握的最小集2.1 valid/ready 握手数据搬运的红绿灯AXI-Stream常简写为AXIS是整个搬运链路的传送带。它不需要地址也没有独立的读写区分数据从一个主设备流到一个从设备一切规则都围绕一对握手信号展开。先记住这个最核心的关系发送方拉高valid表示当前总线上的数据是有效的接收方拉高ready表示现在有能力接收只有当valid和ready同时为高的一拍数据才算真正传输完成。这个规则和现实中的红绿灯非常像valid是绿灯告诉后面我这辆车要过路口了ready是路口另一侧的管理员说我这边能接,两灯同时亮车才真正通过。这里有一个初学者很容易犯的错误发送方先看ready再决定要不要拉valid甚至用ready做valid的组合逻辑结果出现组合环或者死锁。正确做法是数据准备好了就无条件拉高valid如果对端一直拉高ready那就每个周期持续送数如果对端暂时不能接收就拉低ready数据在源端多等几拍。这个valid不等ready的原则能避免很多调试噩梦。规范的握手逻辑大概是这样的always (posedge clk or negedge rst_n) begin if (!rst_n) begin axis_tvalid 1b0; axis_tdata d0; axis_tlast 1b0; cnt d0; end else if (axis_tready) begin axis_tvalid 1b1; axis_tdata data_ram[cnt]; axis_tlast (cnt total_len - 1); cnt (cnt total_len - 1) ? d0 : cnt 1b1; end end注意上面代码里有一个隐含细节axis_tvalid在整段传输中始终保持为高表示发送方没有停顿接收方的ready可以随时变化这是AXIS最灵活的背压方式。另外tlast用来标识最后一个数据拍接收方靠它判断一个数据包的边界在有不定长包传输时尤其重要。除了valid/ready/data/last这四个基本信号AXIS还常用tkeep或tstrb来表示总线中哪些字节有效。比如64位总线上一次能带8个字节如果这次的包尾只有3个字节有效就需要拉低tkeep的对应位。很多人在调试时只关注data和last忽略了字节有效信号导致接收方把无效字节当有效数据处理这个细节我会在踩坑部分再提。2.2 AXI 与 AXIS 的区别流式 vs 地址式AXIS只是整个AMBA总线协议家族里的一员完整版的AXI总线是另一套东西理解它们的区别才知道什么时候该接谁。AXI全协议带地址典型特征是读写通道分离。以一次写传输为例主机先在AW通道上发送写地址在W通道上发送写数据从机处理完之后通过B通道返回写响应。读传输则分为AR通道读地址和R通道读数据。正因为地址通道和数据通道是分开的主机可以在等待上一次写响应时提前发起下一次地址请求形成流水线。另外还有AXI-Lite它是AXI的精简版不支持突发传输一次只能读写单个数据非常适合用来配置寄存器。我们在工程里常见的一种接法就是CPU或调试逻辑通过AXI-Lite配置DMA而DMA和DDR控制器之间用完整的AXI走大数据各端口之间的数据流则用AXIS传递。做个简单对照接口特点典型用途AXIS无地址、单向流、有握手无响应模块间数据搬运、音频/图像流AXI-Lite有地址、无突发、每次单读单写寄存器配置、状态查询AXI全协议有地址、支持突发、读写通道分离DDR控制器、DMA主接口在架构选型时我的原则很简单凡是搬运大块数据的路径一律用AXI全协议凡是配置类的路径用AXI-Lite凡是模块间持续的数据流用AXIS。三者各司其职工程结构会清爽很多。2.3 突发长度、字节通道和地址对齐DDR高效读写的底层原因AXI全协议和大吞吐最相关的特性就是突发传输burst。一次地址请求可以携带长度信息例如arlen为15时表示后续还有15拍数据总共16拍每拍按总线位宽传输。对64位总线来说16拍可以一次传128字节。DDR控制器最喜欢这种连续突发因为它可以把SDRAM中同一行row的数据连续读出来不需要反复切换行地址。突发长度不是越大越好它受限于DDR控制器IP的设置。比如控制器可能规定最大突发64拍如果用户一次请求128拍控制器内部会自己拆成两笔小突发虽然逻辑上没有问题但中间会插入额外的总线占用。所以在配置DMA时最好把单次传输长度控制在DDR控制器支持的最大突发之内。地址对齐则是另一个效率杀手。AXI总线突发有个规则一次突发不能跨越4KB边界如果起始地址不在自然对齐位置控制器会把这个长突发拆成多段每段都要重新发一次地址请求、重新计算行地址。我实测过同样的数据量对齐和未对齐的吞吐差距可以超过30%。行业里常见的做法是让DMA的缓存基地址按64字节对齐DDR控制器内部如果按照最佳页面大小配置还能进一步和缓存行对齐把效率拉到最高。3. 中间人DMA把搬运工做成可编程的硬件模块3.1 DMA的本质地址、长度、方向聊到DMA很多从软件转过来的人会下意识把它理解成快速memcpy这个类比其实不准确。CPU里的memcpy是软件指令逐字节拷贝而FPGA里的DMA是硬件状态机它做的事情只有三件记住源地址、记住目的地址、记住传输长度。启动之后它自己完成地址递增、突发组装、长度递减、结束中断。一个完整的DMA控制器通常拆成两条独立通道。一条叫MM2S意思是内存映射到流负责从DDR读取数据然后以AXIS流的形式发送出去另一条叫S2MM意思是流到内存映射负责接收外部AXIS数据写成DDR。这个叫法在很多厂商的IP核文档里都能看到为了方便下面直接用这两个缩写。为什么要把读写分成两条独立通道因为DDR一侧是双端口性质可以同时处理读写请求而数据流走到内外系统两侧往往不是对称的。比如一个回放场景读通道可能一直在持续出数而写通道偶尔才有一段数据进来如果共用一个控制状态机回放节奏很容易被写操作干扰。拆开后读通道和写通道各自维护自己的地址计数器和长度计数器互不干扰。3.2 寄存器模式还是描述符模式如何选择DMA的控制模式大致分两种。第一种是寄存器模式主机往DMA的寄存器里写入源地址、目的地址、长度然后置位启动位DMA就跑完一笔结束后拉中断。这种模式实现简单、调试直观适合搬运固定地址的连续数据块比如把一段正弦波表从DDR搬给DAC。第二种是描述符环模式在DDR里维护一个描述符表每个描述符里存放地址、长度、控制位和下一个描述符的指针DMA自动去DDR抓取描述符按描述符逐个搬运全部搬完再产生中断。这种方式的好处是CPU只需要在起始时告诉DMA描述符环在哪个地址后续所有搬运都不需要CPU介入特别适合多段不连续数据的连续采集。我的建议是如果只是做单通道、数据量不大、地址固定的搬运动作寄存器模式完全够用复杂机制反而引入风险如果要做连续采样、数据要按帧写入多个缓冲区、或者多个数据流向需要仲裁再上描述符环。别为了用上高级功能而牺牲可调试性。3.3 在AXI与AXIS之间搭桥两大通道的工作流程展开讲一下MM2S通道的工作流程S2MM理解后就是它的镜像。启动前主机先通过AXI-Lite接口把源地址和字节长度配置进去。DMA收到启动命令后内部的主机接口开始往AXI总线上发送读请求先是AR通道上出现地址和突发长度DDR控制器接受到请求后后续R通道会不断返回数据。每返回一笔数据DMA内部就把它推进一个缓冲FIFO当FIFO里攒够一定字节数后才从AXIS主接口输出同时把tvalid拉高。为什么要经过这个内部FIFO而不是让AXI过来的数据直接透传到AXIS原因有两个第一AXI的读数据是带地址通道触发、可能乱序返回的而AXIS是严格有序的流必须通过FIFO排队重新整形第二总线位宽和数据通道位宽可能不一致比如AXI侧是256位AXIS侧配置成64位必须靠FIFO做位宽转换。S2MM方向则反过来外部AXIS数据先进入DMA的写数据FIFODMA侧的主机接口凑齐一个整数突发后向DDR发起一个AW写地址请求然后逐渐把FIFO中的数据在W通道上送出。接收DDR返回的B响应后这一次写突发才算完成。如果进来的数据速率忽高忽低写FIFO还能起到流量整形的作用把小片段攒成大突发以便提高DDR效率。读和写之间还有一个共同点都要维护长度计数器和完成标志。DMA内部每完成一笔数据搬运长度计数器就减一次。当减到零时会产生一个完成中断同时将状态寄存器里的对应位拉高。主机收到中断后要么再次配置新任务要么直接读取状态判断搬运是否结束。4. DDR缓存架构从单组FIFO到完整的缓存分层4.1 DDR控制器的带宽真相与背压含义DDR的标称带宽非常好算数据速率乘以位宽再除以8。比如DDR4-2400数据速率是2400 MT/s64位总线理论带宽就是2400×64/819.2 GB/s。但这个数字只是理论上限实际能拿到的有效带宽通常只有60%到80%剩下的都被以下环节吃掉了刷新操作DDR颗粒需要定期刷新刷新期间该bank的读写都要暂停读写切换总线上从读切到写或者从写切到读都需要插入空闲周期bank切换不同行的数据需要预充电和激活耗时比连续读同一行高出不少。理解背压是理解缓存架构的前提。所谓背压就是下游忙不过来时向上游传递压力的过程。在AXI总线上这个压力表现为DDR控制器在数据通道上拉低ready上游拿不到ready就无法发送下一拍数据只能堆积在中间的FIFO里。如果这个FIFO也满了压力就会继续向上游传播直到源头被暂停。架构设计的关键问题不是DDR有多快而是DDR在什么情况下会暂停暂停多久暂停时前端的数据应该放到哪里去。这三个问题搞清楚了缓存深度的计算就水到渠成。4.2 缓存架构设计前端AXIS、中间缓存、后端DDR我这里给出一套非常经典、也最适合0基础起步的分层结构。沿着数据流向依次是前端AXIS数据源、写缓存FIFO、DMA写通道、DDR控制器、DMA读通道、读缓存FIFO、后端AXIS数据目的。这里写缓存和读缓存是两个独立的FIFO不要用一个双口RAM硬扛两个方向否则地址管理会非常痛苦。为什么要在前端和DDR控制器之间硬塞一层FIFO而不是把AXIS直接接到DMA上第一是时钟域转换。很多ADC或以太网接口的时钟域和DDR用户时钟域不同FIFO天然承担异步时钟转换的功能。第二是流量整形。AXIS数据可能是一个一个字节到的如果来一个字节就发一次AXI写请求效率会非常低下FIFO把数据攒起来攒够一个标准突发长度后再触发DMA搬运DDR效率能提升一个数量级。读方向也类似。当后端设备需要数据时它会通过AXIS的valid/ready机制拉数据。如果直接从DDR读每次只发一小段请求DDR控制器会被频繁打断带宽和响应时间都受影响。通过读缓存FIFO可以预先从DDR里多取一些数据后端消费一帧DMA就自动补一帧形成流水。4.3 缓存深度估算与水位线设定一个计算实例很多教程告诉你FIFO当然是越大越好这句话在资源充足的板卡上没错但在资源紧张或需要控制延迟的场合盲目加大FIFO会带来资源浪费和延迟抬升。我习惯的做法是先算可容忍停顿时间再反推FIFO深度。仍然以250 MSPS、16 bit采样的ADC为例数据率约500 MB/s。假设DDR控制器因为刷新和bank调度每过一段时间会停顿约10µs。为了让这10µs期间前端完全不丢数据写缓存至少要能容纳500 MB/s乘以10µs等于5 KB的数据。再加上读写切换、地址冲突等不确定因素我会再乘上1.5到2的余量最终设计成深度为8KB到16KB的FIFO。这个深度在实际工程中非常常见。FIFO确定后水位线设置同样关键。写缓存不要等全满才让DMA来搬也不要一有数据就每笔都搬我一般设置为超过一半才触发DMA搬运这样DMA每笔搬走的都是足够长的数据块。读缓存则设低水位线比如FIFO中的数据被消费到只剩四分之一时DMA才开始下一次预取避免一空就请求、一请求又立刻满满当当的抖动式搬运。方向推荐水位策略效果写缓存半满触发DMA搬走减少小突发次数提升DDR写效率读缓存低水位触发预取平滑后端消费抖动减少读延迟双缓冲两块完整帧交替使用简化帧边界控制适合图像类数据5. 实测链路搭建与三个真实踩坑案例5.1 一条完整的搬运链路的搭建步骤讲完原理直接给一套能落地的搭建顺序。假设你已经有了一块带DDR颗粒的开发板并且前几篇已经把AXI-Lite读写寄存器的小实验调通了。第一步在FPGA工程里加入DDR控制器IP把数据位宽配成和DMA相匹配的位宽例如64位或128位确保突发长度配置一致。第二步加入DMA控制器IP把它的主接口连到DDR控制器的从接口把配置接口接到你的AXI-Lite总线上AXIS接口则分别接到你自己的数据源和数据目的模块上。第三步做仿真验证这一步千万不要跳过很多地址错误、长度错误在仿真里一眼就能看出上板后再查就费劲了。仿真通过之后上板验证要有一个固定流程。先跑内环回环测试在DDR里预先写入一段已知模式数据然后启动DMA读通道把读出来的数据和原始数据做比对。比对通过后再接入真实数据源从写通道进数据读到存储区之后做CRC校验。全程记录时间戳用搬运的总字节数除以耗时就能得到实际吞吐。5.2 踩坑一地址未对齐导致突发长度被截断这个坑是我在调一条图像采集链路时踩到的。现象很奇怪DDR控制器的报告显示用户接口利用率很高但实际吞吐只有理想值的一半。我一开始以为是时钟频率不够后来把AR通道的信号抓出来看发现本来期望的64拍长突发被系统拆成了好几段16拍的小突发中间还隔了空闲周期。根因是DMA的源地址没有按DDR控制器的最佳突发边界对齐。AXI协议规定突发不能跨4KB边界而DDR控制器内部又有自己的页大小约束当地址落在边界的偏置处控制器只能把长突发切成两段甚至多段。每一段都要重新仲裁、重新预充电效率自然上不去。解决方案分成两层。第一层是缓存地址对齐所有分配给DMA的缓冲区基地址统一按64字节对齐分配如果用的是带描述符模式的DMA还要保证每个描述符内的地址也满足对齐条件。第二层是长度对齐单次搬运长度尽量取突发长度的整数倍否则最后一小段只能单独发起一笔短突发白白浪费一次总线授权。5.3 踩坑二FIFO半空时的死锁问题第二个坑是系统在长时间运行后会间歇性卡死表现为所有AXIS数据流停止但DDR控制器的状态没有报错DMA也没有超时。我查了很久才发现问题出在我对读缓存FIFO的触发时机判断得太简单当时用的是空信号作为DMA预取触发条件只要FIFO空了DMA就去DDR取数据取回来填满整个FIFO。表面上看逻辑没问题但实际运行中后端消费数据的速度并不恒定当FIFO栈底还有几个数据、还没变成空信号时DMA不会启动下一次预取而前端数据源又在等待写缓存有空位写缓存又在等待DMA搬走数据。于是读通道等空、写通道等满两条通道互相等待总线进入死锁状态。正确做法是使用FIFO的可编程水位信号比如prog_empty和prog_full通过设定精确阈值做到还没空就开始预取和还没满就允许搬运。另一个更稳妥的补充手段是给DMA加一个看门狗超时机制一旦某条通道超过预设时间没有完成预期进度就主动发起软复位。5.4 踩坑三DDR刷新与带宽冲突的怪现象第三个坑更隐蔽。我在跑一个长时间大数据量传输测试时发现偶发出现校验错误而且出错位置完全没有规律重新上电后又可能几个小时不出现。起初怀疑是信号完整性后来用仿真反复验证都没复现最后回到DDR控制器的文档里翻到了刷新仲裁的细节。DDR颗粒要求定期执行刷新命令否则存储的电荷会泄漏导致数据损坏。DDR控制器内部有自动刷新机制但当用户接口的访问请求非常密集时某些控制器如果按用户请求优先的策略调度刷新命令就可能被一拖再拖。拖的时间太长恰好又有温度升高数据出错概率就会显著上升。解决思路有三条第一在高带宽传输场景中不要把用户接口的请求队列全部占满留出一定余量给自动刷新第二某些DDR控制器IP支持温度补偿刷新开启后控制器会根据芯片温度动态调整刷新频率第三如果用的是自己设计的读写控制器可以在状态机里加入刷新窗口机制每搬完一定字节数就主动让出一个周期给刷新命令。想验证这个问题也很简单看链路空闲时的稳定性是否比满载时好得多基本就能指向刷新调度。这三类问题在网上的讨论区里经常被归为玄学问题其实背后都有明确的协议和时序原因。地址对齐属于静态配置错误死锁属于触发条件设计不足刷新冲突属于资源调度考虑不周。调试顺序建议是先查地址对齐再查水位逻辑最后才查刷新仲裁。最后分享一个我自己的调试习惯搭建这种带DDR的搬运链路时一定要在关键节点预留可观测的调试接口。比如在AXIS通道上加一个数据数量统计计数器在DMA完成标志上加一个GPIO翻转在DDR控制器侧保留错误状态寄存器的读取路径。这些额外的测试逻辑看起来占用资源但能让你在复现问题时快速缩小范围。等你把架构设计调成熟再把这些调试逻辑去掉就好。