欢迎光临
我们一直在努力

STM32 BootLoader 实战(八):A/B 双分区升级、启动选择与失败回滚设计

STM32 BootLoader 实战(八):A/B 双分区升级、启动选择与失败回滚设计

摘要

前面几篇默认采用的是单 APP 分区:

BootLoader 区
APP 区
参数区 / 升级记录区

单 APP 分区能满足很多场景,但它有一个明显问题:一旦开始擦除 APP,旧程序就不完整了。如果升级中途断电、传输断开、Flash 写失败、CRC 校验失败,设备就只能停留在 BootLoader,等待重新升级。

如果设备在现场无人值守,或者升级失败后必须尽量自动恢复,就要考虑 A/B 双分区。

A/B 双分区的核心思路是:

当前运行 A 区 APP
升级时把新固件写到 B 区
B 区校验通过后,下次启动切到 B 区
B 区运行成功后,由 APP 确认
如果 B 区启动失败,BootLoader 回滚到 A 区

这篇重点讲 A/B 双分区的 Flash 划分、启动选择、升级记录、APP 确认、失败回滚,以及在 STM32F103C8T6、STM32F407VET6、STM32G473VET6 上做这件事时要注意什么。

目录

  • 1. 单 APP 分区的问题
  • 2. A/B 双分区的基本思路
  • 3. 哪些项目适合做 A/B 双分区
  • 4. Flash 空间够不够
  • 5. F103C8T6 的分区建议
  • 6. F407VET6 的分区建议
  • 7. G473VET6 的分区建议
  • 8. A/B 分区记录结构
  • 9. 固件头需要增加哪些字段
  • 10. BootLoader 上电如何选择启动分区
  • 11. 升级时为什么要写非活动分区
  • 12. 新 APP 第一次启动为什么不能马上确认成功
  • 13. APP 如何确认启动成功
  • 14. 回滚逻辑怎么设计
  • 15. APP 向量表和链接地址
  • 16. Flash 擦写注意事项
  • 17. A/B 分区状态机
  • 18. 常见问题
  • 19. 总结

1. 单 APP 分区的问题

单 APP 分区结构一般是这样:

0x08000000 +——————–+
| BootLoader |
+——————–+
APP_BASE | APP |
| |
+——————–+
PARAM_BASE | Upgrade Record |
+——————–+

升级流程:

收到固件头
检查固件头
擦除 APP 区
接收 APP 数据
写入 APP 区
CRC 校验
标记 APP 有效
跳转 APP

问题出在“擦除 APP 区”之后。

只要擦除开始,旧 APP 就不完整了。

如果此时发生下面任意一种情况:

断电
串口线拔掉
网线断开
上位机崩溃
YMODEM 失败
Flash 写失败
CRC 校验失败

BootLoader 都不能再跳旧 APP。

它只能停在 BootLoader,等待用户重新升级。

这种设计在调试阶段、有人维护的设备、低成本项目里没问题。但如果设备分布在现场,升级失败后没人能马上处理,单 APP 分区就不够稳。

A/B 双分区就是为了解决这个问题。

2. A/B 双分区的基本思路

A/B 双分区结构:

0x08000000 +——————–+
| BootLoader |
+——————–+
APP_A_BASE | APP A |
| |
+——————–+
APP_B_BASE | APP B |
| |
+——————–+
PARAM_BASE | Boot Record |
+——————–+

设备当前运行 A 区时,升级新固件写到 B 区。

设备当前运行 B 区时,升级新固件写到 A 区。

也就是:

活动分区:当前正在运行的 APP
备用分区:本次升级要写入的新 APP

升级过程:

当前运行 APP A
BootLoader 收到新固件
擦除 APP B
写入 APP B
校验 APP B
标记 APP B 为 PENDING
复位
BootLoader 启动 APP B
APP B 运行正常后确认
BootLoader 标记 APP B 为 VALID

如果 APP B 启动失败:

BootLoader 发现 B 区没有确认
回滚到 APP A

这样做的价值是:

