STM32H7 的 D-Cache 与 DMA 一致性:从 Cache 维护到 MPU 配置的完整避坑指南
STM32H7 系列凭借 480MHz 主频和 Cortex-M7 内核,成为高性能嵌入式应用的首选。但许多开发者从 F1/F4 迁移到 H7 后,常遇到 DMA 传输数据错乱、外设通信失败等诡异问题。根源往往在于 D-Cache 与 DMA 的数据一致性。本文将带你彻底理解并解决这一难题。
一、为什么 D-Cache 会与 DMA 冲突?
Cortex-M7 的 D-Cache 位于内核与总线矩阵之间。当 CPU 访问内存时,数据会被缓存到 D-Cache 中。而 DMA 控制器直接访问物理内存,不经过 Cache。
- 写操作:CPU 写数据到 Cache 后,若未及时写回内存,DMA 读取到的仍是旧数据。
- 读操作:DMA 将新数据写入内存后,CPU 若从 Cache 读取,可能得到旧缓存值。
这种不一致性在以下场景尤为突出:
- ADC/DAC 连续采样
- UART/SPI 高速收发
- 摄像头 DCMI 图像传输
- SDMMC 读写
二、三种解决方案对比
| 方案 | 原理 | 适用场景 | 性能影响 | |------|------|----------|----------| | 手动维护 | 在 DMA 传输前后调用 Clean/Invalidate | 偶发传输、大数据块 | 中等 | | MPU Write-Through | 配置内存区域为透写模式 | 频繁小数据 | 较小 | | MPU Non-Cacheable | 完全禁用该区域 Cache | 实时性要求高 | 较大 |
三、方案一:手动 Cache 维护(最常用)
3.1 核心函数
CMSIS 提供了两个关键函数:
// 将 Cache 内容写回内存(CPU 写 -> DMA 读)
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
// 丢弃 Cache 内容,强制从内存重新加载(DMA 写 -> CPU 读)
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
3.2 完整示例:UART DMA 接收
#include "stm32h7xx.h"
#define RX_BUFFER_SIZE 256
// 必须 32 字节对齐!
__attribute__((aligned(32))) uint8_t rx_buffer[RX_BUFFER_SIZE];
void uart_dma_init(void)
{
// ... UART 和 DMA 初始化代码 ...
// 启动 DMA 接收前,Invalidate 缓存
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, RX_BUFFER_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 处理数据前再次 Invalidate,确保读到最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, RX_BUFFER_SIZE);
// 此时 rx_buffer 中的数据是有效的
process_data(rx_buffer, RX_BUFFER_SIZE);
}
}
3.3 发送场景
__attribute__((aligned(32))) uint8_t tx_buffer[TX_BUFFER_SIZE];
void uart_send_dma(uint8_t *data, uint16_t len)
{
memcpy(tx_buffer, data, len);
// 发送前 Clean,确保 DMA 读到最新数据
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, TX_BUFFER_SIZE);
HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}
四、方案二:MPU 配置 Write-Through 模式
对于频繁小数据量传输,手动维护开销较大。可将 DMA 缓冲区所在内存区域配置为 Write-Through 模式,CPU 写操作同时更新 Cache 和内存。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 假设 DMA 缓冲区位于 0x30000000 (SRAM1)
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:Write-Through 仍可能因乱序执行导致问题,最保险的是 Non-Cacheable。
五、方案三:MPU 配置 Non-Cacheable 区域(最稳妥)
将 DMA 缓冲区完全排除在 Cache 之外,彻底避免一致性问题。
void MPU_Config_NonCacheable(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_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
六、避坑要点总结
-
地址对齐:Cache 维护函数要求地址 32 字节对齐,缓冲区必须使用
__attribute__((aligned(32)))。 -
长度对齐:
SCB_InvalidateDCache_by_Addr的长度参数应为 32 的倍数,否则可能误伤相邻数据。 - 顺序不能错:发送前 Clean,接收后 Invalidate,顺序颠倒会导致数据丢失。
- 避免 Cache 行伪共享:不同 DMA 缓冲区不要放在同一 32 字节 Cache 行内。
- 中断中慎用:Cache 维护函数执行时间较长,避免在高速中断中频繁调用。
- DMA 描述符:若使用链表模式,描述符本身也需对齐并维护 Cache。
- 调试技巧:可临时关闭 D-Cache 验证问题是否由一致性引起。
七、结语
D-Cache 与 DMA 的一致性是 STM32H7 开发中的经典难题。理解其原理后,根据实际场景选择合适方案:偶发大数据用手动维护,频繁小数据用 MPU Write-Through,实时性要求极高用 Non-Cacheable。掌握这些技巧,你的 H7 项目将更加稳定可靠。