找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

806

积分

0

好友

108

主题
发表于 5 小时前 | 查看: 0| 回复: 0

引言:一个让人抓狂的周五下午

那是一个普通的周五下午,项目即将进入量产阶段,测试组突然报告了一个诡异的 Bug:STM32H743 项目中,UART 通过 DMA 接收数据时,偶尔会读到全 0,或者读到上一次的旧数据。更令人困惑的是,这个问题在 Debug 模式下很少出现,但在 Release 优化后频繁复现。

MANAGED CACHE 与 INCONSISTENT CACHE 对比示意图

我们尝试了各种方法:检查 DMA 配置、验证硬件连接、更换测试板,问题依然存在。直到凌晨 2 点,当我在 ARM Cortex-M7 的技术参考手册中看到“Data Cache”这个章节时,才恍然大悟——这是一个典型的 Cache 一致性问题

这不是个例。自从 ARM Cortex-M7、STM32H7 系列、NXP i.MX RT 系列等带有 Cache 的高性能 MCU 开始普及,DMA 与 Cache 的一致性问题就成了嵌入式工程师的“必修课”。这个问题的隐蔽性在于:

  • 偶发性:不是每次都出错,让人误以为是硬件问题  
  • 优化相关:Debug 模式正常,Release 出错,容易归咎于编译器 Bug  
  • 平台迁移:从 STM32F4 升级到 H7 时,原本正常的代码突然出问题  

本文将深入剖析 DMA 与 Cache 一致性问题的本质,分享那些年我们踩过的坑,并提供一套工程化的解决方案。

DMA 工作原理回顾

DMA 是什么?

DMA(Direct Memory Access,直接内存访问)是一种允许外设绕过 CPU 直接访问系统内存的技术。其核心特点是:

传统方式:  外设 → CPU → 内存
DMA方式:   外设 ←→ DMA控制器 ←→ 内存 (CPU可以干别的事)

DMA 的典型应用场景

  1. 串口通信:UART/USART 通过 DMA 收发大量数据,CPU 无需逐字节搬运  
  2. SPI/I2C:高速 SPI Flash 读写、I2C 传感器批量采集  
  3. ADC 采集:连续转换模式下,DMA 自动搬运 ADC 结果到内存  
  4. SD 卡/eMMC:文件系统读写依赖 DMA 实现高吞吐  
  5. 以太网:网络数据包通过 DMA 描述符链表管理  

为什么 DMA 能提升性能?

假设 UART 以 115200 波特率接收 1KB 数据:

// 传统中断方式:每接收1字节触发1次中断,共1024次中断
void UART_IRQHandler(void) {
    buffer[index++] = UART->DR;  // CPU被中断1024次
}

// DMA方式:只在传输完成时触发1次中断
void DMA_IRQHandler(void) {
    // CPU被中断1次,期间可以处理其他任务
}

性能提升的关键:CPU 从“数据搬运工”中解放出来,可以并行处理业务逻辑、算法计算等高价值任务。

Cache 基础知识

为什么需要 Cache?

现代 MCU 的运行频率(如 400MHz)远高于外部存储器的访问速度(如 SDRAM 100MHz)。CPU 每次访问内存都要等待,会严重拖慢性能。Cache 就是在 CPU 和内存之间插入的高速缓存,典型访问速度对比:

存储类型 访问延迟 相对速度
CPU 寄存器 1 cycle 基准
L1 Cache 3-4 cycles 3-4x
L2 Cache 10-20 cycles 10-20x
SRAM 30-50 cycles 30-50x
SDRAM 100+ cycles 100x+

Cache 的层级结构

CPU Core
    ↓
L1 I-Cache (指令Cache) | L1 D-Cache (数据Cache)
    ↓                         ↓
        L2 Unified Cache (某些芯片有)
            ↓
        System Bus
            ↓
        Physical Memory
  • I-Cache:缓存指令代码,加速程序执行  
  • D-Cache:缓存数据读写,加速变量访问  
  • L1:容量小(通常 4-64KB),速度快,集成在 CPU 核内  
  • L2:容量大(通常 128KB-1MB),速度稍慢,可能在核外  

Cache 的关键概念

1. Cache Line(缓存行)

Cache 并非按字节管理,而是按固定大小的“行”(Line)管理,ARM Cortex-M7 的 Cache Line 通常是 32 字节。这意味着:

uint8_t buffer[100];  // 这个数组会占用 ceil(100/32) = 4个Cache Line

2. Cache 映射策略

  • 直接映射:内存地址只能映射到固定的 Cache Line  
  • 组相联:内存地址可以映射到一组 Cache Line 中的任意一个(Cortex-M7 多采用此方式)  
  • 全相联:内存地址可以映射到任意 Cache Line(成本高)  

3. 写策略

  • 写回(Write-Back):CPU 写数据时只更新 Cache,稍后才写回内存(性能高,但有一致性风险)  
  • 写穿(Write-Through):CPU 写数据时同时更新 Cache 和内存(性能低,一致性好)  

ARM Cortex-M7 的 D-Cache 默认使用 Write-Back 策略,这正是 DMA 问题的根源之一!

ARM Cortex-M7/A 系列的 Cache 特性

特性 Cortex-M7 Cortex-A7 Cortex-A53
I-Cache 可选,最大 64KB 8-64KB 8-64KB
D-Cache 可选,最大 64KB 8-64KB 8-256KB
Cache Line 32 字节 32/64 字节 64 字节
写策略 Write-Back Write-Back Write-Back
L2 Cache 可选 可选

STM32H7 系列(基于 Cortex-M7)通常配置:

  • I-Cache: 16KB  
  • D-Cache: 16KB  
  • Cache Line: 32 字节  

Cache 一致性问题的本质

问题场景 1: DMA 写入,CPU 读取(最常见)

这是最典型的场景:外设通过 DMA 将数据写入内存,但 CPU 读到的却是旧数据。

故障重现代码

uint8_t rx_buffer[256] __attribute__((aligned(32)));  // DMA接收缓冲区

void UART_DMA_Receive_Test(void) {
    // 步骤1: 初始化缓冲区
    memset(rx_buffer, 0xAA, sizeof(rx_buffer));  // CPU写入0xAA

    // 步骤2: 启动DMA接收
    HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);

    // 步骤3: 等待DMA完成
    while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_RX);

    // 步骤4: 读取数据
    printf("First byte: 0x%02X\n", rx_buffer[0]);  // 期望读到UART数据
    // 实际可能读到0xAA!
}

内存状态分析

时间轴:
T0: CPU执行 memset,写入0xAA
    Memory:   [0xAA, 0xAA, 0xAA, ...]
    D-Cache:  [0xAA, 0xAA, 0xAA, ...]  (Write-Back策略,只更新Cache)

T1: DMA开始接收,外设通过DMA写入新数据0x55
    Memory:   [0x55, 0x55, 0x55, ...]  (DMA直接写物理内存)
    D-Cache:  [0xAA, 0xAA, 0xAA, ...]  (Cache未更新!)

T2: CPU读取 rx_buffer[0]
    D-Cache命中! 返回0xAA  (读到旧数据)

问题根源:DMA 绕过了 CPU 的 Cache,直接修改物理内存,但 CPU 的 Cache 中还保留着旧数据,导致 Cache 与内存不一致。

问题场景 2: CPU 写入,DMA 读取

这是另一个常见场景:CPU 准备发送数据,但 DMA 读取到的却是未更新的旧数据。

故障重现代码

uint8_t tx_buffer[256] __attribute__((aligned(32)));

void UART_DMA_Transmit_Test(void) {
    // 步骤1: CPU填充数据
    for(int i = 0; i < 256; i++) {
        tx_buffer[i] = i;  // CPU写入0x00, 0x01, 0x02, ...
    }

    // 步骤2: 立即启动DMA发送
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, 256);

    // 问题: DMA可能发送的是内存中的旧数据,而非CPU刚写入的新数据!
}

内存状态分析

时间轴:
T0: CPU执行 tx_buffer[i] = i
    Memory:   [0x00, 0x00, 0x00, ...]  (旧数据,尚未写回)
    D-Cache:  [0x00, 0x01, 0x02, ...]  (新数据在Cache中)

T1: DMA开始发送,从物理内存读取数据
    DMA读取:  [0x00, 0x00, 0x00, ...]  (读到内存中的旧数据!)

T2: (稍后)Cache写回策略触发,新数据才写入内存
    Memory:   [0x00, 0x01, 0x02, ...]  (为时已晚,DMA已经读完)

问题根源:由于 Write-Back 策略,CPU 写入的数据只更新了 Cache,尚未写回物理内存,DMA 却直接从内存读取,导致读到旧数据。

问题场景 3: 指令 Cache 不一致

虽然不常见,但在某些特殊场景下也会遇到 I-Cache 问题:

典型场景

  1. 动态加载代码:从 Flash/SD 卡加载程序到 RAM 执行  
  2. Bootloader 更新:Bootloader 将新固件写入 Flash  
  3. 自修改代码:程序运行时修改自己的指令(罕见)  

故障示例

// 从SD卡加载代码到RAM
void Load_And_Execute_Code(void) {
    uint8_t *code_ram = (uint8_t *)0x20000000;

    // 1. 通过DMA从SD卡读取代码到RAM
    Read_From_SD_Card_DMA(code_ram, 4096);

    // 2. 跳转执行 (可能执行到旧指令!)
    void (*func)(void) = (void (*)(void))code_ram;
    func();  // I-Cache中可能还是旧代码,导致执行错误
}

解决方法:在执行前使指令 Cache 失效:

// 使I-Cache失效
SCB_InvalidateICache();

数据流示意图

场景1: DMA → Memory, CPU ← Cache (不一致)
┌─────────┐         ┌──────────┐         ┌─────────┐
│  外设   │ ──DMA→ │  Memory  │         │   CPU   │
│ (UART)  │         │ 0x55     │         │         │
└─────────┘         └──────────┘         └─────────┘
                          ↑                ↑
                          │                │
                          │          ┌──────────┐
                          └──────×───│ D-Cache  │
                           (未同步)    │ 0xAA     │
                                     └──────────┘

场景2: CPU → Cache, DMA ← Memory (不一致)
┌─────────┐         ┌──────────┐         ┌─────────┐
│  外设   │ ←─DMA── │  Memory  │         │   CPU   │
│ (UART)  │         │ 0x00(旧) │         │         │
└─────────┘         └──────────┘         └─────────┘
   ↑                   ↑                │
   │                   │                ↓
   └───────────────────┘          ┌──────────┐
        (读到旧数据)              │ D-Cache  │
                                  │ 0x01(新) │
                                  └──────────┘

