STM32H7 零等待日志:MDMA 与双 Bank Flash 并行搬运实战
👁 7 阅读 · 2026-08-27 · 嵌入式
在高速数据采集与实时控制系统中,日志记录常因 Flash 擦写延迟而阻塞主流程。STM32H7 凭借双 Bank Flash 架构与 MDMA 控制器,可实现日志数据的零等待异步搬运。本文深入解析 MDMA 与双 Bank 并行操作原理,给出基于 HAL 库的完整配置步骤与代码示例,并探讨时序优化与注意事项,助你构建高性能日志系统。
# STM32H7 零等待日志:MDMA 与双 Bank Flash 并行搬运实战
## 一、为什么需要零等待日志?
在嵌入式系统中,日志记录是调试与数据追溯的关键。传统方式下,CPU 直接写 Flash,每次擦写需等待数十毫秒,导致实时任务被阻塞。STM32H7 系列(如 H743/H750)内置双 Bank Flash(各 1MB),支持同时读写不同 Bank,配合 MDMA(Master DMA)可让数据在内存与 Flash 间异步传输,CPU 仅需发起请求,彻底解放处理能力。
## 二、核心原理:双 Bank 与 MDMA 协同
### 1. 双 Bank Flash 架构
- STM32H7 Flash 分为 Bank1 和 Bank2,各自独立擦写单元。
- 当 CPU 或 DMA 访问 Bank1 时,Bank2 可同时进行擦除/编程操作,反之亦然。
- 利用此特性,日志写入可交替使用两个 Bank,避免单 Bank 擦写期间的等待。
### 2. MDMA 的作用
- MDMA 是高级 DMA,支持内存到内存、外设到内存等传输,且可链接列表(Linked List)。
- 它能在后台将 RAM 中的日志缓冲区搬运至 Flash 编程接口(如 FLASH_CR),无需 CPU 干预。
- 通过中断或标志通知 CPU 传输完成,实现异步日志。
### 3. 并行搬运策略
- 设计双缓冲区:CPU 写日志到 Buffer A,MDMA 将 Buffer B 数据写入 Flash Bank1;同时 Flash Bank2 预擦除,准备下一次写入。
- 交替使用 Bank1/Bank2,实现“写-擦-写”流水线,消除擦除等待。
## 三、硬件与软件准备
- 硬件:STM32H743 开发板(双 Bank Flash),外部晶振 25MHz。
- 软件:STM32CubeIDE 1.13+,HAL 库(F7 系列支持 H7)。
- 关键外设:MDMA、FLASH、GPIO(用于指示)。
## 四、配置步骤(基于 CubeMX)
1. **时钟配置**:启用 HSE,主频 400MHz,AHB 分频确保 MDMA 时钟。
2. **MDMA 配置**:
- 在 Connectivity 中选择 MDMA,添加通道。
- 设置源地址(RAM 缓冲区)、目标地址(Flash 编程寄存器)。
- 传输模式:内存到内存,数据宽度 32 位,突发传输。
- 启用中断(传输完成)。
3. **Flash 配置**:
- 在 Memory 中启用双 Bank 模式(默认开启)。
- 设置编程延迟(等待状态),根据主频调整。
4. **生成代码**:CubeMX 自动生成初始化函数。
## 五、代码实现
以下为关键代码片段,完整工程可参考文末链接。
```c
/* 日志缓冲区定义 */
#define LOG_BUF_SIZE 1024
uint32_t log_buf_A[LOG_BUF_SIZE/4];
uint32_t log_buf_B[LOG_BUF_SIZE/4];
volatile uint8_t active_buf = 0; // 当前 CPU 写入的缓冲区
/* MDMA 句柄 */
MDMA_HandleTypeDef hmdma_log;
/* Flash 编程地址(示例:Bank1 起始) */
#define FLASH_LOG_ADDR 0x08020000 // 根据实际调整
/* 初始化 MDMA */
void MDMA_Log_Init(void) {
hmdma_log.Instance = MDMA_Channel0;
hmdma_log.Init.Request = MDMA_REQUEST_MEM2MEM;
hmdma_log.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER;
hmdma_log.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma_log.Init.Endianness = MDMA_LITTLE_ENDIANNESS;
hmdma_log.Init.SourceInc = MDMA_SRC_INC_WORD;
hmdma_log.Init.DestInc = MDMA_DEST_INC_WORD;
hmdma_log.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
hmdma_log.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma_log.Init.DataAlignment = MDMA_DATAALIGN_PACKET;
hmdma_log.Init.BufferTransferLength = LOG_BUF_SIZE/4;
hmdma_log.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
hmdma_log.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
hmdma_log.Init.SourceBlockAddressOffset = 0;
hmdma_log.Init.DestBlockAddressOffset = 0;
HAL_MDMA_Init(&hmdma_log);
/* 配置中断 */
HAL_NVIC_SetPriority(MDMA_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(MDMA_IRQn);
}
/* 启动一次 MDMA 传输:将指定缓冲区写入 Flash */
void MDMA_Log_Write(uint32_t *src, uint32_t dest, uint32_t size) {
/* 等待 MDMA 空闲 */
while (HAL_MDMA_GetState(&hmdma_log) != HAL_MDMA_STATE_READY);
/* 配置源和目标地址 */
hmdma_log.Init.SourceAddress = (uint32_t)src;
hmdma_log.Init.DestAddress = dest;
hmdma_log.Init.BufferTransferLength = size/4;
/* 启动传输 */
HAL_MDMA_Start_IT(&hmdma_log, (uint32_t)src, dest, size/4);
}
/* MDMA 传输完成回调 */
void HAL_MDMA_IRQHandler(MDMA_HandleTypeDef *hmdma) {
if (hmdma == &hmdma_log) {
HAL_MDMA_IRQHandler(hmdma);
// 传输完成,切换缓冲区
active_buf ^= 1;
// 可在此处触发下一次预擦除
}
}
/* 日志写入函数(CPU 调用) */
void Log_Write(uint32_t *data, uint32_t len) {
uint32_t *cur_buf = (active_buf == 0) ? log_buf_A : log_buf_B;
// 将数据复制到当前缓冲区(简单示例,实际可用环形队列)
memcpy(cur_buf, data, len);
// 启动 MDMA 写入当前缓冲区到 Flash(目标地址根据 active_buf 选择 Bank)
uint32_t dest = (active_buf == 0) ? FLASH_LOG_ADDR : FLASH_LOG_ADDR + 0x100000; // Bank2 偏移
MDMA_Log_Write(cur_buf, dest, len);
// 同时预擦除另一个 Bank 的对应区域(需在后台执行,此处简化)
// 实际应使用 FLASH 中断或定时器触发擦除
}
/* 主函数示例 */
int main(void) {
HAL_Init();
SystemClock_Config();
MDMA_Log_Init();
// 预擦除 Bank1 和 Bank2 的日志区域
FLASH_Erase_InitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Banks = FLASH_BANK_1;
erase.Sector = 12; // 示例扇区
erase.NbSectors = 1;
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASHEx_Erase(&erase, &error);
// 主循环
while (1) {
// 模拟日志产生
uint32_t log_data[64];
for (int i=0; i<64; i++) log_data[i] = HAL_GetTick() + i;
Log_Write(log_data, sizeof(log_data));
HAL_Delay(100); // 模拟其他任务
}
}
```
## 六、时序优化与注意事项
- **Flash 编程电压**:确保 VOS 和电压范围匹配,否则擦写失败。
- **MDMA 优先级**:设置高优先级,避免被其他 DMA 抢占导致延迟。
- **缓冲区对齐**:MDMA 要求源和目标地址按字对齐,建议使用 `__attribute__((aligned(4)))`。
- **擦除操作**:预擦除应在 MDMA 传输期间进行,但需确保不访问同一 Bank。建议使用定时器中断或后台任务。
- **错误处理**:检查 MDMA 传输错误标志,如 `HAL_MDMA_GetError()`。
- **功耗**:MDMA 和 Flash 同时工作会增加功耗,低功耗场景需权衡。
## 七、总结
通过 MDMA 与双 Bank Flash 的配合,STM32H7 实现了日志的零等待写入,CPU 利用率大幅提升。本方案可扩展至数据采集、故障记录等场景。实际应用中需根据 Flash 擦写时间调整缓冲区大小和预擦除策略,以达到最优性能。
(完整工程代码可访问 [GitHub 示例仓库](https://github.com/example/stm32h7-mdma-log))