STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、Clean/Invalidate 时序与常见踩坑
STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,并带有 16KB 的 D-Cache 和 I-Cache。开启 D-Cache 后,CPU 访问 SRAM 的命中率大幅提升,但 DMA 直接访问物理内存,不经过 Cache。如果 CPU 先写数据到 Cache 而未写回 SRAM,DMA 读到的就是旧数据;反之 DMA 写入 SRAM 后,CPU 可能仍从 Cache 读到旧值。这就是经典的 Cache 一致性问题。
一、为什么会有缓存一致性问题
Cortex-M7 的 D-Cache 是写回(Write-Back)且写分配(Write-Allocate)的。CPU 写数据时,若 Cache 命中,只更新 Cache 行并标记为脏(Dirty),不会立即写回 SRAM。DMA 是总线主设备,直接访问 SRAM,因此看不到 Cache 中的新数据。
- CPU 写 → DMA 读:需要先 Clean(写回)Cache,把脏数据刷到 SRAM,再启动 DMA。
- DMA 写 → CPU 读:DMA 完成后,需要 Invalidate(无效化)对应 Cache 行,让 CPU 下次读取时从 SRAM 重新加载。
二、地址对齐与 Cache 行
Cortex-M7 的 D-Cache 行大小为 32 字节。Clean/Invalidate 操作以 Cache 行为单位,因此缓冲区地址和长度必须按 32 字节对齐,否则会误伤相邻数据。
- 缓冲区起始地址建议 32 字节对齐:
__attribute__((aligned(32)))。 - 缓冲区长度建议向上取整到 32 字节的整数倍。
- 若缓冲区跨越两个 Cache 行且未对齐,Invalidate 可能丢弃相邻变量尚未写回的数据,导致“莫名其妙”的变量被清零。
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) static uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) static uint8_t dma_rx_buf[DMA_BUF_SIZE];
三、Clean 与 Invalidate 的时序
3.1 CPU 发送数据(TX)
- CPU 填充
dma_tx_buf。 - 调用
SCB_CleanDCache_by_Addr()将脏行写回 SRAM。 - 启动 DMA 传输。
- 等待 DMA 完成(中断或轮询)。
3.2 CPU 接收数据(RX)
- 启动 DMA 接收前,先 Invalidate 对应 Cache 行,避免 Cache 中的旧数据干扰。
- 等待 DMA 完成。
- 再次 Invalidate,确保 CPU 读到 DMA 写入的新数据。
注意:Invalidate 必须在 DMA 启动前和完成后各做一次,且期间 CPU 不要访问该缓冲区。
四、完整代码示例
以下以 UART DMA 发送为例,展示 Clean 操作。
#include "stm32h7xx_hal.h"
extern UART_HandleTypeDef huart1;
#define TX_SIZE 128
__attribute__((aligned(32))) static uint8_t tx_buf[TX_SIZE];
void uart_dma_send(uint8_t *data, uint16_t len)
{
/* 1. 拷贝数据到对齐缓冲区 */
memcpy(tx_buf, data, len);
/* 2. Clean D-Cache,将 tx_buf 写回 SRAM */
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_SIZE);
/* 3. 启动 DMA 发送 */
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
接收方向示例:
#define RX_SIZE 128
__attribute__((aligned(32))) static uint8_t rx_buf[RX_SIZE];
volatile uint8_t rx_done = 0;
void uart_dma_start_rx(void)
{
/* 启动前 Invalidate,丢弃 Cache 中旧数据 */
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_SIZE);
}
/* DMA 完成回调 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
/* 完成后再次 Invalidate,确保读到新数据 */
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_SIZE);
rx_done = 1;
}
}
五、MPU 配置:更优雅的方案
如果不想每次手动 Clean/Invalidate,可以用 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through。这样 CPU 和 DMA 看到的内存始终一致,代价是访问速度略降。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; /* D2 SRAM 示例 */
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);
}
六、常见踩坑清单
- 缓冲区未 32 字节对齐:Invalidate 会误伤相邻变量,表现为变量随机被清零。
- Clean/Invalidate 长度不是 32 的倍数:同样会误伤相邻 Cache 行。
- RX 启动前忘记 Invalidate:Cache 中的旧数据可能被 DMA 覆盖后仍被 CPU 读到。
- DMA 传输中 CPU 访问缓冲区:Cache 与 DMA 竞争,数据不可预测。
-
使用
SCB_InvalidateDCache()全局无效化:会丢弃所有脏数据,导致程序崩溃,应使用by_Addr版本。 - 栈上变量做 DMA 缓冲区:栈地址通常未对齐,且生命周期不可控。
- 忘记配置 MPU 或未开启 Cache:性能上不去,但一致性反而“正常”,容易掩盖问题。
七、总结
STM32H7 的 D-Cache 是性能利器,但 DMA 一致性必须手动处理。核心原则:TX 前 Clean,RX 前后 Invalidate,缓冲区 32 字节对齐,传输中 CPU 不碰缓冲区。如果嫌麻烦,用 MPU 把 DMA 区域设为 Non-Cacheable 是最省心的方案。掌握这些,你就能在 H7 上放心地跑高速 DMA 了。