STM32H7 零拷贝日志存储实战:MDMA 与双 Bank Flash 的深度配置
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统开发中,日志存储往往面临性能与可靠性的双重挑战。STM32H7 系列凭借其强大的 MDMA 控制器和双 Bank Flash 架构,为实现零拷贝日志存储提供了硬件基础。本文将深入剖析 MDMA 与双 Bank Flash 的工作原理,并给出从初始化到中断处理的完整配置步骤,帮助开发者彻底告别 CPU 搬运数据的时代,实现高效、非阻塞的日志记录。
# STM32H7 零拷贝日志存储实战:MDMA 与双 Bank Flash 的深度配置
## 1. 为什么需要零拷贝日志存储?
传统日志存储通常依赖 CPU 将日志数据从缓冲区拷贝到 Flash 写入缓冲区,再通过 Flash 编程接口写入。这种方式存在两大痛点:
- **CPU 占用高**:每次拷贝和编程都占用 CPU 时间,影响实时任务。
- **阻塞风险**:Flash 编程期间 CPU 无法响应中断,可能导致系统卡顿。
STM32H7 的 MDMA(Master DMA)支持内存到内存、内存到外设的高速传输,且具备双 Bank Flash 的并行编程能力,使得日志数据可以直接从 RAM 缓冲区搬运到 Flash,无需 CPU 介入,实现真正的零拷贝。
## 2. 硬件基础:MDMA 与双 Bank Flash
### 2.1 MDMA 关键特性
- **双通道传输**:支持 8/16/32 位数据宽度,可配置突发传输(Burst)。
- **链表模式**:支持多个传输描述符(LLI),实现连续传输。
- **中断触发**:传输完成或错误可触发中断,便于软件处理。
### 2.2 双 Bank Flash 架构
STM32H7 的 Flash 分为 Bank1 和 Bank2,每个 Bank 可独立擦除和编程。通过配置 `FLASH_CR` 寄存器的 `DBANK` 位,可启用双 Bank 模式,允许同时擦写两个 Bank,显著提升写入吞吐量。
## 3. 系统设计思路
目标:将日志数据从 RAM 缓冲区(如 `uint8_t log_buffer[1024]`)通过 MDMA 传输到 Flash 的指定区域,传输完成后自动擦除并写入下一块区域,全程无需 CPU 参与。
设计要点:
- 使用 MDMA 的 LLI 链表,将日志数据分块传输到 Flash 编程数据寄存器(如 `FLASH_CR` 和 `FLASH_DR`)。
- 利用双 Bank 交替写入,避免擦除等待。
- 通过中断通知日志系统传输完成,更新写指针。
## 4. 配置步骤
### 4.1 使能 MDMA 和 Flash 时钟
```c
__HAL_RCC_MDMA_CLK_ENABLE();
__HAL_RCC_FLASH_CLK_ENABLE();
```
### 4.2 配置 Flash 双 Bank 模式
```c
FLASH_OBProgramInitTypeDef ob_init;
HAL_FLASHEx_OBGetConfig(&ob_init);
ob_init.Banks = FLASH_BANK_BOTH;
ob_init.OptionType = OPTIONBYTE_WRP;
// 启用双 Bank(需在解锁后设置 DBANK 位)
FLASH_OB_Unlock();
FLASH_OB_EnableDualBank();
FLASH_OB_Lock();
// 注意:修改后需复位生效
```
### 4.3 配置 MDMA 传输
使用 STM32CubeMX 或直接代码配置 MDMA。以下为关键配置:
```c
MDMA_HandleTypeDef hmdma;
void MDMA_Config(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; // 目标地址递增
hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
hmdma.Init.BufferTransferLength = 256; // 每次传输 256 字(1KB)
HAL_MDMA_Init(&hmdma);
// 配置 LLI 链表(用于连续传输多个块)
MDMA_LLIInitTypeDef lli;
lli.SrcAddress = (uint32_t)log_buffer;
lli.DstAddress = (uint32_t)flash_write_addr;
lli.NextLLI = (uint32_t)&lli_node[1];
lli.BufferTransferLength = 256;
HAL_MDMA_ConfigLLI(&hmdma, &lli);
}
```
### 4.4 启动 MDMA 传输
```c
HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)flash_write_addr, 256);
```
### 4.5 中断处理
在中断回调中更新日志写指针,并触发下一次传输(如果需要)。
```c
void HAL_MDMA_IRQHandler(MDMA_HandleTypeDef *hmdma) {
if (hmdma->Instance == MDMA_Channel0) {
// 传输完成,更新写指针
log_write_index += 1024;
if (log_write_index >= LOG_BUFFER_SIZE) {
log_write_index = 0;
}
// 可在此处触发下一次传输
}
}
```
## 5. 完整代码示例
以下是一个简化的日志存储模块,演示了 MDMA 传输和双 Bank 擦写配合:
```c
#include "stm32h7xx_hal.h"
#define LOG_BUFFER_SIZE 4096
#define FLASH_LOG_ADDR_BANK1 0x08020000 // 假设 Bank1 起始地址
#define FLASH_LOG_ADDR_BANK2 0x08060000 // Bank2 起始地址
uint8_t log_buffer[LOG_BUFFER_SIZE];
volatile uint32_t log_write_index = 0;
MDMA_HandleTypeDef hmdma;
void Flash_Erase_Bank(uint32_t bank) {
FLASH_EraseInitTypeDef erase;
uint32_t sector_error = 0;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Banks = bank;
erase.Sector = 0;
erase.NbSectors = 8; // 根据实际扇区数调整
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&erase, §or_error);
HAL_FLASH_Lock();
}
void Log_Init(void) {
// 初始化 MDMA
MDMA_Config();
// 擦除两个 Bank 的日志区域
Flash_Erase_Bank(FLASH_BANK_1);
Flash_Erase_Bank(FLASH_BANK_2);
// 启动第一次传输
HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)FLASH_LOG_ADDR_BANK1, LOG_BUFFER_SIZE/4);
}
void Log_Write(uint8_t *data, uint32_t len) {
// 将数据拷贝到 log_buffer(此处为示例,实际可设计为环形缓冲区)
memcpy(log_buffer + log_write_index, data, len);
log_write_index += len;
// 当缓冲区满时,启动 MDMA 传输(此处简化,实际需处理并发)
if (log_write_index >= LOG_BUFFER_SIZE) {
HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)FLASH_LOG_ADDR_BANK1, LOG_BUFFER_SIZE/4);
log_write_index = 0;
}
}
void HAL_MDMA_IRQHandler(MDMA_HandleTypeDef *hmdma) {
if (hmdma->Instance == MDMA_Channel0) {
// 传输完成,可切换 Bank 或继续
static uint8_t bank_toggle = 0;
if (bank_toggle == 0) {
// 下次写入 Bank2
HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)FLASH_LOG_ADDR_BANK2, LOG_BUFFER_SIZE/4);
bank_toggle = 1;
} else {
bank_toggle = 0;
}
}
}
```
## 6. 注意事项
- **Flash 编程时序**:MDMA 传输到 Flash 时,必须确保 Flash 已解锁,且编程地址对齐(通常为 32 位)。
- **双 Bank 擦除**:擦除操作会阻塞 CPU,建议在系统空闲时进行,或使用 Flash 的 RWW(Read-While-Write)特性。
- **MDMA 配置**:LLI 链表长度需与缓冲区大小匹配,否则可能传输不完整。
- **中断优先级**:MDMA 中断应设置为高优先级,避免日志丢失。
- **缓存一致性**:如果使用 D-Cache,需在传输前执行 `SCB_CleanDCache()`,确保数据从缓存写入 RAM。
## 7. 总结
通过 MDMA 与双 Bank Flash 的配合,STM32H7 实现了日志存储的零拷贝,CPU 负载大幅降低,系统响应更加实时。本文提供的配置方法和代码可直接应用于实际项目,开发者可根据需求调整缓冲区大小和传输策略。希望这篇实战指南能帮助你在嵌入式开发中更高效地利用 STM32H7 的硬件资源。