一、为什么 Cache 会让 DMA 数据“错乱”?

STM32H7 的 Cortex-M7 内核包含 L1 数据 Cache(D-Cache),默认写回(Write-Back)策略。当 CPU 写数据时,数据可能只停留在 Cache 中,并未立即写入 SRAM;当 DMA 从 SRAM 读取时,读到的就是旧数据。反之,DMA 将新数据写入 SRAM 后,CPU 若直接读 Cache,可能读到旧缓存内容。这就是 Cache 一致性(Coherency) 问题。

  • 写回(Write-Back):CPU 写只更新 Cache,延迟写回内存。
  • 写通(Write-Through):CPU 写同时更新 Cache 和内存,但性能略低。
  • Cache 行(Cache Line):Cortex-M7 的 D-Cache 行大小为 32 字节,所有 Clean/Invalidate 操作均以行为单位。

二、解决思路:MPU 配置 + 手动维护

2.1 MPU 配置(推荐)

通过 MPU 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable(非缓存) 或 Write-Through(写通),可从根本上避免一致性问题。

  • Non-Cacheable:CPU 访问直接读写 SRAM,无 Cache 介入,性能略低但最安全。
  • Write-Through:CPU 写同时更新 Cache 和 SRAM,DMA 读安全;但 DMA 写后 CPU 读仍需 Invalidate。

2.2 手动 Clean/Invalidate(灵活)

若缓冲区仍需 Cache 加速,则必须在 DMA 传输前后手动维护:

  • DMA 发送前(CPU 写 → DMA 读):调用 SCB_CleanDCache_by_Addr(),将 Cache 中脏数据写回 SRAM。
  • DMA 接收后(DMA 写 → CPU 读):调用 SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中旧数据,强制从 SRAM 重新加载。

注意:Invalidate 会丢弃未写回的数据,若 CPU 在 DMA 接收前写过该缓冲区,必须先 Clean 再 Invalidate,否则数据丢失。

三、实战踩坑记录

坑 1:DMA 发送前未 Clean,数据“发不出去”

现象:UART DMA 发送字符串,串口助手收到乱码或旧数据。 原因:CPU 写缓冲区后,数据在 Cache 中,DMA 从 SRAM 读到旧值。 解决:发送前调用 SCB_CleanDCache_by_Addr()。

uint8_t tx_buf[64] __attribute__((aligned(32)));

void uart_dma_send(uint8_t *data, uint16_t len) {
    memcpy(tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len); // 关键!
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

坑 2:DMA 接收后未 Invalidate,CPU 读到旧数据

现象:ADC DMA 采集的数据不变,或 Ethernet 接收包解析错误。 原因:DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据。 解决:接收完成后调用 SCB_InvalidateDCache_by_Addr()。

uint8_t rx_buf[128] __attribute__((aligned(32)));

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf)); // 关键!
    process_data(rx_buf, sizeof(rx_buf));
}

坑 3:MPU 属性配置错误,触发 HardFault

现象:配置 MPU 后,访问 DMA 缓冲区立即 HardFault。 原因:MPU 区域未按 32 字节对齐,或属性组合非法(如 Normal 内存却设为 Device)。 解决:确保基地址和大小对齐(大小需为 2 的幂,且 ≥32 字节),属性使用 ARM_MPU_ATTR_NORMAL_NON_CACHEABLE 等标准宏。

void MPU_Config(void) {
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 非缓存
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

坑 4:Clean/Invalidate 地址未对齐,操作越界

现象:调用 Clean/Invalidate 后,相邻变量被意外修改。 原因:Cortex-M7 的 Cache 操作以 32 字节行为单位,若地址未对齐,会波及相邻数据。 解决:缓冲区必须 32 字节对齐,长度建议向上取整到 32 的倍数。使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。

__attribute__((aligned(32))) uint8_t dma_buf[256];
// 或使用 CMSIS 宏
ALIGN_32BYTES(uint8_t dma_buf[256]);

四、完整示例:ADC + DMA + Cache 维护

#include "stm32h7xx_hal.h"

#define ADC_BUF_LEN 128
ALIGN_32BYTES(uint16_t adc_buf[ADC_BUF_LEN]);

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

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    // DMA 传输完成,Invalidate Cache 以读取最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
    // 处理数据
    for (int i = 0; i < ADC_BUF_LEN; i++) {
        float voltage = adc_buf[i] * 3.3f / 65535.0f;
        // ...
    }
    // 若需再次启动 DMA,且 CPU 修改了 adc_buf,则先 Clean
    // SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
    // HAL_ADC_Start_DMA(...);
}

五、注意事项与最佳实践

  • 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠,避免手动维护遗漏。
  • 若必须使用 Cache,严格遵循“发送前 Clean,接收后 Invalidate” 原则。
  • 缓冲区必须 32 字节对齐,长度建议为 32 的倍数。
  • 避免在 DMA 传输过程中 CPU 访问同一缓冲区,否则需额外同步。
  • 使用 SCB_EnableDCache() 后,所有 DMA 相关代码都需检查一致性。
  • 调试时若发现数据“时好时坏”,优先怀疑 Cache 问题。
  • 参考 ST 官方文档 AN4838《Managing memory protection unit (MPU) in STM32 MCUs》和 AN4839《Level 1 cache on STM32F7 Series and STM32H7 Series》。

掌握以上要点,你就能在 STM32H7 上驯服 Cache 与 DMA,让高性能与数据一致性兼得。