一、为什么 D-Cache 会让 DMA 数据错位?
STM32H7 的 Cortex-M7 内核带有 16KB D-Cache 和 I-Cache。D-Cache 默认关闭,一旦开启,CPU 访问内存时会将数据缓存到 Cache 中,后续读写直接命中 Cache,不再访问实际 SRAM。
而 DMA 是独立于 CPU 的总线主设备,它直接读写 SRAM,不经过 D-Cache。这就导致:
- CPU 写数据到 Cache,但未回写到 SRAM,DMA 从 SRAM 读到的仍是旧数据;
- DMA 将新数据写入 SRAM,但 CPU 读到的仍是 Cache 中的旧数据。
这就是典型的缓存一致性问题,表现为串口收发错位、ADC 采样值跳变、SD 卡读写失败等。
二、解决思路:MPU 配置回写区间
Cortex-M7 提供 MPU(内存保护单元),可将特定内存区域配置为不同的 Cache 策略:
- Write-Through:CPU 写操作同时更新 Cache 和 SRAM,读操作可缓存;
- Write-Back:CPU 写操作只更新 Cache,延迟回写,性能最高;
- Non-Cacheable:完全不使用 Cache,性能最低但一致性最好。
对于 DMA 缓冲区,推荐使用 Write-Back + Read-Allocate 并配合手动清理/无效化操作,或者直接配置为 Non-Cacheable 简化设计。本文重点介绍 按 MPU 配置回写区间 的方法,兼顾性能与一致性。
三、配置步骤
3.1 确定 DMA 缓冲区地址
将 DMA 缓冲区放在一个独立的、按 32 字节对齐的 SRAM 区域,例如 0x30000000 开始的 64KB(D2 SRAM)。
3.2 配置 MPU 区域
使用 HAL 库配置 MPU:
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 DMA 缓冲区区域:0x30000000,大小 64KB
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_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_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);
}
此配置将 0x30000000 区域设为 Write-Back、Write-Allocate 的 Cache 属性。
3.3 使能 D-Cache
SCB_EnableICache();
SCB_EnableDCache();
3.4 DMA 传输前后的 Cache 维护
对于 Write-Back 区域,DMA 发送前需 Clean(回写)Cache,DMA 接收后需 Invalidate(无效化)Cache。
// DMA 发送前:将 CPU 写入的数据从 Cache 回写到 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));
// DMA 接收完成后:无效化 Cache,确保 CPU 读取到 SRAM 中的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
注意:SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议为 32 的倍数。
四、完整示例:UART DMA 收发
#include "stm32h7xx_hal.h"
#define BUFFER_SIZE 64
// 缓冲区必须 32 字节对齐,放在 MPU 配置的区域
__attribute__((aligned(32))) uint8_t tx_buffer[BUFFER_SIZE] __attribute__((section(".dma_buffer")));
__attribute__((aligned(32))) uint8_t rx_buffer[BUFFER_SIZE] __attribute__((section(".dma_buffer")));
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
void UART_DMA_Init(void)
{
// 初始化代码略,重点在 Cache 维护
}
void UART_SendData(uint8_t *data, uint16_t len)
{
memcpy(tx_buffer, data, len);
// 回写 Cache,确保 DMA 读到最新数据
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, len);
HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}
void UART_ReceiveData(uint8_t *data, uint16_t len)
{
// 无效化 Cache,确保 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, len);
memcpy(data, rx_buffer, len);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUFFER_SIZE);
// 处理数据...
}
}
五、注意事项
- 地址对齐:Cache 维护函数要求地址 32 字节对齐,长度建议为 32 的倍数,否则可能误伤相邻数据。
- 避免频繁维护:每次 DMA 传输都 Clean/Invalidate 会降低性能,可考虑将小缓冲区配置为 Non-Cacheable。
- 多缓冲区:若使用多个 DMA 缓冲区,确保它们位于同一 MPU 区域,或分别配置。
- 中断安全:在中断中调用 Cache 维护函数时,注意不要与主循环中的 DMA 操作冲突。
- 调试技巧:若数据错位,可先关闭 D-Cache 验证问题是否由 Cache 引起。
-
链接脚本:使用
__attribute__((section(".dma_buffer")))需在链接脚本中定义该段并分配到正确地址。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的常见坑点。通过 MPU 将 DMA 缓冲区配置为 Write-Back 回写区间,并在传输前后正确调用 SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr,即可在保证性能的同时避免数据错位。掌握这一机制,能让你的 H7 项目稳定运行在 480MHz 的高性能模式下。