STM32H7 的 Cache 与 DMA 一致性排查:从 MPU 配置到 Clean/Invalidate 的实战避坑
STM32H7 系列凭借 Cortex-M7 内核和 L1 Cache(I-Cache/D-Cache)带来了极高的性能,但同时也引入了 Cache 与 DMA 之间的数据一致性问题。许多开发者在 H7 上使用 DMA 时,常遇到“数据错乱”、“传输不完整”甚至“HardFault”等问题,根源往往在于 Cache 未正确维护。本文将系统讲解原理、MPU 配置、Clean/Invalidate 操作,并给出实战代码与避坑要点。
一、为什么 Cache 会导致 DMA 数据不一致?
Cortex-M7 的 D-Cache 是写回(Write-Back)且写分配(Write-Allocate)的。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回实际 RAM;同样,DMA 直接读写 RAM,但 CPU 可能从 Cache 中读到旧数据。具体场景:
- CPU 写数据 -> DMA 读:CPU 写入的数据还在 Cache 中(脏行),DMA 从 RAM 读到的仍是旧数据。
- DMA 写数据 -> CPU 读:DMA 将新数据写入 RAM,但 CPU 读的是 Cache 中的旧数据(若该地址曾被缓存)。
因此,必须在 DMA 传输前后正确执行 Clean(将 Cache 脏行写回 RAM)和 Invalidate(丢弃 Cache 行,强制从 RAM 重新加载)。
二、MPU 配置:为 DMA 缓冲区设置正确的内存属性
最根本的解决方法是利用 MPU 将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable 或 Write-Through,从而避免一致性问题。但并非所有场景都适合关闭 Cache(会损失性能),因此需要权衡。
2.1 MPU 配置步骤(以 Non-Cacheable 为例)
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;并调用ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk); - 定义 MPU 区域:设置基地址、大小、属性(如
ARM_MPU_ATTR(ARM_MPU_AP_FULL, ARM_MPU_NORMAL_NON_CACHEABLE))。 - 加载区域并启用。
#include "cmsis_compiler.h"
void MPU_Config(void)
{
ARM_MPU_Disable();
// 定义 DMA 缓冲区区域:0x30000000 (SRAM1) 起始,大小 32KB
ARM_MPU_SetRegion(
0, // 区域编号
0x30000000, // 基地址
ARM_MPU_RASR(0, // 禁用指令访问
ARM_MPU_AP_FULL, // 全权限
0, // TEX=0
0, // C=0 (Non-cacheable)
0, // B=0
0, // S=0
ARM_MPU_SIZE_32KB,
ARM_MPU_ENABLE)
);
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
}
注意:若使用 DTCM RAM(0x20000000),它本身不经过 Cache,无需 MPU 配置。但 AXI SRAM(0x24000000)等区域默认是 Cacheable 的。
三、Clean 与 Invalidate 的正确使用时机
如果无法将缓冲区设为 Non-Cacheable,则必须在 DMA 操作前后手动维护 Cache。
3.1 操作原则
-
CPU 写 -> DMA 读:在启动 DMA 前执行
SCB_CleanDCache_by_Addr(),确保数据写回 RAM。 -
DMA 写 -> CPU 读:在 DMA 完成后执行
SCB_InvalidateDCache_by_Addr(),丢弃旧 Cache 行。 - 双向传输:先 Clean 再启动 DMA,完成后 Invalidate。
3.2 关键函数
// 清理指定地址范围的数据 Cache(写回)
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
// 无效化指定地址范围的数据 Cache(丢弃)
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
地址必须 32 字节对齐,长度建议向上取整到 32 字节边界。
四、完整代码示例:UART DMA 发送与接收
以下示例使用 UART DMA 发送和接收,展示 Cache 维护流程。假设缓冲区位于 AXI SRAM(0x24000000),且未配置 MPU Non-Cacheable。
#include "stm32h7xx_hal.h"
#define BUFFER_SIZE 128
// 注意:缓冲区必须 32 字节对齐
__attribute__((aligned(32))) uint8_t tx_buffer[BUFFER_SIZE];
__attribute__((aligned(32))) uint8_t rx_buffer[BUFFER_SIZE];
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
// 发送数据:先 Clean,再启动 DMA
void UART_Send_DMA(uint8_t *data, uint16_t len)
{
memcpy(tx_buffer, data, len);
// 清理 D-Cache,确保数据写入 RAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, len);
HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}
// DMA 接收完成回调:Invalidate 后处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 无效化 D-Cache,丢弃旧数据,强制从 RAM 读取
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);
// 此时 rx_buffer 中为 DMA 写入的新数据
process_data(rx_buffer, BUFFER_SIZE);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}
}
// 初始化时启动接收
void UART_Start_Receive(void)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}
五、常见避坑指南
- 地址对齐:Clean/Invalidate 的地址和长度必须 32 字节对齐,否则可能误伤相邻数据。
- Invalidate 前先 Clean:若缓冲区中既有 CPU 写入又有 DMA 写入,Invalidate 会丢弃未写回的数据,导致丢失。正确顺序:先 Clean 再 Invalidate。
- DMA 描述符与缓冲区分离:若使用链表模式,描述符本身也需注意 Cache 一致性。
- 中断中调用:Cache 维护函数执行时间较长,避免在高速中断中频繁调用。
- 多核/多主设备:H7 部分型号有双核,需考虑核间共享内存的 Cache 一致性。
- 调试时关闭 Cache:初期调试可暂时关闭 D-Cache 验证问题是否由 Cache 引起。
六、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典陷阱。通过合理配置 MPU 将 DMA 缓冲区设为 Non-Cacheable,或在关键节点正确执行 Clean/Invalidate,即可从根本上解决。建议优先使用 MPU 方案,性能与稳定性兼得;若必须使用 Cacheable 内存,务必严格遵循“写前 Clean,读后 Invalidate”的原则,并注意对齐与顺序。掌握这些技巧,你就能在 H7 上放心驰骋 DMA 了。