Administrator
发布于 2026-09-25 / 0 阅读
0
0

现场升级不翻车:手写一个带 A/B 双分区与自动回滚的串口 IAP

一句话结论:IAP 只要做到三件事就不会变砖——A/B 双分区(新固件写到「另一边」)、整包 CRC 校验 + 试运行计数、元数据双备份。本文实测:升级中途断电、丢帧、新固件连续启动失败,每一次都能自动回到能跑的旧版本。

一、完整工程下载

压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。

下载 iap-boot.zip (20.2 KB,共 11 个文件)

.gitignore
Makefile
README.md
include/
  boot_crc.h
  boot_flash.h
  boot_iap.h
platformio.ini
src/
  boot_crc.c
  boot_flash.c
  boot_iap.c
test/
  test_iap.c

一、为什么必须有 A/B 双分区

最朴素的 IAP 是"收到新固件就直接覆盖 App 区"。它的致命缺陷是:

[Bootloader][        App       ]
                    ↑ 正在擦这里时断电 ⇒ 两个都跑不了 = 变砖

只要在擦除/写入过程中掉一次电,设备就报废了,只能拆机接 SWD。 野外设备(农机、水表、路灯控制器)根本没法返修,所以这条路不能走。

A/B 双分区的思路是「永远不动正在运行的那一份」:

0x00000  ┌──────────────┐
         │  Bootloader  │  16 KB   (永远不升级,只认协议)
0x04000  ├──────────────┤
         │   App Slot A │  48 KB
0x10000  ├──────────────┤
         │   App Slot B │  48 KB
0x1C000  ├──────────────┤
         │   Meta 区    │  16 KB   (记录"当前该跑哪个")
0x20000  └──────────────┘

新固件永远写到非活动的那一份。写坏了无所谓, 活动分区完好无损,重启还是跑旧版本。

二、Meta 区:记录"该跑谁"

升级状态必须能在断电后恢复,所以要落盘。但 Meta 本身也可能写坏, 所以存两份,各带 CRC 和序号,取序号大且 CRC 正确的那份:

typedef struct {
    uint16_t magic;         /* 0x1AP */
    uint16_t seq;           /* 单调递增,判断哪份更新 */
    uint8_t  active;        /* 0 = A, 1 = B */
    uint8_t  trial;         /* 1 = 新固件还在试运行期 */
    uint8_t  boot_fail;     /* 试运行期连续启动失败次数 */
    uint8_t  reserved;
    uint32_t app_len[2];    /* 两个分区的固件长度 */
    uint32_t app_crc[2];    /* 两个分区的整包 CRC32 */
    uint16_t crc;           /* 上面所有字节的 CRC16 */
} boot_meta_t;

写入顺序:先写备份槽(旧槽),校验通过后再更新指针缓存 —— 任一时刻至少有一份可用。

三、上电启动流程

1. 读 Meta(两份取新的)
2. 若 active 分区 CRC 不符            -> 换另一个分区
3. 若 trial == 1:
       boot_fail++
       若 boot_fail >= 3  -> 判定新固件有问题,rollback(切回另一个分区)
       否则写回 Meta(boot_fail 已加)
4. 跳转到 active 分区

新固件启动成功后,必须主动调用 boot_confirm() 把 trial 清掉。 如果它一启动就死机(比如时钟配错、看门狗复位), boot_fail 就会一路涨到 3,Bootloader 自动回滚。 这就是"自动回滚"的全部秘密——靠计数,不靠运气。

四、协议帧

串口上跑一个简单可靠的二进制帧:

请求: AA | cmd(1) | len(2,LE) | payload(len) | crc16(2,LE)
应答: 55 | status(1) | len(2,LE) | payload(len) | crc16(2,LE)

cmd:  0x01 HELLO     握手,返回 Bootloader 版本与 Flash 布局
      0x02 BEGIN     开始升级:总长度 + 整包 CRC32 + 目标分区
      0x03 DATA      数据块:offset(4) + 数据
      0x04 END       结束:Bootloader 校验整包 CRC
      0x05 ACTIVATE  激活新分区(进入试运行)
      0x06 CONFIRM   由 App 调用,确认新固件没问题
      0x07 ROLLBACK  主动回滚

CRC16 校验每一帧,保证丢字节/错字节立刻被发现; 整包 CRC32 校验整个固件,保证"每一帧都对但拼起来不对"也能被抓出来。

五、工程内容

  • boot_flash.c/h:RAM 模拟的 Flash 驱动(1→0 写入、按扇区擦除、可注入掉电)
  • 分区布局 + Meta 双备份读写
  • boot_iap.c/h:按字节喂入的协议状态机(真实串口中断正是这样工作的)
  • 升级引擎(擦除 / 写块 / 整包校验 / 激活 / 回滚)
  • test/test_iap.c:7 组实验,全部走真实字节流(模拟串口中断逐字节喂入)

六、主机实测(本文数据来源)

一份 16384 字节的"固件",按 256 字节分 64 块传输。所有数据都是本机 gcc 实编译、 实运行打出来的。

#实验实测结果
1握手版本 1、Slot 48 KB、试运行上限 3 次;活动分区 A,trial=0
2正常升级到 B 并激活BEGIN+DATA+END 返回 0,共处理 67 帧(1 BEGIN + 64 DATA + 1 END + 1 ACTIVATE 之后);B 分区读回逐字节一致;CONFIRM 后重启 trial=0
3第 7 块故意丢帧上位机重传 1 次后补齐,坏帧应答 0 次,最终返回 0
4整包 CRC 不符END 返回 4 (VERIFY_ERR);强行 ACTIVATE 同样返回 4;重启仍跑分区 A
5写到第 5000 字节掉电已提交 4864 字节(第 20 块只写进去 136 字节)就断电;重启后仍跑分区 A,旧分区内容未变
6新固件连续 3 次启动失败第 1、2 次重启仍跑 B(boot_fail=1、2),第 3 次自动回滚到 A,回滚后 A 分区与快照逐字节一致
7试图写正在运行的分区返回 2 (SEQ_ERR) 被拦住;非法分区号返回 3 (NOMEM)
第 3 项要看仔细:丢帧本身不会产生"坏帧应答"。设备那边只是没收到东西,安静地等着; 真正触发重传的是上位机"发出去却没等到应答"这件事。所以协议里 「收到就马上回 ACK、超时就重发同一块」这两条缺一不可。

七、写这套东西时踩的四个坑(都是实测撞出来的)

这一节是本文最有价值的部分。下面四个 bug 全部真实发生在这份代码的早期版本里, 而且全都不报错、不崩溃,只是"看起来在正常工作"。

坑 1:接收缓冲区按"载荷大小"开,少算了包头

#define IAP_MAX_PAYLOAD 256u
uint8_t rx_buf[IAP_MAX_PAYLOAD + 8];      /* 264 字节 —— 差 2 字节 */

数据帧最长是 SOF(1) + cmd(1) + len(2) + 偏移(4) + 数据(256) + CRC16(2) = 266 字节。 缓冲区只有 264,于是解析器拿到长度字段后算出 266 > 264,判定"长度非法", 丢弃并重新找 SOF——而它丢掉的这半帧里,载荷中恰好也含有 0xAA 字节, 于是重新同步在一个错误的位置上,后面所有帧全部错位。

实测表现:本该处理 67 帧的升级流程,只处理了 2 帧,BEGIN+DATA+END 返回 −2。

/* 正确写法:把整帧的最大长度定义出来,别让调用方自己算 */
#define IAP_FRAME_MAX (6u + 4u + IAP_MAX_PAYLOAD)
uint8_t rx_buf[IAP_FRAME_MAX];

坑 2:掉电模拟的计数器算错了基准

模拟"写到第 5000 字节掉电"的代码原本是这样写的:

if (f->cut_after >= 0 && (int32_t)i >= f->cut_after) { /* 掉电 */ }