升级失败不影响旧 APP
新 APP 第一次运行失败可以回滚
断电恢复逻辑更可靠
现场维护压力小很多

3. 哪些项目适合做 A/B 双分区

A/B 双分区不是所有项目都要做。

适合做 A/B 的项目:

设备长期运行在现场
升级失败后不能变砖
设备无人值守
网口远程升级
客户现场不方便接调试器
APP 体积没有占满 Flash
产品对可靠性要求高

不一定适合做 A/B 的项目:

Flash 很小
APP 很大
升级总是有人现场操作
成本极限敏感
产品允许失败后手动重新刷机

不要为了“看起来高级”硬做 A/B。

A/B 双分区会增加:

Flash 空间占用
分区管理复杂度
启动选择逻辑
APP 链接地址处理
升级记录区设计
测试工作量

如果 F103C8T6 只有 64KB Flash,而 APP 已经 40KB,再强行分 A/B,就很痛苦。

4. Flash 空间够不够

做 A/B 前先算空间。

公式很简单:

总 Flash >= BootLoader + APP_A + APP_B + 参数区 + 预留空间

如果 APP A 和 APP B 大小一样:

APP_SLOT_SIZE = (FLASH_SIZE – BOOT_SIZE – PARAM_SIZE) / 2

实际工程里还要留一点余量:

BootLoader 后续可能变大
APP 后续可能增加功能
参数区可能增加字段
Flash 擦除粒度可能不允许刚好切分

不要把 Flash 刚好分满。

建议至少留:

BootLoader 预留 4KB ~ 16KB
APP 分区预留 10% 左右空间
参数区至少双记录

不同 STM32 系列的 Flash 擦除粒度不同,分区边界必须对齐擦除页或 Sector。

5. F103C8T6 的分区建议

STM32F103C8T6 常见 Flash 是 64KB,页大小通常按 1KB 处理。

它的空间很紧。

如果做单 APP 分区,常见划分:

0x08000000 +——————–+
| BootLoader 16KB |
0x08004000 +——————–+
| APP 46KB |
0x0800F800 +——————–+
| Record 2KB |
0x08010000 +——————–+

如果做 A/B 双分区,要非常克制:

0x08000000 +——————–+
| BootLoader 12KB |
0x08003000 +——————–+
| APP A 24KB |
0x08009000 +——————–+
| APP B 24KB |
0x0800F000 +——————–+
| Record 4KB |
0x08010000 +——————–+

这个方案的问题很明显:

BootLoader 只能 12KB 左右
每个 APP 只有 24KB 左右
YMODEM、CRC、Flash、日志都要压缩
APP 功能稍微复杂就不够

所以 F103C8T6 不推荐强行做完整 A/B。

更现实的选择:

方案 1:单 APP 分区 + 断电恢复
方案 2:换更大 Flash 型号,比如 F103RCT6 / F103VET6
方案 3:外部 SPI Flash 存新固件,校验后再覆盖内部 APP
方案 4:只保留最小 BootLoader,APP 做得非常小,再做 A/B

如果产品要求远程升级可靠性,F103C8T6 这种容量最好一开始就慎重。

6. F407VET6 的分区建议

STM32F407VET6 常见 Flash 是 512KB。

F407 的内部 Flash 是 Sector 结构,前面几个 Sector 小,后面 Sector 大。

常见 Sector:

Sector 0: 0x08000000 ~ 0x08003FFF 16KB
Sector 1: 0x08004000 ~ 0x08007FFF 16KB
Sector 2: 0x08008000 ~ 0x0800BFFF 16KB
Sector 3: 0x0800C000 ~ 0x0800FFFF 16KB
Sector 4: 0x08010000 ~ 0x0801FFFF 64KB
Sector 5: 0x08020000 ~ 0x0803FFFF 128KB
Sector 6: 0x08040000 ~ 0x0805FFFF 128KB
Sector 7: 0x08060000 ~ 0x0807FFFF 128KB

一种比较稳的 A/B 划分:

