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

我们尝试了各种方法:检查 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 的典型应用场景
- 串口通信:UART/USART 通过 DMA 收发大量数据,CPU 无需逐字节搬运
- SPI/I2C:高速 SPI Flash 读写、I2C 传感器批量采集
- ADC 采集:连续转换模式下,DMA 自动搬运 ADC 结果到内存
- SD 卡/eMMC:文件系统读写依赖 DMA 实现高吞吐
- 以太网:网络数据包通过 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 问题:
典型场景
- 动态加载代码:从 Flash/SD 卡加载程序到 RAM 执行
- Bootloader 更新:Bootloader 将新固件写入 Flash
- 自修改代码:程序运行时修改自己的指令(罕见)
故障示例
// 从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 中断中直接打印数据,数据正确;但在任务中读取,数据错误
原因分析
- Cache 未失效:DMA 将数据写入物理内存,但 D-Cache 中还保留着初始化时的旧数据
- 编译器优化:Release 模式下,编译器可能将数组访问优化为寄存器操作,进一步加剧 Cache 命中
- 中断与任务:中断中访问时 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校验失败,数据损坏!
}
原因分析
- FatFs 的缓冲区:FatFs 内部有扇区缓冲区,SDMMC 通过 DMA 读取到这些缓冲区
- 多次读取累积:第一次读取正常,第二次读取时 Cache 中还保留第一次的数据
- 非对齐访问: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]; // 接收缓冲区
原因分析
- 双重 Cache 问题:
- DMA 描述符本身需要与内存一致(DMA 读取描述符)
- 数据缓冲区也需要与内存一致(DMA 读写数据)
- 频繁更新:以太网收发频繁,描述符状态不断变化,Cache 更新不及时
- 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; // 可能读到旧值
}
原因分析
- 小数组问题:8 个 uint16_t 只占 16 字节,不足 1 个 Cache Line(32 字节),容易 Cache 命中
- 连续传输:ADC DMA 是循环模式,不断更新数组,Cache 刷新不及时
- 对齐问题:数组未对齐到 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);
}
}
原因分析
- 任务切换:FreeRTOS 任务切换时,不会自动处理 Cache
- 共享资源:多个任务访问同一缓冲区,Cache 状态复杂
- 时序竞争:任务 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 操作的注意事项
- 地址对齐:
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);
- 大小计算:传递的 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);
}
-
避免过度使用全局操作:SCB_CleanDCache() 会清理整个 16KB D-Cache,开销很大(可能数百个时钟周期),应优先使用按地址操作
-
内存屏障的必要性: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 调试器
- 打开“View” → “Analysis Windows” → “Cache”
- 可以查看 Cache 命中率、未命中次数
- 可以手动 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
- 打开“View” → “Data” → “Cache Statistics”
- 可以实时监控 Cache 命中率
- 支持 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行为更复杂,一致性问题更隐蔽
}
调试建议
- 先用-O0 调试,排除基本逻辑错误
- 用-O2 测试,暴露 Cache 问题
- 关键函数禁用优化:
__attribute__((optimize("O0")))
void DMA_Critical_Function(void) {
// 该函数不优化,便于调试
}
- 使用 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 注意事项
-
D1/D2/D3 域总线速度不同:
- D1(AXI): 200MHz,最快
- D2(AHB): 200MHz
- D3(AHB): 200MHz
- 建议高速 DMA 使用 D1 域
-
以太网 MAC 只能访问 D2 域:
// 以太网缓冲区必须在SRAM1/2(D2域)
__attribute__((section(".eth_buffers")))
uint8_t eth_buffer[1536]; // 0x30000000地址
- 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,内存屏障不能省。
希望这篇文章能帮你少踩几个坑,少熬几个夜。如果你也有类似的踩坑经历,欢迎在评论区分享,我们一起成长。