4、DMA与外设接口:SPI、I2C、UART、ADC等外设如何与DMA配合实现高效数据采集

TinyML 设备的数据采集链路中,CPU 往往是最宝贵的资源——它既要运行推理模型,又要处理中断、维护协议状态机、搬运数据。传统的中断驱动采集方式在高速外设(如 1Msps 的 ADC、8MHz 的 SPI 传感器)面前会迅速耗尽 CPU 预算,导致推理帧率下降或数据丢失。DMA(Direct Memory Access)的核心价值在于:它允许外设与内存之间直接交换数据,仅在传输完成或错误时通知 CPU。本章将深入剖析 SPI、I2C、UART、ADC 四类典型外设与 DMA 的配合机制,并给出工程化的配置判断方法与实战案例。

DMA 与外设接口协同采集架构 外设接口组 SPI (MISO/MOSI) I2C (SDA/SCL) UART (TX/RX) ADC (12-bit) DMA 控制器 通道映射 (CH0-CH7) 传输模式: 单次/循环 数据宽度: 8/16/32-bit 内存 (SRAM) 双缓冲 A/B 环形缓冲区 推理输入张量 CPU (Cortex-M) 仅处理完成中断/错误 完成中断 关键设计点: 1. 外设请求信号直接触发 DMA 传输,无需 CPU 介入 2. 双缓冲模式下,CPU 处理 A 缓冲时 DMA 填充 B 缓冲,实现零拷贝交接 3. 循环模式适合 ADC 连续采样,半传输中断用于帧边界同步

4.1 核心原理:DMA 如何消除 CPU 搬运瓶颈

DMA 的本质是外设与内存之间的“直通车道”。以 STM32 系列为例,DMA 控制器包含多个通道,每个通道可绑定特定外设的请求信号。当外设的数据寄存器就绪(如 SPI RX 非空、ADC 转换完成),硬件自动将数据搬运至内存地址,整个过程不需要 CPU 执行加载/存储指令。传输完成后,DMA 置位传输完成标志,并可触发中断通知 CPU。

理解 DMA 的关键在于三个维度:传输粒度(字节/半字/字)、传输模式(单次/循环/突发)和缓冲策略(单缓冲/双缓冲/环形)。在 TinyML 场景中,双缓冲是最常用的策略:DMA 填充缓冲区 A 时,CPU 处理缓冲区 B 的数据(如特征提取或推理),当 DMA 完成 A 的填充后自动切换至 B,同时触发中断让 CPU 处理 A。这种“乒乓”机制将采集与计算完全流水线化。

工程要点:DMA 并非万能。对于小于 8 字节的短传输,DMA 的初始化开销可能超过中断方式。经验法则是:单次传输超过 16 字节或传输频率超过 10kHz 时,DMA 才具有明显优势。在 TinyML 中,IMU 数据(6轴×2字节×50Hz)和麦克风 PDM 数据(16kHz×2字节)都适合 DMA。

4.2 各外设的 DMA 配合机制与判断方法

4.2.1 SPI + DMA:高速传感器数据流

SPI 是 TinyML 中最常见的高速接口,典型设备包括 9 轴 IMU(如 BMI270)、气压计(BMP390)和外部 Flash。SPI 的 DMA 配合有两种模式:接收专用(只读传感器数据)和全双工(写寄存器+读数据)。

配置判断流程如下:

  1. 确认外设支持突发传输:查看数据手册中的最大 SPI 时钟频率(通常 10MHz-80MHz),计算每帧数据量(如 IMU 的 6 轴×2 字节=12 字节)。
  2. 选择 DMA 通道:参考 MCU 的 DMA 请求映射表,确保 SPIx_RX 和 SPIx_TX 绑定到不同通道(避免冲突)。
  3. 设置数据宽度:SPI 数据寄存器通常为 8 位或 16 位,DMA 宽度必须与外设寄存器一致,否则会错位。
  4. 启用 SPI 的 DMA 请求:在 SPI 控制寄存器中设置 RXDMAEN 和 TXDMAEN 位。
