news 2026/10/17 3:54:42

HC32远程IAP升级:从Flash分区到Bootloader可靠性设计全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HC32远程IAP升级:从Flash分区到Bootloader可靠性设计全解析

简介:面向华大单片机开发者的HC32L136远程在线IAP升级DEMO,以BOOT引导程序、APP应用程序和上位机软件三部分为主线,完整呈现远程固件在线升级的工程化实现;HC32F030、HC32L130、HC32L(F)17X、HC32L(F)19X等系列开发者同样可参考这套框架进行移植。压缩包共282个文件,整体大小3.29MB,核心代码由57个C头文件与39个C源文件组成,另有工程配置脚本、J-Link烧录文件、批处理工具和bin/hex固件,覆盖从编译、烧录到上位机联调的完整链路。整个资源包结构清晰,便于按模块自主学习。已有3525人浏览学习。对需要为华大MCU搭建IAP升级通道的工程师而言,这份资料能够帮其避开大量底层移植坑点,快速理解Boot与App的分区跳转、升级协议交互等关键设计,目录结构及工程模板可直接复用,尤其适合在项目早期评估远程升级方案的可行性与开发成本。

1. 远程升级变砖:为什么 HC32 单片机的在线 IAP 不能只靠「擦掉重写」

远程给 HC32L136 这类华大单片机做在线升级,最怕的不是协议写错,而是固件传到一半断电:Flash 里 App 区的旧程序被擦成半成品,新的又没写完,设备直接变砖。IAP(In-Application Programming)这套 bootloader 机制之所以值得深挖,正是因为它要回答三个底层问题:Flash 怎么分区才安全、上电后凭什么决定加载哪份固件、升级中途出故障凭什么自己把自己捞回来。搞懂这三点,你就不是只会调用烧录接口,而是能独立设计一套扛得住掉电、误码、跑飞的远程 IAP 方案。

2. Flash 分区与启动流程:Bootloader 凭什么知道该跑哪份固件

2.1 分区规划:为什么 Flash 至少要拆成三块

很多第一次做 IAP 的人会犯同一个错:把 bootloader 和 App 塞在同一段连续 Flash 里,升级时直接往 App 地址写。这在物理上行得通,但只要一次擦写被打断,整个芯片就变成一块不认任何下载器的砖头。所以分区规划是整个 IAP 机制的基石。

常见的分区思路是把 Flash 拆成三块:bootloader 区、App 区、参数区。以 HC32 系列 128KB Flash 的典型配置为例:

区域起始地址大小作用
Bootloader0x000016KB启动引导、升级接收、Flash 擦写
App0x4000108KB用户业务固件
参数区0x1F0004KB升级标志、版本号、CRC 校验结果、启动计数

Bootloader 区独立出来的意义不只是「隔离代码」,而是让「擦除」这个危险操作永远只发生在可控上下文里。上电后 CPU 永远先跑 bootloader,由它决定是跳进 App 还是进入升级模式,这样即使 App 区被擦得面目全非,bootloader 还活着,设备就还有救。

参数区单独划出来同样关键。升级状态标志如果放在 App 区,App 每次升级都会先被擦掉,那「前一版固件是否完整」就无从判断了。放在独立的参数区,bootloader 每次启动先读这里,就能准确知道上次升级的结果是成功、失败还是进行到一半。

提示:分区地址和大小必须和具体型号的 Flash 页大小对齐。HC32L136 这类 M0+ 内核芯片,页擦除单位在数据手册里都有明确标注,App 起始地址建议对齐到页边界,否则擦除时会把 bootloader 的尾巴一起抹掉。

2.2 上电启动链:从复位向量到 App 跳转的具体动作

分区的意义要落到一段真实存在的跳转代码上才能体现。Cortex-M0+ 内核复位后从 0x00000000 取栈顶指针、从 0x00000004 取复位向量,这是硬件行为。如果 bootloader 放在 0x0000,它天然就是第一个被执行的程序。bootloader 做完自检后,要跳进 App 区,标准做法是这样:

typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t msp_value = *(volatile uint32_t *)app_addr; // App 初始栈顶指针 uint32_t reset_vector = *(volatile uint32_t *)(app_addr + 4); // App 复位向量 __disable_irq(); // 关全局中断,跳转期间不被任何中断打扰 SCB->VTOR = app_addr; // 把中断向量表重定位到 App 区 __set_MSP(msp_value); // 切换到 App 的栈空间 pFunction jump = (pFunction)reset_vector; jump(); // 从 App 的复位向量开始执行 }

