一、问题现象:偶发数据错乱

最近在调试一个基于 STM32H743 的项目时,遇到了一个非常典型的偶发问题:

  • 使用 ADC 连续扫描 + DMA 循环模式采集 8 通道数据,缓冲区定义在 0x24000000(AXI SRAM)。
  • 大部分时间数据正常,但偶尔会出现某个通道的值突然变成 0 或跳变到极大值。
  • 降低主频或关闭 D-Cache 后问题消失。

这类“时好时坏、与 Cache 相关”的现象,几乎可以断定是 D-Cache 与 DMA 的数据一致性 问题。

二、原理:D-Cache 与 DMA 为何会冲突

Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM;而 DMA 是直接访问物理内存的,它看不到 Cache 里的最新数据。

  • CPU 写 → DMA 读:CPU 写入的数据还在 Cache 中(脏行),DMA 从 SRAM 读到的是旧数据。
  • DMA 写 → CPU 读:DMA 把新数据写入 SRAM,但 CPU 读到的却是 Cache 中的旧副本(若该行之前被缓存过)。

解决方法是使用 Cache 维护操作:

  • SCB_CleanDCache_by_Addr():将 Cache 中的脏数据写回 SRAM(Clean)。
  • SCB_InvalidateDCache_by_Addr():丢弃 Cache 中的副本,强制下次从 SRAM 重新加载(Invalidate)。
  • SCB_CleanInvalidateDCache_by_Addr():先 Clean 再 Invalidate。

三、根因:维护操作放错位置

很多开发者知道要用这些函数,但放错位置同样会导致偶发错误。常见错误有两种:

错误 1:DMA 发送前忘记 Clean

// 错误示例:CPU 填充缓冲区后直接启动 DMA
fill_buffer(tx_buf, len);
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&uart->TDR, len);
// 缺少 SCB_CleanDCache_by_Addr

此时 tx_buf 的最新数据可能还在 Cache 中,DMA 发送的是旧数据。

错误 2:DMA 接收后忘记 Invalidate,或 Invalidate 时机不对

// 错误示例:DMA 接收完成中断中直接读缓冲区
void DMA_IRQHandler(void) {
    process(rx_buf, len); // 可能读到 Cache 中的旧数据
}

正确做法是在 DMA 启动之前 Invalidate 接收缓冲区,并在 DMA 完成之后再 Invalidate 一次(如果 CPU 之前访问过该缓冲区)。

更隐蔽的错误是:在 DMA 传输过程中调用 Invalidate。如果此时 CPU 刚写过缓冲区,Invalidate 会丢弃尚未写回的脏数据,导致数据丢失。

四、正确配置步骤

以 ADC + DMA 循环采集为例,推荐流程:

  1. 将 DMA 缓冲区放在非缓存区域,或使用 MPU 配置为 Write-Through/Non-Cacheable。
  2. 若必须使用 Cache,则在 DMA 启动前对接收缓冲区执行 SCB_InvalidateDCache_by_Addr
  3. DMA 传输完成后(中断或轮询标志),再次执行 SCB_InvalidateDCache_by_Addr 再读取数据。
  4. 对于发送方向,在启动 DMA 前执行 SCB_CleanDCache_by_Addr
  5. 注意地址和长度必须 32 字节对齐,否则维护操作可能无效或越界。

五、完整代码示例

#include "stm32h7xx.h"

#define ADC_BUF_LEN  8
#define ADC_BUF_SIZE (ADC_BUF_LEN * sizeof(uint16_t))

// 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) static uint16_t adc_dma_buf[ADC_BUF_LEN];

void adc_dma_init(void)
{
    // 1. 启动 DMA 前,无效化 Cache,确保 CPU 不会读到旧副本
    SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buf, ADC_BUF_SIZE);

    // 2. 配置并启动 ADC + DMA(循环模式)
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buf, ADC_BUF_LEN);
}

// DMA 传输完成回调(例如半满/全满中断)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    // 3. DMA 写入 SRAM 后,CPU 读取前必须再次无效化 Cache
    SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buf, ADC_BUF_SIZE);

    // 4. 此时读取的数据才是 DMA 写入的最新值
    for (int i = 0; i < ADC_BUF_LEN; i++) {
        process_adc_value(adc_dma_buf[i]);
    }
}

// 发送方向示例
__attribute__((aligned(32))) static uint8_t tx_buf[64];

void uart_dma_send(uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);

    // 5. 启动 DMA 前 Clean,确保数据写回 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);

    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

六、注意事项与避坑指南

  • 地址与长度对齐SCB_*DCache_by_Addr 要求地址 32 字节对齐,长度建议向上取整到 32 字节的倍数。
  • 不要频繁 Invalidate:Invalidate 会丢弃 Cache 中的脏数据,若在 CPU 写后、DMA 读前调用,会导致数据丢失。
  • 循环 DMA 的特殊性:循环模式下 DMA 持续写入,CPU 读取前必须 Invalidate,且最好使用双缓冲(半满/全满)减少冲突。
  • MPU 配置更优:对于频繁 DMA 的缓冲区,建议用 MPU 将其配置为 Non-Cacheable 或 Write-Through,从根本上避免一致性问题。
  • 调试技巧:关闭 D-Cache 后问题消失,基本可确认是 Cache 一致性问题;也可用 SCB_InvalidateDCache() 全局无效化辅助定位。
  • 性能权衡:Cache 维护操作有开销,频繁调用会降低性能,需在正确性与效率间平衡。

七、总结

STM32H7 的 D-Cache 与 DMA 数据一致性问题,本质是“CPU 视角”与“DMA 视角”看到的内存不一致。Cache 维护操作本身不难,难的是放对位置:发送前 Clean、接收前 Invalidate、接收后再 Invalidate。理解数据流向,配合 32 字节对齐和 MPU 配置,就能彻底告别这类偶发数据错乱。