一、为什么 D-Cache 会让 DMA 数据“错乱”?
STM32H7 的 Cortex-M7 内核带有一级数据缓存(D-Cache),默认写回(Write-Back)策略。CPU 访问内存时,数据可能只停留在 Cache 中,并未写入实际 RAM。而 DMA 直接访问物理内存,它看不到 Cache 里的新数据。
典型场景:
- CPU 写、DMA 读:CPU 写入缓冲区后立即启动 DMA 发送,但数据还在 Cache 中,DMA 读到的是旧值。
- DMA 写、CPU 读:DMA 将外设数据写入 RAM,但 CPU 读取时命中 Cache 中的旧数据,看不到新内容。
根本矛盾:Cache 与 DMA 对内存的视图不一致。
二、解决思路:MPU 配置 + Cache 维护
2.1 MPU 配置:将 DMA 缓冲区设为 Non-Cacheable
最彻底的方法是利用 MPU 将 DMA 使用的内存区域配置为 Non-Cacheable(非缓存)。这样 CPU 和 DMA 都直接访问物理内存,无需任何维护操作。
优点:一劳永逸,代码简单。
缺点:该区域访问速度下降,适合对性能不敏感但要求稳定的场景。
配置步骤(以 STM32H7 HAL 库为例):
- 使能 MPU 时钟(在
HAL_MPU_Enable前调用__HAL_RCC_MPU_CLK_ENABLE())。 - 定义 MPU 区域,设置
BaseAddress、Size、Number。 - 属性设为
MPU_ACCESS_NOT_CACHEABLE、MPU_ACCESS_NOT_BUFFERABLE、MPU_ACCESS_SHAREABLE。 - 调用
HAL_MPU_ConfigRegion并HAL_MPU_Enable。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 假设 DMA 缓冲区起始地址 0x30000000,大小 32KB
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
2.2 动态维护:Clean 与 Invalidate
如果缓冲区必须使用 Cache(例如频繁 CPU 读写),则需在 DMA 传输前后手动维护 Cache:
- Clean(清理):将 Cache 中的脏数据写回 RAM。用于 CPU 写 → DMA 读 之前。
- Invalidate(无效化):将 Cache 中的对应行标记为无效,强制下次读取从 RAM 加载。用于 DMA 写 → CPU 读 之后。
关键原则:
- 传输前:若 CPU 写过数据,先 Clean。
- 传输后:若 DMA 写过数据,先 Invalidate 再读。
- 注意:Invalidate 会丢弃 Cache 中未写回的数据,因此必须先 Clean 再 Invalidate 如果同一区域既有 CPU 写又有 DMA 写。
三、完整代码示例:UART DMA 发送与接收
以 UART 空闲中断 + DMA 接收为例,展示 Cache 维护的正确位置。
#include "stm32h7xx_hal.h"
#define RX_BUFFER_SIZE 128
__attribute__((aligned(32))) uint8_t rxBuffer[RX_BUFFER_SIZE];
__attribute__((aligned(32))) uint8_t txBuffer[RX_BUFFER_SIZE];
// 发送前:Clean D-Cache
void UART_Send_DMA(uint8_t *data, uint16_t len)
{
memcpy(txBuffer, data, len);
SCB_CleanDCache_by_Addr((uint32_t *)txBuffer, len);
HAL_UART_Transmit_DMA(&huart1, txBuffer, len);
}
// 接收完成回调(DMA 写入了 rxBuffer)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 先 Invalidate,确保 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuffer, RX_BUFFER_SIZE);
// 处理数据...
process_data(rxBuffer, RX_BUFFER_SIZE);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}
}
注意:SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议为 32 的整数倍。使用 __attribute__((aligned(32))) 确保缓冲区对齐。
四、避坑要点与最佳实践
- 对齐!对齐! Cache 操作以 32 字节为单位。若缓冲区未对齐,维护操作可能影响相邻数据,导致难以调试的随机错误。
- 避免在中断中频繁 Clean/Invalidate:这些操作耗时,可能影响实时性。尽量用 MPU 配置非缓存区。
- DMA 传输期间不要访问缓冲区:否则可能引发 Cache 与 DMA 的竞争。
-
使用
SCB_InvalidateDCache_by_Addr前先 Clean:如果该区域有 CPU 未写回的数据,Invalidate 会直接丢弃,造成数据丢失。 -
MPU 配置与链接脚本配合:将 DMA 缓冲区放在特定段(如
.dma_buffer),再对该段配置 MPU,避免手动计算地址。 - 调试时关闭 D-Cache:若问题诡异,可临时关闭 D-Cache 验证是否为一致性问题。
- STM32H7 的 AXI SRAM 与 DTCM:DTCM 不支持 DMA,且默认非缓存,适合放 DMA 缓冲区,但容量有限(128KB)。
五、总结
D-Cache 与 DMA 的一致性问题是 STM32H7 开发中的经典陷阱。核心解决方案有二:MPU 配置非缓存区(推荐用于固定缓冲区)和手动 Clean/Invalidate(适合动态场景)。无论哪种,都要牢记对齐、时序和竞争条件。掌握这些,你的 H7 项目将既快又稳。