一、为什么 D-Cache 会成为 DMA 的“猪队友”?
STM32H7 的 Cortex-M7 内核带有一级 D-Cache(数据缓存),默认写回(Write-Back)策略。CPU 访问内存时,数据可能只停留在 Cache 中,并未写入实际 RAM。而 DMA 控制器直接访问物理 RAM,它看不到 Cache 里的新数据。
- CPU 写,DMA 读:CPU 写入的数据还在 Cache 里,DMA 从 RAM 读到旧数据。
- DMA 写,CPU 读:DMA 把新数据写入 RAM,但 CPU 读到的却是 Cache 中的旧数据。
这就是典型的数据一致性问题。解决思路有两种:
- 将 DMA 缓冲区所在内存区域配置为非缓存(Non-Cacheable),通过 MPU 实现。
- 在 DMA 传输前后手动执行 Clean(写回)和 Invalidate(无效化)操作。
二、MPU 配置:一劳永逸的非缓存方案
使用 MPU 将特定内存区域(如 SRAM 中的 DMA 缓冲区)设为 Non-Cacheable,可避免手动维护缓存。
2.1 配置步骤
- 使能 MPU:
SCB_EnableMPU()前需先SCB_EnableICache()和SCB_EnableDCache()。 - 定义 MPU 区域:设置基地址、大小、属性。
- 关键属性:
TEX=0, C=0, B=0, S=1表示 Non-Cacheable, Shareable。
2.2 代码示例
#include "stm32h7xx.h"
#define DMA_BUFFER_ADDR 0x30000000 // AXI SRAM 起始地址
#define DMA_BUFFER_SIZE 0x10000 // 64KB
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 AXI SRAM 前 64KB 为非缓存
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 区域大小必须是 2 的幂,且基地址对齐。若缓冲区跨区域,需拆分配置。
三、手动 Clean/Invalidate:灵活但易错
若不想牺牲 Cache 性能,可在 DMA 传输前后手动操作。
3.1 核心 API
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 数据写回 RAM。 -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 数据,强制从 RAM 重新加载。
3.2 正确使用顺序
CPU 写 → DMA 读(发送):
- CPU 填充缓冲区。
-
SCB_CleanDCache_by_Addr()写回。 - 启动 DMA 发送。
DMA 写 → CPU 读(接收):
- 启动 DMA 接收。
- DMA 完成中断中调用
SCB_InvalidateDCache_by_Addr()。 - CPU 读取缓冲区。
3.3 代码示例
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[BUF_SIZE];
void DMA_Send_Data(void)
{
// 填充数据
for (int i = 0; i < BUF_SIZE; i++) {
dma_tx_buf[i] = i;
}
// Clean:确保数据写入 RAM
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, BUF_SIZE);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, BUF_SIZE);
}
void DMA_Receive_Data(void)
{
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// Invalidate:丢弃 Cache 旧数据,从 RAM 重新加载
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, BUF_SIZE);
// 此时读取才是 DMA 写入的新数据
for (int i = 0; i < BUF_SIZE; i++) {
printf("%02X ", dma_rx_buf[i]);
}
}
四、踩坑记录与注意事项
4.1 地址对齐与大小
-
SCB_CleanDCache_by_Addr的地址必须 32 字节对齐,大小建议为 32 的倍数。否则可能遗漏部分 Cache 行。 - 使用
__attribute__((aligned(32)))修饰缓冲区。
4.2 Invalidate 的陷阱
- Invalidate 会丢弃未写回的数据。若 CPU 刚修改了缓冲区,紧接着 Invalidate,修改将丢失。务必先 Clean 再 Invalidate。
- 对于 DMA 接收,Invalidate 前确保 CPU 没有未写回的数据。
4.3 多缓冲区与 Cache 行伪共享
- 若两个缓冲区共享同一个 Cache 行(32 字节),Invalidate 一个可能影响另一个。建议缓冲区按 32 字节对齐且大小对齐。
4.4 MPU 配置的优先级
- MPU 区域可重叠,高编号区域优先级更高。确保 DMA 缓冲区区域不被其他缓存区域覆盖。
4.5 性能权衡
- 非缓存区域会降低 CPU 访问速度。若缓冲区访问频繁,建议仅对 DMA 缓冲区使用非缓存,其他内存保持缓存。
五、总结
STM32H7 的 D-Cache 与 DMA 数据一致性是嵌入式开发中的经典难题。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须使用 Cache,则严格遵循 Clean/Invalidate 顺序,并注意对齐与伪共享问题。掌握这些技巧,你的 H7 项目将既高效又稳定。