十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DMA+SG+FIFO:嵌入式高性能数据通路的实战解析

DMA+SG+FIFO:嵌入式高性能数据通路的实战解析 简介DMA_SG_FIFO.zip是一套基于Vivado 2017的AXI DMA工程例程面向FPGA开发者演示在Xilinx AX7015Kintex-7平台上通过Scatter-Gather模式和FIFO实现高效数据搬运适合刚接触DMA或希望移植该IP的工程师学习。压缩包共1174个文件合计约48.73MB内容覆盖硬件与软件两侧既包含大量Verilog/VHDL和C语言源码、XDC约束文件、Tcl工程脚本、XCI IP配置也包含bit流、elf可执行文件和HDF硬件定义文件以及一些文本说明与日志基本可还原一个完整VivadoSDK工程。目前已有1262人学习下载属于AXI DMA主题下较受欢迎的工程样例。通过分析工程结构和代码可以清晰看到AXI DMA从IP配置、SG列表构建到FIFO读写时序的完整链路理解DMA如何绕开CPU完成连续多缓冲区传输工程中的脚本和约束文件还可用于快速复现实验环境为后续PCIe、视频传输等高速应用打基础。 干活的人都知道搞嵌入式最烦的不是业务逻辑而是数据搬运。CPU 去 memcpy 一堆外设数据费时费力还容易丢中断。所以看到这个 DMA_SG_FIFO.zip我第一反应就是“懂行的来了”——DMA 搬运、SG 模式解决物理内存不连续的问题、FIFO 做缓冲削峰这三样凑一块儿基本就把嵌入式高性能数据通路上的大坑全填上了。这个压缩包解决的是一个很典型的痛点当你面对串口不定长数据、ADC 多通道连续采样、或者高速外设比如 UFS、SD 卡、网卡的数据流时普通 DMA 的连续内存限制会让驱动写得很难受。要么申请大块连续内存系统跑久了碎片化严重要么频繁中断 CPU 去拆包组包性能直接拉胯。而 SGScatter-Gather配合 FIFO就是把“数据从哪来、放哪去、什么时候处理”这三件事彻底解耦。适合正在调驱动、写 BSP、或者被串口 DMA 没收定长数据折磨的朋友尤其是用 STM32、Zynq、瑞萨这类带 DMA 控制器平台的开发者。整个代码包的思路我拆开看了一下核心设计不是堆功能而是把数据流理顺。下面按我自己的理解把架构、细节、实操和坑位一次说清。1. 整体设计思路DMA、SG、FIFO 为什么要组合在一起1.1 三个关键词各管哪一段先聊清楚这三者的分工不然看代码容易晕。DMADirect Memory Access管的是“搬运不占 CPU”。外设和内存之间的数据交换直接走硬件通道CPU 只需要在搬运开始和结束时被通知一下。这里容易被忽略的是DMA 搬运的触发源可以是软件也可以是硬件事件——比如串口的 RXNE、ADC 的 EOC、定时器的更新事件。SGScatter-Gather管的是“物理内存不连续也能搬”。普通 DMA 要求源地址和目标地址是连续的但嵌入式系统的 RAM 在使用一段时间后很难保证拿到一块大的连续物理内存。SG 模式通过一个描述符表SG Table把多个不连续的内存块串起来DMA 硬件逐个处理搬完一块自动切到下一块。对上层应用来说看到的还是一段连续的虚拟地址空间。FIFOFirst In First Out管的是“速率不匹配和数据粘包”。DMA 搬运是突发性的而 CPU 处理逻辑可能还在忙别的事或者业务层需要按帧/按包读取数据。FIFO 在中间做一个缓冲池DMA 只管往 FIFO 里填应用层按自己的节奏从 FIFO 里取两边互不阻塞。有一次我在调试一块带 DMA 的采集卡驱动板子跑久了之后 kmalloc 连续内存失败整个采集流程直接崩了。后来切到 SG 模式用几个 4KB 的不连续页就解决了问题——这就是这三件套存在的意义它们解决的是真实世界里“内存不完美”的问题。1.2 这套组合为什么比裸 DMA 好用单用 DMA 也能跑但碰到大数据量或不定长数据就会很痛苦。比如串口接收如果用固定长度的环形 Buffer DMA必须处理“半满中断”和“全满中断”的边界情况如果数据帧长度小于 DMA 配置长度你永远不知道一帧数据什么时候结束。SG FIFO 的方案把这个问题拆成了两层SG 层负责把数据散落到多个缓冲区描述符中每个描述符记录块地址、长度和下一个描述符的指针。这样即使业务层产生了“拆包”的需求也只是在描述符层面做链接不涉及数据拷贝。FIFO 层负责按字节流或按包的语义向上抛数据。底层 DMA 写完一块就往 FIFO 里 push 一块应用层按自己的协议解析从 FIFO 里 pop 数据。数据从“硬实时搬运”变成了“软实时消费”极大降低丢数据的概率。我见过不少开发者把 FIFO 和 Buffer 混为一谈其实差别不小。Buffer 强调的是一块存储区域本身没有语义而 FIFO 强调的是“先入先出”的顺序保证带读指针和写指针的管理。拿刷牙来类比Buffer 相当于你装了杯水FIFO 则是你按牙膏、牙刷、杯子的固定顺序取用——顺序错了体验就崩了。2. SG-DMA 模式的细节拆解描述符与链表2.1 SG 表怎么组织SG 模式在硬件上依赖 DMA 控制器的 SG 功能在软件上核心是一个数组或链表每个节点其实就是一个描述符typedef struct { uint32_t addr; // 内存块物理地址 uint32_t len; // 块长度 uint32_t next; // 下一个描述符地址 uint32_t ctrl; // 控制位中断使能、最后一块标记等 } sg_desc_t;有些 DMA 控制器比如 STM32 的 DMA 流用的是固定大小的描述符数组有些则用链式结构。使用链式的灵活性更高增删一个内存块只需要修改前后指针不需要重新配置整个 DMA。实操中有个关键点描述符本身要放在内存里而且地址对齐要求非常严格。我调 STM32 的时候描述符数组必须放在 4 字节对齐的地址上否则 DMA 控制器会直接报总线错误。建议在链接脚本或代码里显式声明对齐属性sg_desc_t sg_table[SG_NUM] __attribute__((aligned(32)));2.2 SG 和普通 DMA 怎么选不是说所有场景都要上 SG。如果你的系统内存足够连续或者单次搬运长度不大普通 DMA 就够了。SG 的“缺点”在于描述符表本身会占用内存还会增加 DMA 配置的复杂度。每次搬运需要在多个描述符之间切换硬件在切换时会有微小的间隙gap对时序极其敏感的外设可能受不了。调试起来比常规 DMA 麻烦一个描述符配错数据流就断了而且错误现象往往是“神秘数据错位”。我的建议是先确认瓶颈在哪里。如果只是 ADC 单通道采样、串口收几十个字节这种量级普通 DMA 完全够如果要做 USB 摄像头采集、以太网收发、UFS 读写这类大吞吐场景SG 是绕不开的。2.3 SG 模式与分布式 DMA 的关系从热搜词里看到“分布式 DMA”这块其实和 SG 是近亲。分布式 DMA 的概念是在系统里多个 DMA 引擎或多级 DMA 路径同时工作每个引擎管理一部分数据搬运任务通过描述符将它们串成一个整体。比如在高性能存储控制器中Host 端下发一个 SG List 到 DMA 引擎DMA 引擎按列表从设备端拉数据到不同的内存位置拉完一轮之后通过中断或状态位通知 Host。这种方式对驱动开发者最大的启发是不要把 DMA 当成一个点对点的搬砖工具而要把整个数据通路当成一条流水线来设计。3. FIFO 缓冲层的实现从“缓冲”到“包边界保护”3.1 为什么 DMA 需要 FIFO 配合DMA 本身不关心数据语义它只是搬运工。如果应用层直接去读 DMA 写过的内存会面临两个问题CPU 处理速度跟不上 DMA 搬运速度数据覆盖。数据是流式的不知道一帧从哪里开始、到哪里结束。FIFO 在这里起到“蓄水池”的作用。DMA 只管往水池里注水应用层按需取水。如果水池快满了FIFO 水位达到高阈值可以触发中断让 CPU 紧急排水如果水池快空了低阈值则可以触发中断让 DMA 重新填水。3.2 环形 FIFO 实现要点工程里用得最多的是环形 FIFO因为它不需要移动数据只需要维护读指针和写指针。核心数据结构typedef struct { uint8_t *buffer; uint32_t size; volatile uint32_t head; // 写指针 volatile uint32_t tail; // 读指针 } ring_fifo_t;读写指针头的自增要取模为了保证效率一般把 size 设置为 2 的幂然后用位与代替取模static inline uint32_t fifo_len(ring_fifo_t *f) { return (f-size - 1) (f-head - f-tail); }这里容易踩坑的是head 和 tail 必须用 volatile 修饰。因为头指针由 DMA 中断或写端更新尾指针由读端可能是主循环或另一个中断更新编译器如果不知道这两个变量会被异步修改很可能优化出错误代码。3.3 异步 FIFO 与包边界保护从热搜词里看到“异步 FIFO 时序图”和“带包边界保护的 AXI-Stream FIFO”这两块偏 FPGA 方向但思路对嵌入式同样适用。异步 FIFO 解决的是跨时钟域问题核心是把读指针和写指针编码成格雷码后同步到对端时钟域从而避免多比特信号在跨时钟域时出现中间态。这类 FIFO 的典型结构是写端口写时钟域写使能、写数据、写指针。读端口读时钟域读使能、读数据、读指针。状态判断满/空状态不能直接比较读写指针要经过两级同步器同步后再比较。关于包边界保护设计思路是在 FIFO 的头部增加一个“帧长度”注册项或者为每个数据包打一个起始/结束标志位。在 AXI-Stream 里就是 TVALID/TREADY 配合 TLAST 信号TLAST 拉高表示一个包的最后一个数据。嵌入式侧的环形 FIFO 也可以仿照这种思想在写入数据前先写入包长度读取时先读长度再按长度读整包数据这样就不会出现“半包”或“跨包”的解析错误。用代码来实现包边界的话就是入队时把整包数据连续写入 FIFO并在 FIFO 的 meta 区记录长度。出队时先 peek 长度确认 FIFO 里有足够的数据后再一次性取走。如果 FIFO 剩余空间不够一包宁可等也不要拆包——这就是“带包边界”的真正含义。4. 实操复盘串口 DMA 接收不定长数据与多通道 ADC 采样4.1 串口 DMA 接收不定长数据的标准打法串口接收不定长数据是网上问得最多的问题很多朋友用串口 DMA 收数据结果发现“数据长度不对”“粘包”“断帧”归根到底是用错了中断策略。标准做法是空闲中断IDLE DMA 接收 FIFO 缓冲。串口在收到一帧数据后总线进入空闲状态硬件触发空闲中断。在空闲中断里只需要做一次 DMA 剩余数据长度的读取算出本次接收了多少字节然后把 DMA 搬运完的数据 push 进 FIFO最后重新配置 DMA 接收即可。伪代码void UART_IDLE_IRQHandler(void) { uint32_t remain DMA_GetCurrDataCounter(DMA_STREAM); uint32_t received RX_BUF_SIZE - remain; fifo_push(rx_fifo, rx_buf, received); DMA_Cmd(DMA_STREAM, DISABLE); DMA_SetCurrDataCounter(DMA_STREAM, RX_BUF_SIZE); DMA_Cmd(DMA_STREAM, ENABLE); }关键细节有三个DMA 的接收缓冲区长度要大于或等于最大帧长但不要设得太大否则空闲中断前的数据搬运延迟会变大。如果外设支持“DMA 半传输中断”和“DMA 传输完成中断”比如 STM32 的 DMA 流可以先把数据搬运到一块较大的缓冲区再配合空闲中断做分包这样可以支持超大帧。要开启 DMA 接收完成中断TC吗不建议把 TC 作为帧结束的唯一依据。TC 表示 DMA 把配置的长度搬完了不能代表“一帧结束”。帧结束的唯一可靠信号是空闲中断或超时中断。4.2 DMA 发送要不要等上一轮完成这个问题很多新手会问。以 STM32 为例如果你在 DMA 发送完成中断里直接修改 DMA 缓冲区数据并再次使能 DMA是安全的因为硬件已经完成了全部搬运。但如果主循环里“粗暴地”关掉 DMA、改数据、再开启 DMA就有风险DMA 可能正好搬运到一半数据被撕成两半发送出去。稳妥的做法是维护一个“发送忙”标志位在 DMA 传输完成中断里清标志。想发送新数据之前先检查标志volatile uint8_t tx_busy 0; void DMA_TX_Complete_IRQHandler(void) { tx_busy 0; } int uart_send_dma(uint8_t *data, uint16_t len) { if (tx_busy) return -1; // 上一轮没发完返回忙 tx_busy 1; memcpy(dma_tx_buf, data, len); DMA_Start(dma_tx_buf, len); return 0; }这里又牵扯到“要不要 memcpy 进 DMA 发送缓冲区”的问题。如果数据本身在内存中生命周期可控可以让 DMA 直接发原地址。但如果数据来自栈上或者会被改写就必须拷贝到 DMA 专用缓冲区中。工程上为了稳妥一般都会准备一个 DMA_TX_BUF 并做一层拷贝代价是多一次内存搬运换来的是安全性。4.3 ADC 多通道 DMA 采样配置要点热搜词里“ADC 四通道使用 DMA”“STM32 HAL 库 ADC 单通道 DMA 多次采样”这类问题也很高频。ADC 多通道 DMA 采样的核心是把 ADC 转换完成事件作为 DMA 请求源每个通道的转换结果依次写入 DMA 缓冲区的不同位置。配置时有几个容易忽略的地方DMA 缓冲区的数据宽度必须和 ADC 数据寄存器宽度一致。12 位 ADC 右对齐时数据寄存器是 16 位DMA 宽度也应配成半字Half Word。多通道扫描模式下DMA 缓冲区大小等于“通道数 × 采样次数”。比如 4 通道采 10 次缓冲区就是 40 个半字。如果开启了 DMA 循环模式缓冲区满了之后会从头部重新开始覆盖写这在连续采样场景下非常适合但要控制读写速度不匹配的问题——建议配合 Ping-Pong 缓冲或半传输中断来切换处理。我遇到过一种诡异现象ADC 多通道 DMA 采集的数据前几个值是“固定错误”的。排查后发现是 ADC 刚启动时通道还没稳定转换DMA 就已经搬运了初值。后来在启动 ADC 前先做几次空转换或者通过软件触发一次转换丢弃结果问题就消失了。这类“启动毛刺”在 ADC DMA 采集里非常常见不是 DMA 配置的锅是 ADC 建立时间的问题。5. 常见问题与排查技巧实录整理一下我在实际调试 DMA、SG、FIFO 过程中踩过的一些坑按现象、原因、方案列成速查表现象可能原因排查与解决方案DMA 接收数据最后一个字节丢失外设的 RXNE 信号和 DMA 请求时序不匹配或 DMA 传输比外设寄存器更新更快检查外设是否支持 DMA 最后字节锁定或按要求配置 40bit 数据宽度对齐必要时降速或用空闲中断兜底数据错位若干字节DMA 缓冲区地址没有按外设要求对齐或缓存一致性问题带 Cache 的 CPU检查地址对齐对 Cortex-M7 等带 Cache 内核需在 DMA 前后执行 SCB_InvalidateDCache 或 CleanDCacheSG 描述符执行时卡死描述符的 next 指针没有正确指向下一个描述符或者最后一块的 ctrl 位没有设置“完成”标志打印全部描述符内容核对地址、长度、next 是否与预期一致建议先用 2 个描述符最小化复现FIFO 读出来数据乱序读指针更新不是原子的或者中断里 push 和主循环 pop 竞争同一缓冲区在 push/pop 操作中用临界区保护确认 head 和 tail 的更新顺序DMA 发送第二轮数据是上一轮的旧数据发送缓冲区在 DMA 搬运完成前被修改严格使用 tx_busy 标志位或使用双缓冲交替写保证“安全写窗口”DMA 收到大量数据后系统卡死DMA 中断频率过高其他中断被饿死或 FIFO 写满后覆盖未读数据提高 FIFO 容量、开启高水位中断在主循环中批量处理数据避免每个字节都进中断几个独家经验愿意看的记住就好SG 描述符表别放在 Cache 区。带 Cache 的 CPU 上DMA 读描述符时可能拿到脏数据。安全做法是把描述符放在非 Cache 的 RAM 区域或每次配置描述符后执行 Cache 清理操作。FIFO 容量不是越大越好。在低延迟控制场景大 FIFO 意味着数据在管道里呆的时间更长系统对外部事件的响应反而变差。FIFO 容量只要大于等于“最坏情况下的突发数据量”即可额外容量只会掩盖调度问题。调试 DMA 频率极高的问题时把信号量全换成“标志位 关中断”。很多 pbuf 类的内核结构在中断上下文里操作是有风险的裸标志位的释放和占有更可控。我做过一次串口 DMA 压力测试用全中断 信号量方案每秒只能处理 2000 包换成“关中断 标志位”后直接到了 8000 包。启动阶段多做一次空搬运。不管是 ADC、SPI 还是串口上电之后外设状态不确定DMA 第一次搬运的数据往往是垃圾数据。软件上可以做一个“启动丢弃”的动作把第一帧数据抛弃或者用固定格式的测试帧来验证链路。如果条件允许强烈建议在调完一轮之后用逻辑分析仪或示波器抓一下 DMA 请求信号和完成中断的时序比对着代码空想要高效得多——DMA 这类硬件行为有时候“看波形一秒胜过查代码三天”。我自己做数据通路调优时还有个习惯先把数据帧格式定死帧头、帧长、校验字固定下来再让 DMA 与 FIFO 配合做收发。这会让问题排查时的边界清晰很多不会出现“数据乱了自己都不知道对不对”的局面。这个 DMA_SG_FIFO 里的核心代码其实也是这个思路的产物先把数据流理顺再去考虑性能优化。后面我打算在这个基础上把 FIFO 层加上超时机制应对那些“发了一半就断掉”的异常帧让整套数据通路更健壮——如果你也正在搞 DMA 相关的方向可以顺着这个思路继续往下做后面遇到有趣的坑再回来聊。本文还有配套的精品资源点击获取
返回列表