一句话结论: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 的硬约束,任何"子槽"设计都必须按扇区对齐, 否则两个槽会互相踩。
八、上真硬件之前,还要注意这些
- 跳转前一定要关中断、关滴答、重设向量表。
SCB->VTOR = APP_ADDR,否则中断会跳到 Bootloader 的中断向量上, 表现为"一进中断就跑飞"。
- 升级前把写 Flash 的代码搬到 RAM 里跑,或者确认你在擦除的
不是当前正在取指的那块 Flash。STM32F1 擦除时同一 bank 会取指停顿, 但如果你在擦自己所在的 bank,就是自杀了。
- 看门狗要伺候好。整包 48 KB 的擦除+写入可能超过看门狗超时,
要么中途喂狗,要么在 Bootloader 里先关狗、跳转前再开。
trial计数放在 Meta 里,不能放 RAM——掉电就没了,
新固件一死机重启,计数又归零,永远回滚不了。
- CRC32 要算"实际写入的长度",不是整个分区。分区尾部是 0xFF,
把它算进去会导致同一份固件每次校验都不一样。
- 升级完别忘了校验。写完就跳转是最常见的偷懒,
结果是 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 cleaninclude/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 实编译实运行 =====