// STM32 HAL 示例:SPI2 接收 12 字节 IMU 数据到双缓冲
__ALIGN_BEGIN static uint8_t dma_buf[2][12] __ALIGN_END;
HAL_SPI_Receive_DMA(&hspi2, dma_buf[0], 12);
// 在 DMA 传输完成中断中切换缓冲
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi == &hspi2) {
        active_buf ^= 1;
        HAL_SPI_Receive_DMA(&hspi2, dma_buf[active_buf], 12);
        // 此时 dma_buf[active_buf^1] 包含完整帧,可交给推理任务
    }
}

4.2.2 I2C + DMA:低速但需注意总线状态

I2C 的 DMA 配合比 SPI 复杂,因为 I2C 协议包含 ACK/NACK、重复起始条件等状态机。多数 I2C 传感器(如温湿度 SHT40、光照 OPT3001)的采样率较低(1-100Hz),数据量小,DMA 收益有限。但在需要连续读取大块数据(如 8KB EEPROM)或需要保持总线时钟稳定时,DMA 仍有价值。

关键判断点:I2C 的 DMA 传输必须与硬件状态机严格同步。在 STM32 上,需要监听 I2C 的 TXIS/RXNE 事件,并通过 DMA 请求信号触发。常见误区是直接使用 HAL_I2C_Master_Receive_DMA() 而不检查总线错误标志,导致 DMA 卡死。

风险提示:I2C 的 DMA 传输中,如果从设备 NACK 或总线仲裁丢失,DMA 可能持续等待请求信号。务必启用 I2C 错误中断(AF、BERR、OVR),并在错误处理中复位 DMA 通道。对于 TinyML 的低速传感器,建议优先使用中断方式,仅在读取超过 32 字节时启用 DMA。

4.2.3 UART + DMA:日志与外部处理器通信

UART 的 DMA 主要用于两类场景:高波特率日志输出(如 921600bps 打印调试信息)和与外部协处理器通信(如树莓派发送特征向量)。UART 的 DMA 接收需要特别处理不定长数据——通常使用空闲中断(IDLE)来检测帧结束。

推荐配置:

  • DMA 接收使用循环模式,配合空闲中断:当总线空闲时,CPU 读取 DMA 当前计数,计算接收长度。
  • DMA 发送使用普通模式,发送完成后自动关闭,避免循环发送相同数据。
  • 数据宽度固定为 8 位,因为 UART 数据寄存器是字节宽。
// 循环 DMA 接收 + 空闲中断实现不定长帧
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
// 在 UART 空闲中断中:
void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) {
    uint16_t len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
    process_frame(rx_buf, len);
    // 重新启动接收(循环模式自动继续)
}

4.2.4 ADC + DMA:连续采样与波形捕获

ADC 是 TinyML 中音频、振动、电流检测的核心接口。ADC 的 DMA 配合几乎是强制性的——如果使用中断方式读取 16kHz 采样率的音频,CPU 将每 62.5 微秒被中断一次,无法运行任何推理。ADC 的 DMA 配置要点:

  1. 扫描模式与 DMA 循环:多通道扫描时,DMA 自动将各通道结果按顺序存入缓冲区。
  2. 过采样与数据对齐:12 位 ADC 右对齐时,DMA 传输半字(16 位),注意缓冲区类型必须为 uint16_t。
  3. 双缓冲与半传输中断:对于音频流,使用半传输中断(HT)和传输完成中断(TC)将缓冲区分为两半,CPU 处理一半时 DMA 填充另一半。
外设典型速率DMA 推荐模式缓冲策略关键注意点
SPI10-80MHz普通/循环双缓冲数据宽度匹配,CS 控制
I2C100-400kHz普通(仅大块读)单缓冲错误中断必须启用
UART115200-921600循环接收+空闲中断环形帧边界检测
ADC1k-1Msps循环双缓冲/环形半传输中断用于帧同步