这段代码里每一步都有讲究。先读app_addr处的第一个 32 位字作为栈顶指针,是因为编译器链接 App 时会把栈顶放在首地址;读第二个字作为复位向量,是因为这对应中断向量表里的 Reset_Handler。跳转前关中断,是为了防止跳转过程中来了一个中断、而向量表还没切过去,直接跑飞到未知地址。

SCB->VTOR这行是跳转成功的灵魂。M0+ 内核默认从 Flash 起始地址取中断向量,App 跑起来之后第一次发生串口中断、定时器中断时,CPU 还是会去 0x0000 处找向量表——那正是 bootloader 的向量表。处理函数指错了位置,行为就是「App 一进中断就死机」。把 VTOR 指向 App 起始地址,整个中断向量表才算真正切到 App 这边。

2.3 中断向量重定向:VTOR 不设置或者没对齐会怎样

VTOR 的坑藏在细节里。M0+ 内核要求 VTOR 的地址对齐到中断向量表的大小,也就是(中断个数 + 16) * 4字节,通常取 64 字节或 128 字节对齐。很多开发者设置了SCB->VTOR = app_addr,但 App 的链接起始地址没有对齐,比如放在了0x4000 + 100这种位置,结果是中断照死。

对齐问题的根源在于 M0+ 的向量表查找逻辑:CPU 取第 N 号中断的处理函数时,实际访问的是VTOR + N * 4。如果 VTOR 本身有偏移,取出来的函数地址就错位了。更隐蔽的是,某些 HC32 型号在 bootloader 里设置过中断优先级分组,跳到 App 后这个配置会残留,导致 App 里中断优先级行为完全不符合预期。

所以规范的跳转函数里,__disable_irq()之后最好顺手把用到的外设全部复位到默认状态,比如把 UART、定时器、DMA 的中断标志清干净,外设时钟关闭。否则 bootloader 里残留的中断请求在 App 启动瞬间被响应,又会引发一次「没头没尾」的死机。这条在调试阶段极为常见,后文避坑章里会展开讲。

3. 升级协议与擦写时序:串口字节流怎么变成 Flash 里的新程序

3.1 帧格式设计:为什么必须有帧头、长度和校验

远程 IAP 的「远程」二字,落地到 HC32 这类没有网络协议栈的 MCU 上,通常就是 UART 外接一个数传模块,服务器推送的升级包经过模块透传变成一串串行字节流。问题是串口是字节流,没有天然边界——bootloader 必须自己从流水里「切」出一帧一帧的数据来。

一个能用于生产的升级帧,至少包含这些字段:

字段长度含义
帧头2B固定值如 0x5A 0xA5,用于字节流同步
命令1B0x01 开始升级 / 0x02 传数据 / 0x03 校验触发 / 0x04 重启
总数2B本次固件总帧数
帧号2B当前帧序号,从 0 递增
数据256B一帧的有效固件内容
CRC324B整帧校验值,含命令和帧号

帧头和长度存在的意义是「重新同步」。串口通信里偶发一个字节的丢失再正常不过,如果收到 0x5A 0xA5 后按长度字段去数数据,即使中间残缺也能知道这一帧废了,继续找下一帧头重新对齐。没有帧边界的协议,一旦错一个字节,后面所有数据全部错位,而且无从恢复。

数据字段定成 256B 不是拍脑袋。这个值要让「一帧数据」和「一轮 Flash 擦写」形成对应关系。如果 HC32 系列某型号的 Flash 页大小是 512B,那两帧正好凑一页;如果是 256B 页,一帧一页。对齐之后,bootloader 可以把「收帧—校验—擦页—写页」做成流水线,每一帧独立完成,而不是把所有数据囤在 RAM 里等收完再写。

提示:CRC32 的 4 个字节建议分散在帧尾,不要放在帧头后面。帧头丢了可以重新同步,CRC 丢了整帧作废重传即可。如果 CRC 放太前面,帧同步逻辑会变复杂,而且误判率升高。

3.2 先擦后写:一次升级的 Flash 操作时序

HC32 的 Flash 有个特性:写只能在擦过的区域上进行,而且擦除最小单位是页/扇区,写入最小单位通常是半字(16 位)。这意味着「升级一个字节」的真实开销是「擦掉整整一页」。所以 bootloader 的写 Flash 流程必须是严格的「先擦后写」:

