STM32H7 D-Cache与DMA一致性实战:地址对齐、Cache Line与Clean/Invalidate踩坑全解析
一、为什么DMA与D-Cache会冲突?
STM32H7主频高达480MHz,为了匹配CPU速度,Cortex-M7内核配备了D-Cache(数据缓存)。CPU访问数据时,若命中Cache则直接读写Cache,不会立即同步到SRAM。而DMA是独立于CPU的控制器,它直接访问物理内存(SRAM)。当CPU写数据到Cache但未回写到SRAM时,DMA读取的是旧数据;反之,DMA更新了SRAM,但CPU读的是Cache中的旧副本。这就是Cache一致性问题。
关键概念:Cache Line
- Cortex-M7的D-Cache按行管理,每行32字节(STM32H7固定为32B)。
- 任何Clean/Invalidate操作都以Cache Line为单位,而非单个字节。
- 若操作地址未对齐到32字节边界,可能误伤相邻数据。
二、地址对齐:必须遵守的硬性规则
2.1 为什么要求32字节对齐?
假设DMA缓冲区起始地址为0x20000004,长度为16字节。该区域跨越了两个Cache Line(0x20000000~0x2000001F和0x20000020~0x2000003F)。执行SCB_InvalidateDCache_by_Addr时,硬件会无效化整个Cache Line,导致相邻的0x20000000~0x20000003数据也被丢弃(若CPU之前修改过,则丢失)。
2.2 正确做法
- DMA缓冲区必须32字节对齐,长度建议为32的整数倍。
- 使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏。
// 定义32字节对齐的DMA缓冲区
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];
三、Clean与Invalidate:何时用哪个?
| 操作 | 作用 | 使用场景 | |------|------|----------| | Clean | 将Cache中已修改的数据写回SRAM | CPU写数据后,DMA要读取(发送) | | Invalidate | 丢弃Cache内容,强制从SRAM重新加载 | DMA写数据后,CPU要读取(接收) | | Clean+Invalidate | 先写回再丢弃 | 不确定Cache状态时的安全操作 |
注意:Invalidate会丢弃未回写的数据!若CPU修改了缓冲区但未Clean,直接Invalidate将丢失修改。
四、完整配置步骤与代码示例
4.1 配置MPU(可选但推荐)
将DMA缓冲区所在内存区域配置为Write-Through, No Write Allocate或Non-Cacheable,可简化一致性管理。但会降低CPU访问性能。
// 将0x24000000开始的512KB区域配置为Non-Cacheable(示例)
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x24000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
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.2 发送流程(CPU→DMA)
// 1. CPU填充数据到dma_tx_buf
memcpy(dma_tx_buf, source, len);
// 2. Clean D-Cache,确保数据写入SRAM
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, len);
// 3. 启动DMA传输
HAL_DMA_Start(&hdma, (uint32_t)dma_tx_buf, (uint32_t)&UART->TDR, len);
4.3 接收流程(DMA→CPU)
// 1. 启动DMA接收前,Invalidate缓冲区(丢弃旧数据)
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, len);
// 2. 启动DMA
HAL_DMA_Start(&hdma, (uint32_t)&UART->RDR, (uint32_t)dma_rx_buf, len);
// 3. 等待DMA完成(中断或轮询)
while(!dma_done);
// 4. 再次Invalidate,确保CPU读取到DMA写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, len);
// 5. 处理数据
process_data(dma_rx_buf, len);
五、踩坑记录与注意事项
-
坑1:缓冲区未对齐。即使长度是32的倍数,起始地址非32对齐也会导致相邻数据被误清。务必使用
aligned(32)。 - 坑2:Invalidate前未Clean。若CPU修改了接收缓冲区(如解析时写入标志),再Invalidate会丢失修改。应先Clean再Invalidate,或避免在DMA区域内写。
- 坑3:DMA传输中操作Cache。DMA进行中执行Clean/Invalidate可能导致数据竞争。应在DMA启动前或完成后操作。
-
坑4:多缓冲区共享Cache Line。两个独立DMA缓冲区若位于同一Cache Line,操作一个会影响另一个。用
__attribute__((aligned(32)))隔离。 - 坑5:忘记配置MPU。默认内存属性可能为Write-Back,导致一致性更难管理。建议为DMA区域配置Non-Cacheable或Write-Through。
-
调试技巧:使用
SCB_InvalidateDCache()全局无效化(慎用,影响性能),或通过内存窗口对比SRAM与Cache内容。
六、总结
STM32H7的D-Cache与DMA一致性核心在于:对齐、Clean/Invalidate时机、MPU配置。牢记“CPU写后Clean,DMA写后Invalidate”,并确保缓冲区32字节对齐,即可避免99%的数据错乱问题。在性能与复杂度之间,若DMA数据量不大,直接配置Non-Cacheable区域是最简单的方案。