那些年踩过的坑

坑 1: UART DMA 接收数据,偶现读到全 0 或旧数据

故障现象

uint8_t uart_rx_buf[1024];

// DMA接收完成中断
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 有时能读到正确数据,有时全是0x00或0xFF
    if(uart_rx_buf[0] == 0xAA && uart_rx_buf[1] == 0x55) {
        // 数据头校验通过
        Process_Data(uart_rx_buf);
    } else {
        error_count++;  // 错误计数持续增长
    }
}

测试发现:

  • Debug 模式(-O0)很少出错  
  • Release 模式(-O2)错误率约 30%  
  • 在 DMA 中断中直接打印数据,数据正确;但在任务中读取,数据错误  

原因分析

  1. Cache 未失效:DMA 将数据写入物理内存,但 D-Cache 中还保留着初始化时的旧数据  
  2. 编译器优化:Release 模式下,编译器可能将数组访问优化为寄存器操作,进一步加剧 Cache 命中  
  3. 中断与任务:中断中访问时 Cache 可能被其他操作刷新,任务中访问时命中旧 Cache  

解决方法

uint8_t uart_rx_buf[1024] __attribute__((aligned(32)));  // Cache Line对齐

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 方法1: DMA传输完成后,使Cache失效
    SCB_InvalidateDCache_by_Addr((uint32_t*)uart_rx_buf, sizeof(uart_rx_buf));

    // 现在可以安全读取
    if(uart_rx_buf[0] == 0xAA && uart_rx_buf[1] == 0x55) {
        Process_Data(uart_rx_buf);
    }
}

// 或者在启动DMA前就失效Cache
HAL_StatusTypeDef UART_Receive_DMA_Safe(UART_HandleTypeDef *huart, 
                                         uint8_t *pData, uint16_t Size) {
    // 传输前失效Cache
    SCB_InvalidateDCache_by_Addr((uint32_t*)pData, Size);
    return HAL_UART_Receive_DMA(huart, pData, Size);
}

坑 2: SD 卡通过 DMA 读取文件,FatFs 偶现数据损坏

故障现象

使用 STM32H7 + FatFs + SDMMC + DMA 读取文件时,偶尔出现:

  • 文件 CRC 校验失败  
  • 读取到的数据与实际文件内容不符  
  • 某些扇区的数据是全 0 或随机值  
FRESULT res;
FIL file;
uint8_t read_buf[4096];
UINT bytes_read;

res = f_open(&file, "test.bin", FA_READ);
res = f_read(&file, read_buf, 4096, &bytes_read);  // 读取成功

uint32_t crc = Calculate_CRC32(read_buf, bytes_read);
if(crc != expected_crc) {
    // CRC校验失败,数据损坏!
}

原因分析

  1. FatFs 的缓冲区:FatFs 内部有扇区缓冲区,SDMMC 通过 DMA 读取到这些缓冲区  
  2. 多次读取累积:第一次读取正常,第二次读取时 Cache 中还保留第一次的数据  
  3. 非对齐访问:FatFs 的缓冲区可能未按 32 字节对齐,导致 Cache 操作影响到相邻数据  

解决方法

方法 1: 修改 FatFs 配置,使用 Non-Cacheable 内存

// 在ffconf.h中定义缓冲区
#define FF_MAX_SS  512

// 在diskio.c中定义扇区缓冲区,放在Non-Cacheable区域
__attribute__((section(".dma_buffer"))) 
__attribute__((aligned(32)))
uint8_t SD_Buffer[FF_MAX_SS];

在链接脚本中定义 Non-Cacheable 区域:

/* STM32H743,SRAM1配置为Non-Cacheable */
.dma_buffer (NOLOAD) :
{
    . = ALIGN(32);
    *(.dma_buffer)
    . = ALIGN(32);
} > RAM_D2

方法 2: 在 BSP 驱动层添加 Cache 维护

// 修改bsp_driver_sd.c中的读取函数
uint8_t BSP_SD_ReadBlocks_DMA(uint32_t *pData, uint32_t ReadAddr, 
                              uint32_t NumOfBlocks) {
    uint32_t size = NumOfBlocks * 512;

    // 1. 传输前使Cache失效
    SCB_InvalidateDCache_by_Addr(pData, size);

    // 2. 启动DMA传输
    if(HAL_SD_ReadBlocks_DMA(&hsd1, (uint8_t*)pData, ReadAddr, NumOfBlocks) 
       != HAL_OK) {
        return MSD_ERROR;
    }

    // 3. 等待传输完成
    while(HAL_SD_GetCardState(&hsd1) != HAL_SD_CARD_TRANSFER);

    // 4. 传输后再次使Cache失效(确保读到最新数据)
    SCB_InvalidateDCache_by_Addr(pData, size);

    return MSD_OK;
}

坑 3: 以太网 DMA 描述符与数据缓冲区一致性问题

故障现象

STM32H7 的以太网 MAC 使用 DMA 描述符链表管理收发包,症状:

  • 偶尔丢包  
  • 接收到的数据包内容错误  
  • 发送的数据包在抓包工具中看到内容不对  
// 以太网DMA描述符
typedef struct {
    __IO uint32_t Status;
    uint32_t ControlBufferSize;
    uint32_t Buffer1Addr;
    uint32_t Buffer2NextDescAddr;
} ETH_DMADescTypeDef;

ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT];  // 接收描述符
ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT];  // 发送描述符
uint8_t Rx_Buff[ETH_RX_DESC_CNT][ETH_RX_BUF_SIZE];  // 接收缓冲区

原因分析

  1. 双重 Cache 问题
    • DMA 描述符本身需要与内存一致(DMA 读取描述符)  
    • 数据缓冲区也需要与内存一致(DMA 读写数据)  
  2. 频繁更新:以太网收发频繁,描述符状态不断变化,Cache 更新不及时  
  3. Scatter-Gather:以太网使用链表结构,跨越多个 Cache Line  

解决方法

STM32 HAL 库的官方方案

// 1. 将描述符和缓冲区放在SRAM1/SRAM2(D2域,Non-Cacheable)
#if defined ( __ICCARM__ )  // IAR
#pragma location = 0x30000000
#elif defined ( __CC_ARM )  // MDK-ARM
__attribute__((at(0x30000000)))
#elif defined ( __GNUC__ )  // GCC
__attribute__((section(".RxDecripSection")))
#endif
ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT];

// 2. 使用MPU配置SRAM为Non-Cacheable
void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    // 配置ETH DMA区域为Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = 0x30000000;  // SRAM1起始地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_256KB;
    MPU_InitStruct.SubRegionDisable = 0x0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // 禁用Cache
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

// 3. 在发送前清理Cache(如果缓冲区在Cacheable区域)
HAL_StatusTypeDef ETH_Transmit_Safe(uint8_t *buf, uint16_t len) {
    // 清理发送缓冲区的Cache,确保DMA读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)buf, len);

    // 启动发送
    return HAL_ETH_Transmit(&heth, buf, len);
}

坑 4: ADC 连续采集模式,DMA 数据不更新或部分更新

故障现象

使用 ADC 多通道连续采集,DMA 传输到数组,但读取时发现:

  • 某些数组元素始终是初始值  
  • 数据更新延迟,总是慢几拍  
  • 不同通道之间的数据出现错位  
uint16_t adc_values[8];  // 8通道ADC采集结果

void ADC_Init_DMA(void) {
    // 配置8通道连续采集
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_values, 8);
}

void Read_ADC_Values(void) {
    // 读取时发现数据异常
    float voltage_ch0 = adc_values[0] * 3.3f / 4096;  // 可能读到旧值
}

原因分析

  1. 小数组问题:8 个 uint16_t 只占 16 字节,不足 1 个 Cache Line(32 字节),容易 Cache 命中  
  2. 连续传输:ADC DMA 是循环模式,不断更新数组,Cache 刷新不及时  
  3. 对齐问题:数组未对齐到 Cache Line 边界,可能与其他变量共享 Cache Line  

解决方法

方法 1: 对齐并使用 Cache 维护

// 1. 确保数组对齐到Cache Line(32字节)
__attribute__((aligned(32))) uint16_t adc_values[16];  // 扩展到32字节

void Read_ADC_Values(void) {
    // 2. 读取前使Cache失效
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_values, sizeof(adc_values));

    // 3. 现在可以安全读取
    float voltage_ch0 = adc_values[0] * 3.3f / 4096;
}

方法 2: 使用 DTCM(Data Tightly Coupled Memory)

STM32H7 有 64KB DTCM,不经过 Cache,访问速度快:

// 将DMA缓冲区放在DTCM
__attribute__((section(".dtcm_data"))) uint16_t adc_values[8];

// 链接脚本配置
.dtcm_data :
{
    . = ALIGN(4);
    *(.dtcm_data)
    . = ALIGN(4);
} > DTCMRAM

方法 3: 周期性全局刷新 Cache(不推荐,影响性能)

void TIM_Callback(void) {
    // 定时器中断中全局刷新D-Cache
    SCB_InvalidateDCache();  // 简单粗暴,但影响性能
}

坑 5: RTOS 环境下,任务切换导致的 Cache 污染

故障现象

使用 FreeRTOS,两个任务共享 DMA 缓冲区:

uint8_t shared_buffer[512] __attribute__((aligned(32)));

// 任务1: 发送数据
void Task_Transmit(void *pvParameters) {
    while(1) {
        // 填充数据
        for(int i = 0; i < 512; i++) {
            shared_buffer[i] = i;
        }

        // DMA发送
        HAL_UART_Transmit_DMA(&huart1, shared_buffer, 512);

        vTaskDelay(100);
    }
}

// 任务2: 读取状态
void Task_Monitor(void *pvParameters) {
    while(1) {
        // 检查缓冲区首字节
        if(shared_buffer[0] != 0x00) {
            // 偶尔会读到错误值!
        }

        vTaskDelay(10);
    }
}

原因分析

  1. 任务切换:FreeRTOS 任务切换时,不会自动处理 Cache  
  2. 共享资源:多个任务访问同一缓冲区,Cache 状态复杂  
  3. 时序竞争:任务 1 写 Cache,DMA 读内存,任务 2 读 Cache,三方数据不一致  

解决方法

方法 1: 使用互斥锁+Cache 维护

SemaphoreHandle_t buffer_mutex;

