STM32H7 的 DCache 与 DMA 一致性:MPU 配置与 clean/invalidate 的正确姿势
一、为什么 DCache 会让 DMA 数据错乱?
STM32H7 基于 Cortex-M7 内核,带有 16KB I-Cache 和 16KB D-Cache。开启 D-Cache 后,CPU 读写的是 Cache 而非直接访问 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理内存。两者若不同步,就会出现:
- CPU 写数据 → DMA 发送:数据可能还在 Cache 中未写回 SRAM,DMA 读到旧数据。
- DMA 接收数据 → CPU 读:DMA 已把新数据写入 SRAM,但 CPU 读到的是 Cache 中的旧数据。
这就是经典的 Cache 一致性问题。解决手段有两类:MPU 配置为 Write-Through/Non-Cacheable,或手动 clean/invalidate。
二、Cache 操作的本质
Cortex-M7 提供 CMSIS 函数:
-
SCB_CleanDCache_by_Addr(addr, size):把 Cache 中已修改的数据写回 SRAM。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从 SRAM 重新加载。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃。
关键原则:
- CPU 写、DMA 读(发送)→ 在启动 DMA 前 clean。
- DMA 写、CPU 读(接收)→ 在 DMA 完成后 invalidate。
- 地址必须 32 字节对齐,长度向上取整到 32 字节,否则会误伤相邻数据。
三、MPU 配置:更优雅的方案
对于频繁 DMA 的缓冲区,推荐用 MPU 将其配置为 Non-Cacheable 或 Write-Through,从根源避免一致性问题。
#include "stm32h7xx_hal.h"
/* 将 DMA 缓冲区所在区域配置为 Non-Cacheable */
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_Init = {0};
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.Number = MPU_REGION_NUMBER0;
MPU_Init.BaseAddress = 0x30000000; /* D2 SRAM 起始 */
MPU_Init.Size = MPU_REGION_SIZE_64KB;
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.TypeExtField = MPU_TEX_LEVEL0;
MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_Init.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; /* 关键 */
MPU_Init.IsBufferable = MPU_ACCESS_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 配置必须在
SCB_EnableDCache()之前完成,否则需先关闭 Cache。
四、手动 clean/invalidate 完整示例
若缓冲区无法用 MPU 隔离,则必须手动维护。以下以 UART DMA 发送/接收为例。
#define BUF_SIZE 256
/* 必须 32 字节对齐 */
alignas(32) uint8_t tx_buf[BUF_SIZE];
alignas(32) uint8_t rx_buf[BUF_SIZE];
/* 发送:CPU 填数据 → clean → 启动 DMA */
void UART_Send_DMA(uint8_t *data, uint16_t len)
{
memcpy(tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
/* 接收完成回调:invalidate → 再读数据 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
/* 此时 rx_buf 中才是 DMA 写入的最新数据 */
ProcessData(rx_buf, BUF_SIZE);
}
顺序不能颠倒:
- 发送:先 clean,后启动 DMA。
- 接收:先等 DMA 完成,后 invalidate,再访问数据。
五、常见坑与注意事项
-
对齐问题:
SCB_*DCache_by_Addr要求地址 32 字节对齐,长度按 32 字节向上取整。建议用alignas(32)定义缓冲区。 - invalidate 前不要读缓冲区:若 CPU 先读,会把旧数据重新载入 Cache,invalidate 后仍需再读一次才正确。
- DMA 描述符/链表:若使用链式 DMA,描述符本身也需 clean。
- 多缓冲区场景:每个缓冲区独立 clean/invalidate,不要跨区域操作。
- 性能权衡:Non-Cacheable 区域访问变慢,适合大块 DMA;小数据可手动 clean/invalidate。
- 调试陷阱:调试器读内存可能触发 Cache 行为,导致问题“消失”,建议用串口打印验证。
六、总结
STM32H7 的 DCache 是性能利器,但必须与 DMA 协同。牢记三句话:发送前 clean,接收后 invalidate,地址 32 字节对齐。对于高频 DMA 缓冲区,优先用 MPU 配置为 Non-Cacheable,从架构上消除隐患。掌握这些姿势,你的 H7 项目将既快又稳。