一、问题现象:DMA 数据为何“时好时坏”?
在 STM32H7 上使用 DMA 搬运 ADC 采样数据或串口收发时,常遇到以下现象:
- 数据偶尔错位、丢失或出现旧值。
- 调试器查看内存正确,但程序读取错误。
- 关闭 D-Cache 后一切正常。
根本原因:Cortex-M7 的 D-Cache 与 DMA 控制器访问内存的路径不同。CPU 通过 Cache 访问,DMA 直接访问物理内存。若 Cache 中有未回写的数据,DMA 读到的是旧数据;若 DMA 更新了内存,而 Cache 中仍有旧副本,CPU 读到的也是旧数据。
二、原理:Cache 一致性与维护操作
Cortex-M7 的 D-Cache 采用**写回(Write-Back)**策略,数据修改后不会立即写入物理内存,而是标记为“脏”。DMA 传输前后必须进行 Cache 维护:
- Clean(清理):将 Cache 中的脏数据写回物理内存。用于 CPU 写、DMA 读 的场景(如发送缓冲区)。
- Invalidate(无效化):丢弃 Cache 中的内容,强制下次读取从物理内存加载。用于 DMA 写、CPU 读 的场景(如接收缓冲区)。
- Clean & Invalidate:两者结合,用于双向传输。
CMSIS 提供了对应函数:
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);
注意:addr 必须 32 字节对齐,dsize 为字节数。
三、配置步骤:手动维护 Cache 的完整流程
以 UART DMA 接收为例,假设缓冲区 uint8_t rx_buf[64] 位于 0x24000000(AXI SRAM)。
步骤 1:初始化 DMA 和 UART
// 使能 DMA1 时钟和 UART4 时钟
__HAL_RCC_DMA1_CLK_ENABLE();
__HAL_RCC_UART4_CLK_ENABLE();
// 配置 DMA 为循环模式,目标地址为 rx_buf
hdma_uart_rx.Instance = DMA1_Stream0;
hdma_uart_rx.Init.Request = DMA_REQUEST_UART4_RX;
hdma_uart_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_uart_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_uart_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_uart_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_uart_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_uart_rx.Init.Mode = DMA_CIRCULAR;
hdma_uart_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_uart_rx);
// 关联 DMA 到 UART
__HAL_LINKDMA(&huart4, hdmarx, hdma_uart_rx);
步骤 2:启动 DMA 前无效化 Cache
// 确保 CPU 不会从 Cache 读取旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
HAL_UART_Receive_DMA(&huart4, rx_buf, sizeof(rx_buf));
步骤 3:DMA 完成中断中再次无效化
void DMA1_Stream0_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_uart_rx);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 数据已由 DMA 写入物理内存,但 Cache 可能仍有旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
// 现在可以安全读取 rx_buf
process_data(rx_buf, sizeof(rx_buf));
}
步骤 4:发送场景(CPU 写,DMA 读)
// 填充发送缓冲区
memcpy(tx_buf, data, len);
// 清理 Cache,确保 DMA 读到最新数据
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
HAL_UART_Transmit_DMA(&huart4, tx_buf, len);
四、进阶:使用 MPU 配置内存属性
手动维护 Cache 容易遗漏,且频繁 Clean/Invalidate 影响性能。更优雅的方案是利用 MPU(内存保护单元) 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through。
配置步骤:
- 在链接脚本中划分一块专用内存区域(如
0x30000000的 SRAM4)。 - 在
main()初始化时配置 MPU。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 SRAM4 区域为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
之后将 DMA 缓冲区定义到该区域:
__attribute__((section(".sram4"))) uint8_t dma_buffer[256];
这样 CPU 和 DMA 访问该内存时都不经过 Cache,无需手动维护。
五、注意事项与避坑指南
-
对齐问题:
SCB_*DCache_by_Addr要求地址 32 字节对齐,长度建议为 32 的倍数。否则可能误伤相邻数据。 - 中断中调用:Cache 维护函数执行时间较长,避免在高速中断中频繁调用。
- 多缓冲区:若使用多个 DMA 缓冲区,确保每个缓冲区独立维护,不要跨区域操作。
- DMA 描述符:若使用链表模式,描述符本身也需注意 Cache 一致性。
- 调试影响:调试器可能绕过 Cache 直接访问内存,导致“调试正常、运行异常”。
- 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,仅建议用于 DMA 缓冲区,不要全局关闭 Cache。
六、总结
STM32H7 的 D-Cache 与 DMA 冲突本质是数据一致性问题。手动维护 Cache 适合小规模、低频传输;MPU 配置 Non-Cacheable 区域则一劳永逸,适合高频、多缓冲场景。理解 SCB_CleanDCache 与 SCB_InvalidateDCache 的语义,结合 MPU 灵活运用,才能充分发挥 H7 的性能优势。