void Task_Transmit(void *pvParameters) {
    while(1) {
        xSemaphoreTake(buffer_mutex, portMAX_DELAY);

        // 填充数据
        for(int i = 0; i < 512; i++) {
            shared_buffer[i] = i;
        }

        // 清理Cache,确保DMA读到最新数据
        SCB_CleanDCache_by_Addr((uint32_t*)shared_buffer, 512);
        __DSB();  // 数据同步屏障

        // DMA发送
        HAL_UART_Transmit_DMA(&huart1, shared_buffer, 512);

        xSemaphoreGive(buffer_mutex);
        vTaskDelay(100);
    }
}

void Task_Monitor(void *pvParameters) {
    while(1) {
        xSemaphoreTake(buffer_mutex, portMAX_DELAY);

        // 使Cache失效,读取最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)shared_buffer, 512);

        if(shared_buffer[0] != 0x00) {
            // 现在读取是正确的
        }

        xSemaphoreGive(buffer_mutex);
        vTaskDelay(10);
    }
}

方法 2: 使用消息队列传递数据拷贝

// 避免共享缓冲区,使用队列传递数据副本
QueueHandle_t data_queue;

void Task_Transmit(void *pvParameters) {
    uint8_t local_buffer[512];

    while(1) {
        for(int i = 0; i < 512; i++) {
            local_buffer[i] = i;
        }

        // 发送副本到队列
        xQueueSend(data_queue, local_buffer, portMAX_DELAY);
        vTaskDelay(100);
    }
}

void Task_Monitor(void *pvParameters) {
    uint8_t received_buffer[512];

    while(1) {
        if(xQueueReceive(data_queue, received_buffer, 10) == pdTRUE) {
            // 读取队列中的副本,无Cache问题
            Process(received_buffer);
        }
    }
}

解决方案与最佳实践

方案 1: Cache 维护操作

ARM 提供了一组 Cache 维护函数,定义在 core_cm7.h 中:

核心 API

// 1. 清理D-Cache(将Cache中的脏数据写回内存)
void SCB_CleanDCache(void);  // 清理全部D-Cache
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);  // 清理指定地址

// 2. 使D-Cache失效(丢弃Cache中的数据,下次访问从内存读取)
void SCB_InvalidateDCache(void);  // 使全部D-Cache失效
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);  // 使指定地址失效

// 3. 清理并使D-Cache失效(组合操作)
void SCB_CleanInvalidateDCache(void);  // 全局操作
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);  // 指定地址

// 4. I-Cache操作
void SCB_InvalidateICache(void);  // 使全部I-Cache失效

// 5. 内存屏障指令
__DSB();  // Data Synchronization Barrier,数据同步屏障
__DMB();  // Data Memory Barrier,数据内存屏障
__ISB();  // Instruction Synchronization Barrier,指令同步屏障

使用场景总结

场景 操作 时机 API
DMA → 内存,CPU 读取 Invalidate DMA 传输前和传输后 SCB_InvalidateDCache_by_Addr()
CPU 写入,DMA → 外设 Clean DMA 传输前 SCB_CleanDCache_by_Addr()
DMA ↔ 内存,CPU 读写 Clean + Invalidate 传输前后 SCB_CleanInvalidateDCache_by_Addr()
动态加载代码 Invalidate I-Cache 跳转执行前 SCB_InvalidateICache()

DMA 接收数据的标准流程(完整版)

/**
 * @brief DMA接收数据的安全封装(适用于UART/SPI/I2C等外设)
 * @param buffer 接收缓冲区地址(必须32字节对齐)
 * @param size   接收数据大小(字节)
 * @return HAL状态
 */
HAL_StatusTypeDef DMA_Receive_Safe(uint8_t *buffer, uint32_t size) {
    // 步骤1: 传输前使Cache失效
    // 目的: 防止后续读取时命中Cache中的旧数据
    // 注意: 即使buffer是新分配的,也可能在Cache中有旧数据(之前使用过相同地址)
    SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);

    // 步骤2: 插入数据同步屏障,确保Cache操作完成
    __DSB();

    // 步骤3: 启动DMA传输
    HAL_StatusTypeDef status = HAL_UART_Receive_DMA(&huart1, buffer, size);
    if(status != HAL_OK) {
        return status;
    }

    // 步骤4: 等待DMA传输完成
    // 方法A: 轮询方式(简单但阻塞)
    while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_RX) {
        // 可以添加超时检测
    }

    // 方法B: 中断方式(推荐,在中断回调中执行步骤5)

    // 步骤5: 传输完成后,再次使Cache失效
    // 目的: DMA传输期间,CPU可能预取了数据到Cache,需要丢弃
    SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);

    // 步骤6: 数据同步屏障
    __DSB();

    // 现在可以安全读取buffer中的数据
    return HAL_OK;
}

// 如果使用中断方式,在回调函数中处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if(huart->Instance == USART1) {
        // DMA传输完成,使Cache失效
        SCB_InvalidateDCache_by_Addr((uint32_t*)uart_rx_buffer, 
                                      sizeof(uart_rx_buffer));
        __DSB();

        // 设置标志位,通知其他任务数据已就绪
        rx_complete_flag = 1;
    }
}

DMA 发送数据的标准流程(完整版)

/**
 * @brief DMA发送数据的安全封装
 * @param buffer 发送缓冲区地址(必须32字节对齐)
 * @param size   发送数据大小(字节)
 * @return HAL状态
 */
HAL_StatusTypeDef DMA_Transmit_Safe(uint8_t *buffer, uint32_t size) {
    // 步骤1: 清理Cache,将CPU写入的数据从Cache写回内存
    // 目的: 确保DMA能从内存中读取到CPU刚写入的最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);

    // 步骤2: 数据同步屏障,确保Cache清理完成且内存写入完成
    __DSB();

    // 步骤3: 启动DMA传输
    HAL_StatusTypeDef status = HAL_UART_Transmit_DMA(&huart1, buffer, size);
    if(status != HAL_OK) {
        return status;
    }

    // 步骤4: 等待DMA传输完成(可选)
    // 注意: 传输期间不要修改buffer内容!

    return HAL_OK;
}

// 使用示例
void Send_Data_Example(void) {
    uint8_t tx_buffer[256] __attribute__((aligned(32)));

    // 填充数据
    for(int i = 0; i < 256; i++) {
        tx_buffer[i] = i;  // CPU写入数据到Cache
    }

    // 安全发送
    DMA_Transmit_Safe(tx_buffer, 256);  // 内部会清理Cache
}

Cache 操作的注意事项

  1. 地址对齐SCB_xxx_by_Addr() 函数要求地址按 32 字节对齐,否则可能影响性能或导致错误
// 错误示例
uint8_t buffer[100];  // 未对齐
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, 100);  // 可能失败

// 正确示例
uint8_t buffer[128] __attribute__((aligned(32)));  // 32字节对齐
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, 128);
  1. 大小计算:传递的 size 应该向上取整到 Cache Line 大小(32 字节)
// 推荐的封装
#define CACHE_LINE_SIZE  32
#define ALIGN_UP(size)  (((size) + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1))

void Invalidate_Cache_Safe(void *addr, uint32_t size) {
    uint32_t aligned_size = ALIGN_UP(size);
    SCB_InvalidateDCache_by_Addr((uint32_t*)addr, aligned_size);
}
  1. 避免过度使用全局操作SCB_CleanDCache() 会清理整个 16KB D-Cache,开销很大(可能数百个时钟周期),应优先使用按地址操作  

  2. 内存屏障的必要性:Cache 操作后必须插入 __DSB(),确保操作完成

SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);
__DSB();  // 必须!否则后续的DMA启动可能先于Cache清理执行

方案 2: MPU 配置 Non-Cacheable 区域

MPU(Memory Protection Unit)可以将特定内存区域配置为 Non-Cacheable,CPU 访问该区域时绕过 Cache,直接访问物理内存。

MPU 配置步骤

步骤 1: 启用 MPU

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 1. 禁用MPU(配置前必须禁用)
    HAL_MPU_Disable();

    // 2. 配置MPU区域
    // 以下代码将SRAM1(0x30000000, 128KB)配置为Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;  // 使用MPU区域0
    MPU_InitStruct.BaseAddress = 0x30000000;     // STM32H7的SRAM1起始地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_128KB; // 区域大小
    MPU_InitStruct.SubRegionDisable = 0x0;       // 启用所有子区域
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; // TEX=0
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; // 读写权限
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; // 禁止执行
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; // 非共享
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 不可Cache(关键!)
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // 不可缓冲

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 3. 启用MPU
    // MPU_PRIVILEGED_DEFAULT: 特权模式下使用默认内存映射,MPU只对用户模式生效
    // 对于裸机或RTOS,应使用 MPU_HFNMI_PRIVDEF_NONE
    HAL_MPU_Enable(MPU_HFNMI_PRIVDEF_NONE);
}

步骤 2: 将 DMA 缓冲区分配到 Non-Cacheable 区域

// 方法A: 使用section属性
__attribute__((section(".dma_buffer"))) 
__attribute__((aligned(32)))
uint8_t uart_rx_buffer[1024];

__attribute__((section(".dma_buffer")))
ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT];

// 方法B: 在代码中动态分配(需要链接脚本支持)
uint8_t *dma_buffer = (uint8_t *)0x30000000;  // 直接使用Non-Cacheable区域

步骤 3: 修改链接脚本(GCC 为例)

/* STM32H743的内存布局 */
MEMORY
{
  DTCMRAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 128K  /* DTCM,不经过Cache */
  RAM_D1 (xrw)   : ORIGIN = 0x24000000, LENGTH = 512K  /* AXI SRAM,Cacheable */
  RAM_D2 (xrw)   : ORIGIN = 0x30000000, LENGTH = 288K  /* SRAM1+SRAM2,配置为Non-Cacheable */
  RAM_D3 (xrw)   : ORIGIN = 0x38000000, LENGTH = 64K   /* SRAM4 */
  FLASH (rx)     : ORIGIN = 0x08000000, LENGTH = 2048K
}

SECTIONS
{
  /* 标准段省略... */

  /* DMA缓冲区段,放在SRAM1(RAM_D2) */
  .dma_buffer (NOLOAD) :
  {
    . = ALIGN(32);
    *(.dma_buffer)
    . = ALIGN(32);
  } > RAM_D2
}

MPU 配置实战案例:以太网缓冲区

// eth.h
#define ETH_RX_DESC_CNT  4
#define ETH_TX_DESC_CNT  4
#define ETH_RX_BUF_SIZE  1536

// 将以太网DMA相关数据结构放在Non-Cacheable区域
#if defined ( __ICCARM__ )  // IAR
#pragma location = 0x30000000
#elif defined ( __GNUC__ )  // GCC
__attribute__((section(".eth_buffers")))
#endif
ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT];

