STM32双Bank Flash在线升级失败后利用BANK2自举恢复的实战方法
1. 为什么需要双Bank自举恢复?
在线升级(OTA)在嵌入式设备中越来越普遍,但升级失败(如断电、通信错误、固件校验失败)可能导致设备无法启动,即“变砖”。传统方案是使用外部看门狗或Bootloader+App双区,但STM32的双Bank Flash提供了更优雅的硬件级解决方案:将Flash分为两个独立的Bank,每个Bank可独立擦写和启动。当Bank1中的App升级失败时,系统可以自动从Bank2启动,运行一个安全的备份固件,从而恢复设备。
2. 双Bank Flash原理
STM32系列(如F4、L4、H7等)的部分型号支持双Bank Flash。以STM32F4为例,Flash容量为1MB时,分为Bank1(地址0x08000000-0x0807FFFF)和Bank2(地址0x08080000-0x080FFFFF)。每个Bank有独立的擦除/编程操作,且可以通过选项字节配置启动模式。
关键点:
- 启动引脚BOOT0/BOOT1:决定芯片从主Flash、系统存储器还是SRAM启动。但双Bank自举不是通过引脚,而是通过选项字节中的nBOOT1位和Flash控制寄存器。
-
自举机制:当设置选项字节
BOOT_CFG为双Bank模式,且检测到Bank1的启动地址无效(如全0xFF或校验失败),硬件会自动从Bank2启动。 -
软件触发:也可以通过软件设置
FLASH_CR的BOR_LEV或调用NVIC_SystemReset()配合选项字节实现。
3. 系统架构设计
- Bank1 (0x08000000):存放主应用程序(App1),支持在线升级。
- Bank2 (0x08080000):存放备份固件(App2),功能精简,仅包含基础通信和恢复逻辑(如恢复出厂设置、重新升级)。
- Bootloader:可放在Bank1起始区域或独立区域,但为了简化,我们假设Bootloader位于Bank1的0x08000000-0x0800FFFF,App1从0x08010000开始。
升级流程:
- 设备运行App1,接收新固件,写入Bank1的App区域。
- 写入完成后,校验CRC或SHA。
- 若校验失败或写入过程中断电,复位后硬件检测到Bank1的App无效,自动从Bank2启动App2。
- App2运行后,可通过通信接口提示用户升级失败,并等待重新升级。
4. 配置步骤
4.1 启用双Bank模式
在STM32CubeMX或代码中,通过修改选项字节启用双Bank。以STM32F4为例,使用HAL库:
void Enable_DualBank(void)
{
FLASH_OBProgramInitTypeDef pOBInit;
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
pOBInit.OptionType = OPTIONBYTE_BANK;
pOBInit.Banks = FLASH_BANK_1;
pOBInit.BankConfig = FLASH_BANK_1_2; // 双Bank模式
HAL_FLASH_OBProgram(&pOBInit);
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
// 需要重新上电或系统复位生效
NVIC_SystemReset();
}
4.2 设置启动地址和自举配置
在双Bank模式下,需要设置选项字节中的BOOT_CFG为0b10(从Bank2启动当Bank1无效)。实际上,当启用双Bank后,硬件自动根据Bank1的复位向量有效性决定启动Bank。但为了确保,我们可以在代码中检查:
uint32_t CheckBank1Valid(void)
{
// 检查Bank1的复位向量是否有效(栈顶地址在RAM范围,复位向量在Flash范围)
uint32_t stack_addr = *(volatile uint32_t*)0x08010000;
uint32_t reset_vector = *(volatile uint32_t*)0x08010004;
if ((stack_addr & 0xFFF00000) != 0x20000000) return 0; // 栈顶不在RAM
if ((reset_vector & 0xFFF00000) != 0x08000000) return 0; // 复位向量不在Flash
return 1;
}
4.3 编写Bootloader(可选)
如果Bootloader位于Bank1起始,它需要负责跳转到App1或App2。但双Bank自举是硬件行为,Bootloader可以简化。实际上,当Bank1无效时,硬件直接执行Bank2的复位向量,无需Bootloader干预。但为了灵活,我们可以在App2中实现恢复逻辑。
5. 完整代码示例
以下代码演示如何在App1中实现升级失败检测并触发自举恢复。
// main.c (App1)
#include "stm32f4xx_hal.h"
// 定义Bank1 App起始地址和大小
#define APP1_ADDR 0x08010000
#define APP1_SIZE 0x70000 // 448KB,留空间给Bootloader
// 定义升级标志(存储在备份寄存器或Flash末尾)
#define UPGRADE_FLAG_ADDR 0x0807FF00
void Check_Upgrade_Status(void)
{
uint32_t flag = *(volatile uint32_t*)UPGRADE_FLAG_ADDR;
if (flag == 0xDEADBEEF) // 升级进行中标志
{
// 检查App1是否有效
if (!CheckBank1Valid())
{
// 升级失败,清除标志,复位后硬件将从Bank2启动
*(volatile uint32_t*)UPGRADE_FLAG_ADDR = 0;
NVIC_SystemReset();
}
else
{
// 升级成功,清除标志
*(volatile uint32_t*)UPGRADE_FLAG_ADDR = 0;
}
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
Check_Upgrade_Status();
// 正常App1运行
while (1)
{
// 主循环
}
}
// 在升级过程中,写入新固件前设置标志
void Start_Upgrade(void)
{
// 擦除Bank1的App区域
FLASH_EraseInitTypeDef eraseInit;
uint32_t pageError = 0;
eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
eraseInit.Sector = Get_Sector_From_Addr(APP1_ADDR);
eraseInit.NbSectors = 8; // 根据实际大小
eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASH_Unlock();
HAL_FLASH_Erase(&eraseInit, &pageError);
// 设置升级标志
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, UPGRADE_FLAG_ADDR, 0xDEADBEEF);
HAL_FLASH_Lock();
// 接收新固件并写入...
}
App2(备份固件)的启动代码
App2位于Bank2,其向量表偏移需设置为0x08080000。在系统初始化时,需要重设向量表:
// App2的system_stm32f4xx.c或main中
#define VECT_TAB_OFFSET 0x08080000
在main函数中,设置向量表偏移:
void SystemInit(void)
{
// ...
SCB->VTOR = 0x08080000;
}
App2的功能:
- 初始化通信(如UART、USB或以太网)。
- 提示用户升级失败,等待重新升级。
- 提供恢复出厂设置或重新升级的接口。
6. 注意事项
- 选项字节修改:修改选项字节后必须复位才能生效,且操作前需解锁Flash和选项字节。
- 向量表偏移:App1和App2的向量表必须正确设置,否则中断异常。
- Flash大小:确保双Bank总容量足够,且每个Bank的App大小不超过其容量。
- 升级标志存储:使用备份寄存器(RTC域)或Flash末尾扇区,注意写入次数限制。
- 硬件自举条件:并非所有STM32型号都支持双Bank自举,需查阅参考手册确认。
- 测试:实际测试断电和写入错误场景,确保恢复机制可靠。
7. 总结
利用STM32双Bank Flash的自举恢复机制,可以显著提高OTA升级的可靠性。通过合理设计Bootloader、App1和App2,以及配置选项字节,即使升级失败,设备也能自动回退到安全固件,避免“变砖”。本文提供的代码和步骤可直接应用于实际项目,但需根据具体芯片型号调整Flash地址和大小。