i 是本次 write 调用内部的下标。而每次只写 256 字节,i 永远到不了 5000—— 掉电永远不会发生。更糟的是它不报错:升级一路成功,测试里那句 "掉电了竟然还报成功"的断言才会把它抓出来。

正确做法是把它当成跨调用的倒计时:

if (f->cut_after >= 0) {
    if (f->cut_after == 0) { f->powered = 0; f->cut_after = -1; return -1; }
    f->cut_after--;
}

修好后实测:4864 字节提交完成、第 20 块写进去 136 字节时断电,返回 −2。

坑 3:用内存缓存判断"哪个分区正在运行"

do_begin() 里原本用 iap->meta.active 来判断"这个目标分区是不是正在跑的那一个"。 问题是这份 meta 是结构体里的缓存副本:只要设备在两次升级之间发生过回滚, 或者上位机重放过旧命令,它就过期了。实测表现非常吓人—— 护栏完全失效,BEGIN 返回 0(成功),直接擦掉了正在运行的分区。

/* 动 Flash 之前,把 Meta 从 Flash 权威副本重新读一遍 */
static void sync_meta(boot_iap_t *iap)
{
    boot_meta_t m;
    if (bmeta_load(iap->flash, &m)) { iap->meta = m; }
}

读一个扇区而已,别省这一步。修好后返回 2 (SEQ_ERR),测试第 7 项通过。

坑 4:Meta 备份槽不是整扇区

两个 Meta 备份槽原本间距 512 字节,而 Flash 擦除粒度是 1024 字节:

#define META_SLOT_SIZE 512u        /* 错:第二个槽 addr+512 不是扇区对齐 */

bflash_erase() 检查到地址没有扇区对齐,直接返回 −1 → bmeta_store() 失败 → Meta 永远写不进去。因为初始化时 memset(mem, 0xFF),bmeta_load() 每次都 "两份都坏了",于是启动流程永远走"回退到默认分区 A"。

#define META_SLOT_SIZE BFLASH_SECTOR   /* 每个槽独占一个扇区 */

这个坑的教训是:擦除粒度是 Flash 的硬约束,任何"子槽"设计都必须按扇区对齐, 否则两个槽会互相踩。

八、上真硬件之前,还要注意这些

  1. 跳转前一定要关中断、关滴答、重设向量表。

SCB->VTOR = APP_ADDR,否则中断会跳到 Bootloader 的中断向量上, 表现为"一进中断就跑飞"。

  1. 升级前把写 Flash 的代码搬到 RAM 里跑,或者确认你在擦除的

不是当前正在取指的那块 Flash。STM32F1 擦除时同一 bank 会取指停顿, 但如果你在擦自己所在的 bank,就是自杀了。

  1. 看门狗要伺候好。整包 48 KB 的擦除+写入可能超过看门狗超时,

要么中途喂狗,要么在 Bootloader 里先关狗、跳转前再开。

  1. trial 计数放在 Meta 里,不能放 RAM——掉电就没了,

新固件一死机重启,计数又归零,永远回滚不了。

  1. CRC32 要算"实际写入的长度",不是整个分区。分区尾部是 0xFF,

把它算进去会导致同一份固件每次校验都不一样。

  1. 升级完别忘了校验。写完就跳转是最常见的偷懒,

结果是 Flash 写错一位,设备直接变砖。

九、进阶方向

  • 换成 CAN / 蓝牙 / 4G 承载,协议层完全不用改(只换字节来源)
  • 加差分升级:只传新旧固件的差异,流量能降一个数量级
  • 加固件签名(Ed25519 / RSA),防住恶意刷写
  • 用 YMODEM 协议承载,可以直接用 SecureCRT / Tera Term 发送文件升级

完整代码

Makefile

CC      ?= gcc
CFLAGS  ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS  ?= 
SRC      = src/boot_crc.c src/boot_flash.c src/boot_iap.c
TEST     = test/test_iap.c

ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)

all: run

$(BIN): $(SRC) $(TEST)
	@mkdir -p build
	$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)

run: $(BIN)
	@$(BIN)

clean:
	rm -rf build

.PHONY: all run clean

include/boot_crc.h

/**
 * boot_crc.h - CRC16 (帧校验) 与 CRC32 (整包校验)
 */
#ifndef BOOT_CRC_H
#define BOOT_CRC_H

#include <stdint.h>
#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

/* 这两个宏替代 offsetof,避免在伪代码里依赖 stddef 的展开细节 */
#define offsetof_meta() (0u)

uint16_t crc16(const uint8_t *data, uint32_t len);
uint32_t crc32(const uint8_t *data, uint32_t len);

typedef struct {
    uint32_t state;
} crc32_ctx_t;

void crc32_start(crc32_ctx_t *c);
void crc32_update(crc32_ctx_t *c, const uint8_t *data, uint32_t len);
uint32_t crc32_finish(const crc32_ctx_t *c);

#ifdef __cplusplus
}
#endif

#endif /* BOOT_CRC_H */

include/boot_flash.h

/**
 * boot_flash.h - RAM 模拟的片内 Flash + 分区布局 + Meta 双备份
 *
 * 分区布局(共 128 KB):
 *   0x00000 - 0x03FFF  Bootloader   (16 KB)
 *   0x04000 - 0x0FFFF  App Slot A   (48 KB)
 *   0x10000 - 0x1BFFF  App Slot B   (48 KB)
 *   0x1C000 - 0x1FFFF  Meta 区      (16 KB)
 */
#ifndef BOOT_FLASH_H
#define BOOT_FLASH_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

#define BFLASH_SIZE         (128u * 1024u)
#define BFLASH_SECTOR       1024u

#define BOOT_ADDR           0x00000u
#define SLOT_A_ADDR         0x04000u
#define SLOT_B_ADDR         0x10000u
#define META_ADDR           0x1C000u
#define SLOT_SIZE           (48u * 1024u)

#define BMETA_MAGIC         0x1A9Cu
#define BMETA_TRY_LIMIT     3u      /* 试运行最多连续失败几次 */

typedef struct {
    uint16_t magic;
    uint16_t seq;
    uint8_t  active;        /* 0 = A, 1 = B */
    uint8_t  trial;         /* 1 = 试运行期 */
    uint8_t  boot_fail;
    uint8_t  reserved;
    uint32_t app_len[2];
    uint32_t app_crc[2];
    uint16_t crc;
} boot_meta_t;

typedef struct {
    uint8_t  mem[BFLASH_SIZE];
    int      powered;
    int32_t  cut_after;     /* >=0:下一次写入只提交这么多字节后掉电 */
    uint32_t erase_ops;
    uint32_t write_ops;
} bflash_t;

void bflash_init(bflash_t *f);
void bflash_power_cycle(bflash_t *f);
void bflash_schedule_power_cut(bflash_t *f, int32_t n);
int  bflash_read(const bflash_t *f, uint32_t addr, void *buf, uint32_t len);
int  bflash_write(bflash_t *f, uint32_t addr, const void *buf, uint32_t len);
int  bflash_erase(bflash_t *f, uint32_t addr, uint32_t len);

/** 从 Meta 区读出最新一份(两份取 seq 大且 CRC 正确的)。失败返回 0。 */
int  bmeta_load(const bflash_t *f, boot_meta_t *out);
/** 写入一份新的 Meta(交替写两份槽)。 */
int  bmeta_store(bflash_t *f, boot_meta_t *m);

uint32_t slot_addr(int slot);
const char *slot_name(int slot);
uint32_t boot_crc32(const uint8_t *data, uint32_t len);

#ifdef __cplusplus
}
#endif

#endif /* BOOT_FLASH_H */

include/boot_iap.h

/**
 * boot_iap.h - 按字节喂入的 IAP 协议状态机 + 升级引擎
 *
 * 真实串口中断正是"来一个字节处理一个字节",所以这里的接口就是
 *   boot_iap_feed(&iap, byte);
 * Bootloader 收到一帧完整且 CRC 正确的请求后调用 boot_iap_handle()。
 */