0x08000000 +————————+
| BootLoader 64KB |
0x08010000 +————————+
| APP A 192KB |
0x08040000 +————————+
| APP B 192KB |
0x08070000 +————————+
| Record / Reserve 64KB |
0x08080000 +————————+

但要注意,F407 Sector 7 是 128KB。

如果你把最后 64KB 单独当记录区,会遇到一个问题:

擦除记录区时会擦整个 Sector 7

所以更符合 F407 Sector 的划分是:

0x08000000 +————————+
| BootLoader 64KB | Sector 0~3
0x08010000 +————————+
| Config / Record 64KB | Sector 4
0x08020000 +————————+
| APP A 128KB | Sector 5
0x08040000 +————————+
| APP B 128KB | Sector 6
0x08060000 +————————+
| Reserve / Factory 128KB| Sector 7
0x08080000 +————————+

这个方案的优点:

每个分区按 Sector 对齐
记录区单独占 Sector 4
APP A/B 擦除互不影响
Sector 7 可以留作工厂固件、备份参数或后续扩展

缺点:

每个 APP 只有 128KB
如果 APP 超过 128KB,就要重新规划

如果 APP 比较大,也可以:

BootLoader + Record 放前 128KB
APP A 使用 192KB
APP B 使用 192KB

但一定要按照 Sector 擦除边界设计,不要在 128KB Sector 中间硬切。

7. G473VET6 的分区建议

STM32G473VET6 常见 Flash 是 512KB,内部 Flash 页大小通常按 2KB 处理。

G4 的分区比 F407 好规划,因为页大小更均匀。

一种常见划分:

0x08000000 +————————+
| BootLoader 64KB |
0x08010000 +————————+
| APP A 192KB |
0x08040000 +————————+
| APP B 192KB |
0x08070000 +————————+
| Record 16KB |
0x08074000 +————————+
| Reserve 48KB |
0x08080000 +————————+

也可以更均衡:

0x08000000 +————————+
| BootLoader 48KB |
0x0800C000 +————————+
| Record 16KB |
0x08010000 +————————+
| APP A 224KB |
0x08048000 +————————+
| APP B 224KB |
0x08080000 +————————+

G473 做 A/B 比 F103 现实很多。

要注意:

APP A 和 APP B 起始地址都要按页对齐
擦除时按 page 计算
写入时按 G4 Flash 写入粒度处理
如果开启 TrustZone 或双 Bank,要再结合实际工程配置

本文先按普通单 Bank Flash 使用方式讲。

8. A/B 分区记录结构

A/B 分区最关键的是记录区。

记录区要回答几个问题:

当前应该启动哪个分区
A 区固件是否有效
B 区固件是否有效
是否有一个新分区正在试运行
试运行失败后要回滚到哪个分区
每个分区的版本号、大小、CRC 是多少
上一次错误码是什么

可以设计成这样:

#define BOOT_RECORD_MAGIC 0x424C5244U /* BLRD */
#define BOOT_RECORD_VERSION 0x00010000U

typedef enum
{
SLOT_A = 0,
SLOT_B = 1,
} AppSlot_t;

typedef enum
{
SLOT_STATE_EMPTY = 0xFFFFFFFFU,
SLOT_STATE_VALID = 0xA5A55A5AU,
SLOT_STATE_PENDING = 0x12345678U,
SLOT_STATE_CONFIRMED = 0x5AA5A55AU,
SLOT_STATE_BAD = 0xDEAD0001U,
} SlotState_t;

typedef struct
{
uint32_t state;
uint32_t version;
uint32_t app_base;
uint32_t app_size;
uint32_t app_crc32;
uint32_t boot_count;
uint32_t reserved[2];
} AppSlotInfo_t;

typedef struct
{
uint32_t magic;
uint32_t record_version;
uint32_t sequence;

uint32_t active_slot;
uint32_t rollback_slot;
uint32_t pending_slot;

uint32_t last_error;
uint32_t flags;

AppSlotInfo_t slot[2];

uint32_t record_crc32;
} BootRecord_t;

字段说明:

active_slot 当前默认启动分区
rollback_slot 新 APP 试运行失败后回滚到哪个分区
pending_slot 当前正在试运行的分区
sequence 双记录时选择最新记录
boot_count PENDING 分区已经尝试启动几次
record_crc32 记录本身的 CRC

这里有两个状态容易混淆。

8.1 VALID 和 CONFIRMED

可以把它们合成一个状态,也可以分开。

一种简单规则:

VALID:
分区 CRC 校验通过,可以被 BootLoader 启动

PENDING:
新固件刚升级成功,允许试运行,但还没被 APP 确认

CONFIRMED:
APP 已经运行成功并确认,可以长期作为活动分区

BAD:
该分区启动失败或校验失败,不允许启动

如果不想分太细,也可以只保留:

EMPTY
VALID
PENDING
BAD

APP 确认后,把 PENDING 改成 VALID。

本文后面采用这个简化规则。

8.2 为什么要双记录

记录区本身写入时也可能断电。

所以不要只存一份记录。

建议:

Record A
Record B

每次写新记录时,写到另一份记录里,并增加 sequence。

上电读取时:

检查 magic
检查 record_crc32
选择 sequence 最大的一条

如果最新记录写坏了,还可以使用上一条完整记录。

9. 固件头需要增加哪些字段

第六篇已经设计过固件头。

A/B 分区下建议增加或明确下面几个字段:

typedef struct
{
uint32_t magic;
uint32_t header_size;
uint32_t header_version;

uint32_t target_mcu;
uint32_t target_board;

uint32_t app_base;
uint32_t app_size;
uint32_t app_crc32;
uint32_t app_version;

uint32_t slot_mode;
uint32_t min_bootloader_version;
uint32_t flags;

uint32_t header_crc32;
uint8_t reserved[256 13 * 4];
} FirmwareHeader_t;

其中:

slot_mode:
0 = 不指定分区,由 BootLoader 自动选择非活动分区
1 = 指定写 A 区
2 = 指定写 B 区

min_bootloader_version:
固件要求的最低 BootLoader 版本

flags:
是否允许回滚
是否强制升级
是否允许写当前活动分区

一般情况下,不建议上位机强制指定 A/B 分区。

更稳的方式是:

BootLoader 自己选择非活动分区
固件头只声明 APP 链接地址和大小
BootLoader 检查固件是否能放入目标分区

如果 APP A 和 APP B 使用不同链接地址,固件头里的 app_base 必须和目标分区一致。

10. BootLoader 上电如何选择启动分区

上电后不要简单地跳 active_slot。

要按规则判断。

推荐流程:

1. 读取 BootRecord
2. 如果记录无效,扫描 A/B 分区
3. 检查 pending_slot
4. 如果存在 PENDING 分区,优先试运行它
5. 如果 PENDING 启动次数超过限制,标记 BAD,回滚 rollback_slot
6. 如果 active_slot 有效,启动 active_slot
7. 如果 active_slot 无效,尝试另一个有效分区
8. 两个分区都无效,停留 BootLoader

伪代码:

static AppSlot_t Boot_SelectSlot(BootRecord_t *rec)
{
if (Boot_RecordIsValid(rec) == 0)
{
return Boot_ScanAndSelectSlot(rec);
}

if (rec->pending_slot <= SLOT_B)
{
AppSlot_t pending = (AppSlot_t)rec->pending_slot;

if (Boot_SlotIsBootable(rec, pending))
{
if (rec->slot[pending].boot_count < BOOT_PENDING_MAX_TRY)
{
rec->slot[pending].boot_count++;
Boot_RecordSave(rec);
return pending;
}
else
{
rec->slot[pending].state = SLOT_STATE_BAD;
rec->last_error = BL_ERR_PENDING_BOOT_TIMEOUT;
rec->pending_slot = 0xFFFFFFFFU;
Boot_RecordSave(rec);
}
}
}

if (Boot_SlotIsBootable(rec, (AppSlot_t)rec->active_slot))
{
return (AppSlot_t)rec->active_slot;
}

if (Boot_SlotIsBootable(rec, SLOT_A))
{
return SLOT_A;
}

if (Boot_SlotIsBootable(rec, SLOT_B))
{
return SLOT_B;
}

return SLOT_INVALID;
}

