DMA_SG_FIFO 技术解析:基于 SG DMA 与 FIFO 架构的高效数据传输方案
在现代嵌入式系统中,一个看似简单的问题却常常成为性能瓶颈——如何让外设和内存之间的海量数据“安静地流动”,而不惊动 CPU?想象一下音频设备持续采集 PCM 流、工业传感器不断上报采样值,或是摄像头实时输出视频帧。如果每字节都要 CPU 亲自搬运,那处理器早就被中断淹没,根本无暇处理真正复杂的逻辑。
于是,DMA(Direct Memory Access)应运而生。它像一条自动化货运铁路,把数据从起点拉到终点,全程无需 CPU 驾驶。但随着应用需求升级,传统 DMA 的“单程票”模式已不够用:内存不再连续,数据源愈发分散,时钟域交错复杂。这时, SG DMA + FIFO 的组合便成了高性能数据通路的标配架构。
这类设计常见于 FPGA 工程(如标题中的 DMA_SG_FIFO.zip 所示),也广泛应用于 SoC 控制器、网络接口、音视频桥接等场景。其核心思想是:用 Scatter-Gather DMA 实现灵活的非连续内存访问 ,再通过 FIFO 缓冲解耦速率差异、吸收抖动 ,最终达成高吞吐、低延迟、低 CPU 占用的数据流管理。
我们不妨从一个实际问题切入:假设你正在开发一款智能录音笔,需要以 48kHz/16bit 立体声持续录制环境声音,要求连续工作数小时不丢帧。你能接受每隔几毫秒就被 I2S 接口打断一次吗?显然不能。而更糟糕的是,ADC 输出节奏并不完全均匀,总线也可能因其他任务繁忙导致响应延迟。
这时候,单纯靠轮询或块传输 DMA 并不能解决问题。你需要的是一个能“自己干活”的系统——它能自动识别多个缓冲区位置,在合适时机批量取数,并容忍一定程度的时序波动。这正是 SG DMA 与 FIFO 协同工作的舞台。
Scatter-Gather DMA:让数据搬运摆脱“连续内存”的束缚
传统 DMA 像是一辆只能跑固定路线的公交车:给定起始地址和长度,一趟拉完所有数据。但如果乘客分布在城市各个角落呢?这就引出了 Scatter-Gather DMA(分散-聚集 DMA) ——它更像是一个智能快递网络,可以根据订单列表逐个派送,无需提前把货物集中装车。
SG DMA 的关键在于引入了 描述符链表(Descriptor Chain) 。每个描述符记录一段传输的信息:
typedef struct {
uint32_t src_addr; // 源物理地址
uint32_t dst_addr; // 目标物理地址
uint32_t length; // 本次传输字节数
uint32_t ctrl_flags; // 控制位:EOF 表示结束,INTR 表示触发中断
uint32_t next_desc_ptr; // 下一个描述符的物理地址
} sg_descriptor_t;
CPU 只需初始化这个链表并告诉 DMA 控制器“从哪里开始”,后续操作全部由硬件自动完成。例如,在环形缓冲录音场景中,你可以预设三个缓冲区 A、B、C,构成循环链表。当 A 写满后,SG DMA 自动切换至 B,再切至 C,最后回到 A——整个过程无需软件干预。
这种机制的优势非常明显:
- 避免内存复制 :传统方式往往需要先将分散数据拷贝到一块连续区域才能交给 DMA,白白消耗带宽;
- 支持无限流式传输 :只要链表不断,数据就可以源源不断地流动;
- 降低中断频率 :可以设置每 N 个缓冲区才通知一次 CPU,显著减少上下文切换开销。
在 Linux 内核中, virtio-net 就利用 SG DMA 处理分片报文;Xilinx 的 AXI DMA IP 核也原生支持 SG 模式用于视频流直通 DDR。这些都不是巧合,而是对效率极致追求的结果。
当然,使用 SG DMA 也有一些工程细节需要注意。比如描述符必须位于物理连续且可被 DMA 主控访问的内存区域(通常使用 dma_alloc_coherent() 分配);某些控制器还要求描述符地址 8 字节或 16 字节对齐。此外,链表更新必须保证原子性,否则可能引发指针错乱导致系统崩溃。
FIFO:不只是缓存,更是系统的“减震器”
如果说 SG DMA 是搬运工,那么 FIFO 就是它的临时仓库。很多人以为 FIFO 只是用来防溢出的小缓冲,其实它的作用远不止于此。
考虑这样一个现实:I2S 接口以固定的位时钟(BCLK)发送数据,而 DMA 访问内存则依赖系统总线(如 AXI),两者时钟不同步,且总线可能被 GPU、USB 或其他外设抢占。如果没有中间缓冲,哪怕只是几十纳秒的延迟,都可能导致数据丢失。
FIFO 在这里扮演了多重角色:
1. 速率解耦
生产者(外设)和消费者(DMA)不必同步运行。传感器可以间歇性采样,FIFO 积累一定量后再唤醒 DMA 发起一次突发传输,极大提升总线利用率。
2. 突发优化
现代总线协议(如 AXI4)支持 INCR 和 WRAP 突发模式,一次传输多个数据可显著降低地址建立开销。FIFO 允许我们“攒够一波再走”,比如等到半满(half-full)再启动 DMA,实现更高的有效带宽。
3. 跨时钟域同步
异步 FIFO 使用格雷码计数器配合双级同步器,可在不同时钟域之间安全传递数据。这对于连接音频 codec、高速 ADC/DAC 等独立时钟设备至关重要。
下面是一个典型的水位检测逻辑,用于触发 DMA 请求:
module fifo_watermark_checker (
input clk,
input rst_n,
input [WIDTH-1:0] data_in,
input wr_en,
input rd_en,
output reg [WIDTH-1:0] data_out,
output reg empty,
output reg almost_full,
output dma_req
);
parameter DEPTH = 16;
parameter WATERMARK = 8;
reg [3:0] wr_ptr, rd_ptr;
reg [WIDTH-1:0] mem [0:DEPTH-1];
wire fifo_full;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) wr_ptr <= 0;
else if (wr_en && !fifo_full) wr_ptr <= wr_ptr + 1;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rd_ptr <= 0;
else if (rd_en && !empty) rd_ptr <= rd_ptr + 1;
end
always @(posedge clk) begin
if (wr_en && !fifo_full)
mem[wr_ptr] <= data_in;
end
assign data_out = mem[rd_ptr];
assign empty = (wr_ptr == rd_ptr);
assign fifo_full = (wr_ptr - rd_ptr == DEPTH - 1);
assign almost_full = (wr_ptr - rd_ptr >= WATERMARK);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dma_req <= 0;
else
dma_req <= almost_full; // 达到阈值即请求 DMA
end
endmodule
这段代码虽然简洁,但体现了关键设计理念: 不要一有数据就搬,而是等值得搬的时候再搬 。这就像物流系统不会为一件商品单独发一辆货车,而是凑够一整车才出发。
实际应用场景:音频采集中的协同运作
让我们回到前面提到的录音笔案例,看看这套机制是如何落地的。
系统结构大致如下:
[Audio Codec]
↓ (I2S Data)
+------------+
| Async FIFO | ← 跨时钟域缓冲,深度 64 字节
+------------+
↓
+------------------+
| SG DMA Controller| ← 自动读取 FIFO 数据并写入内存
+------------------+
↓
[DDR Buffer A → B → C → A...] ← 环形缓冲池
工作流程如下:
- CPU 初始化三个 1KB 的接收缓冲区,构建 SG 描述符链,指向这三个区域;
- 启动 SG DMA,监听来自 FIFO 的
dma_req信号; - 音频数据进入异步 FIFO,当积攒到 32 字节时,
almost_full触发,发出 DMA 请求; - SG DMA 响应请求,从 FIFO 读取数据,按当前描述符信息写入 Buffer A;
- 当 A 写满后,自动跳转至 B,依此类推;
- 整个链表完成一轮循环后,触发 EOF 中断,CPU 获取完整音频帧进行处理。
整个过程中,CPU 仅在中断时介入,其余时间可休眠或执行其他任务。即使总线短暂拥塞,FIFO 也能提供足够的缓冲裕量防止溢出。更重要的是,由于使用了 SG 模式,缓冲区无需连续分配,极大提升了内存管理灵活性。
设计实践中的关键考量
要在真实项目中稳定运行这套机制,还需注意以下几点:
✅ FIFO 深度选择
至少覆盖最坏情况下的响应延迟。例如,在裸机环境下中断延迟约 10μs,在 RTOS 中可能达 50μs 以上。对于 48kHz 音频(每秒 96k 字节),这意味着最多可能积累 4.8 字节/ms,建议最小深度为 64~128 字节。
✅ 对齐与内存一致性
描述符和缓冲区应确保物理地址对齐(常见为 8 或 16 字节)。在 ARM 架构下,若使用 Cache,需注意 DMA 缓冲区的 cache 一致性问题,必要时调用 clean/invalidate 操作。
✅ 中断合并策略
频繁中断会拖慢系统。可设置“每两帧中断一次”或使用定时器驱动的 polling 机制,在延迟与负载之间取得平衡。
✅ 错误检测与恢复
监控 FIFO overflow/underflow 状态。一旦发生错误,应及时停用 DMA、刷新 FIFO、重新加载描述符链,避免数据进一步错乱。
✅ 功耗控制
在低功耗设备中,空闲时可关闭 DMA 时钟,通过外设事件(如 FIFO 非空)唤醒系统,实现动态节能。
这套 SG DMA + FIFO 架构之所以强大,是因为它不仅仅是一项技术,更是一种系统思维: 将控制流与数据流分离,让硬件做擅长的事,让软件专注高层次调度 。
无论是 FPGA 工程师编写 Verilog 实现定制化流控引擎,还是嵌入式开发者编写 Linux 驱动管理 buffer pool,理解这一架构都能带来质的飞跃。它支撑着确定性延迟的数据管道,是实现实时音视频、工业控制、边缘 AI 推理等关键应用的基础。
未来,随着 AIoT 和边缘计算的发展,这类“零拷贝、低延迟、高并发”的数据通路还将继续演进。我们可以预见,更多高级特性将被集成进来:
- 支持 Cache 一致性的 CHI 总线接口
- 虚拟化环境下的 vDMA 技术
- 基于 QoS 的多优先级传输调度
但无论形式如何变化,其核心理念始终不变:让数据自由流动,让系统更加高效。而这,正是优秀系统设计的魅力所在。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
61

被折叠的 条评论
为什么被折叠?



