STM32H7 的 D-Cache 与 DMA 一致性坑:按行清理与按地址清理的实测差异与选型建议
STM32H7 主频高达 480MHz,为了匹配内核与存储器速度,ST 引入了 L1 D-Cache。但 DMA 直接访问物理内存,不经过 Cache,若两者操作同一块缓冲区,就会出现数据不一致。很多开发者知道要调用 SCB_CleanDCache_by_Addr() 或 SCB_InvalidateDCache_by_Addr(),却忽略了 Cache 按行操作 的本质,导致“清理了却没用”甚至数据被意外覆盖。
一、原理:Cache Line 是操作的最小单位
Cortex-M7 的 D-Cache 行大小为 32 字节。任何 Cache 维护操作(Clean/Invalidate)都以行为单位。当你传入一个地址和长度时,内核会自动对齐到行边界,并操作覆盖该范围的所有行。
- Clean:将 Cache 中已修改的数据写回物理内存。
- Invalidate:丢弃 Cache 中的数据,下次读取时从物理内存重新加载。
- Clean+Invalidate:先写回再丢弃。
关键点:如果你只清理了变量本身,但变量与相邻数据共享同一 Cache Line,那么相邻数据可能被意外写回或丢弃。这就是“按地址清理”与“按行清理”差异的根源。
二、实测差异:一个 ADC+DMA 的典型场景
假设我们使用 ADC1 连续扫描 4 个通道,DMA 循环模式将数据搬运到 adc_buf[4]。缓冲区定义如下:
uint16_t adc_buf[4] __attribute__((aligned(32))); // 强制32字节对齐
uint16_t other_var = 0x1234; // 紧邻 adc_buf 的变量
2.1 错误做法:按地址清理(只清理变量大小)
// 错误:只清理了 8 字节,但 Cache Line 是 32 字节
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
实测现象:adc_buf 数据看似正确,但 other_var 可能被意外修改。原因:adc_buf 和 other_var 可能位于同一 Cache Line。Invalidate 操作会丢弃整个行的 Cache 内容,如果 other_var 之前被 CPU 修改但未写回,这些修改就丢失了。
2.2 正确做法:按行清理(对齐并覆盖整行)
// 正确:确保地址 32 字节对齐,长度向上取整到 32 的倍数
#define CACHE_LINE_SIZE 32
#define ALIGN_UP(x, a) (((x) + (a) - 1) & ~((a) - 1))
void dma_invalidate_buffer(void *buf, size_t len) {
uint32_t addr = (uint32_t)buf;
size_t aligned_len = ALIGN_UP(len, CACHE_LINE_SIZE);
SCB_InvalidateDCache_by_Addr((uint32_t*)addr, aligned_len);
}
实测现象:adc_buf 数据正确,other_var 不受影响。但前提是 adc_buf 起始地址必须 32 字节对齐,且长度覆盖到行边界。
三、配置步骤:从 MPU 到代码的完整流程
3.1 启用 D-Cache
SCB_EnableICache();
SCB_EnableDCache();
3.2 使用 MPU 配置 DMA 缓冲区为 Non-Cacheable(推荐)
最彻底的方法是让 DMA 缓冲区不经过 Cache。通过 MPU 将特定内存区域设为 Non-Cacheable:
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 配置 SRAM1 的 0x30000000 起始 64KB 为 Non-Cacheable
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_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);
}
将 DMA 缓冲区放在该区域,则无需手动清理 Cache。
3.3 若必须使用 Cacheable 内存,则严格按行清理
// 发送前 Clean,接收后 Invalidate
void dma_send_prepare(void *buf, size_t len) {
SCB_CleanDCache_by_Addr((uint32_t*)buf, ALIGN_UP(len, 32));
}
void dma_recv_complete(void *buf, size_t len) {
SCB_InvalidateDCache_by_Addr((uint32_t*)buf, ALIGN_UP(len, 32));
}
四、完整代码示例:ADC+DMA 双缓冲
#include "stm32h7xx.h"
#define BUF_SIZE 256
#define CACHE_LINE 32
#define ALIGN_UP(x, a) (((x) + (a) - 1) & ~((a) - 1))
// 双缓冲,强制 32 字节对齐
__attribute__((aligned(32))) uint16_t adc_buf[2][BUF_SIZE];
volatile uint8_t buf_ready[2] = {0};
void ADC_DMA_Init(void) {
// ... ADC 和 DMA 初始化代码(略)
// 配置 DMA 目标地址为 adc_buf[0],循环模式
}
// DMA 传输完成中断回调
void DMA1_Stream0_IRQHandler(void) {
if (DMA1->LISR & DMA_LISR_TCIF0) {
DMA1->LIFCR = DMA_LIFCR_CTCIF0;
uint8_t current = (DMA1_Stream0->CR & DMA_SxCR_CT) ? 1 : 0;
// 按行清理当前缓冲区
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf[current], ALIGN_UP(BUF_SIZE * 2, CACHE_LINE));
buf_ready[current] = 1;
}
}
// 主循环处理
void Process_ADC_Data(void) {
for (int i = 0; i < 2; i++) {
if (buf_ready[i]) {
// 此时 adc_buf[i] 数据已从物理内存加载到 Cache
for (int j = 0; j < BUF_SIZE; j++) {
// 处理 adc_buf[i][j]
}
buf_ready[i] = 0;
}
}
}
五、注意事项与选型建议
- 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable:一劳永逸,避免手动维护。代价是 CPU 访问该区域速度稍慢,但对 DMA 缓冲区通常可接受。
-
若必须使用 Cacheable 内存:
- 缓冲区必须 32 字节对齐,长度向上取整到 32 的倍数。
- 发送前 Clean,接收后 Invalidate,顺序不能错。
- 避免多个变量共享同一 Cache Line,尤其是 DMA 缓冲区与普通变量相邻时。
-
不要使用
SCB_InvalidateDCache()全局失效:会丢弃所有未写回数据,导致程序崩溃。 - 注意 DMA 的突发传输与 Cache Line 的交互:若 DMA 传输长度不是 32 的倍数,末尾可能部分覆盖,建议缓冲区大小设为 32 的倍数。
- 调试时若发现数据“偶尔正确”:大概率是 Cache 一致性问题,可先关闭 D-Cache 验证。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性没有银弹。按行清理 是正确操作 Cache 的唯一方式,但更推荐通过 MPU 将 DMA 缓冲区隔离为 Non-Cacheable。理解 Cache Line 的 32 字节边界,严格对齐与长度处理,才能彻底避开这个隐蔽的坑。