故障现象:偶发的数据错乱

某工业网关项目使用STM32H743,通过DMA2将ADC采样数据搬运到外部SDRAM缓冲区,CPU再读取处理。测试中发现:约每几百次采样会出现一次数据错乱,表现为个别通道值突变或旧数据残留。重启后故障消失,运行一段时间又复现。

初步排查排除了ADC配置、DMA通道冲突、电源干扰,最终定位到D-Cache与DMA一致性处理时,Clean/Invalidate的粒度选择错误

原理:为什么Cache和DMA会打架

STM32H7的Cortex-M7内核带D-Cache(数据缓存),CPU访问内存时先查Cache,命中则直接读写Cache,不访问实际内存。而DMA是总线主设备,直接访问物理内存,完全绕过Cache。

当DMA向某块内存写入新数据时,如果该内存区域在D-Cache中有旧副本,CPU读到的仍是旧数据;反之,CPU修改了Cache中的数据但未写回内存,DMA搬运的则是旧数据。这就是Cache一致性问题。

解决手段有两个:

  • Clean(清理):将Cache中已修改的数据写回内存,使DMA能读到最新值。
  • Invalidate(无效化):将Cache中的副本标记为无效,强制CPU下次读时从内存重新加载,从而读到DMA写入的新值。

关键坑点:粒度选错

Cortex-M7的D-Cache按Cache Line操作,STM32H7的Cache Line大小为32字节。Clean和Invalidate的最小单位就是一行(32字节)。

很多开发者习惯用SCB_CleanDCache_by_Addr(addr, size)SCB_InvalidateDCache_by_Addr(addr, size),认为传入任意地址和长度就能精确操作。但底层实现是按行对齐处理的

  • 如果addr未32字节对齐,函数会从该地址所在行的起始处开始操作,可能误伤相邻数据
  • 如果size不是32的倍数,函数会操作到覆盖末尾的整行,同样可能影响缓冲区之外的数据

在故障代码中,DMA目标缓冲区定义如下:

uint8_t dma_buf[256];  // 未强制对齐

处理时调用:

SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf));

由于dma_buf起始地址可能不是32字节对齐(链接器随机分配),Invalidate操作会从该地址所在行的开头开始,把该行中位于dma_buf之前的数据也无效化了。如果那些数据恰好是其他变量的缓存副本,就会导致其他变量读取时从内存加载到旧值,引发偶发错乱。

正确配置步骤

1. 使能D-Cache

SCB_EnableICache();
SCB_EnableDCache();

2. 将DMA缓冲区对齐到Cache Line

使用__attribute__((aligned(32)))强制32字节对齐:

__attribute__((aligned(32))) uint8_t dma_buf[256];

3. 根据数据流向选择操作

  • DMA写内存,CPU读(如ADC采样):DMA传输完成后,CPU读之前,执行Invalidate
  • CPU写内存,DMA读(如发送数据):CPU写完后,启动DMA前,执行Clean
  • 双向:先Clean再Invalidate,或使用SCB_CleanInvalidateDCache_by_Addr

4. 使用正确的API并确保长度对齐

// DMA接收完成后
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf));

注意sizeof(dma_buf)必须是32的倍数,且dma_buf地址32字节对齐。

完整代码示例

#include "stm32h7xx.h"

#define BUF_SIZE  256

// 强制32字节对齐
__attribute__((aligned(32))) uint8_t dma_buf[BUF_SIZE];

void DMA_Init(void) {
    // 使能DMA时钟、配置DMA2 Stream0等(略)
    // 设置外设地址、内存地址为dma_buf,传输长度BUF_SIZE
}

void Process_Data(void) {
    // 等待DMA传输完成标志
    while (!(DMA2->LISR & DMA_LISR_TCIF0)) {}
    DMA2->LIFCR = DMA_LIFCR_CTCIF0;

    // 无效化Cache,确保CPU读到DMA写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);

    // 安全读取数据
    for (int i = 0; i < BUF_SIZE; i++) {
        // 处理dma_buf[i]
    }
}

void Send_Data(void) {
    // 准备要发送的数据
    for (int i = 0; i < BUF_SIZE; i++) {
        dma_buf[i] = i;
    }

    // 清理Cache,确保DMA读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);

    // 启动DMA发送
    DMA2_Stream0->CR |= DMA_SxCR_EN;
}

注意事项

  • 对齐是前提:DMA缓冲区必须32字节对齐,长度建议为32的倍数。
  • 避免频繁全Cache操作SCB_CleanInvalidateDCache()会操作整个Cache,影响性能,尽量用地址范围版本。
  • MPU配置:可将DMA缓冲区配置为Write-Through或Non-Cacheable,从根本上避免一致性问题,但会牺牲性能。
  • 中断中操作:在DMA完成中断中执行Clean/Invalidate是常见做法,但注意中断上下文时间。
  • 多缓冲区:若使用多个DMA缓冲区,每个都要对齐并单独处理。
  • 调试技巧:可在操作前后读取SCB->CCR确认Cache状态,或使用__DSB()确保操作完成。

总结

STM32H7的D-Cache与DMA一致性处理,粒度对齐是核心。Clean/Invalidate按32字节行操作,地址和长度必须对齐,否则会误伤相邻数据,导致偶发、难复现的故障。正确做法是:缓冲区强制32字节对齐,长度取32倍数,根据数据流向选择Clean或Invalidate。掌握这些,就能避开这个经典的“坑”。