ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GD32F103 IAP串口升级:Bootloader+App分区架构与避坑指南

GD32F103 IAP串口升级:Bootloader+App分区架构与避坑指南

简介:面向嵌入式单片机开发者,这份GD32F103 IAP升级源代码提供了一套完整的固件在线升级(IAP)实现方案。由于GD32与STM32同属Cortex-M3内核,IAP流程与Flash地址映射基本一致,因此对STM32用户同样具有直接参考价值。压缩包共164个文件,以C源文件(61个)和头文件(75个)为主体,辅以Keil工程文件(uvprojx)、链接脚本(sct)、烧录脚本(bat)以及编译生成的hex/bin文件,整包仅624KB,结构清晰。已有5180人学习浏览,适合正在做Bootloader或远程升级方案的开发者参考。源码给出了明确的Flash空间划分:Bootloader占用30KB(0x8000000-0x80077ff),用户程序区占34KB(0x8007800-0x800ffff),并按1KB扇区管理;同时集成GD32F10x标准外设库中的常用模块(如enet、timer、rcu),便于在此基础上修改实现自定义升级协议与跳转逻辑。

1. GD32F103 IAP 升级源代码:串口刷固件,不用再拆机插下载器

做嵌入式硬件的人迟早要碰 IAP,GD32F103 的 IAP 升级源代码解决的是"产品已经装进设备,固件怎么迭代"的问题。这套代码把 Bootloader + App 双区架构做成可改参复用的模板:出厂用下载器只烧一次引导程序,之后升级全走串口,单片机自己擦 Flash、写 Flash、校验、跳转。对做单片机产品的人,省的是反复拆机插调试器的工时;对刚入门 ARM 的新手,它把向量表跳转、Flash 擦写、帧协议这些概念一次性落到能跑的代码上。手上有 GD32F103 开发板,改改分区地址和波特率就能在自己的项目里复现这条链路。

2. Flash 分区与跳转逻辑:地址划不对,后面全是坑

2.1 分区方案:32KB Bootloader、App 从 0x08008000 起

GD32F103 的片内 Flash 常见容量从 64KB 到 512KB,我见过的大多数产品把 Bootloader 划 32KB,App 从 0x08008000 开始。Bootloader 本身只有串口驱动、FMC 擦写和帧协议,优化后一般 8~12KB,划 32KB 看着浪费,但有两个实际理由:

  • 页对齐。GD32F103 的 Flash 页尺寸按型号分,小容量芯片是 1KB/页,大容量是 2KB/页。Bootloader 分区取页大小的整数倍,擦除边界才干净,不会出现两个区共用一页、擦 App 时把 Bootloader 最后几字节带走的情况。
  • 功能余量。后续要加 CAN 升级、固件加密校验、回滚存储时,Bootloader 代码量会涨,一次定够比后期挪分区省事得多。

分区方案可以按下面这张表理解:

区起始地址大小内容
Bootloader 区0x080000000x8000(32KB)引导逻辑 + 串口升级逻辑
App 区0x08008000剩余全部业务固件
信息区App 尾部独立一页1KB 或 2KB版本号、升级标志、CRC

App 尾部那 1KB 很多人会忽略:断点续传、失败回滚都要一个固定位置存状态,放在 App 末尾独立一页,升级时最后擦写,避免和业务代码混在一起。

选分区还要看一个约束:App 起始地址必须是页对齐的。如果你的芯片页是 2KB,Bootloader 划 30KB 这种"留 2KB 空白"的做法会让 App 的页边界错位,FMC 擦页时地址换算很容易出问题。我一般就直接按 32KB、64KB 这种整幂次划,地址好算,代码里也好写死。

分区这件事看着简单,但它决定后续所有事情的边界。跳转地址、链接地址、上位机下发的目标地址,全都由这一张表派生出来,改一处必须同步改另外三处,这也是后面避坑章里反复出现的主题。

2.2 上电引导流程:等待窗口和升级判定

Bootloader 上电后的执行顺序,决定了整个升级链路是否好用:

  1. 初始化时钟、串口、LED,按系统时钟配置 FMC_WS 等待周期。
  2. 打开一个 200ms 的"等待窗口",期间持续监听串口。
  3. 窗口内收到合法帧头 0x5A 0xA5 → 进入升级模式,停留在 Bootloader 等后续升级帧。
  4. 窗口结束没收到帧头 → 检查 App 区头部魔术字和 CRC。
  5. 校验通过 → 跳转 App;不通过 → 继续留在 Bootloader 等串口,不跳转。

