# 引言 在嵌入式开发中,日志存储常因 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 的极致存储能力!