Boot_SlotIsBootable() 至少检查:

分区状态是否 VALID 或 PENDING
APP 栈顶是否在 SRAM
Reset_Handler 是否在该分区范围内
APP CRC 是否正确

不要只看状态标志。

11. 升级时为什么要写非活动分区

A/B 分区升级的核心就是写非活动分区。

假设当前运行 A:

active_slot = A
target_slot = B

升级流程:

检查固件头
检查 B 区空间
擦 B 区
写 B 区
校验 B 区
设置 B 区 PENDING
设置 rollback_slot = A
复位

这样升级过程中即使失败:

A 区没有被擦
A 区还能启动

失败处理:

固件头错误 -> 不擦 B,继续运行 A
B 区擦除失败 -> A 仍然可用
B 区写入失败 -> A 仍然可用
B 区 CRC 错 -> A 仍然可用

这就是 A/B 的价值。

如果升级时写当前活动分区,就退化成单 APP 分区了。

除非是特殊工厂模式,否则不建议允许写当前活动分区。

12. 新 APP 第一次启动为什么不能马上确认成功

很多人第一次做 A/B 会这样处理:

B 区 CRC 通过
马上把 B 区标记为 VALID
以后都启动 B

这还不够安全。

因为 CRC 通过只能说明:

Flash 里的 B 区数据和固件包一致

它不能说明:

B 区 APP 能正常初始化时钟
B 区 APP 能正常初始化外设
B 区 APP 没有启动后 HardFault
B 区 APP 没有因为参数格式变化卡死
B 区 APP 能跑到主循环

所以新 APP 第一次启动应该是 PENDING 状态。

流程:

BootLoader 校验 B 区通过
标记 B 区 PENDING
启动 B 区
B 区运行起来后,主动告诉 BootLoader:我启动成功
BootLoader 才把 B 区改为 VALID

如果 B 区启动失败,没有确认:

BootLoader 下次上电看到 B 还是 PENDING
如果超过尝试次数,就回滚到 A

这叫“试运行确认”。

13. APP 如何确认启动成功

APP 确认启动成功有几种方式。

13.1 APP 直接写 BootRecord

APP 运行稳定后,直接写记录区:

void App_ConfirmRunning(void)
{
BootRecord_t rec;

if (Boot_RecordLoad(&rec) != 0)
{
return;
}

if (rec.pending_slot == APP_CURRENT_SLOT)
{
rec.slot[APP_CURRENT_SLOT].state = SLOT_STATE_VALID;
rec.active_slot = APP_CURRENT_SLOT;
rec.pending_slot = 0xFFFFFFFFU;
rec.rollback_slot = 0xFFFFFFFFU;
rec.last_error = 0;

Boot_RecordSave(&rec);
}
}

这个方式简单,但要注意:

APP 必须知道记录区地址
APP 和 BootLoader 要共用记录结构
APP 写 Flash 时不能影响自己的代码区
APP 写记录区时也要考虑断电

13.2 APP 写确认标志,复位后由 BootLoader 确认

APP 不直接修改完整记录,只写一个确认标志。

例如:

APP_CONFIRM_MAGIC
current_slot
app_version
confirm_crc

然后软复位。

BootLoader 上电后看到确认标志,再更新 BootRecord。

这种方式的好处:

记录区主要由 BootLoader 管理
APP 写入逻辑更少
减少 APP 和 BootLoader 对记录区结构的耦合

缺点:

确认后需要复位一次
流程稍微多一步

13.3 APP 通过命令请求 BootLoader 确认

如果系统有内部通信机制,也可以让 APP 通过命令请求 BootLoader。

但大多数裸机 STM32 项目里,BootLoader 和 APP 不会同时运行,所以这种方式不常见。

实际工程里,我更推荐前两种。

如果项目简单,可以 APP 直接写 BootRecord。

