跳转至

双区升级与A/B分区策略

概述

双区升级(Dual Bank Upgrade)是一种高可靠性的固件更新策略,通过将Flash存储器划分为两个独立的分区(Bank A和Bank B),实现固件的原子升级和快速回滚。这种方案广泛应用于对可靠性要求极高的嵌入式系统,如工业控制、医疗设备、汽车电子等领域。

本文将深入探讨双区升级的核心技术,包括:

  • 双区架构的设计原理和优势
  • A/B分区的切换机制
  • 原子升级的实现方法
  • 快速回滚和故障恢复
  • 分区状态管理和验证
  • 实际项目中的应用案例

完成本文学习后,你将能够:

  • 理解双区升级的核心概念和工作原理
  • 掌握A/B分区的设计和实现方法
  • 实现可靠的固件原子升级
  • 设计快速回滚机制保证系统可用性
  • 处理升级过程中的各种异常情况
  • 评估双区升级方案的优缺点

前置知识

在开始本文学习之前,你需要:

  • 理解Bootloader的基本概念和工作原理
  • 熟悉Flash存储器的操作方法
  • 了解IAP在线升级的实现原理
  • 掌握固件版本管理的基本方法
  • 理解系统启动流程和分区管理

背景知识

传统升级方式的局限性

在传统的单分区IAP升级中,固件更新过程存在以下风险:

主要问题

  1. 升级中断风险:断电或通信中断导致固件损坏
  2. 无法回滚:升级失败后系统无法启动
  3. 停机时间长:需要完整下载和写入固件
  4. 验证滞后:只能在升级完成后才能验证
  5. 恢复困难:需要专用工具重新烧录固件

影响

  • 系统可用性降低
  • 维护成本增加
  • 用户体验差
  • 安全风险高

双区升级的优势

双区升级通过冗余设计解决了传统方案的问题:

核心优势

  1. 原子升级:升级要么完全成功,要么完全失败,不存在中间状态
  2. 快速回滚:升级失败可立即切换回旧版本
  3. 零停机:可以在运行时下载新固件
  4. 安全验证:新固件在独立分区中验证,不影响当前运行
  5. 故障隔离:升级失败不影响系统正常运行

应用场景

  • 工业控制系统(不允许长时间停机)
  • 医疗设备(安全性要求高)
  • 汽车电子(可靠性要求高)
  • 智能家居(用户体验要求高)
  • 网络设备(需要持续在线)

双区架构设计

双区架构将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. 高可靠性
  2. 升级失败不影响系统运行
  3. 可以快速回滚到旧版本
  4. 原子升级保证一致性

  5. 零停机升级

  6. 可以在运行时下载新固件
  7. 切换分区只需重启一次
  8. 最小化服务中断时间

  9. 安全性高

  10. 新固件在独立分区中验证
  11. 不会破坏当前运行的固件
  12. 支持多次验证和测试

  13. 易于维护

  14. 分区管理清晰
  15. 状态跟踪完整
  16. 便于故障诊断

缺点

  1. 存储空间需求大
  2. 需要两倍的固件存储空间
  3. 对小容量设备不友好
  4. 增加硬件成本

  5. 实现复杂度高

  6. 需要完善的状态管理
  7. 分区切换逻辑复杂
  8. 测试工作量大

  9. 升级时间较长

  10. 需要完整下载固件
  11. 不支持增量升级
  12. 网络传输时间长

  13. 版本管理复杂

  14. 需要维护两个分区的版本
  15. 回滚策略需要仔细设计
  16. 兼容性问题需要考虑

适用场景

推荐使用: - 工业控制系统(高可靠性要求) - 医疗设备(安全性要求高) - 汽车电子(不允许升级失败) - 网络设备(需要持续在线) - 智能家居(用户体验要求高)

不推荐使用: - 存储空间极其有限的设备 - 对成本敏感的消费类产品 - 升级频率很低的设备 - 简单的单片机应用

最佳实践

1. 分区大小规划

合理规划分区大小是双区升级成功的关键:

规划原则

  • 预留足够的增长空间(建议预留30-50%)
  • 考虑未来功能扩展的需求
  • 平衡存储空间和成本
  • 确保两个分区大小完全相同

示例规划(512KB Flash):

