为什么 D-Cache 会让 DMA 数据“变味”?
STM32H7 主频高达 480MHz,为了匹配 CPU 与存储器速度,Cortex-M7 内核配备了 L1 D-Cache(通常 16KB 或 32KB)。当 CPU 访问 SRAM 或外部 SDRAM 时,数据会被缓存到 D-Cache 中。
问题来了:DMA 是独立于 CPU 的总线主设备,它直接读写物理内存,不经过 D-Cache。于是出现两种典型错误:
- DMA 写入内存,CPU 读到的却是旧缓存:CPU 之前读过的数据还在 Cache 里,DMA 更新了物理内存,但 CPU 仍从 Cache 取旧值。
- CPU 写入数据,DMA 发送的却是旧值:CPU 写数据只更新了 Cache(写回策略),物理内存尚未更新,DMA 却从物理内存取数据发送。
解决思路只有两条:要么让 DMA 缓冲区不经过 Cache(MPU 配置为 Non-Cacheable),要么在恰当时机手动维护 Cache 一致性(Clean/Invalidate)。
地址对齐:32 字节是硬性要求
Cortex-M7 的 Cache 行(Cache Line)大小为 32 字节。Clean 和 Invalidate 操作的最小单位就是一行。
- 如果 DMA 缓冲区起始地址或长度不是 32 字节对齐,维护 Cache 时可能误伤相邻数据,导致其他变量被意外丢弃或写回。
- 强烈建议:所有 DMA 缓冲区使用
__attribute__((aligned(32)))强制 32 字节对齐,且长度向上取整到 32 的倍数。
// 定义 32 字节对齐的 DMA 缓冲区
#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[BUF_SIZE];
MPU 配置:让 DMA 区域“绕过”Cache
最省心的方案是用 MPU 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable, Non-Shareable。这样 CPU 和 DMA 都直接访问物理内存,无需任何 Clean/Invalidate 操作。
以 STM32H7 的 D2 SRAM(0x30000000)为例,配置 MPU 区域:
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 D2 SRAM 区域为 Non-Cacheable
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;
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_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 配置必须在使能 D-Cache 之前完成,且区域不能与内核默认属性冲突。
Clean 与 Invalidate 的实战顺序
如果因性能原因必须使用 Cacheable 内存,就必须手动维护一致性。核心原则:
- CPU 写 → DMA 读(发送方向):先 Clean(将 Cache 写回内存),再启动 DMA。
- DMA 写 → CPU 读(接收方向):先 Invalidate(丢弃 Cache 旧数据),再启动 DMA;DMA 完成后,再次 Invalidate 才能安全读取。
发送方向(TX)
// 1. 填充数据到 dma_tx_buf
memcpy(dma_tx_buf, src, len);
// 2. Clean D-Cache,确保数据写入物理内存
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, BUF_SIZE);
// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, BUF_SIZE);
接收方向(RX)
// 1. 启动 DMA 前,Invalidate 缓冲区,丢弃旧缓存
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);
// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, BUF_SIZE);
// 3. DMA 完成中断中,再次 Invalidate 才能读取最新数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);
// 现在可以安全处理 dma_rx_buf 中的数据
}
完整示例:UART DMA 收发 + Cache 维护
#include "stm32h7xx.h"
#include "stm32h7xx_hal.h"
#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[BUF_SIZE];
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
void DMA_Config(void)
{
// 使能 DMA 时钟(略)
// 配置 DMA 流,方向、外设地址、内存地址、数据长度等
// 注意:内存地址必须 32 字节对齐
}
void UART_Init(void)
{
// UART 初始化(略)
}
void Send_Data(uint8_t *src, uint16_t len)
{
if (len > BUF_SIZE) len = BUF_SIZE;
memcpy(dma_tx_buf, src, len);
// 清理 D-Cache,确保 DMA 读到最新数据
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}
void Start_Receive(void)
{
// 无效化 D-Cache,避免 CPU 读到旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, BUF_SIZE);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 再次无效化,确保读到 DMA 写入的最新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);
// 处理数据...
// 重新启动接收
Start_Receive();
}
int main(void)
{
HAL_Init();
MPU_Config(); // 先配置 MPU
SCB_EnableICache(); // 使能 I-Cache
SCB_EnableDCache(); // 使能 D-Cache
UART_Init();
DMA_Config();
Start_Receive();
uint8_t msg[] = "Hello STM32H7 DMA + Cache\r\n";
Send_Data(msg, sizeof(msg) - 1);
while (1) { }
}
注意事项与避坑指南
- 不要对同一缓冲区同时 Clean 和 Invalidate:除非你明确知道数据流向,否则可能丢失未写回的数据。
- DMA 完成中断中必须再次 Invalidate:否则 CPU 可能读到 Cache 中的旧数据。
- 避免在 DMA 传输过程中操作 Cache:可能引发数据竞争。
-
使用
SCB_*函数时地址必须 32 字节对齐:否则行为未定义。 - 如果使用外部 SDRAM,务必配置 MPU 为 Write-Through 或 Non-Cacheable:否则性能与一致性都难以保证。
- 调试时若发现数据“偶尔”错误,优先检查 Cache 一致性:这是 H7 上最常见的隐蔽 bug。
掌握以上三点——对齐、MPU、Clean/Invalidate 顺序——你就能在 STM32H7 上放心地同时享受 D-Cache 的高性能和 DMA 的高效率。