如果可靠性要求高,可以 APP 写确认标志,复位后由 BootLoader 统一更新。

14. 回滚逻辑怎么设计

回滚只处理一种情况:

新 APP 试运行失败

不要把所有错误都叫回滚。

例如:

固件头检查失败:拒绝升级,不叫回滚
写 B 区失败:升级失败,继续用 A,不叫回滚
B 区 CRC 失败:升级失败,继续用 A,不叫回滚
B 区启动后没有确认:回滚到 A

回滚判断关键是 PENDING。

伪代码:

static void Boot_HandlePendingSlot(BootRecord_t *rec)
{
AppSlot_t pending = (AppSlot_t)rec->pending_slot;

if (pending > SLOT_B)
{
return;
}

if (rec->slot[pending].boot_count < BOOT_PENDING_MAX_TRY)
{
rec->slot[pending].boot_count++;
Boot_RecordSave(rec);
Boot_JumpToSlot(pending);
}

rec->slot[pending].state = SLOT_STATE_BAD;
rec->pending_slot = 0xFFFFFFFFU;
rec->last_error = BL_ERR_PENDING_NOT_CONFIRMED;

if (Boot_SlotIsBootable(rec, (AppSlot_t)rec->rollback_slot))
{
rec->active_slot = rec->rollback_slot;
Boot_RecordSave(rec);
Boot_JumpToSlot((AppSlot_t)rec->rollback_slot);
}

Boot_RecordSave(rec);
Boot_StayInBootloader();
}

BOOT_PENDING_MAX_TRY 可以设置为:

1 次:最严格,新 APP 第一次没确认就回滚
2 次:允许偶发复位
3 次:给复杂系统更多启动机会

不要无限尝试 PENDING 分区。

否则新 APP 一直 HardFault,设备会一直重启。

15. APP 向量表和链接地址

A/B 双分区最容易踩坑的是链接地址。

15.1 A/B 使用不同链接地址

如果 APP A 在:

APP_A_BASE = 0x08010000

APP B 在:

APP_B_BASE = 0x08040000

那么 APP A 和 APP B 的链接地址不同。

你需要分别编译两个 bin:

app_a.bin 链接地址 0x08010000
app_b.bin 链接地址 0x08040000

优点:

APP 运行时不需要重定位代码
BootLoader 直接跳对应地址

缺点:

同一份源码要生成两个固件
上位机要知道发 A 包还是 B 包
打包流程更复杂

15.2 A/B 使用相同链接地址

另一种方式是让 APP 始终按同一个地址链接,比如都按 APP_RUN_BASE。

但如果 B 区实际地址不同,就涉及重映射或拷贝运行。

在普通 STM32 内部 Flash 场景下,这样做会复杂很多。

所以多数项目更推荐:

A 区 APP 按 A 区地址链接
B 区 APP 按 B 区地址链接

15.3 如何减少打包复杂度

可以让打包工具根据目标 slot 生成固件包:

make app_a
pack –slot A –base 0x08010000 –input app_a.bin –output firmware_a.pkg

make app_b
pack –slot B –base 0x08040000 –input app_b.bin –output firmware_b.pkg

也可以在上位机连接设备后,先读取当前活动分区:

设备当前运行 A
上位机选择 B 包发送

设备当前运行 B
上位机选择 A 包发送

如果想让上位机只发一个固件包,那就要设计更复杂的可重定位 APP 或运行时重映射。普通项目不建议一开始就这样做。

15.4 VTOR 设置

APP 启动后仍然要设置自己的向量表。

APP A:

SCB->VTOR = 0x08010000;

APP B:

SCB->VTOR = 0x08040000;

如果同一份 APP 源码编译 A/B 两个版本,可以用宏:

#define APP_BASE_ADDR 0x08010000U

void SystemInit_User(void)
{
SCB->VTOR = APP_BASE_ADDR;
}

编译 B 版本时改成:

#define APP_BASE_ADDR 0x08040000U

不要忘记中断向量表。

A/B 分区能跳进去,不代表中断一定正常。

