STM32H7 的 L1 Cache 与 DMA 数据一致性:从 Cache 维护操作到 MPU 配置的完整避坑指南
1. 为什么 Cache 和 DMA 会打架?
STM32H7 基于 Cortex-M7 内核,主频高达 480MHz,为了弥补 CPU 与存储器之间的速度鸿沟,芯片内部集成了 L1 Cache(I-Cache 和 D-Cache)。D-Cache 默认关闭,一旦开启,CPU 访问 SRAM 或外部 SDRAM 时,数据会先被缓存到 Cache Line(通常 32 字节)中。
DMA 控制器则绕过 Cache,直接读写物理内存。这就导致两个经典问题:
- DMA 读取外设数据到内存:DMA 把新数据写入 SRAM,但 CPU 可能仍从 D-Cache 中读取旧数据(Cache 未失效)。
- CPU 写入内存后启动 DMA 发送:CPU 写入的数据可能还在 D-Cache 中未写回 SRAM,DMA 却从 SRAM 读到了旧数据。
根本原因:Cache 与 DMA 对内存的访问不同步。
2. 三种解决思路对比
| 方法 | 原理 | 优点 | 缺点 | |------|------|------|------| | Cache 维护操作 | 手动 Clean/Invalidate | 灵活,按需使用 | 易遗漏,性能损耗 | | MPU 配置 Write-Through | 写操作同时更新 Cache 和内存 | 自动一致,性能较好 | 读操作仍需注意 | | MPU 配置 Non-Cacheable | 该区域完全不使用 Cache | 彻底避免一致性问题 | 牺牲性能 |
推荐策略:对 DMA 缓冲区使用 MPU 配置为 Non-Cacheable 或 Write-Through,既简单又可靠。若必须使用 Cacheable 内存,则严格配合 Clean/Invalidate 操作。
3. Cache 维护操作详解
Cortex-M7 提供了以下 CMSIS 函数:
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中已修改的数据写回内存。 -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 内容,强制从内存重新加载。 -
SCB_CleanInvalidateDCache_by_Addr():先 Clean 再 Invalidate。
使用场景:
-
CPU 写 → DMA 读(发送):调用
SCB_CleanDCache_by_Addr(),确保数据写入内存。 -
DMA 写 → CPU 读(接收):调用
SCB_InvalidateDCache_by_Addr(),丢弃旧 Cache,读取新数据。
注意:地址必须 32 字节对齐,长度需为 32 字节的整数倍,否则可能影响相邻数据。
4. MPU 配置实战
MPU(内存保护单元)可将特定内存区域设为 Non-Cacheable 或 Write-Through。以 STM32H7 的 SRAM1 区域(0x30000000)为例,配置为 Non-Cacheable:
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域 0:SRAM1 起始 0x30000000,大小 128KB,Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
MPU_InitStruct.SubRegionDisable = 0x0;
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);
}
若希望保留一定性能,可配置为 Write-Through:
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; // Write-Through, no write allocate
关键点:MPU 区域必须按 2 的幂次对齐,且大小不能重叠。配置后需调用 SCB_EnableDCache() 使能 D-Cache。
5. 完整代码示例:UART DMA 接收
假设使用 UART4 接收不定长数据到缓冲区 rx_buf[256],该缓冲区位于 Non-Cacheable 区域(0x30000000)。
#define RX_BUF_SIZE 256
__attribute__((section(".non_cacheable"))) uint8_t rx_buf[RX_BUF_SIZE];
void UART_DMA_Init(void)
{
// 1. 配置 MPU(略,见上文)
MPU_Config();
// 2. 使能 D-Cache
SCB_EnableICache();
SCB_EnableDCache();
// 3. 初始化 UART4 和 DMA
// ... HAL_UART_Init, HAL_DMA_Init ...
// 4. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart4, rx_buf, RX_BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == UART4) {
// 由于 rx_buf 是 Non-Cacheable,CPU 直接读取内存,无需 Invalidate
process_data(rx_buf, RX_BUF_SIZE);
HAL_UART_Receive_DMA(&huart4, rx_buf, RX_BUF_SIZE); // 重新启动
}
}
若缓冲区位于 Cacheable 区域,则回调中必须调用:
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
6. 避坑指南
- 地址对齐:Cache 维护操作地址必须 32 字节对齐,长度建议为 32 的倍数。
- DMA 描述符:若使用链表模式,描述符本身也需放在 Non-Cacheable 区域。
- 中断与 Cache:在中断中调用 Cache 维护函数可能影响实时性,尽量在 DMA 完成回调中处理。
- 多缓冲区:使用双缓冲时,每个缓冲区都要独立维护 Cache。
- 调试陷阱:开启 D-Cache 后,通过调试器查看内存可能看到旧数据,需手动 Clean/Invalidate 或关闭 Cache 调试。
- 性能权衡:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区设为 Non-Cacheable,其他代码和数据仍使用 Cache。
7. 总结
STM32H7 的 L1 Cache 与 DMA 数据一致性问题是嵌入式开发中的高频“坑点”。核心解决方案有三:Cache 维护操作、MPU 配置 Write-Through、MPU 配置 Non-Cacheable。推荐对 DMA 缓冲区使用 MPU 配置为 Non-Cacheable,简单可靠;若追求性能,可配置为 Write-Through 并配合 Clean 操作。无论哪种方案,务必注意地址对齐和操作时机,才能确保数据万无一失。