STM32H7 双 Bank 模式下在线固件升级:地址映射与跳转陷阱深度解析
一、双 Bank 模式概述
STM32H7 系列(如 STM32H743、H750)内置双 Bank Flash,每个 Bank 容量相等(如 1MB+1MB)。双 Bank 模式允许在运行一个 Bank 中的程序时,对另一个 Bank 进行擦写操作,从而实现无缝的固件升级。这种架构天然支持 A/B 分区,即 Bootloader 和 APP 可分别位于不同 Bank,或两个 APP 互为备份。
关键特性:
- 双 Bank 可独立擦写,支持同时读写(RWW)
- 支持硬件自动切换 Bank 启动(通过选项字节)
- 每个 Bank 拥有独立的地址空间,但映射方式受选项字节控制
二、地址映射与启动机制
2.1 默认映射(单 Bank 模式)
默认情况下,STM32H7 的 Flash 从 0x08000000 开始连续映射,Bank1 和 Bank2 依次排列。例如,1MB+1MB 的芯片,Bank1 地址为 0x08000000-0x080FFFFF,Bank2 为 0x08100000-0x081FFFFF。
2.2 双 Bank 映射(SWAP 模式)
通过设置选项字节 nSWAP_BANK,可以交换 Bank1 和 Bank2 的映射地址。当 SWAP_BANK=1 时,Bank2 被映射到 0x08000000,而 Bank1 被映射到 0x08100000。这种机制允许 Bootloader 在 Bank1 中运行,同时将新固件写入 Bank2,然后通过软件触发交换,使新固件在下次复位后从 0x08000000 启动。
地址映射示意:
默认模式:
0x08000000 - 0x080FFFFF : Bank1 (Bootloader)
0x08100000 - 0x081FFFFF : Bank2 (APP)
SWAP模式:
0x08000000 - 0x080FFFFF : Bank2 (APP)
0x08100000 - 0x081FFFFF : Bank1 (Bootloader)
三、Bootloader 与 APP 的地址规划
3.1 典型分区方案
-
Bootloader:位于 Bank1 起始地址
0x08000000,大小 32KB(0x08000000 - 0x08007FFF) -
APP:位于 Bank2 起始地址
0x08100000(默认映射),或通过 SWAP 后映射到0x08000000
3.2 链接脚本配置
Bootloader 的链接脚本(.ld)关键部分:
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
APP 的链接脚本(.ld)关键部分:
FLASH (rx) : ORIGIN = 0x08100000, LENGTH = 1M - 32K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
注意:APP 的起始地址必须与 Bank2 的基地址对齐,且中断向量表需重定位。
四、跳转实现与陷阱规避
4.1 跳转原理
Bootloader 在收到完整固件并校验通过后,需要跳转到 APP 执行。跳转的核心步骤:
- 关闭全局中断
- 设置主栈指针(MSP)为 APP 的初始栈顶值
- 设置向量表偏移寄存器(VTOR)指向 APP 的中断向量表
- 跳转到 APP 的复位处理函数(Reset_Handler)
4.2 完整跳转代码示例
// Bootloader 跳转函数
void jump_to_app(uint32_t app_addr)
{
// 检查 APP 栈顶地址是否合法(RAM 范围内)
uint32_t app_stack = *(volatile uint32_t *)app_addr;
if ((app_stack & 0xFFF00000) != 0x20000000)
{
return; // 非法地址,拒绝跳转
}
// 关闭全局中断
__disable_irq();
// 关闭所有外设中断(可选,根据实际外设情况)
for (int i = 0; i < 8; i++)
{
NVIC->ICER[i] = 0xFFFFFFFF;
NVIC->ICPR[i] = 0xFFFFFFFF;
}
// 设置主栈指针
__set_MSP(app_stack);
// 设置向量表偏移(仅适用于 Cortex-M7)
SCB->VTOR = app_addr;
// 获取复位处理函数地址
uint32_t reset_handler = *(volatile uint32_t *)(app_addr + 4);
// 跳转
void (*jump_func)(void) = (void (*)(void))reset_handler;
jump_func();
// 不会执行到这里
while (1);
}
4.3 常见陷阱与解决方案
陷阱1:VTOR 设置无效
现象:跳转后中断无法触发或死机。
原因:STM32H7 的 VTOR 要求偏移量按 256 字节对齐(VTOR_TBLBASE 位域),且 APP 的链接脚本中 VECT_TAB_OFFSET 必须与 Flash 地址偏移一致。
解决:确保 APP 工程中 system_stm32h7xx.c 的 VECT_TAB_OFFSET 设置为 0x100000(即 Bank2 偏移),并在跳转前正确设置 SCB->VTOR。
// APP 工程中设置向量表偏移
#define VECT_TAB_OFFSET 0x100000U /*!< Vector Table base offset field */
陷阱2:跳转后系统时钟异常
现象:APP 运行但外设工作不正常,如串口乱码。
原因:Bootloader 配置了时钟,APP 启动时重新初始化时钟,但若 Bootloader 中启用了看门狗或低功耗模式,可能导致 APP 初始化失败。
解决:跳转前关闭所有外设时钟(__HAL_RCC_DEINIT()),并复位时钟配置。
// 跳转前复位时钟
HAL_RCC_DeInit();
陷阱3:双 Bank 交换后跳转地址错误
现象:使用 SWAP 功能后,APP 无法启动。
原因:SWAP 操作后,Bank2 映射到 0x08000000,但 APP 的链接脚本仍指向 0x08100000,导致跳转地址错误。
解决:若使用 SWAP,APP 应始终链接到 0x08000000(即 Bank1 的地址),但实际存储在 Bank2。Bootloader 跳转时,需根据当前映射状态决定跳转地址。
// 检查当前映射状态(通过选项字节)
if (FLASH->OPTSR_CUR & FLASH_OPTSR_SWAP_BANK)
{
// 已交换,APP 在 0x08000000
jump_to_app(0x08000000);
}
else
{
// 未交换,APP 在 0x08100000
jump_to_app(0x08100000);
}
陷阱4:中断向量表对齐问题
现象:跳转后,中断服务函数无法执行。
原因:Cortex-M7 要求向量表地址按 256 字节对齐,且向量表大小需为 2 的幂次。
解决:在 APP 的链接脚本中,确保 .isr_vector 段对齐到 256 字节。
.isr_vector :
{
. = ALIGN(256);
KEEP(*(.isr_vector))
. = ALIGN(256);
} >FLASH
五、完整 Bootloader 升级流程示例
以下是一个简化的 Bootloader 主程序,演示了接收固件、写入 Bank2、校验并跳转的完整流程。
// Bootloader 主程序(简化)
int main(void)
{
HAL_Init();
SystemClock_Config();
UART_Init();
// 检查是否需要升级(如通过串口命令)
if (check_update_request())
{
// 擦除 Bank2
erase_bank2();
// 接收固件并写入 Bank2
receive_firmware_to_bank2();
// 校验固件(CRC或SHA)
if (verify_firmware())
{
// 设置 SWAP 位(可选,若使用硬件交换)
set_swap_bank();
// 跳转到 APP(根据映射状态)
if (FLASH->OPTSR_CUR & FLASH_OPTSR_SWAP_BANK)
jump_to_app(0x08000000);
else
jump_to_app(0x08100000);
}
else
{
// 校验失败,回滚或重试
error_handler();
}
}
else
{
// 直接跳转到现有 APP
jump_to_app(0x08100000);
}
while (1);
}
六、注意事项与最佳实践
- 固件校验:务必在跳转前进行完整性校验(如 CRC32),避免执行损坏的固件。
- 看门狗处理:跳转前关闭看门狗,或在 APP 中尽快重新初始化。
- 外设状态:跳转前将所有外设恢复到默认状态,避免 APP 初始化冲突。
- 调试接口:在跳转后,调试器可能无法正常工作,建议在 APP 中重新配置调试端口。
- 双 Bank 交换:使用 SWAP 时,需确保 APP 链接地址与映射后的地址一致,否则会跳转失败。
七、总结
STM32H7 双 Bank 模式为 OTA 提供了强大的硬件支持,但地址映射和跳转逻辑是其中的核心难点。理解默认映射与 SWAP 映射的区别,正确配置链接脚本和向量表偏移,并规避常见陷阱,才能实现稳定可靠的在线升级。希望本文的剖析和代码示例能帮助你少走弯路,构建出健壮的 Bootloader 系统。