Bootloader:  32KB  (6%)
Bank A:      224KB (44%)
Bank B:      224KB (44%)
共享数据:    32KB  (6%)

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. 测试策略

全面的测试是确保双区升级可靠性的关键:

测试项目

  1. 正常升级测试
  2. 完整的升级流程
  3. 不同大小的固件
  4. 不同版本的固件

  5. 异常情况测试

  6. 升级过程中断电
  7. 通信中断
  8. 固件损坏
  9. CRC校验失败

  10. 回滚测试

  11. 自动回滚触发
  12. 手动回滚操作
  13. 多次回滚

  14. 压力测试

  15. 连续多次升级
  16. 快速切换分区
  17. 长时间运行测试

  18. 兼容性测试

  19. 不同版本间升级
  20. 跨主版本升级
  21. 降级限制测试

5. 安全考虑

增强双区升级的安全性:

安全措施

  1. 固件签名验证
  2. 集成数字签名验证
  3. 防止未授权固件安装
  4. 使用安全启动机制

  5. 加密传输

  6. 固件传输过程加密
  7. 防止中间人攻击
  8. 使用TLS/DTLS协议

  9. 防回滚保护

  10. 实现安全版本号机制
  11. 防止降级到有漏洞的版本
  12. 使用单调计数器

  13. 访问控制

  14. 升级操作需要认证
  15. 限制升级权限
  16. 记录升级操作日志

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;

    // 写入日志存储区
    // ...
}

扩展阅读

相关技术

  1. 差分升级
  2. 只传输固件的变化部分
  3. 减少升级时间和流量
  4. 需要差分算法支持

  5. 压缩固件

  6. 减少存储空间需求
  7. 加快传输速度
  8. 需要解压缩支持

  9. 增量升级

  10. 支持部分模块升级
  11. 提高升级灵活性
  12. 需要模块化设计

  13. 远程升级管理

  14. 云端固件管理
  15. 批量设备升级
  16. 升级进度监控

行业标准

  • AUTOSAR:汽车行业软件架构标准
  • IEC 62443:工业自动化安全标准
  • ISO 26262:汽车功能安全标准
  • DO-178C:航空软件开发标准

参考资料

  1. STM32 Flash编程手册
  2. ARM Cortex-M启动流程文档
  3. 嵌入式系统可靠性设计指南
  4. OTA升级最佳实践白皮书

总结

双区升级是一种高可靠性的固件更新方案,通过冗余设计实现了原子升级和快速回滚。虽然需要额外的存储空间和更复杂的实现,但在对可靠性要求高的应用场景中,这些代价是值得的。

关键要点

  1. 双区架构提供了固件冗余和快速切换能力
  2. 原子升级保证了系统的一致性
  3. 回滚机制确保了系统的可用性
  4. 完善的状态管理是实现可靠性的基础
  5. 安全性和兼容性需要特别关注

实施建议

  • 根据实际需求评估是否需要双区升级
  • 合理规划Flash分区布局
  • 实现完善的状态管理和验证机制
  • 进行充分的测试和验证
  • 建立监控和日志系统
  • 制定应急预案和恢复流程

通过本文的学习,你应该已经掌握了双区升级的核心原理和实现方法。在实际项目中,需要根据具体需求进行调整和优化,确保升级系统的可靠性和安全性。

练习与思考

  1. 设计练习:为一个256KB Flash的MCU设计双区升级方案,包括分区布局和状态管理。

  2. 实现练习:实现一个简化的双区升级系统,支持基本的升级和回滚功能。

  3. 思考问题

  4. 如何在双区升级中实现差分升级?
  5. 如何处理共享数据区的版本兼容性?
  6. 如何在三个或更多分区之间实现升级?
  7. 如何优化升级过程的性能?

  8. 扩展挑战

  9. 实现加密固件的双区升级
  10. 添加远程升级管理功能
  11. 实现升级进度的实时监控
  12. 设计多设备批量升级方案

下一步学习

完成本文学习后,建议继续学习:

  • OTA升级系统设计:了解完整的远程升级解决方案
  • 安全启动技术:增强固件的安全性
  • 固件加密与防护:保护固件知识产权
  • 启动时间优化:提高系统启动速度

文档版本:v1.0
最后更新:2024-01-15
作者:嵌入式知识平台
许可:CC BY-NC-SA 4.0