这个等待窗口是关键细节。不加延时直接跳,串口工具还没来得及打开板子就进 App 了,想升级都来不及;窗口太长,上电到 App 跑起来要等半天,产品体验很差。200ms 是我常用的折中值,上位机握手简单的话 500ms 也常见,具体看你上位机程序从启动到能发包要多久。

升级判定建议用"帧头探测"而不是"引脚电平"。GPIO 按键判定看着简单,但产品装进外壳后不一定能碰到引脚,串口帧头探测是纯软件方案,适应性好得多。判定成功后的状态切换要干净:进升级模式后,中断和时钟保持初始化状态,直接进入帧接收循环,不要再走 App 跳转路径。

2.3 跳转函数:取 MSP、设 VTOR、关中断,四步缺一不可

跳转的本质不是执行一条 goto,而是把 CPU 的执行环境完整交给 App。Cortex-M3 的向量表第一项是初始栈指针(MSP),第二项是复位入口地址。跳转要做的是把这两样取出来装好,再把向量表基址寄存器指过去:

typedef void (*app_entry_t)(void); void boot_jump_to_app(uint32_t app_addr) { uint32_t app_sp = *(volatile uint32_t *)app_addr; /* 向量表[0]: 初始 MSP */ app_entry_t app = (app_entry_t)(*(volatile uint32_t *)(app_addr + 4));/* 向量表[1]: Reset_Handler */ __disable_irq(); /* 跳转前关全局中断 */ SysTick->CTRL = 0; /* 停系统节拍,避免跳转瞬间 SysTick 中断 */ /* 常见做法:把 Bootloader 初始化过的外设时钟逐个关闭,同时清 NVIC 挂起位 */ SCB->VTOR = app_addr; /* 向量表重定位到 App */ __set_MSP(app_sp); /* 换栈为 App 的初始 MSP */ app(); /* 进 App 的 Reset_Handler,不返回 */ }

逻辑说明:__disable_irq()必须在最前面,否则跳转瞬间如果有中断挂起,CPU 会从旧向量表取入口执行,行为完全不可控。SCB->VTOR是 CM3 的向量表偏移寄存器,写入 App 地址后,App 里任何一个中断都会从新向量表取入口。app()相当于调用一个永远不会 return 的函数,所以它后面不要再放任何清理代码,写了也不会执行。

参数说明:app_addr必须和 App 工程链接脚本里的起始地址一致。三个地方要收敛成同一个值:链接脚本的 IROM 起始地址、跳转函数入参、上位机下发时的目标地址。我习惯把这三个值收敛成一个宏或者一个配置文件,改分区时只动一处,漏改的概率会小很多。

还有一个自我检查方法:跳转后在 App 的 Reset_Handler 第一行打断点,能停在断点说明跳转链路通了一半;如果停在 HardFault_Handler,优先查 VTOR 写没写对、中断关没关干净。这两个点是跳转失败的最高频原因。

3. 串口升级协议与 FMC 擦写:帧格式、解锁和页擦除代码

3.1 帧协议:一帧一个动作,解析状态机怎么写

串口升级的协议设计,我踩过"一条消息干三件事"的坑,排查的时候全是玄学。后来统一成最小动作帧:一帧只表达一个动作,擦除、写数据、校验、跳转各占一个帧类型:

字段字节说明
帧头20x5A 0xA5,用于同步
帧类型10x01 擦除 / 0x02 写数据 / 0x03 校验 / 0x04 跳转
长度2数据域长度,小端
地址4目标 Flash 地址,小端
数据N写数据帧的内容,N 不超过一页字节数
CRC162覆盖类型 + 长度 + 地址 + 数据,多项式 0x8005

接收端用状态机逐字节解析,比收完一整包再处理更稳,天然能处理串口粘包:

#define FRAME_HEAD0 0x5A #define FRAME_HEAD1 0xA5 static uint8_t rx_buf[512]; static uint16_t rx_len; /* 当前帧期望长度 */ static uint16_t rx_idx; /* 已收字节数 */ static uint8_t rx_state; int uart_parse_byte(uint8_t ch) { switch (rx_state) { case 0: /* 等帧头0 */ if (ch == FRAME_HEAD0) rx_state = 1; break; case 1: /* 等帧头1 */ if (ch == FRAME_HEAD1) { rx_state = 2; rx_idx = 0; } else rx_state = 0; /* 帧头错,回到初始 */ break; case 2: rx_buf[rx_idx++] = ch; if (rx_idx == 5) /* 收齐 类型+长度+地址 前 5 字节 */ rx_len = rx_buf[2] | (rx_buf[3] << 8); /* 取出长度字段 */ if (rx_idx == rx_len + 5) { /* 数据收齐 */ rx_state = 0; if (crc16(rx_buf, rx_len + 5) == 0) /* CRC 覆盖 类型+长度+地址+数据 */ return 1; /* 一帧完整到达,交给上层处理 */ } break; } return 0; }