#if defined ( __ICCARM__ )
#pragma location = 0x30000200
#elif defined ( __GNUC__ )
__attribute__((section(".eth_buffers")))
#endif
ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT];

#if defined ( __ICCARM__ )
#pragma location = 0x30000400
#elif defined ( __GNUC__ )
__attribute__((section(".eth_buffers")))
#endif
uint8_t Rx_Buff[ETH_RX_DESC_CNT][ETH_RX_BUF_SIZE];

// main.c
int main(void) {
    // 1. 系统初始化
    HAL_Init();
    SystemClock_Config();

    // 2. 配置MPU(在其他初始化之前)
    MPU_Config_Ethernet();

    // 3. 启用I-Cache和D-Cache
    SCB_EnableICache();
    SCB_EnableDCache();

    // 4. 初始化以太网
    MX_ETH_Init();

    while(1) {
        // 使用以太网时,无需手动维护Cache!
    }
}

void MPU_Config_Ethernet(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    // 配置0x30000000-0x30010000(64KB)为Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.SubRegionDisable = 0x0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;  // 以太网需要共享
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;  // 允许缓冲

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_HFNMI_PRIVDEF_NONE);
}

MPU 配置的优缺点

优点

  • ✅ 配置后无需手动维护 Cache,代码简洁  
  • ✅ 硬件保证一致性,不会因为忘记 Cache 操作而出错  
  • ✅ 适合频繁 DMA 操作的场景(如以太网)  

缺点

  • ❌ 访问 Non-Cacheable 区域速度慢(相当于直接访问内存)  
  • ❌ CPU 读写 DMA 缓冲区的性能下降  
  • ❌ MPU 区域数量有限(通常 8-16 个),不能滥用  
  • ❌ 区域大小和地址有对齐要求(必须是 2 的幂次)  

性能对比

访问Cacheable SRAM:     ~1-3 cycles (命中)
访问Non-Cacheable SRAM: ~30-50 cycles
访问SDRAM:              ~100+ cycles

推荐使用场景

  • 以太网 DMA 描述符和缓冲区  
  • SD 卡 FatFs 缓冲区  
  • USB 传输缓冲区  
  • 大块 DMA 传输(> 4KB)  

不推荐使用场景

  • 小数据量的 DMA(如单次 ADC 采集)  
  • 需要 CPU 频繁访问的缓冲区  
  • 实时性要求高的数据处理  

方案 3: 内存屏障指令

ARM 提供了三种内存屏障指令,用于控制指令执行顺序和内存访问顺序:

内存屏障类型

// 1. DMB (Data Memory Barrier) - 数据内存屏障
__DMB();
// 作用: 确保DMB之前的所有内存访问完成后,才执行DMB之后的内存访问
// 场景: 多核系统、共享内存、DMA操作

// 2. DSB (Data Synchronization Barrier) - 数据同步屏障
__DSB();
// 作用: 确保DSB之前的所有内存访问和Cache操作完成后,才执行DSB之后的指令
// 场景: Cache维护操作后、DMA启动前、外设寄存器配置

// 3. ISB (Instruction Synchronization Barrier) - 指令同步屏障
__ISB();
// 作用: 刷新流水线,确保ISB之前的指令修改(如MPU配置)生效
// 场景: 修改系统配置寄存器后、启用/禁用Cache后

内存屏障的使用场景

场景 1: Cache 维护后插入 DSB

// 错误示例:没有内存屏障
SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);
HAL_UART_Transmit_DMA(&huart1, buffer, size);  // 可能在Cache清理完成前就启动DMA!

// 正确示例:插入DSB
SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);
__DSB();  // 等待Cache清理完成
HAL_UART_Transmit_DMA(&huart1, buffer, size);  // 现在安全了

场景 2: 配置外设寄存器后插入 DSB

// DMA配置
DMA1_Stream0->CR = DMA_CONF;  // 配置DMA控制寄存器
DMA1_Stream0->NDTR = size;    // 配置传输数量
__DSB();  // 确保寄存器写入完成
DMA1_Stream0->CR |= DMA_SxCR_EN;  // 启动DMA

场景 3: 修改 MPU 配置后插入 ISB

HAL_MPU_Disable();
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_HFNMI_PRIVDEF_NONE);
__DSB();  // 等待MPU配置写入完成
__ISB();  // 刷新流水线,使MPU配置生效

// 现在可以访问MPU保护的区域

场景 4: 启用/禁用 Cache 后插入 ISB

SCB_EnableDCache();
__DSB();
__ISB();  // 确保Cache启用生效

// 开始使用Cacheable内存

内存屏障的完整示例

/**
 * @brief DMA传输的完整流程,包含所有必要的内存屏障
 */
void DMA_Transfer_Complete_Example(void) {
    uint8_t tx_buffer[512] __attribute__((aligned(32)));
    uint8_t rx_buffer[512] __attribute__((aligned(32)));

    // === 发送流程 ===

    // 1. CPU填充数据
    memset(tx_buffer, 0x55, sizeof(tx_buffer));

    // 2. 插入编译器屏障,防止编译器重排
    __DMB();

    // 3. 清理Cache,将数据写回内存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));

    // 4. 数据同步屏障,确保Cache清理和内存写入完成
    __DSB();

    // 5. 配置并启动DMA发送
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));

    // === 接收流程 ===

    // 1. 使Cache失效,防止读到旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));

    // 2. 数据同步屏障
    __DSB();

    // 3. 启动DMA接收
    HAL_UART_Receive_DMA(&huart2, rx_buffer, sizeof(rx_buffer));

    // 4. 等待接收完成(假设使用中断)
    // ...

    // 5. 在中断回调中:
    // void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    //     SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
    //     __DSB();
    //     // 现在可以安全读取rx_buffer
    // }
}

内存屏障的性能开销

指令 开销(时钟周期) 说明
__DMB() 1-4 等待内存访问完成
__DSB() 4-10 等待内存访问和 Cache 操作完成
__ISB() 10-20 刷新流水线,开销最大

优化建议

  • 不要过度使用内存屏障(特别是 ISB)  
  • 只在必要的地方插入(Cache 操作后、外设配置后)  
  • 合并多个操作,减少屏障次数  

方案 4: 使用特定内存区域

STM32H7 系列具有复杂的内存架构,不同内存区域有不同的特性:

STM32H7 内存布局

┌──────────────────────────────────────────┐
│ 0x2000_0000 - 0x2001_FFFF (128KB)       │  DTCM RAM
│ - 不经过Cache,访问速度快               │  (推荐用于DMA缓冲区)
│ - CPU独占,DMA不能直接访问              │
├──────────────────────────────────────────┤
│ 0x2400_0000 - 0x2407_FFFF (512KB)       │  AXI SRAM (D1域)
│ - 经过D-Cache,高速访问                 │  (推荐用于程序数据)
│ - 可被DMA访问                          │
├──────────────────────────────────────────┤
│ 0x3000_0000 - 0x3004_7FFF (288KB)       │  SRAM1+SRAM2+SRAM3 (D2域)
│ - 经过D-Cache                          │  (推荐配置为Non-Cacheable)
│ - 可被DMA访问                          │  (用于以太网/SD卡缓冲区)
├──────────────────────────────────────────┤
│ 0x3800_0000 - 0x3800_FFFF (64KB)        │  SRAM4 (D3域)
│ - 经过D-Cache                          │  (低功耗域,适合待机数据)
│ - 可被DMA访问                          │
└──────────────────────────────────────────┘

方案 4A: 使用 DTCM 存放 DMA 缓冲区(推荐小数据量)

DTCM(Data Tightly Coupled Memory)是 CPU 核内的紧耦合内存,特点:

  • ✅ 不经过 Cache,无一致性问题  
  • ✅ 访问速度极快(0 等待)  
  • ✅ 无需 Cache 维护操作  
  • ❌ 容量小(128KB)  
  • DMA 无法直接访问(这是关键限制!)  

注意:STM32H7 的 DTCM 不能被 DMA 直接访问,因此不能用于 DMA 缓冲区!

正确的用法是:

// ❌ 错误:DTCM无法被DMA访问
__attribute__((section(".dtcm_data"))) 
uint8_t dma_buffer[1024];  // DMA传输会失败!

// ✅ 正确:DTCM用于CPU频繁访问的变量
__attribute__((section(".dtcm_data"))) 
uint32_t counter;  // 计数器

__attribute__((section(".dtcm_data"))) 
float fir_coefficients[128];  // FIR滤波器系数

方案 4B: 使用 AXI SRAM(D1 域) + Cache 维护

适合大部分场景:

// 默认情况下,全局变量就在AXI SRAM
uint8_t uart_buffer[1024] __attribute__((aligned(32)));

void UART_DMA_Transfer(void) {
    // 需要手动维护Cache
    SCB_CleanDCache_by_Addr((uint32_t*)uart_buffer, sizeof(uart_buffer));
    __DSB();
    HAL_UART_Transmit_DMA(&huart1, uart_buffer, sizeof(uart_buffer));
}

方案 4C: 使用 SRAM1/2/3(D2 域),配置为 Non-Cacheable

适合以太网、SD 卡等高吞吐场景:

步骤 1: 链接脚本配置

MEMORY
{
  RAM_D1 (xrw)  : ORIGIN = 0x24000000, LENGTH = 512K  /* AXI SRAM */
  RAM_D2 (xrw)  : ORIGIN = 0x30000000, LENGTH = 288K  /* SRAM1+2+3 */
}

SECTIONS
{
  /* 普通数据段,放在AXI SRAM */
  .data :
  {
    *(.data)
  } > RAM_D1

  /* DMA缓冲区段,放在SRAM1 */
  .dma_buffers (NOLOAD) :
  {
    . = ALIGN(32);
    *(.dma_buffers)
    . = ALIGN(32);
  } > RAM_D2
}

步骤 2: 声明 DMA 缓冲区

// eth.c
__attribute__((section(".dma_buffers"))) 
__attribute__((aligned(32)))
uint8_t eth_rx_buffer[ETH_RX_BUF_CNT][ETH_RX_BUF_SIZE];

__attribute__((section(".dma_buffers"))) 
__attribute__((aligned(32)))
uint8_t eth_tx_buffer[ETH_TX_BUF_CNT][ETH_TX_BUF_SIZE];

// sdcard.c
__attribute__((section(".dma_buffers"))) 
__attribute__((aligned(32)))
uint8_t sd_buffer[512];

步骤 3: MPU 配置 SRAM1 为 Non-Cacheable

