一、为什么D-Cache与DMA会冲突?
STM32H7 的 Cortex-M7 内核带有 16KB D-Cache 和 16KB I-Cache。D-Cache 作为 CPU 与主存之间的高速缓冲,能大幅提升数据访问效率。但 DMA 控制器直接访问物理内存,不经过 Cache。
冲突场景:
- CPU 写数据到缓冲区(数据暂存在 Cache 中,未写回内存),随后启动 DMA 发送。DMA 从内存读到的是旧数据。
- DMA 将外设数据写入内存,CPU 读取同一缓冲区时,Cache 中可能仍是旧内容,导致读到脏数据。
因此,必须通过 MPU 配置内存属性 或 手动 clean/invalidate 操作 来维护一致性。
二、MPU 配置:从根源上划分内存区域
MPU(Memory Protection Unit)可将内存划分为不同属性的区域。对于 DMA 缓冲区,推荐配置为 Non-Cacheable 或 Write-Through,避免手动维护。
2.1 关键属性说明
- TEX=0, C=0, B=0:Non-Cacheable,读写均不经过 Cache,适合 DMA 缓冲区。
- TEX=0, C=1, B=0:Write-Back, Write-Allocate,性能最高但需手动维护。
- TEX=0, C=1, B=1:Write-Through,写操作同时更新 Cache 和内存,读仍可缓存。
2.2 MPU 配置步骤(以 Non-Cacheable 为例)
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; - 关闭 D-Cache 和 I-Cache(配置期间)。
- 禁用 MPU:
MPU->CTRL = 0; - 配置区域基址、属性、大小。
- 使能 MPU 和 Cache。
#include "stm32h7xx.h"
// 定义 DMA 缓冲区所在区域,例如 0x30000000 (D2 SRAM)
#define DMA_BUFFER_ADDR 0x30000000
#define DMA_BUFFER_SIZE 1024 // 1KB
void MPU_Config(void)
{
// 关闭 D-Cache 和 I-Cache
SCB_DisableDCache();
SCB_DisableICache();
// 禁用 MPU
MPU->CTRL = 0;
// 配置 Region 0
MPU->RNR = 0; // 选择区域 0
// 基址:必须对齐到区域大小
MPU->RBAR = DMA_BUFFER_ADDR & MPU_RBAR_ADDR_Msk;
// 属性:Non-Cacheable, Non-Shareable, 可读写
// AP=011 (Full access), TEX=0, C=0, B=0, S=0
MPU->RASR = (0x3 << MPU_RASR_AP_Pos) |
(0x0 << MPU_RASR_TEX_Pos) |
(0x0 << MPU_RASR_C_Pos) |
(0x0 << MPU_RASR_B_Pos) |
(0x0 << MPU_RASR_S_Pos) |
MPU_RASR_ENABLE_Msk;
// 设置区域大小:1KB -> SIZE=9 (2^(9+1)=1024)
// 注意:SIZE 字段编码为 log2(size)-1
MPU->RASR |= (9 << MPU_RASR_SIZE_Pos);
// 使能 MPU
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
// 使能 Cache
SCB_EnableICache();
SCB_EnableDCache();
}
注意:区域大小必须为 2 的幂,且基址按大小对齐。若缓冲区跨越多个区域,需分别配置。
三、手动 clean/invalidate 的边界条件
若无法使用 MPU(如缓冲区动态分配),则需手动维护。核心函数:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 中脏数据写回内存。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重新加载。
3.1 正确使用顺序
CPU 写 -> DMA 读(发送):
- CPU 填充缓冲区。
-
SCB_CleanDCache_by_Addr写回内存。 - 启动 DMA。
DMA 写 -> CPU 读(接收):
- 启动 DMA 前,
SCB_InvalidateDCache_by_Addr确保 Cache 无旧数据。 - DMA 完成中断中,再次
SCB_InvalidateDCache_by_Addr丢弃可能被预取的旧行。 - CPU 读取数据。
3.2 边界条件与陷阱
- 地址对齐:操作地址必须 32 字节对齐,长度向上取整到 32 字节。否则可能误伤相邻数据。
- 缓冲区重叠:若两个 DMA 缓冲区共享同一 Cache 行,clean 一个可能影响另一个。建议缓冲区按 32 字节对齐并填充。
- 中断中调用:clean/invalidate 本身不阻塞,但需确保 DMA 未在传输中。
- 性能损耗:频繁 clean/invalidate 会降低 Cache 效率,尽量用 MPU 替代。
// 安全封装:处理对齐
void Safe_CleanDCache(void *addr, uint32_t size)
{
uint32_t start = (uint32_t)addr & ~0x1F; // 向下对齐
uint32_t end = ((uint32_t)addr + size + 0x1F) & ~0x1F;
SCB_CleanDCache_by_Addr((uint32_t*)start, end - start);
}
void Safe_InvalidateDCache(void *addr, uint32_t size)
{
uint32_t start = (uint32_t)addr & ~0x1F;
uint32_t end = ((uint32_t)addr + size + 0x1F) & ~0x1F;
SCB_InvalidateDCache_by_Addr((uint32_t*)start, end - start);
}
四、完整实战示例:UART DMA 接收
以 UART 空闲中断 + DMA 接收为例,展示一致性处理。
#define RX_BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t rxBuffer[RX_BUFFER_SIZE];
void UART_DMA_Init(void)
{
// 1. 配置 DMA 为循环模式,目标地址 rxBuffer
// 2. 使能 UART 空闲中断
// 3. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}
// UART 空闲中断回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
// 丢弃 Cache 中可能存在的旧数据
Safe_InvalidateDCache(rxBuffer, Size);
// 此时 rxBuffer 中为最新数据,可安全处理
ProcessData(rxBuffer, Size);
// 重新启动接收(注意:需先 invalidate 再启动)
Safe_InvalidateDCache(rxBuffer, RX_BUFFER_SIZE);
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}
五、注意事项总结
- 优先使用 MPU:将 DMA 缓冲区设为 Non-Cacheable 是最简单可靠的方式。
- 对齐是生命线:所有 clean/invalidate 操作必须 32 字节对齐,长度向上取整。
- 避免缓冲区共享 Cache 行:不同 DMA 缓冲区之间至少间隔 32 字节。
- DMA 传输中禁止 invalidate:否则可能丢失未写回的数据。
- 调试时关闭 Cache:若数据异常,可暂时关闭 D-Cache 定位问题。
- 参考手册:Cortex-M7 的 TRM 和 STM32H7 参考手册中 Cache 章节有详细说明。
掌握这些要点,你就能在 STM32H7 上安全地同时使用 D-Cache 和 DMA,充分发挥芯片性能。