一、为什么 H7 上 DMA 与 D-Cache 会打架?
STM32H7 的 Cortex-M7 内核带有 16KB 的 D-Cache,用于加速数据访问。但 DMA 控制器直接访问物理内存(SRAM),不经过 Cache。当 CPU 写数据到 Cache 后,若未回写到 SRAM,DMA 读到的就是旧数据;反之,DMA 写入新数据到 SRAM,CPU 若命中 Cache 中的旧副本,也会读到旧值。这就是数据一致性问题。
解决手段只有两个:
- Clean:将 Cache 中已修改的数据写回 SRAM。
- Invalidate:将 Cache 中的对应行标记为无效,强制下次读取时从 SRAM 重新加载。
但 Clean/Invalidate 不是随便调用的,边界条件处理不当会引入更隐蔽的 Bug。
二、Cache 操作的核心 API 与边界条件
CMSIS 提供了以下函数(需包含 core_cm7.h):
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
边界条件 1:地址必须 32 字节对齐
Cortex-M7 的 Cache 行大小为 32 字节。上述函数的 addr 必须是 32 字节对齐的,否则行为未定义(可能误伤相邻数据)。若缓冲区起始地址未对齐,需先对齐到 32 字节边界,并调整 dsize。
// 安全封装:自动处理对齐
void safe_clean_dcache(void *buf, uint32_t len) {
uint32_t start = (uint32_t)buf & ~0x1F; // 向下对齐到 32 字节
uint32_t end = ((uint32_t)buf + len + 0x1F) & ~0x1F; // 向上对齐
SCB_CleanDCache_by_Addr((uint32_t*)start, end - start);
}
边界条件 2:Invalidate 前必须确保 Cache 中没有未回写的数据
如果 CPU 刚写过缓冲区,Cache 中有脏数据,此时直接 Invalidate 会丢弃这些修改,导致数据丢失。正确顺序是:先 Clean,再 Invalidate(或直接用 CleanInvalidate)。
边界条件 3:DMA 传输期间禁止 CPU 访问缓冲区
否则 Cache 可能再次载入数据,破坏一致性。通常用信号量或标志位保护。
三、典型场景与正确操作流程
场景 1:CPU 发送数据 → DMA 搬运到外设(如 UART TX)
- CPU 填充缓冲区。
- Clean 缓冲区,确保数据写入 SRAM。
- 启动 DMA。
- 等待 DMA 完成(期间 CPU 不碰缓冲区)。
uint8_t tx_buf[64] __attribute__((aligned(32)));
void uart_dma_send(void) {
// 填充数据
for (int i = 0; i < 64; i++) tx_buf[i] = i;
// Clean D-Cache
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
// 启动 DMA
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));
}
场景 2:DMA 从外设接收数据 → CPU 读取
- 启动 DMA 接收。
- 等待 DMA 完成。
- Invalidate 缓冲区,丢弃 Cache 中的旧副本。
- CPU 读取数据。
uint8_t rx_buf[64] __attribute__((aligned(32)));
void uart_dma_receive(void) {
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
// 等待完成(实际应用用中断/回调)
while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);
// Invalidate D-Cache
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
// 安全读取
for (int i = 0; i < 64; i++) process(rx_buf[i]);
}
四、实测踩坑记录
坑 1:未对齐导致相邻变量被清空
某次使用 SCB_InvalidateDCache_by_Addr 时,缓冲区起始地址为 0x20000004,未对齐。结果 Invalidate 操作覆盖了 0x20000000~0x2000001F 整个 Cache 行,导致相邻的全局变量被意外丢弃,程序跑飞。务必使用 32 字节对齐的缓冲区,可用 __attribute__((aligned(32)))。
坑 2:MPU 配置不当导致 Cache 属性错误
若某块内存被 MPU 配置为 Write-Through 或 Non-Cacheable,则无需 Clean/Invalidate。但若误配为 Write-Back,而 DMA 又访问该区域,必须手动维护一致性。建议将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through,可省去手动操作,但会损失性能。
坑 3:DMA 传输中 CPU 读取缓冲区
在 DMA 接收过程中,CPU 提前读取缓冲区,此时 Cache 可能已缓存了部分旧数据,导致读到半新半旧的内容。必须等 DMA 完成后再 Invalidate 并读取。
坑 4:中断中调用 Cache 操作函数
SCB_* 函数执行时间较长(尤其大缓冲区),在中断中调用可能导致实时性下降。建议在任务级处理,或使用 SCB_CleanDCache_by_Addr 的小范围操作。
五、最佳实践总结
-
缓冲区对齐:所有 DMA 缓冲区使用
__attribute__((aligned(32)))。 - 操作顺序:发送前 Clean,接收后 Invalidate。
- 避免冗余:若 MPU 已配置为 Non-Cacheable,则无需手动维护。
- 保护共享:DMA 传输期间禁止 CPU 访问缓冲区。
- 性能权衡:频繁小数据 DMA 可考虑关闭 D-Cache 或使用 DTCM RAM(无需 Cache 维护)。
六、完整示例:UART DMA 回环测试
#include "stm32h7xx_hal.h"
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
uint8_t tx_buf[32] __attribute__((aligned(32)));
uint8_t rx_buf[32] __attribute__((aligned(32)));
volatile uint8_t dma_done = 0;
void DMA1_Stream0_IRQHandler(void) { // RX 完成中断
HAL_DMA_IRQHandler(&hdma_usart1_rx);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
dma_done = 1;
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// 填充发送数据
for (int i = 0; i < 32; i++) tx_buf[i] = i + 0x30;
// 启动接收 DMA
HAL_UART_Receive_DMA(&huart1, rx_buf, 32);
// 发送数据前 Clean
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, 32);
HAL_UART_Transmit_DMA(&huart1, tx_buf, 32);
// 等待接收完成
while (!dma_done);
// 接收后 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, 32);
// 验证回环数据
for (int i = 0; i < 32; i++) {
if (rx_buf[i] != tx_buf[i]) {
Error_Handler();
}
}
while (1);
}
七、结语
STM32H7 的 D-Cache 与 DMA 一致性问题是高性能应用的必修课。理解 Clean/Invalidate 的边界条件,遵循“发送前 Clean、接收后 Invalidate”的原则,并注意对齐与保护,就能避开大多数坑。若项目对实时性要求极高,可考虑将 DMA 缓冲区放在 DTCM RAM(无需 Cache 维护)或配置 MPU 为 Non-Cacheable,从根源上消除一致性问题。