一、为什么 H7 上 DMA 会“莫名其妙”出错?
STM32H7 采用 Cortex-M7 内核,带 16KB D-Cache 和 16KB I-Cache。开启 D-Cache 后,CPU 访问 SRAM 会先查缓存,命中则直接读缓存,不访问实际内存。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理 SRAM,不经过 Cache。
这就导致两个经典问题:
- DMA 写入内存后,CPU 读到的仍是 Cache 中的旧数据(读旧值)。
- CPU 写入数据还在 Cache 中未回写,DMA 启动后读到的是内存中的旧数据(写丢失)。
根本原因:CPU 视角的数据在 Cache,DMA 视角的数据在 SRAM,两者不同步。
二、Cache 行对齐:最容易忽略的坑
Cortex-M7 的 D-Cache 行大小为 32 字节。Clean/Invalidate 操作以“行”为单位,若缓冲区起始地址或长度未按 32 字节对齐,会误伤相邻数据。
- Invalidate 的风险:若缓冲区尾部与相邻变量共享同一 Cache 行,Invalidate 会丢弃该行中尚未回写的 CPU 数据,导致相邻变量值丢失。
- Clean 的风险:若缓冲区头部与相邻变量共享一行,Clean 会把无关数据也写回内存,通常无害但可能覆盖 DMA 刚写入的内容。
避坑准则:
- DMA 缓冲区必须 32 字节对齐,长度建议为 32 的整数倍。
- 使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏。 - 若无法对齐,可将缓冲区放在单独的 MPU 区域并配置为 Write-Through 或 Non-Cacheable。
// 推荐:32 字节对齐的 DMA 缓冲区
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
三、Clean 与 Invalidate 的正确语义
- Clean(清理):将 Cache 中已修改的数据写回 SRAM。用于 CPU 写、DMA 读 的场景(如发送)。
- Invalidate(无效化):丢弃 Cache 中的内容,强制下次读取从 SRAM 加载。用于 DMA 写、CPU 读 的场景(如接收)。
- Clean+Invalidate:先写回再丢弃,用于双向场景或不确定状态时。
关键原则:操作顺序不能错。
- 发送前:CPU 填好数据 →
SCB_CleanDCache_by_Addr()→ 启动 DMA。 - 接收后:DMA 完成 →
SCB_InvalidateDCache_by_Addr()→ CPU 读数据。
注意:Invalidate 必须在 DMA 完成之后、CPU 读取之前执行。若在 DMA 传输过程中 Invalidate,可能丢弃 DMA 刚写入但尚未完成的数据。
四、完整代码示例:UART DMA 收发
以下以 UART 空闲中断 + DMA 接收为例,展示完整流程。
#include "stm32h7xx.h"
#define RX_BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
volatile uint8_t rx_flag = 0;
volatile uint16_t rx_len = 0;
void uart_dma_init(void)
{
// 1. 配置 UART 和 DMA(略去底层寄存器,可用 HAL)
// 2. 启动 DMA 接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
// DMA 空闲回调(HAL 中为 HAL_UARTEx_RxEventCallback)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1) {
rx_len = Size;
// 关键:DMA 已写入 SRAM,但 CPU Cache 中可能是旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
rx_flag = 1;
}
}
void process_rx_data(void)
{
if (rx_flag) {
rx_flag = 0;
// 此时读取 rx_buf 才是 DMA 写入的最新数据
for (uint16_t i = 0; i < rx_len; i++) {
// 处理 rx_buf[i]
}
// 重新启动接收前,确保缓冲区状态一致
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
}
// 发送示例
void uart_send_dma(uint8_t *data, uint16_t len)
{
// 确保数据在 SRAM 中最新
SCB_CleanDCache_by_Addr((uint32_t *)data, len);
HAL_UART_Transmit_DMA(&huart1, data, len);
}
五、MPU 配置:一劳永逸的方案
若不想每次手动 Clean/Invalidate,可用 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through。
- Non-Cacheable:CPU 直接访问 SRAM,无一致性问题,但性能略低。
- Write-Through:写操作同时更新 Cache 和 SRAM,读仍走 Cache,适合读多写少。
// MPU 配置示例(将 0x30000000 开始的 64KB 配置为 Non-Cacheable)
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
六、注意事项与避坑清单
- 对齐是前提:所有 DMA 缓冲区必须 32 字节对齐,长度建议 32 的倍数。
- 顺序不能反:发送先 Clean,接收后 Invalidate,切勿在 DMA 传输中操作 Cache。
- Invalidate 有风险:若缓冲区与其它变量共享 Cache 行,Invalidate 会丢数据,优先用对齐或 MPU。
- 多缓冲区要分别处理:每个 DMA 缓冲区独立 Clean/Invalidate,不能只处理一个。
-
中断中调用:
SCB_*DCache_by_Addr可在中断中调用,但注意耗时,避免影响实时性。 - 调试时关闭 Cache:若问题诡异,先关 D-Cache 验证是否为一致性问题。
- HAL 库的坑:部分 HAL 版本在 DMA 收发中未自动处理 Cache,需手动添加。
掌握以上要点,STM32H7 的 D-Cache 与 DMA 就能稳定协同,既享受高主频性能,又避免数据错乱。