STM32H7实战:AXI SRAM与TCM间DMA传输的缓存一致性处理
👁 3 阅读 · 2026-08-29 · 嵌入式
STM32H7系列凭借双核架构和高速总线,在嵌入式领域广受欢迎,但其复杂的存储系统(AXI SRAM、TCM、L1-Cache)常让开发者陷入缓存一致性的泥潭。本文从硬件架构出发,剖析DMA传输时缓存不一致的根源,并给出三种实战解决方案:Cache清理/失效、MPU配置、以及免缓存DMA缓冲区设计。通过完整代码示例,助你彻底告别数据错乱,提升系统稳定性。
# 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**:非缓存段,适合高频、大数据量传输,是工业级推荐做法。
掌握这些技巧,你的嵌入式系统将更加健壮,远离数据错乱的噩梦。