一、问题现象:偶发的数据错乱
项目使用STM32H743,通过UART+DMA接收上位机指令,帧长固定32字节。测试时发现:每接收几十帧,就会有一帧数据部分字节错误,错误位置随机,且重新上电后概率变化。用调试器单步运行或关闭D-Cache时问题消失。
初步判断与Cache有关,但代码中已经调用了SCB_InvalidateDCache_by_Addr(),为何还会错乱?
二、原理回顾:D-Cache与DMA为何冲突
- D-Cache:CPU访问内存时,数据会被缓存到D-Cache。写操作可能只更新Cache,未写回SRAM(Write-Back模式)。
- DMA:直接访问SRAM,不经过D-Cache。若CPU已缓存了某块内存,DMA写入新数据后,CPU读到的仍是旧Cache内容。
-
一致性维护:
- Clean:将Cache中已修改的数据写回SRAM,防止DMA读到旧数据。
- Invalidate:将Cache中对应行标记为无效,下次CPU读取时从SRAM重新加载,防止CPU读到旧数据。
关键点:Invalidate操作会丢弃Cache中未写回的数据!若在DMA写入前Invalidate,而CPU之前有未Clean的写操作,数据将永久丢失。
三、排查过程:定位误用点
3.1 原始错误代码
// DMA接收完成回调中
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 错误:先Invalidate,再处理数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuffer, RX_LEN);
processData(rxBuffer);
// 重新启动DMA接收
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_LEN);
}
看似合理:DMA写入了新数据,Invalidate后CPU读取最新值。但问题在于:rxBuffer可能被CPU写过(如上一帧处理后修改了某些字节),这些修改还在Cache中未写回。此时Invalidate会直接丢弃这些修改,导致DMA写入的新数据与旧数据混合,出现随机错误。
3.2 正确做法
在启动DMA接收之前,必须确保Cache中对应区域的数据已写回SRAM(Clean),或者该区域从未被CPU写过。标准流程:
- DMA写入前:Clean缓冲区(若CPU可能写过)。
- DMA写入后:Invalidate缓冲区(确保CPU读取最新)。
但注意:Clean和Invalidate的顺序不能颠倒。对于接收方向:
// 启动DMA接收前
SCB_CleanDCache_by_Addr((uint32_t*)rxBuffer, RX_LEN);
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_LEN);
// DMA完成回调中
SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuffer, RX_LEN);
processData(rxBuffer);
四、完整修正代码
#define RX_LEN 32
__attribute__((aligned(32))) uint8_t rxBuffer[RX_LEN]; // 32字节对齐
void startDmaReceive(void)
{
// 确保CPU对缓冲区的修改已写回SRAM
SCB_CleanDCache_by_Addr((uint32_t*)rxBuffer, RX_LEN);
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_LEN);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 使Cache无效,强制CPU从SRAM读取DMA写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuffer, RX_LEN);
processData(rxBuffer);
startDmaReceive(); // 重新启动
}
五、注意事项与避坑指南
-
对齐要求:
SCB_*DCache_by_Addr要求地址32字节对齐,长度建议为32的整数倍。使用__attribute__((aligned(32)))。 - 发送方向:DMA发送前必须Clean,因为CPU写数据可能还在Cache中。发送后无需Invalidate。
- 避免频繁操作:Clean/Invalidate按Cache行(32字节)操作,小缓冲区可合并处理。
- MPU配置:可将DMA缓冲区配置为Write-Through或Non-Cacheable,一劳永逸,但会牺牲性能。
-
调试陷阱:调试器可能自动Clean/Invalidate,导致问题隐藏。建议用
__DSB()和__ISB()确保操作完成。 - RTOS环境:若使用RTOS,确保在任务切换或中断中正确维护Cache,避免跨任务污染。
六、总结
STM32H7的D-Cache与DMA共存时,数据一致性必须由软件维护。核心原则:
- DMA写内存前:CPU若写过该区域,先Clean。
- DMA写内存后:CPU读之前,先Invalidate。
- DMA读内存前:CPU若写过该区域,先Clean。
- DMA读内存后:无需Invalidate。
遵循上述规则,即可根治偶发数据错乱。建议将Cache维护封装成宏或函数,减少误用。