void MPU_Config_DMA_Region(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    // 配置SRAM1(0x30000000, 256KB)为Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_256KB;
    MPU_InitStruct.SubRegionDisable = 0x0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // 关键!
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_HFNMI_PRIVDEF_NONE);
}

// main.c
int main(void) {
    HAL_Init();
    SystemClock_Config();

    // 先配置MPU,再启用Cache
    MPU_Config_DMA_Region();

    SCB_EnableICache();
    SCB_EnableDCache();

    // 后续DMA操作无需Cache维护
    while(1) {
        // ...
    }
}

内存区域选择决策树

是否需要DMA传输?
├─ 否 → 使用AXI SRAM(默认)
└─ 是 → 数据量大小?
    ├─ < 4KB → 使用AXI SRAM + Cache维护
    └─ >= 4KB → 是否频繁DMA?
        ├─ 否(偶尔传输) → AXI SRAM + Cache维护
        └─ 是(持续传输,如以太网) → SRAM1(D2域) + MPU配置Non-Cacheable

方案 5: 硬件 Cache 一致性(高级)

某些高端 SoC(如 Cortex-A 系列)提供硬件 Cache 一致性支持,但 STM32H7 不支持此特性,这里仅作了解。

ACP (Accelerator Coherency Port)

在 Cortex-A 系列(如 A53/A57)中,可以配置 DMA 通过 ACP 端口访问内存:

传统DMA路径:  DMA → 内存 (绕过Cache)
ACP路径:      DMA → ACP → SCU → Cache → 内存

优势

  • ✅ 硬件自动维护一致性  
  • ✅ 无需软件干预  

劣势

  • ❌ 增加延迟  
  • ❌ 仅高端 SoC 支持  
  • ❌ STM32 系列不支持  

CCI (Cache Coherent Interconnect)

用于多核系统的 Cache 一致性:

Core0 Cache ←→ CCI ←→ Core1 Cache
                  ↓
                Memory

STM32MP1 系列(双核 Cortex-A7)支持 CCI,但配置复杂,超出本文范围。

工程化方案模板

为了减少重复劳动和避免遗漏,这里提供一套可复用的代码框架:

模板 1: DMA 操作封装

/**
 * @file dma_cache_safe.h
 * @brief DMA操作的Cache安全封装
 */

#ifndef __DMA_CACHE_SAFE_H
#define __DMA_CACHE_SAFE_H

#include "stm32h7xx_hal.h"
#include <stdint.h>

/* Cache Line大小(ARM Cortex-M7) */
#define CACHE_LINE_SIZE  32

/* 向上对齐到Cache Line */
#define CACHE_ALIGN_UP(size)  (((size) + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1))

/* 检查地址是否对齐 */
#define IS_CACHE_ALIGNED(addr)  (((uint32_t)(addr) & (CACHE_LINE_SIZE - 1)) == 0)

/**
 * @brief DMA接收数据(Cache安全)
 * @param huart  UART句柄
 * @param pData  接收缓冲区(必须32字节对齐)
 * @param Size   接收大小
 * @retval HAL状态
 */
static inline HAL_StatusTypeDef UART_Receive_DMA_CacheSafe(
    UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
    /* 断言:地址必须对齐 */
    if(!IS_CACHE_ALIGNED(pData)) {
        return HAL_ERROR;  // 或者触发断言
    }

    /* 传输前使Cache失效 */
    SCB_InvalidateDCache_by_Addr((uint32_t*)pData, CACHE_ALIGN_UP(Size));
    __DSB();

    /* 启动DMA传输 */
    return HAL_UART_Receive_DMA(huart, pData, Size);
}

/**
 * @brief DMA发送数据(Cache安全)
 * @param huart  UART句柄
 * @param pData  发送缓冲区(必须32字节对齐)
 * @param Size   发送大小
 * @retval HAL状态
 */
static inline HAL_StatusTypeDef UART_Transmit_DMA_CacheSafe(
    UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
    /* 断言:地址必须对齐 */
    if(!IS_CACHE_ALIGNED(pData)) {
        return HAL_ERROR;
    }

    /* 传输前清理Cache */
    SCB_CleanDCache_by_Addr((uint32_t*)pData, CACHE_ALIGN_UP(Size));
    __DSB();

    /* 启动DMA传输 */
    return HAL_UART_Transmit_DMA(huart, pData, Size);
}

/**
 * @brief DMA接收完成回调(在HAL_UART_RxCpltCallback中调用)
 * @param pData  接收缓冲区
 * @param Size   接收大小
 */
static inline void DMA_RxCplt_CacheInvalidate(uint8_t *pData, uint16_t Size)
{
    /* 传输完成后再次使Cache失效 */
    SCB_InvalidateDCache_by_Addr((uint32_t*)pData, CACHE_ALIGN_UP(Size));
    __DSB();
}

/* SPI DMA操作 */
static inline HAL_StatusTypeDef SPI_Receive_DMA_CacheSafe(
    SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)pData, CACHE_ALIGN_UP(Size));
    __DSB();
    return HAL_SPI_Receive_DMA(hspi, pData, Size);
}

static inline HAL_StatusTypeDef SPI_Transmit_DMA_CacheSafe(
    SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size)
{
    SCB_CleanDCache_by_Addr((uint32_t*)pData, CACHE_ALIGN_UP(Size));
    __DSB();
    return HAL_SPI_Transmit_DMA(hspi, pData, Size);
}

/* I2C DMA操作 */
static inline HAL_StatusTypeDef I2C_Mem_Read_DMA_CacheSafe(
    I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,
    uint16_t MemAddSize, uint8_t *pData, uint16_t Size)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)pData, CACHE_ALIGN_UP(Size));
    __DSB();
    return HAL_I2C_Mem_Read_DMA(hi2c, DevAddress, MemAddress, 
                                MemAddSize, pData, Size);
}

static inline HAL_StatusTypeDef I2C_Mem_Write_DMA_CacheSafe(
    I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,
    uint16_t MemAddSize, uint8_t *pData, uint16_t Size)
{
    SCB_CleanDCache_by_Addr((uint32_t*)pData, CACHE_ALIGN_UP(Size));
    __DSB();
    return HAL_I2C_Mem_Write_DMA(hi2c, DevAddress, MemAddress, 
                                 MemAddSize, pData, Size);
}

#endif /* __DMA_CACHE_SAFE_H */

模板 2: DMA 缓冲区宏定义

/**
 * @file dma_buffer_def.h
 * @brief DMA缓冲区定义宏
 */

#ifndef __DMA_BUFFER_DEF_H
#define __DMA_BUFFER_DEF_H

/* 方法1: 定义在AXI SRAM(默认,需要Cache维护) */
#define DMA_BUFFER_CACHEABLE(type, name, size) \
    type name[size] __attribute__((aligned(32)))

/* 方法2: 定义在Non-Cacheable区域(需要MPU配置和链接脚本支持) */
#define DMA_BUFFER_NONCACHEABLE(type, name, size) \
    __attribute__((section(".dma_buffers"))) \
    __attribute__((aligned(32))) \
    type name[size]

/* 方法3: 定义在DTCM(不能用于DMA!) */
#define FAST_BUFFER(type, name, size) \
    __attribute__((section(".dtcm_data"))) \
    __attribute__((aligned(4))) \
    type name[size]

/* 使用示例 */
#if 0
// 场景1: 小数据量,偶尔传输,使用方法1
DMA_BUFFER_CACHEABLE(uint8_t, uart_rx_buf, 256);

// 场景2: 大数据量,频繁传输,使用方法2
DMA_BUFFER_NONCACHEABLE(uint8_t, eth_rx_buf, 1536);

// 场景3: CPU频繁访问的变量,不用于DMA
FAST_BUFFER(uint32_t, fir_state, 128);
#endif

#endif /* __DMA_BUFFER_DEF_H */

模板 3: 完整的 UART DMA 驱动

/**
 * @file uart_dma_driver.c
 * @brief UART DMA驱动(Cache安全版本)
 */

#include "uart_dma_driver.h"
#include "dma_cache_safe.h"
#include <string.h>

/* UART DMA缓冲区 */
DMA_BUFFER_CACHEABLE(uint8_t, uart1_rx_buffer, 1024);
DMA_BUFFER_CACHEABLE(uint8_t, uart1_tx_buffer, 1024);

/* 接收状态 */
static volatile uint8_t rx_complete = 0;

/**
 * @brief 初始化UART DMA
 */
void UART_DMA_Init(void) {
    /* HAL库初始化(CubeMX生成) */
    MX_USART1_UART_Init();
    MX_DMA_Init();

    /* 启动DMA接收 */
    UART_Receive_DMA_CacheSafe(&huart1, uart1_rx_buffer, 
                               sizeof(uart1_rx_buffer));
}

/**
 * @brief DMA接收完成回调
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if(huart->Instance == USART1) {
        /* 使Cache失效,确保读取到最新数据 */
        DMA_RxCplt_CacheInvalidate(uart1_rx_buffer, sizeof(uart1_rx_buffer));

        /* 设置标志位 */
        rx_complete = 1;

        /* 重新启动接收 */
        UART_Receive_DMA_CacheSafe(&huart1, uart1_rx_buffer, 
                                   sizeof(uart1_rx_buffer));
    }
}

/**
 * @brief 发送数据
 * @param data  要发送的数据
 * @param len   数据长度
 * @return 0成功, -1失败
 */
int UART_DMA_Transmit(const uint8_t *data, uint16_t len) {
    /* 检查长度 */
    if(len > sizeof(uart1_tx_buffer)) {
        return -1;
    }

    /* 拷贝到DMA缓冲区 */
    memcpy(uart1_tx_buffer, data, len);

    /* DMA发送(自动清理Cache) */
    if(UART_Transmit_DMA_CacheSafe(&huart1, uart1_tx_buffer, len) != HAL_OK) {
        return -1;
    }

    return 0;
}

/**
 * @brief 接收数据
 * @param data  接收缓冲区
 * @param len   缓冲区长度
 * @param timeout  超时时间(ms)
 * @return 接收到的字节数, -1超时
 */
int UART_DMA_Receive(uint8_t *data, uint16_t len, uint32_t timeout) {
    uint32_t start_tick = HAL_GetTick();

    /* 等待接收完成 */
    while(!rx_complete) {
        if((HAL_GetTick() - start_tick) > timeout) {
            return -1;  // 超时
        }
    }

    /* 拷贝数据 */
    uint16_t copy_len = (len < sizeof(uart1_rx_buffer)) ? 
                        len : sizeof(uart1_rx_buffer);
    memcpy(data, uart1_rx_buffer, copy_len);

    /* 清除标志 */
    rx_complete = 0;

    return copy_len;
}

