一、为什么 DCache 会让 DMA 数据“失控”?

STM32H7 主频高达 480MHz,为了匹配内核速度,芯片内置了 L1 Cache(I-Cache 和 D-Cache)。当 CPU 访问内存时,数据会被缓存到 D-Cache 中。而 DMA 控制器直接访问物理内存(SRAM),不经过 Cache。

这就导致两个经典问题:

  • CPU 写,DMA 读:CPU 写入的数据可能还在 Cache 里(Write-Back 模式),内存中还是旧数据,DMA 发送出去的是错误内容。
  • DMA 写,CPU 读:DMA 把新数据写入内存,但 CPU 读到的却是 Cache 中的旧副本。

要解决这个问题,必须手动维护 Cache 与内存的一致性。

二、地址对齐:Cache Line 的硬性约束

Cortex-M7 的 D-Cache 按 Cache Line 管理,每条 Line 大小为 32 字节。所有 Clean/Invalidate 操作都以 32 字节为最小单位。

关键规则:

  • DMA 缓冲区地址必须 32 字节对齐。
  • 缓冲区大小最好是 32 字节的整数倍。
  • 如果缓冲区跨越了多个 Cache Line,操作时必须覆盖所有涉及的 Line。

错误示例:

uint8_t dma_buf[100]; // 地址可能未对齐,大小也不是32的倍数

正确做法:

// 使用编译器属性强制32字节对齐
__attribute__((aligned(32))) uint8_t dma_buf[128];

// 或者使用 C11 标准
#include <stdalign.h>
alignas(32) uint8_t dma_buf[128];

注意:即使缓冲区起始地址对齐,如果大小不是 32 的倍数,末尾的 Cache Line 仍可能包含其他变量。Invalidate 时会把其他变量的修改丢弃,导致隐蔽 Bug。

三、Clean 与 Invalidate:顺序决定成败

3.1 核心概念

  • Clean:将 Cache 中“脏”数据写回内存。
  • Invalidate:将 Cache 中的内容标记为无效,下次读取时从内存重新加载。

3.2 两种典型场景

场景一:CPU 发送数据给 DMA(如 UART 发送)

  1. CPU 填充缓冲区。
  2. Clean 缓冲区对应的 Cache Line,确保数据写入内存。
  3. 启动 DMA 发送。
// 填充数据
memcpy(dma_buf, src, len);

// 清理 D-Cache,将数据写回内存
SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, len);

// 启动 DMA
HAL_UART_Transmit_DMA(&huart1, dma_buf, len);

场景二:DMA 接收数据给 CPU(如 UART 接收)

  1. 启动 DMA 接收前,Invalidate 缓冲区,丢弃 Cache 中的旧数据。
  2. 等待 DMA 完成。
  3. 再次 Invalidate,确保 CPU 读取到 DMA 写入的新数据。
// 启动接收前,无效化 Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, len);
HAL_UART_Receive_DMA(&huart1, dma_buf, len);

// 等待完成...

// 接收完成后,再次无效化
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, len);
// 现在可以安全读取 dma_buf

3.3 顺序陷阱

  • 先 Invalidate 再 Clean 是危险的:如果缓冲区有未写回的数据,Invalidate 会直接丢弃,造成数据丢失。
  • DMA 接收前必须 Invalidate:否则 CPU 可能读到 Cache 中的旧数据,覆盖 DMA 写入的新数据。
  • DMA 发送前必须 Clean:否则 DMA 读到的是内存中的旧数据。

四、完整代码示例:UART DMA 回环测试

#include "stm32h7xx_hal.h"

#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];

void DMA_Send(uint8_t *data, uint16_t len) {
    memcpy(tx_buf, data, len);
    // 清理 D-Cache,确保数据写入内存
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

void DMA_Receive_Start(void) {
    // 无效化 D-Cache,丢弃旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 再次无效化,确保 CPU 读取到新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    // 处理数据...
}

五、常见踩坑与注意事项

  • 坑1:忘记对齐。未对齐的缓冲区会导致 Clean/Invalidate 操作影响到相邻变量,引发随机错误。务必使用 __attribute__((aligned(32)))。
  • 坑2:Invalidate 前未 Clean。如果缓冲区有 CPU 写入的数据尚未写回,Invalidate 会直接丢弃,造成数据丢失。
  • 坑3:DMA 传输中访问缓冲区。DMA 工作时 CPU 不应读写该缓冲区,否则 Cache 状态混乱。
  • 坑4:使用 SCB_CleanInvalidateDCache() 全局操作。该函数会影响整个 Cache,可能破坏其他数据,应使用 by_Addr 版本。
  • 坑5:忽略 MPU 配置。可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题。例如:
MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // 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);
  • 坑6:中断中调用 Cache 维护函数。这些函数执行时间较长,可能影响实时性,建议在 DMA 完成回调中处理。

六、总结

STM32H7 的 DCache 与 DMA 协同工作,核心就是 对齐、Clean、Invalidate 三件事。牢记:

  • DMA 发送前 Clean,接收前和接收后 Invalidate。
  • 缓冲区 32 字节对齐,大小取 32 的倍数。
  • 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。

掌握这些原则,就能让 H7 的性能与稳定性兼得。