一、为什么 D-Cache 会让 DMA 数据“出错”?

STM32H7 的 Cortex-M7 内核带 16KB D-Cache。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM。而 DMA 控制器直接访问物理内存,不经过 Cache。

  • 写方向(CPU 写 → DMA 读):CPU 写入的数据还在 Cache 里(Write-Back 模式),DMA 读到的却是 SRAM 中的旧数据。
  • 读方向(DMA 写 → CPU 读):DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 中的旧缓存行。

解决思路:在 DMA 传输前后,手动维护 Cache 一致性。

二、地址对齐:容易被忽视的致命细节

Cache 操作以 32 字节 Cache Line 为单位。若缓冲区地址或长度未按 32 字节对齐,Clean/Invalidate 会波及相邻数据,导致“误伤”。

  • 必须对齐:DMA 缓冲区起始地址 32 字节对齐,长度建议为 32 的整数倍。
  • 推荐做法:使用 __attribute__((aligned(32))) 定义缓冲区,或使用 SCB_InvalidateDCache_by_Addr 时传入对齐后的地址和长度。
// 正确:32字节对齐的DMA缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];

三、Clean 与 Invalidate 的顺序

  • Clean:将 Cache 中已修改的数据写回 SRAM。
  • Invalidate:丢弃 Cache 中的内容,强制下次读取从 SRAM 获取。

顺序原则

  1. CPU 写 → DMA 读:先 Clean,再启动 DMA。
  2. DMA 写 → CPU 读:DMA 完成后,先 Invalidate,再读数据。
  3. DMA 写 → CPU 写同一区域:先 Invalidate,再写,避免旧数据覆盖。

注意:Invalidate 会丢弃未 Clean 的数据!若 CPU 刚写过该区域,必须先 Clean 再 Invalidate。

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

#include "stm32h7xx.h"

#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t uart_rx_buf[DMA_BUF_SIZE];
volatile uint8_t dma_rx_complete = 0;

void uart_dma_init(void) {
    // 1. 配置UART和DMA(略去具体寄存器配置)
    // 2. 启动DMA接收前,Invalidate Cache,确保DMA写入的数据不会被Cache旧值覆盖
    SCB_InvalidateDCache_by_Addr((uint32_t*)uart_rx_buf, DMA_BUF_SIZE);
    // 3. 启动DMA接收
    // HAL_UART_Receive_DMA(&huart1, uart_rx_buf, DMA_BUF_SIZE);
}

// DMA接收完成中断回调
void DMA_RxComplete_Callback(void) {
    // 4. DMA写入了新数据,Invalidate Cache,让CPU读到最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)uart_rx_buf, DMA_BUF_SIZE);
    dma_rx_complete = 1;
}

// CPU发送数据前
void uart_send_data(uint8_t *data, uint16_t len) {
    // 5. 将CPU写入的数据Clean到SRAM,确保DMA读到最新值
    SCB_CleanDCache_by_Addr((uint32_t*)data, len);
    // 6. 启动DMA发送
    // HAL_UART_Transmit_DMA(&huart1, data, len);
}

五、实测踩坑记录

  • 坑1:缓冲区未对齐,相邻变量被误清。定义两个相邻数组,Invalidate 一个导致另一个数据丢失。解决:所有 DMA 缓冲区强制 32 字节对齐。
  • 坑2:Clean 后忘记 Invalidate。DMA 接收完成后直接读数据,发现全是旧值。解决:接收完成必须 Invalidate。
  • 坑3:在中断中调用 Cache 维护函数耗时过长。H7 的 Cache 操作约几十个周期,高频中断下影响实时性。解决:仅对实际数据区域操作,避免全 Cache 维护。
  • 坑4:使用 SCB_InvalidateDCache() 全局失效。这会清掉所有 Cache,包括栈和代码,导致程序跑飞。解决:始终使用 by_Addr 版本。
  • 坑5:MPU 配置为 Write-Through 模式。虽然简化了一致性,但性能下降。建议对 DMA 区域配置为 Non-Cacheable,或使用 Write-Back + 手动维护。

六、最佳实践总结

  • 优先将 DMA 缓冲区放在 Non-Cacheable 内存区域(通过 MPU 配置),一劳永逸。
  • 若必须使用 Cacheable 区域,严格遵循:写前 Clean,读后 Invalidate
  • 缓冲区地址和长度 32 字节对齐
  • 使用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr,避免全局操作。
  • 在 DMA 传输期间,CPU 不要访问该缓冲区,否则需额外同步。

掌握这些,你就能在 STM32H7 上安全地驾驭 D-Cache 与 DMA,兼顾性能与数据一致性。