一、为什么 DMA 与 D-Cache 会打架?
STM32H7 的 Cortex-M7 内核包含 16KB 的 D-Cache(数据缓存),它位于 CPU 和 AXI SRAM 之间。CPU 访问数据时,若命中 Cache 则直接读写缓存,不会立即同步到物理内存。而 DMA 控制器直接访问物理内存,完全绕过 Cache。
当 CPU 写数据到缓冲区后启动 DMA 发送,若数据还在 Cache 中未写回,DMA 读到的是旧数据;反之,DMA 接收数据到内存后,CPU 若从 Cache 读,可能读到旧缓存。这就是数据一致性问题。
二、Cache Line 与对齐的关系
Cortex-M7 的 D-Cache 按 Cache Line 管理,每个 Line 为 32 字节。Cache 维护操作(如 clean、invalidate)以 Line 为单位。
- 若缓冲区起始地址或大小未按 32 字节对齐,clean/invalidate 会波及相邻数据。
- 例如:缓冲区 100 字节,起始地址 0x20000010,则首尾 Line 包含其他变量。invalidate 时可能丢弃其他变量的最新值,导致隐蔽 Bug。
因此,DMA 缓冲区必须按 32 字节对齐,且大小最好是 32 的整数倍。
三、配置步骤
3.1 定义对齐的缓冲区
使用 __attribute__((aligned(32))) 强制对齐:
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[BUF_SIZE];
3.2 配置 MPU 将缓冲区设为 Non-Cacheable
更简单的方案:通过 MPU 将 DMA 缓冲区区域设为 Non-Cacheable,彻底避免一致性问题。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)dma_tx_buf; // 或整个 SRAM 区域
MPU_InitStruct.Size = MPU_REGION_SIZE_256B; // 根据实际大小调整
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.3 使用 Cache 维护函数(若必须 Cacheable)
若缓冲区仍为 Cacheable,则需在 DMA 传输前后手动维护:
-
发送前:CPU 写数据后,执行
SCB_CleanDCache_by_Addr()将 Cache 写回内存。 -
接收后:DMA 写内存后,执行
SCB_InvalidateDCache_by_Addr()丢弃 Cache 中旧数据。
注意:invalidate 前若 CPU 曾写该缓冲区,必须先 clean,否则丢失数据。
四、完整代码示例(UART DMA 发送)
#include "stm32h7xx.h"
#define TX_SIZE 64
__attribute__((aligned(32))) uint8_t tx_buf[TX_SIZE];
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
// 1. 拷贝数据到对齐缓冲区
memcpy(tx_buf, data, len);
// 2. 若缓冲区为 Cacheable,clean 确保数据写入内存
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_SIZE);
// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
// DMA 接收完成回调中处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 4. 接收后 invalidate,确保 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_SIZE);
// 处理 rx_buf ...
}
}
五、注意事项
- 对齐是前提:所有 DMA 缓冲区必须 32 字节对齐,大小建议为 32 的整数倍。
- MPU 优先:若性能允许,将 DMA 区域设为 Non-Cacheable 是最省心的方案。
- 维护范围:clean/invalidate 的地址和长度必须按 32 字节对齐,否则会误伤相邻数据。
- 避免频繁维护:Cache 维护有开销,批量传输时尽量合并操作。
- 多缓冲区隔离:不同 DMA 流使用独立对齐缓冲区,防止伪共享。
- 调试技巧:若数据异常,先检查缓冲区地址是否 32 字节对齐,再确认是否遗漏 clean/invalidate。
掌握以上方法,即可在 STM32H7 上安全高效地使用 DMA 与 D-Cache,充分发挥芯片性能。