4. DMA 基础概念:DMA 控制器工作原理、传输模式与 CPU 协同
在摄像头驱动中,图像传感器产生的数据量巨大(例如 1080p@30fps 的 YUV 数据流约 74 MB/s)。若完全依赖 CPU 逐字节搬运,不仅会耗尽处理器资源,还会导致帧率下降、系统响应延迟。DMA(Direct Memory Access,直接存储器访问)正是解决这一瓶颈的核心硬件机制。
4.1 DMA 控制器工作原理
DMA 控制器(DMAC)是一个独立的总线主控设备,能够在没有 CPU 干预的情况下,在外设与内存之间、或内存与内存之间直接传输数据。其核心工作流程如下:
- CPU 配置 DMA 通道:设置源地址、目的地址、传输宽度、传输长度、触发源等参数。
- 外设触发 DMA 请求:例如摄像头 ISP 输出一帧数据后,向 DMAC 发出 DMA 请求(DREQ)。
- DMAC 获取总线控制权:DMAC 向总线仲裁器申请总线使用权,获得授权后成为总线主控。
- DMAC 执行传输:从源地址读取数据,写入目的地址,每次传输后更新地址计数器并递减剩余长度。
- 传输完成中断:当剩余长度归零时,DMAC 向 CPU 发出传输完成中断(TC interrupt)。
关键指标: DMA 传输延迟通常为几十纳秒级别,远低于 CPU 中断驱动的 PIO 模式(微秒级)。在摄像头驱动中,DMA 可将 CPU 从数据搬运中解放出来,专注于图像处理算法和协议栈。
硬件依赖: 大多数 SoC 集成了多通道 DMA 控制器(如 PL330、DW_ahb_dmac),每个通道可独立配置。摄像头驱动通常独占一个 DMA 通道,避免与其他外设争用。
4.2 DMA 传输模式
根据应用场景的不同,DMA 控制器支持多种传输模式。摄像头驱动中最常用的三种模式如下:
| 传输模式 | 工作原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 单次传输 | 每次触发传输一个数据块(如一行像素),完成后停止,需 CPU 重新配置 | 单帧采集、低分辨率摄像头 | 实现简单,资源占用少 | 每帧需多次配置,CPU 干预频繁 |
| 循环传输 | 传输完成后自动重置地址和长度,持续循环,无需 CPU 重新配置 | 连续视频流、环形缓冲区 | 零 CPU 干预,适合高帧率 | 需固定缓冲区大小,溢出处理复杂 |
| 链表传输 | DMAC 自动从内存读取描述符链表,依次执行多个不连续的传输任务 | 多帧缓存、分散-聚集 I/O | 灵活管理非连续内存,支持复杂传输序列 | 描述符管理开销,硬件需支持链表模式 |
驱动开发注意: 在摄像头驱动中,循环传输模式常与环形缓冲区(ring buffer)配合使用。若 DMA 传输速度超过 CPU 处理速度,会导致缓冲区覆盖(overrun)。务必在驱动中实现帧完成中断处理与缓冲区切换的同步机制。
4.3 DMA 与 CPU 的协同
DMA 并非完全取代 CPU,而是与 CPU 分工协作。在摄像头驱动中,典型的协同流程如下:
- 初始化阶段(CPU 主导):CPU 分配 DMA 通道,配置源地址(摄像头 FIFO 寄存器地址)、目的地址(内存缓冲区地址)、传输宽度(32 位或 64 位)、突发长度(burst length,通常为 8 或 16 拍)。
- 传输阶段(DMA 主导):DMA 控制器独立完成数据搬运,CPU 可执行其他任务(如图像处理、网络协议栈)。
- 同步阶段(CPU 响应中断):DMA 传输完成后触发中断,CPU 在中断服务程序中处理帧数据(如格式转换、压缩、发送到应用层)。
- 错误处理(CPU 介入):当 DMA 传输超时、总线错误或缓冲区溢出时,CPU 负责重置 DMA 通道并恢复传输。
性能优化要点:
- Cache 一致性:DMA 直接访问物理内存,若 CPU 使用 Cache,需在 DMA 传输前后执行 Cache 刷新/无效化操作(如 ARM 的
dma_map_single/dma_unmap_single)。 - 中断合并:高帧率场景下,每帧中断可能过于频繁。可配置 DMA 在完成多个数据块后产生一次中断,降低 CPU 负载。
- 双缓冲机制:使用两个缓冲区交替接收 DMA 数据,一个缓冲区被 DMA 填充时,CPU 处理另一个缓冲区,避免竞争。
4.4 摄像头驱动中的 DMA 配置示例
以下是一个基于 Linux 内核的摄像头驱动中,使用 DMA 引擎 API 配置循环传输的典型代码片段:
/* 摄像头驱动 DMA 配置示例 - 循环传输模式 */
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
struct cam_dma_chan {
struct dma_chan *chan;
struct dma_slave_config config;
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;
};
static int cam_dma_setup(struct cam_dma_chan *dma, dma_addr_t src, dma_addr_t dst, size_t len)
{
struct dma_slave_config config = {
.direction = DMA_DEV_TO_MEM,
.src_addr = src,
.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.src_maxburst = 8,
.dst_addr = dst,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.dst_maxburst = 8,
};
/* 配置 DMA 通道 */
dmaengine_slave_config(dma->chan, &config);
/* 准备循环传输描述符 */
dma->tx = dmaengine_prep_dma_cyclic(dma->chan, dst, len, len / 2,
DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
if (!dma->tx)
return -ENOMEM;
/* 注册传输完成回调 */
dma->tx->callback = cam_dma_callback;
dma->tx->callback_param = dma;
/* 提交传输 */
dma->cookie = dmaengine_submit(dma->tx);
dma_async_issue_pending(dma->chan);
return 0;
}
static void cam_dma_callback(void *param)
{
struct cam_dma_chan *dma = param;
/* 处理帧数据:格式转换、推送至应用层 */
cam_process_frame(dma);
}
关键 API 说明:
dmaengine_prep_dma_cyclic():准备循环传输,参数buf_len为总缓冲区大小,period_len为每次中断的周期长度(通常为半缓冲区)。DMA_PREP_INTERRUPT标志:使能传输完成中断,驱动在回调中处理帧数据。dma_async_issue_pending():启动 DMA 传输,此后 DMAC 独立工作。
4.5 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| DMA 传输数据错位 | 源/目的地址未对齐(如 4 字节对齐要求) | 使用 dma_map_single 确保物理地址对齐 |
| 帧数据不完整 | 突发长度(burst)设置过大,超出 FIFO 深度 | 根据摄像头 FIFO 深度调整 src_maxburst |
| 中断丢失 | 中断处理耗时过长,错过下一帧完成中断 | 使用 tasklet 或 workqueue 延迟处理,中断中只做最小操作 |
| Cache 数据不一致 | CPU 读取 DMA 写入的内存时,Cache 中为旧数据 | 在 DMA 完成后调用 dma_sync_single_for_cpu |
硬件调试建议: 在开发初期,使用逻辑分析仪或 SoC 内置的 DMA 调试寄存器(如 PL330 的 Debug Status Register)观察 DMA 请求、传输计数和错误标志。切勿仅依赖软件日志,因为 DMA 错误可能发生在 CPU 完全无感知的情况下。