STM32H7 的 L1 Cache 与 DMA 数据一致性:地址对齐、Clean/Invalidate 顺序与常见踩坑

1. 为什么 Cache 会与 DMA 冲突?

STM32H7 基于 Cortex-M7 内核,配备 L1 Cache(I-Cache 和 D-Cache)。D-Cache 作为 CPU 与主存之间的高速缓冲,会缓存最近访问的数据。当 CPU 写数据时,若命中 Cache,数据仅写入 Cache 而未同步到主存;当 CPU 读数据时,若命中 Cache,则直接返回 Cache 中的旧数据。

DMA 控制器直接访问主存(SRAM、SDRAM 等),不经过 Cache。因此:

  • CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,DMA 读到的是主存中的旧数据。
  • DMA 写 → CPU 读:DMA 将新数据写入主存,但 CPU 可能从 Cache 中读到旧数据。

这就是数据一致性问题。解决手段是 Clean(将 Cache 写回主存)和 Invalidate(将 Cache 行标记为无效,强制下次从主存读取)。

2. 地址对齐:Cache 行与 DMA 边界

Cortex-M7 的 D-Cache 行大小为 32 字节。Clean/Invalidate 操作以 Cache 行为单位。如果 DMA 缓冲区未按 32 字节对齐,或长度不是 32 的整数倍,那么操作会波及相邻数据,导致意外覆盖或丢失。

关键原则

  • DMA 缓冲区起始地址必须 32 字节对齐
  • 缓冲区大小建议为 32 字节的整数倍
  • 若无法满足,需使用 SCB_InvalidateDCache_by_Addr 等函数时传入对齐后的地址和长度,但可能影响相邻变量。

对齐示例

// 错误:未对齐
uint8_t dma_buf[100];

// 正确:32 字节对齐,大小为 32 的倍数
__attribute__((aligned(32))) uint8_t dma_buf[128];

3. Clean 与 Invalidate 的顺序

根据 DMA 传输方向,操作顺序不同:

3.1 CPU 写 → DMA 读(发送)

  1. CPU 填充缓冲区。
  2. Clean D-Cache:将缓冲区数据写回主存。
  3. 启动 DMA 发送。
__attribute__((aligned(32))) uint8_t tx_buf[128];

// 填充数据
fill_tx_buffer(tx_buf, 128);

// Clean D-Cache
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, 128);

// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, 128);

3.2 DMA 写 → CPU 读(接收)

  1. Invalidate D-Cache:丢弃缓冲区对应的 Cache 行(防止 CPU 读到旧数据)。
  2. 启动 DMA 接收。
  3. DMA 完成中断中,再次 Invalidate D-Cache(因为 DMA 期间 CPU 可能预取或访问了该区域)。
  4. CPU 读取数据。
__attribute__((aligned(32))) uint8_t rx_buf[128];

// 启动接收前 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, 128);
HAL_UART_Receive_DMA(&huart1, rx_buf, 128);

// DMA 完成回调中再次 Invalidate
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, 128);
    // 现在可以安全读取 rx_buf
}

注意:Invalidate 操作会丢弃 Cache 中未写回的数据。如果 CPU 在 DMA 期间修改了缓冲区,这些修改会丢失。因此,DMA 接收缓冲区在传输期间不应被 CPU 写入。

4. 完整配置步骤

4.1 启用 D-Cache

main() 初始化阶段启用:

void enable_dcache(void) {
    SCB_EnableICache();
    SCB_EnableDCache();
}

4.2 配置 MPU(可选但推荐)

对于 DMA 缓冲区,可配置 MPU 将区域设为 Non-Cacheable,从而避免手动 Clean/Invalidate。但会降低 CPU 访问性能。

void mpu_config_dma_region(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    HAL_MPU_Disable();
    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.3 使用 Cache 维护函数

CMSIS 提供以下函数:

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize)
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)
  • SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)

注意:addr 必须 32 字节对齐,dsize 为字节数。

5. 常见踩坑与注意事项

  • 坑 1:忘记 Clean 导致 DMA 发送旧数据。CPU 写后必须 Clean,否则 DMA 读到主存旧值。
  • 坑 2:Invalidate 顺序错误。接收前未 Invalidate,CPU 可能从 Cache 读旧数据;接收后未再次 Invalidate,DMA 期间 CPU 可能预取导致数据不一致。
  • 坑 3:缓冲区未对齐。导致 Clean/Invalidate 影响相邻变量,引发随机错误。
  • 坑 4:在 DMA 传输期间 CPU 访问缓冲区。若 CPU 写,Invalidate 会丢失写入;若 CPU 读,可能读到不完整数据。应使用双缓冲或标志位同步。
  • 坑 5:使用 SCB_InvalidateDCache() 全局操作。会清空整个 D-Cache,性能极差,且可能丢失其他数据。务必使用 by_Addr 版本。
  • 坑 6:忽略写缓冲。Cortex-M7 有写缓冲,Clean 后仍需 DSB 指令确保完成。CMSIS 函数内部已包含屏障。
  • 坑 7:MPU 配置错误。若将 DMA 区域设为 Cacheable 但未正确维护,问题依旧。

最佳实践

  • 为 DMA 缓冲区使用 __attribute__((aligned(32)))
  • 发送前 Clean,接收前和接收后 Invalidate。
  • 考虑使用 Non-Cacheable 内存区域存放 DMA 缓冲区,简化软件设计。
  • 使用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 而非全局操作。

6. 总结

STM32H7 的 L1 Cache 与 DMA 数据一致性是嵌入式开发中的经典难题。核心在于理解 Cache 行、地址对齐以及 Clean/Invalidate 的时机。遵循本文的配置步骤和注意事项,可有效避免数据错乱,充分发挥 H7 的高性能优势。