一、为什么 H7 的 Cache 会成为 DMA 的“隐形杀手”
STM32H7 基于 Cortex-M7,主频可达 480MHz,为了弥补内核与存储器之间的速度差,芯片内部集成了 L1 Cache(I-Cache 和 D-Cache)。D-Cache 默认是关闭的,但一旦开启,CPU 访问内存时会先查缓存,这就带来了经典问题:
- CPU 写数据到内存:数据可能只停留在 D-Cache 中,并未真正写入 SRAM。此时启动 DMA 发送,DMA 从 SRAM 读到的还是旧数据。
- DMA 写数据到内存:DMA 直接修改 SRAM,但 CPU 之前可能已经缓存了该区域,后续 CPU 读取时命中旧缓存,看不到 DMA 写入的新数据。
这就是 Cache 一致性(Coherency) 问题。STM32H7 没有硬件自动维护一致性的机制(如 ACP 端口),必须靠软件处理。
二、解决思路:MPU 配置 + Cache 维护
主要有两种手段,通常结合使用:
- MPU 配置:将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable 或 Write-Through,从根源上避免缓存不一致。
-
Cache 维护操作:在 DMA 传输前后调用
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr()手动同步。
对于频繁 DMA 传输的大块数据,推荐用 MPU 将缓冲区设为 Non-Cacheable,简单可靠;对于偶尔传输的小数据,可用 Cache 维护函数。
三、MPU 配置实战
3.1 确定内存区域
假设我们使用 D2 SRAM(地址 0x30000000)作为 DMA 缓冲区。先定义区域大小,例如 32KB。
3.2 配置 MPU
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 D2 SRAM 区域为 Non-Cacheable, Non-Shared
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
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);
}
注意:BaseAddress 和 Size 必须对齐,32KB 区域要求基址 32KB 对齐。
3.3 在 main 中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 必须在使能 Cache 之前配置
SCB_EnableICache();
SCB_EnableDCache();
// ... 其他初始化
}
四、Cache 维护函数的使用
如果不想用 MPU,或者缓冲区是动态分配的,可以在 DMA 传输前后手动维护:
// 发送前:将 Cache 中的数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)buffer, length);
// 接收前:无效化 Cache,确保 CPU 读取时从 SRAM 重新加载
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, length);
// 接收后:如果 CPU 要读数据,先无效化再读
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, length);
关键点:length 必须是 32 字节的整数倍(Cache Line 大小),且地址要 32 字节对齐,否则可能误伤相邻数据。
五、完整示例:UART DMA 接收
#define RX_BUFFER_SIZE 128
__attribute__((aligned(32))) uint8_t rxBuffer[RX_BUFFER_SIZE];
void UART_DMA_Init(void)
{
// 假设已初始化 UART 和 DMA
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 无效化 Cache,确保读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuffer, RX_BUFFER_SIZE);
// 处理数据...
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}
}
如果 rxBuffer 已通过 MPU 配置为 Non-Cacheable,则无需调用 SCB_InvalidateDCache_by_Addr。
六、常见失效现象与排查清单
- 数据错乱:DMA 发送的数据部分正确、部分错误,通常是 Cache 未 Clean。
- 偶发死机:DMA 写入时覆盖了 Cache 中的其他变量,导致指针异常。
- 性能骤降:将整个 SRAM 都设为 Non-Cacheable,CPU 访问变慢。应只对 DMA 缓冲区区域做配置。
-
MPU 配置无效:忘记调用
HAL_MPU_Enable或区域重叠导致优先级错误。
排查步骤:
- 确认 D-Cache 是否开启(
SCB->CCR的 bit16)。 - 检查 DMA 缓冲区地址是否在 MPU 配置的 Non-Cacheable 区域内。
- 若使用 Cache 维护函数,检查地址和长度是否 32 字节对齐。
- 用调试器查看内存实际值,对比 CPU 读到的值。
- 临时关闭 D-Cache 验证问题是否消失。
七、注意事项
- MPU 配置必须在使能 Cache 之前完成,否则可能不生效。
- 多个 MPU 区域重叠时,编号小的优先级高,注意规划。
- 使用
SCB_InvalidateDCache_by_Addr前,确保该区域没有未写回的数据,否则会丢失。 - DMA 描述符、控制结构体等也应放在 Non-Cacheable 区域或手动维护。
- 对于以太网、USB 等高速外设,强烈建议用 MPU 将缓冲区设为 Non-Cacheable。
掌握这些,你就能在 STM32H7 上安心使用 Cache 和 DMA,既享受高性能,又避免数据一致性的坑。