一、为什么 D-Cache 与 DMA 会“打架”?
STM32H7 的 Cortex-M7 内核带有 16KB 的 D-Cache。开启后,CPU 读写数据会先经过 Cache,而不是直接访问 SRAM。DMA 则绕过 Cache 直接读写 SRAM。这就导致:
- CPU 写数据到 Cache,但未写回 SRAM:DMA 从 SRAM 读到的还是旧数据。
- DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据:CPU 看不到更新。
因此,必须在 DMA 传输前后正确维护 Cache 一致性。
二、MPU 配置:为 DMA 缓冲区划定“安全区”
最优雅的方案是利用 MPU 将 DMA 缓冲区配置为 Write-Through, No Write-Allocate 或 Non-Cacheable,从而避免手动维护。
2.1 配置步骤(以 Non-Cacheable 为例)
- 使能 MPU:
SCB_EnableMPU()前先SCB_DisableMPU()。 - 定义 MPU 区域:基地址为缓冲区地址,大小需 32 字节对齐。
- 设置属性:
TEX=1, C=0, B=0, S=1表示 Non-Cacheable, Shareable。 - 使能该区域并开启 MPU。
#include "stm32h7xx.h"
#define DMA_BUF_ADDR 0x30000000 // AXI SRAM 起始地址
#define DMA_BUF_SIZE 1024 // 1KB
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = DMA_BUF_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_1KB;
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_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 区域大小必须是 2 的幂,且基地址对齐到该大小。
三、Clean 与 Invalidate 的正确时机
若无法使用 MPU(例如缓冲区分散),则必须手动调用 CMSIS 函数:
- Clean:将 Cache 中已修改的数据写回 SRAM。
- Invalidate:丢弃 Cache 中的旧数据,强制从 SRAM 重新加载。
3.1 发送方向(CPU → DMA)
- CPU 填充缓冲区。
- Clean 缓冲区(确保数据到达 SRAM)。
- 启动 DMA 发送。
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&periph, len);
3.2 接收方向(DMA → CPU)
- Invalidate 缓冲区(丢弃旧 Cache 行,防止 DMA 写入后 CPU 读旧值)。
- 启动 DMA 接收。
- DMA 完成中断中再次 Invalidate(因为 DMA 可能已写入新数据,但 Cache 中可能残留旧行)。
- CPU 读取数据。
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
HAL_DMA_Start_IT(&hdma, (uint32_t)&periph, (uint32_t)rx_buf, len);
// DMA 完成回调中
void DMA_Complete_Callback(void)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
// 处理数据
}
四、完整实战代码:UART DMA 不定长接收
以 UART 空闲中断 + DMA 接收为例,展示完整流程。
#define RX_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
void UART_DMA_Init(void)
{
// 1. 配置 MPU(可选,若使用 Non-Cacheable 则无需手动维护)
// MPU_Config();
// 2. 启动 DMA 接收前 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
// UART 空闲中断回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart1) {
// 3. 数据已到达 SRAM,Invalidate 使 CPU 可见
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
// 4. 处理数据
ProcessData(rx_buf, Size);
// 5. 重新启动接收
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
}
五、常见踩坑与注意事项
-
地址未对齐:
SCB_CleanDCache_by_Addr要求地址 32 字节对齐,否则可能误伤相邻数据。使用__attribute__((aligned(32)))修饰缓冲区。 - Invalidate 前未 Clean:若缓冲区中既有 CPU 写入又有 DMA 写入,先 Invalidate 会丢失 CPU 未写回的数据。正确顺序:先 Clean 再 Invalidate。
- DMA 传输中访问缓冲区:DMA 工作时 CPU 不应读写该缓冲区,否则 Cache 行可能被意外修改。
- MPU 配置冲突:多个 MPU 区域重叠时,编号高的优先级高,需仔细规划。
- 性能陷阱:频繁 Clean/Invalidate 会降低性能,尽量用 MPU 将 DMA 缓冲区设为 Non-Cacheable。
- 中断中调用:Cache 维护函数执行时间较长,在高速中断中慎用,可考虑延迟处理。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性是嵌入式开发中的经典难题。核心原则:明确数据流向,在正确时机 Clean 或 Invalidate。优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,可一劳永逸。若必须手动维护,牢记“发送前 Clean,接收前 Invalidate,接收后再次 Invalidate”。掌握这些,即可让 H7 的高性能与 DMA 的便利性完美结合。