模板 4: MPU 配置模板

/**
 * @file mpu_config.c
 * @brief MPU配置模板
 */

#include "mpu_config.h"

/**
 * @brief 配置MPU(在main函数最开始调用)
 */
void MPU_Config_Init(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    /* 禁用MPU */
    HAL_MPU_Disable();

    /* ===== 区域0: SRAM1(0x30000000, 256KB) - Non-Cacheable ===== */
    /* 用于以太网/SD卡DMA缓冲区 */
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_256KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /* ===== 区域1: QSPI Flash(0x90000000, 64MB) - Cacheable ===== */
    /* 用于XIP(Execute In Place)执行代码 */
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER1;
    MPU_InitStruct.BaseAddress = 0x90000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_64MB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
    MPU_InitStruct.AccessPermission = MPU_REGION_PRIV_RO;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /* ===== 区域2: FMC SDRAM(0xD0000000, 8MB) - Write-Through ===== */
    /* 用于外部SDRAM,降低写回Cache的风险 */
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER2;
    MPU_InitStruct.BaseAddress = 0xD0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;  // Cacheable
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;  // Write-Through
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /* 启用MPU */
    HAL_MPU_Enable(MPU_HFNMI_PRIVDEF_NONE);

    /* 内存屏障,确保MPU配置生效 */
    __DSB();
    __ISB();
}

模板 5: 链接脚本模板(GCC)

/* STM32H743XIHx_FLASH.ld */

/* Entry Point */
ENTRY(Reset_Handler)

/* Memory Layout */
MEMORY
{
  FLASH (rx)     : ORIGIN = 0x08000000, LENGTH = 2048K
  DTCMRAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 128K
  RAM_D1 (xrw)   : ORIGIN = 0x24000000, LENGTH = 512K  /* AXI SRAM */
  RAM_D2 (xrw)   : ORIGIN = 0x30000000, LENGTH = 256K  /* SRAM1+SRAM2 */
  RAM_D3 (xrw)   : ORIGIN = 0x38000000, LENGTH = 64K   /* SRAM4 */
}

SECTIONS
{
  /* 代码段 */
  .text :
  {
    . = ALIGN(4);
    *(.text)
    *(.text*)
    *(.rodata)
    *(.rodata*)
    . = ALIGN(4);
  } > FLASH

  /* 已初始化数据段,存放在AXI SRAM */
  .data :
  {
    . = ALIGN(4);
    _sdata = .;
    *(.data)
    *(.data*)
    . = ALIGN(4);
    _edata = .;
  } > RAM_D1 AT> FLASH

  /* 未初始化数据段(BSS),存放在AXI SRAM */
  .bss :
  {
    . = ALIGN(4);
    _sbss = .;
    *(.bss)
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM_D1

  /* DTCM数据段(快速访问,不能用于DMA) */
  .dtcm_data (NOLOAD) :
  {
    . = ALIGN(4);
    *(.dtcm_data)
    . = ALIGN(4);
  } > DTCMRAM

  /* DMA缓冲区段(Non-Cacheable,需要MPU配置) */
  .dma_buffers (NOLOAD) :
  {
    . = ALIGN(32);
    _sdma_buffers = .;
    *(.dma_buffers)
    *(.eth_buffers)
    . = ALIGN(32);
    _edma_buffers = .;
  } > RAM_D2

  /* 低功耗数据段,存放在SRAM4(D3域) */
  .d3_data (NOLOAD) :
  {
    . = ALIGN(4);
    *(.d3_data)
    . = ALIGN(4);
  } > RAM_D3

  /* 堆栈段 */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } > RAM_D1
}

性能优化与权衡

不同方案的性能对比

假设场景:传输 1KB 数据,CPU 主频 400MHz

方案 平均耗时 CPU 占用率 优点 缺点
无 Cache(关闭 D-Cache) ~100µs 无一致性问题 整体性能下降 50%
Cache + 全局操作 ~150µs 实现简单 影响其他代码性能
Cache + 按地址操作 ~50µs 开销小,精确控制 需要对齐,代码复杂
MPU Non-Cacheable ~120µs 无需维护 Cache 访问缓冲区慢
DTCM(仅 CPU) ~20µs 最快 不能用于 DMA

Cache 操作的开销分析

/* 测试代码:比较不同Cache操作的开销 */
void Cache_Performance_Test(void) {
    uint8_t buffer[1024] __attribute__((aligned(32)));
    uint32_t start, end;

    /* 测试1: 全局Invalidate */
    start = DWT->CYCCNT;
    SCB_InvalidateDCache();
    end = DWT->CYCCNT;
    printf("Global Invalidate: %lu cycles\n", end - start);
    // 结果: ~2000 cycles (约5µs @ 400MHz)

    /* 测试2: 按地址Invalidate (1KB) */
    start = DWT->CYCCNT;
    SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, 1024);
    end = DWT->CYCCNT;
    printf("By Addr Invalidate 1KB: %lu cycles\n", end - start);
    // 结果: ~200 cycles (约0.5µs @ 400MHz)

    /* 测试3: Clean + Invalidate */
    start = DWT->CYCCNT;
    SCB_CleanInvalidateDCache_by_Addr((uint32_t*)buffer, 1024);
    end = DWT->CYCCNT;
    printf("Clean + Invalidate 1KB: %lu cycles\n", end - start);
    // 结果: ~400 cycles (约1µs @ 400MHz)
}

何时应该禁用 Cache vs 何时使用 Cache 维护

场景 1: 小数据量(< 1KB),低频率(< 10 次/秒) → Cache 维护

/* ADC单次采集,每100ms采集一次 */
uint16_t adc_result[8] __attribute__((aligned(32)));

void ADC_Periodic_Sampling(void) {
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_result, 8);

    // 等待完成
    while(HAL_ADC_GetState(&hadc1) == HAL_ADC_STATE_BUSY);

    // Invalidate Cache
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_result, sizeof(adc_result));

    // 处理数据
    float voltage = adc_result[0] * 3.3f / 4096;
}

理由:开销小(<1µs),代码简单,不影响整体性能。

场景 2: 大数据量(> 4KB),高频率(持续传输) → MPU Non-Cacheable

/* 以太网接收,持续高吞吐 */
__attribute__((section(".dma_buffers"))) 
uint8_t eth_rx_buffer[ETH_RX_BUF_CNT][1536];

void Ethernet_Init(void) {
    // 配置MPU,SRAM1为Non-Cacheable
    MPU_Config_Ethernet();

    // 初始化以太网(无需Cache维护)
    HAL_ETH_Init(&heth);
}

理由:避免频繁 Cache 操作,降低 CPU 占用率,提升吞吐量。

场景 3: 中等数据量(1-4KB),中等频率 → 混合方案

/* UART接收,每秒10-100次 */
uint8_t uart_buffer[2048] __attribute__((aligned(32)));

void UART_Receive_Handler(void) {
    // 小数据(<512B): 使用Cache维护
    if(size <= 512) {
        SCB_InvalidateDCache_by_Addr((uint32_t*)uart_buffer, size);
    }
    // 大数据(>512B): 考虑迁移到Non-Cacheable区域
    else {
        // 或者使用全局Invalidate
        SCB_InvalidateDCache();
    }
}

决策流程图

         [DMA数据大小]
               |
       +-------+-------+
       |               |
     < 1KB           > 1KB
       |               |
   [传输频率]      [传输频率]
       |               |
   +---+---+       +---+---+
   |       |       |       |
 < 10Hz  >10Hz   <100Hz  >100Hz
   |       |       |       |
   |       |       |       |
Cache维护  Cache维护  混合   Non-Cacheable
(按地址)  (全局)    方案    (MPU)

Benchmark 数据参考

以下是实际测试数据(STM32H743 @ 400MHz):

测试 1: UART DMA 接收 1KB 数据

方案 总耗时 Cache 操作耗时 传输耗时 CPU 占用
无 Cache 维护 50µs 0 50µs ❌ 数据错误
全局 Invalidate 75µs 25µs 50µs 50%
按地址 Invalidate 52µs 2µs 50µs 4%
Non-Cacheable 60µs 0 60µs 0%(但访问慢)

测试 2: 以太网连续接收(10Mbps)

方案 平均 CPU 占用 丢包率 吞吐量
Cache 维护(全局) 35% 5% 8Mbps
Cache 维护(按地址) 20% 2% 9.5Mbps
Non-Cacheable 10% 0% 10Mbps

结论:对于高吞吐场景,Non-Cacheable 方案性能最优。

测试 3: SD 卡文件读取(FatFs)

读取 10MB 文件:

方案 总耗时 速度 CPU 占用
Cache 维护 1.2s 8.3MB/s 60%
Non-Cacheable 1.5s 6.7MB/s 20%

结论:SD 卡场景下,Cache 维护速度更快,但 CPU 占用高;根据需求选择。

调试技巧

如何判断是否遇到了 Cache 一致性问题?

症状清单

高度怀疑 Cache 问题的典型症状:

  • DMA 传输成功,但 CPU 读到的数据是旧值或全 0  
  • 数据错误在 Debug 模式(-O0)很少出现,Release 模式(-O2)频繁出现  
  • 在 DMA 中断回调中读取数据正常,在任务中读取异常  
  • 代码在 STM32F4(无 Cache)正常,移植到 H7(有 Cache)出错  
  • 添加 volatile 关键字或 printf 调试后,问题“神奇”消失  
  • DMA 发送的数据与预期不符,但源数据确实是正确的  

快速验证方法

方法 1: 临时禁用 Cache

int main(void) {
    HAL_Init();
    SystemClock_Config();

    // 不启用Cache,测试是否正常
    // SCB_EnableICache();  // 注释掉
    // SCB_EnableDCache();  // 注释掉

    // 后续代码...
    while(1);
}

如果禁用 Cache 后问题消失,100%确认是 Cache 一致性问题。

方法 2: 使用全局 Cache 操作

// 在DMA操作前后添加全局Cache操作
void Test_DMA_Transfer(void) {
    // 发送前
    SCB_CleanDCache();
    __DSB();
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, size);

    // 接收后
    while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_RX);
    SCB_InvalidateDCache();
    __DSB();

    // 读取数据
    Process_Data(rx_buffer);
}

如果添加后问题解决,确认是 Cache 问题,后续优化为按地址操作。

方法 3: 添加魔数校验

