STM32H7 零拷贝日志存储:MDMA 与双 Bank Flash 并行搬运实战
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,日志记录往往受限于 Flash 擦写速度和 CPU 占用。STM32H7 系列凭借其强大的 MDMA 控制器和双 Bank Flash 架构,可以实现日志数据的零拷贝并行搬运,大幅提升存储效率。本文将深入讲解 MDMA 的工作原理、双 Bank 交错写入策略,并给出完整的配置步骤和代码示例,帮助开发者构建高性能、低干扰的日志系统。
# STM32H7 零拷贝日志存储:MDMA 与双 Bank Flash 并行搬运实战
## 1. 为什么需要零拷贝日志存储?
在嵌入式开发中,日志系统常面临两大痛点:
- **CPU 占用高**:传统方式需 CPU 将日志数据从缓冲区拷贝到 Flash 写入缓冲区,再进行擦写操作,耗时且阻塞主流程。
- **Flash 擦写慢**:STM32H7 的 Flash 擦除时间可达数百毫秒,若在日志写入时打断实时任务,将导致系统响应延迟。
STM32H7 提供了 **MDMA(Master Direct Memory Access)** 和 **双 Bank Flash** 架构,通过硬件级并行处理,实现日志数据的零拷贝搬运,即数据从内存直接传输到 Flash,无需 CPU 介入,且两个 Bank 可交替写入,隐藏擦除延迟。
## 2. 核心原理
### 2.1 MDMA 与普通 DMA 的区别
MDMA 是 STM32H7 中的高级 DMA 控制器,支持:
- **内存到内存传输**:无需外设触发,可自主搬运大块数据。
- **链表模式**:支持多个传输块(Block)串联,自动切换源/目的地址。
- **双缓冲**:在传输过程中可动态更新缓冲区,实现流水线操作。
相比普通 DMA,MDMA 更适合 Flash 编程场景,因为它能处理非对齐数据、支持 32 位带宽,且不占用 CPU 周期。
### 2.2 双 Bank Flash 交错写入
STM32H7 的 Flash 分为两个 Bank(如 Bank1 和 Bank2),每个 Bank 独立擦除和编程。当向 Bank1 写入日志时,可同时擦除 Bank2 的旧数据,反之亦然。通过 MDMA 交替向两个 Bank 搬运数据,可隐藏擦除时间,实现连续写入。
### 2.3 零拷贝实现
传统流程:`日志缓冲区 -> CPU 拷贝 -> Flash 编程缓冲区 -> 等待擦写`。
零拷贝流程:`日志缓冲区(内存) -> MDMA 直接搬运 -> Flash 编程寄存器`,CPU 仅需配置 MDMA 和 Flash 控制器,然后继续执行主任务。
## 3. 硬件与软件准备
- **硬件**:STM32H743 开发板(或其他 H7 系列),外部晶振 25MHz。
- **软件**:STM32CubeIDE 1.15+,HAL 库(含 MDMA 和 Flash 驱动)。
- **关键外设**:MDMA、FLASH、RCC(时钟)。
## 4. 配置步骤
### 4.1 时钟配置
确保 MDMA 和 Flash 时钟已使能,且 Flash 等待周期正确(H7 在 480MHz 下需 7 个等待周期)。
```c
RCC->AHB3ENR |= RCC_AHB3ENR_MDMAEN; // 使能 MDMA 时钟
FLASH->ACR |= FLASH_ACR_LATENCY_7WS; // 设置等待周期
```
### 4.2 MDMA 配置
使用 HAL 库初始化 MDMA,配置为内存到内存模式,传输方向为内存到 Flash 编程地址。
```c
MDMA_HandleTypeDef hmdma;
void MDMA_Init(void) {
hmdma.Instance = MDMA_Channel0;
hmdma.Init.Request = MDMA_REQUEST_MEM2MEM; // 内存到内存
hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER; // 块传输
hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma.Init.Endianness = MDMA_LITTLE_ENDIAN;
hmdma.Init.SourceInc = MDMA_SRC_INC_WORD; // 源地址递增
hmdma.Init.DestinationInc = MDMA_DEST_INC_WORD; // 目的地址递增(Flash 编程地址)
hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
hmdma.Init.BufferTransferLength = 1024; // 每次传输 1024 字(4KB)
HAL_MDMA_Init(&hmdma);
}
```
### 4.3 Flash 编程配置
使用 HAL 的 Flash 编程接口,但需注意:MDMA 写入 Flash 时,Flash 必须处于编程模式,且地址对齐。
```c
void Flash_Init(void) {
HAL_FLASH_Unlock();
// 设置编程宽度为 32 位(双字)
FLASH->CR1 |= FLASH_CR1_PSIZE_1; // 32-bit programming
}
```
### 4.4 双 Bank 切换逻辑
定义两个 Bank 的基地址和大小,通过变量切换当前写入 Bank。
```c
#define BANK1_ADDR 0x08000000
#define BANK2_ADDR 0x08020000 // 假设每个 Bank 128KB
#define LOG_SIZE 4096 // 每次日志块大小
uint32_t current_bank = 0; // 0: Bank1, 1: Bank2
void Switch_Bank(void) {
if (current_bank == 0) {
// 擦除 Bank2(准备下一次写入)
HAL_FLASHEx_Erase(FLASH_TYPEERASE_MASS, BANK2_ADDR, 1, NULL);
current_bank = 1;
} else {
HAL_FLASHEx_Erase(FLASH_TYPEERASE_MASS, BANK1_ADDR, 1, NULL);
current_bank = 0;
}
}
```
## 5. 完整代码示例
以下示例演示如何将日志缓冲区中的数据通过 MDMA 写入当前 Bank,并在后台切换 Bank。
```c
#include "stm32h7xx_hal.h"
// 日志缓冲区(内存)
uint32_t log_buffer[1024]; // 4KB
// MDMA 句柄
extern MDMA_HandleTypeDef hmdma;
// 当前写入地址
uint32_t write_addr;
// 初始化 MDMA 和 Flash
void Log_Init(void) {
MDMA_Init();
Flash_Init();
write_addr = BANK1_ADDR; // 从 Bank1 开始
current_bank = 0;
// 预擦除 Bank2
HAL_FLASHEx_Erase(FLASH_TYPEERASE_MASS, BANK2_ADDR, 1, NULL);
}
// 启动 MDMA 搬运(非阻塞)
void Log_Write(uint32_t* data, uint32_t size) {
// 配置 MDMA 传输长度(以字为单位)
hmdma.Init.BufferTransferLength = size;
HAL_MDMA_Init(&hmdma);
// 启动 MDMA,源为 data,目的为 Flash 编程地址
HAL_MDMA_Start(&hmdma, (uint32_t)data, write_addr, size);
// 等待传输完成(实际应用中可用中断或轮询)
HAL_MDMA_PollForTransfer(&hmdma, HAL_MDMA_FULL_TRANSFER, HAL_MAX_DELAY);
// 写入完成后,切换 Bank 并更新地址
write_addr = (current_bank == 0) ? BANK2_ADDR : BANK1_ADDR;
Switch_Bank();
}
// 主函数示例
int main(void) {
HAL_Init();
SystemClock_Config();
Log_Init();
while (1) {
// 模拟日志数据
for (int i = 0; i < 1024; i++) {
log_buffer[i] = i + HAL_GetTick();
}
Log_Write(log_buffer, 1024);
HAL_Delay(1000); // 每秒写一次
}
}
```
## 6. 注意事项
- **Flash 编程时序**:MDMA 写入 Flash 时,Flash 控制器必须处于编程状态,且地址需按 32 位对齐。若数据未对齐,需先拷贝到对齐缓冲区(会损失零拷贝优势)。
- **擦除与写入冲突**:双 Bank 交错时,确保擦除操作在 Bank 切换前完成,否则可能写入失败。建议使用中断或 DMA 完成回调来触发切换。
- **缓存一致性**:如果日志缓冲区位于可缓存区域(如 SRAM),需在 MDMA 启动前执行 `SCB_CleanDCache()`,否则可能读到旧数据。
- **MDMA 错误处理**:务必检查 MDMA 传输错误标志(如 `HAL_MDMA_GetError`),并在回调中恢复。
- **性能优化**:增大 MDMA 单次传输长度(如 8KB)可减少启动开销,但需权衡内存占用。
## 7. 总结
通过 MDMA 与双 Bank Flash 的配合,STM32H7 实现了日志数据的零拷贝并行搬运,CPU 负载几乎为零,且写入速度接近 Flash 最大编程速率。该方法适用于高频日志记录、数据采集等场景,是嵌入式高性能存储的典范。开发者可根据实际需求调整缓冲区大小和 Bank 数量,进一步优化系统。