Flash 分区布局

STM32F103 拥有 512 KB 片内 Flash(0x08000000 ~ 0x0807FFFF),页大小 2 KB。我们将整个 Flash 划分为以下区域:

区域起始地址大小用途
Bootloader0x0800000032 KB引导加载程序
Slot A0x08008000236 KB应用固件槽位 A
Slot B0x08043000236 KB应用固件槽位 B
Boot Control 页0x0807E0002 KB启动控制元数据
State 页0x0807E8002 KB升级状态镜像
Diagnostics 页0x0807F0002 KBG780s 诊断数据
Config 页0x0807F8002 KBG780s 远程配置

这些地址定义在 BSP/Upgrade.h 中:

#define UPGRADE_BOOT_BASE_ADDR         0x08000000UL
#define UPGRADE_BOOT_MAX_SIZE          0x00008000UL   // 32 KB

#define UPGRADE_SLOT_A_BASE_ADDR       0x08008000UL
#define UPGRADE_SLOT_B_BASE_ADDR       0x08043000UL
#define UPGRADE_SLOT_MAX_SIZE          0x0003B000UL   // 236 KB

#define UPGRADE_BOOTCTRL_PAGE_ADDR     0x0807E000UL
#define UPGRADE_STATE_PAGE_ADDR        0x0807E800UL

Bootloader 固定在 Flash 起始位置,不会被升级覆盖。两个应用槽位大小相同,升级时写入”非当前运行”的那个槽位,实现无缝切换。

升级状态页与 BootControl 页

UpgradeStateImage — 升级状态镜像

持久化在 UPGRADE_STATE_PAGE_ADDR(0x0807E800),记录当前升级过程的完整状态:

typedef struct {
    uint32_t magic;               // 0x55504753 ("UPGS")
    uint16_t version;             // 结构体版本,当前 v2
    uint16_t state;               // IDLE/REQUESTED/ERASING/PROGRAMMING/VERIFYING/DONE/FAILED
    uint16_t request_source;      // NONE/LOCAL/G780S/REMOTE/MQTT
    uint32_t target_fw_version;
    uint32_t image_size;
    uint32_t image_crc32;
    uint8_t  image_sha256[32];    // v2 新增
    uint32_t written_bytes;       // 写入进度
    uint32_t last_ok_offset;      // 断点恢复位置
    uint16_t error_code;
    uint16_t active_boot_count;   // 卡在"升级活跃"状态的启动次数
    uint16_t crc16;               // Modbus CRC16 校验
} UpgradeStateImage;

状态机流转:IDLE → REQUESTED → ERASING → PROGRAMMING → VERIFYING → DONE(或任意阶段 → FAILED)。

UpgradeBootControl — 启动控制

持久化在 UPGRADE_BOOTCTRL_PAGE_ADDR(0x0807E000),管理 A/B 槽位的启动决策:

typedef struct {
    uint32_t magic;               // 0x55424743 ("UBGC")
    uint16_t active_slot;         // 当前运行槽位
    uint16_t confirmed_slot;      // 已确认稳定的槽位
    uint16_t pending_slot;        // 新写入、等待试运行的槽位
    uint16_t boot_attempts;       // 试运行启动次数
    uint16_t watchdog_reset_count;// 连续 IWDG 复位次数
    UpgradeSlotRecord slots[2];   // A/B 槽位元数据(版本、大小、CRC32、SHA-256)
    uint16_t crc16;
} UpgradeBootControl;

两个页面均使用 CRC16(Modbus 多项式 0xA001)做完整性校验,并支持 v1 → v2 的向前兼容迁移。

YMODEM 接收与写入流程

Bootloader 通过 USART3(RS-485,115200 波特率)接收固件镜像,使用 YMODEM-C 协议。完整流程:

1. 会话初始化

Bootloader 配置 YMODEM 回调结构体:

const YmodemReceiveConfig config = {
    .read_byte  = BootProtocol_YmodemReadByte,    // 逐字节读取
    .write_bytes = BootProtocol_YmodemWriteBytes,  // RS-485 方向切换 + 发送
    .on_start   = BootProtocol_YmodemStart,        // 头包回调 → BootFlash_BeginImage
    .on_data    = BootProtocol_YmodemData,          // 数据包回调 → BootFlash_WriteImageData
};

2. 头包解析

YMODEM 的 packet#0 包含文件名和元数据字符串:

"<size> 0x<crc32> <target_fw_version> <sha256_hex>"

解析器从中提取文件大小、CRC32、目标固件版本和 SHA-256 摘要(64 位十六进制解码为 32 字节)。

3. 擦除与编程

BootFlash_BeginImage() 选择下载槽位(始终写入”另一个”槽位),然后按需擦除 2KB 页。数据包到达后,BootFlash_WriteImageData() 以半字(16-bit)为单位编程,每 2KB 或最后一个包时将状态持久化到 Flash,支持断点恢复。

4. 传输协议

YMODEM-C 支持 SOH(128 字节)和 STX(1024 字节)两种包格式,每包 CRC-16 校验,错误重传最多 5 次。

三重校验机制

固件写入完成后,Bootloader 执行三重验证:

static uint16_t BootVerify_ValidateDigestsAndVector(...)
{
    // 1. 大小校验
    if (image_size == 0u || image_size > UPGRADE_SLOT_MAX_SIZE)
        return BOOT_ERR_BAD_SIZE;

    // 2. CRC32 校验
    if (calc_crc32 != image_crc32)
        return BOOT_ERR_VERIFY_CRC32;

    // 3. SHA-256 校验(向后兼容:全零则跳过)
    if (BootVerify_HasDigest(expected_digest) != 0u) {
        if (memcmp(digest, expected_digest, 32) != 0)
            return BOOT_ERR_VERIFY_SHA256;
    }

    // 4. 向量表合法性校验
    if (Upgrade_IsSlotVectorValid(slot) == 0u)
        return BOOT_ERR_VECTOR_INVALID;

    return BOOT_ERR_NONE;
}

向量表校验检查 STM32 启动向量的合法性:

uint8_t Upgrade_IsAppVectorValid(uint32_t app_base_addr)
{
    uint32_t app_stack = *(__IO uint32_t *)app_base_addr;       // MSP
    uint32_t app_reset = *(__IO uint32_t *)(app_base_addr + 4); // Reset Handler

    // 栈指针必须在 SRAM 范围内
    if ((app_stack & 0x2FFE0000u) != 0x20000000u) return 0u;
    // 复位向量必须指向 App 区域内
    if (app_reset < app_base_addr ||
        app_reset >= (app_base_addr + UPGRADE_APP_MAX_SIZE)) return 0u;
    return 1u;
}

CRC32 和 SHA-256 均直接在 Flash 上流式计算,无需将整个镜像拷贝到 RAM。SHA-256 使用纯软件实现,按 256 字节分块读取 Flash。

试运行与自动回退

升级写入完成后,Bootloader 设置 pending_slot 并复位。下次启动时进入试运行流程:

试运行逻辑

  1. 检测到 pending_slot != NONE,重新校验该槽位的 Flash 内容
  2. 校验通过且 boot_attempts < 3:设置 active_slot = pending_slot,递增计数,跳转运行
  3. boot_attempts >= 3 仍未确认:回退到已确认槽位(优先级:confirmed_slot > active_slot > Slot A > Slot B)

App 侧确认

应用启动后,满足以下两个条件才确认当前槽位稳定:

  • 运行时间 ≥ 60 秒(APP_UPGRADE_CONFIRM_DELAY_MS
  • 至少 3 次成功的传感器读取(APP_UPGRADE_CONFIRM_MIN_HEALTHY_SAMPLES

确认后清除 pending_slotboot_attempts,将当前槽位标记为 confirmed_slot

IWDG 复位回退

Bootloader 在每次启动时检查 RCC_FLAG_IWDGRST 标志:

if (g_boot_runtime.boot_control.watchdog_reset_count < UPGRADE_WDG_RESET_LIMIT)
    return 0u;  // 还没到阈值,允许继续启动

如果连续 3 次 IWDG 复位:

  1. 识别”嫌疑槽位”(pending_slot > active_slot > confirmed_slot)
  2. 找到稳定的回退槽位(不能是嫌疑槽位)
  3. 执行回退:重置 active_slot/confirmed_slot,清除 pending 计数,标记升级为 FAILED

超时回退

如果状态页在”升级活跃”状态(REQUESTED/ERASING/PROGRAMMING/VERIFYING)连续 3 次启动未推进,Bootloader 自动回退到稳定槽位,错误码 BOOT_ERR_TIMEOUT_RECOVERY

升级请求来源

系统支持三种升级发起路径,通过 request_source 字段区分:

1. 本地升级(LOCAL)

用户通过 RS-485 连接 PC,使用 YMODEM-C 协议发送固件。进入 Bootloader 的方式:

  • 硬件按键:PE4 在启动时保持低电平
  • UART 魔术字节:启动 2500ms 内发送 'B'(0x42)或 'b'(0x62)

2. G780s 远程升级(G780S / REMOTE)

两个子路径:

  • G780S 路径:Modbus 主站发送 G780S_CMD_ENTER_BOOT_UPGRADE(0x0005)命令,App 调用 Upgrade_RequestBootMode() 后复位,Bootloader 进入 YMODEM 等待
  • REMOTE 路径:云端发送 ota_prepare JSON 命令,App 进入 OTA 静默模式,复位后 Bootloader 等待 PC/工具通过串口发送固件

3. MQTT OTA(完全云端)

这是完全基于云端的固件更新路径,App 本身负责将固件写入 Flash,无需进入 Bootloader 接收数据:

  1. ota_begin:云端下发会话参数(session_key、image_size、crc32、sha256、target_slot),App 擦除目标槽位
  2. ota_chunk:逐块接收 base64 编码的固件数据,每块独立 CRC32 校验,顺序写入 Flash
  3. ota_commit:全部接收后,全量 CRC32 + SHA-256 重算,通过则设置 pending_slot,复位进入试运行

关键安全特性:如果 MQTT OTA 期间 App 崩溃或设备复位,Bootloader 检测到 request_source == MQTT 且状态仍为”升级活跃”,立即回退到稳定槽位,不会进入 YMODEM 等待循环

启动决策总览

Bootloader 的启动决策树:

加载 State 页 + BootControl 页

  ├─ 状态为"升级活跃"?
  │   ├─ MQTT OTA 中断 → 立即回退
  │   ├─ active_boot_count >= 3 → 超时回退
  │   └─ 否则 → 留在 Loader 等待 YMODEM 恢复

  ├─ watchdog_reset_count >= 3?
  │   └─ 是 → 回退到稳定槽位

  ├─ 硬件按键 / UART 魔术字节?
  │   └─ 是 → 留在 Loader

  ├─ pending_slot 有效?
  │   ├─ 校验通过 + boot_attempts < 3 → 试运行
  │   └─ 否则 → 回退

  └─ 默认 → 找到有效槽位,跳转运行