STM32 HAL库中DMA与Cache一致性问题的定位与规避实战

一、问题现象:为什么数据“不更新”

在STM32F7/H7等带D-Cache的MCU上,常见两类诡异现象:

  • 接收方向:DMA把串口/ADC数据写入SRAM,CPU读到的却是旧值或全0。
  • 发送方向:CPU填好缓冲区后启动DMA,总线上发出的却是旧数据或部分新数据。

根因是CPU与DMA看到的内存视图不一致。Cortex-M7的D-Cache按Cache Line(通常32字节)操作,CPU写数据可能只停留在Cache中(Write-Back),DMA直接访问物理SRAM自然读不到;反之DMA写入SRAM后,CPU若命中旧Cache行,也不会去内存取新值。

关键认知:DMA不经过Cache,它只认物理内存。

二、原理:Cache Line与三种维护操作

  • Clean(回写):把Cache中脏行写回SRAM,供DMA读取。用于发送前。
  • Invalidate(无效化):丢弃Cache行内容,强制CPU下次从SRAM取。用于接收后。
  • Clean+Invalidate:两者都做,常用于接收缓冲区复用前。

HAL库提供对应API(定义在stm32h7xx_hal.h等):

void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);

注意dsize单位为字节,且地址需按32字节对齐,长度建议向上取整到Cache Line倍数,否则会误伤相邻数据。

三、定位方法:三步确认是否命中Cache问题

  1. 关闭D-Cache复测:在main初始化后调用SCB_DisableDCache()(仅调试用),若问题消失,基本可判定为一致性问题。
  2. 观察地址对齐:打印DMA缓冲区地址,检查是否32字节对齐;非对齐缓冲区极易在Invalidate时误伤邻变量。
  3. 单步对比内存:用调试器Memory窗口直接看SRAM物理地址,与CPU变量值对比,二者不一致即为Cache未同步。

四、规避方案(按推荐度排序)

方案A:缓冲区放入Non-Cacheable区域(最稳)

利用链接脚本把DMA缓冲区放到MPU配置为Non-Cacheable的RAM段,彻底免维护。以H7为例,在.ld中新增段:

.dma_buffer (NOLOAD) :
{
  . = ALIGN(32);
  *(.dma_buffer)
  . = ALIGN(32);
} > RAM_D1

C代码中声明并对齐:

__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[256];

再用MPU把该区域设为MPU_ACCESS_NOT_CACHEABLE。此法零运行时开销,适合高频DMA。

方案B:手动维护Cache(改动最小)

适合已有工程快速修复。核心原则:发送前Clean,接收后Invalidate。

#define DMA_BUF_SIZE  256
#define CACHE_LINE    32

__attribute__((aligned(32)))
static uint8_t rx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32)))
static uint8_t tx_buf[DMA_BUF_SIZE];

/* 发送:CPU写完后Clean,再启动DMA */
void uart_dma_send(uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, DMA_BUF_SIZE);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

/* 接收完成回调:先Invalidate,再读数据 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, DMA_BUF_SIZE);
    process_data(rx_buf, DMA_BUF_SIZE);
    /* 重新挂接收前,先Clean+Invalidate,避免脏行回写覆盖新数据 */
    SCB_CleanInvalidateDCache_by_Addr((uint32_t *)rx_buf, DMA_BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, DMA_BUF_SIZE);
}

五、注意事项

  • 长度必须按Cache Line向上取整:Invalidate按行操作,长度不足会漏刷,过长会误伤相邻变量。
  • 接收回调里先Invalidate再读,顺序反了读到旧值。
  • 环形缓冲区要按“已写区间”精确维护,不能整块无脑Invalidate,否则可能丢弃CPU刚写入但未回写的数据。
  • DMA描述符、链表同样要对齐并维护,H7的MDMA尤其要注意。
  • 调试器访问内存可能触发Cache行为,定位时以物理地址为准。
  • 若使用HAL_UARTEx_ReceiveToIdle_DMA,在RxEventCallback中同样执行Invalidate。

六、小结

DMA与Cache一致性问题的本质是“两个主设备看同一块内存”。工程上优先用Non-Cacheable段从架构上消除问题;存量工程用Clean/Invalidate + 32字节对齐快速修复。牢记口诀:发前Clean,收后Invalidate,长度对齐Cache Line,即可规避绝大多数隐蔽Bug。