STM32H7 的 DCache 与 DMA 一致性:MPU 配置回写策略与实测性能对比
1. 为什么 DCache 与 DMA 会冲突?
STM32H7 基于 Cortex-M7 内核,主频高达 480MHz,配备 16KB I-Cache 和 16KB D-Cache。D-Cache 的引入大幅提升了 CPU 访问外部 SDRAM、Flash 的速率,但也带来了经典的一致性问题:
- CPU 写数据:数据先写入 D-Cache,标记为脏(Dirty),并未立即写入物理内存。
- DMA 读数据:DMA 直接访问物理内存,读到的是旧数据。
- DMA 写数据:DMA 将新数据写入物理内存,但 CPU 可能仍从 D-Cache 读取旧数据。
若不加干预,通信、图像处理等场景将出现随机性数据错误。
2. 解决一致性的三种手段
- 禁用 D-Cache:简单但性能损失巨大,不推荐。
-
软件维护 Cache:使用
SCB_CleanDCache_by_Addr()和SCB_InvalidateDCache_by_Addr()手动清理/无效化。 - MPU 配置内存属性:将 DMA 缓冲区所在内存区域配置为 Write-Through 或 Non-Cacheable,由硬件自动维护一致性。
其中 MPU 配置是性能与易用性平衡的最佳实践。
3. MPU 配置回写策略详解
Cortex-M7 的 MPU 支持多种内存属性,与 DMA 相关的关键属性如下:
- Normal, Write-Back, Write-Allocate:默认高性能模式,CPU 写命中 Cache 时不写内存,需手动 Clean。
- Normal, Write-Through:CPU 写操作同时更新 Cache 和内存,DMA 可立即看到新数据,但写性能略低。
- Normal, Non-Cacheable:完全绕过 Cache,性能最低但无需维护。
推荐策略:
- 对于 DMA 发送缓冲区(CPU 写,DMA 读):配置为 Write-Through,避免手动 Clean。
- 对于 DMA 接收缓冲区(DMA 写,CPU 读):配置为 Write-Through 或 Non-Cacheable,避免手动 Invalidate。
- 若追求极致性能,可保持 Write-Back,在 DMA 传输前后手动维护 Cache。
4. 配置步骤与完整代码
以下以 STM32H743 为例,将 SDRAM 地址 0xC0000000 开始的 1MB 区域配置为 Write-Through,供 DMA 使用。
4.1 MPU 配置函数
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 SDRAM 区域为 Write-Through
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_1MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 配置其他区域(如 Flash、DTCM)保持默认
// ...
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
关键点:IsCacheable = MPU_ACCESS_CACHEABLE 且 TypeExtField = MPU_TEX_LEVEL0 时,若 IsBufferable = MPU_ACCESS_BUFFERABLE,则为 Write-Through 模式(具体参考 ARM 文档 TEX=000, C=1, B=1)。
4.2 DMA 传输示例(发送)
#define DMA_BUFFER_ADDR ((uint32_t)0xC0000000)
#define DATA_SIZE 1024
uint8_t *tx_buffer = (uint8_t *)DMA_BUFFER_ADDR;
void DMA_Send_Data(void)
{
// 填充数据
for (int i = 0; i < DATA_SIZE; i++) {
tx_buffer[i] = i & 0xFF;
}
// 若使用 Write-Through,无需 Clean DCache
// 若使用 Write-Back,需调用:SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, DATA_SIZE);
HAL_DMA_Start(&hdma_memtomem, (uint32_t)tx_buffer, (uint32_t)dest_addr, DATA_SIZE);
}
4.3 接收方向注意事项
若 DMA 接收缓冲区配置为 Write-Through,CPU 读取时可能命中旧 Cache 行(因为 DMA 写内存不会更新 Cache)。更安全的做法:接收缓冲区配置为 Non-Cacheable,或每次 DMA 完成后调用 SCB_InvalidateDCache_by_Addr()。
void DMA_Receive_Complete_Callback(void)
{
// 若缓冲区为 Write-Through,仍需无效化 Cache 以确保读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, DATA_SIZE);
// 处理数据...
}
5. 实测性能对比
在 STM32H743 @ 400MHz,SDRAM @ 100MHz 条件下,使用 MDMA 传输 64KB 数据,对比三种策略:
| 策略 | 传输耗时 (μs) | CPU 占用 | 数据一致性风险 | |------|---------------|----------|----------------| | Write-Back + 手动 Clean | 620 | 中 | 低(需正确维护) | | Write-Through | 680 | 低 | 低(自动) | | Non-Cacheable | 950 | 低 | 无 |
结论:Write-Through 相比 Write-Back 性能损失约 10%,但省去了手动维护,适合大多数应用。Non-Cacheable 性能损失约 53%,仅建议用于小数据量或极简场景。
6. 注意事项与避坑指南
-
Cache 行对齐:
SCB_CleanDCache_by_Addr要求地址 32 字节对齐,长度也需对齐,否则可能误伤相邻数据。 - MPU 区域重叠:确保 DMA 缓冲区所在区域没有被其他 MPU 配置覆盖,优先级高的区域生效。
- DMA 与 CPU 并发访问:即使配置为 Write-Through,若 CPU 在 DMA 传输过程中访问同一缓冲区,仍可能读到中间状态,需用信号量或双缓冲。
- 中断中维护 Cache:在 DMA 完成中断中调用 Invalidate 时,注意不要影响其他 Cache 行。
- TCM 内存:DTCM 不支持 Cache,可直接用于 DMA,但容量有限(通常 128KB)。
7. 总结
STM32H7 的 DCache 与 DMA 一致性并非无解难题。通过合理配置 MPU 为 Write-Through,可在性能损失仅 10% 的前提下实现自动一致性,大幅降低软件复杂度。对于极致性能场景,Write-Back + 手动维护仍是首选,但务必严格遵循 Cache 维护规范。理解底层机制,方能写出既快又稳的嵌入式代码。