逻辑说明:这个状态机在每收到一个串口字节时被调用一次,rx_state维护接收进度。状态 0 和 1 负责找帧头对,两个字节都匹配才进入数据收集;数据收齐后一次性校验 CRC。CRC 算完等于 0 是利用了 CRC16 的性质——把校验字节一起算进去结果应为 0,省一次逐字节比较。

参数说明:rx_buf[512]要能容纳一页数据加帧头开销,如果你的页是 2KB,缓冲区要扩到 2048 + 16;rx_len是数据域长度,从帧里第 3、4 字节解析出来,在状态 2 的前 5 字节收齐后计算。

3.2 FMC 解锁与擦写:GD32 寄存器时序不能直接照搬

很多从通用 F103 工程移植过来的 flash.c,拿到 GD32 上编译能过,但跑起来会有各种怪问题,根源在 FMC 寄存器和写时序的差异。GD32 的解锁键值、擦除触发方式都要按 GD32F10x 参考手册来:

/* 解锁 Flash:GD32 需要依次写入两把键值 */ static void fmc_unlock(void) { FMC->KEY0 = 0x45670123; FMC->KEY1 = 0xCDEF89AB; } /* 页擦除:入参必须是页对齐的起始地址 */ static int fmc_erase_page(uint32_t page_addr) { while (FMC->STAT & FMC_STAT_BUSY); /* 等上次操作完成 */ FMC->CTL = FMC_CTL_PER; /* 置页擦除模式 */ FMC->ADDR = page_addr; /* 写入目标页地址 */ FMC->CTL |= FMC_CTL_START; /* 触发擦除 */ while (FMC->STAT & FMC_STAT_BUSY); /* 等擦除完成,期间不要碰 Flash */ FMC->CTL &= ~FMC_CTL_PER; /* 退出页擦除模式 */ if (FMC->STAT & FMC_STAT_WPERR) /* 写保护错误 */ return -1; return 0; } /* 字写入:GD32 一次编程 32 位,地址必须 4 字节对齐 */ static int fmc_program_word(uint32_t addr, uint32_t data) { while (FMC->STAT & FMC_STAT_BUSY); *(volatile uint32_t *)addr = data; /* 对 Flash 地址赋值即触发编程 */ while (FMC->STAT & FMC_STAT_BUSY); return (*(volatile uint32_t *)addr == data) ? 0 : -1; /* 回读校验 */ }

逻辑说明:GD32 的 Flash 编程动作是由"对 Flash 地址的写访问"触发的,不需要先置一个编程模式位,写指针赋值本身就是命令。擦除则需要先设模式、再写地址、最后置 START 位触发。两段代码都有 BUSY 轮询,这是必须的——GD32 页擦除耗时明显比通用 F103 长,主频 72MHz 下单页擦除可能要几十到上百毫秒,不轮询直接做下一步,下一条命令会被 FMC 状态机吞掉。

参数说明:page_addr必须是页对齐地址,且落在主 Flash 区;addr必须 4 字节对齐,否则 FMC 报错或者写入结果错位。写数据帧到达后,先按字拆成 32 位逐字写入,最后不足一个字的部分要按字补全再写,不能留半个字。

提示:FMC 相关寄存器位名在不同 SDK 版本里略有差异,编译报错时以你手里的 FMC 头文件为准,流程都是解锁、设模式、写地址、轮询 BUSY 这套。

擦写之前还有一个前提:FMC_WS(等待周期)必须和系统时钟匹配。GD32F103 跑到 72MHz 时等待周期配置不足,Flash 读取都会出问题,症状是代码随机 HardFault、擦写后回读不对。按手册的 HCLK 频率对照表设 FMC_WS,上电第一件事就做。

3.3 应答与超时:让升级链路不"半路卡死"

