基于 Cortex-M7 的 D-Cache 一致性维护:从 MPU 配置到 DMA 双缓冲实战
👁 3 阅读 · 2026-08-27 · 嵌入式
Cortex-M7 内核引入的 D-Cache 大幅提升了数据访问性能,但也带来了缓存一致性问题,尤其在 DMA 与 CPU 共享数据时。本文从 MPU 配置入手,深入讲解 D-Cache 的工作原理,并给出一个基于 DMA 双缓冲的完整实战示例,帮助开发者彻底掌握 Cache 维护技巧,避免数据错乱。
# 基于 Cortex-M7 的 D-Cache 一致性维护:从 MPU 配置到 DMA 双缓冲实战
## 为什么需要 D-Cache 一致性维护?
Cortex-M7 是首款支持 L1 缓存的 Cortex-M 内核,其 D-Cache 可缓存数据访问,大幅减少对 SRAM 或外部存储器的访问延迟。然而,当 DMA 外设直接访问内存时,CPU 可能已经将数据缓存到 D-Cache 中,而 DMA 读写的是物理内存,这会导致数据不一致:
- **DMA 写入内存**:CPU 缓存中可能保留旧数据,后续读取得到错误值。
- **DMA 读取内存**:CPU 可能尚未将缓存中的数据写回物理内存,DMA 读到旧数据。
因此,必须通过 MPU 配置和软件维护指令(如 `SCB_CleanDCache`、`SCB_InvalidateDCache`)来保证一致性。
## MPU 配置:为 DMA 缓冲区设置非缓存属性
MPU(内存保护单元)可以定义内存区域的缓存策略。对于 DMA 缓冲区,推荐配置为 **非缓存(Non-cacheable)** 或 **写透(Write-through)**,这样 CPU 访问时直接操作物理内存,避免缓存一致性问题。
### 配置步骤
1. **使能 MPU**:在系统初始化时调用 `MPU_Enable()`。
2. **配置区域**:使用 `MPU_Region_InitTypeDef` 结构体设置缓冲区地址、大小、访问权限和缓存属性。
3. **设置缓存策略**:对于 DMA 缓冲区,设置 `MPU_REGION_NO_CACHE` 或 `MPU_REGION_WRITE_THROUGH`。
### 示例代码(基于 STM32H7 HAL 库)
```c
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置 DMA 缓冲区区域(例如地址 0x30000000,大小 32KB)
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
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);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
```
## DMA 双缓冲实战:数据采集与处理
双缓冲(Ping-Pong Buffer)是 DMA 传输中常用的技术,可让 CPU 处理一个缓冲区的同时,DMA 填充另一个缓冲区,提高吞吐量。但若使用 D-Cache,必须正确处理缓存一致性。
### 场景描述
假设使用 ADC 通过 DMA 连续采集数据,数据存入两个缓冲区(BufferA 和 BufferB),CPU 在中断中处理已满的缓冲区。
### 代码实现
```c
#define BUFFER_SIZE 1024
uint32_t BufferA[BUFFER_SIZE] __attribute__((section(".dma_buffer")));
uint32_t BufferB[BUFFER_SIZE] __attribute__((section(".dma_buffer")));
volatile uint8_t buffer_ready = 0;
// DMA 中断回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// 判断当前使用的缓冲区
if (hadc->DMA_Handle->Instance->CR & DMA_SxCR_CT) {
// 当前 DMA 写入 BufferB,CPU 处理 BufferA
// 使 CPU 缓存中的 BufferA 数据失效(因为 DMA 已更新物理内存)
SCB_InvalidateDCache_by_Addr((uint32_t*)BufferA, sizeof(BufferA));
buffer_ready = 1;
} else {
SCB_InvalidateDCache_by_Addr((uint32_t*)BufferB, sizeof(BufferB));
buffer_ready = 2;
}
}
void ProcessBuffer(uint32_t* buf, uint32_t size)
{
// 处理数据,例如计算平均值
uint32_t sum = 0;
for (uint32_t i = 0; i < size; i++) {
sum += buf[i];
}
// 其他处理...
}
int main(void)
{
// 初始化 MPU(配置非缓存区域)
MPU_Config();
// 初始化 ADC 和 DMA(略)
// 启动 DMA 双缓冲传输
HAL_ADC_Start_DMA(&hadc, (uint32_t*)BufferA, (uint32_t*)BufferB, BUFFER_SIZE);
while (1) {
if (buffer_ready == 1) {
ProcessBuffer(BufferA, BUFFER_SIZE);
buffer_ready = 0;
} else if (buffer_ready == 2) {
ProcessBuffer(BufferB, BUFFER_SIZE);
buffer_ready = 0;
}
}
}
```
### 关键点解析
- **MPU 配置非缓存**:如果缓冲区被配置为非缓存,则无需手动调用 `SCB_InvalidateDCache`,但会牺牲缓存性能。
- **手动失效(Invalidate)**:若缓冲区使用默认的写回(Write-back)缓存策略,则在 CPU 读取 DMA 写入的数据前,必须调用 `SCB_InvalidateDCache_by_Addr` 使缓存行失效,强制从物理内存重新加载。
- **手动清洁(Clean)**:若 CPU 先写数据再由 DMA 读取(如 DMA 发送),则需调用 `SCB_CleanDCache_by_Addr` 将缓存写回内存。
## 注意事项
- **缓存行对齐**:DMA 缓冲区建议按 32 字节对齐(Cortex-M7 缓存行大小),否则失效操作可能影响相邻数据。
- **使用链接脚本**:将 DMA 缓冲区放入独立段(如 `.dma_buffer`),便于 MPU 配置和内存管理。
- **中断安全**:在中断中调用 Cache 维护函数时,确保操作是原子的,或使用临界区保护。
- **性能权衡**:非缓存区域会降低 CPU 访问速度,因此仅对 DMA 共享缓冲区使用,其他数据保持缓存。
## 总结
Cortex-M7 的 D-Cache 是一把双刃剑,正确配置 MPU 和掌握 Cache 维护指令是嵌入式开发者的必备技能。通过 MPU 将 DMA 缓冲区设为非缓存,或在使用时手动 Clean/Invalidate,可以彻底解决一致性问题。双缓冲结合 Cache 维护,能实现高效的数据采集与处理,适用于音频、图像等高性能场景。