STM32H7 双 Bank Flash 交替执行与 MDMA 零等待代码搬运:配置细节深度解析

1. 为什么需要零等待代码搬运?

STM32H7 系列主频高达 480MHz(甚至 550MHz),但内嵌 Flash 的访问速度受限于其物理特性,通常需要插入等待状态(如 7 个等待周期)。这意味着直接从 Flash 取指时,CPU 会频繁停顿,严重降低实时性。虽然 ART(自适应实时加速器)和缓存能缓解,但在确定性要求极高的场景(如电机控制、音频处理)中,仍可能出现抖动。

解决方案:将关键代码(如中断服务函数、实时算法)搬运到 RAM 中执行。但传统 DMA 搬运需要 CPU 介入,且搬运期间代码无法执行。利用 STM32H7 的双 Bank Flash 架构,我们可以实现交替执行:CPU 从 Bank1 执行代码,同时 MDMA 将 Bank2 中的新代码搬运至 RAM;完成后切换执行源,实现无缝更新。

2. 双 Bank Flash 架构基础

STM32H7 的 Flash 分为两个 Bank(Bank1 和 Bank2),每个 Bank 容量可达 1MB(部分型号)。关键特性:

  • 独立读访问:两个 Bank 可同时被访问(如 CPU 读 Bank1,MDMA 读 Bank2)。
  • 编程/擦除并行:对一个 Bank 进行写操作时,另一个 Bank 可正常读。
  • 映射切换:通过选项字节或寄存器,可将 Bank2 映射到地址 0x08000000,实现无缝切换。

交替执行原理

  1. 初始状态:CPU 从 Bank1(地址 0x08000000)执行代码。
  2. MDMA 将 Bank2 中的新代码(地址 0x08100000)搬运至 RAM(如 AXI SRAM)。
  3. 搬运完成后,设置一个标志,CPU 跳转到 RAM 中的代码执行(或通过函数指针调用)。
  4. 同时,MDMA 可将 Bank1 中的另一段代码搬运至另一块 RAM,准备下一次切换。

这样,CPU 始终执行 RAM 中的“最新”代码,而 Flash 的等待状态被完全规避。

3. MDMA 配置关键点

MDMA(Master DMA)是 STM32H7 的高性能 DMA,支持 128 位数据传输、链接列表、4GB 地址空间。配置步骤:

3.1 时钟使能

__HAL_RCC_MDMA_CLK_ENABLE();

3.2 配置 MDMA 通道

使用 HAL 库或 LL 库。以下为 HAL 示例:

MDMA_HandleTypeDef hmdma;

void MDMA_Config(void) {
    hmdma.Instance = MDMA_Channel0;
    hmdma.Init.Request = MDMA_REQUEST_SW; // 软件触发,便于控制
    hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER; // 块传输
    hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
    hmdma.Init.Endianness = MDMA_LITTLE_ENDIAN;
    hmdma.Init.SourceInc = MDMA_SRC_INC_WORD; // 源地址递增
    hmdma.Init.DestinationInc = MDMA_DEST_INC_WORD; // 目的地址递增
    hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD; // 32位
    hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
    hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
    hmdma.Init.BufferTransferLength = 0; // 稍后设置
    hmdma.Init.SyncMode = MDMA_SYNC_FULL;
    hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
    hmdma.Init.SyncRequest = MDMA_SYNC_SW;
    hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
    hmdma.Init.SyncValue = 0;
    hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
    hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
    hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
    hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
    hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
    hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
    hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
    hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
    hmdma.Init.SyncRequest = MDMA_SYNC_SW;
    hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
    hmdma.Init.SyncValue = 0;
    hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
    hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
    hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
    hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
    hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
    hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
    hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
    hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
    hmdma.Init.SyncRequest = MDMA_SYNC_SW;
    hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
    hmdma.Init.SyncValue = 0;
    hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
    hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
    hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
    hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
    hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
    hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
    hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
    hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
    hmdma.Init.SyncRequest = MDMA_SYNC_SW;
    hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
    hmdma.Init.SyncValue = 0;
    hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
    hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
    hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
    hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
    hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
    hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
    hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
    // 注意:以上重复字段仅示意,实际只需设置一次。
    // 简化配置:
    hmdma.Init.Request = MDMA_REQUEST_SW;
    hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER;
    hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
    hmdma.Init.SourceInc = MDMA_SRC_INC_WORD;
    hmdma.Init.DestinationInc = 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 = 0;
    hmdma.Init.SyncMode = MDMA_SYNC_FULL;
    hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
    hmdma.Init.SyncRequest = MDMA_SYNC_SW;
    hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
    hmdma.Init.SyncValue = 0;
    hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
    hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
    hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
    hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
    hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
    hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
    hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
    hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
    hmdma.Init.SyncRequest = MDMA_SYNC_SW;
    hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
    hmdma.Init.SyncValue = 0;
    hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
    hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
    hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
    hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
    hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
    hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
    hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
    // 实际配置请精简,此处为示意。
    HAL_MDMA_Init(&hmdma);
}