#ifndef BOOT_IAP_H
#define BOOT_IAP_H

#include <stdint.h>

#include "boot_flash.h"

#ifdef __cplusplus
extern "C" {
#endif

#define IAP_REQ_SOF     0xAAu
#define IAP_RSP_SOF     0x55u
#define IAP_MAX_PAYLOAD 256u        /* 单帧最大“数据字节数”(不含偏移量) */

/* 一整帧的最大字节数 = SOF + cmd + len(2) + [偏移4 + 数据] + CRC16(2)
 * 这里是最容易写错的地方:如果只按 IAP_MAX_PAYLOAD + 6 开缓冲区,
 * 数据帧长度恰好超一个字节,解析器就会把每一帧都判成“长度非法”并失步。 */
#define IAP_FRAME_MAX   (6u + 4u + IAP_MAX_PAYLOAD)

#define IAP_CMD_HELLO    0x01u
#define IAP_CMD_BEGIN    0x02u
#define IAP_CMD_DATA     0x03u
#define IAP_CMD_END      0x04u
#define IAP_CMD_ACTIVATE 0x05u
#define IAP_CMD_CONFIRM  0x06u
#define IAP_CMD_ROLLBACK 0x07u

#define IAP_ST_OK         0x00u
#define IAP_ST_CRC_ERR    0x01u
#define IAP_ST_SEQ_ERR    0x02u
#define IAP_ST_NOMEM      0x03u
#define IAP_ST_VERIFY_ERR 0x04u
#define IAP_ST_NOT_READY  0x05u
#define IAP_ST_BAD_CMD    0x06u

typedef struct {
    /* 接收状态机 */
    uint8_t  rx_buf[IAP_FRAME_MAX];
    uint16_t rx_len;
    uint16_t rx_need;
    int      rx_active;

    /* 发送缓冲(由调用方通过 tx_fn 发出去) */
    void (*tx_fn)(void *ctx, const uint8_t *data, uint32_t len);
    void *tx_ctx;

    /* 升级上下文 */
    bflash_t *flash;
    boot_meta_t meta;
    int      target_slot;
    uint32_t expect_len;
    uint32_t expect_crc;
    uint32_t written;
    uint32_t rx_crc32;
    int      upgrading;

    uint32_t frames_ok;
    uint32_t frames_bad;
} boot_iap_t;

void boot_iap_init(boot_iap_t *iap, bflash_t *flash,
                   void (*tx)(void *ctx, const uint8_t *d, uint32_t n), void *ctx);

/** 上电启动流程:读 Meta、判 CRC、试运行计数、必要时回滚。返回要跳转的分区。 */
int  boot_select_slot(bflash_t *flash, boot_meta_t *out_meta, char *log, uint32_t logsz);

/** 喂一个字节。返回 1 表示刚收完整帧并已处理完毕。 */
int  boot_iap_feed(boot_iap_t *iap, uint8_t byte);

/** App 启动成功后调用:清掉 trial 标记。 */
int  boot_iap_confirm(boot_iap_t *iap);

#ifdef __cplusplus
}
#endif

#endif /* BOOT_IAP_H */

platformio.ini

[platformio]
default_envs = bluepill

