STM32H7 的 D-Cache 与 DMA 数据一致性:地址对齐、Cache Line 回写与失效的实战配置

1. 为什么 DMA 与 D-Cache 会“打架”?

STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,为了弥补 CPU 与存储器之间的速度差距,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会先将数据从 SRAM 加载到 Cache Line(通常为 32 字节),后续读写直接操作 Cache,从而大幅提升性能。

然而,DMA 控制器是独立于 CPU 的总线主设备,它直接访问 SRAM 或外设,不经过 D-Cache。这就导致了经典的一致性矛盾:

  • CPU 写,DMA 读:CPU 写入的数据可能还停留在 Cache 中(Write-Back 模式),尚未写回 SRAM,DMA 读到的就是旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 的 Cache 中仍缓存着旧数据,CPU 读到的还是旧值。

解决这一问题的核心手段就是Cache 回写(Clean)和失效(Invalidate),并辅以地址对齐和 MPU 配置。

2. Cache Line 与地址对齐:必须遵守的规则

Cortex-M7 的 D-Cache Line 大小为 32 字节。Cache 维护操作(Clean/Invalidate)的最小单位是一整行,而非单个字节。

  • 地址对齐要求:DMA 缓冲区地址必须按 32 字节对齐,且缓冲区大小最好是 32 字节的整数倍。否则,操作一行 Cache 时可能会误伤相邻数据,导致不可预知的错误。
  • 推荐做法:使用 __attribute__((aligned(32))) 定义 DMA 缓冲区,或使用 ALIGN_32BYTES 宏。
// 定义 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];

3. 核心操作:Clean 与 Invalidate

CMSIS 提供了标准函数用于 Cache 维护:

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中已修改的数据写回 SRAM。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 中的内容,强制下次读取时从 SRAM 重新加载。
  • SCB_CleanInvalidateDCache_by_Addr:先回写再失效,用于双向传输。

使用时机:

  • CPU 写 → DMA 读:在启动 DMA 传输之前,调用 SCB_CleanDCache_by_Addr。
  • DMA 写 → CPU 读:在 DMA 传输完成之后,调用 SCB_InvalidateDCache_by_Addr。
  • 双向传输:在 DMA 启动前 Clean,在 DMA 完成后 Invalidate。

4. 实战配置:MPU 与 DMA 缓冲区

更优雅的方案是利用 MPU(内存保护单元)将 DMA 缓冲区配置为 Write-Through, No Write-Allocate 或 Non-Cacheable 属性,从而避免手动维护 Cache。但手动维护方式更灵活,适合对性能有极致要求的场景。

以下是一个完整的 ADC + DMA 采集示例,展示如何正确维护 Cache:

#include "stm32h7xx.h"

#define ADC_DMA_BUFFER_SIZE  256

// 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) static uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];
volatile uint8_t dma_transfer_complete = 0;

// DMA 传输完成回调
void DMA1_Stream0_IRQHandler(void)
{
    if (DMA1->LISR & DMA_LISR_TCIF0) {
        DMA1->LIFCR = DMA_LIFCR_CTCIF0; // 清除标志
        
        // DMA 写入完成,使 Cache 失效,确保 CPU 读取到最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buffer, 
                                     sizeof(adc_dma_buffer));
        
        dma_transfer_complete = 1;
    }
}

void adc_dma_init(void)
{
    // 1. 使能 DMA1 时钟和 ADC 时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    
    // 2. 配置 DMA 流(以 DMA1_Stream0 为例)
    DMA1_Stream0->PAR  = (uint32_t)&ADC1->DR;          // 外设地址
    DMA1_Stream0->M0AR = (uint32_t)adc_dma_buffer;     // 存储器地址
    DMA1_Stream0->NDTR = ADC_DMA_BUFFER_SIZE;          // 传输数量
    DMA1_Stream0->CR   = DMA_SxCR_PL_1 |               // 高优先级
                         DMA_SxCR_MSIZE_0 |            // 16 位存储器
                         DMA_SxCR_PSIZE_0 |            // 16 位外设
                         DMA_SxCR_MINC |               // 存储器增量
                         DMA_SxCR_CIRC |               // 循环模式
                         DMA_SxCR_TCIE;                // 传输完成中断
    
    // 3. 使能 DMA 流
    DMA1_Stream0->CR |= DMA_SxCR_EN;
    
    // 4. 配置 ADC 为 DMA 模式并启动
    ADC1->CFGR |= ADC_CFGR_DMAEN | ADC_CFGR_DMACFG;
    ADC1->CR |= ADC_CR_ADEN;
    while (!(ADC1->ISR & ADC_ISR_ADRDY));
    ADC1->CR |= ADC_CR_ADSTART;
}

int main(void)
{
    // 使能 D-Cache(通常 SystemInit 中已使能)
    SCB_EnableDCache();
    
    adc_dma_init();
    
    while (1) {
        if (dma_transfer_complete) {
            dma_transfer_complete = 0;
            
            // 此时 adc_dma_buffer 中的数据已是最新,可安全处理
            uint32_t sum = 0;
            for (int i = 0; i < ADC_DMA_BUFFER_SIZE; i++) {
                sum += adc_dma_buffer[i];
            }
            // ... 进一步处理
        }
    }
}

5. 关键注意事项

  • 避免在 DMA 传输过程中操作 Cache:否则可能导致数据撕裂或总线错误。
  • Invalidate 前确保 DMA 已停止:如果 DMA 仍在写入,Invalidate 可能丢弃尚未写入的数据。
  • 多缓冲区与 Cache Line 重叠:若两个缓冲区共享同一 Cache Line,对一个缓冲区的 Clean/Invalidate 会影响另一个。务必确保每个 DMA 缓冲区独占整数个 Cache Line。
  • 使用 MPU 配置 Non-Cacheable 区域:对于频繁 DMA 的大数据块,可将其配置为 Non-Cacheable,一劳永逸,但会牺牲 CPU 访问性能。
  • 调试时关闭 Cache:初期调试可暂时关闭 D-Cache,排除一致性问题后再逐步开启优化。

6. 总结

STM32H7 的 D-Cache 与 DMA 是一对“性能与一致性”的博弈。掌握 32 字节对齐、Clean 与 Invalidate 的正确时机,并结合 MPU 灵活配置,才能写出既高效又稳定的嵌入式代码。记住:CPU 写后 Clean,DMA 写后 Invalidate,这是解决数据一致性的黄金法则。