一、问题现象:DMA 数据为何“凭空消失”?
在 STM32H7 上,许多开发者会遇到这样的场景:
- DMA 从外设搬运数据到内存,CPU 读取时却发现数据是旧的。
- CPU 写入内存后启动 DMA 发送,接收端收到的却是乱码。
- 调试时单步执行正常,全速运行却出错。
这些现象大多源于 D-Cache 与 DMA 的一致性冲突。STM32H7 的 Cortex-M7 内核带有 D-Cache,CPU 访问内存时会优先命中 Cache,而 DMA 直接访问物理内存,两者看到的数据可能不同步。
二、原理:为什么 Cache 会“欺骗”你?
- CPU 视角:读写数据经过 D-Cache,若命中则直接操作 Cache,不立即写回内存(Write-Back 模式)。
- DMA 视角:直接读写 SRAM,不经过 Cache。
-
冲突:
- CPU 写数据到 Cache 但未写回,DMA 读取的是旧内存。
- DMA 写入新数据到内存,CPU 读取时却命中旧 Cache。
解决思路只有两个:让 CPU 绕过 Cache 或 在 DMA 传输前后维护 Cache。
三、MPU 配置:为 DMA 缓冲区划定“非缓存”区域
最彻底的方法是将 DMA 缓冲区配置为 Non-Cacheable,CPU 访问时直接读写内存。
3.1 配置步骤
- 使能 MPU:
SCB_EnableMPU()。 - 定义区域基址和大小(需 32 字节对齐)。
- 配置属性:
TEX=1, C=0, B=0, S=0表示 Normal Non-Cacheable。 - 调用
HAL_MPU_ConfigRegion()并启用。
3.2 代码示例
#include "stm32h7xx_hal.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
/* 配置 SRAM1 的 0x30000000 起始 64KB 为非缓存 */
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:链接脚本需将 DMA 缓冲区定位到该区域,或使用
__attribute__((section(".dma_buffer")))。
四、Cache 维护:使用 SCB_CleanDCache 与 Invalidate
若无法使用 Non-Cacheable 区域,则必须在 DMA 传输前后维护 Cache。
4.1 关键函数
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 内容写回内存(CPU 写 → DMA 读)。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重读(DMA 写 → CPU 读)。 -
SCB_CleanInvalidateDCache_by_Addr():先写回再无效化。
4.2 使用时机
-
CPU 写数据后启动 DMA 发送:调用
SCB_CleanDCache_by_Addr()。 -
DMA 接收数据完成后 CPU 读取:调用
SCB_InvalidateDCache_by_Addr()。 - 双向传输:先 Clean 再 Invalidate。
4.3 代码示例
#define DMA_BUFFER_SIZE 1024
__attribute__((aligned(32))) uint8_t dma_buffer[DMA_BUFFER_SIZE];
void DMA_Send_Data(void)
{
// 填充数据
for (int i = 0; i < DMA_BUFFER_SIZE; i++) {
dma_buffer[i] = i & 0xFF;
}
// 清理 D-Cache,确保数据写入内存
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, DMA_BUFFER_SIZE);
// 启动 DMA 发送
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_buffer, (uint32_t)dest, DMA_BUFFER_SIZE);
}
void DMA_Receive_Complete_Callback(void)
{
// DMA 接收完成,无效化 Cache,强制 CPU 从内存读取
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, DMA_BUFFER_SIZE);
// 此时读取 dma_buffer 才是最新数据
process_data(dma_buffer, DMA_BUFFER_SIZE);
}
五、完整实战:串口 DMA 收发一致性处理
以 UART 空闲中断 + DMA 接收为例:
#define RX_BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buffer[RX_BUFFER_SIZE];
void UART_DMA_Init(void)
{
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}
void UART_IDLE_Callback(void)
{
// 停止 DMA 以获取已接收长度
HAL_UART_DMAStop(&huart1);
uint16_t len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
// 无效化 Cache,确保读取到 DMA 写入的最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, len);
// 处理数据
parse_packet(rx_buffer, len);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}
六、注意事项与避坑指南
- 地址与大小对齐:Cache 维护函数要求地址 32 字节对齐,大小建议为 32 的倍数。
- 避免频繁维护:Cache 维护开销较大,尽量将 DMA 缓冲区集中管理。
- 中断优先级:在 DMA 完成中断中调用 Cache 维护函数时,确保中断优先级不冲突。
- 多缓冲区管理:若使用多个 DMA 缓冲区,每个缓冲区都需独立维护。
- 调试陷阱:调试器可能自动执行 Cache 维护,导致问题隐藏,务必全速运行验证。
- MPU 与 Cache 维护二选一:优先使用 MPU 配置 Non-Cacheable 区域,性能更好且代码简洁。
七、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典难题。通过 MPU 配置非缓存区域 或 合理使用 SCB_CleanDCache/InvalidateDCache,可以彻底解决。推荐优先使用 MPU 方案,若受限于内存布局,则务必在 DMA 传输前后正确维护 Cache。掌握这些技巧,你的 H7 应用将既高效又稳定。