// 在DMA缓冲区头尾添加魔数
#define MAGIC_HEADER  0xAA55AA55
#define MAGIC_FOOTER  0x55AA55AA

typedef struct {
    uint32_t header;  // 魔数
    uint8_t data[1024];
    uint32_t footer;  // 魔数
} DMA_Buffer_t;

DMA_Buffer_t dma_buf __attribute__((aligned(32)));

void Check_Buffer_Integrity(void) {
    // DMA传输前初始化魔数
    dma_buf.header = MAGIC_HEADER;
    dma_buf.footer = MAGIC_FOOTER;

    // DMA传输...

    // 传输后检查
    if(dma_buf.header != MAGIC_HEADER || dma_buf.footer != MAGIC_FOOTER) {
        printf("Cache coherency issue detected!\n");
        printf("Header: 0x%08X (expected 0x%08X)\n", dma_buf.header, MAGIC_HEADER);
        printf("Footer: 0x%08X (expected 0x%08X)\n", dma_buf.footer, MAGIC_FOOTER);
    }
}

使用调试器查看 Cache 状态

Keil MDK 调试器

  1. 打开“View” → “Analysis Windows” → “Cache”  
  2. 可以查看 Cache 命中率、未命中次数  
  3. 可以手动 Flush/Invalidate Cache  

STM32CubeIDE(GDB)

# 查看Cache启用状态
(gdb) info reg SCB_CCR
# 如果bit 16(DC)=1,表示D-Cache已启用
# 如果bit 17(IC)=1,表示I-Cache已启用

# 手动使Cache失效
(gdb) call SCB_InvalidateDCache()

# 查看内存内容(物理内存)
(gdb) x/16xw 0x24000000  # 查看AXI SRAM

SEGGER Ozone

  1. 打开“View” → “Data” → “Cache Statistics”  
  2. 可以实时监控 Cache 命中率  
  3. 支持 Cache Line 级别的可视化  

使用逻辑分析仪/示波器验证 DMA 时序

┌────────────────────────────────────────────┐
│  逻辑分析仪抓取信号:                        │
│                                            │
│  CH1: DMA_EN(DMA启用信号)                  │
│  CH2: GPIO_Toggle(代码插入的时间戳)         │
│  CH3: UART_TX(物理信号)                    │
└────────────────────────────────────────────┘

代码插桩:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);  // CH2拉高
    SCB_InvalidateDCache_by_Addr(...);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);  // CH2拉低
}

分析:
- 如果UART_TX信号在DMA_EN之前出现 → DMA配置问题
- 如果CH2脉冲宽度过长(>10µs) → Cache操作开销过大
- 如果UART_TX数据内容错误 → Cache一致性问题

添加校验和、魔数等调试手段

方法 1: CRC 校验

#include "stm32h7xx_hal_crc.h"

CRC_HandleTypeDef hcrc;

void DMA_Transfer_With_CRC(void) {
    uint8_t tx_buffer[512] __attribute__((aligned(32)));

    // 1. 填充数据
    for(int i = 0; i < 512; i++) {
        tx_buffer[i] = i & 0xFF;
    }

    // 2. 计算CRC(在Cache中计算)
    uint32_t crc_expected = HAL_CRC_Calculate(&hcrc, (uint32_t*)tx_buffer, 512/4);

    // 3. DMA发送
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, 512);
    __DSB();
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, 512);

    // 4. 接收端验证CRC
    // (如果CRC不匹配,说明DMA发送的数据与Cache中不一致)
}

方法 2: 数据镜像对比

// 在传输前备份数据到另一个缓冲区(不参与DMA)
uint8_t dma_buffer[1024] __attribute__((aligned(32)));
uint8_t backup_buffer[1024];  // 普通缓冲区

void DMA_Transmit_With_Backup(void) {
    // 1. 填充数据
    for(int i = 0; i < 1024; i++) {
        dma_buffer[i] = i & 0xFF;
    }

    // 2. 备份到普通缓冲区
    memcpy(backup_buffer, dma_buffer, 1024);

    // 3. DMA发送
    SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, 1024);
    __DSB();
    HAL_UART_Transmit_DMA(&huart1, dma_buffer, 1024);

    // 4. 传输后比较
    // (注意:此时不能读取dma_buffer,要读取物理内存)
    // 可以通过调试器或接收端验证
}

编译器优化对 Cache 问题的影响

不同优化级别的行为

uint8_t buffer[100];

void Test_Function(void) {
    // -O0 (无优化):
    // 每次访问buffer都会从内存读取,Cache命中率低
    for(int i = 0; i < 100; i++) {
        buffer[i] = i;  // 逐字节写入,可能触发Cache写回
    }

    // -O2 (优化):
    // 编译器可能:
    // 1. 将循环展开
    // 2. 使用寄存器缓存buffer地址
    // 3. 使用SIMD指令(如果支持)
    // 4. 重排指令顺序
    // 结果: Cache行为更复杂,一致性问题更隐蔽
}

调试建议

  1. 先用-O0 调试,排除基本逻辑错误  
  2. 用-O2 测试,暴露 Cache 问题  
  3. 关键函数禁用优化
__attribute__((optimize("O0")))
void DMA_Critical_Function(void) {
    // 该函数不优化,便于调试
}
  1. 使用 volatile 防止优化
volatile uint8_t *dma_ptr = dma_buffer;
for(int i = 0; i < 100; i++) {
    dma_ptr[i] = i;  // 强制每次访问内存
}

编译器优化与 Cache 的交互

// 示例:编译器可能生成的汇编代码

// -O0: 
// LDR R0, =buffer
// MOV R1, #0
// loop:
//   STRB R1, [R0], #1  // 逐字节写入,每次都可能写入Cache
//   ADD R1, R1, #1
//   CMP R1, #100
//   BNE loop

// -O2:
// LDR R0, =buffer
// MOV R1, #0
// MOV R2, #0x03020100  // 常量
// loop:
//   STR R2, [R0], #4  // 按字(4字节)写入,减少Cache操作
//   ADD R1, R1, #4
//   CMP R1, #100
//   BNE loop

关键点:优化后的代码可能按 Cache Line 大小批量写入,增加了 Cache 一致性风险。

不同平台的差异

STM32F7 vs STM32H7 vs STM32MP1

特性 STM32F7 STM32H7 STM32MP1
内核 Cortex-M7 Cortex-M7 Cortex-A7(双核)
主频 216MHz 400-480MHz 650MHz-800MHz
I-Cache 4-16KB 16KB 32KB(每核)
D-Cache 4-16KB 16KB 32KB(每核)
L2 Cache 256KB(共享)
Cache Line 32 字节 32 字节 64 字节
内存区域 DTCM+SRAM DTCM+AXI+SRAM1/2/3/4 DDR3/4
MPU 区域 8 个 16 个 MMU(页表)
硬件一致性 CCI-400(支持)

STM32F7 特点

  • Cache 较小(4-16KB),一致性问题相对简单  
  • 内存区域单一,主要使用 SRAM  
  • DMA 缓冲区推荐放在 DTCM(虽然不能 DMA 直接访问,但可以作为中转)  
// STM32F746推荐做法
uint8_t dtcm_buffer[1024] __attribute__((section(".dtcm")));  // CPU快速访问
uint8_t dma_buffer[1024];  // SRAM,用于DMA

void F7_DMA_Safe_Transfer(void) {
    // CPU → DTCM (快速)
    memcpy(dtcm_buffer, data_source, 1024);

    // DTCM → SRAM (一次性拷贝)
    memcpy(dma_buffer, dtcm_buffer, 1024);

    // SRAM → DMA (需要Cache维护)
    SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, 1024);
    HAL_UART_Transmit_DMA(&huart1, dma_buffer, 1024);
}

STM32H7 特点

  • 多个内存区域(DTCM/AXI/SRAM1/2/3),灵活但复杂  
  • D1/D2/D3 域,需要理解总线架构  
  • MPU 区域多(16 个),可以精细配置  
// STM32H743推荐内存分配
/*
DTCM (0x20000000, 128KB):  CPU密集计算的变量,不用于DMA
AXI  (0x24000000, 512KB):  程序主数据区,偶尔DMA可用Cache维护
SRAM1(0x30000000, 128KB):  频繁DMA,配置为Non-Cacheable
SRAM2(0x30020000, 128KB):  频繁DMA,配置为Non-Cacheable
SRAM3(0x30040000, 32KB):   保留
SRAM4(0x38000000, 64KB):   低功耗数据,待机保持
*/

// 实际代码:
__attribute__((section(".dtcm_data"))) float fpu_data[1024];  // FPU计算
uint8_t general_buffer[4096];  // AXI SRAM,默认
__attribute__((section(".dma_buffers"))) uint8_t eth_buf[1536];  // SRAM1

STM32MP1 特点(Cortex-A7, Linux 系统)

  • 使用 MMU(Memory Management Unit),页表管理内存  
  • 支持 CCI-400 硬件 Cache 一致性  
  • DMA 通常由 Linux 内核驱动管理,应用层使用 DMA API  
// Linux用户空间DMA示例(需要自定义驱动)
#include <linux/dma-mapping.h>

void *virt_addr;
dma_addr_t phys_addr;

// 分配DMA一致性内存(自动处理Cache)
virt_addr = dma_alloc_coherent(dev, size, &phys_addr, GFP_KERNEL);

// 使用virt_addr访问数据,无需手动Cache维护

// 释放
dma_free_coherent(dev, size, virt_addr, phys_addr);

NXP i.MX RT 系列

芯片 内核 Cache 特点
i.MX RT1050 Cortex-M7 I:32KB, D:32KB FlexRAM 可配置
i.MX RT1060 Cortex-M7 I:32KB, D:32KB 增强型以太网
i.MX RT1170 Cortex-M7+M4 M7: I:32KB, D:32KB / M4:无 双核异构

FlexRAM 特性

i.MX RT 系列独特的 FlexRAM 可以灵活配置为 DTCM/ITCM/OCRAM:

// 示例:配置512KB FlexRAM
// - 128KB DTCM (0x20000000)
// - 128KB ITCM (0x00000000)
// - 256KB OCRAM (0x20200000)

// 在OCRAM中分配DMA缓冲区
__attribute__((section(".ocram_data"))) 
uint8_t dma_buffer[1024];

// DTCM用于CPU密集操作
__attribute__((section(".dtcm_data"))) 
float fast_array[256];

NXP 推荐的 DMA 实践

// NXP SDK提供的Cache维护宏
#include "fsl_cache.h"

