双区升级与A/B分区策略¶
概述¶
双区升级(Dual Bank Upgrade)是一种高可靠性的固件更新策略,通过将Flash存储器划分为两个独立的分区(Bank A和Bank B),实现固件的原子升级和快速回滚。这种方案广泛应用于对可靠性要求极高的嵌入式系统,如工业控制、医疗设备、汽车电子等领域。
本文将深入探讨双区升级的核心技术,包括:
- 双区架构的设计原理和优势
- A/B分区的切换机制
- 原子升级的实现方法
- 快速回滚和故障恢复
- 分区状态管理和验证
- 实际项目中的应用案例
完成本文学习后,你将能够:
- 理解双区升级的核心概念和工作原理
- 掌握A/B分区的设计和实现方法
- 实现可靠的固件原子升级
- 设计快速回滚机制保证系统可用性
- 处理升级过程中的各种异常情况
- 评估双区升级方案的优缺点
前置知识¶
在开始本文学习之前,你需要:
- 理解Bootloader的基本概念和工作原理
- 熟悉Flash存储器的操作方法
- 了解IAP在线升级的实现原理
- 掌握固件版本管理的基本方法
- 理解系统启动流程和分区管理
背景知识¶
传统升级方式的局限性¶
在传统的单分区IAP升级中,固件更新过程存在以下风险:
主要问题:
- 升级中断风险:断电或通信中断导致固件损坏
- 无法回滚:升级失败后系统无法启动
- 停机时间长:需要完整下载和写入固件
- 验证滞后:只能在升级完成后才能验证
- 恢复困难:需要专用工具重新烧录固件
影响:
- 系统可用性降低
- 维护成本增加
- 用户体验差
- 安全风险高
双区升级的优势¶
双区升级通过冗余设计解决了传统方案的问题:
核心优势:
- 原子升级:升级要么完全成功,要么完全失败,不存在中间状态
- 快速回滚:升级失败可立即切换回旧版本
- 零停机:可以在运行时下载新固件
- 安全验证:新固件在独立分区中验证,不影响当前运行
- 故障隔离:升级失败不影响系统正常运行
应用场景:
- 工业控制系统(不允许长时间停机)
- 医疗设备(安全性要求高)
- 汽车电子(可靠性要求高)
- 智能家居(用户体验要求高)
- 网络设备(需要持续在线)
双区架构设计¶
双区架构将Flash存储器划分为两个对称的分区:
┌─────────────────────────────────────┐
│ Bootloader (32KB) │ 0x0800 0000
│ - 启动管理 │
│ - 分区切换 │
│ - 固件验证 │
├─────────────────────────────────────┤
│ Bank A (240KB) │ 0x0800 8000
│ - 固件分区A │
│ - 当前运行版本 │
│ │
├─────────────────────────────────────┤
│ Bank B (240KB) │ 0x0804 4000
│ - 固件分区B │
│ - 备份或新版本 │
│ │
├─────────────────────────────────────┤
│ 共享数据区 (16KB) │ 0x0808 0000
│ - 配置参数 │
│ - 用户数据 │
│ - 分区状态 │
└─────────────────────────────────────┘
设计原则:
- 两个分区大小相同,可以互换
- Bootloader独立于应用分区
- 共享数据区用于存储配置和状态
- 每个分区都可以独立运行
核心内容¶
1. 分区状态管理¶
双区升级的核心是管理两个分区的状态,确保系统始终知道哪个分区是活动的。
分区状态定义¶
// 分区状态枚举
typedef enum {
BANK_STATE_EMPTY = 0, // 空分区(未写入固件)
BANK_STATE_UPDATING, // 正在更新
BANK_STATE_READY, // 就绪(已验证,可启动)
BANK_STATE_ACTIVE, // 活动(当前运行)
BANK_STATE_INVALID, // 无效(验证失败)
BANK_STATE_ROLLBACK // 回滚(从此分区回滚)
} BankState_t;
// 分区信息结构
typedef struct {
uint32_t magic; // 魔数:0x424E4B49 ("BNKI")
BankState_t state; // 分区状态
uint32_t version_major; // 主版本号
uint32_t version_minor; // 次版本号
uint32_t version_build; // 编译号
uint32_t firmware_size; // 固件大小
uint32_t firmware_crc32; // 固件CRC32
uint32_t update_timestamp; // 更新时间戳
uint32_t boot_count; // 启动次数
uint32_t fail_count; // 失败次数
uint32_t reserved[6]; // 保留字段
} BankInfo_t;
// 分区配置
#define BANK_A_ADDRESS 0x08008000
#define BANK_B_ADDRESS 0x08044000
#define BANK_SIZE 0x3C000 // 240KB
#define BANK_INFO_ADDRESS 0x08080000 // 共享数据区
// 分区索引
typedef enum {
BANK_A = 0,
BANK_B = 1
} BankIndex_t;
分区信息读写¶
/**
* @brief 读取分区信息
* @param bank 分区索引
* @param info 输出的分区信息
* @return 0成功,-1失败
*/
int ReadBankInfo(BankIndex_t bank, BankInfo_t *info) {
uint32_t info_address = BANK_INFO_ADDRESS + bank * sizeof(BankInfo_t);
// 从Flash读取
memcpy(info, (void*)info_address, sizeof(BankInfo_t));
// 验证魔数
if (info->magic != 0x424E4B49) {
printf("Invalid bank info magic for Bank %c\r\n", 'A' + bank);
return -1;
}
return 0;
}
/**
* @brief 写入分区信息
* @param bank 分区索引
* @param info 分区信息
* @return 0成功,-1失败
*/
int WriteBankInfo(BankIndex_t bank, BankInfo_t *info) {
uint32_t info_address = BANK_INFO_ADDRESS + bank * sizeof(BankInfo_t);
// 设置魔数
info->magic = 0x424E4B49;
// 解锁Flash
FLASH_Unlock();
// 擦除信息区域(如果需要)
if (bank == BANK_A) {
FLASH_EraseSector(BANK_INFO_ADDRESS);
}
// 写入信息
uint32_t *data = (uint32_t*)info;
for (uint32_t i = 0; i < sizeof(BankInfo_t) / 4; i++) {
if (!FLASH_ProgramWord(info_address + i * 4, data[i])) {
FLASH_Lock();
return -1;
}
}
// 锁定Flash
FLASH_Lock();
printf("Bank %c info written\r\n", 'A' + bank);
return 0;
}
/**
* @brief 更新分区状态
* @param bank 分区索引
* @param new_state 新状态
* @return 0成功,-1失败
*/
int UpdateBankState(BankIndex_t bank, BankState_t new_state) {
BankInfo_t info;
// 读取当前信息
if (ReadBankInfo(bank, &info) != 0) {
// 如果读取失败,初始化新信息
memset(&info, 0, sizeof(BankInfo_t));
}
// 更新状态
info.state = new_state;
// 写回
return WriteBankInfo(bank, &info);
}
获取活动分区¶
/**
* @brief 获取当前活动分区
* @return BANK_A或BANK_B,-1表示无活动分区
*/
int GetActiveBank(void) {
BankInfo_t info_a, info_b;
// 读取两个分区的信息
int ret_a = ReadBankInfo(BANK_A, &info_a);
int ret_b = ReadBankInfo(BANK_B, &info_b);
// 检查Bank A
if (ret_a == 0 && info_a.state == BANK_STATE_ACTIVE) {
return BANK_A;
}
// 检查Bank B
if (ret_b == 0 && info_b.state == BANK_STATE_ACTIVE) {
return BANK_B;
}
// 如果都不是活动状态,选择版本较新的就绪分区
if (ret_a == 0 && info_a.state == BANK_STATE_READY) {
if (ret_b == 0 && info_b.state == BANK_STATE_READY) {
// 比较版本号
if (info_a.version_major > info_b.version_major ||
(info_a.version_major == info_b.version_major &&
info_a.version_minor > info_b.version_minor) ||
(info_a.version_major == info_b.version_major &&
info_a.version_minor == info_b.version_minor &&
info_a.version_build > info_b.version_build)) {
return BANK_A;
} else {
return BANK_B;
}
}
return BANK_A;
}
if (ret_b == 0 && info_b.state == BANK_STATE_READY) {
return BANK_B;
}
printf("No active bank found\r\n");
return -1;
}
/**
* @brief 获取非活动分区(用于升级)
* @return BANK_A或BANK_B,-1表示错误
*/
int GetInactiveBank(void) {
int active_bank = GetActiveBank();
if (active_bank == BANK_A) {
return BANK_B;
} else if (active_bank == BANK_B) {
return BANK_A;
}
// 如果没有活动分区,默认使用Bank A
return BANK_A;
}
2. 固件验证¶
在切换分区之前,必须验证固件的完整性和有效性。
/**
* @brief 验证分区固件
* @param bank 分区索引
* @return 0验证成功,-1验证失败
*/
int VerifyBankFirmware(BankIndex_t bank) {
BankInfo_t info;
// 读取分区信息
if (ReadBankInfo(bank, &info) != 0) {
printf("Failed to read bank info\r\n");
return -1;
}
// 检查固件大小
if (info.firmware_size == 0 || info.firmware_size > BANK_SIZE) {
printf("Invalid firmware size: %u\r\n", info.firmware_size);
return -1;
}
// 获取分区地址
uint32_t bank_address = (bank == BANK_A) ? BANK_A_ADDRESS : BANK_B_ADDRESS;
printf("Verifying Bank %c firmware...\r\n", 'A' + bank);
printf(" Address: 0x%08X\r\n", bank_address);
printf(" Size: %u bytes\r\n", info.firmware_size);
// 计算CRC32
uint32_t calculated_crc = CRC32_Calculate(
(uint32_t*)bank_address,
info.firmware_size / 4
);
printf(" Expected CRC32: 0x%08X\r\n", info.firmware_crc32);
printf(" Calculated CRC32: 0x%08X\r\n", calculated_crc);
// 验证CRC
if (calculated_crc != info.firmware_crc32) {
printf("CRC verification failed!\r\n");
return -1;
}
// 验证固件头部(检查栈指针是否有效)
uint32_t stack_pointer = *(__IO uint32_t*)bank_address;
if ((stack_pointer & 0x2FFE0000) != 0x20000000) {
printf("Invalid stack pointer: 0x%08X\r\n", stack_pointer);
return -1;
}
printf("Bank %c firmware verified successfully\r\n", 'A' + bank);
return 0;
}
3. 分区切换机制¶
分区切换是双区升级的核心操作,需要确保原子性和可靠性。
/**
* @brief 切换到指定分区
* @param target_bank 目标分区
* @return 0成功,-1失败
*/
int SwitchToBank(BankIndex_t target_bank) {
BankInfo_t info;
printf("Switching to Bank %c...\r\n", 'A' + target_bank);
// 验证目标分区
if (VerifyBankFirmware(target_bank) != 0) {
printf("Target bank verification failed\r\n");
return -1;
}
// 读取目标分区信息
if (ReadBankInfo(target_bank, &info) != 0) {
return -1;
}
// 获取当前活动分区
int current_bank = GetActiveBank();
// 如果有当前活动分区,将其状态改为READY
if (current_bank >= 0) {
UpdateBankState(current_bank, BANK_STATE_READY);
printf("Bank %c set to READY\r\n", 'A' + current_bank);
}
// 将目标分区状态改为ACTIVE
info.state = BANK_STATE_ACTIVE;
info.boot_count = 0;
info.fail_count = 0;
if (WriteBankInfo(target_bank, &info) != 0) {
printf("Failed to update target bank state\r\n");
return -1;
}
printf("Bank %c set to ACTIVE\r\n", 'A' + target_bank);
printf("Switch complete, system will reset\r\n");
return 0;
}
/**
* @brief 跳转到指定分区
* @param bank 分区索引
*/
void JumpToBank(BankIndex_t bank) {
uint32_t bank_address = (bank == BANK_A) ? BANK_A_ADDRESS : BANK_B_ADDRESS;
// 检查栈指针是否有效
if (((*(__IO uint32_t*)bank_address) & 0x2FFE0000) == 0x20000000) {
typedef void (*pFunction)(void);
uint32_t app_sp = *(__IO uint32_t*)bank_address;
uint32_t app_entry = *(__IO uint32_t*)(bank_address + 4);
pFunction app_reset_handler = (pFunction)app_entry;
printf("Jumping to Bank %c at 0x%08X\r\n", 'A' + bank, bank_address);
// 关闭所有中断
__disable_irq();
// 关闭SysTick
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
// 重新设置中断向量表
SCB->VTOR = bank_address;
// 设置主堆栈指针
__set_MSP(app_sp);
// 跳转到应用程序
app_reset_handler();
} else {
printf("Invalid application at Bank %c\r\n", 'A' + bank);
}
}
4. 升级流程实现¶
双区升级的完整流程包括下载、验证、切换三个阶段。
/**
* @brief 开始双区升级
* @param firmware_size 固件大小
* @param firmware_crc32 固件CRC32
* @param version_major 主版本号
* @param version_minor 次版本号
* @param version_build 编译号
* @return 0成功,-1失败
*/
int DualBank_StartUpgrade(uint32_t firmware_size, uint32_t firmware_crc32,
uint32_t version_major, uint32_t version_minor,
uint32_t version_build) {
// 获取非活动分区
int target_bank = GetInactiveBank();
if (target_bank < 0) {
printf("Failed to get inactive bank\r\n");
return -1;
}
printf("Starting upgrade to Bank %c\r\n", 'A' + target_bank);
printf("Firmware size: %u bytes\r\n", firmware_size);
printf("Version: %u.%u.%u\r\n", version_major, version_minor, version_build);
// 检查固件大小
if (firmware_size == 0 || firmware_size > BANK_SIZE) {
printf("Invalid firmware size\r\n");
return -1;
}
// 准备分区信息
BankInfo_t info;
memset(&info, 0, sizeof(BankInfo_t));
info.state = BANK_STATE_UPDATING;
info.version_major = version_major;
info.version_minor = version_minor;
info.version_build = version_build;
info.firmware_size = firmware_size;
info.firmware_crc32 = firmware_crc32;
info.update_timestamp = HAL_GetTick();
// 写入分区信息
if (WriteBankInfo(target_bank, &info) != 0) {
printf("Failed to write bank info\r\n");
return -1;
}
// 擦除目标分区
uint32_t bank_address = (target_bank == BANK_A) ? BANK_A_ADDRESS : BANK_B_ADDRESS;
printf("Erasing Bank %c...\r\n", 'A' + target_bank);
FLASH_Unlock();
uint32_t address = bank_address;
uint32_t end_address = bank_address + BANK_SIZE;
while (address < end_address) {
if (!FLASH_EraseSector(address)) {
FLASH_Lock();
printf("Flash erase failed\r\n");
return -1;
}
// 移动到下一个扇区
uint8_t sector = FLASH_GetSector(address);
if (sector < 4) {
address += 0x4000; // 16KB
} else if (sector == 4) {
address += 0x10000; // 64KB
} else {
address += 0x20000; // 128KB
}
}
FLASH_Lock();
printf("Bank %c erased, ready to receive firmware\r\n", 'A' + target_bank);
return 0;
}
/**
* @brief 写入固件数据
* @param offset 偏移量
* @param data 数据
* @param length 数据长度
* @return 0成功,-1失败
*/
int DualBank_WriteFirmware(uint32_t offset, uint8_t *data, uint32_t length) {
// 获取正在更新的分区
int target_bank = -1;
BankInfo_t info_a, info_b;
if (ReadBankInfo(BANK_A, &info_a) == 0 && info_a.state == BANK_STATE_UPDATING) {
target_bank = BANK_A;
} else if (ReadBankInfo(BANK_B, &info_b) == 0 && info_b.state == BANK_STATE_UPDATING) {
target_bank = BANK_B;
}
if (target_bank < 0) {
printf("No bank in updating state\r\n");
return -1;
}
// 计算写入地址
uint32_t bank_address = (target_bank == BANK_A) ? BANK_A_ADDRESS : BANK_B_ADDRESS;
uint32_t write_address = bank_address + offset;
// 写入数据
FLASH_Unlock();
uint32_t *data_ptr = (uint32_t*)data;
uint32_t word_count = (length + 3) / 4;
for (uint32_t i = 0; i < word_count; i++) {
if (!FLASH_ProgramWord(write_address + i * 4, data_ptr[i])) {
FLASH_Lock();
printf("Flash write failed at offset %u\r\n", offset + i * 4);
return -1;
}
}
FLASH_Lock();
return 0;
}
/**
* @brief 完成升级
* @return 0成功,-1失败
*/
int DualBank_FinishUpgrade(void) {
// 获取正在更新的分区
int target_bank = -1;
BankInfo_t info;
if (ReadBankInfo(BANK_A, &info) == 0 && info.state == BANK_STATE_UPDATING) {
target_bank = BANK_A;
} else if (ReadBankInfo(BANK_B, &info) == 0 && info.state == BANK_STATE_UPDATING) {
target_bank = BANK_B;
}
if (target_bank < 0) {
printf("No bank in updating state\r\n");
return -1;
}
printf("Finishing upgrade for Bank %c\r\n", 'A' + target_bank);
// 验证固件
if (VerifyBankFirmware(target_bank) != 0) {
printf("Firmware verification failed\r\n");
UpdateBankState(target_bank, BANK_STATE_INVALID);
return -1;
}
// 更新状态为READY
info.state = BANK_STATE_READY;
if (WriteBankInfo(target_bank, &info) != 0) {
printf("Failed to update bank state\r\n");
return -1;
}
printf("Upgrade complete, Bank %c is ready\r\n", 'A' + target_bank);
return 0;
}
5. 回滚机制¶
当新固件启动失败时,系统需要能够快速回滚到旧版本。
// 最大启动失败次数
#define MAX_BOOT_FAILURES 3
/**
* @brief 记录启动尝试
* @param bank 分区索引
* @return 0成功,-1失败
*/
int RecordBootAttempt(BankIndex_t bank) {
BankInfo_t info;
if (ReadBankInfo(bank, &info) != 0) {
return -1;
}
info.boot_count++;
printf("Bank %c boot attempt: %u\r\n", 'A' + bank, info.boot_count);
return WriteBankInfo(bank, &info);
}
/**
* @brief 记录启动失败
* @param bank 分区索引
* @return 0成功,-1失败
*/
int RecordBootFailure(BankIndex_t bank) {
BankInfo_t info;
if (ReadBankInfo(bank, &info) != 0) {
return -1;
}
info.fail_count++;
printf("Bank %c boot failure: %u\r\n", 'A' + bank, info.fail_count);
// 检查是否超过最大失败次数
if (info.fail_count >= MAX_BOOT_FAILURES) {
printf("Bank %c exceeded max failures, marking as invalid\r\n", 'A' + bank);
info.state = BANK_STATE_INVALID;
}
return WriteBankInfo(bank, &info);
}
/**
* @brief 执行回滚
* @return 0成功,-1失败
*/
int PerformRollback(void) {
int active_bank = GetActiveBank();
if (active_bank < 0) {
printf("No active bank to rollback from\r\n");
return -1;
}
printf("Performing rollback from Bank %c\r\n", 'A' + active_bank);
// 标记当前分区为回滚状态
UpdateBankState(active_bank, BANK_STATE_ROLLBACK);
// 获取另一个分区
int rollback_bank = (active_bank == BANK_A) ? BANK_B : BANK_A;
// 验证回滚目标
if (VerifyBankFirmware(rollback_bank) != 0) {
printf("Rollback target Bank %c is invalid\r\n", 'A' + rollback_bank);
return -1;
}
// 切换到回滚目标
return SwitchToBank(rollback_bank);
}
/**
* @brief 检查是否需要回滚
* @return 1需要回滚,0不需要
*/
int CheckRollbackNeeded(void) {
int active_bank = GetActiveBank();
if (active_bank < 0) {
return 0;
}
BankInfo_t info;
if (ReadBankInfo(active_bank, &info) != 0) {
return 0;
}
// 检查失败次数
if (info.fail_count >= MAX_BOOT_FAILURES) {
printf("Bank %c has too many failures, rollback needed\r\n", 'A' + active_bank);
return 1;
}
return 0;
}
6. Bootloader主流程¶
将所有功能整合到Bootloader主程序中:
int main(void) {
// 系统初始化
SystemInit();
HAL_Init();
SystemClock_Config();
// 初始化外设
UART_Init();
LED_Init();
printf("\r\n");
printf("========================================\r\n");
printf(" Dual Bank Bootloader v1.0\r\n");
printf(" Build: %s %s\r\n", __DATE__, __TIME__);
printf("========================================\r\n");
// LED快速闪烁表示Bootloader运行
for (int i = 0; i < 3; i++) {
LED_On();
HAL_Delay(100);
LED_Off();
HAL_Delay(100);
}
// 检查是否需要回滚
if (CheckRollbackNeeded()) {
printf("Rollback needed, switching banks...\r\n");
if (PerformRollback() == 0) {
HAL_Delay(1000);
NVIC_SystemReset();
} else {
printf("Rollback failed, entering recovery mode\r\n");
// 进入恢复模式
while (1) {
LED_Toggle();
HAL_Delay(200);
}
}
}
// 获取活动分区
int active_bank = GetActiveBank();
if (active_bank < 0) {
printf("No active bank found\r\n");
printf("Waiting for firmware upload...\r\n");
// 进入IAP升级模式
while (1) {
LED_Toggle();
HAL_Delay(500);
// 处理IAP命令
}
}
printf("Active bank: %c\r\n", 'A' + active_bank);
// 记录启动尝试
RecordBootAttempt(active_bank);
// 验证活动分区
if (VerifyBankFirmware(active_bank) != 0) {
printf("Active bank verification failed\r\n");
RecordBootFailure(active_bank);
// 尝试回滚
if (PerformRollback() == 0) {
HAL_Delay(1000);
NVIC_SystemReset();
}
// 回滚失败,进入恢复模式
printf("Entering recovery mode\r\n");
while (1) {
LED_Toggle();
HAL_Delay(200);
}
}
// 显示分区信息
BankInfo_t info;
ReadBankInfo(active_bank, &info);
printf("Firmware version: %u.%u.%u\r\n",
info.version_major, info.version_minor, info.version_build);
printf("Boot count: %u\r\n", info.boot_count);
printf("Fail count: %u\r\n", info.fail_count);
// 延时后跳转
printf("Jumping to application in 2 seconds...\r\n");
LED_On();
HAL_Delay(2000);
LED_Off();
// 跳转到活动分区
JumpToBank(active_bank);
// 永远不会执行到这里
while (1);
}
实践示例¶
示例1:完整的升级流程¶
演示从开始到完成的完整双区升级过程:
/**
* @brief 双区升级示例
*/
void DualBank_UpgradeExample(void) {
// 假设通过串口接收到升级命令
uint32_t firmware_size = 0x30000; // 192KB
uint32_t firmware_crc32 = 0x12345678;
uint32_t version_major = 2;
uint32_t version_minor = 0;
uint32_t version_build = 100;
// 1. 开始升级
printf("Step 1: Starting upgrade...\r\n");
if (DualBank_StartUpgrade(firmware_size, firmware_crc32,
version_major, version_minor, version_build) != 0) {
printf("Failed to start upgrade\r\n");
return;
}
// 2. 接收并写入固件数据(分块传输)
printf("Step 2: Receiving firmware data...\r\n");
uint32_t offset = 0;
uint8_t buffer[1024];
while (offset < firmware_size) {
// 接收数据块(实际应从串口或网络接收)
uint32_t chunk_size = (firmware_size - offset > 1024) ? 1024 : (firmware_size - offset);
// 模拟接收数据
// UART_Receive(buffer, chunk_size);
// 写入Flash
if (DualBank_WriteFirmware(offset, buffer, chunk_size) != 0) {
printf("Failed to write firmware at offset %u\r\n", offset);
return;
}
offset += chunk_size;
// 显示进度
uint8_t progress = (offset * 100) / firmware_size;
printf("Progress: %u%%\r", progress);
}
printf("\n");
// 3. 完成升级
printf("Step 3: Finishing upgrade...\r\n");
if (DualBank_FinishUpgrade() != 0) {
printf("Failed to finish upgrade\r\n");
return;
}
// 4. 切换到新分区
printf("Step 4: Switching to new firmware...\r\n");
int new_bank = GetInactiveBank();
if (SwitchToBank(new_bank) != 0) {
printf("Failed to switch bank\r\n");
return;
}
// 5. 重启系统
printf("Step 5: Rebooting system...\r\n");
HAL_Delay(1000);
NVIC_SystemReset();
}
示例2:应用程序中的健康检查¶
应用程序需要定期报告健康状态,防止被误判为启动失败:
// 应用程序中的代码
#include "stm32f4xx.h"
#define BANK_INFO_ADDRESS 0x08080000
/**
* @brief 清除启动失败计数
* @note 应用程序启动成功后调用
*/
void App_ClearBootFailures(void) {
// 获取当前运行的分区
// 这里简化处理,实际应该从Bootloader传递参数
BankIndex_t current_bank = BANK_A; // 或从启动参数获取
BankInfo_t info;
uint32_t info_address = BANK_INFO_ADDRESS + current_bank * sizeof(BankInfo_t);
// 读取分区信息
memcpy(&info, (void*)info_address, sizeof(BankInfo_t));
// 清除失败计数
if (info.fail_count > 0) {
info.fail_count = 0;
// 写回Flash
FLASH_Unlock();
FLASH_EraseSector(BANK_INFO_ADDRESS);
uint32_t *data = (uint32_t*)&info;
for (uint32_t i = 0; i < sizeof(BankInfo_t) / 4; i++) {
FLASH_ProgramWord(info_address + i * 4, data[i]);
}
FLASH_Lock();
printf("Boot failure count cleared\r\n");
}
}
/**
* @brief 应用程序主函数
*/
int main(void) {
// 系统初始化
SystemInit();
HAL_Init();
SystemClock_Config();
// 初始化外设
UART_Init();
LED_Init();
printf("\r\n");
printf("========================================\r\n");
printf(" Application v2.0.100\r\n");
printf("========================================\r\n");
// 清除启动失败计数(表示应用程序正常启动)
App_ClearBootFailures();
// 应用程序主循环
while (1) {
// 正常业务逻辑
LED_Toggle();
HAL_Delay(1000);
// 定期报告健康状态
// ...
}
}
示例3:升级工具(Python)¶
PC端工具用于触发双区升级:
#!/usr/bin/env python3
"""
双区升级工具
通过串口触发设备的双区升级
"""
import serial
import struct
import time
import sys
# 命令定义
CMD_DUAL_BANK_START = 0x10
CMD_DUAL_BANK_DATA = 0x11
CMD_DUAL_BANK_FINISH = 0x12
CMD_DUAL_BANK_SWITCH = 0x13
class DualBankUploader:
def __init__(self, port, baudrate=115200):
self.ser = serial.Serial(port, baudrate, timeout=2)
def send_command(self, cmd, data=b''):
"""发送命令"""
packet = struct.pack('<BB', 0xAA, cmd)
packet += struct.pack('<H', len(data))
packet += data
packet += struct.pack('B', 0x55)
self.ser.write(packet)
# 等待响应
response = self.ser.read(8)
if len(response) < 8:
return None
return response[4] # 返回状态码
def upload_firmware(self, firmware_path):
"""上传固件"""
# 读取固件
with open(firmware_path, 'rb') as f:
firmware = f.read()
print(f"Firmware size: {len(firmware)} bytes")
# 计算CRC32
import zlib
crc32 = zlib.crc32(firmware) & 0xFFFFFFFF
print(f"Firmware CRC32: 0x{crc32:08X}")
# 发送开始命令
print("Sending start command...")
start_data = struct.pack('<IIIII', len(firmware), crc32, 2, 0, 100)
status = self.send_command(CMD_DUAL_BANK_START, start_data)
if status != 0:
print("Start command failed!")
return False
print("Start command OK")
# 分块发送固件
chunk_size = 1024
total_chunks = (len(firmware) + chunk_size - 1) // chunk_size
for i in range(total_chunks):
start = i * chunk_size
end = min(start + chunk_size, len(firmware))
chunk = firmware[start:end]
# 发送数据块
data_packet = struct.pack('<I', start) + chunk
status = self.send_command(CMD_DUAL_BANK_DATA, data_packet)
if status != 0:
print(f"Data send failed at chunk {i}")
return False
progress = (i + 1) * 100 // total_chunks
print(f"Progress: {progress}%", end='\r')
print("\nAll data sent")
# 发送完成命令
print("Sending finish command...")
status = self.send_command(CMD_DUAL_BANK_FINISH)
if status != 0:
print("Finish command failed!")
return False
print("Firmware uploaded successfully!")
# 发送切换命令
print("Sending switch command...")
status = self.send_command(CMD_DUAL_BANK_SWITCH)
if status != 0:
print("Switch command failed!")
return False
print("Bank switch initiated, device will reboot")
return True
# 使用示例
if __name__ == '__main__':
if len(sys.argv) != 3:
print("Usage: python dual_bank_uploader.py <COM_PORT> <FIRMWARE_FILE>")
sys.exit(1)
port = sys.argv[1]
firmware_file = sys.argv[2]
uploader = DualBankUploader(port)
if uploader.upload_firmware(firmware_file):
print("Upgrade successful!")
else:
print("Upgrade failed!")
优缺点分析¶
优点¶
- 高可靠性
- 升级失败不影响系统运行
- 可以快速回滚到旧版本
-
原子升级保证一致性
-
零停机升级
- 可以在运行时下载新固件
- 切换分区只需重启一次
-
最小化服务中断时间
-
安全性高
- 新固件在独立分区中验证
- 不会破坏当前运行的固件
-
支持多次验证和测试
-
易于维护
- 分区管理清晰
- 状态跟踪完整
- 便于故障诊断
缺点¶
- 存储空间需求大
- 需要两倍的固件存储空间
- 对小容量设备不友好
-
增加硬件成本
-
实现复杂度高
- 需要完善的状态管理
- 分区切换逻辑复杂
-
测试工作量大
-
升级时间较长
- 需要完整下载固件
- 不支持增量升级
-
网络传输时间长
-
版本管理复杂
- 需要维护两个分区的版本
- 回滚策略需要仔细设计
- 兼容性问题需要考虑
适用场景¶
推荐使用: - 工业控制系统(高可靠性要求) - 医疗设备(安全性要求高) - 汽车电子(不允许升级失败) - 网络设备(需要持续在线) - 智能家居(用户体验要求高)
不推荐使用: - 存储空间极其有限的设备 - 对成本敏感的消费类产品 - 升级频率很低的设备 - 简单的单片机应用
最佳实践¶
1. 分区大小规划¶
合理规划分区大小是双区升级成功的关键:
规划原则:
- 预留足够的增长空间(建议预留30-50%)
- 考虑未来功能扩展的需求
- 平衡存储空间和成本
- 确保两个分区大小完全相同
示例规划(512KB Flash):
2. 状态持久化¶
确保分区状态信息的可靠存储:
建议方案:
- 使用专用的Flash扇区存储状态
- 实现状态信息的冗余备份
- 使用CRC或ECC保护状态数据
- 定期验证状态信息的完整性
状态备份示例:
// 状态信息备份(存储两份)
#define BANK_INFO_PRIMARY 0x08080000
#define BANK_INFO_BACKUP 0x08080800
int WriteBankInfoWithBackup(BankIndex_t bank, BankInfo_t *info) {
// 写入主副本
if (WriteBankInfoTo(BANK_INFO_PRIMARY + bank * sizeof(BankInfo_t), info) != 0) {
return -1;
}
// 写入备份副本
if (WriteBankInfoTo(BANK_INFO_BACKUP + bank * sizeof(BankInfo_t), info) != 0) {
return -1;
}
return 0;
}
3. 版本兼容性¶
处理不同版本之间的兼容性问题:
兼容性策略:
- 定义清晰的版本号规则(语义化版本)
- 维护版本兼容性矩阵
- 在升级前检查版本兼容性
- 提供版本降级的限制机制
版本检查示例:
int CheckVersionCompatibility(uint32_t current_major, uint32_t current_minor,
uint32_t new_major, uint32_t new_minor) {
// 主版本号不同,不兼容
if (new_major != current_major) {
printf("Major version mismatch: %u -> %u\r\n", current_major, new_major);
return -1;
}
// 次版本号只能升级,不能降级
if (new_minor < current_minor) {
printf("Cannot downgrade minor version: %u -> %u\r\n",
current_minor, new_minor);
return -1;
}
return 0;
}
4. 测试策略¶
全面的测试是确保双区升级可靠性的关键:
测试项目:
- 正常升级测试
- 完整的升级流程
- 不同大小的固件
-
不同版本的固件
-
异常情况测试
- 升级过程中断电
- 通信中断
- 固件损坏
-
CRC校验失败
-
回滚测试
- 自动回滚触发
- 手动回滚操作
-
多次回滚
-
压力测试
- 连续多次升级
- 快速切换分区
-
长时间运行测试
-
兼容性测试
- 不同版本间升级
- 跨主版本升级
- 降级限制测试
5. 安全考虑¶
增强双区升级的安全性:
安全措施:
- 固件签名验证
- 集成数字签名验证
- 防止未授权固件安装
-
使用安全启动机制
-
加密传输
- 固件传输过程加密
- 防止中间人攻击
-
使用TLS/DTLS协议
-
防回滚保护
- 实现安全版本号机制
- 防止降级到有漏洞的版本
-
使用单调计数器
-
访问控制
- 升级操作需要认证
- 限制升级权限
- 记录升级操作日志
6. 监控和日志¶
实现完善的监控和日志系统:
监控指标:
- 升级成功率
- 升级耗时
- 回滚次数
- 失败原因统计
日志记录:
typedef struct {
uint32_t timestamp;
uint8_t event_type;
uint8_t bank;
uint32_t version;
uint8_t result;
} UpgradeLog_t;
#define LOG_EVENT_START 0x01
#define LOG_EVENT_FINISH 0x02
#define LOG_EVENT_SWITCH 0x03
#define LOG_EVENT_ROLLBACK 0x04
void LogUpgradeEvent(uint8_t event_type, BankIndex_t bank,
uint32_t version, uint8_t result) {
UpgradeLog_t log;
log.timestamp = HAL_GetTick();
log.event_type = event_type;
log.bank = bank;
log.version = version;
log.result = result;
// 写入日志存储区
// ...
}
扩展阅读¶
相关技术¶
- 差分升级
- 只传输固件的变化部分
- 减少升级时间和流量
-
需要差分算法支持
-
压缩固件
- 减少存储空间需求
- 加快传输速度
-
需要解压缩支持
-
增量升级
- 支持部分模块升级
- 提高升级灵活性
-
需要模块化设计
-
远程升级管理
- 云端固件管理
- 批量设备升级
- 升级进度监控
行业标准¶
- AUTOSAR:汽车行业软件架构标准
- IEC 62443:工业自动化安全标准
- ISO 26262:汽车功能安全标准
- DO-178C:航空软件开发标准
参考资料¶
- STM32 Flash编程手册
- ARM Cortex-M启动流程文档
- 嵌入式系统可靠性设计指南
- OTA升级最佳实践白皮书
总结¶
双区升级是一种高可靠性的固件更新方案,通过冗余设计实现了原子升级和快速回滚。虽然需要额外的存储空间和更复杂的实现,但在对可靠性要求高的应用场景中,这些代价是值得的。
关键要点:
- 双区架构提供了固件冗余和快速切换能力
- 原子升级保证了系统的一致性
- 回滚机制确保了系统的可用性
- 完善的状态管理是实现可靠性的基础
- 安全性和兼容性需要特别关注
实施建议:
- 根据实际需求评估是否需要双区升级
- 合理规划Flash分区布局
- 实现完善的状态管理和验证机制
- 进行充分的测试和验证
- 建立监控和日志系统
- 制定应急预案和恢复流程
通过本文的学习,你应该已经掌握了双区升级的核心原理和实现方法。在实际项目中,需要根据具体需求进行调整和优化,确保升级系统的可靠性和安全性。
练习与思考¶
-
设计练习:为一个256KB Flash的MCU设计双区升级方案,包括分区布局和状态管理。
-
实现练习:实现一个简化的双区升级系统,支持基本的升级和回滚功能。
-
思考问题:
- 如何在双区升级中实现差分升级?
- 如何处理共享数据区的版本兼容性?
- 如何在三个或更多分区之间实现升级?
-
如何优化升级过程的性能?
-
扩展挑战:
- 实现加密固件的双区升级
- 添加远程升级管理功能
- 实现升级进度的实时监控
- 设计多设备批量升级方案
下一步学习¶
完成本文学习后,建议继续学习:
- OTA升级系统设计:了解完整的远程升级解决方案
- 安全启动技术:增强固件的安全性
- 固件加密与防护:保护固件知识产权
- 启动时间优化:提高系统启动速度
文档版本:v1.0
最后更新:2024-01-15
作者:嵌入式知识平台
许可:CC BY-NC-SA 4.0