2. DMA基础原理

DMA(Direct Memory Access,直接存储器访问)的本质,是让外设与内存之间的大块数据搬运脱离 CPU 的逐字节参与。CPU 只负责“下命令”和“收完成通知”,真正的数据流由 DMA 控制器(DMAC)在总线上自主完成。理解 DMA,必须同时看清五件事:控制器内部由哪些通道与引擎组成、数据往哪个方向流、地址如何映射到物理内存、总线带宽如何被占用、以及突发传输和描述符如何把一次大传输拆解成可调度的任务。

一、DMA控制器架构:通道、引擎与寄存器

一个典型的 DMAC 由总线主接口、通道逻辑、传输引擎和寄存器组构成。总线主接口让 DMAC 能主动发起读写事务,这是它区别于普通外设的关键;通道逻辑负责仲裁多个外设请求;传输引擎按配置完成源地址递增、目的地址递增、传输宽度和突发长度控制;寄存器组则保存源地址、目的地址、传输长度、控制位和状态位。

在 Linux 中,DMAC 被抽象为 dma_device 与 dma_chan。驱动通过 dma_request_chan() 申请通道,用 dma_slave_config() 配置方向、地址和突发参数,再用 dma_prep_* 准备传输。下面是一个典型的从设备配置片段:

struct dma_slave_config cfg = {
    .direction = DMA_DEV_TO_MEM,
    .src_addr = (dma_addr_t)dev_phys_addr,
    .src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
    .src_maxburst = 16,
    .dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
    .dst_maxburst = 16,
};
dmaengine_slave_config(chan, &cfg);

二、传输方向与地址映射

DMA 传输方向通常分为三类:内存到外设(MEM_TO_DEV)、外设到内存(DEV_TO_MEM)和内存到内存(MEM_TO_MEM)。方向决定了源地址与目的地址谁是“设备侧”、谁是“内存侧”,也决定了地址是否需要固定。例如 UART 接收时,外设地址固定、内存地址递增;而内存拷贝时,两端地址都递增。

地址映射是 DMA 最容易出错的地方。CPU 看到的是虚拟地址,DMAC 看到的是总线物理地址。驱动必须用 dma_map_single() 或 dma_map_sg() 把内存缓冲区映射为 DMA 可访问地址,并在传输结束后用 dma_unmap_single() 解除映射。若使用流式映射,还要注意 cache 一致性:CPU 写内存后需 dma_sync_single_for_device(),DMAC 写回后需 dma_sync_single_for_cpu()。

常见误区:直接把 kmalloc() 返回的虚拟地址写入 DMA 寄存器。DMAC 无法识别虚拟地址,轻则传输失败,重则踩坏物理内存。必须使用 dma_alloc_coherent() 或流式映射得到的 DMA 地址。

三、总线带宽、突发传输与描述符

总线带宽决定 DMA 的理论上限。若总线位宽 64 bit、频率 200 MHz,则峰值带宽为 64/8 × 200M = 1.6 GB/s。但实际有效带宽受仲裁、握手、读写切换和 cache 一致性开销影响。突发传输(burst)通过一次总线事务搬运多个数据单元,减少地址阶段和仲裁开销,从而逼近峰值带宽。突发长度过小,事务开销占比高;过大则可能挤占其他主设备带宽,增加延迟。

描述符(descriptor)是 DMA 从“单次搬运”走向“可调度传输”的关键。一个描述符通常包含源地址、目的地址、长度、控制/状态字段和下一个描述符指针。多个描述符可组成链表或环形队列,DMAC 按链自动执行,完成后再触发中断。这样 CPU 只需准备描述符链,而不必逐次干预。

DMA控制器架构与描述符链数据流 CPU / 驱动 配置寄存器 准备描述符链 DMA控制器 (DMAC) 通道仲裁 传输引擎 寄存器组 源/目的地址 · 长度 · 突发长度 · 控制/状态 外设 (UART/PCIe) FIFO / 寄存器 系统内存 DMA缓冲区 配置 请求 读写 完成中断 描述符链(Descriptor Chain) Desc 0 src/dst/len Desc 1 src/dst/len Desc 2 src/dst/len Desc 3 src/dst/len 循环

四、案例场景:UART 接收 4 KB 数据的 DMA 路径

假设 UART 以 115200 波特率接收 4 KB 数据,驱动采用 DEV_TO_MEM 方向。流程如下:

  1. 驱动用 dma_alloc_coherent() 分配 4 KB DMA 缓冲区,得到虚拟地址和 DMA 地址。
  2. 调用 dmaengine_slave_config() 设置源地址为 UART FIFO 物理地址、目的地址为 DMA 缓冲区地址、突发长度为 16。
  3. 用 dma_prep_dma_cyclic() 或 dma_prep_slave_sg() 准备描述符,提交给通道。
  4. DMAC 收到 UART 请求后,按突发长度从 FIFO 搬运到内存,地址递增,长度递减。
  5. 传输完成触发中断,驱动调用 dma_sync_single_for_cpu() 后读取数据,再重新提交描述符。

这个案例中,描述符链让驱动可以一次性提交多个缓冲区,DMAC 自动切换,CPU 只在整链完成或半满时被唤醒,显著降低中断频率。

五、参数关系与常见误区复盘

参数/概念 作用 典型取值/注意点
传输方向 决定源/目的谁是设备侧 DEV_TO_MEM 时源地址固定
突发长度 减少总线事务开销 常取 4/8/16,需匹配 FIFO 深度
传输宽度 单次搬运字节数 1/2/4/8 字节,需与外设对齐
DMA 地址 DMAC 实际访问的物理地址 必须由 dma_map 或 dma_alloc_coherent 获得
描述符 描述一次搬运任务 含 src/dst/len/next,可链式或环形
cache 一致性 保证 CPU 与 DMAC 看到相同数据 流式映射需 sync,一致性映射免 sync
调试建议:若 DMA 传输卡死,先检查描述符的 next 指针是否成环、长度是否为 0、地址是否对齐;若数据错乱,优先排查 cache 一致性与突发长度是否超过外设 FIFO 深度。

总结来看,DMA 控制器通过总线主接口和传输引擎把数据搬运从 CPU 卸载;传输方向决定地址行为;地址映射保证 DMAC 能正确访问内存;总线带宽与突发长度共同决定吞吐;描述符则把大传输拆成可链式调度的任务单元。掌握这五者的关系,才能在高带宽场景下设计出稳定、低 CPU 占用的 DMA 驱动。