一、为什么STM32H7的DMA会与Cache冲突?
STM32H7 的 Cortex-M7 内核包含 D-Cache(数据缓存),默认关闭。开启后,CPU 读写内存会先经过 Cache,而 DMA 直接访问物理内存(SRAM/SDRAM)。当两者访问同一块缓冲区时,会出现:
- CPU 写数据到 Cache,但未写回内存 → DMA 读到旧数据。
- DMA 写入新数据到内存,但 CPU 读的是 Cache 中的旧数据 → CPU 读到旧值。
这就是 Cache 一致性(Coherency)问题。
二、MPU 配置:为 DMA 缓冲区设置正确的内存属性
STM32H7 的 MPU(Memory Protection Unit)可以按区域配置内存属性。对于 DMA 缓冲区,推荐两种策略:
- Write-Through, no write allocate:CPU 写立即透传到内存,读可缓存。适合 DMA 读、CPU 写的场景。
- Non-cacheable:完全绕过 Cache,最简单但性能有损失。
下面以将 0x30000000 开始的 64KB 区域配置为 Write-Through 为例。
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域0:DMA 缓冲区,地址 0x30000000,大小 64KB
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
关键点:
-
IsCacheable = MPU_ACCESS_CACHEABLE且IsBufferable = MPU_ACCESS_NOT_BUFFERABLE实现 Write-Through。 - 若想完全 Non-cacheable,设置
IsCacheable = MPU_ACCESS_NOT_CACHEABLE即可。
三、Cache 维护函数:何时用 Clean、Invalidate?
即使 MPU 配置为 Write-Through,仍建议在 DMA 传输前后调用维护函数,确保万无一失。CMSIS 提供以下函数:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 中脏数据写回内存。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重新加载。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效。
使用时机:
| 场景 | CPU 操作 | DMA 方向 | 维护操作 | |------|----------|----------|----------| | CPU 发数据给外设 | 写缓冲区 | 内存→外设 | 传输前 Clean | | 外设发数据给 CPU | 读缓冲区 | 外设→内存 | 传输后 Invalidate | | 双向 | 读写 | 双向 | 传输前 Clean,传输后 Invalidate |
四、完整实战代码:UART DMA 收发
以 UART 空闲中断 + DMA 接收为例,展示 Cache 维护。
#define DMA_BUF_SIZE 256
__attribute__((at(0x30000000))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
// 启动 DMA 接收前:无效化 Cache,确保 CPU 读到最新内存
void UART_DMA_StartReceive(void)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, DMA_BUF_SIZE);
}
// 空闲中断回调:处理接收数据
void UART_IdleCallback(void)
{
uint32_t len = DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
// 无效化 Cache,保证读取到 DMA 写入的最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, len);
// 处理数据...
process_data(dma_rx_buf, len);
}
// 发送数据前:Clean Cache,确保 DMA 读到内存中的新数据
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
memcpy(dma_tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}
注意:SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议按 32 字节向上取整。
五、注意事项与常见坑
- 地址对齐:Cache 维护函数要求地址 32 字节对齐,否则触发 HardFault。
- 缓冲区大小:若缓冲区跨 Cache 行(32 字节),Invalidate 可能丢弃相邻数据,建议缓冲区按 32 字节对齐且大小为 32 的倍数。
- DMA 描述符:若使用链表模式,描述符本身也需放在 Non-cacheable 区域或手动维护。
- 中断优先级:Cache 维护函数执行时间较长,避免在高速中断中频繁调用。
- 调试技巧:若数据错乱,先关闭 D-Cache 验证是否为一致性问题。
六、总结
STM32H7 的 Cache 与 DMA 一致性是嵌入式开发中的经典难题。核心思路:MPU 配置合理的内存属性 + 传输前后正确调用 Cache 维护函数。掌握本文的步骤,你就能在享受 H7 高性能的同时,避免数据错乱的坑。