接收端要处理两个问题:粘包和等待。Bootloader 每收完一帧回一个应答字节,0x06 表示成功、0x15 表示失败,上位机没有流控也能知道每一步的结果。超时规则我一般这样定:上位机发完一帧等 2 秒,超时重发同一帧,最多重试 3 次;Bootloader 收帧过程中如果两个字节间隔超过 500ms 判定超时,清空接收状态重新等帧头。这样握手、擦页、写数据各自有各自的节奏,一个超时不会把整个状态机搅乱。

波特率的选择也影响可靠性。115200 是默认值,一个 64KB 的 App 约 6 秒传完;如果固件经常上百 KB,可以上 230400 甚至 460800,但要注意两个前提:Bootloader 的串口时钟要能整除出目标波特率,以及写页期间不能丢数据。这个权衡在避坑章第 4 条还会说到。

4. App 侧三处修改:链接地址、向量表偏移、bin 生成

4.1 IROM1 与分散加载文件:App 从 0x08008000 开始链接

App 工程编译出来的目标代码必须放在 Bootloader 留给它的地址区间。在 MDK 的 Target 页里,IROM1 的起始地址从默认的 0x08000000 改成 0x08008000,大小改成剩余容量。以 64KB Flash 为例,Bootloader 占 32KB,App 最大就是 0x8000(32KB)。如果 App 编译出来超过这个值,链接阶段会直接报错,这比运行时才发现写超要好得多。

如果工程用的是分散加载文件,等价改法是改 .sct 文件:

LR_IROM1 0x08008000 0x00008000 { ER_IROM1 0x08008000 0x00008000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (+RW +ZI) } }

逻辑说明:LR_IROM1的 0x08008000 是加载域和执行域起始地址,紧跟的 0x00008000 是区大小。RESET, +First保证向量表排在区的最前面,跳转函数才能从 App 首地址读到 MSP 和 Reset_Handler。

参数说明:如果芯片是 256KB 或更大,Bootloader 划 32KB 不变,App 区大小改成总容量减 32KB;RW_IRAM1 按芯片实际 SRAM 大小填,GD32F103 小容量是 20KB,大容量是 48KB 或 64KB,以数据手册为准。

链接地址改不对的典型现场:App 能编译能烧录,Bootloader 跳过去就 HardFault。问题不在代码逻辑,而是 App 的符号地址还按 0x08000000 排布,跳到 0x08008000 时取到的根本不是向量表。

4.2 向量表偏移:VTOR 写在 main 最前面

App 需要把自己的向量表基址指到 0x08008000。GD32 官方固件库在 system_gd32f10x.c 里预留了 VECT_TAB_OFFSET 宏,默认是 0,要改成 0x8000。如果你用的是不带这个宏的工程模板,就在 main 一开始、任何外设初始化之前写:

#define VECT_TAB_OFFSET 0x8000 /* 必须与链接地址偏移一致 */ SCB->VTOR = 0x08000000 | VECT_TAB_OFFSET; /* 0x08008000 */

逻辑说明:Cortex-M3 的 VTOR 寄存器决定中断向量表从哪取。Bootloader 跳转前已经设过一次,App 里再设一遍是双保险——App 内部如果有软复位逻辑,复位后 VTOR 必须回到 App 自己的向量表,否则所有中断都会跑回 Bootloader 的向量表去取入口。

参数说明:VECT_TAB_OFFSET的单位是字节,0x8000 对应 32KB 偏移,必须和 4.1 节的链接起始地址一致。另一个隐藏要求是 VTOR 的对齐:向量表基址必须对齐到向量表大小的整数倍,0x08008000 天然满足,但如果你把 App 放在一个奇怪的偏移上,要单独检查对齐。

常见误区是只改宏不重新全量编译。宏定义在头文件里,增量编译有时会漏掉依赖它的编译单元,症状就是 App 能跑、中断一进就死。这个在避坑章第 5 条展开。

4.3 bin 与头部信息:fromelf 生成、脚本拼 header

App 编译产生的 AXF 带调试信息,不能直接当刷写固件用。转成纯二进制 bin 用 ARM 工具链的 fromelf,在 MDK 的 After Build 命令里加一行:

fromelf --bin --output=app.bin ./build/app.axf

逻辑说明:--bin输出原始字节流,不带地址信息,烧写时由目标地址决定落点,正好和升级协议里的地址字段配合。

参数说明:--output=app.bin是输出文件名,./build/app.axf是工程生成的 AXF 路径;如果你用 GCC 工具链,等价命令是arm-none-eabi-objcopy -O binary app.elf app.bin效果相同。

