26-06-15 22:31 微博认证:超话粉丝大咖(赵梦超话)

#【嵌入式每日一题】: 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 拷贝。
```

发布于 日本