int handle_rx_frame(uint8_t *buf, uint32_t len, uint32_t frame_no) { uint32_t crc = crc32_calc(buf, len - 4); if (crc != *(uint32_t *)(buf + len - 4)) { return FRAME_CRC_ERROR; // 校验不过,丢弃,等重传 } uint32_t page_off = frame_no * 256; // 假设 256B 对应一页 uint32_t app_buf_addr = APP_BASE_ADDR + page_off; flash_sector_erase(app_buf_addr); // 先擦除目标页 flash_program_halfword(app_buf_addr, (uint16_t *)buf, 128); // 再写入 128 个半字 return FRAME_OK; }

这段代码的机制要点是「擦写分离」。先擦后写的顺序不能反:如果在没擦的页上直接写,写入操作要么失败、要么写进去的数据和旧数据做按位与,得到一堆乱码。很多人在调试时遇到「写进去了但读出来不对」,八成就是没先擦页。

擦除本身的耗时是个隐藏变量。数据手册给的页擦除时间通常是 20ms 上下,看起来不长,但升级一个 100KB 的固件,按 512B 一页算需要擦 200 次,光擦除时间就是 4 秒。这 4 秒里如果看门狗没喂,系统直接复位,擦到一半的 Flash 就停在残缺状态。所以 bootloader 的擦写循环里必须搭配喂狗策略,这个话题后面专门展开。

3.3 升级状态机:bootloader 怎么知道「这次升级到底成没成」

有了帧格式和擦写流程,还需要一个状态机把整个升级过程串起来。bootloader 上电后的逻辑不是「有没有数据就升级」,而是一套严格的状态转移:

typedef enum { ST_IDLE, // 空闲:校验 App 有效则直接跳转 ST_FIRST, // 已收到升级请求,等待第一帧 ST_RECEIVING, // 正在接收固件数据帧 ST_VERIFY, // 所有帧收完,做整包 CRC 校验 ST_READY // 校验通过,写完成标志,准备重启 } upgrade_state_t; upgrade_state_t state = ST_IDLE; while (1) { switch (state) { case ST_IDLE: if (check_app_valid() && !upgrade_requested()) { jump_to_app(APP_BASE_ADDR); // 老固件完好且没人要求升级,直接跑 } else { state = ST_FIRST; // 否则留在 bootloader 等升级 } break; case ST_RECEIVING: if (frame_timeout > 5000) { // 5 秒没收到下一帧,判定超时 state = ST_IDLE; // 清标志回空闲,下次上电还能跑老固件 } break; } }

状态机的核心思想是「每一步都允许退出」。把超时、CRC 错误、帧号跳变都设计成「回到稳态」而不是「继续死等」,整个升级过程才是可中断的。升级到一半断电重启?没关系,bootloader 上电看到完成标志没写,直接回退到验证老固件的路径。

这里有个容易忽略的参数:帧号跳变。串口丢一帧会导致帧号不连续,如果 bootloader 允许乱序写入,那 Flash 里就会留下空洞,最后的整包校验必然失败。简单的做法是要求帧号严格递增,丢一帧就让上位机从这一帧开始重传;复杂一点的做法是 bitmap 记录已收帧,支持跳跃补传。后者对大固件友好,但 bootloader 的 RAM 占用会变大。HC32L136 的 RAM 通常只有 8KB 上下,做 bitmap 要精打细算。

4. 可靠性兜底:掉电、丢包、跑飞之后,系统凭什么还能活

4.1 双区备份:给升级失败准备的后悔药

到此为止的机制能保证「失败不致命」,但还谈不上「失败不影响业务」。因为单区方案下,升级失败后设备停留在 bootloader 里,等待重新升级,这在工厂产线没问题,在远程部署场景就是故障。A/B 双区方案是更彻底的兜底:Flash 里同时存在两个 App 区,升级时新固件写入备份区,写完后切换标志,而不是覆盖当前运行区。

双区方案的启动逻辑很清楚:bootloader 检查「下次启动区」标志,决定从 A 区还是 B 区启动。升级时把新固件写入当前没在运行的那个区,全部校验通过后才翻转标志。这样任意时刻 Flash 里都有一个「上一次验证过能跑」的固件,掉电最多损失一次升级尝试,不影响当前运行版本。

这种方式在资源受限的 MCU 上的代价是 Flash 容量直接减半。128KB 的 HC32 如果只跑一个 60KB 的固件,双区绰绰有余;但如果固件已经膨胀到 100KB,就得回到单区方案。这是典型的「空间换可靠性」取舍,没有绝对正确答案。

4.2 启动计数回滚:能兜住「能启动但跑飞」的固件

双区方案有一个盲区:固件烧进去了、CRC 也过了,但新固件一运行就死机。如果只是「死机后停在原地」,看门狗会定时复位,复位后 bootloader 又会跳进这个坏固件,形成不死不活的重启循环。启动计数机制就是专门治这个的。

bootloader 每次跳进新固件之前,先把参数区里的「连续启动次数」加 1。新固件正常运行时,会在初始化完成且自检通过后,把「固件有效」标志写进参数区。bootloader 启动时看到这个标志,就把启动计数清零。逻辑如下:

if (get_boot_count() >= MAX_BOOT_FAILURES) { rollback_to_previous(); // 连续 3 次都没活过验证期,回滚 } else if (!app_is_marked_valid()) { inc_boot_count(); // 新固件尚未自检通过,计数累加 jump_to_app(APP_BASE_ADDR); }

这套机制能兜住的场景是「固件能启动但跑飞」:比如 App 初始化到一半死循环、堆栈配错、外设配置把头文件烧没了。它兜不住的是「压根起不来」,但那已经被看门狗和双区方案覆盖了。三层机制结合,才敢说远程升级有基本的容错能力。

4.3 看门狗与擦写耗时:为什么越简单的方案越容易翻车

把看门狗放进 IAP 设计里,是很多人的血泪教训。前面算过,擦完一个 100KB 固件需要 4 秒以上的连续擦除,如果擦写循环里没有喂狗指令,独立看门狗在 1 秒内就会把芯片复位。复位后 bootloader 发现升级没完成,又从头开始,然后又在同一个位置被看门狗复位——死循环。

喂狗策略不能是「每擦一页喂一次」,因为页擦除本身耗时 20ms,如果在擦除期间看门狗计数归零,照样复位。实际做法是升级前关闭看门狗或把窗口拉大,在每帧擦写完成后的「擦写间隙」喂狗。还有一种稳妥思路:把看门狗的超时窗口设成比页擦除时间大一个量级,比如 500ms,这样只要主循环没卡死,哪怕擦除慢一点也能兜住。

注意:升级过程中千万别在页擦除的临界点喂狗。擦除动作一旦被看门狗复位打断,Flash 控制器可能停在中间状态,比直接不擦还要难恢复。宁可把超时设大,也不要卡着窗口喂。

5. IAP 调试避坑:从黑匣子里捞出来的 5 条血泪记录

5.1 现象:App 一进中断就死机,复位后又能跑

原因:跳转 App 时没设置SCB->VTOR,或者设置时地址没按向量表大小对齐。M0+ 内核的中断向量查询仍然指向 bootloader 区,App 里的中断处理和 bootloader 的向量表对不上,一进中断就跑飞到非法地址。

解决:跳转函数里强制重设 VTOR,并且用#pragma或链接脚本确保 App 的起始地址对齐到 64 字节。调试阶段可以加一个断言:(app_addr % 64 == 0),不满足直接报错,别让它带病运行。

5.2 现象:升级完成后第一次上电能跑,第二次上电变砖

原因:升级时把参数区的「固件有效标志」放在了 App 区的末尾。App 升级时擦除了整个 App 区,标志随之消失;第一次运行是 bootloader 跳转前设置的临时标志,断电后丢失。第二次上电 bootloader 按「无有效固件」处理,不跳转了。

解决:标志位必须放在独立的参数区,和 App 区物理隔离。每次升级先写「升级中」状态,全部完成后再写「固件有效」。这样无论擦 App 擦到哪一步,参数区都保有能让 bootloader 做决策的有效信息。

5.3 现象:升级到一半看门狗复位,反复在原地重启

原因:擦写循环里没有喂狗,或者喂狗位置不对。页擦除期间喂狗指令不执行,看门狗累计超时。复位后升级重来,再次超时,死循环。

解决:把看门狗超时时间调到大于单页擦除时间的两倍以上,在每帧擦写完成后的循环底部喂狗。更彻底的做法是升级期间暂时关闭看门狗,等跳转前再重新初始化。

5.4 现象:串口工具发固件没反应,bootloader 毫无响应

原因:调试者直接选了 XMODEM 协议发送 .bin 文件,但 bootloader 端实现的是自定义帧协议,两边帧格式对不上。数据进来了,bootloader 等不到帧头,一直卡在同步状态。

解决:先确认协议匹配,再查帧头、CRC 字段的字节序。CRC32 常见的坑是上位机用小端发、单片机按大端解析,或者两边多项式基值差了一个0xFFFFFFFF的初始化量。对调 CRC 初始化值后再试,通常能立刻定位。

5.5 现象:新固件能跑,但串口打印乱码,功能时好时坏

原因:bootloader 跳转前没有把用过的外设恢复到默认状态。bootloader 初始化过 UART 和定时器,跳到 App 后这些外设的中断标志、配置寄存器还残留着,App 初始化时某些寄存器没重写,行为就不可预期。

解决:跳转函数里做「外设清理」:关掉 bootloader 用过的所有外设时钟,清中断挂起位,把 GPIO 恢复默认态。宁可多写几行复位代码,也不要留着上一个程序的「魂魄」在 App 里飘。

6. 单区与双区:IAP 设计里那道没法两全的选择题

走到这一步,你会发现 IAP 方案的所有复杂度最后都收拢到一个选择上:用单区加标志位,还是用双区加切换标志?这两个方案各有各的死角,没法两全。

维度单区 + 回滚标志双区 A/B
Flash 占用低,App 区容量几乎全用高,可用容量减半
升级失败影响设备停在 bootloader,需重新升级继续跑旧固件,业务无感知
实现复杂度低,状态机短,RAM 占用小高,需要额外处理启动计数和回滚
场景适配产线烧录、现场可维护远程部署、无人值守

如果让我给一个倾向:凡是「设备装上去之后人不方便再碰」的场景,比如光伏逆变器、农业物联网终端、野外数据采集器,直接上双区,别为了省那几十 KB Flash 给自己埋雷。反之如果设备在产线上,升级失败可以立刻用烧录器救回来,单区加标志位完全够用,代码还少一半。

验证一个 IAP 方案靠不靠谱,最有效的方法是故障注入:人为在随机位置断电、随机丢帧、随机在升级中敲复位,连续做几十次。每次故障后检查设备是否还能正常启动到旧固件或新固件,而不是卡死在 bootloader。这套测试跑通了,你的升级方案才算真正落地。

我早期做某跨平台系统的远程升级时,就是单区方案加一个标志位,测试时只验证了「正常升级路径」,没做掉电注入。结果第一批设备部署出去,第一次远程升级就碰上供电不稳,几十台设备全部停在 bootloader。后来我把状态机重写,参数区独立,加上启动计数回滚,再配合故障注入测试,才把这个问题治住。从那以后我每次设计 IAP,都强制把「升级中途断电」「新固件启动即崩溃」这两条路径走一遍验证,再谈功能完善。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 3:54:38

Win7终极精简版镜像解读:版本号、优化部署与老平台适配全攻略

拿到这套镜像的时候,我第一反应是:Win7这条线,终于还是画上句号了。文件名里的信息量其实非常大——Win7x64.SP1.7601.28117中简优化终极版2026.01.31,64位架构、SP1、内核版本7601、修订号28117、简体中文、优化精简、终极版。每…

作者头像 李华
网站建设 2026/10/17 3:54:21

USB转串口驱动安装与调试全攻略:从识别芯片到回环压测

简介:USB TO 232驱动与配套软件包,面向需要将现代计算机USB端口转换为RS-232串行接口的工程师、运维人员及电子爱好者。它解决了笔记本电脑等设备缺少传统COM口、无法连接PLC、仪表等老式串行设备的难题,并兼容Windows、macOS、Linux等多系统…

作者头像 李华
网站建设 2026/10/17 3:54:08

从滤镜到风格迁移:本地批量图像艺术化管线设计与参数调优实战

我记得最早做图像处理那会儿,最抓狂的不是算法选型,而是“把图片变好看”这个需求永远说不清。有人要水墨感,有人要油画厚涂感,还有人只是想给商品图加一层手绘纹理,最后拿出来的结果却经常是“滤镜味太冲”的廉价效果…

作者头像 李华
网站建设 2026/10/17 3:53:57

SQL Server CONVERT日期转换全解析:从style参数到性能优化

做 SQL Server 开发的人,几乎没有不跟 CONVERT 打交道的。尤其是日期转换,“CONVERT(varchar, GETDATE(), 23)”这种写法在公司的存储过程、日常查询、数据导出脚本里到处都是。可说实话,真正能把这个函数讲清楚、用明白的人并不多。大多数情…

作者头像 李华
网站建设 2026/10/17 3:53:17

8421法则:用位运算压缩权限系统,提升鉴权性能

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华