4. Linux DMA API:dma_alloc_coherent、dma_map_single、dma_map_sg、API使用场景与注意事项
在嵌入式Linux系统中,DMA(Direct Memory Access)是提升外设吞吐量的核心技术。Linux内核提供了一套标准化的DMA API,用于管理设备与内存之间的数据传输。本章将深入剖析三个最核心的DMA API:dma_alloc_coherent、dma_map_single 和 dma_map_sg,并结合Raw格式输出与Buffer共享的实际场景,讲解其使用方式与避坑要点。
核心概念: DMA API 的核心目标是解决“Cache一致性”问题。CPU通过Cache加速内存访问,而DMA设备直接访问物理内存。如果Cache中的数据与物理内存不一致,就会导致数据错误。Linux DMA API通过一致性映射(Coherent)或流式映射(Streaming)来管理这种一致性。
4.1 dma_alloc_coherent:一致性DMA缓冲区
dma_alloc_coherent 用于分配一块在CPU和DMA设备之间保持缓存一致性的内存区域。该函数返回两个地址:一个给CPU使用的虚拟地址(vaddr),一个给DMA设备使用的物理地址(dma_handle)。
函数原型与参数
#include <linux/dma-mapping.h>
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
| 参数 | 说明 | 典型值 |
|---|---|---|
dev |
请求DMA的设备指针 | 从platform_device或pci_dev获取 |
size |
分配的字节数(必须为2的幂次对齐) | PAGE_SIZE 或其倍数 |
dma_handle |
输出参数,返回DMA总线地址 | 传递给DMA控制器寄存器 |
flag |
内存分配标志 | GFP_KERNEL(进程上下文)或 GFP_ATOMIC(中断上下文) |
使用场景
- Raw格式视频帧缓冲区: 摄像头传感器通过DMA直接将Raw数据写入此缓冲区,CPU可直接读取处理,无需手动刷新Cache。
- 音频DMA缓冲区: I2S控制器与内存之间的音频数据流,要求低延迟且无Cache一致性问题。
- 控制类数据交换: 如USB控制传输、SPI命令缓冲区,数据量小但要求实时一致。
注意事项:
- 分配的内存是连续的物理内存,大块分配(> 4MB)可能失败,建议使用
dma_alloc_noncoherent或 CMA(Contiguous Memory Allocator)。 - 该API内部会禁用该段内存的Cache(或使用特殊内存属性),因此CPU访问速度可能略慢于普通内存。
- 必须配对使用
dma_free_coherent释放,否则造成内存泄漏。
4.2 dma_map_single:流式DMA映射
dma_map_single 用于将一段已存在的内核缓冲区(如kmalloc分配的、栈上的或全局数组)映射给DMA设备使用。它不分配新内存,而是建立DMA地址与物理页的映射关系,并处理Cache同步。
函数原型与参数
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
| 参数 | 说明 | 典型值 |
|---|---|---|
dev |
设备指针 | 同 dma_alloc_coherent |
cpu_addr |
CPU虚拟地址(必须为内核逻辑地址或直接映射地址) | kmalloc 返回的地址 |
size |
映射的字节数 | 缓冲区实际数据长度 |
dir |
数据传输方向 | 见下方方向枚举 |
数据传输方向枚举
- DMA_TO_DEVICE: CPU写入数据,DMA读取(如发送网络包)。
- DMA_FROM_DEVICE: DMA写入数据,CPU读取(如接收网络包)。
- DMA_BIDIRECTIONAL: 双向传输,性能开销最大,尽量少用。
- DMA_NONE: 仅用于调试或初始化。
使用场景
- 网络驱动: 将skb的数据缓冲区映射给DMA引擎发送或接收。
- 块设备驱动: 将bio请求的页映射给存储控制器。
- Raw格式输出优化: 将用户态通过mmap映射的缓冲区,在内核态使用
dma_map_single映射给显示控制器或ISP。
最佳实践: 流式映射适用于“临时借用”内存给DMA使用。每次DMA传输完成后,必须调用
dma_unmap_single 解除映射,并调用 dma_sync_single_for_cpu 确保CPU看到最新数据。
4.3 dma_map_sg:分散/聚集DMA映射
dma_map_sg 用于将一组物理上不连续的缓冲区(Scatter-Gather列表)映射为DMA可访问的地址列表。它返回映射成功的段数,并填充每个sg_entry的DMA地址和长度。
函数原型与参数
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
| 参数 | 说明 | 典型值 |
|---|---|---|
dev |
设备指针 | 同前 |
sg |
scatterlist 数组首地址 | 由上层分配并填充 |
nents |
sg 数组中的条目数 | 物理段数量 |
dir |
数据传输方向 | 同 dma_map_single |
使用场景
- 存储驱动: NVMe、SD/MMC 驱动处理不连续的块请求。
- 视频处理: 将多个YUV平面(Y、U、V分别在不同物理页)映射给硬件编码器。
- Raw格式Buffer共享: 当Raw帧由多个物理不连续的chunk组成时,使用SG列表一次性映射,减少DMA设置开销。
API使用流程
- 分配并初始化
struct scatterlist数组。 - 调用
sg_set_page或sg_set_buf填充每个段。 - 调用
dma_map_sg获取DMA映射。 - 遍历映射后的sg列表,使用
sg_dma_address和sg_dma_len获取DMA地址和长度。 - DMA传输完成后,调用
dma_unmap_sg解除映射。
性能优势: 相比多次调用
dma_map_single,dma_map_sg 只需一次IOMMU页表更新或Cache同步操作,显著降低映射开销。在Raw格式高帧率输出场景中,这是关键优化点。
4.4 API对比与选择指南
| 特性 | dma_alloc_coherent | dma_map_single | dma_map_sg |
|---|---|---|---|
| 内存分配 | 分配新内存 | 映射已有内存 | 映射已有内存(多段) |
| Cache一致性 | 硬件保证(禁用Cache或硬件同步) | 软件管理(需sync操作) | 软件管理(需sync操作) |
| 物理连续性 | 连续 | 连续(单段) | 不连续(多段) |
| 适用数据量 | 中等(建议 < 4MB) | 小到中等 | 大(可超过4MB) |
| 典型延迟 | 分配时高,使用时低 | 映射时低,需sync | 映射时略高,传输效率高 |
| Raw格式场景 | 固定帧缓冲区 | 单帧临时映射 | 多平面/多chunk帧 |
4.5 常见错误与调试技巧
常见错误清单:
- 忘记解映射: 每次
dma_map_single或dma_map_sg后,必须在DMA传输完成且CPU访问前调用对应的dma_unmap_*,否则导致内存损坏。 - 方向错误: 使用
DMA_FROM_DEVICE但CPU在映射后写入数据,会导致数据被Cache刷出覆盖。 - 越界访问: DMA设备写入长度超过映射的size,破坏相邻内存。
- 在中断上下文使用 GFP_KERNEL:
dma_alloc_coherent在中断中必须使用GFP_ATOMIC。 - 忽略 dma_set_mask: 未设置设备的DMA掩码(如64位地址支持),导致分配失败或地址截断。
调试技巧
- 启用内核DMA调试:
CONFIG_DMA_API_DEBUG=y,可检测未配对映射、双重释放等错误。 - 使用
dma_debug_show_all查看当前所有活跃的DMA映射。 - 在QEMU或FPGA验证平台上,使用
trace_printk跟踪dma_addr和传输长度。 - 对于Raw格式输出,建议在驱动中增加
dma_sync_single_for_cpu后的数据校验(如CRC或模式检查)。
性能优化要点:
- 优先使用
dma_map_sg替代多次dma_map_single,减少IOMMU页表更新次数。 - 对于高频小数据包,考虑使用
dma_pool预分配一致性内存池。 - 在Raw格式多Buffer乒乓操作中,使用
dma_alloc_coherent预分配多个帧缓冲区,避免运行时分配。 - 利用
dma_get_required_mask查询设备实际支持的地址范围,避免不必要的bounce buffer。