刷写用的固件还要在前面拼一段 header,存魔术字、CRC32 和长度,供 Bootloader 在跳转前做合法性判断。这段拼装我在 PC 端用脚本完成,把 bin 和 header 合成一个升级包文件:

import struct, zlib, sys def make_pkg(bin_path, out_path, magic=0x5A5A5A5A): with open(bin_path, 'rb') as f: app = f.read() crc = zlib.crc32(app) & 0xFFFFFFFF hdr = struct.pack('<III', magic, crc, len(app)) # 12 字节头 with open(out_path, 'wb') as f: f.write(hdr + app) print(f'pkg size: {len(hdr) + len(app)}, crc32: {crc:08X}') if __name__ == '__main__': make_pkg(sys.argv[1], sys.argv[2])

逻辑说明:header 放在 App 起始地址第一个字开始的位置,Bootloader 读 App 首 12 字节就能解出 magic、CRC 和长度,不用扫描整个 App 区。CRC 用标准 CRC32(zlib 实现),Bootloader 端用软件 CRC32 计算 App 区内容做对比,两边多项式一致才有意义,否则校验永远失败。

参数说明:struct.pack('<III')是小端三个 32 位无符号整数,分别对应 magic、crc、length;Bootloader 端要用同样的字节序去解,串口协议里地址和长度字段也统一小端,少踩一个字节序的坑。

5. 避坑记录:五种把板子刷成砖的姿势

这套 IAP 源码本身能跑,但能不能在你板子上稳定跑起来,全看分区、地址、时序这三个变量有没有对齐。下面五条是我在实际项目里踩过、也在调试器里确认过的坑,每条按现象、原因、解决三步写,方便对照排错。

5.1 跳转即 HardFault:中断没关干净

现象:在 Bootloader 里单步调跳转函数,最后一步entry()执行后,调试器直接停在 HardFault_Handler,PC 指向一个奇怪的地址,复位后重复出现。

原因:跳转前没有关全局中断,或者 VTOR 没重设。跳转瞬间任何挂起的中断都会从旧向量表取入口,取到 Bootloader 的中断服务函数里去执行,栈却是 App 的,行为完全不可控。另一个隐蔽原因是跳转前外设时钟没关,App 初始化外设时发现时钟已经在跑,配置写不进去。

解决:跳转函数按固定四步走——先__disable_irq(),再清 NVIC 挂起位、逐个关闭 Bootloader 用过的外设时钟,然后设SCB->VTOR,最后__set_MSP换栈再调用入口。顺序不能反,换栈前的任何一帧中断都会让前面的工作白费。

5.2 FMC 擦除卡 BUSY:等待周期和时钟不匹配

现象:擦除函数进去后 BUSY 轮询永远不通过,调试器暂停看FMC->STAT,BUSY 位常亮。上位机表现为升级进度停在同一页不动,超时重试也没用。

原因:两类情况,一是没等上一条 FMC 命令结束就发下一条,命令被 FMC 状态机吞掉,状态机卡在中间状态;二是系统时钟跑到 72MHz 但 FMC_WS 等待周期没配对,Flash 访问本身就不稳定,擦除命令根本没被正确执行。

解决:每次擦写前先轮询FMC_STAT_BUSY,这是硬性要求;时钟初始化后按 HCLK 频率查手册设 FMC_WS。排查时用调试器看 FMC 的 STAT 寄存器,BUSY 常亮基本就是时钟配置的问题;如果只有擦除卡、CPU 运行正常,再查擦除模式下 ADDR 是否写到了合法的页地址。

5.3 整片变砖:App 地址越界写穿了 Bootloader

现象:升级过程中上位机显示擦除成功,但升级完成后重启板子完全没有反应,Bootloader 也没跑起来,整片 Flash 内容看起来被清空了,只能用下载器接口恢复。

原因:App 写入地址越界,把 Bootloader 区覆盖了。常见于上位机把 App 偏移算错,从 0x08000000 开始擦;或者 App 实际大小超过分区,后面页的擦除地址落进了 Bootloader 区。Bootloader 自己没有做范围防御,来什么擦什么。

解决:Bootloader 里对任何擦除、写请求先做范围检查,目标地址必须在 App 起始地址 0x08008000 之后,且地址加长度不超过分区末尾,越界直接回 0x15 拒绝,不做任何 Flash 操作。这条防御成本极低,强烈建议放在协议处理的最前面。