4.3 案例拆解:多传感器 TinyML 数据采集系统

以一个真实的可穿戴活动识别设备为例,该设备需要同时采集 6 轴 IMU(SPI,100Hz)、麦克风(ADC,16kHz)和环境温度(I2C,1Hz),并在 Cortex-M4 上运行轻量级 CNN 进行跌倒检测。

初始目标与约束:CPU 主频 80MHz,推理需要 30ms 时间预算,数据采集不能超过 10% CPU 占用。初始方案使用中断采集,实测 CPU 占用高达 42%,导致推理帧率从 20FPS 降至 8FPS。

关键参数分析:

  • IMU:SPI 时钟 10MHz,每帧 12 字节,100Hz → 每秒 1200 字节,中断频率 100Hz。
  • 麦克风:ADC 16kHz,每次采样 2 字节 → 每秒 32000 字节,中断频率 16kHz(这是主要瓶颈)。
  • 温度:I2C 1Hz,每次 2 字节,可忽略。

DMA 改造方案:

  1. IMU 使用 SPI1-RX DMA 双缓冲,每 10ms 切换一次缓冲,CPU 仅处理 100Hz 的完成中断。
  2. ADC 使用 DMA 循环模式,缓冲区 512 字节(256 个采样点),半传输中断每 8ms 触发一次,CPU 处理 256 个样本的预处理。
  3. 温度传感器保持中断方式(1Hz 无压力)。

实施结果:CPU 采集占用降至 6.5%,推理帧率恢复至 20FPS,且音频数据无丢失。DMA 配置耗时约 2 小时,主要时间花在调试 SPI 的 DMA 数据宽度匹配上——最初设置为字节传输,导致 12 位 ADC 数据错位。

复盘经验:DMA 调试时,先用逻辑分析仪或打印 DMA 计数器值验证传输长度,再接入实际传感器。对于 ADC 数据,务必确认 DMA 传输宽度与 ADC 分辨率匹配(12 位 ADC 使用半字传输)。另外,双缓冲切换时要注意缓存一致性——Cortex-M4 无需手动清缓存,但若使用 DMA 描述符,需确保描述符在 CCM RAM 中。

4.4 常见误区与总结

误区一:所有外设都适合 DMA。对于 I2C 的低速传感器(<10Hz),DMA 初始化开销反而增加延迟。正确做法是评估中断频率和单次数据量,只有当 CPU 中断负载超过 20% 或数据量超过 16 字节/次时才考虑 DMA。

误区二:忽略 DMA 通道优先级。当多个外设同时请求 DMA 时,优先级低的通道可能被饿死。在音频+IMU 场景中,应将 ADC 设为高优先级(音频数据连续性强),SPI 设为中优先级。

误区三:缓冲区大小设置不当。DMA 缓冲区过小会导致频繁中断,过大则浪费 SRAM。对于 TinyML,建议缓冲区大小满足:缓冲区时长 = 推理周期 / 2。例如推理周期 50ms,则缓冲区应容纳 25ms 的数据。

误区四:忘记处理 DMA 错误。DMA 传输错误(如总线错误、对齐错误)会静默停止。务必在 DMA 错误中断中设置标志位,并在主循环中检查。

总结而言,DMA 与外设的配合是 TinyML 系统设计的分水岭。掌握 SPI 的双缓冲、UART 的循环接收、ADC 的半传输中断,以及 I2C 的谨慎使用,能够将 CPU 从数据搬运中解放出来,专注于推理计算。在实际项目中,建议先绘制数据流图,标注每个外设的速率、数据量和中断频率,再决定 DMA 通道分配和缓冲策略。这种系统化的方法能显著减少调试时间,并确保采集链路在极端负载下依然稳定。