STM32H7 的 D-Cache 与 DMA 一致性:从 Cache 行回写到 MPU 配置的完整避坑指南
1. 为什么 DMA 遇上 D-Cache 会“翻车”
STM32H7 的 Cortex-M7 内核最高运行在 480MHz,为了弥补内核与 SRAM/外部存储器之间的速度鸿沟,芯片内置了 D-Cache(数据缓存)。CPU 读写数据时,实际访问的是 Cache,而不是直接访问 SRAM。
DMA 控制器则是一个“独立搬运工”,它直接访问物理内存,完全不知道 Cache 的存在。这就导致两类经典问题:
- DMA 读取(外设→内存):DMA 把新数据写入 SRAM,但 CPU 可能仍从 Cache 中读到旧数据(因为 Cache 里的副本没更新)。
- DMA 发送(内存→外设):CPU 刚写入的数据还在 Cache 中(写回策略下未同步到 SRAM),DMA 却从 SRAM 读到旧值,导致发送错误数据。
根本原因:Cache 与 DMA 看到的内存视图不一致。
2. Cache 行与回写机制
Cortex-M7 的 D-Cache 以 Cache Line(缓存行) 为单位管理,STM32H7 的 Cache Line 大小为 32 字节。所有维护操作都必须按 32 字节对齐进行。
- 写回(Write-Back):CPU 写数据只更新 Cache,标记为“脏”;只有 Cache 行被驱逐时才写回 SRAM。
- 写通(Write-Through):CPU 写数据同时更新 Cache 和 SRAM,一致性较好但性能略低。
STM32H7 默认使用 Write-Back,因此 DMA 发送前必须手动将 Cache 行“清洗”到 SRAM。
3. 软件维护:Clean / Invalidate 的正确姿势
CMSIS 提供了三个关键函数:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 中脏数据写回 SRAM。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从 SRAM 读取。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先清洗再无效。
使用规则:
- DMA 发送前:
CleanDCache,确保 SRAM 数据最新。 - DMA 接收后:
InvalidateDCache,丢弃旧缓存,让 CPU 读到 DMA 写入的新数据。 - 若缓冲区可能被 CPU 修改过,接收前应先
Clean再Invalidate,防止脏数据覆盖 DMA 结果。
注意:地址和长度必须 32 字节对齐,否则会误伤相邻数据。
#include "stm32h7xx.h"
#define DMA_BUF_SIZE 128
#define CACHE_LINE 32
// 按 32 字节对齐的缓冲区
__attribute__((aligned(CACHE_LINE))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(CACHE_LINE))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
// DMA 发送前:清洗 Cache
void dma_send_prepare(void *buf, uint32_t len)
{
uint32_t addr = (uint32_t)buf;
uint32_t size = (len + CACHE_LINE - 1) & ~(CACHE_LINE - 1);
SCB_CleanDCache_by_Addr((uint32_t *)addr, size);
}
// DMA 接收后:无效 Cache
void dma_recv_complete(void *buf, uint32_t len)
{
uint32_t addr = (uint32_t)buf;
uint32_t size = (len + CACHE_LINE - 1) & ~(CACHE_LINE - 1);
SCB_InvalidateDCache_by_Addr((uint32_t *)addr, size);
}
4. MPU 配置:一劳永逸的 DMA 缓冲区方案
软件维护虽然有效,但每次传输都要调用函数,容易遗漏。更优雅的方式是使用 MPU(内存保护单元) 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through 属性。
推荐配置:
- Non-Cacheable:CPU 访问该区域不经过 Cache,DMA 与 CPU 天然一致,但 CPU 访问速度下降。
- Write-Through + Read-Allocate:折中方案,写操作同步到 SRAM,读操作仍可缓存。
以下示例将 0x30000000 开始的 64KB 区域配置为 Non-Cacheable:
void MPU_Config_DMA_Region(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
配置后,将 DMA 缓冲区放入该区域(例如使用链接脚本定位到 0x30000000),即可省去手动维护 Cache 的步骤。
5. 完整示例:UART DMA 接收 + Cache 维护
#define RX_SIZE 64
__attribute__((aligned(32))) uint8_t uart_rx[RX_SIZE];
void UART_DMA_Init(void)
{
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, uart_rx, RX_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 无效 Cache,确保 CPU 读到 DMA 新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)uart_rx, RX_SIZE);
// 处理数据...
}
}
6. 避坑清单
-
地址对齐:所有 Cache 维护操作必须 32 字节对齐,缓冲区用
__attribute__((aligned(32)))。 - 长度向上取整:长度不是 32 的倍数时,向上取整到 Cache Line 边界。
- 顺序不能反:发送前 Clean,接收后 Invalidate;接收前若缓冲区被 CPU 写过,先 Clean 再 Invalidate。
- 不要对栈变量做 DMA:栈变量地址可能未对齐,且生命周期不可控。
- MPU 区域不要重叠:多个 MPU 区域重叠时,编号高的优先,配置时注意顺序。
- 中断中调用 Cache 维护函数:这些函数执行时间较长,避免在高频中断中调用。
- 调试时关闭 Cache:若问题诡异,可先关闭 D-Cache 验证是否为一致性问题。
掌握以上原理与配置,STM32H7 的 D-Cache 与 DMA 就能和谐共处,既享受高性能,又保证数据可靠。