STM32H7 D-Cache与DMA一致性实战:用MPU精准配置回写区间,告别全片Clean/Invalidate
1. 为什么D-Cache会让DMA“失灵”?
STM32H7主频高达480MHz,为了匹配CPU速度,芯片内置了D-Cache。CPU访问数据时,会先将数据从SRAM加载到Cache,后续读写都直接操作Cache,而不是SRAM。
DMA则不同,它直接访问SRAM,不经过Cache。这就导致了经典的一致性问题:
- CPU写,DMA读:CPU写入的数据可能还在Cache中(Write-Back模式),SRAM里还是旧数据。DMA从SRAM读,拿到的是旧数据。
- DMA写,CPU读:DMA将新数据写入SRAM,但CPU的Cache中可能还保留着该地址的旧数据。CPU读时直接命中Cache,读到旧数据。
解决思路有两种:
- 全片Clean/Invalidate:简单粗暴,但每次DMA传输都操作整个Cache,耗时且可能影响其他任务的数据。
- MPU配置特定区域为Write-Through或Non-Cacheable:精准控制,只对DMA缓冲区生效,效率高。
本文重点介绍第二种方法,并给出可落地的代码。
2. MPU配置:将DMA缓冲区设为回写区间
MPU(Memory Protection Unit)可以定义内存区域的属性。对于DMA缓冲区,我们通常有两种选择:
- Non-Cacheable:CPU访问不经过Cache,彻底避免一致性问题,但会降低CPU访问速度。
- Write-Through:CPU写操作同时更新Cache和SRAM,DMA读能拿到最新数据;但DMA写后,CPU读仍可能命中旧Cache,需要Invalidate。
更推荐的做法是:将DMA缓冲区配置为Write-Back,但在DMA传输前后手动执行Clean/Invalidate,且只针对该缓冲区。这样兼顾性能与安全。
下面以STM32H7为例,使用HAL库配置MPU。
2.1 定义MPU区域
假设我们使用0x30000000开始的SRAM区域(D2域SRAM1~3)作为DMA缓冲区,大小32KB。
#include "stm32h7xx_hal.h"
#define DMA_BUFFER_ADDR 0x30000000
#define DMA_BUFFER_SIZE 0x8000 // 32KB
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置DMA缓冲区区域:Write-Back, Read/Write allocate
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
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_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 其他区域保持默认(通常为Write-Back)
// ...
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
关键点:
-
IsCacheable = MPU_ACCESS_CACHEABLE和IsBufferable = MPU_ACCESS_BUFFERABLE组合为Write-Back。 -
TypeExtField = MPU_TEX_LEVEL0表示普通内存属性。 - 区域大小根据实际缓冲区调整,务必对齐。
2.2 使能Cache
在main()中初始化后使能D-Cache:
SCB_EnableICache();
SCB_EnableDCache();
3. DMA传输前后的精准Cache维护
配置好MPU后,DMA缓冲区仍可能被缓存。因此,在DMA传输前后必须执行Clean/Invalidate,但只针对该缓冲区。
3.1 CPU写 → DMA读(发送)
CPU填充缓冲区后,需要将Cache中的数据写回SRAM,即Clean操作。
void DMA_SendData(uint8_t *src, uint32_t len)
{
// 确保数据在SRAM中
SCB_CleanDCache_by_Addr((uint32_t *)src, len);
// 启动DMA传输
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dest, len);
}
3.2 DMA写 → CPU读(接收)
DMA传输完成后,CPU要读取新数据,必须先Invalidate对应Cache行,否则可能读到旧数据。
void DMA_ReceiveData(uint8_t *dst, uint32_t len)
{
// 启动DMA接收
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, len);
// 等待传输完成(中断或轮询)
// ...
// 无效化Cache,确保CPU读取的是SRAM中的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dst, len);
}
注意:SCB_InvalidateDCache_by_Addr的地址必须32字节对齐,长度建议为32的倍数。如果缓冲区未对齐,需先对齐处理。
4. 完整示例:DMA双缓冲串口接收
以UART空闲中断+DMA接收为例,展示如何安全使用Cache。
#define RX_BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t rxBuffer[RX_BUFFER_SIZE];
void UART_DMA_Init(void)
{
// 配置DMA为循环模式,接收串口数据
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}
// 空闲中断回调
void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 停止DMA获取已接收长度
HAL_UART_DMAStop(huart);
uint16_t len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// 无效化Cache,确保读取到最新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuffer, len);
// 处理数据
ProcessData(rxBuffer, len);
// 重新启动DMA
HAL_UART_Receive_DMA(huart, rxBuffer, RX_BUFFER_SIZE);
}
}
5. 注意事项与避坑指南
-
地址对齐:Cache操作要求地址32字节对齐,长度也应为32的倍数。使用
__attribute__((aligned(32)))定义缓冲区。 -
避免全片操作:
SCB_CleanDCache()和SCB_InvalidateDCache()会影响整个Cache,可能导致其他任务数据丢失,务必使用_by_Addr版本。 - DMA传输期间禁止CPU访问:如果CPU在DMA传输过程中访问了缓冲区,可能引发Cache与SRAM不一致。建议使用双缓冲或信号量保护。
- MPU配置顺序:先配置MPU,再使能Cache。修改MPU后需要重新使能Cache。
- 性能权衡:Non-Cacheable区域CPU访问慢,但无需维护;Write-Back性能高,但需手动维护。根据数据量选择。
- 调试技巧:如果发现数据错乱,可临时将缓冲区设为Non-Cacheable验证是否为一致性问题。
6. 总结
STM32H7的D-Cache与DMA一致性并非无解。通过MPU将DMA缓冲区配置为Write-Back,并在传输前后精准执行Clean/Invalidate,既能发挥Cache的性能优势,又能保证数据正确。记住三个关键:对齐、按地址操作、避免全片。掌握这些,你就能在H7上放心使用DMA了。