16. Flash 擦写注意事项

16.1 擦除范围必须限制在目标 slot

写 B 区时,绝对不能擦到 A 区。

擦除前检查:

static int Boot_CheckSlotRange(AppSlot_t slot, uint32_t addr, uint32_t len)
{
uint32_t base = Boot_GetSlotBase(slot);
uint32_t size = Boot_GetSlotSize(slot);

if (addr < base)
{
return 1;
}

if ((addr + len) > (base + size))
{
return 1;
}

return 0;
}

每次写入也检查。

不要只在开始时检查一次。

16.2 F407 要特别注意 Sector

F407 的 Sector 不均匀。

如果 APP A/B 不是按 Sector 边界切分,擦除一个 Sector 时可能把另一个分区擦掉。

例如:

Sector 5: 0x08020000 ~ 0x0803FFFF
Sector 6: 0x08040000 ~ 0x0805FFFF

APP A 放 Sector 5,APP B 放 Sector 6,就比较清楚。

不要这样:

APP A: 0x08020000 ~ 0x08047FFF
APP B: 0x08048000 ~ …

因为 0x08040000 ~ 0x0805FFFF 是一个完整 Sector,擦 APP B 前半部分时会影响 APP A 尾部。

16.3 记录区不要和 APP 共用擦除页

记录区必须独立。

不要让最后一页既放 APP,又放记录。

否则写记录时可能擦掉 APP 尾部。

正确做法:

APP 分区结束地址对齐到 page/sector
记录区单独占用 page/sector

16.4 写入完成后读回校验

写 Flash 后可以分两级校验:

写入一小段后读回比较
整包写完后 CRC32 校验

读回比较能更早发现 Flash 写入问题。

整包 CRC32 是最终判断。

17. A/B 分区状态机

A/B 双分区下,状态机比单 APP 多几个状态。

typedef enum
{
BL_STATE_INIT = 0,
BL_STATE_LOAD_RECORD,
BL_STATE_SELECT_SLOT,
BL_STATE_WAIT_TRIGGER,

BL_STATE_RECV_HEADER,
BL_STATE_CHECK_HEADER,
BL_STATE_SELECT_TARGET_SLOT,
BL_STATE_ERASE_TARGET_SLOT,
BL_STATE_WRITE_TARGET_SLOT,
BL_STATE_VERIFY_TARGET_SLOT,
BL_STATE_MARK_PENDING,

BL_STATE_JUMP_SLOT,
BL_STATE_WAIT_CONFIRM,
BL_STATE_ROLLBACK,
BL_STATE_ERROR,
} BootState_t;

升级流程:

LOAD_RECORD
SELECT_SLOT
WAIT_TRIGGER
RECV_HEADER
CHECK_HEADER
SELECT_TARGET_SLOT
ERASE_TARGET_SLOT
WRITE_TARGET_SLOT
VERIFY_TARGET_SLOT
MARK_PENDING
JUMP_SLOT

上电启动流程:

LOAD_RECORD
SELECT_SLOT
如果有 PENDING 且未超过次数 -> JUMP_SLOT
如果 PENDING 超过次数 -> ROLLBACK
如果 active_slot 有效 -> JUMP_SLOT
否则停留 BootLoader

APP 确认流程:

APP 写确认标志
系统复位
BootLoader 读取确认标志
把 pending_slot 改为 active_slot
清除 pending_slot
启动该分区

错误处理:

固件头检查失败:
不修改 active_slot
不擦目标分区

目标分区写入失败:
标记目标分区 BAD
active_slot 不变

目标分区 CRC 失败:
标记目标分区 BAD
active_slot 不变

PENDING 启动失败:
标记 pending 分区 BAD
回滚 rollback_slot

可以看到,A/B 分区下,大部分升级失败都不影响当前可用 APP。

这正是它的意义。

18. 常见问题

18.1 A/B 双分区是不是一定能防止变砖

不是。

它能降低升级失败导致不可运行的概率,但不是万能。

下面这些情况仍然危险:

