STM32H7 的 D-Cache 与 DMA 数据一致性:按 Cache Line 对齐缓冲区的实战配置与踩坑记录
一、为什么 DMA 和 D-Cache 会打架?
STM32H7 主频高达 480MHz,为了匹配内核速度,芯片内置了 L1 D-Cache(数据缓存)。CPU 访问数据时,若命中 Cache 则直接读写缓存,不会立即同步到 SRAM。而 DMA 控制器是独立于 CPU 的总线主设备,它直接访问物理 SRAM,完全绕过 Cache。
这就导致两个经典问题:
- DMA 写入 SRAM 后,CPU 读到的却是 Cache 中的旧数据(因为 Cache 未更新)。
- CPU 修改了 Cache 中的数据但未写回 SRAM,DMA 却从 SRAM 读到了旧数据。
更隐蔽的是,D-Cache 的操作以 Cache Line 为单位(Cortex-M7 为 32 字节)。即使你只修改了 1 个字节,Clean 操作也会把整个 32 字节写回 SRAM;Invalidate 则会丢弃整个 Line 的缓存内容。如果缓冲区没有按 32 字节对齐,一次 Invalidate 可能误伤相邻变量,导致程序跑飞。
二、核心原则:按 Cache Line 对齐 + 正确维护
解决思路分三步:
- 将 DMA 缓冲区强制对齐到 32 字节边界,并确保其大小是 32 字节的整数倍。
- 配置 MPU,将 DMA 缓冲区所在区域设为 Write-Through 或 Non-Cacheable,从根源避免一致性问题(推荐)。
- 若必须使用 Cacheable 内存,则在 DMA 传输前后正确执行 Clean 和 Invalidate 操作。
三、实战配置步骤
3.1 定义对齐的 DMA 缓冲区
使用 __attribute__((aligned(32))) 强制对齐,并用 __IO 防止编译器优化。
#include "stm32h7xx.h"
#define DMA_BUF_SIZE 256
// 接收缓冲区,32 字节对齐
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
// 发送缓冲区,32 字节对齐
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
3.2 配置 MPU 将缓冲区设为 Non-Cacheable
这是最省心的方案。以 STM32H743 为例,将 SRAM 中某块区域(如 0x30000000 起始的 64KB)配置为 Non-Cacheable。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域 0:DMA 缓冲区专用,Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM 起始
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_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
将 DMA 缓冲区链接到该区域(修改链接脚本),或直接使用该地址定义指针。
3.3 若必须使用 Cacheable 内存:Clean 与 Invalidate
对于 CPU 写、DMA 读(发送):在启动 DMA 前 Clean 缓冲区,确保数据写入 SRAM。
// 发送前:将 Cache 数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, DMA_BUF_SIZE);
对于 DMA 写、CPU 读(接收):在 DMA 完成中断中 Invalidate 缓冲区,丢弃 Cache 旧数据,强制 CPU 从 SRAM 重新读取。
void DMA_RxComplete_Callback(void)
{
// 接收完成:无效化 Cache,确保 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);
// 此时再处理 dma_rx_buf 中的数据
ProcessData(dma_rx_buf, DMA_BUF_SIZE);
}
注意:SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度建议为 32 的倍数,否则会误伤相邻数据。
四、踩坑记录:一个字节引发的血案
坑 1:缓冲区未对齐导致 HardFault
早期项目定义 uint8_t rx_buf[100];,未加对齐属性。调用 SCB_InvalidateDCache_by_Addr 时,函数内部会向下对齐到 32 字节边界,导致 rx_buf 前面的变量被无效化。若该变量恰好是栈上的指针,后续解引用直接 HardFault。解决:所有 DMA 缓冲区必须 aligned(32),且长度设为 32 的倍数。
坑 2:DMA 传输中 CPU 访问缓冲区
在 DMA 传输进行中,CPU 若读取接收缓冲区,可能触发 Cache 预取,将旧数据填入 Cache。等 DMA 完成后再 Invalidate 虽然能解决,但若在传输中读取并使用了数据,就会得到错误值。解决:DMA 传输期间禁止 CPU 访问该缓冲区,或使用双缓冲区交替。
坑 3:忘记 Clean 导致发送数据错乱
使用 HAL_UART_Transmit_DMA 发送一个结构体,CPU 刚填充完数据,Cache 中还是脏的,DMA 直接从 SRAM 读到了旧内容。解决:发送前务必 SCB_CleanDCache_by_Addr。
坑 4:MPU 配置与链接脚本冲突
将 MPU 区域设为 Non-Cacheable 后,却忘记把缓冲区放到该地址,结果配置无效。解决:使用 __attribute__((section(".dma_buffer"))) 配合链接脚本,或直接定义指针指向 MPU 区域。
五、总结与最佳实践
- 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。
- 若使用 Cacheable 内存,缓冲区必须 32 字节对齐,长度取 32 的倍数。
- 发送前 Clean,接收后 Invalidate,顺序不能错。
- 避免在 DMA 传输期间 CPU 访问同一缓冲区。
- 使用
SCB_InvalidateDCache_by_Addr时,地址和长度都要对齐,否则误伤无辜。
掌握这些要点,STM32H7 的 D-Cache 与 DMA 就能和谐共处,让你的高性能应用稳定驰骋。