2. DMA基础原理
DMA(Direct Memory Access,直接存储器访问)是一种让外设与内存之间、或内存与内存之间在不经过 CPU 逐字节搬运的前提下完成大批量数据转移的硬件机制。它的核心价值在于把 CPU 从“搬数据”的重复劳动中解放出来,让 CPU 专注于协议解析、业务处理与调度,而由 DMA 控制器(DMAC)承担数据通路的搬运工作。理解 DMA 的定义、工作模式、总线主控、地址映射、描述符与通道机制,是后续分析吞吐瓶颈、定位传输异常与做性能优化的基础。
一、DMA 的定义与基本数据通路
从系统结构看,DMA 传输涉及三个角色:源端(外设 FIFO 或内存缓冲区)、目的端(内存缓冲区或外设 FIFO)、以及DMA 控制器。CPU 只负责“配置”这次传输:告诉 DMAC 源地址、目的地址、传输长度、位宽、突发长度、传输方向以及完成后的中断方式。配置完成后,DMAC 向总线发起读写请求,数据在源与目的之间直接流动,CPU 在此期间可以继续执行其他任务。
一次典型的 DMA 传输生命周期可以概括为:CPU 写寄存器启动 → DMAC 申请总线 → 按描述符搬运数据 → 计数递减到 0 → 产生完成中断 → CPU 在中断中回收缓冲区。这个生命周期决定了后续所有性能分析的基本观察点:启动开销、总线占用、搬运效率与中断处理成本。
二、工作模式:从单次到循环与分散聚集
不同场景对 DMA 的连续性、实时性和缓冲区管理要求不同,因此 DMAC 通常支持多种工作模式:
- 单次传输(Single/Block):一次配置搬运固定长度,完成后停止。适合一次性大块数据,如固件加载。
- 循环传输(Circular/Cyclic):缓冲区首尾相接,搬完自动回到起点继续。适合音频、ADC 采样等连续流场景。
- 分散聚集(Scatter-Gather):用一组描述符把多个不连续的内存块串成一次“逻辑连续”的传输,避免大块连续内存分配压力。
- 双缓冲/乒乓(Double Buffer):两块缓冲区交替使用,一块在搬运时另一块可被 CPU 处理,降低等待与丢包风险。
三、总线主控与地址映射
DMA 能否高效工作,取决于 DMAC 是否具备总线主控(Bus Master)能力。作为总线主控,DMAC 可以主动发起总线读写事务,与 CPU 竞争总线带宽。在多主总线系统中,仲裁器决定谁获得总线使用权,因此 DMA 吞吐不仅受 DMAC 自身限制,也受总线仲裁策略、优先级和并发访问影响。
地址映射是另一个高频出错点。CPU 视角的虚拟地址、物理地址,与外设视角的总线地址往往并不相同:
- CPU 虚拟地址:驱动中 kmalloc/vmalloc 返回的地址,需经 MMU 映射。
- 物理地址:virt_to_phys 或 page_to_phys 得到的地址,是内存真实位置。
- 总线/DMA 地址:外设通过总线看到的地址,可能经过 IOMMU 或桥片转换。
因此驱动必须使用 dma_map_single()、dma_map_sg() 等 DMA API 建立映射,而不是直接把虚拟地址写入 DMA 寄存器。映射同时承担 cache 一致性维护职责:dma_sync_single_for_device() 在设备读取前把 cache 写回内存,dma_sync_single_for_cpu() 在 CPU 读取前使 cache 失效,避免读到旧数据。
/* 典型映射与同步流程 */
dma_addr_t dma_handle;
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
/* 流式映射:单向传输 */
dma_addr_t addr = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, addr)) { /* 处理失败 */ }
/* 设备读取前,确保数据已写入内存 */
dma_sync_single_for_device(dev, addr, size, DMA_TO_DEVICE);
/* 传输完成后解除映射 */
dma_unmap_single(dev, addr, size, DMA_TO_DEVICE);
四、描述符与通道机制
描述符(Descriptor)是 DMA 传输的“任务单”,通常包含源地址、目的地址、传输长度、控制位(中断使能、方向、突发类型)以及指向下一个描述符的指针。硬件按描述符链依次执行,形成链表式或环形队列式传输。描述符机制让驱动可以预先排好一批传输任务,DMAC 自动连续执行,显著降低 CPU 干预频率。
通道(Channel)是 DMAC 内部的独立传输引擎。一个 DMAC 往往有多个通道,每个通道可绑定不同外设请求线(如 UART TX、SPI RX、I2S TX)。通道之间可并行工作,但共享总线带宽,因此多通道并发时要注意带宽争用与优先级配置。通道与请求线的绑定关系、通道优先级、以及是否支持硬件握手(Hardware Handshake),直接决定外设能否以稳定速率触发 DMA 搬运。
五、案例场景:UART 高速接收的 DMA 配置
假设需要以 3 Mbps 波特率连续接收 UART 数据,若采用中断逐字节读取,CPU 每秒要处理约 30 万次中断,几乎无法承受。改用 DMA 循环模式后,配置流程如下:
# 伪代码:UART RX 循环 DMA 配置
1. 分配环形缓冲区 rx_buf[64KB],dma_alloc_coherent 获取总线地址
2. 配置 DMAC 通道 CH2:
src = UART_RDR 寄存器地址(固定)
dst = rx_buf 总线地址(递增)
len = 64KB,循环模式,半满/全满中断使能
3. 使能 UART 的 DMA 接收请求线
4. 启动通道,DMAC 等待 UART FIFO 触发阈值后自动搬运
5. 半满中断:处理前半缓冲区;全满中断:处理后半缓冲区
该场景中,描述符链把 64KB 拆成多个小块,循环模式让缓冲区自动回卷,半满/全满中断把 CPU 唤醒频率从“每字节”降到“每 32KB”,CPU 占用率大幅下降。若发现丢包,应优先检查:缓冲区是否在回卷前处理完、总线仲裁是否被高优先级主控长期占用、cache 同步是否遗漏。
六、常见误区与复盘清单
| 误区 | 现象 | 正确做法 |
|---|---|---|
| 直接把虚拟地址写入 DMA 寄存器 | 传输到错误内存或触发 IOMMU 异常 | 使用 dma_map_single / dma_alloc_coherent 获取总线地址 |
| 忽略 cache 一致性 | CPU 读到旧数据,或设备读到未写回数据 | 传输前后调用 dma_sync_single_for_device / for_cpu |
| 循环模式缓冲区过小 | CPU 未处理完就被新数据覆盖,出现丢包 | 按最坏处理延迟估算缓冲区大小,配合半满中断 |
| 多通道无优先级规划 | 高带宽通道挤占低延迟通道,实时性变差 | 按业务优先级配置通道仲裁与突发长度 |
| 描述符链未对齐或越界 | DMAC 取描述符失败,传输中途停止 | 描述符按硬件要求对齐,链尾置结束标志 |
掌握以上基础后,后续分析 DMA 吞吐时就能把问题定位到具体环节:是启动开销过大、总线带宽争用、描述符链断裂,还是 cache 同步引入额外延迟。DMA 的性能优化,本质上是对这些环节的逐一量化与取舍。