[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
    -Wall
    -Wextra
    -Isrc
lib_ldf_mode = deep+

src/boot_crc.c

#include "boot_crc.h"

uint16_t crc16(const uint8_t *data, uint32_t len)
{
    uint16_t crc = 0xFFFFu;
    uint32_t i;
    int b;

    for (i = 0; i < len; i++) {
        crc ^= (uint16_t)((uint16_t)data[i] << 8);
        for (b = 0; b < 8; b++) {
            if (crc & 0x8000u) {
                crc = (uint16_t)((crc << 1) ^ 0x1021u);
            } else {
                crc = (uint16_t)(crc << 1);
            }
        }
    }
    return crc;
}

void crc32_start(crc32_ctx_t *c)
{
    c->state = 0xFFFFFFFFu;
}

void crc32_update(crc32_ctx_t *c, const uint8_t *data, uint32_t len)
{
    uint32_t crc = c->state;
    uint32_t i;
    int b;

    for (i = 0; i < len; i++) {
        crc ^= data[i];
        for (b = 0; b < 8; b++) {
            if (crc & 1u) {
                crc = (crc >> 1) ^ 0xEDB88320u;
            } else {
                crc >>= 1;
            }
        }
    }
    c->state = crc;
}

uint32_t crc32_finish(const crc32_ctx_t *c)
{
    return c->state ^ 0xFFFFFFFFu;
}

uint32_t crc32(const uint8_t *data, uint32_t len)
{
    crc32_ctx_t c;

    crc32_start(&c);
    crc32_update(&c, data, len);
    return crc32_finish(&c);
}

src/boot_flash.c

#include "boot_flash.h"

#include <string.h>

#include "boot_crc.h"

/* Meta 两个备份槽各占一个完整扇区,这样两次"擦-写"互不影响,
 * 也能保证擦除地址永远是扇区对齐的(这是最容易写错的一处)。 */
#define META_SLOT_SIZE  BFLASH_SECTOR

void bflash_init(bflash_t *f)
{
    memset(f, 0, sizeof(*f));
    memset(f->mem, 0xFF, sizeof(f->mem));
    f->powered = 1;
    f->cut_after = -1;
}

void bflash_power_cycle(bflash_t *f)
{
    f->powered = 1;
    f->cut_after = -1;
}

void bflash_schedule_power_cut(bflash_t *f, int32_t n)
{
    f->cut_after = n;
}

int bflash_read(const bflash_t *f, uint32_t addr, void *buf, uint32_t len)
{
    if (addr + len > BFLASH_SIZE) {
        return -1;
    }
    memcpy(buf, f->mem + addr, len);
    return 0;
}

int bflash_write(bflash_t *f, uint32_t addr, const void *buf, uint32_t len)
{
    const uint8_t *p = (const uint8_t *)buf;
    uint32_t i;

    if (addr + len > BFLASH_SIZE || !f->powered) {
        return -1;
    }
    for (i = 0; i < len; i++) {
        /* cut_after 是"还能再写多少个字节"的倒计时,跨多次 write 调用累计;
         * 如果只在本次调用的下标里比较,就永远也到不了 5000,掉电模拟会静默失效。 */
        if (f->cut_after >= 0) {
            if (f->cut_after == 0) {
                f->powered = 0;
                f->cut_after = -1;
                return -1;
            }
            f->cut_after--;
        }
        if ((f->mem[addr + i] & p[i]) != p[i]) {
            return -1;      /* 只能 1 -> 0 */
        }
        f->mem[addr + i] &= p[i];
        f->write_ops++;
    }
    return 0;
}

int bflash_erase(bflash_t *f, uint32_t addr, uint32_t len)
{
    if (addr + len > BFLASH_SIZE || !f->powered) {
        return -1;
    }
    if ((addr % BFLASH_SECTOR) != 0u || (len % BFLASH_SECTOR) != 0u) {
        return -1;          /* 必须是整扇区 */
    }
    memset(f->mem + addr, 0xFF, len);
    f->erase_ops += len / BFLASH_SECTOR;
    return 0;
}

uint32_t slot_addr(int slot)
{
    return (slot == 0) ? SLOT_A_ADDR : SLOT_B_ADDR;
}

const char *slot_name(int slot)
{
    return (slot == 0) ? "A" : "B";
}

/* ---------------- Meta 双备份 ---------------- */

/* 校验和覆盖整个结构体,计算时把 crc 字段本身清零 */
static uint16_t meta_crc_of(const boot_meta_t *m)
{
    boot_meta_t t = *m;
    t.crc = 0;
    return crc16((const uint8_t *)&t, (uint32_t)sizeof(t));
}

int bmeta_load(const bflash_t *f, boot_meta_t *out)
{
    boot_meta_t a;
    boot_meta_t b;
    int ok_a;
    int ok_b;

    if (bflash_read(f, META_ADDR, &a, sizeof(a)) != 0) {
        return 0;
    }
    if (bflash_read(f, META_ADDR + META_SLOT_SIZE, &b, sizeof(b)) != 0) {
        return 0;
    }
    ok_a = (a.magic == BMETA_MAGIC) && (meta_crc_of(&a) == a.crc);
    ok_b = (b.magic == BMETA_MAGIC) && (meta_crc_of(&b) == b.crc);

    if (ok_a && ok_b) {
        *out = (a.seq >= b.seq) ? a : b;
        return 1;
    }
    if (ok_a) {
        *out = a;
        return 1;
    }
    if (ok_b) {
        *out = b;
        return 1;
    }
    return 0;       /* 两份都坏了:调用方应该走"恢复默认" */
}

int bmeta_store(bflash_t *f, boot_meta_t *m)
{
    uint32_t slot;
    uint16_t cur_seq = 0;
    boot_meta_t old;

    /* 轮换两个槽:读出现有 seq 决定写哪个(写得越晚的 seq 越大) */
    if (bmeta_load(f, &old)) {
        cur_seq = old.seq;
    }
    slot = ((uint32_t)cur_seq % 2u == 0u) ? 1u : 0u;

    m->magic = BMETA_MAGIC;
    m->seq = (uint16_t)(cur_seq + 1u);
    m->crc = meta_crc_of(m);

    if (bflash_erase(f, META_ADDR + slot * META_SLOT_SIZE, BFLASH_SECTOR) != 0) {
        return -1;
    }
    return bflash_write(f, META_ADDR + slot * META_SLOT_SIZE, m, sizeof(*m));
}

src/boot_iap.c

#include "boot_iap.h"

#include <stdio.h>
#include <string.h>

#include "boot_crc.h"

/* ---------------- 发送一帧应答 ---------------- */

static void send_rsp(boot_iap_t *iap, uint8_t status,
                     const uint8_t *payload, uint16_t len)
{
    uint8_t buf[8 + IAP_MAX_PAYLOAD];
    uint16_t crc;
    uint32_t n;

    buf[0] = IAP_RSP_SOF;
    buf[1] = status;
    buf[2] = (uint8_t)(len & 0xFFu);
    buf[3] = (uint8_t)((len >> 8) & 0xFFu);
    if (len > 0u && payload != NULL) {
        memcpy(buf + 4, payload, len);
    }
    crc = crc16(buf, (uint32_t)(4u + len));
    n = 4u + len;
    buf[n] = (uint8_t)(crc & 0xFFu);
    buf[n + 1u] = (uint8_t)((crc >> 8) & 0xFFu);
    if (iap->tx_fn != NULL) {
        iap->tx_fn(iap->tx_ctx, buf, n + 2u);
    }
}

/* ---------------- 升级引擎 ---------------- */

static uint32_t slot_len(int slot, const boot_meta_t *m)
{
    return (slot == 0) ? m->app_len[0] : m->app_len[1];
}

static uint32_t slot_crc(int slot, const boot_meta_t *m)
{
    return (slot == 0) ? m->app_crc[0] : m->app_crc[1];
}

static void set_slot_meta(boot_meta_t *m, int slot, uint32_t len, uint32_t crc)
{
    m->app_len[slot] = len;
    m->app_crc[slot] = crc;
}

/* 校验某个分区的整包 CRC */
static int verify_slot(bflash_t *f, const boot_meta_t *m, int slot)
{
    uint8_t buf[256];
    uint32_t remain = slot_len(slot, m);
    uint32_t addr = slot_addr(slot);
    crc32_ctx_t c;

    if (remain == 0u || remain > SLOT_SIZE) {
        return 0;
    }
    crc32_start(&c);
    while (remain > 0u) {
        uint32_t n = (remain > sizeof(buf)) ? (uint32_t)sizeof(buf) : remain;
        if (bflash_read(f, addr, buf, n) != 0) {
            return 0;
        }
        crc32_update(&c, buf, n);
        addr += n;
        remain -= n;
    }
    return (crc32_finish(&c) == slot_crc(slot, m));
}

/* ---------------- 请求处理 ---------------- */

static void do_hello(boot_iap_t *iap)
{
    uint8_t p[8];

    p[0] = 1u;                       /* 协议版本 */
    p[1] = (uint8_t)(SLOT_SIZE / 1024u);
    p[2] = (uint8_t)BMETA_TRY_LIMIT;
    p[3] = (uint8_t)iap->meta.active;
    p[4] = (uint8_t)iap->meta.trial;
    p[5] = (uint8_t)iap->meta.boot_fail;
    p[6] = 0u;
    p[7] = 0u;
    send_rsp(iap, IAP_ST_OK, p, 8);
}

/* 准备动 Flash 之前,一定要把 Meta 从 Flash 重新读一遍。
 *
 * 用内存里的缓存副本去判断「哪个分区正在运行」是个隐藏很深的坑:
 * 只要设备在两次升级之间发生过回滚、或者上位机重放了旧命令,
 * 这份缓存就是过期的,而「禁止写正在运行的分区」那道护栏会直接失效——
 * 结果是把自己的运行分区擦掉,重启即变砖。读一次扇区而已,别省。 */
static void sync_meta(boot_iap_t *iap)
{
    boot_meta_t m;

    if (bmeta_load(iap->flash, &m)) {
        iap->meta = m;
    }
}

static void do_begin(boot_iap_t *iap, const uint8_t *p, uint16_t len)
{
    uint32_t total;
    uint32_t want_crc;
    uint8_t target;

    if (len != 9u) {
        send_rsp(iap, IAP_ST_BAD_CMD, NULL, 0);
        return;
    }
    total = (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
            ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
    want_crc = (uint32_t)p[4] | ((uint32_t)p[5] << 8) |
               ((uint32_t)p[6] << 16) | ((uint32_t)p[7] << 24);
    target = p[8];

    sync_meta(iap);

    if (target > 1u || total == 0u || total > SLOT_SIZE) {
        send_rsp(iap, IAP_ST_NOMEM, NULL, 0);
        return;
    }
    /* 绝不允许往当前正在运行的分区写 */
    if ((int)target == iap->meta.active) {
        send_rsp(iap, IAP_ST_SEQ_ERR, NULL, 0);
        return;
    }
    if (bflash_erase(iap->flash, slot_addr(target),
                     ((total + BFLASH_SECTOR - 1u) / BFLASH_SECTOR) * BFLASH_SECTOR)
        != 0) {
        send_rsp(iap, IAP_ST_NOMEM, NULL, 0);
        return;
    }
    iap->target_slot = (int)target;
    iap->expect_len = total;
    iap->expect_crc = want_crc;
    iap->written = 0;
    iap->rx_crc32 = 0;
    iap->upgrading = 1;
    {
        crc32_ctx_t c;
        crc32_start(&c);
        iap->rx_crc32 = c.state;      /* 保存累加器初值 */
    }
    send_rsp(iap, IAP_ST_OK, (const uint8_t *)&total, 4);
}

static void do_data(boot_iap_t *iap, const uint8_t *p, uint16_t len)
{
    uint32_t off;
    uint16_t n;
    crc32_ctx_t c;

    if (!iap->upgrading) {
        send_rsp(iap, IAP_ST_NOT_READY, NULL, 0);
        return;
    }
    if (len < 5u) {
        send_rsp(iap, IAP_ST_BAD_CMD, NULL, 0);
        return;
    }
    off = (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
          ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
    n = (uint16_t)(len - 4u);
    if (off != iap->written || (uint32_t)off + n > iap->expect_len) {
        send_rsp(iap, IAP_ST_SEQ_ERR, NULL, 0);
        return;
    }
    if (bflash_write(iap->flash, slot_addr(iap->target_slot) + off, p + 4, n) != 0) {
        send_rsp(iap, IAP_ST_NOMEM, NULL, 0);
        return;
    }
    /* 累加整包 CRC32:把上下文状态存回来 */
    c.state = iap->rx_crc32;
    crc32_update(&c, p + 4, n);
    iap->rx_crc32 = c.state;
    iap->written += n;
    {
        uint32_t w = iap->written;
        send_rsp(iap, IAP_ST_OK, (const uint8_t *)&w, 4);
    }
}

static void do_end(boot_iap_t *iap)
{
    uint8_t p[8];
    uint32_t got;
    uint32_t want;

    if (!iap->upgrading) {
        send_rsp(iap, IAP_ST_NOT_READY, NULL, 0);
        return;
    }
    got = iap->rx_crc32 ^ 0xFFFFFFFFu;
    want = iap->expect_crc;
    p[0] = (uint8_t)(got & 0xFFu);
    p[1] = (uint8_t)((got >> 8) & 0xFFu);
    p[2] = (uint8_t)((got >> 16) & 0xFFu);
    p[3] = (uint8_t)((got >> 24) & 0xFFu);
    p[4] = (uint8_t)(want & 0xFFu);
    p[5] = (uint8_t)((want >> 8) & 0xFFu);
    p[6] = (uint8_t)((want >> 16) & 0xFFu);
    p[7] = (uint8_t)((want >> 24) & 0xFFu);

    if (iap->written != iap->expect_len || got != want) {
        iap->upgrading = 0;
        send_rsp(iap, IAP_ST_VERIFY_ERR, p, 8);
        return;
    }
    /* 再把 Flash 里的内容重新读一遍算一次 CRC,防止"写进去的"和"收到的"不一致 */
    if (!verify_slot(iap->flash, &iap->meta, iap->target_slot)) {
        /* 长度/CRC 还没写进 meta,这里临时构造一份来校验 */
        boot_meta_t tmp = iap->meta;
        set_slot_meta(&tmp, iap->target_slot, iap->expect_len, iap->expect_crc);
        if (!verify_slot(iap->flash, &tmp, iap->target_slot)) {
            iap->upgrading = 0;
            send_rsp(iap, IAP_ST_VERIFY_ERR, p, 8);
            return;
        }
    }
    iap->upgrading = 0;
    send_rsp(iap, IAP_ST_OK, p, 8);
}

static void do_activate(boot_iap_t *iap)
{
    boot_meta_t m;

    if (!bmeta_load(iap->flash, &m)) {
        send_rsp(iap, IAP_ST_NOT_READY, NULL, 0);
        return;
    }
    /* 激活之前必须再校验一次目标分区 */
    set_slot_meta(&m, iap->target_slot, iap->expect_len, iap->expect_crc);
    if (!verify_slot(iap->flash, &m, iap->target_slot)) {
        send_rsp(iap, IAP_ST_VERIFY_ERR, NULL, 0);
        return;
    }
    m.active = (uint8_t)iap->target_slot;
    m.trial = 1u;
    m.boot_fail = 0u;
    if (bmeta_store(iap->flash, &m) != 0) {
        send_rsp(iap, IAP_ST_NOMEM, NULL, 0);
        return;
    }
    iap->meta = m;
    send_rsp(iap, IAP_ST_OK, &m.active, 1);
}

static void do_confirm(boot_iap_t *iap)
{
    boot_meta_t m;

    if (!bmeta_load(iap->flash, &m)) {
        send_rsp(iap, IAP_ST_NOT_READY, NULL, 0);
        return;
    }
    m.trial = 0u;
    m.boot_fail = 0u;
    if (bmeta_store(iap->flash, &m) != 0) {
        send_rsp(iap, IAP_ST_NOMEM, NULL, 0);
        return;
    }
    iap->meta = m;
    send_rsp(iap, IAP_ST_OK, &m.active, 1);
}

static void do_rollback(boot_iap_t *iap)
{
    boot_meta_t m;
    uint8_t other;

    if (!bmeta_load(iap->flash, &m)) {
        send_rsp(iap, IAP_ST_NOT_READY, NULL, 0);
        return;
    }
    other = (uint8_t)(1u - m.active);
    if (!verify_slot(iap->flash, &m, other)) {
        send_rsp(iap, IAP_ST_VERIFY_ERR, NULL, 0);
        return;
    }
    m.active = other;
    m.trial = 0u;
    m.boot_fail = 0u;
    if (bmeta_store(iap->flash, &m) != 0) {
        send_rsp(iap, IAP_ST_NOMEM, NULL, 0);
        return;
    }
    iap->meta = m;
    send_rsp(iap, IAP_ST_OK, &m.active, 1);
}

/* ---------------- 接收状态机 ---------------- */

static void handle_frame(boot_iap_t *iap)
{
    const uint8_t *p = iap->rx_buf;
    uint8_t cmd;
    uint16_t len;

    if (p[0] != IAP_REQ_SOF) {
        iap->frames_bad++;
        send_rsp(iap, IAP_ST_CRC_ERR, NULL, 0);
        return;
    }
    cmd = p[1];
    len = (uint16_t)p[2] | ((uint16_t)p[3] << 8);
    if (4u + len + 2u != iap->rx_len) {
        iap->frames_bad++;
        send_rsp(iap, IAP_ST_CRC_ERR, NULL, 0);
        return;
    }
    {
        uint16_t crc = crc16(p, (uint32_t)(4u + len));
        uint16_t rxc = (uint16_t)p[4 + len] | ((uint16_t)p[5 + len] << 8);
        if (crc != rxc) {
            /* 丢字节 / 错字节都在这里被抓住 */
            iap->frames_bad++;
            send_rsp(iap, IAP_ST_CRC_ERR, NULL, 0);
            return;
        }
    }
    iap->frames_ok++;
    switch (cmd) {
    case IAP_CMD_HELLO:
        do_hello(iap);
        break;
    case IAP_CMD_BEGIN:
        do_begin(iap, p + 4, len);
        break;
    case IAP_CMD_DATA:
        do_data(iap, p + 4, len);
        break;
    case IAP_CMD_END:
        do_end(iap);
        break;
    case IAP_CMD_ACTIVATE:
        do_activate(iap);
        break;
    case IAP_CMD_CONFIRM:
        do_confirm(iap);
        break;
    case IAP_CMD_ROLLBACK:
        do_rollback(iap);
        break;
    default:
        send_rsp(iap, IAP_ST_BAD_CMD, NULL, 0);
        break;
    }
}

void boot_iap_init(boot_iap_t *iap, bflash_t *flash,
                   void (*tx)(void *ctx, const uint8_t *d, uint32_t n), void *ctx)
{
    memset(iap, 0, sizeof(*iap));
    iap->flash = flash;
    iap->tx_fn = tx;
    iap->tx_ctx = ctx;
    iap->rx_need = 5u;      /* 先收够 SOF + cmd + len,再知道总长 */
    if (!bmeta_load(flash, &iap->meta)) {
        memset(&iap->meta, 0, sizeof(iap->meta));
        iap->meta.active = 0;
        iap->meta.app_len[0] = 0;
    }
}

int boot_iap_feed(boot_iap_t *iap, uint8_t byte)
{
    if (iap->rx_len >= sizeof(iap->rx_buf)) {
        iap->rx_len = 0;
        iap->rx_need = 5u;
        iap->rx_active = 0;
    }

    /* 还没同步上 SOF 的字节直接丢掉 */
    if (iap->rx_len == 0u && byte != IAP_REQ_SOF) {
        return 0;
    }
    iap->rx_buf[iap->rx_len++] = byte;

    if (iap->rx_len == 5u && !iap->rx_active) {
        uint16_t len = (uint16_t)iap->rx_buf[2] | ((uint16_t)iap->rx_buf[3] << 8);
        if (4u + len + 2u > sizeof(iap->rx_buf)) {
            /* 长度非法:丢弃,重新同步 */
            iap->rx_len = 0;
            iap->rx_need = 5u;
            iap->rx_active = 0;
            iap->frames_bad++;
            send_rsp(iap, IAP_ST_BAD_CMD, NULL, 0);
            return 0;
        }
        iap->rx_need = (uint16_t)(4u + len + 2u);
        iap->rx_active = 1;
    }
    if (iap->rx_active && iap->rx_len >= iap->rx_need) {
        handle_frame(iap);
        iap->rx_len = 0;
        iap->rx_need = 5u;
        iap->rx_active = 0;
        return 1;
    }
    return 0;
}

int boot_iap_confirm(boot_iap_t *iap)
{
    boot_meta_t m;

    if (!bmeta_load(iap->flash, &m)) {
        return -1;
    }
    m.trial = 0u;
    m.boot_fail = 0u;
    if (bmeta_store(iap->flash, &m) != 0) {
        return -1;
    }
    iap->meta = m;
    return 0;
}

/* ---------------- 上电启动流程 ---------------- */

int boot_select_slot(bflash_t *flash, boot_meta_t *out_meta, char *log, uint32_t logsz)
{
    boot_meta_t m;
    int slot;
    int other;

    if (!bmeta_load(flash, &m)) {
        /* 两份 Meta 都坏了:回退到"跑 A 分区"这个人畜无害的选择 */
        memset(&m, 0, sizeof(m));
        m.active = 0;
        if (log != NULL) {
            snprintf(log, logsz, "Meta 区两份都坏了,回退到默认分区 A");
        }
        *out_meta = m;
        return 0;
    }

    slot = (int)m.active;
    other = 1 - slot;

    /* 活动分区校验不过 -> 换另一个 */
    if (!verify_slot(flash, &m, slot)) {
        if (verify_slot(flash, &m, other)) {
            if (log != NULL) {
                snprintf(log, logsz,
                         "分区 %s 校验失败,改跑分区 %s", slot_name(slot),
                         slot_name(other));
            }
            m.active = (uint8_t)other;
            m.trial = 0u;
            m.boot_fail = 0u;
            bmeta_store(flash, &m);
            *out_meta = m;
            return other;
        }
        if (log != NULL) {
            snprintf(log, logsz, "两个分区都校验不过,只能停下来");
        }
        *out_meta = m;
        return -1;
    }

    /* 试运行期:失败次数累加,超限就回滚 */
    if (m.trial == 1u) {
        m.boot_fail = (uint8_t)(m.boot_fail + 1u);
        if (m.boot_fail >= (uint8_t)BMETA_TRY_LIMIT) {
            if (verify_slot(flash, &m, other)) {
                m.active = (uint8_t)other;
                m.trial = 0u;
                m.boot_fail = 0u;
                bmeta_store(flash, &m);
                if (log != NULL) {
                    snprintf(log, logsz,
                             "新固件连续 %u 次启动失败,自动回滚到分区 %s",
                             BMETA_TRY_LIMIT, slot_name(other));
                }
                *out_meta = m;
                return other;
            }
            if (log != NULL) {
                snprintf(log, logsz, "新固件失败但备份分区也不可用,只能继续试");
            }
            m.boot_fail = 0u;
        }
        bmeta_store(flash, &m);
        if (log != NULL && m.active == (uint8_t)slot) {
            snprintf(log, logsz, "分区 %s 试运行,第 %u 次启动",
                     slot_name(slot), m.boot_fail);
        }
    } else if (log != NULL) {
        /* 一定要给 log 写上内容:调用方往往复用同一个缓冲区,
         * 不写就会把它上一轮的结论当成这一次的结果打印出来。 */
        snprintf(log, logsz, "分区 %s 正常启动(已 CONFIRM)", slot_name(slot));
    }

    *out_meta = m;
    return slot;
}

test/test_iap.c

/**
 * 主机端测试:串口 IAP 升级 / 掉电 / 回滚
 *
 * gcc -std=c99 -Wall -Wextra -Iinclude src/boot_crc.c src/boot_flash.c \
 *     src/boot_iap.c test/test_iap.c -o build/test
 */
#include <stdio.h>
#include <string.h>

#include "boot_crc.h"
#include "boot_flash.h"
#include "boot_iap.h"

#define FW_SIZE 16384u
#define BLOCK   256u

static int failed = 0;
static bflash_t g_flash;
static boot_iap_t g_iap;

static void check(int cond, const char *what)
{
    if (!cond) {
        printf("      [FAIL] %s\n", what);
        failed++;
    }
}

/* ---------------- 假装是 PC 端的上位机 ---------------- */

typedef struct {
    uint8_t  buf[4096];
    uint32_t len;
    uint8_t  last_status;
    uint32_t rsp_count;
    uint32_t tx_bytes;
} host_t;

static host_t g_host;

static void host_tx(void *ctx, const uint8_t *d, uint32_t n)
{
    host_t *h = (host_t *)ctx;

    if (d == NULL || n == 0u || n > sizeof(h->buf)) {
        return;
    }
    /* send_rsp 每次都是一整帧,所以直接整帧覆盖即可 */
    memcpy(h->buf, d, n);
    h->len = n;
    h->tx_bytes += n;
    h->last_status = (n >= 2u) ? d[1] : 0u;
    h->rsp_count++;
}

/* 把一段字节喂给 bootloader */
static void feed(const uint8_t *d, uint32_t n)
{
    uint32_t i;

    for (i = 0; i < n; i++) {
        boot_iap_feed(&g_iap, d[i]);
    }
}

/* 组一帧请求并发送 */
static uint32_t make_req(uint8_t *out, uint8_t cmd, const uint8_t *payload, uint16_t len)
{
    uint16_t crc;

    out[0] = IAP_REQ_SOF;
    out[1] = cmd;
    out[2] = (uint8_t)(len & 0xFFu);
    out[3] = (uint8_t)((len >> 8) & 0xFFu);
    if (len > 0u && payload != NULL) {
        memcpy(out + 4, payload, len);
    }
    crc = crc16(out, (uint32_t)(4u + len));
    out[4 + len] = (uint8_t)(crc & 0xFFu);
    out[5 + len] = (uint8_t)((crc >> 8) & 0xFFu);
    return (uint32_t)(6u + len);
}

static uint8_t g_fw[FW_SIZE];

/* 生成一份"固件":用一个确定性伪随机序列,方便复现 */
static void gen_fw(uint32_t seed)
{
    uint32_t i;
    unsigned r = seed;

    for (i = 0; i < FW_SIZE; i++) {
        r = r * 1103515245u + 12345u;
        g_fw[i] = (uint8_t)((r >> 16) & 0xFFu);
    }
}

/* 完整传一遍固件。drop_at >= 0 表示在第 drop_at 块故意丢一帧 */
static uint32_t g_retry = 0;

static int upload(int target, uint32_t total, uint32_t crc_want, int drop_at)
{
    uint8_t req[512];
    uint8_t pl[260];
    uint32_t off;
    uint32_t len;
    int block = 0;

    pl[0] = (uint8_t)(total & 0xFFu);
    pl[1] = (uint8_t)((total >> 8) & 0xFFu);
    pl[2] = (uint8_t)((total >> 16) & 0xFFu);
    pl[3] = (uint8_t)((total >> 24) & 0xFFu);
    pl[4] = (uint8_t)(crc_want & 0xFFu);
    pl[5] = (uint8_t)((crc_want >> 8) & 0xFFu);
    pl[6] = (uint8_t)((crc_want >> 16) & 0xFFu);
    pl[7] = (uint8_t)((crc_want >> 24) & 0xFFu);
    pl[8] = (uint8_t)target;
    feed(req, make_req(req, IAP_CMD_BEGIN, pl, 9));
    if (g_host.last_status != IAP_ST_OK) {
        return -1;
    }

    off = 0;
    while (off < total) {
        uint32_t n;
        len = total - off;
        if (len > BLOCK) {
            len = BLOCK;
        }
        n = len;
        if (block != drop_at) {
            pl[0] = (uint8_t)(off & 0xFFu);
            pl[1] = (uint8_t)((off >> 8) & 0xFFu);
            pl[2] = (uint8_t)((off >> 16) & 0xFFu);
            pl[3] = (uint8_t)((off >> 24) & 0xFFu);
            memcpy(pl + 4, g_fw + off, n);
            feed(req, make_req(req, IAP_CMD_DATA, pl, (uint16_t)(n + 4u)));
            if (g_host.last_status != IAP_ST_OK) {
                return -2;
            }
            off += n;           /* 这一块确实送到设备上了,偏移量才前进 */
        } else {
            g_retry++;          /* 丢帧:偏移量不动,下一轮会重传同一块 */
        }
        block++;
    }
    feed(req, make_req(req, IAP_CMD_END, NULL, 0));
    return (int)g_host.last_status;
}

int main(void)
{
    uint8_t req[128];
    char log[128];
    boot_meta_t meta;
    int slot;
    uint32_t crc;

    printf("===== 串口 IAP:A/B 双分区 + 自动回滚 实测 =====\n");
    printf("固件 %u 字节,数据块 %u 字节,Slot 大小 %u 字节\n\n",
           FW_SIZE, BLOCK, SLOT_SIZE);

    /* 先把"出厂固件"装到 A 分区 */
    bflash_init(&g_flash);
    gen_fw(0x1111u);
    crc = crc32(g_fw, FW_SIZE);
    bflash_write(&g_flash, SLOT_A_ADDR, g_fw, FW_SIZE);
    memset(&meta, 0, sizeof(meta));
    meta.active = 0u;
    meta.app_len[0] = FW_SIZE;
    meta.app_crc[0] = crc;
    bmeta_store(&g_flash, &meta);

    /* ---------- [1] 握手 ---------- */
    printf("[1] 握手(HELLO)\n");
    {
        boot_iap_init(&g_iap, &g_flash, host_tx, &g_host);
        g_host.len = 0;
        feed(req, make_req(req, IAP_CMD_HELLO, NULL, 0));
        check(g_host.last_status == IAP_ST_OK, "HELLO 失败");
        printf("    Bootloader 版本 %u,Slot %u KB,试运行上限 %u 次\n",
               g_host.buf[4], g_host.buf[5], g_host.buf[6]);
        printf("    当前活动分区 %c,trial=%u,boot_fail=%u\n",
               'A' + g_host.buf[7], g_host.buf[8], g_host.buf[9]);
    }

    /* ---------- [2] 正常升级 ---------- */
    printf("\n[2] 正常升级:把新固件写进 B 分区并激活\n");
    {
        int rc;

        gen_fw(0x2222u);
        crc = crc32(g_fw, FW_SIZE);
        rc = upload(1, FW_SIZE, crc, -1);
        printf("    BEGIN+DATA+END 返回 %d(0 = OK),共处理 %u 帧\n", rc, g_iap.frames_ok);
        check(rc == IAP_ST_OK, "升级传输失败");

        feed(req, make_req(req, IAP_CMD_ACTIVATE, NULL, 0));
        check(g_host.last_status == IAP_ST_OK, "ACTIVATE 失败");

        /* 模拟重启:重新走一次 boot_select_slot */
        slot = boot_select_slot(&g_flash, &meta, log, sizeof(log));
        printf("    激活后重启 -> 选择分区 %c(%s)\n", 'A' + slot, log);
        check(slot == 1, "重启后没有切到 B 分区");

        /* 校验 B 分区内容 */
        {
            static uint8_t rd[FW_SIZE];
            bflash_read(&g_flash, SLOT_B_ADDR, rd, FW_SIZE);
            check(memcmp(rd, g_fw, FW_SIZE) == 0, "B 分区内容和发出去的不一致");
            printf("    B 分区读回逐字节比对:一致\n");
        }

        /* App 启动成功,CONFIRM */
        feed(req, make_req(req, IAP_CMD_CONFIRM, NULL, 0));
        check(g_host.last_status == IAP_ST_OK, "CONFIRM 失败");
        boot_iap_init(&g_iap, &g_flash, host_tx, &g_host);
        slot = boot_select_slot(&g_flash, &meta, log, sizeof(log));
        printf("    CONFIRM 之后再重启 -> 分区 %c,trial=%u(应该是 0)\n",
               'A' + slot, meta.trial);
        check(slot == 1 && meta.trial == 0u, "CONFIRM 没有清掉 trial");
    }

    /* ---------- [3] 丢帧重传 ---------- */
    printf("\n[3] 传输中丢一帧(模拟串口丢字节),看能不能重传补上\n");
    {
        int rc;
        uint32_t bad_before = g_iap.frames_bad;

        gen_fw(0x3333u);
        crc = crc32(g_fw, FW_SIZE);
        g_retry = 0;
        rc = upload(0, FW_SIZE, crc, 7);
        printf("    第 7 块丢掉不发,上位机随后重传该块\n");
        printf("    重传次数 %u,坏帧应答 %u 次\n",
               g_retry, g_iap.frames_bad - bad_before);
        printf("    最终返回 %d(0 = OK)\n", rc);
        check(rc == IAP_ST_OK, "丢帧后重传没能补上");
        check(g_retry == 1u, "重传逻辑没有生效");
        feed(req, make_req(req, IAP_CMD_ACTIVATE, NULL, 0));
        check(g_host.last_status == IAP_ST_OK, "重传后激活失败");

        slot = boot_select_slot(&g_flash, &meta, log, sizeof(log));
        check(slot == 0, "应该切回 A 分区");
        feed(req, make_req(req, IAP_CMD_CONFIRM, NULL, 0));
        printf("    -> 帧 CRC 让丢失立刻暴露,上位机重传同一块即可\n");
    }

    /* ---------- [4] 整包 CRC 不符 ---------- */
    printf("\n[4] 每一帧都对,但整包 CRC 对不上(比如上位机读错了文件)\n");
    {
        int rc;
        boot_meta_t before;

        bmeta_load(&g_flash, &before);
        gen_fw(0x4444u);
        crc = crc32(g_fw, FW_SIZE);
        /* 故意把期望 CRC 写错 */
        rc = upload(1, FW_SIZE, crc ^ 0x5A5A5A5Au, -1);
        printf("    END 返回 %d(%d = VERIFY_ERR)\n", rc, IAP_ST_VERIFY_ERR);
        check(rc == IAP_ST_VERIFY_ERR, "整包 CRC 错误没有被拦住");

        /* 尝试激活:也应该失败 */
        feed(req, make_req(req, IAP_CMD_ACTIVATE, NULL, 0));
        printf("    强行 ACTIVATE 返回 %d(应该也是 VERIFY_ERR)\n", g_host.last_status);
        check(g_host.last_status != IAP_ST_OK, "坏固件竟然激活成功了");

        slot = boot_select_slot(&g_flash, &meta, log, sizeof(log));
        printf("    重启后仍然跑分区 %c,%s\n", 'A' + slot, log);
        check(slot == (int)before.active, "坏固件影响了活动分区");
    }

    /* ---------- [5] 升级中途掉电 ---------- */
    printf("\n[5] 升级中途掉电:写到第 5000 字节时断电\n");
    {
        uint8_t pl2[260];
        uint32_t off;
        boot_meta_t before;
        int rc;

        bmeta_load(&g_flash, &before);
        gen_fw(0x5555u);
        crc = crc32(g_fw, FW_SIZE);

        bflash_power_cycle(&g_flash);
        bflash_schedule_power_cut(&g_flash, 5000);
        rc = upload(1, FW_SIZE, crc, -1);
        printf("    升级过程返回 %d;已提交 %u 字节就断电了,"
               "第 %u 块只写进去 %u 字节\n",
               rc, g_iap.written, (unsigned)(g_iap.written / BLOCK + 1u),
               (unsigned)(5000u - g_iap.written));
        check(rc != IAP_ST_OK, "掉电了竟然还报成功");
        check(g_iap.written < FW_SIZE, "掉电点没有生效");

        bflash_power_cycle(&g_flash);
        boot_iap_init(&g_iap, &g_flash, host_tx, &g_host);
        slot = boot_select_slot(&g_flash, &meta, log, sizeof(log));
        printf("    断电重启 -> 选择分区 %c,%s\n", 'A' + slot, log);
        check(slot == (int)before.active, "掉电后没能回到旧分区");

        /* 旧分区内容仍然完好 */
        {
            uint8_t buf[64];
            bflash_read(&g_flash, SLOT_A_ADDR, buf, sizeof(buf));
            printf("    旧分区头部 8 字节:%02X %02X %02X %02X %02X %02X %02X %02X\n",
                   buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]);
        }
        (void)pl2;
        (void)off;
        printf("    -> 新固件写坏了一半无所谓,活动分区从头到尾没被碰过\n");
    }

    /* ---------- [6] 新固件连续启动失败 -> 自动回滚 ---------- */
    printf("\n[6] 新固件能通过校验但一启动就死机(不 CONFIRM),连重启 3 次\n");
    {
        static uint8_t rd[FW_SIZE];
        static uint8_t snap[FW_SIZE];       /* 「能跑的旧版本」快照 */
        int i;

        /* 先记下当前活动分区(A)的内容,回滚之后要拿它逐字节比对 */
        bflash_read(&g_flash, SLOT_A_ADDR, snap, FW_SIZE);
        printf("    先把 A 分区当前内容快照下来(%u 字节,CRC32=0x%08X)\n",
               FW_SIZE, crc32(snap, FW_SIZE));

        gen_fw(0x6666u);
        crc = crc32(g_fw, FW_SIZE);
        bflash_power_cycle(&g_flash);
        boot_iap_init(&g_iap, &g_flash, host_tx, &g_host);
        check(upload(1, FW_SIZE, crc, -1) == IAP_ST_OK, "上一步升级失败");
        feed(req, make_req(req, IAP_CMD_ACTIVATE, NULL, 0));
        check(g_host.last_status == IAP_ST_OK, "ACTIVATE 失败");
        printf("    新固件已写入 B 并激活,trial=1(还差一次 CONFIRM)\n");

        for (i = 1; i <= 4; i++) {
            /* 每次重启都会把 boot_fail 加一,但不调用 CONFIRM */
            slot = boot_select_slot(&g_flash, &meta, log, sizeof(log));
            printf("    第 %d 次重启 -> 分区 %c  %s(boot_fail=%u)\n",
                   i, 'A' + slot, log, meta.boot_fail);
            if (slot == 0) {
                break;
            }
        }
        printf("    第 %d 次重启时完成回滚\n", i);
        check(slot == 0, "连续启动失败后没有自动回滚");
        check(i <= 3, "回滚用掉的次数超过了试运行上限");

        /* 回滚之后旧分区内容必须逐字节完好 */
        {
            uint32_t k;
            int ok = 1;

            bflash_read(&g_flash, SLOT_A_ADDR, rd, FW_SIZE);
            for (k = 0; k < FW_SIZE; k++) {
                if (rd[k] != snap[k]) {
                    ok = 0;
                    break;
                }
            }
            check(ok, "回滚后 A 分区内容不对");
            printf("    回滚后 A 分区与快照逐字节一致:%s(首个差异字节偏移 %u)\n",
                   ok ? "是" : "否", ok ? 0u : k);
        }
    }

    /* ---------- [7] 禁止往正在运行的分区写 ---------- */
    printf("\n[7] 安全护栏:试图往「当前正在运行」的分区写\n");
    {
        boot_meta_t m;
        uint8_t pl3[9];

        bmeta_load(&g_flash, &m);
        memset(pl3, 0, sizeof(pl3));
        /* 长度字段给一个合法值,这样才会走到「禁止自我覆盖」那道判断 */
        pl3[0] = (uint8_t)(FW_SIZE & 0xFFu);
        pl3[1] = (uint8_t)((FW_SIZE >> 8) & 0xFFu);
        pl3[8] = m.active;
        feed(req, make_req(req, IAP_CMD_BEGIN, pl3, 9));
        printf("    BEGIN(target=当前分区) 返回 %d(%d = SEQ_ERR)\n",
               g_host.last_status, IAP_ST_SEQ_ERR);
        check(g_host.last_status == IAP_ST_SEQ_ERR, "没有拦住自我覆盖");

        pl3[8] = 5u;
        feed(req, make_req(req, IAP_CMD_BEGIN, pl3, 9));
        printf("    BEGIN(target=5,非法分区) 返回 %d(%d = NOMEM)\n",
               g_host.last_status, IAP_ST_NOMEM);
        check(g_host.last_status == IAP_ST_NOMEM, "非法分区号没被拒绝");
    }

    printf("\n===== %s =====\n",
           failed == 0 ? "全部通过:以上数据由本机 gcc 实编译实运行"
                       : "有失败项!");
    return failed == 0 ? 0 : 1;
}

实测输出

下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):

===== 串口 IAP:A/B 双分区 + 自动回滚 实测 =====
固件 16384 字节,数据块 256 字节,Slot 大小 49152 字节

[1] 握手(HELLO)
    Bootloader 版本 1,Slot 48 KB,试运行上限 3 次
    当前活动分区 A,trial=0,boot_fail=0

[2] 正常升级:把新固件写进 B 分区并激活
    BEGIN+DATA+END 返回 0(0 = OK),共处理 67 帧
    激活后重启 -> 选择分区 B(分区 B 试运行,第 1 次启动)
    B 分区读回逐字节比对:一致
    CONFIRM 之后再重启 -> 分区 B,trial=0(应该是 0)

[3] 传输中丢一帧(模拟串口丢字节),看能不能重传补上
    第 7 块丢掉不发,上位机随后重传该块
    重传次数 1,坏帧应答 0 次
    最终返回 0(0 = OK)
    -> 帧 CRC 让丢失立刻暴露,上位机重传同一块即可

[4] 每一帧都对,但整包 CRC 对不上(比如上位机读错了文件)
    END 返回 4(4 = VERIFY_ERR)
    强行 ACTIVATE 返回 4(应该也是 VERIFY_ERR)
    重启后仍然跑分区 A,分区 A 正常启动(已 CONFIRM)

[5] 升级中途掉电:写到第 5000 字节时断电
    升级过程返回 -2;已提交 4864 字节就断电了,第 20 块只写进去 136 字节
    断电重启 -> 选择分区 A,分区 A 正常启动(已 CONFIRM)
    旧分区头部 8 字节:21 03 A8 00 30 8B D6 C4
    -> 新固件写坏了一半无所谓,活动分区从头到尾没被碰过

[6] 新固件能通过校验但一启动就死机(不 CONFIRM),连重启 3 次
    先把 A 分区当前内容快照下来(16384 字节,CRC32=0xF6F68D61)
    新固件已写入 B 并激活,trial=1(还差一次 CONFIRM)
    第 1 次重启 -> 分区 B  分区 B 试运行,第 1 次启动(boot_fail=1)
    第 2 次重启 -> 分区 B  分区 B 试运行,第 2 次启动(boot_fail=2)
    第 3 次重启 -> 分区 A  新固件连续 3 次启动失败,自动回滚到分区 A(boot_fail=0)
    第 3 次重启时完成回滚
    回滚后 A 分区与快照逐字节一致:是(首个差异字节偏移 0)

[7] 安全护栏:试图往「当前正在运行」的分区写
    BEGIN(target=当前分区) 返回 2(2 = SEQ_ERR)
    BEGIN(target=5,非法分区) 返回 3(3 = NOMEM)

===== 全部通过:以上数据由本机 gcc 实编译实运行 =====

评论