注意:MDMA 配置项较多,务必参考参考手册逐一核对。建议使用 CubeMX 生成基础配置,再手动调整。

3.3 启动传输

void MDMA_StartTransfer(uint32_t src, uint32_t dst, uint32_t size) {
    hmdma.Init.BufferTransferLength = size / 4; // 以字为单位
    HAL_MDMA_ConfigTransfer(&hmdma, src, dst, size);
    HAL_MDMA_Start(&hmdma, src, dst, size);
}

4. 双 Bank 交替执行实现步骤

4.1 链接脚本设置

将需要搬运的代码段放入独立区域,例如:

/* 在链接脚本中定义 */
__RAM_EXEC_START = 0x24000000; /* AXI SRAM 起始地址 */
__RAM_EXEC_SIZE = 0x10000; /* 64KB */

SECTIONS {
    .ram_exec : {
        *(.ram_exec)
    } > RAM_EXEC
}

在代码中,使用 __attribute__((section(".ram_exec"))) 标记关键函数。

4.2 初始化流程

void SystemInit_Exec(void) {
    // 1. 使能 MDMA 时钟
    __HAL_RCC_MDMA_CLK_ENABLE();
    MDMA_Config();

    // 2. 将 Bank2 中的代码搬运到 RAM
    uint32_t src = 0x08100000; // Bank2 起始地址
    uint32_t dst = (uint32_t)&__RAM_EXEC_START;
    uint32_t size = (uint32_t)&__RAM_EXEC_SIZE;
    MDMA_StartTransfer(src, dst, size);

    // 3. 等待传输完成
    while (HAL_MDMA_GetState(&hmdma) != HAL_MDMA_STATE_READY);

    // 4. 跳转到 RAM 中的函数(例如 main_ram)
    void (*ram_main)(void) = (void (*)(void))dst;
    ram_main();
}

4.3 交替执行策略

  • 使用两个 RAM 缓冲区(如 AXI SRAM 和 D2 SRAM),交替作为执行区。
  • 当 CPU 执行缓冲区 A 时,MDMA 将下一段代码搬运到缓冲区 B;完成后切换。
  • 通过中断或轮询标志实现同步。

5. 完整示例:搬运一个中断服务函数

// 定义在 Flash Bank2 中的函数
__attribute__((section(".ram_exec"))) void ISR_Handler(void) {
    // 实时处理代码
    GPIOE->ODR ^= (1 << 1);
}

// 主程序
int main(void) {
    HAL_Init();
    SystemClock_Config();
    // ... 初始化外设

    // 搬运 ISR_Handler 到 RAM
    extern uint32_t __ram_exec_start;
    extern uint32_t __ram_exec_end;
    uint32_t size = (uint32_t)&__ram_exec_end - (uint32_t)&__ram_exec_start;
    MDMA_StartTransfer((uint32_t)ISR_Handler, (uint32_t)&__ram_exec_start, size);
    while (HAL_MDMA_GetState(&hmdma) != HAL_MDMA_STATE_READY);

    // 设置中断向量表指向 RAM(可选)
    SCB->VTOR = (uint32_t)&__ram_exec_start;

    // 使能中断,ISR 将从 RAM 执行
    HAL_NVIC_SetPriority(EXTI1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(EXTI1_IRQn);

    while (1) {
        // 主循环
    }
}

6. 注意事项与常见陷阱

  • 缓存一致性:MDMA 搬运后,需确保 CPU 缓存与 RAM 内容一致。使用 SCB_CleanDCache()SCB_InvalidateDCache() 或配置 MPU 区域为非缓存。
  • 对齐要求:MDMA 要求源和目的地址按 32 位对齐,传输长度也需为 4 的倍数。
  • 中断安全:在搬运过程中,避免中断访问正在被写入的 RAM 区域,否则可能读到半更新数据。
  • Flash 编程冲突:如果同时进行 Flash 编程(如写入日志),需确保 MDMA 读操作不与编程冲突,建议使用互斥机制。
  • 链接脚本:确保 .ram_exec 段正确放置,且起始地址在 RAM 区域内。
  • 调试:使用调试器时,注意断点可能影响时序,建议在发布版本中测试。

7. 总结

通过双 Bank Flash 交替执行与 MDMA 搬运,STM32H7 可以完全规避 Flash 等待状态,实现确定性的零等待代码执行。本文详细介绍了架构原理、MDMA 配置、实现步骤及注意事项,为高性能嵌入式应用提供了有力参考。实际项目中,可根据需求调整缓冲区大小和切换策略,以达到最佳性能。