void NXP_DMA_Receive(void) {
    // 接收前Invalidate
    DCACHE_InvalidateByRange((uint32_t)rx_buffer, size);

    // 启动DMA
    UART_TransferReceiveNonBlocking(UART1, &handle, &xfer);

    // 等待完成
    while(xfer.rxState != kUART_RxIdle);

    // 再次Invalidate
    DCACHE_InvalidateByRange((uint32_t)rx_buffer, size);
}

TI Sitara AM 系列

芯片 内核 Cache 特点
AM335x Cortex-A8 L1:32KB, L2:256KB 工业控制
AM437x Cortex-A9 L1:32KB, L2:512KB 更高性能
AM57xx Cortex-A15+M4 L1:32KB, L2:2MB 多核异构

TI 平台的 Cache 特点

  • 使用 MMU,页表管理内存属性  
  • 支持硬件 Cache 一致性(部分型号)  
  • 通常运行 Linux,使用内核 DMA API  
// TI Linux驱动中的DMA操作
#include <linux/dma-mapping.h>

// 方法1: 一致性内存(dma_alloc_coherent)
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// 该区域是Non-Cacheable的,无需手动维护

// 方法2: 流式DMA(dma_map_single)
dma_addr_t dma_addr = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);
// 内核会自动Clean Cache

// 传输完成后
dma_unmap_single(dev, dma_addr, size, DMA_TO_DEVICE);

各平台特殊注意事项

STM32H7 注意事项

  1. D1/D2/D3 域总线速度不同

    • D1(AXI): 200MHz,最快  
    • D2(AHB): 200MHz  
    • D3(AHB): 200MHz  
    • 建议高速 DMA 使用 D1 域  
  2. 以太网 MAC 只能访问 D2 域

// 以太网缓冲区必须在SRAM1/2(D2域)
__attribute__((section(".eth_buffers"))) 
uint8_t eth_buffer[1536];  // 0x30000000地址
  1. ADC3 只能访问 D3 域的 SRAM4
// ADC3的DMA缓冲区必须在SRAM4
__attribute__((section(".d3_data"))) 
uint16_t adc3_result[100];  // 0x38000000地址

i.MX RT 注意事项

  • FlexRAM 配置需要在 Fuse 中设置,启动后通过 IOMUXC_GPR 寄存器微调  
  • SDRAM(外部)访问慢,不推荐用于实时 DMA  
  • LCD DMA 使用专用 eLCDIF 控制器,自动处理 Cache 一致性  

TI Sitara 注意事项

  • PRU(Programmable Real-time Unit)子系统:
    • PRU 有独立内存,不经过主 CPU Cache  
    • 适合实时数据采集  
  • EDMA3 控制器复杂,推荐使用 TI 提供的驱动库  
  • Linux 环境下,用户空间避免直接操作 DMA,使用 UIO 或字符设备驱动  

总结与 Checklist

DMA 开发的 Cache 一致性检查清单

在开始 DMA 开发前,请逐项检查:

✅ 硬件与配置检查

  • 确认 MCU 是否有 Cache(Cortex-M7/A 系列)  
  • 确认 Cache 是否启用(SCB->CCR 寄存器 bit 16/17)  
  • 了解目标平台的内存布局(DTCM/SRAM/SDRAM 位置)  
  • 确认 MPU 是否配置(如果使用 Non-Cacheable 方案)  
  • 检查链接脚本,确认 DMA 缓冲区在正确的内存区域  

✅ DMA 缓冲区定义检查

  • DMA 缓冲区是否 32 字节对齐(__attribute__((aligned(32))))  
  • 缓冲区大小是否是 Cache Line 的整数倍(推荐)  
  • 是否使用了正确的内存区域(Cacheable vs Non-Cacheable)  
  • 是否避免了 DTCM(DTCM 不能被 DMA 访问)  
  • 是否避免了栈上分配(栈可能与其他变量共享 Cache Line)  

✅ DMA 发送(CPU → DMA)检查

  • 发送前是否调用 SCB_CleanDCache_by_Addr()  
  • 是否插入 __DSB() 内存屏障  
  • 是否等待 Cache 操作完成后才启动 DMA  
  • 是否避免在 DMA 传输期间修改缓冲区  

✅ DMA 接收(DMA → CPU)检查

  • 接收前是否调用 SCB_InvalidateDCache_by_Addr()  
  • 接收后是否再次调用 SCB_InvalidateDCache_by_Addr()  
  • 是否在 DMA 完成中断后才读取数据  
  • 是否避免在 DMA 传输期间读取缓冲区  

✅ 中断与回调检查

  • DMA 完成中断中是否执行了 Cache 维护  
  • 中断优先级是否合理(避免被其他中断打断)  
  • 是否避免在中断中执行耗时操作(如全局 Cache 清理)  

✅ RTOS 环境检查

  • 多任务访问 DMA 缓冲区是否加锁  
  • 任务切换是否考虑 Cache 污染  
  • 是否使用消息队列传递数据副本(避免共享)  

✅ 调试与测试检查

  • 是否测试了 Debug 和 Release 两种模式  
  • 是否进行了压力测试(连续传输)  
  • 是否测试了边界情况(缓冲区边界,跨 Cache Line)  
  • 是否添加了 CRC/魔数校验  
  • 是否使用逻辑分析仪验证了时序  

推荐的开发流程

1. 需求分析
   ├─ 确定DMA使用场景(UART/SPI/ADC/Ethernet?)
   ├─ 估算数据量和传输频率
   └─ 选择合适的方案(Cache维护 vs Non-Cacheable)

2. 内存规划
   ├─ 设计内存布局(哪些区域用于DMA)
   ├─ 编写链接脚本,划分内存区域
   └─ 配置MPU(如果使用Non-Cacheable)

3. 代码实现
   ├─ 使用提供的模板和封装函数
   ├─ 严格遵循Cache维护流程
   └─ 添加断言和错误检查

4. 单元测试
   ├─ 先在Debug模式(-O0)下测试基本功能
   ├─ 用小数据量验证流程正确性
   └─ 添加CRC校验,确保数据完整性

5. 集成测试
   ├─ 切换到Release模式(-O2)
   ├─ 进行压力测试(连续传输,大数据量)
   └─ 测试边界情况和异常场景

6. 性能优化
   ├─ 使用Benchmark测量性能
   ├─ 根据测试结果调整方案
   └─ 权衡性能与复杂度

7. 文档与维护
   ├─ 记录内存布局和配置
   ├─ 注释关键代码(为什么要这样做)
   └─ 留下调试信息(日志、断言)

经验法则(Rule of Thumb)

规则 1: 对齐是基础

// ❌ 错误
uint8_t buffer[100];

// ✅ 正确
uint8_t buffer[128] __attribute__((aligned(32)));

规则 2: 发送 Clean,接收 Invalidate

发送: CPU写数据 → Clean Cache → DMA读内存 → 外设
接收: 外设 → DMA写内存 → Invalidate Cache → CPU读数据

规则 3: 内存屏障不能省

// ❌ 错误
SCB_CleanDCache_by_Addr(...);
HAL_UART_Transmit_DMA(...);  // 可能乱序执行

// ✅ 正确
SCB_CleanDCache_by_Addr(...);
__DSB();  // 必须!
HAL_UART_Transmit_DMA(...);

规则 4: 传输前后都要维护 Cache(接收场景)

// ❌ 只在传输后Invalidate(可能命中传输前的Cache)
HAL_UART_Receive_DMA(...);
SCB_InvalidateDCache_by_Addr(...);  // 不够!

// ✅ 传输前后都Invalidate
SCB_InvalidateDCache_by_Addr(...);  // 传输前
HAL_UART_Receive_DMA(...);
// 等待完成...
SCB_InvalidateDCache_by_Addr(...);  // 传输后

规则 5: 小数据用 Cache 维护,大数据用 Non-Cacheable

< 1KB  → Cache维护(按地址)
1-4KB  → 根据频率决定
> 4KB且频繁传输 → Non-Cacheable(MPU配置)

规则 6: 宁可多一次 Cache 操作,也不要少

// 多一次Cache操作的开销(几微秒)远小于排查Bug的时间(几小时)
SCB_InvalidateDCache_by_Addr(...);  // 保险起见,再失效一次

规则 7: 避免栈上分配 DMA 缓冲区

// ❌ 错误:栈上分配
void Bad_Function(void) {
    uint8_t buffer[1024];  // 栈上,可能与其他变量共享Cache Line
    HAL_UART_Receive_DMA(&huart1, buffer, 1024);  // 危险!
}

// ✅ 正确:全局或静态
static uint8_t buffer[1024] __attribute__((aligned(32)));

规则 8: 调试时先禁用 Cache,确认是否是 Cache 问题

// 如果问题频繁出现,先试试禁用Cache
// SCB_EnableDCache();  // 注释掉
// 如果问题消失,100%是Cache一致性问题

规则 9: Release 模式下测试是必须的

Debug(-O0): Cache行为简单,问题少
Release(-O2): 编译器优化,Cache行为复杂,问题暴露

规则 10: 文档胜过记忆

/*
 * 注意: uart_tx_buffer用于DMA发送,必须在发送前Clean Cache
 * 原因: Write-Back策略可能导致Cache中的数据未写回内存
 * 日期: 2024-12-30
 * 作者: Ocean
 */
SCB_CleanDCache_by_Addr((uint32_t*)uart_tx_buffer, size);

写在最后

DMA 与 Cache 的一致性问题,是高性能嵌入式系统开发中的一道“必答题”。它不像段错误、栈溢出那样会立即崩溃,而是以一种隐蔽的方式悄悄破坏数据,让你在 Debug 和 Release 之间来回切换,在凌晨的办公室里怀疑人生。

但只要理解了其本质——DMA 绕过 Cache 直接访问物理内存,而 CPU 通过 Cache 访问数据——就能用正确的方法驯服这把“双刃剑”:

  • 小数据、低频率:按地址维护 Cache,开销小,代码清晰  
  • 大数据、高频率:MPU 配置 Non-Cacheable,省心省力  
  • 实时性要求高:使用 DTCM 或专用内存区域  
  • 跨平台项目:统一封装,隐藏平台差异  

记住那条黄金法则:发送前 Clean,接收后 Invalidate,内存屏障不能省

希望这篇文章能帮你少踩几个坑,少熬几个夜。如果你也有类似的踩坑经历,欢迎在评论区分享,我们一起成长。




上一篇:C语言基于FreeRTOS的嵌入式日志系统简易设计实现详解
下一篇:网络安全入门指南:从防火墙到SIEM,20种常用设备功能与部署详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-26 17:31 , Processed in 0.284900 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表