STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、MPU 配置与 Clean/Invalidate 的实战避坑
STM32H7 系列凭借 Cortex-M7 内核和 480MHz 主频,成为高性能嵌入式应用的首选。但许多开发者从 F4/F7 迁移到 H7 后,常遇到 DMA 传输数据错乱、HardFault 等问题,根源往往是 D-Cache 与 DMA 的缓存一致性。本文从原理到实战,帮你彻底避开这些坑。
一、为什么会有缓存一致性问题?
Cortex-M7 的 D-Cache 位于 CPU 和 AXI 总线之间。当 CPU 访问内存时,数据可能被缓存在 D-Cache 中,而 DMA 直接访问物理内存(SRAM)。这就导致:
- CPU 写数据:数据可能还在 Cache 中,未写回 SRAM,DMA 读到旧数据。
- DMA 写数据:DMA 将新数据写入 SRAM,但 CPU 读到的仍是 Cache 中的旧数据。
因此,必须通过 Clean(写回) 和 Invalidate(无效化) 操作来同步 Cache 与内存。
二、地址对齐:容易被忽视的硬性要求
Cortex-M7 的 Cache 行大小为 32 字节。Clean/Invalidate 操作以 Cache 行为单位,若缓冲区地址或长度未按 32 字节对齐,会导致:
- 误清除相邻变量的 Cache,引发数据损坏。
- 操作越界,触发 HardFault。
避坑指南:
- DMA 缓冲区必须 32 字节对齐,长度建议为 32 的整数倍。
- 使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏。
// 定义 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];
三、MPU 配置:为 DMA 区域设置正确的内存属性
默认情况下,STM32H7 的 SRAM 区域是 Write-Back, Write-Allocate 的,这会导致 DMA 访问时数据不一致。推荐将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through。
3.1 使用 MPU 配置 Non-Cacheable 区域
以 STM32H743 为例,将 0x30000000 开始的 64KB SRAM 配置为 Non-Cacheable:
#include "stm32h7xx_hal.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 SRAM4 (0x38000000) 为 Non-Cacheable,用于 DMA
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x38000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.2 使用链接脚本将变量放入 Non-Cacheable 区域
在链接脚本中定义段,将 DMA 缓冲区放入该区域:
MEMORY
{
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
}
SECTIONS
{
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} >RAM_D3
}
然后在代码中:
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_buffer[256];
四、Clean 与 Invalidate 的正确使用
即使配置了 MPU,有时仍需手动维护 Cache。关键原则:
- CPU 写,DMA 读:在启动 DMA 前,对缓冲区执行 Clean(写回)。
- DMA 写,CPU 读:在 DMA 完成后,对缓冲区执行 Invalidate(无效化)。
- CPU 读写,DMA 读写:先 Clean 再 Invalidate。
4.1 使用 CMSIS 函数
#include "cmsis_compiler.h"
// 写回并无效化
SCB_CleanInvalidateDCache_by_Addr((uint32_t *)dma_buffer, sizeof(dma_buffer));
// 仅写回
SCB_CleanDCache_by_Addr((uint32_t *)dma_buffer, sizeof(dma_buffer));
// 仅无效化
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buffer, sizeof(dma_buffer));
4.2 完整 DMA 发送示例
void DMA_SendData(uint8_t *data, uint16_t len)
{
// 1. 确保数据已写入 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)data, len);
// 2. 启动 DMA
HAL_UART_Transmit_DMA(&huart1, data, len);
}
4.3 完整 DMA 接收示例
void DMA_ReceiveData(uint8_t *buffer, uint16_t len)
{
// 1. 无效化 Cache,确保 CPU 读取时从 SRAM 获取
SCB_InvalidateDCache_by_Addr((uint32_t *)buffer, len);
// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, buffer, len);
}
// DMA 完成回调中再次无效化
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, RX_SIZE);
// 处理数据...
}
五、实战避坑清单
- 地址对齐:DMA 缓冲区必须 32 字节对齐,长度建议为 32 的倍数。
- MPU 配置:将 DMA 区域设为 Non-Cacheable 或 Write-Through,避免手动维护。
- Clean 时机:CPU 写数据后、启动 DMA 前。
- Invalidate 时机:DMA 完成后、CPU 读数据前。
- 避免频繁操作:Clean/Invalidate 开销较大,尽量批量处理。
- 调试技巧:若数据错乱,先检查 Cache 操作是否遗漏或地址未对齐。
-
使用
__DSB():在 Clean/Invalidate 后插入内存屏障,确保操作完成。
SCB_CleanDCache_by_Addr((uint32_t *)data, len);
__DSB(); // 确保写回完成
六、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是高性能应用的“必修课”。核心思路:要么让 DMA 区域不可缓存(MPU 配置),要么在正确时机手动 Clean/Invalidate。同时务必保证地址 32 字节对齐。掌握这些要点,你就能在 H7 上稳定高效地使用 DMA,避免那些令人抓狂的随机错误。