#【嵌入式每日一题】: DMA、双缓冲、Cache一致性、mmap零拷贝总结
## 1. 总览
DMA 解决的是**大数据搬运不想占用 CPU**的问题;双缓冲 DMA 解决的是**连续采集时采集和处理并行**的问题;Cache 一致性解决的是**CPU 和 DMA 看到的数据可能不一致**的问题;mmap/零拷贝解决的是**内核态和用户态之间少拷贝甚至不拷贝**的问题。面经里也把 DMA 的核心归纳为:DMA 独立于 CPU,通过总线仲裁在外设寄存器和内存之间搬运数据;双缓冲则是两个 buffer 交替使用,一边搬运一边处理。
---
## 2. DMA 是什么
DMA,全称 **Direct Memory Access,直接内存访问**。
它的作用是:
> 不让 CPU 一个字节一个字节搬数据,而是让 DMA 控制器直接在外设和内存之间搬运数据。
普通方式:
```text
外设数据 -> CPU读取 -> CPU写入内存
```
DMA方式:
```text
外设数据 -> DMA控制器 -> 内存
```
CPU 只负责配置 DMA,真正的数据搬运由 DMA 控制器完成。
---
## 3. DMA 的工作原理
DMA 工作前,CPU 需要配置:
```text
源地址:数据从哪里来
目标地址:数据搬到哪里去
传输长度:搬多少字节
数据宽度:8bit / 16bit / 32bit
传输方向:外设到内存、内存到外设、内存到内存
传输模式:单次、循环、双缓冲
中断方式:传输完成中断、半传输中断、错误中断
```
启动 DMA 后,DMA 控制器通过系统总线直接访问外设和内存。
搬运完成后,DMA 产生中断通知 CPU:
```text
CPU:配置DMA
DMA:搬运数据
DMA:搬完后中断通知CPU
CPU:处理数据
```
---
## 4. 图像采集为什么用 DMA
图像数据量非常大。
例如一帧 1920 × 1080 的 RGB888 图像:
```text
1920 × 1080 × 3 Byte ≈ 6 MB
```
如果是 30 FPS:
```text
6 MB × 30 ≈ 180 MB/s
```
如果让 CPU 自己搬运这些图像数据,会严重占用 CPU,影响后续的图像处理、双目匹配、深度计算、ROI 判断等任务。
所以图像采集一般是:
```text
摄像头 / MIPI CSI
↓
DMA
↓
DDR中的图像buffer
↓
CPU / OpenCV / 双目算法处理
```
这样 CPU 不需要逐像素搬运,只在一帧采集完成后处理图像。
---
## 5. 双缓冲 DMA 是什么
双缓冲 DMA,也叫 **Ping-Pong Buffer**。
准备两个 buffer:
```text
buffer A
buffer B
```
工作方式:
```text
DMA 写 buffer A 时,CPU 处理 buffer B
DMA 写 buffer B 时,CPU 处理 buffer A
```
流程图:
```text
时间1:
DMA -> buffer A
CPU -> buffer B
时间2:
DMA -> buffer B
CPU -> buffer A
时间3:
DMA -> buffer A
CPU -> buffer B
```
---
## 6. 双缓冲 DMA 的作用
双缓冲的核心作用是:
```text
采集和处理并行
```
如果只有一个 buffer,会出现:
```text
DMA 正在写新图像
CPU 正在读旧图像
```
这样 CPU 可能读到一半旧数据、一半新数据,图像会错乱。
双缓冲可以避免 DMA 和 CPU 同时访问同一个 buffer。
优点:
```text
1. 采集和处理并行
2. 减少 CPU 等待
3. 减少丢帧
4. 提高实时性
5. 适合图像、音频、ADC等连续数据采集
```
---
## 7. Cache 是什么
Cache 是 CPU 和 DDR 内存之间的一层高速缓存。
结构可以理解为:
```text
CPU <-> Cache <-> DDR
```
CPU 访问数据时,通常先访问 Cache;如果 Cache 中有,就直接读取;如果没有,才去 DDR 中读取。
Cache 的作用是提高 CPU 访问内存的速度。
但是 DMA 通常不经过 CPU Cache,而是直接访问 DDR:
```text
CPU <-> Cache <-> DDR
DMA ----------------> DDR
```
所以 CPU 和 DMA 可能看到的数据不一样。
---
## 8. DMA 和 Cache 一致性问题
DMA 和 Cache 一致性问题,本质是:
> CPU 看到的是 Cache 里的数据,DMA 看到的是 DDR 里的数据,两者可能不一致。
分两种情况。
---
## 9. 情况一:CPU 写数据,DMA 读数据
场景:
```text
CPU 写 buffer
DMA 读 buffer
```
问题:
```text
CPU 写的新数据可能还在 Cache 里
DDR 里还是旧数据
DMA 直接从 DDR 读
DMA 读到旧数据
```
解决方法:
```text
DMA 读之前,要 clean cache
```
clean cache 的作用:
```text
把 Cache 中的新数据写回 DDR
```
记忆:
```text
CPU -> DMA:clean cache
```
典型场景:
```text
CPU准备发送数据
DMA从内存读数据并发送给外设
```
---
## 10. 情况二:DMA 写数据,CPU 读数据
图像采集就是这个场景。
场景:
```text
摄像头 -> DMA -> DDR图像buffer
CPU读取图像buffer
```
问题:
```text
DMA 已经把新图像写入 DDR
但是 CPU Cache 里可能还有旧图像
CPU 再读时可能读到旧 Cache
```
解决方法:
```text
DMA 写完后,CPU 读之前,要 invalidate cache
```
invalidate cache 的作用:
```text
把 Cache 中的旧数据作废,让 CPU 重新从 DDR 读取新数据
```
记忆:
```text
DMA -> CPU:invalidate cache
```
---
## 11. clean 和 invalidate 对比
| 场景 | 问题 | 操作 |
| ----------- | ------------------ | ---------------- |
| CPU 写,DMA 读 | 新数据在 Cache,DDR 是旧的 | clean cache |
| DMA 写,CPU 读 | DDR 是新的,Cache 是旧的 | invalidate cache |
最重要的记忆口诀:
```text
CPU -> DMA:clean
DMA -> CPU:invalidate
```
---
## 12. Linux 下怎么处理 DMA 和 Cache 一致性
Linux 驱动中一般不用自己手动刷 Cache,而是使用 DMA API。
常见方式有两种。
### 方式一:申请一致性 DMA 内存
```c
dma_alloc_coherent()
```
作用:
```text
申请一块 CPU 和 DMA 都可以一致访问的内存
```
优点:
```text
使用简单,适合作为长期 DMA buffer
```
缺点:
```text
性能可能略低,大块连续内存分配有限制
```
---
### 方式二:DMA 映射
CPU 到 DMA:
```c
dma_map_single(dev, buf, size, DMA_TO_DEVICE);
```
DMA 到 CPU:
```c
dma_map_single(dev, buf, size, DMA_FROM_DEVICE);
```
传输结束后:
```c
dma_unmap_single()
```
这些 API 会根据方向自动做 Cache 同步。
---
## 13. 裸机 / RTOS 下怎么处理
在裸机或 RTOS 中,尤其是 Cortex-M7 这类带 D-Cache 的 MCU,可能要手动调用 Cache 维护函数。
CPU 写数据给 DMA 读:
```c
CPU写buffer;
SCB_CleanDCache_by_Addr(buffer, size);
启动DMA发送;
```
DMA 写数据给 CPU 读:
```c
启动DMA接收;
等待DMA完成;
SCB_InvalidateDCache_by_Addr(buffer, size);
CPU读取buffer;
```
注意:
```text
buffer 地址和长度最好按照 Cache Line 对齐
```
例如 32 字节或 64 字节对齐,否则可能误影响相邻数据。
---
## 14. volatile 能不能解决 DMA 和 Cache 一致性
不能。
volatile 解决的是:
```text
防止编译器优化变量访问
```
比如中断修改的变量、硬件寄存器、状态标志位,需要 volatile。
但是 volatile 不能解决:
```text
CPU Cache 和 DDR 数据不一致
```
所以:
```text
volatile ≠ Cache同步
volatile ≠ 原子操作
volatile ≠ DMA一致性
```
面试回答:
> volatile 只能防止编译器优化,不能解决 DMA 和 Cache 一致性。DMA 直接访问内存,而 CPU 访问数据可能命中 Cache,因此仍然需要 clean 或 invalidate cache,或者使用 Linux DMA API。
---
## 15. mmap 和 DMA 的关系
DMA 负责把设备数据搬到内核中的 DMA buffer。
mmap 负责把这块 buffer 映射到用户空间,让用户程序可以直接访问。
传统 read 方式:
```text
设备 -> DMA -> 内核buffer -> CPU拷贝 -> 用户buffer -> 用户程序处理
```
mmap 方式:
```text
设备 -> DMA -> DMA buffer <-mmap-> 用户空间直接访问
```
mmap 的作用是:
```text
避免内核buffer到用户buffer之间的CPU拷贝
```
---
## 16. mmap 和零拷贝的关系
零拷贝不是说完全没有数据移动,而是:
> 尽量减少 CPU 参与的数据拷贝,尤其是减少内核态和用户态之间的拷贝。
mmap 是实现零拷贝的一种常见方式。
它让用户空间和内核空间映射到同一块物理内存。
所以用户程序可以直接访问 DMA 写入的图像数据,而不需要 read 后再 copy_to_user。
---
## 17. V4L2 摄像头采集中的 DMA + mmap
Linux 摄像头采集常见流程:
```text
open("/dev/video0")
VIDIOC_REQBUFS // 申请多个buffer
VIDIOC_QUERYBUF // 查询buffer信息
mmap() // 映射到用户空间
VIDIOC_QBUF // buffer入队
VIDIOC_STREAMON // 开始采集
VIDIOC_DQBUF // 取出采集完成的一帧
用户程序处理图像
VIDIOC_QBUF // buffer重新入队
```
数据链路:
```text
摄像头 -> MIPI CSI -> DMA -> V4L2 buffer -> mmap -> 用户程序
```
这就是典型的:
```text
DMA + mmap + 多缓冲 = 图像零拷贝采集
```
---
## 18. 结合双目相机:
在电动变焦双目深度相机项目中,左右相机通过 MIPI 输出连续图像流,数据量很大,所以图像采集链路中需要 DMA 将相机数据直接搬运到 DDR 中的图像 buffer。
RK3588 的 CPU 不需要逐像素搬运图像,而是在帧完成后取 buffer 做双目校正、立体匹配、深度计算和 ROI 判断。
为了提高实时性,可以采用双缓冲或多缓冲机制。DMA 写当前帧 buffer 的时候,算法线程处理上一帧 buffer,从而实现采集和处理并行。
同时,由于 DMA 直接写 DDR,而 CPU 读取图像时可能命中 Cache,所以在 DMA 写完图像后,CPU 读取前需要保证对应 buffer 的 Cache 已经 invalidate,否则可能读到旧帧,导致图像异常或深度计算错误。
如果在 Linux 下使用 V4L2 的 mmap 方式,则驱动申请 DMA buffer,并将其映射到用户空间,用户程序可以直接访问图像数据,减少一次内核态到用户态的 CPU 拷贝。
---
## 19. 标准回答
DMA 是直接内存访问控制器,可以在不经过 CPU 拷贝的情况下,实现外设和内存之间的数据搬运。CPU 只需要配置源地址、目标地址、传输长度、数据宽度、传输方向和中断,启动后由 DMA 控制器通过总线完成数据搬运,传输完成后再通过中断通知 CPU。
图像采集中数据量很大,如果由 CPU 逐字节读取,会占用大量算力并影响实时性,所以一般使用 DMA 将 MIPI/CSI 接收到的图像数据直接写入 DDR 中的图像 buffer。
双缓冲 DMA 是准备两个 buffer,DMA 写 buffer A 时 CPU 处理 buffer B,DMA 写完后切换到 buffer B,CPU 再处理 buffer A。这样可以实现采集和处理并行,避免 CPU 和 DMA 同时访问同一块 buffer,减少丢帧。
DMA 和 Cache 一致性问题是因为 CPU 访问数据通常经过 Cache,而 DMA 直接访问 DDR。CPU 写数据给 DMA 读之前,需要 clean cache,把 Cache 中的新数据写回 DDR;DMA 写数据给 CPU 读之后,需要 invalidate cache,让 CPU 不再读旧 Cache。Linux 下通常通过 dma_alloc_coherent 或 dma_map_single / dma_unmap_single 处理,裸机下可以手动调用 CleanDCache 和 InvalidateDCache 接口。
mmap 可以把内核中的 DMA buffer 映射到用户空间,使用户程序直接访问 DMA 写入的数据,避免 read/write 中的内核态到用户态拷贝。结合摄像头采集来看,就是设备通过 DMA 把图像写入内核 buffer,然后用户程序通过 mmap 直接访问这块 buffer,这就是常见的图像零拷贝采集方式。
---
## 20. 总结:
```text
DMA 是搬运工:
外设和内存之间直接搬数据,减少 CPU 拷贝。
双缓冲是流水线:
DMA 写一块,CPU 处理另一块。
Cache 一致性是视角问题:
CPU 看 Cache,DMA 看 DDR。
CPU -> DMA:
clean cache。
DMA -> CPU:
invalidate cache。
mmap 是映射:
把 DMA buffer 映射到用户空间。
零拷贝是目标:
减少内核态和用户态之间的 CPU 拷贝。
```
发布于 日本
