用Vivado搭建多通道DMA系统:从零讲透软硬件协同设计
你有没有遇到过这样的场景?
四路ADC同时采样,每秒产生几GB的数据,结果CPU还没开始处理,FIFO就已经溢出了。或者视频流一上来,整个系统卡顿、丢帧严重——问题不在于算法不够快,而是数据搬不动。
在高性能FPGA系统中,瓶颈往往不在逻辑计算,而在于数据通路的效率。这时候,靠CPU一个字节一个字节去读外设早已过时。真正高效的方案是:让DMA替你干活,CPU只负责调度和决策。
今天我们就来手把手拆解一个实战级的解决方案:如何利用Xilinx Vivado中的AXI DMA IP核,构建一个多通道、高吞吐、低延迟的DMA通信系统。不只是“照着菜单点菜”,而是带你理解每一步背后的工程逻辑,让你下次自己也能搭出来。
为什么非得用DMA?先看一组对比
假设我们要从PL端的ADC模块把数据搬到DDR内存里。三种方式,差距有多大?
| 方式 | CPU占用 | 吞吐率 | 实时性 | 适用场景 |
|---|---|---|---|---|
| 轮询PIO | 高到爆表(>90%) | <100 MB/s | 差 | 小数据量调试 |
| 简易DMA | 中等(~40%) | ~500 MB/s | 一般 | 中速采集 |
| AXI DMA + Scatter-Gather | 极低(<5%) | >1.5 GB/s | 强 | 多路高速实时系统 |
看到没?同样是“搬数据”,性能差了十几倍。关键就在于——谁来控制总线。
传统方式是CPU亲自下场,每收到一个数据就写一次内存;而DMA则是交给专用硬件,CPU只说一句:“你去把这堆数据搬到那个地址。”然后就可以继续干别的去了。
尤其是在Zynq这类SoC平台上,PS(ARM处理器)和PL(FPGA逻辑)之间每天都在“传情递信”,没有高效通道,再强的算法也白搭。
AXI DMA到底是什么?别被名字吓住
AXI DMA,全称叫AXI Direct Memory Access,是Xilinx提供的一款标准IP核,本质就是一个“自动搬运工”。
它有两个主要通道:
- S2MM(Stream to Memory Map):把来自FPGA侧的AXI Stream数据写进DDR;
- MM2S(Memory Map to Stream):从DDR读数据发给FPGA逻辑。
这两个名字听起来玄乎,其实很好记:
- S2MM → “Stream进来,存到Memory”
- MM2S → “Memory拿出来,变成Stream发出去”
每个通道都支持Scatter-Gather模式,也就是说,哪怕你的数据分散在内存各处,DMA也能自动拼起来传输,不需要你提前整理成一大块连续空间。这对Linux系统尤其友好,因为虚拟内存本来就是碎片化的。
而且它基于AXI4协议,天然支持突发传输、乱序响应、高带宽访问,理论峰值能跑到64位宽 × 250MHz = 2 GB/s以上,完全满足大多数高速应用需求。
多通道不是简单复制粘贴,得讲究架构
你说,我要四个ADC通道,那就放四个DMA核不就行了?没错,但怎么连、怎么管、会不会打架,这才是重点。
我们以最常见的多实例法为例——即每个通道独立使用一个AXI DMA IP核。这种方式结构清晰、调试方便,适合初学者掌握,也是工业项目中最常用的方案之一。
硬件架构长什么样?
[ADC0] → [FIFO + AXIS Register Slice] → [DMA_0] ↘ [ADC1] → [FIFO + AXIS Register Slice] → [DMA_1] →→ [AXI Interconnect] → DDR Controller [ADC2] → [FIFO + AXIS Register Slice] → [DMA_2] ↗ [ADC3] → [FIFO + AXIS Register Slice] → [DMA_3] ↗ ↓ [Zynq PS - ARM Core] ↓ [Bare-metal 或 Linux App]关键点如下:
- 每个ADC输出走独立的AXI4-Stream路径;
- 中间加FIFO和Register Slice做时钟域隔离与背压缓冲;
- 每个DMA有自己的M_AXI_MM2S接口,通过AXI Interconnect汇聚到PS端的HP(High Performance)端口;
- PS端分配不同的基地址给各个DMA,软件可以分别控制;
- 数据最终写入预分配的物理内存区域,供CPU后续处理。
这样做的好处是:各通道相互隔离,避免互相干扰。即使其中一个通道速率波动,也不会影响其他通道的稳定性。
在Vivado里怎么搭?一步步来
打开Vivado,新建Block Design,接下来几步至关重要:
第一步:添加ZYNQ Processing System
不管是Zynq-7000还是UltraScale+ MPSoC,都要先把这个核心IP拖进来。
双击配置,进入Clock Configuration,确保PL侧时钟足够驱动数据流(比如100MHz或更高);然后进HP Slave Ports,启用至少两个HP接口(如HP0、HP1),用于连接多个DMA。
⚠️ 提示:如果你有四个DMA,建议使用AXI Interconnect来聚合,而不是全接到同一个HP口上,否则容易带宽争抢。
第二步:添加多个AXI DMA IP核
在IP Catalog里搜AXI DMA,拖四个出来,分别命名为dma_0到dma_3。
每个DMA的关键配置建议如下:
| 参数 | 推荐设置 | 原因说明 |
|---|---|---|
| Enable Scatter Gather | ✔️ 开启 | 支持大块/非连续内存传输 |
| Buffer Length Register Width | 23-bit | 单次最大支持8MB缓冲区 |
| Include Slave Sideband Port | ✔️ 开启 | 可传递tuser、tlast等用户信号 |
| Maximum Burst Size | 256 | 匹配DDR突发长度,提升效率 |
| Address Width | 32-bit 或 64-bit | 根据系统内存大小选择 |
特别注意:一旦开启Scatter Gather模式,你就不能再用简单的SimpleTransfer接口了,必须使用BD(Buffer Descriptor)链表管理机制。不过对于多数应用场景,先用Simple模式跑通也没问题。
第三步:连接AXI总线
将每个DMA的M_AXI_S2MM和M_AXI_MM2S(如果用双向)连接到AXI Interconnect的Slave端口;Interconnect的Master端接Zynq的S_AXI_HPx接口。
记得为每个DMA分配唯一的基地址!Vivado会自动生成,但你可以手动调整,便于后期软件寻址。
最后运行Validate Design,检查是否有未连接或冲突问题。
第四步:生成HDL封装 & 导出硬件
点击Generate Block Design,生成顶层包装文件;然后右键Design →Create HDL Wrapper。
完成后导出硬件平台(.xsa或.hdf),准备进入Vitis进行软件开发。
软件怎么写?别忘了缓存一致性!
很多人硬件搭得好好的,结果软件读出来全是错的——原因往往是忽略了Cache一致性。
ARM处理器有缓存,FPGA写的内存数据可能还在cache里没刷出来,CPU就读了旧值。解决办法很简单:三步走策略。
#include "xaxidma.h" #include "xparameters.h" #include "xil_cache.h" XAxiDma AxiDmaInst[4]; // 四个DMA实例 // 初始化第N个DMA int init_dma_channel(int chan_id, u16 device_id) { XAxiDma_Config *cfg; int status; cfg = XAxiDma_LookupConfig(device_id); if (!cfg) return XST_FAILURE; status = XAxiDma_CfgInitialize(&AxiDmaInst[chan_id], cfg); if (status != XST_SUCCESS) return XST_FAILURE; // 关闭中断(若用轮询) XAxiDma_IntrDisable(&AxiDmaInst[chan_id], XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_MEMORY); return XST_SUCCESS; }初始化之后,启动接收要格外小心:
// 启动某通道接收数据 int start_receive(int chan_id, u32 phy_addr, u32 len) { XAxiDma *dma = &AxiDmaInst[chan_id]; int status; // 【关键】清除DCache,确保FPGA可写入最新内存 Xil_DCacheFlushRange(phy_addr, len); status = XAxiDma_SimpleTransfer(dma, phy_addr, len, XAXIDMA_DEVICE_TO_MEMORY); if (status != XST_SUCCESS) { return XST_FAILURE; } return XST_SUCCESS; } // 并行启动四个通道 void start_all_channels() { start_receive(0, CH0_BUF_PHY, BUF_LEN); start_receive(1, CH1_BUF_PHY, BUF_LEN); start_receive(2, CH2_BUF_PHY, BUF_LEN); start_receive(3, CH3_BUF_PHY, BUF_LEN); }当传输完成时,如果是中断模式,记得在ISR中执行:
void dma_isr(void *callback) { // 【关键】使无效cache,强制CPU重新加载新数据 Xil_DCacheInvalidateRange(buffer_addr, length); // 此时读取buffer才是最新数据 process_data((u8*)buffer_addr); }记住口诀:发送前Flush,接收后Invalidate。
常见坑点与调试秘籍
❌ 问题1:数据错位、丢失
现象:采集波形歪了,或者每隔一段就跳变。
排查方向:
- PL端数据速率是否超过DMA写DDR能力?比如16位@100MHz = 200MB/s,看起来不高,但如果多个通道叠加,很容易逼近极限。
- FIFO深度够吗?建议≥512深度,并加上异步复位保护。
- 是否忘记刷新Cache?这是90%初学者踩过的坑。
❌ 问题2:多通道互相干扰
现象:单独跑一个通道正常,一起跑就卡顿甚至死机。
根本原因:多个DMA共用同一个AXI Interconnect或DDR控制器,引发总线仲裁延迟。
优化手段:
- 给每个DMA分配独立HP端口(如HP0、HP1、HP2、HP3);
- 使用Vivado的AXI Frequency Scaling工具评估带宽占用;
- 错峰启动,比如延时几毫秒依次开启,避免瞬时拥塞。
✅ 调试技巧推荐
- ILA抓AXI信号:把
tvalid,tready,tdata,tlast打进去,看握手是否正常,有没有背压阻塞。 - 查DMA状态寄存器:通过
XAxiDma_ReadReg()读内部寄存器,判断是否发生Timeout或Alignment Error。 - Tcl脚本批量生成DMA:写个Tcl脚本自动创建多个DMA并命名,省时又不易出错。
for {set i 0} {$i < 4} {incr i} { create_bd_cell -type ip -vlnv xilinx.com:ip:axi_dma:dma_$i set_property CONFIG.Component_Name axi_dma_$i [get_bd_cells axi_dma_$i] }实际应用场景举例
这套架构适用于哪些真实项目?
✅ 多路同步ADC采集
- 医疗设备中的多导联心电图
- 工业传感器阵列(温度、振动、压力)
- 雷达/声呐回波信号并行捕获
✅ 视频图像处理
- 多摄像头输入拼接
- HDMI环出+本地分析双路并行
- 图像预处理(去噪、缩放)前置加速
✅ 通信与网络
- 多路Ethernet数据汇聚
- RF采样数据实时上传
- PCIe-to-AXI桥接转发
只要涉及“多个高速数据源 → 统一内存池 → CPU处理”的场景,这套多通道DMA架构都能派上大用场。
最后一点思考:未来还能怎么升级?
你现在掌握了基础版的多通道DMA系统,下一步呢?
- 想对接Linux?可以用PetaLinux构建系统,配合UIO驱动或更高级的DMA-BUF机制实现用户空间零拷贝。
- 想做动态调度?可以用AXI Streaming Switch配合单个DMA,实现数据路由切换,节省资源。
- 想进一步提速?试试VDMA(Video DMA)或CDMA(Central DMA),针对特定场景优化。
- 想做远程监控?把DMA数据打包通过UDP发送,实现高速遥测。
技术和架构永远在演进,但核心思想不变:让合适的模块干合适的事。FPGA擅长并行流水,CPU擅长复杂调度,DMA就是它们之间的“高速公路”。
当你能把这条通路打通,你会发现,很多曾经束手无策的性能难题,突然就有了答案。
如果你正在做类似项目,欢迎留言交流经验,我们一起把这条路走得更稳、更快。