5.4 升级丢帧:写 Flash 时串口缓冲区被覆盖

现象:升级到某一个固定页附近开始出现 CRC 校验失败,上位机重试几次后放弃;掉帧的位置不固定,但总是在大固件、高波特率时更容易出现。

原因:Bootloader 在页擦除期间 CPU 被 FMC 占住,串口仍然在收数据,但接收中断来不及把数据搬走,缓冲区被新数据覆盖。帧越长、波特率越高,擦除窗口里堆进来的字节越多,越容易触发。

解决:写入期间暂停串口接收的应答,写完再恢复;主机端按"发一页等一帧应答"的节奏走,不做流水线发包。缓冲区做双缓冲是最彻底的方案,但 Bootloader 的 RAM 通常紧张,先限帧长、加握手是性价比更高的做法,我后来一直用"发一整页、等应答、再下一页"的同步模式,一套流程下来一次丢帧都没出现过。

5.5 中断一进就死:向量表偏移编译没生效

现象:跳转成功、App 的 main 正常执行,LED 在闪,但一按按键触发外部中断、或者串口来数据触发串口中断,系统立刻死机,看门狗都不一定能救。

原因:向量表偏移宏改了,但增量编译没把依赖它的文件重编,实际运行的向量表还在 0x08000000;或者VECT_TAB_OFFSET值和链接地址不一致,一个改了另一个漏改。中断发生时 CPU 从旧的向量表取入口,取到的是 Bootloader 的中断服务地址,栈和中断标志对不上,必死。

解决:改完分区后 Bootloader 和 App 都做一次全量 Rebuild;然后在 main 最前面打断点,单步看SCB->VTOR的值是否等于 0x08008000。两处值一起核对,缺一不可。还有一个验证技巧:App 启动后主动触发一次 SysTick 中断,能进中断说明向量表生效,进不去说明偏移没设置成功。

6. 升级可靠性验证:CRC 前置校验与断点续传实测

6.1 跳转前校验:不能让坏固件上电就跑

Bootloader 跳转前不应该直接相信"这里有 App",而是校验 App 区头部信息。我用的 header 是 12 字节:魔术字、CRC32、长度,放在 App 起始地址:

typedef struct { uint32_t magic; /* 固定 0x5A5A5A5A */ uint32_t crc32; /* App 区 bin 的 CRC32 */ uint32_t length; /* App 实际长度 */ } app_header_t; static int app_check_valid(uint32_t app_addr) { app_header_t *h = (app_header_t *)app_addr; if (h->magic != 0x5A5A5A5A) return 0; if (h->length == 0 || h->length > APP_MAX_SIZE) return 0; return (crc32((uint8_t *)app_addr + sizeof(app_header_t), h->length - sizeof(app_header_t)) == h->crc32); }

逻辑说明:校验的三道关卡分别是魔术字、长度上限、内容 CRC。魔术字挡住"没刷过的空 Flash",长度挡住"半截写入",CRC 挡住"内容被破坏",三道全过才认为 App 可运行。

参数说明:APP_MAX_SIZE是 App 分区的字节数上限;h->length - sizeof(app_header_t)是参与 CRC 计算的字节数,header 本身不算进 CRC,否则 Bootloader 永远算不对。

校验不过就留在 Bootloader 等串口,校验过才设 VTOR 跳转。这样升级到一半断电,重启后也不会带着半截固件跑业务,这是整套设计里最值钱的一条防线。

6.2 回滚与断点续传:两个标志位带来的安全感

回滚的常见做法是划一页独立区域存升级状态。Bootloader 每次上电先看"升级中"标志——为真说明上次没刷完,直接判新 App 无效,走旧 App 路径。断点续传则靠主机端记住已完成的页序号,重连后从断点页继续;Bootloader 端不用维护任务状态,Flash 里哪页还是 0xFF 就从哪页开写,天然可续。

6.3 三轮实测判定升级链路是否可靠

我的验证习惯是跑三轮:正常升级、升级中途断电重启、重复升级十次。正常升级看应答、版本号、功能三个点;断电重启看是否停在 Bootloader 且旧 App 还能跑;重复十次看有没有越擦越慢、CRC 误报。这三轮过完,这套代码才算真正落地。

最后说一个习惯:从那以后我每次调整分区大小,都会强制把链接地址、跳转地址、上位机协议里的地址上限这三个值重新对一遍,再全量编译一次。这个动作让我少填了至少三次板子,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进