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 自动生成初始化函数。

五、代码实现

以下为关键代码片段,完整工程可参考文末链接。

/* 日志缓冲区定义 */
#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 示例仓库