2. DMA基础概念
在嵌入式与服务器 Linux 驱动开发中,CPU 频繁搬运数据会带来两个直接后果:一是占用大量指令周期,二是打断缓存与流水线的局部性。DMA(Direct Memory Access,直接存储器访问)正是为解决这一矛盾而设计的硬件机制:它允许外设与内存之间、内存与内存之间在不经过 CPU 逐字节搬运的前提下完成数据块传输,CPU 只需配置好源地址、目的地址、传输长度和触发方式,随后即可去处理其他任务,传输完成后再由中断通知。
一、DMA 的工作原理
一次典型的 DMA 传输可以拆成五个阶段:请求、仲裁、配置、搬运、完成通知。外设(如 UART、SPI、ADC、网卡)在需要发送或接收数据时,通过请求线向 DMA 控制器发出 DMA Request;DMA 控制器根据通道优先级进行仲裁,获得总线使用权后,从内存或外设 FIFO 读取数据并写入目标地址;每完成一个数据单元(beat)或一个块(burst),地址寄存器按配置递增或保持不变;当传输计数减到零时,DMA 控制器产生完成中断,驱动在中断服务程序中回收缓冲区、唤醒等待队列或提交网络协议栈。
从总线视角看,DMA 控制器是总线上的一个主设备(Master),它与 CPU 竞争总线带宽。因此在高带宽场景下,必须关注总线矩阵、QoS 和带宽分配,否则 DMA 可能反过来拖慢 CPU 对内存的访问。
二、传输类型与通道、请求线
按源和目的的组合,DMA 传输通常分为三类:内存到外设(如音频播放、UART 发送)、外设到内存(如 ADC 采样、网卡收包)、内存到内存(如大块数据搬移、图像处理)。前两类是驱动中最常见的,第三类在部分控制器上支持,但通常效率低于 CPU 的 SIMD 拷贝,需谨慎使用。
DMA 控制器内部由多个通道(Channel)组成,每个通道可独立配置源、目的、长度和触发源。通道与请求线(Request Line)之间通过多路选择器连接:外设的请求信号进入 DMAC 后,由通道映射表决定哪个请求触发哪个通道。Linux 中这一映射通常由设备树描述,例如:
/* 设备树片段:将 UART2 的发送请求映射到 DMA 通道 3 */
uart2: serial@40004000 {
compatible = "vendor,uart";
dmas = <&dma0 3 0>;
dma-names = "tx";
};
dma0: dma-controller@40020000 {
compatible = "vendor,dma";
#dma-cells = <2>;
dma-channels = <8>;
dma-requests = <16>;
};
驱动侧通过 DMA Engine 框架申请通道,核心 API 如下:
struct dma_chan *chan;
struct dma_slave_config cfg = {
.direction = DMA_MEM_TO_DEV,
.dst_addr = uart_base + UART_TDR,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
.dst_maxburst = 16,
};
chan = dma_request_chan(dev, "tx");
dmaengine_slave_config(chan, &cfg);
struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_slave_sg(chan, sgl, sg_len,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
tx->callback = uart_tx_done;
tx->callback_param = uart;
dmaengine_submit(tx);
dma_async_issue_pending(chan);
三、Linux 驱动中的典型应用场景
| 场景 | 方向 | 典型外设 | 驱动关注点 |
|---|---|---|---|
| 串口高速收发 | 内存 ↔ 外设 | UART / SPI | 环形缓冲区、半满中断、流控 |
| 音频播放与录制 | 内存 ↔ 外设 | I2S / SAI | 周期传输、双缓冲、指针同步 |
| 网络数据包收发 | 外设 ↔ 内存 | 以太网 MAC | 描述符环、NAPI、缓存一致性 |
| ADC 连续采样 | 外设 → 内存 | ADC / DMA | 循环模式、采样率、溢出处理 |
| 存储块传输 | 内存 ↔ 外设 | SDIO / NAND | 大块 SG 列表、超时与错误恢复 |
四、常见误区与复盘要点
误区一:认为 DMA 传输不需要缓存维护。对于非一致性架构,CPU 写内存后启动 DMA 发送,必须先 clean 缓存;DMA 接收完成后,CPU 读取前必须先 invalidate 缓存,否则可能读到旧数据或覆盖新数据。
误区二:在中断中做大量拷贝。DMA 的价值在于卸载搬运,若 ISR 中又用 CPU 逐字节拷贝,等于把卸载的工作重新捡回来,应尽量使用零拷贝或 SG 列表。
误区三:忽略通道资源竞争。多个外设共享 DMA 控制器时,若通道分配不当或优先级配置不合理,会出现请求丢失、传输延迟抖动,甚至死锁。
调试建议:先确认 dma_request_chan() 是否成功,再检查 dmaengine_slave_config() 的地址宽度与 burst 是否与外设 FIFO 匹配;用 cat /proc/interrupts 观察 DMA 中断计数是否增长;用 dmesg 查看 DMA 映射失败或超时日志。若传输完成但数据异常,优先排查缓存一致性与地址对齐问题。
理解 DMA 的定义、工作原理、传输类型、通道与请求线,以及它在 Linux 驱动中的典型落点,是后续学习“中断协同”和“缓存一致性调试”的前提。只有先建立“谁在搬、搬什么、搬完怎么通知、搬的过程中缓存是否一致”这条完整链路,才能在真实项目中快速定位 DMA 相关问题。