一、问题现象:DMA 传输的数据为何“错位”?
在 STM32H7 上使用 DMA 搬运数据时,常遇到以下现象:
- CPU 填充发送缓冲区后启动 DMA,接收端收到的却是旧数据或部分随机值。
- DMA 接收完成后,CPU 读取缓冲区,发现数据没有更新,或只有前几个字节正确。
- 调试时单步运行正常,全速运行就出错。
这些现象大多源于 D-Cache 与 DMA 的一致性冲突。STM32H7 的 Cortex-M7 内核带有 D-Cache,CPU 访问数据时可能只读写 Cache,而不直接访问物理内存。DMA 是独立于 CPU 的总线主设备,它直接访问物理内存,不经过 Cache。
二、原理:D-Cache 如何导致数据错位
2.1 写缓冲与回写策略
Cortex-M7 的 D-Cache 支持写通(Write-Through)和回写(Write-Back)两种策略。默认或常用配置为 回写+写分配:
- CPU 写数据时,若 Cache 命中,只更新 Cache 行,并标记为“脏”(Dirty),不立即写回内存。
- 只有 Cache 行被替换或显式清理时,脏数据才写回物理内存。
2.2 DMA 与 Cache 的冲突场景
场景一:CPU 写,DMA 读(发送)
- CPU 填充发送缓冲区
tx_buf,数据可能只写入 D-Cache,内存中仍是旧值。 - CPU 启动 DMA 发送,DMA 从物理内存读取
tx_buf,读到旧数据。 - 结果:发送数据错位。
场景二:DMA 写,CPU 读(接收)
- DMA 将接收数据写入物理内存
rx_buf。 - CPU 读取
rx_buf,若该地址在 Cache 中有旧缓存行,则直接返回旧值,不访问内存。 - 结果:CPU 读到旧数据。
三、解决方案:MPU 配置回写策略
解决一致性问题的核心是:让 DMA 缓冲区所在的内存区域不经过 Cache,或使用正确的 Cache 维护操作。STM32H7 提供了 MPU(内存保护单元),可以按区域配置内存属性。
3.1 配置思路
- 将 DMA 缓冲区放在一个专门的 MPU 区域。
- 将该区域配置为 Normal, Non-cacheable 或 Normal, Write-Through。
- 若必须使用 Cache 提升性能,则需在 DMA 传输前后手动执行 Cache 清理/无效化操作。
本文推荐 Non-cacheable 策略,简单可靠,适合大多数 DMA 应用。
3.2 MPU 配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;并调用ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk); - 定义 MPU 区域,设置基地址、大小、属性。
- 将 DMA 缓冲区链接到该区域。
3.3 完整代码示例
以下代码基于 STM32H7 HAL 库,使用 MPU 将 0x30000000 开始的 64KB 区域配置为 Non-cacheable,用于 DMA 缓冲区。
#include "stm32h7xx_hal.h"
/* DMA 缓冲区定义在 Non-cacheable 区域 */
#define DMA_BUFFER_ADDR 0x30000000
#define DMA_BUFFER_SIZE (64 * 1024)
__attribute__((section(".dma_buffer"))) __attribute__((aligned(32)))
uint8_t dma_tx_buf[1024];
uint8_t dma_rx_buf[1024];
/* MPU 配置函数 */
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
/* 配置 DMA 缓冲区区域:0x30000000,64KB,Non-cacheable */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* 使能 MPU,并允许背景区域 */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
/* 链接脚本中需将 .dma_buffer 段放到 0x30000000 */
/* 例如在 STM32H7 的 .ld 文件中添加:
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
} > RAM_D2
*/
/* DMA 发送示例 */
void DMA_SendData(void)
{
/* 填充数据,由于缓冲区 Non-cacheable,CPU 写入直接到内存 */
for (int i = 0; i < 1024; i++) {
dma_tx_buf[i] = i & 0xFF;
}
/* 启动 DMA 发送,DMA 从物理内存读取,数据一致 */
HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)dma_tx_buf,
(uint32_t)&huart1.Instance->TDR, 1024);
}
/* DMA 接收示例 */
void DMA_ReceiveData(void)
{
/* 启动 DMA 接收,DMA 写入物理内存 */
HAL_DMA_Start(&hdma_usart1_rx, (uint32_t)&huart1.Instance->RDR,
(uint32_t)dma_rx_buf, 1024);
/* 等待传输完成 */
HAL_DMA_PollForTransfer(&hdma_usart1_rx, HAL_DMA_FULL_TRANSFER, 1000);
/* CPU 读取缓冲区,由于 Non-cacheable,直接读取内存,数据一致 */
for (int i = 0; i < 1024; i++) {
process_data(dma_rx_buf[i]);
}
}
四、替代方案:Cache 维护操作
若 DMA 缓冲区必须使用 Cache(例如性能敏感),则需在传输前后执行 Cache 维护:
-
发送前:调用
SCB_CleanDCache_by_Addr()将脏数据写回内存。 -
接收后:调用
SCB_InvalidateDCache_by_Addr()无效化 Cache,强制 CPU 从内存读取。
/* 发送前清理 Cache */
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, sizeof(dma_tx_buf));
/* 接收后无效化 Cache */
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
注意:SCB_InvalidateDCache_by_Addr 的地址和大小必须按 32 字节对齐,否则可能误伤相邻数据。
五、注意事项与避坑指南
- MPU 区域大小必须为 2 的幂,且基地址按大小对齐。
- DMA 缓冲区地址和大小建议 32 字节对齐,避免 Cache 行边界问题。
-
使用
__attribute__((aligned(32)))确保变量对齐。 - 链接脚本中正确放置段,否则变量可能仍在 Cacheable 区域。
- 不要对同一块内存同时使用 MPU Non-cacheable 和手动 Cache 维护,避免重复操作。
- 多核或双 Bank 场景下,注意总线矩阵和 Cache 一致性协议,必要时使用硬件信号量。
- 调试时可临时关闭 D-Cache 验证问题,但正式产品不建议关闭,以免性能下降。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典陷阱。通过 MPU 将 DMA 缓冲区配置为 Non-cacheable,可以从根本上避免数据错位,代码简单且可靠。若追求极致性能,也可采用 Cache 清理/无效化操作,但必须严格对齐并注意调用时机。理解 Cache 与 DMA 的交互原理,是写出稳定高效 STM32H7 程序的关键。