一、问题现象:ADC 数据偶尔“跳变”

最近在基于 STM32H743 的项目中,使用 ADC + DMA 连续采集 8 通道数据,发现一个诡异现象:

  • 系统运行几分钟后,ADC 缓冲区中个别通道的值突然变成 0 或极大值;
  • 重启后正常,但运行一段时间又复现;
  • 关闭 D-Cache 后问题消失,但 CPU 性能明显下降。

这明显是 D-Cache 与 DMA 一致性 问题。STM32H7 的 Cortex-M7 内核带有 D-Cache,DMA 直接访问物理内存,而 CPU 访问的是 Cache。若两者不同步,就会读到旧数据或写入丢失。

二、原理:为什么 DMA 与 D-Cache 会“打架”

Cortex-M7 的 D-Cache 是**写回(Write-Back)、写分配(Write-Allocate)**策略:

  • CPU 写数据时,先写 Cache,不立即写回内存;
  • CPU 读数据时,若 Cache 未命中,从内存加载到 Cache。

DMA 则绕过 Cache 直接读写内存。因此:

  • CPU 写 → DMA 读:CPU 数据可能还在 Cache 中未写回,DMA 读到旧数据;
  • DMA 写 → CPU 读:DMA 已更新内存,但 CPU 可能读到 Cache 中的旧数据。

解决方法是使用 CMSIS 提供的 Cache 维护函数:

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 内容写回内存(Clean);
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重新加载(Invalidate);
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先 Clean 再 Invalidate。

三、排查过程:SCB_CleanDCache_by_Addr 的误用

3.1 错误代码

原代码在启动 DMA 前调用了:

SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);

看似正确,但 adc_buf 定义如下:

uint16_t adc_buf[BUF_SIZE];  // BUF_SIZE = 256

问题出在 地址对齐 和 长度单位。

3.2 三个致命错误

  1. 地址未 32 字节对齐:Cortex-M7 的 Cache 行大小为 32 字节。SCB_CleanDCache_by_Addr 要求地址 32 字节对齐,否则可能操作到相邻数据,甚至触发 HardFault。adc_buf 是 uint16_t 数组,默认对齐 2 字节,不满足要求。
  2. 长度参数错误:函数第二个参数是 字节数,但内部按 32 字节行处理。若长度不是 32 的倍数,会多清理一行,可能破坏其他变量。
  3. 方向混淆:DMA 是“读”内存(外设到内存),CPU 在 DMA 完成后要读数据,应该用 Invalidate,而不是 Clean。用 Clean 反而把 Cache 中的旧数据写回,覆盖了 DMA 的新数据!

四、正确配置步骤

4.1 内存对齐

将缓冲区定义为 32 字节对齐:

__attribute__((aligned(32))) uint16_t adc_buf[BUF_SIZE];

或使用 ALIGN_32BYTES 宏(CMSIS 提供)。

4.2 根据数据流向选择维护函数

  • CPU 写 → DMA 读(如发送):先 SCB_CleanDCache_by_Addr,再启动 DMA。
  • DMA 写 → CPU 读(如接收):DMA 完成后,调用 SCB_InvalidateDCache_by_Addr,再读数据。
  • 双向:使用 SCB_CleanInvalidateDCache_by_Addr。

4.3 完整示例(ADC + DMA 接收)

#include "stm32h7xx.h"

#define BUF_SIZE 256
__attribute__((aligned(32))) uint16_t adc_buf[BUF_SIZE];
volatile uint8_t dma_done = 0;

void ADC_DMA_Init(void) {
    // ... ADC 和 DMA 初始化代码省略
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);
}

// DMA 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    // 无效化 Cache,确保 CPU 读取到 DMA 写入的最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
    dma_done = 1;
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    SCB_EnableDCache();  // 使能 D-Cache
    ADC_DMA_Init();

    while (1) {
        if (dma_done) {
            dma_done = 0;
            // 安全读取 adc_buf
            process_adc_data(adc_buf, BUF_SIZE);
            // 重新启动 DMA 前,若 CPU 修改了缓冲区,需 Clean
            // SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
            HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);
        }
    }
}

五、注意事项与避坑指南

  • 地址必须 32 字节对齐,长度建议为 32 的倍数,否则用 SCB_CleanDCache_by_Addr 等函数可能越界。
  • 区分 Clean 与 Invalidate:Clean 是“写回”,Invalidate 是“丢弃”。方向搞反会导致数据错乱。
  • DMA 缓冲区不要与栈或全局变量混用,避免 Cache 行共享导致误伤。
  • 使用 MPU 配置 Write-Through 或 Non-Cacheable 内存:对于频繁 DMA 的缓冲区,可将其配置为 Non-Cacheable,一劳永逸,但会牺牲 CPU 访问速度。
  • 调试时关闭 D-Cache 可快速定位,但正式产品必须开启并正确维护。
  • CMSIS 函数内部已处理 32 字节对齐,但传入未对齐地址仍可能出错,务必自行保证。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典“坑”。SCB_CleanDCache_by_Addr 用错只是表象,根源在于对 Cache 维护机制理解不深。牢记三点:对齐、方向、时机。正确使用 Cache 维护函数,既能享受 H7 的高性能,又能保证数据可靠。