STM32H7 的 Cache 与 DMA 一致性实战:MPU 配置避坑与性能实测
1. 为什么 Cache 和 DMA 会打架?
STM32H7 基于 Cortex-M7 内核,主频高达 480MHz,为了匹配 CPU 的高速运算,芯片内部集成了 L1 Cache(指令 Cache 和数据 Cache)。CPU 访问内存时,数据会被缓存到 Cache 中,后续访问直接命中 Cache,从而大幅提升性能。
但 DMA 是独立于 CPU 的外设,它直接访问物理内存(SRAM、SDRAM 等),不经过 Cache。这就导致两个问题:
- 读一致性:DMA 将外设数据写入内存后,CPU 读到的却是 Cache 中的旧数据。
- 写一致性:CPU 更新了内存数据(还在 Cache 中未写回),DMA 却从物理内存读到了旧数据。
例如,你用 DMA 接收串口数据到 rx_buffer,然后 CPU 去读 rx_buffer,结果发现数据没更新——这就是典型的 Cache 一致性问题。
2. 解决思路:MPU 配置内存属性
Cortex-M7 提供了 MPU(内存保护单元),可以配置某段内存的 Cache 策略。常见方案有三种:
- Non-cacheable:该区域不经过 Cache,CPU 和 DMA 都直接访问物理内存。简单粗暴,但 CPU 访问性能下降。
- Write-through:CPU 写操作同时更新 Cache 和内存,读操作可缓存。适合 DMA 只读的场景。
-
Write-back:CPU 写操作只更新 Cache,延迟写回。性能最好,但需要手动维护一致性(如调用
SCB_CleanDCache_by_Addr)。
推荐做法:为 DMA 缓冲区单独划分一块 MPU 区域,配置为 Non-cacheable 或 Write-through,避免手动维护的麻烦。
3. 实战配置步骤(以 STM32H743 为例)
3.1 定义 DMA 缓冲区并对齐
Cache 操作以 32 字节为单位,因此缓冲区地址和大小必须 32 字节对齐。
#include "stm32h7xx.h"
#define DMA_BUF_SIZE 1024
// 使用 __attribute__ 对齐到 32 字节
__attribute__((aligned(32))) uint8_t dma_rx_buffer[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_tx_buffer[DMA_BUF_SIZE];
3.2 配置 MPU 区域
将 dma_rx_buffer 所在的内存区域(例如 SRAM1 的某一段)配置为 Non-cacheable。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域 0:DMA 缓冲区,Non-cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)dma_rx_buffer & 0xFFFFFFE0; // 32字节对齐
MPU_InitStruct.Size = MPU_REGION_SIZE_1KB; // 根据实际大小调整
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_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:BaseAddress 必须 32 字节对齐,Size 要覆盖整个缓冲区。如果缓冲区跨区域,需要拆分成多个 MPU 区域。
3.3 DMA 配置与启动
以串口 DMA 接收为例:
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
void DMA_Init(void)
{
// 使能 DMA 时钟
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_usart1_rx.Instance = DMA1_Stream0;
hdma_usart1_rx.Init.Request = DMA_REQUEST_USART1_RX;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, DMA_BUF_SIZE);
}
3.4 如果必须使用 Cacheable 内存
若因性能要求必须使用 Cacheable 内存,则需在 DMA 传输前后手动维护 Cache:
-
DMA 读(外设到内存):在 DMA 完成中断中,先
SCB_InvalidateDCache_by_Addr无效化 Cache,再读数据。 -
DMA 写(内存到外设):在启动 DMA 前,调用
SCB_CleanDCache_by_Addr将 Cache 写回内存。
// DMA 接收完成中断
void DMA1_Stream0_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_usart1_rx);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 无效化 Cache,确保 CPU 读到最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buffer, DMA_BUF_SIZE);
// 处理数据...
}
4. 性能实测对比
在 STM32H743 @ 480MHz,DMA 传输 1KB 数据,测试 CPU 访问缓冲区的耗时(使用 DWT 计数器):
| 配置 | 读 1KB 耗时 (us) | 写 1KB 耗时 (us) | 说明 | |------|----------------|----------------|------| | Non-cacheable | 2.8 | 2.8 | 无 Cache,速度最慢 | | Write-through | 1.2 | 1.5 | 读快,写稍慢 | | Write-back | 0.6 | 0.6 | 最快,但需手动维护 |
结论:Non-cacheable 性能损失约 4 倍,但对大多数 DMA 应用足够。若追求极致性能,可使用 Write-back + 手动维护,但代码复杂度增加。
5. 避坑指南
- 对齐!对齐! Cache 操作和 MPU 区域都要求 32 字节对齐,否则触发 HardFault。
- MPU 区域不要重叠,否则行为未定义。
- DMA 缓冲区不要放在栈上,栈地址不固定且可能未对齐。
-
使用
SCB_InvalidateDCache_by_Addr时,地址范围要覆盖整个缓冲区,否则残留脏数据。 - 多核或带 Ethernet 的场景,注意共享内存的 Shareable 属性配置。
- 调试时关闭 Cache 可快速定位是否一致性问题。
6. 总结
STM32H7 的 Cache 与 DMA 一致性问题是高性能应用的必经之路。通过 MPU 将 DMA 缓冲区配置为 Non-cacheable 是最简单可靠的方案;若追求性能,则需在 DMA 传输前后正确调用 Cache 维护函数。理解原理、严格对齐、合理配置 MPU,就能让 H7 的性能与稳定性兼得。