STM32H7 零等待日志存储:MDMA 与双 Bank Flash 并行搬运实战
👁 2 阅读 · 2026-08-27 · 嵌入式
在高速数据采集与实时控制系统中,日志记录往往成为性能瓶颈。STM32H7 凭借其强大的 MDMA 控制器和双 Bank Flash 架构,可实现数据搬运与存储的完全并行,从而达成“零等待”日志存储。本文将深入剖析其工作原理,手把手教你配置 MDMA 与 Flash 双 Bank 交替写入,并提供完整代码示例与关键注意事项,助你彻底释放 H7 的存储潜能。
# 引言
在嵌入式开发中,日志存储常因 Flash 擦写耗时而被诟病。STM32H7 系列(如 H743/H750)内置了 MDMA(Master DMA)和双 Bank Flash,通过巧妙设计,可以让数据搬运(内存→Flash)与 CPU 执行完全解耦,甚至实现双 Bank 交替写入,消除擦除等待。本文面向有 STM32 基础的开发者,深入讲解如何利用 MDMA 与双 Bank 实现零等待日志存储。
# 原理剖析
## 1. 为什么需要 MDMA?
传统 DMA 只能在外设与内存间搬运,而 MDMA 支持内存到内存、内存到外设,且具备 64 位带宽、4KB 突发传输和链表模式。在日志场景中,CPU 只需将日志数据写入 RAM 缓冲区,MDMA 自动将其搬运至 Flash 编程地址,CPU 可继续执行其他任务,实现“零等待”写入。
## 2. 双 Bank Flash 的优势
STM32H7 的 Flash 分为两个 Bank(Bank1 和 Bank2),每个 Bank 可独立擦除/编程。当 Bank1 正在擦除时,MDMA 可向 Bank2 写入数据;反之亦然。通过交替使用两个 Bank,擦除操作被隐藏,日志写入看似连续无延迟。
## 3. 并行搬运机制
- **MDMA 链表**:将多个传输块链接,每个块对应一个 Bank 的写入任务。
- **中断同步**:MDMA 传输完成中断触发 Flash 编程,编程完成中断切换 Bank。
- **双缓冲**:RAM 中设置两个缓冲区,一个用于 CPU 写入日志,另一个用于 MDMA 读取搬运。
# 硬件与软件准备
- 硬件:STM32H743 开发板(或 H750),外部晶振 25MHz。
- 软件:STM32CubeIDE 1.13+,HAL 库。
- 配置:启用 MDMA、Flash 双 Bank 模式(需注意 H7 的 Flash 选项字节)。
# 配置步骤
## 1. 使能 MDMA 时钟并配置
在 CubeMX 中,选择 MDMA,添加一个通道,配置如下:
- 传输方向:内存到内存
- 数据宽度:32 位(或 64 位)
- 突发大小:4 或 8(提高效率)
- 循环模式:禁用(使用链表)
- 中断:传输完成中断使能
## 2. 配置 Flash 双 Bank 模式
H7 默认工作在单 Bank 模式。需通过选项字节将 Flash 配置为双 Bank 模式(DBANK=1)。在代码中可调用 `HAL_FLASH_OB_Unlock()` 和 `HAL_FLASH_OB_Launch()` 实现,或直接在 CubeMX 中设置。
## 3. 编写 MDMA 链表配置
```c
// 定义 MDMA 节点结构体
MDMA_LinkNodeTypeDef Node1, Node2;
void MDMA_Config(void)
{
// 初始化 MDMA 句柄
hmdma.Init.Request = MDMA_REQUEST_SW;
hmdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER;
hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma.Init.Endianness = MDMA_LITTLE_ENDIANNESS;
hmdma.Init.SourceInc = MDMA_SRC_INC_WORD;
hmdma.Init.DestInc = 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 = 1024; // 每次传输 1024 字
hmdma.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
hmdma.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
hmdma.Init.SourceBlockAddressOffset = 0;
hmdma.Init.DestBlockAddressOffset = 0;
HAL_MDMA_Init(&hmdma);
// 配置链表节点
Node1.SourceAddress = (uint32_t)buffer1;
Node1.DestAddress = FLASH_BANK1_BASE;
Node1.BlockDataLength = 1024;
Node1.NextNodeAddress = (uint32_t)&Node2;
Node2.SourceAddress = (uint32_t)buffer2;
Node2.DestAddress = FLASH_BANK2_BASE;
Node2.BlockDataLength = 1024;
Node2.NextNodeAddress = 0; // 链表结束
// 将链表链接到 MDMA
HAL_MDMA_LinkNode(&hmdma, &Node1);
}
```
## 4. Flash 编程与 Bank 切换
```c
// 擦除 Bank1 并等待完成
void EraseBank1(void)
{
FLASH_EraseInitTypeDef erase;
uint32_t error = 0;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Banks = FLASH_BANK_1;
erase.Sector = 0;
erase.NbSectors = 8;
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&erase, &error);
HAL_FLASH_Lock();
}
// 编程函数(由 MDMA 中断调用)
void FlashProgram(uint32_t dest, uint32_t *src, uint32_t size)
{
HAL_FLASH_Unlock();
for (uint32_t i = 0; i < size; i++) {
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, dest + i*4, src[i]);
}
HAL_FLASH_Lock();
}
```
## 5. 主循环与中断处理
```c
volatile uint8_t bank_ready = 0;
void MDMA_IRQHandler(void)
{
HAL_MDMA_IRQHandler(&hmdma);
}
void HAL_MDMA_XferCpltCallback(MDMA_HandleTypeDef *hmdma)
{
// 根据当前使用的 Bank 切换
if (current_bank == 1) {
FlashProgram(FLASH_BANK2_BASE, buffer2, 1024);
EraseBank1(); // 擦除 Bank1 为下次使用
current_bank = 2;
} else {
FlashProgram(FLASH_BANK1_BASE, buffer1, 1024);
EraseBank2();
current_bank = 1;
}
bank_ready = 1;
}
int main(void)
{
// 初始化...
MDMA_Config();
EraseBank1();
EraseBank2();
while (1) {
// 等待缓冲区满
if (log_ready) {
HAL_MDMA_Start_IT(&hmdma, (uint32_t)buffer1, FLASH_BANK1_BASE, 1024);
log_ready = 0;
}
}
}
```
# 完整代码示例
限于篇幅,以上为核心代码。完整工程可在 GitHub 获取(示例链接)。关键点:
- 使用双缓冲:`buffer1` 和 `buffer2` 交替。
- MDMA 链表自动切换源/目的地址。
- 中断中执行 Flash 编程和 Bank 擦除。
# 注意事项
- **Flash 编程时间**:H7 的 Flash 编程需要约 20-40us/字,MDMA 传输 1024 字约 10us,因此编程是瓶颈。务必确保 MDMA 传输完成后再编程,否则数据丢失。
- **Bank 擦除时间**:擦除一个 Bank(1MB)约 1-2s,因此日志数据量不宜过大,建议每 Bank 存储 512KB 日志,交替使用。
- **缓存一致性**:MDMA 访问 RAM 时,需确保 CPU 缓存已刷新(`SCB_CleanDCache()`),否则可能读到脏数据。
- **中断优先级**:MDMA 中断应设为最高优先级,避免日志丢失。
- **双 Bank 配置**:修改选项字节后需复位生效,且注意备份用户代码。
# 总结
通过 MDMA 与双 Bank Flash 的配合,STM32H7 实现了日志存储的“零等待”写入,CPU 几乎零开销。本文提供的方案可直接应用于工业数据记录、故障分析等场景。实际项目中,可根据日志大小调整缓冲区与链块数量,进一步优化性能。希望本文能帮助你解锁 H7 的极致存储能力!