一、为什么 DMA 遇上 D-Cache 会出错?
STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,为了匹配高速内核与相对较慢的存储器,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会先将数据从 SRAM 加载到 Cache 中,后续读写直接操作 Cache,从而大幅提升性能。
但 DMA 是独立于 CPU 的外设,它直接访问物理内存(SRAM),不经过 D-Cache。这就导致了经典的一致性矛盾:
- CPU 写,DMA 读:CPU 写入的数据可能还停留在 Cache 中(Write-Back 模式),未同步到 SRAM,DMA 读到的是旧数据。
- DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 的 Cache 中可能还保留着该地址的旧数据,CPU 读到的仍是旧值。
解决这一问题的核心手段有两个:MPU 配置内存属性 和 手动维护 Cache 一致性(Clean/Invalidate)。
二、MPU 配置:为 DMA 缓冲区划定“安全区”
MPU(Memory Protection Unit)允许我们将特定内存区域配置为不同的访问属性。对于 DMA 缓冲区,通常有两种策略:
2.1 方案一:将缓冲区配置为 Non-Cacheable
这是最简单粗暴的方法。将该区域标记为不可缓存,CPU 和 DMA 都直接访问 SRAM,从根本上避免一致性问题。缺点是 CPU 访问速度会下降。
// MPU 配置示例:将 0x30000000 开始的 32KB 区域设为 Non-Cacheable
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
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_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
2.2 方案二:Write-Through 模式
将区域配置为 Write-Through(透写),CPU 写操作会同时更新 Cache 和 SRAM。这样 DMA 读时能拿到最新数据,但 DMA 写时 CPU Cache 仍可能有过时数据,需要配合 Invalidate 操作。
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // Write-Through
三、Clean 与 Invalidate 的精确边界
当无法使用 Non-Cacheable 区域时,必须在 DMA 传输前后手动维护 Cache。关键是要理解 Clean 和 Invalidate 的语义:
- Clean:将 Cache 中已修改的数据写回 SRAM。用于 CPU 写 → DMA 读 场景。
- Invalidate:丢弃 Cache 中的数据,强制下次读取从 SRAM 加载。用于 DMA 写 → CPU 读 场景。
3.1 发送数据(CPU → DMA)
// 准备发送数据
memcpy(tx_buffer, source, length);
// 关键:Clean 操作,确保数据写入 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, length);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, length);
3.2 接收数据(DMA → CPU)
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, length);
// 在 DMA 完成中断中:
void DMA_Complete_Callback(void)
{
// 关键:Invalidate 操作,丢弃 Cache 中的旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, length);
// 现在 CPU 可以安全读取新数据
process_data(rx_buffer, length);
}
3.3 地址对齐要求
Cortex-M7 的 Cache 操作要求地址按 32 字节对齐。如果缓冲区未对齐,SCB_CleanDCache_by_Addr 等函数可能无法正确工作。建议使用 __attribute__((aligned(32))) 声明缓冲区:
__attribute__((aligned(32))) uint8_t tx_buffer[256];
__attribute__((aligned(32))) uint8_t rx_buffer[256];
四、完整实战:UART DMA 收发
以下代码展示了在 STM32H7 上使用 UART DMA 收发时,如何正确维护 Cache 一致性。
#include "stm32h7xx_hal.h"
#define BUFFER_SIZE 128
__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;
// 发送函数
void UART_Send_Data(uint8_t *data, uint16_t len)
{
if (len > BUFFER_SIZE) return;
memcpy(tx_buffer, data, len);
// Clean D-Cache,确保数据写入 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, len);
HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// Invalidate D-Cache,丢弃旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);
// 处理接收到的数据
Process_Received_Data(rx_buffer, BUFFER_SIZE);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}
}
// 初始化 MPU(将 DMA 缓冲区所在区域设为 Non-Cacheable 或 Write-Through)
void System_MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 假设缓冲区位于 0x30000000 (SRAM1)
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_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // Non-Cacheable
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
五、注意事项与避坑指南
- 地址对齐:所有 Cache 维护函数的地址必须 32 字节对齐,长度建议为 32 的整数倍。
- 避免过度 Invalidate:Invalidate 会丢弃 Cache 中所有未写回的数据,如果该区域包含 CPU 刚写入但未 Clean 的数据,会导致数据丢失。
- DMA 描述符对齐:如果使用链表式 DMA,描述符本身也需注意对齐和 Cache 问题。
-
中断中调用:
SCB_CleanDCache_by_Addr等函数执行时间较长,在高速中断中频繁调用可能影响实时性,建议合理规划缓冲区大小。 - 多缓冲区策略:使用双缓冲(Ping-Pong)时,确保每个缓冲区都正确维护 Cache。
- 调试技巧:如果数据异常,可先关闭 D-Cache 验证是否为一致性问题,再逐步开启并添加维护操作。
六、总结
STM32H7 的 D-Cache 与 DMA 数据一致性是嵌入式开发中的经典难题。核心解决思路是:要么让 DMA 缓冲区不可缓存(MPU 配置),要么在正确的时间点执行 Clean/Invalidate。理解 Clean 和 Invalidate 的语义边界,结合 32 字节对齐要求,就能在享受 D-Cache 高性能的同时,确保 DMA 数据传输的绝对可靠。