资讯动态

GD32F103 IAP固件升级:Bootloader跳转与断点续传

发布时间:2026/10/26 21:49:54 来源:尧图企业网站定制
简介面向GD32F103C8T6等ARM Cortex-M单片机的IAP在线升级源代码包适合已具备一定嵌入式基础、需要为量产设备加入Bootloader与应用程序跳转能力的开发者参考。资源围绕64KB Flash的地址划分展开明确30KB Bootloader0x8000000—0x80077FF与34KB用户程序0x8007800—0x800FFFF的典型分区并给出1024Byte扇区下的空间规划思路附可烧录的bin、hex产物便于对照理解升级路径及与内核相关的实现要点对学习IAP升级机制、Flash读写与地址映射具有参考价值。压缩包共164个文件以75个头文件、61个C源文件为主辅以启动汇编、Keil工程、链接脚本、批处理文件和少量配置调试文件覆盖外设驱动、工程组织与烧录产物等层面整体约624KB工程结构贴近实际项目便于按模块查阅、复现与裁剪。已有5183人学习下载可用于GD32与STM32同内核IAP移植、Flash扇区划分、Bootloader与用户程序分离及跳转排错等实践参考也适合从STM32平台迁移至GD32时对照验证升级流程。1. 从一次整批返修说起GD32F103 IAP 要解决的三个真实问题某开发者做的第一款带壳产品用的是 GD32F103出厂三个月后客户要改一条按键长按逻辑产线只能把机器一台台拆开、夹上烧录夹、重新下载。那次返修之后他把 IAP 升级补进了固件Flash 里常驻一段 BootloaderApp 通过串口把新固件收进来写进 Flash写完自己跳过去。这类需求在 arm 嵌入式硬件和单片机项目里反复出现IAP 本身不难难的是升级中断电、校验失败、跳转跑飞之后设备还能不能救回来。下面这套方案围绕 GD32F103 展开覆盖分区、跳转、协议、校验和验收适合正在做相关毕设或课设、需要一套能跑通又能讲清楚的人。2. 总体方案设计Bootloader 与 App 的分工边界动手写代码之前最该先定下来的不是协议而是 Flash 怎么切。分区一旦定死后面 Bootloader 的擦除范围、App 的链接地址、上位机的固件长度校验全都要跟着改改起来是连锁反应。我一般会先在纸上把存储布局画出来确认每个区域的起始地址都是页对齐的再开始写第一行代码。2.1 三段式 Flash 布局与容量分配GD32F103 的内部 Flash 从 0x08000000 开始线性映射CPU 可以直接按地址取指这也是内部 Flash 做 IAP 最省事的地方——不需要额外的读写驱动擦写接口就在固件库里。以 64KB Flash 的中容量器件为例我通常切成三段Bootloader 占前 12KB第 13 页单独留给升级标志剩下的全部给 App。区域起始地址大小职责Bootloader0x0800000012KB12 页串口收发、Flash 擦写、CRC 校验、跳转升级标志页0x080030001KB1 页升级请求标志、固件长度、CRC32、版本号App 区0x0800340051KB用户应用、业务逻辑、通信协议把标志单独放一页而不是塞进 Bootloader 内部理由是擦除粒度。标志需要频繁改写如果和 Bootloader 代码共处一页每次写标志都要先擦掉整页等于把自己的代码擦没了。独立一页之后写标志就是「擦一页、写两个半字」风险被隔离在很小的范围里。地址之所以选 0x08003400是因为 0x3400 正好是 1024 的整数倍页对齐不会出现「擦除范围跨到 App 第一页」这种低级但致命的错误。提示页大小必须查具体型号的手册中容量器件常见 1KB 一页大容量器件常见 2KB 一页代码里一定用宏而不是硬编码否则换型号时整个擦除逻辑都要重算。App 区的上限也不是随便写的。51KB 是 64KB 减去 13KB 的结果实际项目中我会再留 1 到 2KB 的余量防止后期加功能时固件悄悄顶到边界。上位机在发送前读一次固件文件大小超过 App 区容量就直接拒绝比写到一半发现越界要从容得多。2.2 升级触发方式与状态机分区定完第二个要定的是「什么时候进 Bootloader」。项目里常见三种触发方式各自的适用场景差别很大。第一种是上电窗口。设备复位后 Bootloader 先跑串口等 500ms 到 2s收到握手帧就进升级流程收不到就跳 App。这种方式最可靠因为不依赖 App 是否还能正常运行缺点是每次上电都要多等这一小段。第二种是通信命令App 运行中收到上位机的升级指令往标志页写一个魔数然后软复位Bootloader 读到魔数就停下来等升级。这种方式用户体验最好但前提是 App 还活着。第三种是硬件条件比如某个引脚被拉低、某个按键长按产线工装常用。实际项目里我一般把第一种和第二种都做上上电窗口保底通信命令做日常。标志页里存的就不只是一个布尔值而是一个结构/* 升级标志页的内容写入前先擦除整页 */ typedef struct { uint32_t magic; /* 0x49415055用来判断这一页是否被写过 */ uint32_t fw_len; /* 待升级固件的字节数 */ uint32_t fw_crc32; /* 整包 CRC32跳转前再核一次 */ uint32_t fw_version; /* 版本号方便回滚时比对 */ uint32_t state; /* 0 空闲 / 1 升级中 / 2 升级完成待跳转 */ } iap_flag_t;magic的作用是区分「这一页是空的」和「这一页存着有效标志」。Flash 擦除后是全 0xFF如果只判断state字段0xFF 会被误读成一个合法状态。加上魔数之后读出来不等于 0x49415055 就一律当作无标志这种防御在掉电场景下能救命。状态机我写成显式枚举而不是靠一堆 if 判断当前的进度typedef enum { IAP_ST_IDLE 0, /* 上电初始等待窗口期 */ IAP_ST_HANDSHAKE, /* 收到握手帧协商单包长度 */ IAP_ST_ERASE, /* App 区擦除中 */ IAP_ST_TRANSFER, /* 数据帧接收中seq 递增 */ IAP_ST_VERIFY, /* 整包 CRC 校验 */ IAP_ST_DONE, /* 写标志、跳转 */ IAP_ST_ERROR /* 任意环节失败回到等待重连 */ } iap_state_t;状态机的价值在于「任何一步出错都有明确的落脚点」。比如 ERASE 阶段收到数据帧直接丢弃并回一个错误码而不是硬着头皮往下写TRANSFER 阶段超时退到 ERROR 而不是卡死。串口升级最怕的不是出错是出错了以后再也进不去升级模式只能拆机。2.3 技术选型内部 Flash、外挂 SPI Flash 还是 SD 卡固件存哪里是方案阶段最容易吵起来的地方。三种做法我都用过结论是小容量 Cortex-M3 器件做 IAP优先内部 Flash。内部 Flash 的优势是零成本、零引脚、零额外驱动。擦写接口在固件库里就有Bootloader 体积能控制在 12KB 以内剩下的空间全给应用。代价有两个一是擦写寿命有限通常按一万次量级估算做产品固件升级完全够用但绝不能拿它当数据日志区天天写二是擦除时 CPU 从同一块 Flash 取指会被总线暂停虽然硬件会自动插入等待但擦除一页动辄几十毫秒如果开了看门狗且喂狗周期设得比较紧就会在擦除过程中被复位。外挂 SPI Flash 的好处是能存两份固件做回滚容量也宽裕代价是多四根线、Bootloader 里要多一套 SPI 驱动和块管理逻辑。SD 卡容量最大但文件系统和卡本身的一致性在工业现场是玄学插拔、掉电、卡质量都能让升级流程翻车我一般只在带屏的消费类设备上考虑。方案成本占用引脚是否支持回滚主要风险内部 Flash无0需双区划分擦写寿命、擦除期间喂狗外挂 SPI Flash器件成本4天然支持多一套驱动与块管理SD 卡卡座成本4 到 6支持卡一致性、文件系统损坏选内部 Flash 意味着 App 区只有一份升级失败就没有旧固件可退。这个短板要靠流程补擦除之前先把固件完整收完并校验通过再动 App 区或者把 App 区切成 A/B 两半轮流写代价是每个区只有一半空间。毕设和课设场景下单区加严格预检已经够用答辩时能把「为什么不双区」讲清楚反而是加分项。3. 跳转与向量表IAP 里最容易写错的两段代码跳转这件事代码只有十几行但十个人写能有六个人第一次跑不通。原因不在跳转本身而在跳过去之前有没有把「现场」清理干净。Cortex-M3 的复位行为是硬件从向量表取前两个字分别装进 MSP 和 PCBootloader 手工跳转等于要手工模拟这一过程少做一步就会以 HardFault 收场。3.1 Bootloader 跳到 App 的完整流程先看代码再解释每一步为什么不能省#define APP_BASE_ADDR 0x08003400u #define APP_MAX_SIZE (51u * 1024u) #define SRAM_BASE 0x20000000u #define SRAM_MASK 0x2FFE0000u typedef void (*app_entry_t)(void); /* 返回值0 表示跳转成功正常不返回非 0 为失败原因 */ uint8_t iap_jump_to_app(uint32_t app_addr) { uint32_t app_sp *(volatile uint32_t *)(app_addr); /* 向量表[0]初始栈顶 */ uint32_t app_pc *(volatile uint32_t *)(app_addr 4u); /* 向量表[1]复位入口 */ /* 栈顶必须落在 SRAM 区间否则说明这段区域没有有效固件 */ if ((app_sp SRAM_MASK) ! SRAM_BASE) { return 1; } /* 复位入口必须落在 App 区内防止误跳到 Bootloader 自己 */ if (app_pc app_addr || app_pc (app_addr APP_MAX_SIZE)) { return 2; } __disable_irq(); /* 关掉 SysTick否则它的中断会打到还没初始化的向量表上 */ SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; /* 清所有外部中断的使能位与挂起位 */ for (uint8_t i 0; i 8u; i) { NVIC-ICER[i] 0xFFFFFFFFu; NVIC-ICPR[i] 0xFFFFFFFFu; } /* 把 Bootloader 打开过的外设恢复默认避免 App 拿到半初始化的硬件 */ iap_periph_deinit(); __set_MSP(app_sp); /* 切换主栈指针 */ ((app_entry_t)app_pc)(); /* 跳到复位入口正常情况不会返回 */ return 3; /* 能走到这里说明 App 入口立即返回了 */ }栈顶检查用的是常见的0x2FFE0000掩码判断高 16 位是不是0x2000。这个写法对 SRAM 起始于 0x20000000 的器件都成立但如果你的型号 SRAM 起始地址不同掩码就要跟着改更稳妥的做法是拿 SRAM 起止地址做区间比较。复位入口检查是很多人忽略的一步如果 App 区是空的向量表全是 0xFFFFFFFFPC 会变成一个非法地址跳过去立刻 HardFault而这种复位在现象上和「上电不启动」几乎一样排查起来非常痛苦。iap_periph_deinit()是这套代码里最容易被省略、也最容易在后期咬人的一步。Bootloader 里通常开了串口、定时器、可能还有 DMA如果跳转前不关App 启动时这些外设还带着旧的配置和挂起标志表现是「串口第一帧数据丢了」「定时器进了一次中断就再也不进」。这类问题不会立刻暴露往往在连续跑几天之后才发作。3.2 App 侧向量表重映射与启动顺序Bootloader 跳过来的时候CPU 的 VTOR 还指向 0x08000000也就是 Bootloader 的向量表。这时候 App 里任何一个中断触发都会跳到 Bootloader 的处理函数上去结果就是复位或者跑飞。所以 App 必须在使能任何中断之前先把向量表偏移改掉/* 放在 App 的 main() 最前面SystemInit 之后、任何外设初始化之前 */ void app_relocate_vector(void) { /* Cortex-M3 的 VTOR 要求按 0x200 对齐0x08003400 满足 */ SCB-VTOR APP_BASE_ADDR; __DSB(); /* 保证写操作完成 */ __ISB(); /* 清流水线确保后面的取指用新向量表 */ }顺序错一步就白干。很多人把这段写在main()中间、外设初始化之后表面上也能跑因为那段时间里没有中断触发一旦打开串口接收中断第一帧数据到来时 VTOR 可能还没生效直接进 HardFault。我一般要求 App 的main()第一行就是这句甚至在启动文件的SystemInit末尾加宁可早不可晚。用固件库的写法是nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x3400)第二个参数是相对 Flash 起始的偏移量不是绝对地址这个细节坑过不少人——写 0x08003400 进去实际偏移就成了一个天文数字。3.3 工程配置IROM 起始地址、链接脚本与合并烧录代码改完工程的链接配置必须同步改否则编译器会把 App 链接到 0x08000000烧进去直接覆盖 Bootloader。Keil 下的做法是打开 Target 选项把 IROM1 的 Start 从 0x08000000 改成 0x08003400Size 改成 0xCC0051KB。GCC 工具链改的是链接脚本/* 链接脚本片段FLASH 起始地址与 Bootloader 错开 */ MEMORY { FLASH (rx) : ORIGIN 0x08003400, LENGTH 51K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K } SECTIONS { /* 向量表必须放在最前面否则 VTOR 指向的位置读不到正确内容 */ .isr_vector : { KEEP(*(.isr_vector)) } FLASH .text : { *(.text*) *(.rodata*) } FLASH }RAM 的长度也要按实际型号改GD32F103 不同容量器件的 SRAM 从 20KB 到 48KB 不等链接脚本里写大了运行到一半踩到不存在的内存现象是随机的数据错乱用调试器都不好抓。量产时还有一步Bootloader 的 hex 和 App 的 hex 要合并成一个文件烧录否则产线要烧两次。合并工具按地址区间拼就行注意 App 的 hex 起始地址必须严格等于分区表的 App 基址中间不能有空洞。合并完成后我习惯用调试器读一次 0x08000000 到 0x08003400 的内容确认 Bootloader 没被覆盖这个动作三十秒能省掉整批返工。4. 固件传输与校验帧协议、Flash 写入与断点续传协议这一层最省事的做法是直接上 Ymodem串口工具自带上位机不用自己写。但 Ymodem 的帧头是固定格式想加版本号、断点续传字段就得自己扩扩完还不如自定义一套简单帧。我的选择是自定义帧结构控制在一屏代码能看完的复杂度。4.1 帧结构与命令字设计帧格式定成两字节帧头、命令字、序号、长度、载荷、CRC16。固定开销 7 字节单包载荷 512 字节或 1024 字节串口波特率 115200 下一包大约几十毫秒重传成本可以接受。命令值方向载荷说明CMD_HANDSHAKE0x01双向4 字节协议版本协商版本与单包最大长度CMD_ERASE0x02上位机发起4 字节固件长度Bootloader 据此算擦除页数CMD_DATA0x03上位机发起固件数据seq 递增用于判重与续传CMD_VERIFY0x04双向4 字节 CRC32整包校验失败要求重传CMD_END0x05上位机发起无写完成标志并跳转CMD_NAK0x7FBootloader 发起1 字节错误码通知上位机重发或终止CRC16 用查表或者 MODBUS 多项式都可以关键是上位机和固件必须用同一套实现。上位机侧的打包逻辑大概是这样import struct def crc16(data: bytes) - int: CRC-16/MODBUS多项式 0xA001和固件里的实现必须完全一致 crc 0xFFFF for b in data: crc ^ b for _ in range(8): crc (crc 1) ^ 0xA001 if crc 1 else crc 1 return crc def build_frame(cmd: int, seq: int, payload: bytes b) - bytes: 按 5A A5 | cmd | seq | len(2B, 大端) | payload | crc16(小端) 组装 body struct.pack(BBH, cmd, seq, len(payload)) payload return b\x5A\xA5 body struct.pack(H, crc16(body))长度字段用大端、CRC 用小端这种不一致看着别扭但串口调试助手一般是按大端显示多字节数据而 CRC 校验值习惯低字节在前两边统一约定好写进文档就行。真正要避免的是同一份代码里两种端序混用查起来能耗掉一整天。序号seq是断点续传的基础。Bootloader 收到一个数据帧先比对 seq 是不是当前期望值等于就写入并回 ACK小于就说明是重发的旧包直接回 ACK 不重复写大于就回 NAK 让上位机退回去重发。这个三段判断能把「ACK 丢了导致上位机重发」的情况处理干净不然同一段数据被写两次Flash 里就多了一份垃圾。4.2 Flash 擦除与写入页对齐、半字编程与回读比对GD32F103 的 Flash 编程最小单位是半字也就是 16 位。写入前必须先擦除擦除的最小单位是页。这两个约束决定了所有写入函数都必须做对齐检查。#define FLASH_PAGE_SIZE 1024u /* 按型号手册改大容量器件可能是 2KB */ /* 按页擦除addr 会自动向下对齐到页首len 向上取整 */ uint8_t iap_erase_region(uint32_t addr, uint32_t len) { uint32_t page addr ~(FLASH_PAGE_SIZE - 1u); uint32_t end addr len; fmc_unlock(); while (page end) { if (fmc_page_erase(page) ! FMC_READY) { /* 内部会等 BUSY 标志 */ fmc_lock(); return 1; } page FLASH_PAGE_SIZE; } fmc_lock(); return 0; } /* 半字写入addr 必须 2 字节对齐len 必须为偶数 */ uint8_t iap_write_halfwords(uint32_t addr, const uint8_t *data, uint32_t len) { if ((addr 0x1u) || (len 0x1u)) { return 1; /* 对齐检查不能省 */ } fmc_unlock(); for (uint32_t i 0; i len; i 2u) { uint16_t hw (uint16_t)data[i] | ((uint16_t)data[i 1u] 8); if (fmc_halfword_program(addr i, hw) ! FMC_READY) { fmc_lock(); return 2; } /* 回读比对电压波动和擦写寿命会让写失败变得很隐蔽 */ if (*(volatile uint16_t *)(addr i) ! hw) { fmc_lock(); return 3; } } fmc_lock(); return 0; }fmc_page_erase内部会等 BUSY 标志清零但有些型号的库函数在超时后直接返回所以外面还要判返回值。回读比对这步很多人觉得多余实际上它是唯一能发现「写入看似成功、实际位没翻转」的手段。工业现场电源不稳的时候这种半成功状态真的会出现而它导致的后果是 App 校验通过、运行到某个函数才跑飞排查成本极高。还有一个隐含前提擦除和写入期间不能从同一块 Flash 取指。Cortex-M3 的硬件会在总线冲突时插入等待功能上没问题但如果看门狗喂狗周期设得比一次页擦除时间还短就会在擦除循环里被复位。通常的做法是在擦除前先喂一次狗并且把看门狗周期放到 2 秒以上。4.3 断点续传、双区备份与回滚策略断点续传的核心不是协议是「进度存在哪」。存在 RAM 里没用掉电就没了存在标志页里又太频繁每包擦一次 Flash寿命直接见底。我的做法是分两级正常传输过程中进度只在 RAM 里维护靠 seq 判重只有在检测到链路中断、准备退出升级模式之前才把「已写入的固件长度」和「最后一次成功包的 seq」写进标志页。这样一次升级最多写两次标志页寿命完全可控。上位机的配合逻辑是连接后先读一次标志页如果发现有未完成的升级记录从记录的偏移继续发而不是从头来。这个设计在实测中把一次 40KB 固件的中断恢复时间从十几秒压到了一秒以内。双区备份是回滚的基础。把 App 区平分成 A、B 两块Bootloader 里维护一个「当前有效区」标志新固件永远写非活动区校验通过后把标志切过去再跳转。如果新固件启动后连续几次没喂狗、被看门狗复位Bootloader 就判定新固件异常把标志切回旧区。代价是每个区只有一半空间64KB 的器件上每个区只剩 25KB 左右功能稍多就装不下。所以双区适合 128KB 以上的型号64KB 的器件还是老老实实做单区加预检。注意回滚判据不能只看「启动失败一次」要连续 N 次加一个启动计数器且计数器本身要存在不会因软件复位被清掉的地方否则新固件正常启动一次就把计数清了回滚机制形同虚设。5. 避坑与排查五类最容易翻车的问题这套代码我在不同项目上重写过三四遍每次出问题的位置都差不多。下面五条是按踩坑频率排的每条都写成现象、原因、解决三段方便直接对照现场。5.1 现象跳过去直接 HardFault连第一个断点都进不去原因通常有三个。一是 VTOR 没设或设晚了App 里第一个中断触发时还在用 Bootloader 的向量表二是链接地址改了但代码里的APP_BASE_ADDR没改两边对不上三是向量表读出来的栈顶不合法跳过去瞬间就崩。解决顺序是先用调试器读 0x08003400 的前两个字确认第一个字落在 0x2000xxxx 区间、第二个字落在 App 区内。这两个值不对说明固件根本没烧到正确地址问题在链接脚本或合并工具不在跳转代码。两个值都对还崩就单步跟iap_jump_to_app看是不是在__set_MSP之后、跳转之前被中断打断——这种情况一定是关中断没做干净。5.2 现象擦完 Flash 设备彻底不启动串口也连不上最典型的场景是「先擦除、后收固件」中途掉电或断线App 区一片空白标志页也没写Bootloader 上电等窗口期结束发现没固件可跳就卡在等待里而如果窗口期实现得不好连等待都没有直接跳一个空地址。解决的根本办法是把顺序倒过来先把整个固件收进 RAM 或者先算好 CRC 收到临时缓冲全部校验通过再擦 App 区。51KB 的固件塞不进 20KB 的 SRAM那就退一步用分块接收加流式 CRC最后收完 CMD_VERIFY 再统一擦写传输阶段只做暂存和校验不动 App 区。这样最坏情况是升级失败但旧固件完好。再补一道保险Bootloader 的等待窗口做成「只要收到任何合法帧就重置超时」而不是固定等 2 秒就跳。这样即使 App 区是空的只要上位机连着永远能重新发起升级。5.3 现象写入偶发失败或者写进去的数据读出来是乱的三个原因按概率排地址没做半字对齐、擦除范围没覆盖到写入地址、写入和擦除之间没有等 BUSY。第一种最常见上位机按字节发固件按半字拼如果包长是奇数最后一包就会多写或少写一个字节。第二种发生在长度不是页大小整数倍的时候最后一页没擦写进去就是旧的位与新值做与运算读出来永远是错的。解决是把所有检查前置。iap_write_halfwords里那两个对齐判断必须保留返回错误码而不是默默截断擦除函数里end的计算要从addr len向上取整到页不能只擦len / PAGE_SIZE个整页。再加上 4.2 节里的回读比对任何一次写失败都能立刻定位到具体地址不用等到 App 跑起来才发现。5.4 现象升级到一半断线重连后怎么发都没有响应这种情况八成是状态机卡在 TRANSFER 里出不来。串口接收中断里收到半帧数据或者超时机制没做状态一直挂在「等下一包」上位机重连发的握手帧被当成非法数据丢弃。解决是把超时做进状态机。每个状态都挂一个超时计数在定时器中断里递减减到 0 就无条件退回 IDLE 并清空接收缓冲。IDLE 状态下只认握手帧其他一律丢弃这样重连时一定是干净的起点。另外接收缓冲要按帧头重新同步收到 0x5A 之后再等 0xA5对不上就把 0x5A 当普通字节跳过否则一次错位会导致后面所有帧都错位。5.5 现象升级成功跑几分钟或者进一次低功耗就死机跳转前外设没复位、中断没清干净的典型症状。Bootloader 里开过串口和定时器跳转后 App 又初始化一遍看起来没问题但 NVIC 里的挂起位还在App 一开总中断就立刻进了一次「幽灵中断」处理函数里访问的还是 App 没初始化的对象。解决是跳转前那段清理必须做全关 SysTick、清 NVIC 的 ICER 和 ICPR、把用过的外设逐个 DeInit、必要时对 RCC 里对应的外设时钟做一次复位。清理函数不要图省事只写一半每个 Bootloader 里开过的外设都要对应一行。做完这些还不能放心的可以在跳转前把 SRAM 的前几 KB 手动清一遍代价很小能消掉一类「上次运行残留」的玄学问题。6. 验收与答辩怎么证明这套 IAP 真的能用功能跑通只是及格线能拿出测试记录才算交付。我一般会按下面这张表逐条过每条都留串口日志或者示波器截图答辩的时候直接放出来比嘴上说「测试过」有说服力得多。用例操作预期结果正常升级完整下发 40KB 固件校验通过复位后版本号更新断点续传发到 60% 时拔线10 秒后重连从断点包继续不整体重来掉电保护擦除阶段直接断电上电停在 Bootloader可重新发起升级校验失败故意翻转固件中 1 个字节返回 CRC 错误旧固件仍可启动反复升级连续升级 20 次每次都成功标志页无残留空片启动擦空 App 区后上电不跑飞串口有等待提示测掉电保护这条要真断电不能靠软复位模拟因为软复位不会让 Flash 处于擦除中途的半写状态测不出真实问题。做法是拿一个可调电源在擦除开始后随机时间拉掉供电重复二三十次看每次上电是不是都能进 Bootloader。这个测试最枯燥但它是唯一能证明「设备不会变砖」的手段。答辩环节我建议按「分区、跳转、协议、异常」四段讲每段两分钟。分区讲清楚为什么留 12KB、为什么标志单独一页跳转讲栈顶和 PC 的取值、VTOR 的时机协议讲帧结构和 seq 的判重逻辑异常讲掉电和校验失败怎么兜底。评委最常追问的三个问题是App 区装不下怎么办、升级过程中掉电怎么办、新旧固件不兼容怎么办。前两个在方案里都有答案第三个要提前想好——通常是靠标志页里的版本号Bootloader 在跳转前做一次兼容性判断不兼容就停在升级模式要求重新下发。从那以后我每次做带升级功能的板子都会在第一次烧录前强制走一遍「擦空 App 区再上电」的流程确认 Bootloader 自己站得住再往下写业务代码。这个习惯救过我好几次因为最容易翻车的地方从来不是升级成功那次而是升级失败之后设备还能不能救回来。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