# STM32H7实战:AXI SRAM与TCM间DMA传输的缓存一致性处理 ## 一、为什么需要关注缓存一致性? STM32H7系列(如H743、H750)内置了强大的存储系统: - **TCM**(Tightly Coupled Memory):ITCM和DTCM,零等待访问,但**不支持DMA**。 - **AXI SRAM**:主存储区,支持DMA,但通过AXI总线连接,**默认启用L1-Cache**。 - **L1-Cache**:CPU核心的缓存,分为I-Cache和D-Cache,用于加速对AXI SRAM的访问。 当CPU通过D-Cache写入AXI SRAM,而DMA外设直接读写AXI SRAM时,双方看到的数据可能不一致。例如: - CPU写数据到AXI SRAM(数据暂存在Cache中),DMA读取时可能读到旧数据。 - DMA写入新数据到AXI SRAM,CPU读取时可能命中Cache中的旧数据。 这种不一致会导致通信错误、图像花屏、控制失灵等严重问题。 ## 二、硬件架构与问题根源 STM32H7的存储映射简化图如下: ``` +----------------+ +----------------+ | CPU | | DMA控制器 | | +----------+ | | | | | L1-Cache| | | | | +----+-----+ | | | +-------|--------+ +--------|-------+ | | v v +----+----+ +----+----+ | AXI SRAM| | AXI SRAM| +---------+ +---------+ ``` - **CPU路径**:CPU访问AXI SRAM时,先经过L1-Cache,Cache命中则直接返回,不访问物理SRAM。 - **DMA路径**:DMA是总线主设备,直接访问AXI SRAM,不经过CPU的Cache。 因此,当CPU和DMA并发访问同一内存区域时,必须手动维护Cache的一致性。 ## 三、解决方案实战 ### 方案1:Cache清理与失效(最直接) 在DMA传输前,CPU需将Cache中的数据写回SRAM(Clean);传输完成后,使Cache失效,强制从SRAM重新加载。 **配置步骤:** 1. 使能D-Cache(默认在SystemInit中已使能)。 2. 使用CMSIS提供的函数:`SCB_CleanDCache()`、`SCB_InvalidateDCache()`。 3. 确保缓冲区地址按32字节对齐(Cache line大小)。 **代码示例:** ```c // 缓冲区定义(注意对齐) __ALIGNED(32) uint8_t tx_buffer[1024]; __ALIGNED(32) uint8_t rx_buffer[1024]; void DMA_Transfer(void) { // 填充tx_buffer memcpy(tx_buffer, "Hello DMA", 10); // 1. 清理Cache,确保数据写入SRAM SCB_CleanDCache(); // 2. 启动DMA传输(假设使用DMA2D或DMA1) HAL_DMA_Start(&hdma, (uint32_t)tx_buffer, (uint32_t)rx_buffer, 1024); HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 1000); // 3. 使Cache失效,让CPU读取SRAM中的最新数据 SCB_InvalidateDCache(); // 现在rx_buffer中的数据是有效的 } ``` **注意:** 如果缓冲区较大,建议使用区域操作函数提高效率: - `SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize)` - `SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)` ### 方案2:MPU配置(将区域设为非缓存) 通过MPU将AXI SRAM的特定区域配置为**非缓存(Non-cacheable)**,这样CPU访问该区域时直接操作SRAM,无需手动维护。 **配置步骤:** 1. 初始化MPU,设置区域属性。 2. 使能MPU。 **代码示例:** ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; // 禁用MPU进行配置 HAL_MPU_Disable(); // 配置区域:例如,将0x24000000开始的64KB设为非缓存 MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE; // 关键:非缓存 MPU_InitStruct.IsShareable = MPU_REGION_NOT_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); // 使能MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` **优点:** 无需每次传输都手动清理Cache,适合高频DMA场景。 **缺点:** 牺牲了该区域的缓存性能,适用于对实时性要求高但数据量不大的场景。 ### 方案3:使用非缓存DMA缓冲区(推荐) 在链接脚本中定义独立的非缓存段,将DMA缓冲区放置其中,彻底避免一致性问题。 **配置步骤:** 1. 修改链接脚本(.ld文件),添加非缓存段。 2. 在代码中声明缓冲区属性。 **链接脚本示例(GCC):** ```c /* 在SECTIONS段中添加 */ .nocache (NOLOAD) : { . = ALIGN(32); *(.nocache) . = ALIGN(32); } >RAM_D1 ``` **代码声明:** ```c __attribute__((section(".nocache"))) uint8_t dma_buffer[1024]; ``` **优点:** 无需MPU配置,也无需手动Cache操作,性能最佳。 **缺点:** 需要修改链接脚本,对新手有一定门槛。 ## 四、完整实战示例(结合UART DMA) 以下示例演示使用方案1(Cache清理/失效)实现UART DMA接收不定长数据。 ```c #include "stm32h7xx_hal.h" #define RX_BUF_SIZE 256 __ALIGNED(32) uint8_t rx_buffer[RX_BUF_SIZE]; volatile uint8_t rx_complete = 0; void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance == USART1) { // DMA接收完成,使Cache失效,确保读取最新数据 SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, Size); rx_complete = 1; } } void UART_DMA_Init(void) { // ... UART和DMA初始化代码(略) // 启动DMA接收(空闲中断模式) HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, RX_BUF_SIZE); } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); // 使能D-Cache(如果未在SystemInit中使能) SCB_EnableDCache(); UART_DMA_Init(); while (1) { if (rx_complete) { // 处理rx_buffer中的数据 rx_complete = 0; } } } ``` ## 五、注意事项与最佳实践 - **缓冲区对齐**:DMA缓冲区必须32字节对齐,否则Cache操作可能失败或效率低下。 - **区域操作**:尽量使用`SCB_CleanDCache_by_Addr`和`SCB_InvalidateDCache_by_Addr`,避免全缓存操作影响性能。 - **MPU配置**:如果使用MPU,确保区域大小和基地址正确,且不与其他配置冲突。 - **双核场景**:在H745/H755等双核芯片中,两个CPU共享AXI SRAM,还需考虑核间通信的缓存一致性,建议使用非缓存共享内存。 - **调试技巧**:使用硬件调试器观察Cache和SRAM内容,确认一致性操作是否生效。 ## 六、总结 缓存一致性是STM32H7高性能开发的必修课。通过本文的三种方案,你可以根据项目需求选择最合适的方法: - **方案1**:简单直接,适合低频传输。 - **方案2**:MPU配置,适合需要平衡性能与一致性的场景。 - **方案3**:非缓存段,适合高频、大数据量传输,是工业级推荐做法。 掌握这些技巧,你的嵌入式系统将更加健壮,远离数据错乱的噩梦。