DMA传输与I2C驱动移植适配工程实践 - 第1节

1.1 DMA 工作原理

直接存储器访问(Direct Memory Access, DMA)是一种允许硬件设备直接与系统内存交换数据而无需 CPU 逐字节干预的机制。在嵌入式系统中,DMA 控制器作为总线上的独立主设备,接管数据搬运任务,使 CPU 能够并行处理计算与控制逻辑。

核心思想:CPU 只需配置 DMA 传输的源地址、目的地址、数据长度和触发条件,随后 DMA 控制器独立完成传输,仅在传输完成或出错时通过中断通知 CPU。

1.1.1 传统 PIO 模式 vs DMA 模式

对比维度 PIO(Programmed I/O) DMA(Direct Memory Access)
数据搬运主体 CPU 逐字/逐字节搬运 DMA 控制器独立搬运
CPU 占用率 高(传输期间被完全占用) 低(仅配置和收尾阶段介入)
传输速率 受限于 CPU 指令执行速度 接近总线带宽,可达数百 MB/s
适用场景 小数据量、低频外设 大数据块、高速外设(I2C/SPI/UART/ADC)
实时性影响 阻塞 CPU,影响中断响应 释放 CPU,保障系统实时性

1.1.2 DMA 传输基本流程

  1. 请求阶段:外设(如 I2C 控制器)产生 DMA 请求信号,通知 DMA 控制器需要数据传输。
  2. 仲裁阶段:DMA 控制器向总线仲裁器申请总线控制权,获得授权后成为总线主设备。
  3. 传输阶段:DMA 控制器直接在外设寄存器与内存之间搬运数据,每次传输后更新地址和计数器。
  4. 终止阶段:当传输计数器归零或遇到错误条件,DMA 控制器释放总线,并向 CPU 发送完成中断。
DMA 传输基本流程 CPU 配置 DMA 外设发起请求 总线仲裁 DMA 搬运数据 地址/计数更新 传输完成中断 关键寄存器 • 源地址寄存器 • 目的地址寄存器 • 传输计数寄存器 • 控制/状态寄存器 • 触发选择寄存器

1.2 DMA 传输模式

根据应用场景的不同,DMA 控制器支持多种传输模式。嵌入式 Linux 驱动开发中,最常用的三种模式为:单次传输(Single Transfer)、循环传输(Circular / Auto-Reload)和链表传输(Scatter-Gather / Linked List)。

1.2.1 单次传输模式

单次传输是最基本的 DMA 模式。配置完成后,DMA 控制器执行一次指定长度的数据传输,传输结束后自动停止,并产生完成中断。若需再次传输,必须由 CPU 重新配置。

  • 特点:一次性、确定性、低资源占用。
  • 适用场景:单次 ADC 采样、单帧图像捕获、一次性内存拷贝。
  • 驱动接口:dmaengine 中的 dma_async_issue_pending() 触发一次。

1.2.2 循环传输模式

循环传输模式下,DMA 控制器在完成一次传输后自动重新加载初始配置,立即开始下一次传输,形成连续的数据流。该模式通常用于需要持续数据采集或输出的场景。

  • 特点:自动重载、无中断开销、持续传输。
  • 适用场景:音频流播放/录制、连续 ADC 数据采集、PWM 波形生成。
  • 驱动接口:设置 DMA_PREP_CIRCULAR_LIST 标志位。
工程提示:循环传输模式下,驱动开发者必须实现“乒乓缓冲”(Ping-Pong Buffer)机制,即使用两个缓冲区交替接收数据,防止数据覆盖。一个缓冲区被 DMA 填充时,CPU 处理另一个缓冲区中的数据。

1.2.3 链表传输模式(Scatter-Gather DMA)

链表传输模式通过硬件链表结构描述多个不连续的内存块,DMA 控制器自动遍历链表,依次完成各块的传输。这是最灵活、最强大的 DMA 模式,也是 Linux DMA Engine 框架的核心能力之一。

特性 单次传输 循环传输 链表传输
传输次数 1 次 无限次(自动重载) N 次(链表节点数)
内存连续性 要求连续 要求连续 支持不连续(Scatter-Gather)
CPU 干预频率 每次传输后需重新配置 仅初始配置 仅初始配置链表
中断次数 1 次 0 次(或按需) 1 次(整个链表完成)
典型应用 单次 SPI 读写 音频环形缓冲区 网络数据包、文件系统 I/O
链表传输模式(Scatter-Gather DMA)结构 DMA 控制器 节点 1 src=0x1000, len=64 节点 2 src=0x2000, len=128 节点 3 src=0x3000, len=32 内存块 A(0x1000) 内存块 B(0x2000) 内存块 C(0x3000) 链表描述符 • next 指针 • 源/目的地址 • 传输长度

1.3 DMA 在嵌入式系统中的角色

在嵌入式 Linux 系统中,DMA 已从“可选加速器”演变为“核心基础设施”。以下从三个维度阐述 DMA 的关键作用:

1.3.1 性能维度:释放 CPU 算力

以 I2C 总线传输 4KB 数据为例:

  • 无 DMA(PIO 模式):CPU 每字节需执行 10~15 条指令,4KB 数据约需 40,000~60,000 条指令,期间 CPU 完全被占用。
  • 有 DMA:CPU 仅需约 50 条指令完成配置,随后可执行其他任务,传输由 DMA 控制器独立完成。
  • 实测数据:在 400kHz I2C 总线上,DMA 模式可将 CPU 占用率从 85% 降至 5% 以下。

1.3.2 实时性维度:降低中断延迟

DMA 通过以下机制改善系统实时性:

  1. 批量中断:DMA 完成整个数据块后才产生一次中断,而非每字节中断一次。
  2. 优先级管理:DMA 请求优先级可独立配置,高优先级外设(如音频)可抢占低优先级传输。
  3. 零拷贝:结合 MMU,DMA 可直接在用户空间缓冲区与设备之间传输,避免数据拷贝。

1.3.3 功耗维度:降低系统能耗

在电池供电的嵌入式设备中,DMA 对功耗的影响尤为显著:

场景 CPU 活跃时间 系统功耗 续航影响
PIO 模式传输 1MB 数据 ~50 ms ~250 mW 高
DMA 模式传输 1MB 数据 ~0.5 ms(配置+中断) ~80 mW 低
CPU 深度睡眠 + DMA 后台传输 0 ms(CPU 休眠) ~15 mW 极低
常见误区:DMA 并非“零开销”。DMA 控制器本身消耗总线带宽和少量功耗;在传输极小块数据(如 < 32 字节)时,DMA 配置开销可能超过 PIO 模式。工程中建议设置“DMA 阈值”,仅当数据量超过阈值时才启用 DMA。

1.3.4 软件架构中的 DMA 抽象层

在 Linux 内核中,DMA 通过分层架构为驱动开发者提供统一接口:

Linux DMA 软件架构分层 用户空间应用程序 / 文件系统 内核子系统(I2C/SPI/ALSA/V4L2 等) DMA Engine 核心层(dmaengine 框架) DMA 控制器驱动 DMA 引擎硬件 驱动开发关注点 • dma_slave_config • dmaengine_prep_slave_sg • dma_async_issue_pending

驱动开发者通常工作在“内核子系统”和“DMA Engine 核心层”之间,通过标准 API 完成 DMA 通道申请、配置、提交和回收,无需关心底层 DMA 控制器的具体寄存器操作。