一、为什么 DCache 会成为 DMA 的“猪队友”?

Cortex-M7 为了提升性能,引入了数据缓存(DCache)。CPU 访问内存时,若数据在 Cache 中则直接命中,避免访问慢速的 SRAM。但 DMA 控制器是独立于 CPU 的总线主设备,它直接读写物理内存,不经过 Cache。这就导致:

  • CPU 写数据到 Cache,但尚未写回内存(Write-Back 模式),DMA 从内存读到的却是旧数据。
  • DMA 将新数据写入内存,但 CPU 读到的仍是 Cache 中的旧副本。

这就是典型的数据一致性问题。STM32H7 默认开启 DCache,若不处理,DMA 传输将随机出错。

二、地址对齐:不仅仅是 4 字节

Cache 操作以 Cache Line 为单位,Cortex-M7 的 Cache Line 为 32 字节。使用 SCB_CleanDCache_by_Addr 等函数时,地址必须按 32 字节对齐,否则可能误伤相邻数据。

关键规则:

  • DMA 缓冲区起始地址必须 32 字节对齐(推荐使用 __attribute__((aligned(32))))。
  • 缓冲区大小最好是 32 字节的整数倍,避免 Clean/Invalidate 时波及无关变量。
  • 若无法对齐,需手动计算覆盖范围,但强烈建议对齐。
// 推荐定义方式
__attribute__((aligned(32))) uint8_t dma_buffer[1024];

三、Clean 与 Invalidate 的时机

  • Clean:将 Cache 中已修改的数据写回内存。用于 CPU 写、DMA 读 的场景(如发送数据)。
  • Invalidate:丢弃 Cache 中的内容,强制 CPU 下次从内存读取。用于 DMA 写、CPU 读 的场景(如接收数据)。
  • Clean+Invalidate:两者结合,用于双向传输或不确定场景。

正确操作顺序:

  1. 发送(CPU→DMA):

    • CPU 填充缓冲区。
    • 执行 SCB_CleanDCache_by_Addr(buf, len)。
    • 启动 DMA 发送。
  2. 接收(DMA→CPU):

    • 启动 DMA 接收前,执行 SCB_InvalidateDCache_by_Addr(buf, len)(防止 Cache 中有旧数据)。
    • 等待 DMA 完成。
    • 再次执行 SCB_InvalidateDCache_by_Addr(buf, len),确保 CPU 读取最新数据。

注意: Invalidate 会丢弃未写回的数据,若缓冲区在 DMA 传输期间被 CPU 修改,将丢失修改。因此接收缓冲区在 DMA 期间不应被 CPU 写入。

四、完整代码示例(以 UART DMA 接收为例)

#include "stm32h7xx.h"

#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
volatile uint8_t dma_done = 0;

void uart_dma_init(void) {
    // 初始化 UART 和 DMA(略)
    // 启动接收前无效化 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) {
    // 无效化 Cache,确保 CPU 读取到 DMA 写入的最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
    dma_done = 1;
}

// 发送示例
__attribute__((aligned(32))) uint8_t tx_buf[] = "Hello DMA";
void uart_send(void) {
    // 清理 Cache,将 CPU 写入的数据同步到内存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
    HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));
}

五、踩坑记录与注意事项

  • 坑1:忘记 Invalidate 导致接收数据错乱。DMA 接收完成后,CPU 读到的仍是 Cache 中的旧数据。务必在 DMA 完成回调中执行 Invalidate。
  • 坑2:缓冲区未对齐,Clean 时破坏相邻变量。曾有一个项目,uint8_t buf[10] 未对齐,Clean 32 字节导致其他变量被意外写回,引发随机死机。
  • 坑3:在 DMA 传输过程中操作缓冲区。若 CPU 在 DMA 接收期间写缓冲区,Invalidate 会丢弃这些写入,导致数据丢失。应使用双缓冲或确保无冲突。
  • 坑4:使用 SCB_CleanInvalidateDCache() 全局操作。该函数影响整个 Cache,可能降低性能,且在多任务环境中可能引发问题。尽量使用按地址操作。
  • 坑5:忽略 MPU 配置。若将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,可简化一致性处理,但会牺牲性能。根据场景权衡。

最佳实践:

  • 将 DMA 缓冲区放在非缓存区域(通过 MPU 配置),一劳永逸。
  • 若必须使用 Cache,严格遵循 Clean/Invalidate 时机,并确保 32 字节对齐。
  • 使用 SCB_InvalidateDCache_by_Addr 前,确认地址和长度已按 32 字节对齐。
  • 在 RTOS 中,注意 Cache 操作与任务调度的互斥。

六、总结

STM32H7 的 DCache 与 DMA 是一对需要精心协调的搭档。理解 Cache Line、对齐要求以及 Clean/Invalidate 的适用场景,是保证数据一致性的关键。实际开发中,建议优先考虑 MPU 配置非缓存区域,若使用 Cache 则严格遵循操作顺序,并利用调试工具验证数据流。避开这些坑,才能充分发挥 H7 的高性能。