一、问题现象:偶发的数据错乱

项目使用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写过。标准流程:

  1. DMA写入前:Clean缓冲区(若CPU可能写过)。
  2. 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维护封装成宏或函数,减少误用。