# 引言 STM32H7 系列(如 STM32H743/750)凭借 Cortex-M7 内核,主频可达 480MHz,并配备 L1-Cache(I-Cache 和 D-Cache)。然而,高性能的代价是 Cache 与 DMA 之间的数据一致性问题。许多开发者初次上手时,常遇到 DMA 接收数据不更新、随机死机等诡异现象,根源往往在于 Cache 未正确管理。本文将带你彻底搞懂 Cache 一致性原理,并通过 MPU 配置实战,确保系统稳定运行。 # Cache 一致性问题的根源 ## 什么是 Cache 一致性? Cortex-M7 内核的 D-Cache 是 CPU 与内存之间的高速缓存。当 CPU 读取或写入数据时,会优先操作 Cache,而非直接访问 SRAM。DMA 外设则直接访问 SRAM,绕过 Cache。这导致两个问题: - **Cache 命中(Stale Data)**:CPU 修改了 Cache 中的数据,但尚未写回 SRAM。此时 DMA 从 SRAM 读取,得到的是旧数据。 - **Cache 未命中(Dirty Data)**:DMA 将新数据写入 SRAM,但 CPU 的 Cache 中仍保留旧副本。CPU 读取时命中 Cache,得到的是过时数据。 ## 典型场景 - **UART DMA 接收**:DMA 将接收数据写入 SRAM,CPU 从 Cache 读取,可能读到旧数据。 - **ADC DMA 传输**:ADC 连续采样写入 SRAM,CPU 处理时可能使用缓存中的旧值。 - **以太网/USB 缓冲**:数据包在 CPU 和 DMA 间频繁交换,一致性至关重要。 # MPU 配置实战 ## 解决方案概述 解决 Cache 一致性的常用方法有两种: 1. **软件维护**:在 DMA 操作前后,调用 `SCB_CleanDCache()` 和 `SCB_InvalidateDCache()` 手动刷新 Cache。但频繁调用会降低性能,且易遗漏。 2. **硬件隔离**:通过 MPU 将特定内存区域配置为 **Non-cacheable**(不可缓存),使 CPU 直接访问 SRAM,彻底避免一致性问题。推荐用于 DMA 缓冲区。 本文采用方案 2,结合 MPU 配置,将一块 SRAM 区域设为非缓存,专用于 DMA 通信。 ## 硬件环境 - MCU:STM32H743ZI(主频 480MHz) - 开发板:Nucleo-H743ZI - 调试器:ST-Link - IDE:STM32CubeIDE 1.15.0 ## 配置步骤 ### 1. 启用 Cache 和 MPU 在 `main.c` 中,系统初始化后启用 I-Cache 和 D-Cache,并配置 MPU。 ```c /* main.c */ int main(void) { HAL_Init(); SystemClock_Config(); /* 启用 I-Cache 和 D-Cache */ SCB_EnableICache(); SCB_EnableDCache(); /* 配置 MPU */ MPU_Config(); /* 初始化外设 */ MX_GPIO_Init(); MX_USART1_UART_Init(); /* 启动 DMA 接收 */ HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE); while (1) { // 主循环 } } ``` ### 2. 定义 MPU 配置函数 在 `mpu.c` 中实现 MPU 配置。我们将 SRAM1(地址 0x30000000,大小 128KB)划分为两个区域:前 64KB 为普通可缓存,后 64KB 为非缓存(用于 DMA)。 ```c /* mpu.c */ #include "mpu.h" void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; /* 禁用 MPU 进行配置 */ HAL_MPU_Disable(); /* 配置区域 0:SRAM1 前 64KB,可缓存,写回策略 */ 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_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置区域 1:SRAM1 后 64KB,非缓存,用于 DMA */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x30010000; // 0x30000000 + 64KB 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_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` ### 3. 定义 DMA 缓冲区 在 `main.h` 中,将 DMA 缓冲区放置在非缓存区域。使用 `__attribute__((section(".noncacheable")))` 将变量放入指定段,并在链接脚本中映射到非缓存地址。 ```c /* main.h */ #define BUFFER_SIZE 256 /* 非缓存段声明 */ __attribute__((section(".noncacheable"))) uint8_t rx_buffer[BUFFER_SIZE]; ``` 在链接脚本(`.ld`)中,添加非缓存段定义: ```c /* STM32H743ZITX_FLASH.ld */ .noncacheable (NOLOAD) : { . = ALIGN(32); *(.noncacheable) . = ALIGN(32); } >RAM_D1 ``` 注意:`RAM_D1` 对应 0x30000000 起始的 SRAM1。确保链接脚本中该区域大小足够。 ### 4. 完整代码示例 以下是一个完整的 UART DMA 接收示例,使用非缓存缓冲区,无需手动 Cache 维护。 ```c /* main.c */ #include "main.h" #include "mpu.h" UART_HandleTypeDef huart1; DMA_HandleTypeDef hdma_usart1_rx; __attribute__((section(".noncacheable"))) uint8_t rx_buffer[BUFFER_SIZE]; volatile uint8_t rx_complete = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { rx_complete = 1; } } int main(void) { HAL_Init(); SystemClock_Config(); SCB_EnableICache(); SCB_EnableDCache(); MPU_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE); while (1) { if (rx_complete) { rx_complete = 0; // 处理数据,直接访问 rx_buffer,无需 Cache 操作 ProcessData(rx_buffer, BUFFER_SIZE); // 重新启动 DMA 接收 HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE); } } } ``` # 注意事项 - **缓冲区对齐**:DMA 缓冲区建议 32 字节对齐,以满足 MPU 区域对齐要求。可使用 `__attribute__((aligned(32)))`。 - **区域大小**:MPU 区域大小必须是 2 的幂次,且最小 32 字节。配置时确保区域不重叠。 - **链接脚本**:非缓存段必须放在正确的内存区域,否则 MPU 配置无效。 - **性能权衡**:非缓存区域访问速度较慢,但 DMA 操作频繁时,避免 Cache 维护开销,整体性能更优。 - **多核场景**:若使用双核(CM7+CM4),需考虑共享内存的一致性,MPU 配置需同时应用于两个内核。 - **调试技巧**:若仍出现数据异常,可临时关闭 D-Cache 测试,确认问题是否由 Cache 引起。 # 总结 STM32H7 的 Cache 一致性是高性能开发中不可回避的挑战。通过 MPU 将 DMA 缓冲区配置为不可缓存,可以彻底消除一致性问题,简化代码逻辑。本文提供了完整的配置流程和代码示例,帮助你在 480MHz 主频下稳定运行。记住,合理利用 MPU 是嵌入式工程师的必备技能,希望本文能助你避开陷阱,发挥 H7 的极致性能。