BootLoader 自己被破坏
记录区两份都损坏
Flash 硬件故障
APP A 和 APP B 都无效
固件包写错目标芯片
APP 确认逻辑写错

BootLoader 自己所在区域仍然要重点保护。

所以 BootLoader 还是应该放在 0x08000000 前面固定区域,并且 APP 升级时绝对不能擦写 BootLoader 区。

18.2 F103C8T6 能不能做 A/B

能做,但不推荐做完整 A/B。

64KB Flash 太紧。

除非 APP 很小,BootLoader 也很小,否则很快就不够。

更建议:

换大 Flash 型号
使用外部 SPI Flash 存新固件
继续使用单 APP 分区 + 完整异常恢复

18.3 APP A 和 APP B 要不要完全一样

源码可以一样,但链接地址通常不同。

也就是:

同一份源码
两套链接脚本
两个输出 bin
两个固件包

不要把按 A 地址链接的固件写到 B 区。

除非你做了可重定位设计。

18.4 新 APP 多久确认成功合适

不要一进 main() 就确认。

建议至少等关键初始化完成:

时钟初始化完成
外设初始化完成
参数加载完成
通信任务启动完成
主循环或关键任务已运行

如果项目有联网功能,可以等网络服务启动后再确认。

如果项目有电机、阀门、加热等危险输出,确认点要放在安全状态建立之后。

18.5 PENDING 尝试次数设置多少

常见设置:

1 次:最稳,失败立即回滚
2 次:允许一次偶发复位
3 次:复杂系统可用,但不要再多

不要无限重试。

18.6 回滚后要不要删除新 APP

可以不删除。

只要把新 APP 所在 slot 标记为 BAD,BootLoader 不再启动它即可。

下次升级时,目标 slot 可以重新擦除覆盖。

保留 BAD 分区还有一个好处:

现场可以读取失败固件信息
分析版本号、CRC、错误码

18.7 记录区写坏了怎么办

用双记录。

两条都坏时,不要冒险跳 APP。

可以扫描 A/B 分区:

检查 A 区向量表和 CRC
检查 B 区向量表和 CRC
选择版本较高或固定优先级的分区
重新生成记录区

但这个恢复策略要谨慎。

如果没有足够信息,宁可停在 BootLoader。

18.8 能不能做三分区

可以。

比如:

APP A
APP B
Factory APP

Factory APP 是一个最小维护固件,用来恢复网络、串口、参数和升级功能。

但 Flash 空间会更紧,逻辑也更复杂。

先把 A/B 做稳,再考虑 Factory 分区。

19. 总结

A/B 双分区解决的是单 APP 分区最大的痛点:升级过程中不破坏当前可用 APP,新 APP 启动失败后可以回滚。

这篇的核心点:

  • 单 APP 分区在擦除 APP 后无法保留旧程序
  • A/B 分区升级时永远写非活动分区
  • 新固件写入并 CRC 通过后,先标记为 PENDING
  • 新 APP 运行成功后,必须主动确认
  • 没有确认的新 APP 不能长期作为有效 APP
  • PENDING 启动失败后回滚到旧分区
  • BootRecord 要保存 active、pending、rollback、slot 状态、版本、CRC
  • 记录区建议做双记录,防止写记录时断电
  • F103C8T6 Flash 太小,不推荐完整 A/B
  • F407VET6 要按 Sector 边界规划
  • G473VET6 做 A/B 相对更舒服,但仍要注意页对齐
  • A/B 的 APP 链接地址要处理清楚,A 包和 B 包不能混写

做到 A/B 双分区后,BootLoader 就从“升级失败后等人救”变成了“升级失败后尽量自己恢复”。这对现场设备、远程升级、无人值守设备都很有价值。

文章标签

STM32
BootLoader
IAP
A/B分区
双分区升级
失败回滚
固件升级
Flash
YMODEM
W5500
嵌入式
单片机

赞(0)
未经允许不得转载:171主机测评 » STM32 BootLoader 实战(八):A/B 双分区升级、启动选择与失败回滚设计
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址