STM32F103_AB_OTA_从零复现教程

STM32F103 A/B 双分区 OTA:从零复现到使用上位机

本教程完全按照仓库现有代码讲解,以 Windows 11 + STM32F103RET6 + ST-Link + 3.3 V USB-TTL 为主线。这里常说"OTA",严格讲当前实现是 UART IAP;接入无线透传模块后,它才成为更大系统中的空中升级链路。

目录

  1. 先把几个概念分清
  2. 硬件、软件与芯片核验
  3. [读懂工程目录和 Flash 地图](#读懂工程目录和 Flash 地图 "#3-%E8%AF%BB%E6%87%82%E5%B7%A5%E7%A8%8B%E7%9B%AE%E5%BD%95%E5%92%8C-flash-%E5%9C%B0%E5%9B%BE")
  4. [Cortex-M3 为什么能从 Bootloader 跳到 App](#Cortex-M3 为什么能从 Bootloader 跳到 App "#4-cortex-m3-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%83%BD%E4%BB%8E-bootloader-%E8%B7%B3%E5%88%B0-app")
  5. [为什么同一 App 必须生成 app_a 和 app_b](#为什么同一 App 必须生成 app_a 和 app_b "#5-%E4%B8%BA%E4%BB%80%E4%B9%88%E5%90%8C%E4%B8%80-app-%E5%BF%85%E9%A1%BB%E7%94%9F%E6%88%90-app_a-%E5%92%8C-app_b")
  6. [构建 Bootloader 和两个 App](#构建 Bootloader 和两个 App "#6-%E6%9E%84%E5%BB%BA-bootloader-%E5%92%8C%E4%B8%A4%E4%B8%AA-app")
  7. [第一次用 ST-Link 部署](#第一次用 ST-Link 部署 "#7-%E7%AC%AC%E4%B8%80%E6%AC%A1%E7%94%A8-st-link-%E9%83%A8%E7%BD%B2")
  8. [连接串口并进入 OTA 模式](#连接串口并进入 OTA 模式 "#8-%E8%BF%9E%E6%8E%A5%E4%B8%B2%E5%8F%A3%E5%B9%B6%E8%BF%9B%E5%85%A5-ota-%E6%A8%A1%E5%BC%8F")
  9. [使用 Qt 图形上位机完成 A→B](#使用 Qt 图形上位机完成 A→B "#9-%E4%BD%BF%E7%94%A8-qt-%E5%9B%BE%E5%BD%A2%E4%B8%8A%E4%BD%8D%E6%9C%BA%E5%AE%8C%E6%88%90-ab")
  10. 验证后确认新固件
  11. [完成 B→A 反向升级](#完成 B→A 反向升级 "#11-%E5%AE%8C%E6%88%90-ba-%E5%8F%8D%E5%90%91%E5%8D%87%E7%BA%A7")
  12. 亲手验证四次复位回滚
  13. [协议、CRC 和 Metadata 原理](#协议、CRC 和 Metadata 原理 "#13-%E5%8D%8F%E8%AE%AEcrc-%E5%92%8C-metadata-%E5%8E%9F%E7%90%86")
  14. 命令行工具用法
  15. 常见故障排查
  16. 当前边界与量产化路线
  17. 代码导览与参考资料

1. 先把几个概念分清

1.1 三种"程序"

名称 存放位置 谁提供 本项目如何进入
STM32 ROM/System Memory Bootloader 芯片系统存储区 ST 出厂固化 由 BOOT 配置进入,协议见 AN2606/AN3155
本项目 Bootloader 用户 Flash 0x08000000 本仓库 MCU 每次复位先运行;首字节收到 U 进入升级循环
Application Slot A 或 Slot B 你的业务代码 Bootloader 校验向量表后跳转

本项目自定义帧以 AA 55 开头,不是 ST ROM Bootloader 的 AN3155 协议。STM32CubeProgrammer 可以用于第一次通过 ST-Link 烧录,但 UART 上位机与 CubeProgrammer 不是同一种协议。

1.2 UART IAP 与联网 OTA

当前数据链路是:

text 复制代码
Windows 上位机 ── USB-TTL / USART1 ──> 用户 Flash Bootloader

它属于 IAP(In-Application Programming)方案。若未来让 4G/Wi-Fi/蓝牙模块透明传输完全相同的字节流,才可以从系统角度称为 OTA。仓库目前没有 TCP、HTTP、MQTT 或无线驱动。

1.3 逻辑 A/B 不等于硬件 Dual-Bank

STM32F103xE 的这里没有使用硬件 Bank Swap。Slot A、Slot B 是同一片 Flash 中的两段地址,Bootloader 通过 SCB->VTOR、MSP 和 Reset_Handler 手动选择运行哪一段。

1.4 CRC 不是签名

  • CRC16:发现单个串口帧传输误码;
  • CRC32:比较完整固件在 PC 和 Flash 中是否一致;
  • 它们都不能抵御主动篡改,也不能验证发布者身份。

量产设备还需要签名、公钥验证、版本策略和密钥管理。


2. 硬件、软件与芯片核验

2.1 推荐硬件清单

  • STM32F103RET6 最小系统板或自制板;
  • ST-Link V2/V3;
  • 3.3 V USB-TTL(例如 CP2102、CH340、FT232 的 TTL 模式);
  • 杜邦线;
  • 稳定的 3.3 V 供电;
  • 可选:SSD1306 OLED 模块,用于 07_ret6_oled 示例。

不要把传统 ±RS-232 电平直接接到 PA9/PA10。USB-TTL 必须使用 3.3 V 逻辑电平,并与 MCU 共地。

2.2 RET6 与目录中的 ZET6

bootloaderapp/app07_ret6_oled 三个 .ioc 当前选择的实际器件均为 STM32F103RET6 / LQFP64。本教程据此编写。

核心容量前提:

text 复制代码
Flash: 0x08000000 ~ 0x0807FFFF = 512 KiB
SRAM : 0x20000000 ~ 0x2000FFFF = 64 KiB
SRAM 顶端/尾后地址              = 0x20010000

初始 MSP 等于 0x20010000 是合法的,因为栈从高地址向低地址生长。README 旧版本把这个范围写成 128 KiB 是错误的。

若你的芯片是 ZET6:

  1. 用 CubeMX 打开每个 .ioc
  2. 将器件改为实物型号和 LQFP144 封装;
  3. 重新核对 LED、USART、OLED、晶振等引脚;
  4. 不要只因 Flash/SRAM 容量相同就直接沿用 RET6 的板级接线。

2.3 Windows 软件

建议安装:

  • STM32CubeProgrammer
  • Arm GNU Toolchain:确保 arm-none-eabi-gccarm-none-eabi-objcopy 在 PATH
  • CMake 3.22 或更高版本
  • Ninja
  • Python 3.10 或更高版本
  • 可选:CLion 或 VS Code

在 PowerShell 中核验:

powershell 复制代码
arm-none-eabi-gcc --version
arm-none-eabi-objcopy --version
cmake --version
ninja --version
py --version

安装上位机依赖:

powershell 复制代码
cd <仓库目录>
py -m pip install -r tools\requirements.txt

requirements.txt 默认安装 PySerial 和 PySide6。GUI 代码也兼容已安装的 PyQt5/PySide2,但教程以 PySide6 为准。


3. 读懂工程目录和 Flash 地图

text 复制代码
stm32_ab_ota/
├── bootloader/                 用户 Flash Bootloader
│   └── Core/Src/
│       ├── main.c              OTA 入口与正常启动状态机
│       ├── boot_protocol.c     AA55 帧和命令处理
│       ├── boot_flash.c        页擦除、半字写入与分区边界
│       ├── boot_jump.c         镜像检查与跳转
│       ├── boot_metadata.c     Metadata 读写
│       └── boot_crc.c          CRC16 / CRC32
├── app/app/                    LED App,同一源码链接成 A/B 两份
├── 07_ret6_oled/               可选 OLED App,固定链接到 Slot B
├── tools/
│   ├── ota.py                  协议实现与 CLI
│   └── ota_gui.py              Qt 图形上位机
└── images/                     教程流程图

3.1 Flash 分区

图 1:A/B 是同一 Flash 中的逻辑槽位,不是两个硬件 Bank。

分区 起始地址 结束地址(含) 大小
Bootloader 0x08000000 0x08007FFF 32 KiB
Slot A 0x08008000 0x0803FFFF 224 KiB
Slot B 0x08040000 0x08077FFF 224 KiB
Metadata 预留 0x08078000 0x0807FFFF 32 KiB

常量来自 bootloader/Core/Inc/boot_config.h。Metadata 虽预留 32 KiB,当前 boot_metadata_save() 只擦写首个 2 KiB 页中的 28 字节,剩余空间尚未用于双副本或磨损均衡。

3.2 为什么每槽是 224 KiB

text 复制代码
512 KiB - 32 KiB Bootloader - 32 KiB Metadata = 448 KiB
448 KiB / 2 = 224 KiB

F103xE Flash 页是 2 KiB,所以所有分区起点都应页对齐。


4. Cortex-M3 为什么能从 Bootloader 跳到 App

4.1 向量表前 8 字节

图 2:每个可启动镜像开头都有自己的向量表。

Cortex-M3 镜像的前两个 32 位数:

text 复制代码
offset +0x00: 初始 MSP(栈顶)
offset +0x04: Reset_Handler 地址,最低位必须为 1(Thumb 状态)

本项目的 boot_app_is_valid() 检查:

  1. 起始地址必须精确等于 Slot A 或 Slot B;
  2. 镜像至少 8 字节且不越过槽位;
  3. MSP 位于 64 KiB SRAM 范围且 8 字节对齐;
  4. Reset_Handler 带 Thumb 位;
  5. 清除 Thumb 位后的地址位于这份镜像范围内。

4.2 跳转步骤

图 3:Bootloader 将 CPU 执行环境交给目标 App。

boot_jump_to_app() 的关键顺序:

c 复制代码
__disable_irq();
HAL_DeInit();
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL  = 0;
SCB->VTOR = app_address;
__DSB();
__ISB();
__set_MSP(app_stack);
((app_entry_t)app_reset_handler)();

App 也会用链接器符号再次设置自己的 VTOR。LED App 已按以下顺序恢复:

c 复制代码
SCB->VTOR = (uint32_t)&g_pfnVectors;
__DSB();
__ISB();
__enable_irq();

必须最后重新打开全局中断,否则 Bootloader 留下的 PRIMASK=1 会使 SysTick 不运行,HAL_Delay() 看起来像"卡死"。

当前 C 跳转函数没有完整清除所有 NVIC enable/pending 状态,也没有使用汇编尾跳。这不妨碍当前教学复现,但量产项目应继续加固。


5. 为什么同一 App 必须生成 app_a 和 app_b

STM32F103 没有 MMU 帮程序在运行时整体搬家。链接器会按照 FLASH ORIGIN 安排向量表、函数和常量地址,因此同一源码必须分别链接。

图 4:app_aapp_b 是相同源码、不同链接地址的两个构建目标。

Slot A 链接脚本:

ld 复制代码
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 224K

Slot B 链接脚本:

ld 复制代码
FLASH (rx) : ORIGIN = 0x08040000, LENGTH = 224K

app/app/CMakeLists.txt 实际定义:

cmake 复制代码
add_executable(app_a)
add_executable(app_b)

然后分别绑定 STM32F103xx_FLASH.ldSTM32F103xx_FLASH_B.ld。这里没有 使用 -DBOOT_SLOT=A/B

5.1 app_b 不是 OLED

  • app_a.bin:LED App 链接到 Slot A;
  • app_b.bin:同一 LED App 链接到 Slot B;
  • 07_ret6_oled.bin:另一个独立 OLED 工程,链接到 Slot B,只是可选演示镜像。

先用 LED A/B 打通完整链路,再尝试 OLED,排错更简单。


6. 构建 Bootloader 和两个 App

以下命令均从仓库根目录执行。

6.1 构建 Bootloader

powershell 复制代码
cmake --preset Debug -S bootloader
cmake --build bootloader\build\Debug --target bootloader

这里直接指定 preset 生成的构建目录,因此这些命令都可以停留在仓库根目录执行。也可以先 cd bootloader,再运行 cmake --preset Debugcmake --build --preset Debug --target bootloader

预期输出目录:

text 复制代码
bootloader/build/Debug/
├── bootloader.elf
├── bootloader.hex
└── bootloader.bin

6.2 构建 app_a 与 app_b

powershell 复制代码
cmake --preset Debug -S app\app
cmake --build app\app\build\Debug --target app_a app_b

预期产物:

text 复制代码
app/app/build/Debug/
├── app_a.elf / app_a.hex / app_a.bin / app_a.map
└── app_b.elf / app_b.hex / app_b.bin / app_b.map

必须显式构建 app_b,不能因为目录里已有旧 app_a.bin 就认为两个目标都构建过。

6.3 可选:构建 OLED Slot B 示例

powershell 复制代码
cmake --preset Debug -S 07_ret6_oled
cmake --build 07_ret6_oled\build\Debug --target 07_ret6_oled

OLED 软件 I²C 的 SCL/SDA 都配置为开漏。模块或板上必须有上拉电阻;否则总线无法可靠释放和读取 ACK。

6.4 检查 ELF / map / BIN

objdump 查看段地址:

powershell 复制代码
arm-none-eabi-objdump -h app\app\build\Debug\app_a.elf
arm-none-eabi-objdump -h app\app\build\Debug\app_b.elf

你应看到向量表或 .text 从对应槽位附近开始:

text 复制代码
app_a: 0x08008000
app_b: 0x08040000

用 Python 查看 BIN 前 8 字节:

powershell 复制代码
py -c "import struct,pathlib; p=pathlib.Path(r'app\app\build\Debug\app_a.bin'); d=p.read_bytes(); print(p,len(d),[hex(x) for x in struct.unpack('<II',d[:8])])"
py -c "import struct,pathlib; p=pathlib.Path(r'app\app\build\Debug\app_b.bin'); d=p.read_bytes(); print(p,len(d),[hex(x) for x in struct.unpack('<II',d[:8])])"

检查规则:

  • 第一个值通常接近 0x20010000,必须在 SRAM 合法范围;
  • 第二个值最低位是 1;清除最低位后,A 镜像地址在 A 的实际镜像范围,B 镜像地址在 B 的实际镜像范围;
  • BIN 总长度必须为偶数且不超过 224 KiB。

最少连接:

text 复制代码
ST-Link SWDIO -> MCU SWDIO
ST-Link SWCLK -> MCU SWCLK
ST-Link GND   -> MCU GND
目标板供电按你的硬件方案连接

BOOT0 保持正常从用户 Flash 启动的状态(通常下拉为 0)。

7.2 推荐用 HEX 首次烧录

.hex 自带目标地址,最不容易填错:

  1. 连接 ST-Link;
  2. 如需清理旧工程,可在首次部署时执行一次整片擦除;
  3. 下载 bootloader.hex
  4. 下载 app_a.hex
  5. 复位。

7.3 用 BIN 时必须填写地址

文件 CubeProgrammer Start address
bootloader.bin 0x08000000
app_a.bin 0x08008000
app_b.bin 0x08040000
07_ret6_oled.bin 0x08040000

图 5:只更新 App 时不要再执行 Full Chip/Mass Erase。

烧 App 时若勾选 Mass Erase,0x08000000 的 Bootloader 也会消失,CPU 复位后无法启动。遇到"串口没有 banner、App 也不运行",优先重新烧录 Bootloader + Slot A。


8. 连接串口并进入 OTA 模式

8.1 接线

text 复制代码
STM32 PA9  / USART1_TX  ----> USB-TTL RX
STM32 PA10 / USART1_RX  <---- USB-TTL TX
STM32 GND                ----- USB-TTL GND

串口参数固定为:

text 复制代码
115200 baud, 8 data bits, no parity, 1 stop bit, no flow control

GUI 现在只显示 115200,因为 Bootloader 源码没有动态波特率协商。

8.2 入口的关键细节

boot_wait_for_ota() 调用一次阻塞接收:

c 复制代码
HAL_UART_Receive(&huart1, &command, 1, 2000)

只有第一个收到的字节 恰好为 ASCII U 才进入 OTA。它不是在 2 秒数据流中不停搜索 U。若第一字节是噪声或串口工具留下的字符,后面的 U 不会补救这次启动。

Python 上位机每 50 ms 重复发送 U,持续最多 10 秒。正确操作:

  1. 先打开串口并启动"连接设备";
  2. 看到"请复位"提示后立即按 RESET 或重新上电;
  3. 等待日志出现:
text 复制代码
OK: Enter OTA mode

进入 OTA 循环后没有空闲超时,不发送 BOOT/SWITCH 就不会自动回到 App。

图 6:TX/RX 交叉连接,GND 必须共地。


9. 使用 Qt 图形上位机完成 A→B

9.1 启动 GUI

powershell 复制代码
cd <仓库目录>\tools
py ota_gui.py

操作顺序:

  1. 点击"刷新",选择 USB-TTL 对应 COM 口;
  2. 波特率保持 115200;
  3. 点击"打开串口";
  4. 点击"连接设备";
  5. 按提示复位 MCU;
  6. 进入 OTA 后点击"查询信息"。

期望 INFO 内容:

text 复制代码
Bootloader 版本: 0.1
Flash 总大小: 512 KB
Slot A: 0x08008000 (224 KB)
Slot B: 0x08040000 (224 KB)

GUI 每次真正开始升级时还会自动发送 INFO 并再次校验设备布局,所以即使没有预先手工点击"查询信息",布局不一致也会在擦除前被拒绝。

9.2 选择正确镜像

当前从 Slot A 升级到 Slot B:

text 复制代码
固件:app_b.bin
目标槽位:Slot B (0x08040000)
回滚槽位:GUI 推导为 Slot A (0x08008000)

上位机无法从 INFO 知道 MCU 当前运行哪个槽位。确认对话框出现时,你必须保证 Slot A 确实是已经验证可用的旧固件。如果刚改变过默认槽或手工烧过 Flash,不要机械点击"是"。

GUI 会在写入前检查:

  • 文件非空且至少 8 字节;
  • 文件不超过 224 KiB;
  • 长度为偶数;
  • 初始 MSP 在 64 KiB SRAM 内且 8 字节对齐;
  • Reset_Handler Thumb 位有效;
  • Reset_Handler 位于所选目标镜像范围。

9.3 上位机实际发送什么

图 7:VERIFY 请求只发送 address + size,设备返回 Flash CRC32,PC 再与本地值比较。

GUI 执行:

text 复制代码
1. ERASE(target, 向上取整后的页长度)
2. DATA(target + offset, 最多 240 字节数据) × N
3. VERIFY(target, 固件实际长度)
4. 比较设备返回 CRC32 与 zlib.crc32(data)
5. MARK_PENDING(target, confirmed, crc32, size),设备还会确认回滚槽含有基本可启动镜像
6. BOOT(target)

成功日志最后会提示 MCU 已离开 OTA 模式。此时 GUI 会把连接状态清除,这是正确行为:Bootloader 已经跳到新 App,不能直接继续发 CONFIRM。


10. 验证后确认新固件

图 8:当前项目没有 app_confirm_boot();确认命令来自 PC。

完整步骤:

  1. BOOT 后观察 Slot B 版本的 LED、OLED 或业务功能;

  2. 如果功能正确,回到 GUI 点击"确认新固件";

  3. GUI 先弹出说明;点击"确定"后,它才开始重复发送 U

  4. 看到日志中的"正在进入 OTA 模式"后立即按 RESET;

  5. Bootloader 进入 OTA 后接收 CMD_CONFIRM

  6. 它重新检查 PENDING 的两个槽位都可启动,并检查目标 size、向量表和 Flash CRC;

  7. 写入:

    text 复制代码
    confirmed_slot = pending_slot
    pending_slot   = 0
    state          = CONFIRMED
    boot_count     = 0
    crc/size       = 0
  8. GUI 日志显示确认成功。

注意:CONFIRM 本身不跳转。确认后 MCU 仍停留在 OTA 命令循环。可再发送 BOOT,或手动复位后不要发送 U,让正常状态机启动新 confirmed 槽。

BOOT、MARK_PENDING、CONFIRM、SWITCH 的区别

操作 是否写 Metadata 是否立即跳转 复位后的默认行为
BOOT(slot) 仍按原 Metadata
MARK_PENDING 写 PENDING 尝试 pending 并计数
升级流程 MARK_PENDING 后 BOOT 未确认时继续试 pending/最终回滚
CONFIRM PENDING→CONFIRMED 启动新 confirmed
SWITCH(slot) 直接写 CONFIRMED 默认启动指定槽

SWITCH 适合 A、B 都已知可用时人工选择默认槽;它没有完整固件 CRC 元数据,只做基本向量检查,因此不要把它当成安全升级确认流程。


11. 完成 B→A 反向升级

前提:Slot B 已通过上一节确认并能正常复位启动。

  1. 让 MCU 进入 Bootloader;
  2. GUI 选择 app_a.bin
  3. 目标槽位选 Slot A;
  4. 确认对话框中的回滚槽是 Slot B;
  5. 开始升级;
  6. App A 运行后验收;
  7. 点击"确认新固件",复位并重新进入 Bootloader;
  8. 确认成功后再复位验证。

不要把 app_b.bin 烧到 A,也不要把 app_a.bin 烧到 B。上位机会通过 Reset_Handler 地址拒绝这种错误。


12. 亲手验证四次复位回滚

图 9:直接 BOOT 不增加计数,之后三次普通复位各加 1,第四次回滚。

12.1 实验准备

假设当前:

text 复制代码
Slot A = confirmed 且功能正常
准备把 app_b.bin 升级到 Slot B

12.2 实验步骤

  1. 正常完成 A→B 升级,直到 GUI 显示已 BOOT 到新固件;
  2. 不要点击确认新固件
  3. 直接 BOOT 的首次试运行:boot_count 仍为 0;
  4. 第 1 次普通复位:不要发送 U,Bootloader 保存 0→1,运行 B;
  5. 第 2 次普通复位:保存 1→2,运行 B;
  6. 第 3 次普通复位:保存 2→3,仍运行 B;
  7. 第 4 次普通复位:一开始看到 count>=3,状态回到 CONFIRMED,并启动 A。

12.3 为什么 HardFault 不一定自动回滚

本项目没有启用 IWDG/WWDG,也没有在 HardFault 中自动复位。若 B 卡在死循环:

  • boot_count 不会在 App 中继续增加;
  • Bootloader 也不会自动重新运行;
  • 必须人工按 RESET、重新上电,或由板外复位电路触发复位。

所以当前机制更准确地叫"跨复位的未确认回滚状态机",不是完整的自动健康监测闭环。


13. 协议、CRC 和 Metadata 原理

13.1 自定义帧

图 10:没有 EOF 字段;55 是两字节帧头的第二个字节。

text 复制代码
[AA][55][CMD][SEQ][LEN_LO][LEN_HI][PAYLOAD...][CRC_LO][CRC_HI]
  • LEN:payload 长度,u16 小端;
  • payload 最大 256 字节;
  • CRC16 覆盖 CMD + SEQ + LEN_LO + LEN_HI + PAYLOAD
  • CRC16 本身按小端发送;
  • 响应复用请求 CMD 并回显 SEQ。

CRC16 参数:

text 复制代码
CRC-16/CCITT-FALSE
poly   = 0x1021
init   = 0xFFFF
refin  = false
refout = false
xorout = 0
"123456789" -> 0x29B1

13.2 命令表

命令 请求 payload 成功响应 payload
INFO 0x01 22 字节版本/Flash/A/B 布局
ERASE 0x02 address:u32 + size:u32 status:u8
DATA 0x03 address:u32 + data status:u8
VERIFY 0x04 address:u32 + size:u32 status:u8 + crc32:u32
BOOT 0x05 slot:u32 status:u8,随后跳转
CONFIRM 0x06 status:u8
MARK_PENDING 0x07 target + confirmed + crc32 + size status:u8
SWITCH 0x08 target:u32 status:u8,随后跳转

所有 u32 都是小端。MARK_PENDING 的字段顺序不能交换。

13.3 为什么 DATA 每块 240 字节

text 复制代码
payload 上限 = 256
DATA 地址     = 4
可用数据上限 = 252

代码选 240 而不是 252:它是偶数,满足 STM32F1 半字写入,也给协议保持整齐余量:

text 复制代码
4 + 240 = 244 <= 256

最后一块也必须是偶数,所以主机对奇数长度 BIN 直接拒绝,而不是偷偷填充并改变 CRC/size 语义。

13.4 整包 CRC32

算法是反射 CRC32:

text 复制代码
poly = 0xEDB88320
init = 0xFFFFFFFF
xorout = 0xFFFFFFFF
"123456789" -> 0xCBF43926

等价 Python:

python 复制代码
zlib.crc32(data) & 0xFFFFFFFF

VERIFY 请求中没有"期望 CRC"字段。Bootloader 根据 address + size 计算后返回,主机负责比较。

13.5 Metadata 与启动状态机

c 复制代码
typedef struct {
    uint32_t magic;
    uint32_t state;
    uint32_t confirmed_slot;
    uint32_t pending_slot;
    uint32_t boot_count;
    uint32_t crc;
    uint32_t size;
} boot_metadata_t;

图 11:只有 magic/state/槽位/size/向量/CRC/计数检查;没有按键、RAM magic 或 Metadata CRC。

图 12:状态属于 Metadata 全局记录,不是每个槽各自维护 EMPTY/INVALID 枚举。

状态机摘要:

text 复制代码
magic 无效              -> 尝试 Slot A
state = CONFIRMED       -> 启动合法 confirmed_slot
未知 state              -> 不按 PENDING 解释,回退尝试 Slot A
state = PENDING:
  pending/confirmed 槽地址非法或相同
                          -> 不修改 Metadata,尝试 confirmed;仅当 confirmed 不是 A 时再尝试出厂 Slot A
  pending size/向量非法   -> 保存 CONFIRMED 并回滚
  Flash CRC 不匹配         -> 保存 CONFIRMED 并回滚
  count >= 3              -> 保存 CONFIRMED 并回滚
  否则                    -> 保存 count+1,再启动 pending

这里要区分两类校验范围:普通复位进入上述 PENDING 分支时,槽位关系合法后只对 pending 镜像 做 size、向量和 CRC 校验;MARK_PENDINGCONFIRM 命令则会额外检查 pending 与 confirmed 两个槽位的向量,确保试运行目标和回滚候选都具备基本可启动条件。

升级命令本身的 MARK_PENDING + BOOT 是直接跳转,不经过上述 count+1 路径。

13.6 Flash 边界保护

Bootloader 现在有两层边界:

  1. 协议层 ERASE 只允许从 A/B 槽首地址开始,DATA/VERIFY 的完整区间必须在单一槽中;
  2. 底层 Flash 接口只放行单一 Slot A、Slot B 或 Metadata 分区,禁止跨区、Bootloader 和 Flash 外地址。

这能阻止错误包直接擦写 Bootloader 或跨槽,但不能判断目标是不是"当前活动槽"。后者仍由操作者负责。


14. 命令行工具用法

查看帮助:

powershell 复制代码
cd tools
py ota.py --help

A→B:

powershell 复制代码
py ota.py ..\app\app\build\Debug\app_b.bin `
  --port COM5 `
  --target 0x08040000 `
  --confirmed 0x08008000

B→A:

powershell 复制代码
py ota.py ..\app\app\build\Debug\app_a.bin `
  --port COM5 `
  --target 0x08008000 `
  --confirmed 0x08040000

命令运行后,在 10 秒内复位 MCU。CLI 会:

  • 强制当前实现的 115200;
  • 验证两个槽地址不同;
  • 验证 BIN 长度、MSP、Thumb 位和 Reset_Handler;
  • 查询 INFO 并校验设备布局;
  • 检查每个响应的 CMD 和 SEQ;
  • 完成 PENDING + BOOT。

CLI 当前只负责升级,不单独提供 confirm 子命令。确认请使用 GUI,或根据协议自行发送 CMD_CONFIRM


15. 常见故障排查

现象 最可能原因 排查/解决
没有 banner、LED 也不运行 Mass Erase 擦掉 Bootloader 用 ST-Link 重烧 bootloader.hexapp_a.hex
GUI 找不到 COM 口 驱动、线材或端口占用 设备管理器确认端口;关闭串口助手;点击刷新
无法进入 OTA TX/RX 未交叉、未共地、波特率错误、复位时机不对 PA9→RX、PA10←TX、115200;先点连接再按 RESET
偶尔进不去 第一个接收字节是噪声/旧数据 关闭其他串口软件,清输入缓存,缩短接线,重新复位
ERASE 返回失败 地址不是槽首、长度为 0/越界 只选 GUI 的 A/B 槽;确认 INFO 布局一致
DATA 最后一包失败 BIN 长度是奇数 修正链接/objcopy 产物;主机现在会提前拒绝
"固件与槽位不匹配" app_a.bin 选给 B,或反之 A 用 app_a.bin,B 用 app_b.bin
MSP 无效 镜像不是 Cortex-M App、芯片/链接脚本不对或文件损坏 检查 BIN 前 8 字节和链接 map
VERIFY CRC 不一致 传输/Flash 写入错误,或两端 CRC 范围不一致 CRC 范围必须是固件实际 size;重试并检查供电
升级后直接 CONFIRM 超时 MCU 已在 App,不在 Bootloader 先验收,再点击确认并按 RESET 重新进入 OTA
CONFIRM status=1 Metadata 不是 PENDING,或 Flash/向量/CRC 已变化 重新执行完整升级,不要在 PENDING 后手工改槽内容
新 App 运行后 HAL_Delay 卡死 App 未恢复全局中断 VTOR + DSB/ISB 后调用 __enable_irq()
三次复位后还在新固件 直接 BOOT 不计数,第三次只把计数变 3 第四次普通复位才回滚
App 卡死却不自动回滚 未启用 watchdog/自动复位 人工复位;量产版增加健康检查和 IWDG
OLED 无 ACK/花屏 SDA 推挽、无上拉或接线错误 SCL/SDA 开漏,确认模块/板上上拉及地址
ZET6 板引脚不对 教程按 RET6/LQFP64 在 CubeMX 按 ZET6/LQFP144 重新映射并核对原理图

16. 当前边界与量产化路线

16.1 当前已经做到

  • A/B 两个独立链接镜像;
  • 自定义串口帧、CRC16、整包 CRC32;
  • 单槽边界擦写保护;
  • PENDING、CONFIRMED 和跨复位尝试计数;
  • 主机侧镜像向量检查;
  • 人工重新进入 Bootloader 后确认;
  • 不确认时第四次普通复位回滚。

16.2 当前没有做到

  • 固件数字签名、认证、加密;
  • anti-rollback 版本策略;
  • 数据包重试、窗口、去重、断点续传;
  • App 自确认 API;
  • IWDG/WWDG 健康闭环;
  • Metadata CRC、双副本、事务提交和掉电原子性;
  • 自动识别当前活动槽;
  • 完整 NVIC/控制寄存器清理与汇编尾跳;
  • 网络下载、TLS、设备身份和密钥轮换。

16.3 推荐量产演进顺序

  1. 为 Metadata 增加双页副本、序列号、结构 CRC 和提交标志;
  2. 引入固件头:magic、硬件型号、版本、长度、hash、签名;
  3. 在 Bootloader 中做公钥签名验证和最低版本策略;
  4. 增加可重试、可去重、可续传协议;
  5. 用 IWDG + App 健康检查形成自动回滚闭环;
  6. 增加 App 请求重启进 Bootloader 的安全通道;
  7. 做断电注入、Flash 故障、模糊帧、重复包和边界测试。

可借鉴 MCUboot/wolfBoot 的设计思想,但不要在未移植代码时声称"项目已经使用 MCUboot/wolfBoot"。


17. 代码导览与参考资料

17.1 从哪几个函数开始读

主题 文件 / 函数
分区常量 bootloader/Core/Inc/boot_config.h
复位后的分流 bootloader/Core/Src/main.cmain()boot_run()
U 入口 bootloader/Core/Src/boot_uart.cboot_wait_for_ota()
帧收发 bootloader/Core/Src/boot_protocol.cboot_receive_frame()boot_send_frame()
命令语义 同文件 handle_erase/data/verify/mark_pending/boot/confirm/switch
Flash 驱动 bootloader/Core/Src/boot_flash.c
App 校验/跳转 bootloader/Core/Src/boot_jump.c
Metadata bootloader/Core/Src/boot_metadata.cboot_metadata.h
CRC bootloader/Core/Src/boot_crc.c
双链接 app/app/CMakeLists.txt 与两份 linker script
Python 协议 tools/ota.py
Qt 工作流 tools/ota_gui.py

17.2 官方资料

17.3 开源项目与工具

这些资料用于学习通用方法。它们的协议、镜像格式、安全模型和本仓库不同,不能直接混称。


实板验收记录模板

完成实验后建议保存:

text 复制代码
芯片丝印:
开发板/原理图版本:
Bootloader commit/日期:
COM 号:
ST-Link/CubeProgrammer 版本:
app_a.bin 大小与 CRC32:
app_b.bin 大小与 CRC32:
A→B 日志:
CONFIRM 日志:
B→A 日志:
未确认回滚四次复位现象:
LED/OLED 实际行为:
异常与处理:

本地静态检查和构建可以证明代码、地址与产物一致;真正的串口时序、供电稳定性和外设现象仍需要在你的实板上完成这一份记录。

相关推荐
Freak嵌入式14 分钟前
一文搞懂 MCP4725 波形发生器:MicroPython 代码实现、定时器驱动与调试流程全梳理
嵌入式
慧都小项40 分钟前
汽车工具链重复测试太耗人?用 TestComplete 自动跑
功能测试·回归·汽车·嵌入式·软件工程·测试覆盖率
Discipline~Hai2 小时前
Linux网络编程02-TCP协议
linux·网络·tcp/ip·嵌入式·linux网络编程
Jason_zhao_MR3 小时前
电能质量监测为什么需要PTP精确时钟同步
linux·单片机·嵌入式硬件·嵌入式
Freak嵌入式4 小时前
全网最详细|树莓派 Pico ADC 软件控制全解析:从machine.ADC API 到定时温度采集实战(含 ISR 避坑指南)
嵌入式
嵌入式阿蔡7 小时前
存储_14:嵌入式存储测试岗面试——可靠性/兼容/车规面试题与项目话术
单片机·嵌入式硬件·面试·嵌入式·存储
Freak嵌入式1 天前
嵌入式 MQTT 项目实战:Pico+MicroPython 向 EMQX 发布 JSON 消息全解析
嵌入式
Freak嵌入式1 天前
MicroPython 开发:树莓派 Pico RTC 控制方法与 NTP 时间同步实现
嵌入式
Freak嵌入式1 天前
一篇搞懂:MicroPython urequests库与 Pico HTTP 客户端实现
嵌入式