STM32H7 的 L1 Cache 与 DMA 数据一致性:地址对齐、Clean/Invalidate 顺序与常见踩坑
1. 为什么 Cache 会与 DMA 冲突?
STM32H7 基于 Cortex-M7 内核,配备 L1 Cache(I-Cache 和 D-Cache)。D-Cache 作为 CPU 与主存之间的高速缓冲,会缓存最近访问的数据。当 CPU 写数据时,若命中 Cache,数据仅写入 Cache 而未同步到主存;当 CPU 读数据时,若命中 Cache,则直接返回 Cache 中的旧数据。
DMA 控制器直接访问主存(SRAM、SDRAM 等),不经过 Cache。因此:
- CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 中,DMA 读到的是主存中的旧数据。
- DMA 写 → CPU 读:DMA 将新数据写入主存,但 CPU 可能从 Cache 中读到旧数据。
这就是数据一致性问题。解决手段是 Clean(将 Cache 写回主存)和 Invalidate(将 Cache 行标记为无效,强制下次从主存读取)。
2. 地址对齐:Cache 行与 DMA 边界
Cortex-M7 的 D-Cache 行大小为 32 字节。Clean/Invalidate 操作以 Cache 行为单位。如果 DMA 缓冲区未按 32 字节对齐,或长度不是 32 的整数倍,那么操作会波及相邻数据,导致意外覆盖或丢失。
关键原则:
- DMA 缓冲区起始地址必须 32 字节对齐。
- 缓冲区大小建议为 32 字节的整数倍。
- 若无法满足,需使用
SCB_InvalidateDCache_by_Addr等函数时传入对齐后的地址和长度,但可能影响相邻变量。
对齐示例:
// 错误:未对齐
uint8_t dma_buf[100];
// 正确:32 字节对齐,大小为 32 的倍数
__attribute__((aligned(32))) uint8_t dma_buf[128];
3. Clean 与 Invalidate 的顺序
根据 DMA 传输方向,操作顺序不同:
3.1 CPU 写 → DMA 读(发送)
- CPU 填充缓冲区。
- Clean D-Cache:将缓冲区数据写回主存。
- 启动 DMA 发送。
__attribute__((aligned(32))) uint8_t tx_buf[128];
// 填充数据
fill_tx_buffer(tx_buf, 128);
// Clean D-Cache
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, 128);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, 128);
3.2 DMA 写 → CPU 读(接收)
- Invalidate D-Cache:丢弃缓冲区对应的 Cache 行(防止 CPU 读到旧数据)。
- 启动 DMA 接收。
- DMA 完成中断中,再次 Invalidate D-Cache(因为 DMA 期间 CPU 可能预取或访问了该区域)。
- CPU 读取数据。
__attribute__((aligned(32))) uint8_t rx_buf[128];
// 启动接收前 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, 128);
HAL_UART_Receive_DMA(&huart1, rx_buf, 128);
// DMA 完成回调中再次 Invalidate
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, 128);
// 现在可以安全读取 rx_buf
}
注意:Invalidate 操作会丢弃 Cache 中未写回的数据。如果 CPU 在 DMA 期间修改了缓冲区,这些修改会丢失。因此,DMA 接收缓冲区在传输期间不应被 CPU 写入。
4. 完整配置步骤
4.1 启用 D-Cache
在 main() 初始化阶段启用:
void enable_dcache(void) {
SCB_EnableICache();
SCB_EnableDCache();
}
4.2 配置 MPU(可选但推荐)
对于 DMA 缓冲区,可配置 MPU 将区域设为 Non-Cacheable,从而避免手动 Clean/Invalidate。但会降低 CPU 访问性能。
void mpu_config_dma_region(void) {
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
4.3 使用 Cache 维护函数
CMSIS 提供以下函数:
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize) -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize) -
SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)
注意:addr 必须 32 字节对齐,dsize 为字节数。
5. 常见踩坑与注意事项
- 坑 1:忘记 Clean 导致 DMA 发送旧数据。CPU 写后必须 Clean,否则 DMA 读到主存旧值。
- 坑 2:Invalidate 顺序错误。接收前未 Invalidate,CPU 可能从 Cache 读旧数据;接收后未再次 Invalidate,DMA 期间 CPU 可能预取导致数据不一致。
- 坑 3:缓冲区未对齐。导致 Clean/Invalidate 影响相邻变量,引发随机错误。
- 坑 4:在 DMA 传输期间 CPU 访问缓冲区。若 CPU 写,Invalidate 会丢失写入;若 CPU 读,可能读到不完整数据。应使用双缓冲或标志位同步。
-
坑 5:使用
SCB_InvalidateDCache()全局操作。会清空整个 D-Cache,性能极差,且可能丢失其他数据。务必使用by_Addr版本。 -
坑 6:忽略写缓冲。Cortex-M7 有写缓冲,Clean 后仍需
DSB指令确保完成。CMSIS 函数内部已包含屏障。 - 坑 7:MPU 配置错误。若将 DMA 区域设为 Cacheable 但未正确维护,问题依旧。
最佳实践:
- 为 DMA 缓冲区使用
__attribute__((aligned(32)))。 - 发送前 Clean,接收前和接收后 Invalidate。
- 考虑使用 Non-Cacheable 内存区域存放 DMA 缓冲区,简化软件设计。
- 使用
SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr而非全局操作。
6. 总结
STM32H7 的 L1 Cache 与 DMA 数据一致性是嵌入式开发中的经典难题。核心在于理解 Cache 行、地址对齐以及 Clean/Invalidate 的时机。遵循本文的配置步骤和注意事项,可有效避免数据错乱,充分发挥 H7 的高性能优势。