一、为什么 Cache 与 DMA 会“打架”?
STM32H7 内置 16KB I-Cache 和 16KB D-Cache,CPU 访问数据时优先走 Cache,而 DMA 直接访问物理内存(SRAM/SDRAM)。当 CPU 写数据到 Cache 但尚未写回内存时,DMA 读取的是旧数据;反之,DMA 更新了内存,CPU 却可能从 Cache 读到旧值。这就是Cache 一致性问题。
典型场景:
- ADC 通过 DMA 采集数据到数组,CPU 读数组发现全是 0 或旧值。
- CPU 填充发送缓冲区后启动 DMA 发送,对方收到乱码。
二、解决思路:MPU 配置 + Cache 维护操作
2.1 MPU 配置:为 DMA 缓冲区设置正确属性
最根本的方法是使用 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,避免 Cache 介入。推荐 Non-Cacheable,简单可靠。
#include "stm32h7xx_hal.h"
// 定义 DMA 缓冲区,必须 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buffer[1024];
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 DMA 缓冲区所在区域为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)dma_buffer;
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_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 区域大小必须是 2 的幂,且基地址按大小对齐。若缓冲区跨区域,需拆分多个 MPU 区域。
2.2 如果必须使用 Cache:Clean/Invalidate 操作
有时为了性能,DMA 缓冲区仍配置为 Cacheable,此时必须手动维护一致性。
-
CPU 写 → DMA 读:在启动 DMA 前执行
SCB_CleanDCache_by_Addr(),将 Cache 数据写回内存。 -
DMA 写 → CPU 读:在 DMA 完成后执行
SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中的旧数据。
// 发送前:清理 D-Cache
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));
// 接收完成回调中:无效化 D-Cache
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
// 现在可以安全读取 rx_buffer
}
三、完整实战示例:UART DMA 收发
以下代码展示在 Cacheable 内存下,如何正确使用 Clean/Invalidate。
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
void UART_DMA_Init(void)
{
// 填充发送数据
for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;
// 1. 清理 D-Cache,确保内存中数据最新
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, BUF_SIZE);
// 2. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, BUF_SIZE);
// 3. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 4. 无效化 D-Cache,丢弃旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
// 5. 安全读取
for (int i = 0; i < BUF_SIZE; i++) {
printf("%02X ", rx_buf[i]);
}
}
四、避坑指南:常见错误与注意事项
- 地址未对齐:Clean/Invalidate 操作要求地址 32 字节对齐,长度也建议 32 字节倍数。否则可能触发 HardFault 或遗漏数据。
- Invalidate 前未 Clean:若缓冲区部分数据在 Cache 中已修改但未写回,直接 Invalidate 会丢失修改。正确顺序:先 Clean 再 Invalidate。
- DMA 传输中操作 Cache:DMA 正在访问内存时执行 Clean/Invalidate 会导致数据错乱。务必在 DMA 完成中断或传输启动前操作。
-
MPU 配置遗漏:忘记配置 MPU 或区域大小不匹配,导致 Cache 属性未生效。建议用
SCB_GetCacheLevel()确认。 - 多缓冲区共享 Cache 行:若两个变量位于同一 32 字节 Cache 行,Invalidate 一个可能影响另一个。务必按 32 字节对齐隔离。
- 中断优先级:DMA 完成中断优先级应低于或等于调用 Clean/Invalidate 的任务,避免竞态。
五、总结
STM32H7 的 Cache 与 DMA 一致性没有银弹。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单且零开销。若必须使用 Cache,则严格遵守“写前 Clean,读后 Invalidate”原则,并注意对齐与操作时机。掌握这些技巧,你的嵌入式系统将既快又稳。