做Linux驱动开发绕不开DMA。我最早是在裸机STM32上接触DMA的,那时候只需要把外设地址、内存地址、传输长度填进寄存器,然后等着外设自动搬数据。后来转到嵌入式Linux,第一次被cache一致性折腾到怀疑人生,才明白Linux下DMA的真正难点不在“怎么触发搬运”,而在“怎么保证CPU视角和设备视角看到的数据一致”。这篇是Linux DMA开发指南的第一篇,重点讲清楚地址映射、方向、一致性这些最基础也最容易出错的点,适合已经有字符设备驱动基础、但还没系统梳理过DMA的开发者。读完你会知道dma_map_single、dma_alloc_coherent到底该在什么场景用,也知道为什么MCU上那套“直接操作寄存器”的DMA思路在Linux上经常行不通。
1. Linux DMA不是“配寄存器”那么简单
1.1 DMA在系统里的位置:CPU、内存、外设三方协作
DMA全称是Direct Memory Access,直译为直接内存访问。它的意义在于,CPU只需要告诉DMA引擎“从哪里搬、搬到哪里、搬多少”,然后就可以去执行别的任务。很多外设的FIFO深度只有几十字节,比如串口接收寄存器,如果不给数据通路加DMA,CPU必须在每个字节到达时及时响应中断并读走,否则一个硬件中断风暴就会吞掉整个CPU性能。DMA的作用就是把这些零碎的搬运工作从CPU手里接过去。
现代SoC里DMA的应用场景覆盖以太网、USB、SD/MMC、显示控制器、音频、SPI、UART等等。有些SoC有独立的DMA控制器,外设之间通过请求信号和握手逻辑连接;有些外设本身内部就带DMA能力。Linux内核并不关心底层硬件怎么接线,它只提供了一套统一的API给你用,这套API叫做DMA Mapping API。无论你是驱动一个SD主机控制器,还是写一个基于dmaengine的SPI驱动,最终都要和这套API打交道。
我在初次接触Linux DMA时最大的困惑是:为什么不能像STM32那样,直接取一个物理地址填进外设寄存器?原因很简单,Linux系统有MMU,有cache,可能还有IOMMU/SMMU。CPU用的地址、物理内存的地址、外设能访问的地址,这三者不一定相等。更关键的是,外设的DMA操作会绕过CPU的cache。如果CPU在cache里缓存了一份数据,而DMA把新数据写到了物理内存,CPU读的时候可能读到的还是cache里的旧值,这就是经典的cache一致性问题。
1.2 从MCU DMA到Linux DMA,思维上要补哪几课
如果你是只做过STM32、GD32这类MCU的DMA,直接跳来做Linux DMA,一开始会非常不习惯。先看一张对比表,能帮你把脑子里那套裸机DMA模型和Linux DMA模型对齐。
| 对比项 | MCU裸机DMA | Linux内核DMA |
|---|---|---|
| 地址空间 | 地址就是物理地址,CPU和外设看到的是同一个地址 | 有虚拟地址、物理地址、总线地址多重映射 |
| 内存访问 | 通常无cache问题,或有少量cache需要手动清理 | cache一致性是硬伤,需要API保证 |
| 缓冲区管理 | 随时定义一个全局数组就能用 | 要分配、映射、同步、取消映射,有生命周期 |
| 寄存器访问 | 直接操作寄存器地址 | 使用ioremap后的虚拟地址,或寄存器的readl/writel |
| 地址宽度 | 基本固定32位 | dma_set_mask_and_coherent需要明确设置 |
| 隔离与重映射 | 没有,外设可访问全部内存 | IOMMU/SMMU可做地址重映射和设备隔离 |
| 错误排查 | 寄存器状态一目了然 | 有时需要dma-debug、IOMMU、ftrace联合排错 |
这张表不是让你死记,而是让你意识到:在Linux里写DMA驱动,本质是在管理地址映射和cache状态,而不是在“操纵DMA引擎”。“让DMA跑起来”是最简单的部分,“让DMA跑完以后数据是正确的”才是重中之重。
1.3 先记住四个概念,后面不会乱
在深入API之前,需要先建立几个概念。这里我用自己的话解释,尽量不引经据典。
第一个是CPU虚拟地址。你使用kmalloc或dma_alloc_coherent得到的地址,能直接被CPU的指针操作访问。这个地址是给CPU用的。第二个是物理地址,它代表实际RAM里的位置,但不是CPU运行时使用的地址,在启用MMU的系统上,物理地址不能直接被CPU指针访问。第三个是总线地址,也常叫设备地址或DMA地址,用dma_addr_t类型表示。这是外设发起DMA访问时使用的地址,经过总线地址转换,可能是物理地址,也可能被IOMMU重映射成另一套地址。第四个是dma_handle,它是一个dma_addr_t变量,在所有一致性映射API中都会出现,对应外设侧需要配置的地址。
这四个地址的关系可以用一句话概括:你往驱动代码里填的寄存器地址,不是CPU虚拟地址,也不是物理地址,而是dma_handle。很多新手第一次写Linux DMA驱动时,直接把虚拟地址强转成u32写进外设地址寄存器,然后在DMA完成后拿到一坨垃圾数据,就是因为在地址视角上搞错了。
另外还要记住DMA方向。Linux定义了DMA_TO_DEVICE、DMA_FROM_DEVICE和DMA_BIDIRECTIONAL。方向不仅是一个语义标签,它直接决定DMA映射API会做flush cache还是invalidate cache。方向搞反,轻则读到脏数据,重则把内存里的内容清空,调试起来非常痛苦。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. DMA映射API选型:一致性映射还是流式映射
2.1 长期共享数据用dma_alloc_coherent
Linux DMA Mapping API把映射分成两大类:一致性映射(coherent mapping)和流式映射(streaming mapping)。这两个概念如果不弄清楚,后面看代码会一头雾水。
一致性映射最适合“CPU和外设长期共享同一块内存”的场景。典型例子是以太网描述符、USB请求块、DMA控制器本身的描述符表。你的驱动需要反复往这块内存里写控制结构,外设也需要反复读这块内存里的控制信息,双方都要求每次更新立即可见。如果这块内存还经过cache,CPU写完描述符后,外设DMA去存储器读数据时可能读到旧数据,外设写完状态,CPU也可能读不到最新值。所以dma_alloc_coherent在底层分配的内存,通常是“非cache”的,或者经过特殊配置,让CPU访问时不会命中cache。这样CPU和DMA看到的都是同一份物理内存,避免手动同步。
使用方式很简单,代码示意如下:
c复制struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
struct desc *desc;
size_t size = 64 * sizeof(struct de
