# 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))