
简介面向华大单片机开发者的远程在线IAP升级完整DEMO以HC32L136为主平台同时给出HC32F030、HC32L130以及HC32L(F)17X/19X等型号的参考实现。资源覆盖BOOT引导程序、APP应用程序和PC端上位机三部分能帮助解决设备部署后固件迭代困难、现场拆机烧录成本高的问题适合正在做工业控制、物联网节点或需要批量维护固件的嵌入式工程师学习。压缩包共282个文件整体约3.29MB内容以C/H源码、IAR与Keil工程文件、链接脚本、批处理脚本、J-Link烧录配置、Hex/Bin固件映像及PDF说明为主。目录按照Boot、App、Host端划分Boot部分包含Flash分区和跳转逻辑APP部分提供业务例程Host端包含上位机源码与可执行程序结构清晰便于移植。目前已有3511人学习下载。通过这份DEMO可以快速掌握Boot与APP的启动跳转流程、自定义升级协议设计、Flash擦写与固件校验策略以及上位机与设备端的联调方法并可直接将对应代码迁移到自己的量产产品中。 搞嵌入式最烦的一件事就是产品已经铺出去了结果现场发现固件有bug或者要加个新功能总不能一台一台拆机烧录。之前做华大HC32系列的项目从HC32L136到HC32F030再到HC32L130我都把远程IAP升级写进了出厂固件里这两天刚好帮同事救了一个升级跳转后卡死的现场问题趁热把整个方案和踩过的坑整理出来。先说结论这三款芯片都是Cortex-M0内核Flash布局、IAP跳转逻辑、Bootloader设计思路完全可以共用一套代码差别主要在内核频率、Flash容量参数和部分外设寄存器地址。这套方案做的事情很简单——设备上电先跑BootloaderBootloader根据标志位和固件校验结果决定是直接跳转APP还是进入升级模式接收新固件升级数据通过UART或者无线透传模块一帧一帧灌进去写完校验通过后复位跳转。文章面向正在做HC32系列量产固件、需要搞远程OTA的工程师也适合想给手头M0项目加IAP能力的同学。1. 远程IAP的整体架构为什么三款芯片能共用一个方案1.1 Bootloader加双分区是最不容易翻车的结构远程升级方案说起来有很多种比如直接用厂商的ICP工具配合上位机、用芯片自带的ROM Bootloader做串口下载但这些都依赖现场有电脑或者专用烧录器做不到远程两个字。真正能落地的还是IAP也就是在用户程序里自己写Flash、自己控制跳转。我推荐的结构是Bootloader加上APP双分区Bootloader区上电最先执行负责检查升级标志、接收固件、校验固件、执行跳转。这个区平时不动理论上永远不升级。APP区实际业务代码正常运行、跑逻辑、处理通信。远程收到新固件后由APP把数据写到另一个备份区或者先暂存下来。备份区/参数区存放升级数据或者升级状态标志比如固件完整待跳转固件损坏需要重传。HC32L136和HC32L130的Flash最大能做到128KBHC32F030常见的是32KB和64KB版本。空间稍微宽裕的建议做成A/B双备份切换Flash紧张的至少也要留出一个Bootloader加一个APP区配合外挂参数区记录状态否则升级中途断电一次设备就成砖了。1.2 三款芯片的资源差异对照我在搭建公共工程时对比过这三款芯片关键参数可以整理成一张参考表具体以你手上的型号和批次手册为准芯片型号内核最高主频典型Flash典型SRAM定位HC32L136Cortex-M048MHz实测常用32MHz64KB/128KB8KB/16KB超低功耗适合电池供电HC32F030Cortex-M048MHz32KB/64KB4KB/8KB通用控制性价比高HC32L130Cortex-M032MHz/48MHz64KB/128KB8KB/16KB超低功耗外设裁剪版需要说明的是虽然Flash容量不同但IAP代码完全可以用条件宏去区分起始地址和大小。我在工程里用一个APP_BASE_ADDR和APP_MAX_SIZE宏搞定三款芯片换芯片型号只改这两个地方其余逻辑不动。这种同内核同系列方案厂家的DDL驱动库API命名也高度一致Flash擦写函数、时钟初始化函数基本通用这也是三款芯片能够共用一套Bootloader的前提。不过要特别留意一点HC32F030和HC32L136的Flash操作等待周期和擦除页大小可能不同写驱动的时候不要想当然。2. Flash分区规划与链接脚本调整改错一个地址全盘皆输2.1 分区表设计参考我以HC32F030的64KB版本为例给出一个我实际在用的分区方案起始地址长度用途0x000000008KBBootloader0x0000200048KBAPP主程序区0x0000E0004KB参数区升级标志、版本号、固件CRC0x0000F0004KB备份/暂存区接收新固件数据这里有个很容易犯的错APP区起始地址必须是Flash擦除页大小的整数倍。HC32系列的Flash页大小一般是512字节或者1KB取0x2000这种对齐地址最省事。如果搞成0x2100这种非对齐地址后面擦写的时候逻辑会非常别扭。Bootloader只占8KB其实很宽裕HC32的DDL库裁剪之后加上串口接收和Flash驱动整体可以压到6KB以内。留出余量是为了以后Bootloader本身要加功能比如改波特率、加加密时不用动APP的链接地址。2.2 链接脚本怎么写如果是GCC工具链hc32_flash.ld里这样划分MEMORY { FLASH_BOOT (rx) : ORIGIN 0x00000000, LENGTH 8K FLASH_APP (rx) : ORIGIN 0x00002000, LENGTH 48K FLASH_PARAM (rw) : ORIGIN 0x0000E000, LENGTH 4K RAM (rwx) : ORIGIN 0x20000000, LENGTH 8K }APP工程用FLASH_APP作为代码段Bootloader工程则用FLASH_BOOT。这个分区在APP工程和Bootloader工程里必须完全一致两边各写各的链接脚本很容易攒下不一致的隐患。我习惯把分区宏提取成独立的头文件两个工程都包含同一份避免改了一边忘了另一边。Keil MDK的话在分散加载文件里对应修改LR_IROM1 0x00002000 0x0000C000 { ER_IROM1 0x00002000 0x0000C000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00002000 { .ANY (RW ZI) } }APP区的First很重要因为M0启动时硬件会自动从Flash的起始地址读取栈顶指针和复位向量。如果APP区链接地址是0x2000那么向量表就要放在0x2000处复位向量是*(0x20004)。2.3 Flash擦写驱动的三个注意点先说解锁。华大HC32的Flash控制寄存器默认是上锁的写入之前必须按手册要求的序列解锁否则寄存器写不进去。官方DDL库里有Flash_Unlock()之类的接口直接用不要自己瞎写。再说中断。擦写Flash期间M0读取指令会停顿如果此时来了一个中断中断向量和中断处理函数都要从Flash取指很容易出现总线访问异常或者看起来卡死。所以擦写Flash的关键代码段必须要关中断写完再恢复。这个问题在跳转后卡死故障里也经常作为帮凶出现。最后是Flash等待周期。HC32在提高系统时钟后Flash读等待周期需要同步调整否则程序在Flash里跑着跑着就随机HardFault。DDL库的时钟初始化函数一般会顺带处理但我见过有人为了省电自己写了套时钟初始化把Flash等待周期漏了导致APP在Bootloader跳转后跑几秒钟就死。3. Bootloader跳转逻辑与向量表重映射跳转不是简单赋个函数指针3.1 跳转前的清场操作很多网上流传的跳转代码就是取栈顶、取复位向量、赋值函数指针、调用这在简单场景下能跑但换成HC32这种外设多、时钟源多的芯片直接跳大概率出问题。我的跳转函数长这样typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_stack *(volatile uint32_t *)app_addr; pFunction app_entry (pFunction)(*(volatile uint32_t *)(app_addr 4)); /* 粗校验栈顶地址是否在RAM范围 */ if ((app_stack 0x2FF00000) ! 0x20000000) { return; } __disable_irq(); /* 关闭所有外设中断清除挂起中断 */ NVIC-ICER[0] 0xFFFFFFFF; NVIC-ICPR[0] 0xFFFFFFFF; /* 停掉SysTick防止跳转后SysTick异常触发 */ SysTick-CTRL 0; SysTick-VAL 0; /* 把系统时钟切回上电默认的内部HRC避免带来不必要的影响 */ CLK_SetClockSource(CLK_SRC_HRC); CLK_SetBusClkDiv(CLK_CLK_DIV1, CLK_CLK_DIV1, CLK_CLK_DIV1); /* 向量表重映射到APP区 */ SCB-VTOR app_addr; __set_MSP(app_stack); app_entry(); }这里每个步骤都有实际意义关闭全局中断、屏蔽NVIC里所有外设中断是为了防止跳转瞬间任何中断打断因为此时APP的中断处理函数可能还没准备好。清挂起中断寄存器是因为有些外设中断在关闭前已经被挂起了跳转后一开中断立刻进入异常。关闭SysTick并清零计数器是因为Bootloader自己可能用SysTick做过延时不清理的话APP的延时函数会被这个残留状态干扰。切回默认时钟源是因为Bootloader如果开了PLLAPP的SystemInit在重新初始化PLL时可能因为状态机不对而卡死。跳转后APP会重新调SystemInit从HRC起步最稳妥。3.2 向量表重映射的两种做法Cortex-M0的VTOR在ARM架构中是可选实现。HC32L136、HC32F030、HC32L130这几款芯片的参考手册上能查到System Control Block相关寄存器我的实测结论是SCB-VTOR可以直接写入并生效。但也保不齐个别型号、个别批次用起来有差异所以我习惯在代码里加一个确认逻辑写完VTOR之后读回来比较如果写不进去就要走RAM向量表方案。RAM向量表方案的具体做法是APP的向量表还是放在Flash的APP区起始处跳转之前先把整个向量表M0是48个中断一共72个字复制到RAM的固定区域然后把VTOR指向RAM区域。这样即使Flash基址映射有问题中断也能从RAM取到正确的处理函数指针。代价是RAM会被占掉几百字节对HC32L130这种本来RAM就紧张的芯片来说有点心疼但确实是M0系列最通用的兜底手段。另一个从原理上必须理解的点跳转后APP的SystemInit()里不要再写SCB-VTOR了。Bootloader跳转前已经做完了向量表重映射APP如果又在启动早期改一次理论上没问题写同一个值但有的库会在这个阶段关闭中断或做额外判断多一事不如少一事。APP只负责在链接层面保证向量表位于自己Flash基址重映射这件事完全交给Bootloader。3.3 跳转失败后的兜底策略升级流程里还有一个容易被忽略的策略Bootloader不是只有升级模式和跳转APP两条路而是有三条有完整新固件标志跳转APP。没有升级请求但当前APP区校验通过跳转APP。APP区校验不通过比如上次升级写了一半断电停留在Bootloader等待重新接收固件。判断有效固件的方式我一般是在APP编译时在固定偏移处放一个结构体包含魔数、固件版本、固件长度、全镜像CRC32。Bootloader在跳转前读取这个结构体做CRC校验全部通过才允许跳转。这样能最大程度避免跳到一个残缺固件里跑飞的灾难。4. 踩坑实录IAP跳转后卡死在hal_delay的完整排查链路这个坑是我同事踩的现象非常典型Bootloader跳转进APP后程序跑到了主循环串口能打印但只要调用hal_delay()就卡死看门狗一直复位。因为他用的是某第三方HAL风格的库底层延时依赖SysTick中断所以问题一度被误导到SysTick配置错误上折腾了大半天。我觉得这个案例很值得拆开讲。4.1 排查第一步看PC指针到底停在哪遇到卡死先别急着猜。用调试器连接暂停内核查看PC寄存器和栈内容能直接定位死循环的位置。我当场看到的现象是PC停在了0x000001C8附近的代码里这个地址落在Bootloader区域范围内而不是APP区。这就非常说明问题了——APP调了hal_delay()SysTick中断进来后CPU从中断向量表取的入口地址是Bootloader的向量表里某个地址而Bootloader的SysTick_Handler是空的甚至被裁剪掉了中断处理函数返回后SysTick继续挂起于是一直在这个空循环或者异常状态里打转表现为卡死。4.2 根因向量表重映射没有真正生效代码里明明写了SCB-VTOR APP_BASE为什么没生效我让他读回VTOR寄存器发现值是APP区地址没错。再细看代码发现问题出在更隐蔽的地方——他为了省事把跳转函数放在了Bootloader里但是Bootloader工程里的SCB-VTOR地址没有开启写保护相关的时钟门控而且跳转前调用的某条库函数又把VTOR改回了0。这类问题用调试器单步跟一遍很容易发现但很多人一看到卡死就怀疑时钟、怀疑延时端口配置反而把最基础的向量表给漏了。排查建议先把读回的VTOR值打出来确认再在APP的SysTick_Handler入口打个断点看地址是否真的来自APP区。4.3 根因背后时钟源配置不一致导致SysTick计数异常同事的问题除了向量表之外还有一个次生问题。他的APP在SystemInit()里配置了PLL倍频到48MHz而Bootloader跳转前的清场代码把时钟切回了HRC且没有恢复PLL。因为跳转后APP的SystemInit执行顺序和他预期不一致第三方HAL启动代码在跳转场景下会跳过部分初始化分支导致SysTick的实际装载值和CPU主频不匹配。SysTick中断一直来ISR入口又是空的处理不过来看起来就像hal_delay里死循环。这个问题的修复其实很结构化跳转前一定把时钟切回默认HRC并恢复默认分频。跳转后APP启动代码必须完整执行SystemInit不要在库的#ifdef里用检测到已初始化就跳过的逻辑。延时函数用SysTick的话必须确认SysTick的时钟源和重装载值匹配主频。我用这个排查顺序救回来了同样的问题先查向量表再查时钟配置最后查外设中断残留基本能覆盖百分之九十的跳转卡死案例。4.4 另一个高频元凶跳转前没清理UART接收中断这类卡死还有一个很常见的原因和UART有关。Bootloader在升级模式里会开串口接收中断接收数据如果跳转前只是调了__disable_irq()而没把UART外设的中断使能位清掉跳转之后APP初始化UART时中断标志位可能保持置起状态APP一开串口中断立刻进入处理但处理函数里读到的数据寄存器状态和Bootloader遗留的不匹配于是死循环。我的习惯是在跳转前把用过的外设全做DeInit回到上电默认状态。不要把关全局中断当成万能药外设级的脏状态靠全局中断是遮不住的。5. 远程升级通信协议与固件包设计5.1 一套够用的帧格式远程升级的通信链路我一般按透明传输来设计不管底层是UART直连、RS485、LoRa还是4G DTU升级协议都跑在同一套帧格式上。参考协议帧如下字节序字段长度说明0帧头2字节0xAA 0x552命令字1字节见下方定义3帧序号2字节大端用于去重和断点续传5数据长度2字节数据域字节数最大2567数据域N字节固件数据或参数7NCRC162字节对帧头之后所有字节做CRC16-Modbus命令字定义0x01握手请求/应答设备回复Bootloader版本、APP版本、Flash剩余空间0x02开始升级请求携带总包数、固件版本号0x03数据包携带固件内容0x04结束升级携带全镜像CRC320x05校验成功请求跳转APP0x06查询升级状态帧序号一定要做因为远程链路不稳定无线模块丢包是家常便饭。接收端只接受上一帧序号1的帧其他一律回复NAK并请求重传当前帧这样天然实现停等式重传应对弱网环境足够用。5.2 校验与写Flash的顺序问题固件包整体校验我用CRC32跑完整包镜像大概几百毫秒在Bootloader里做完全无压力。传输过程中每一帧单独用CRC16做链路校验帧不对直接丢弃不写Flash。这里有一个顺序上的坑不要收到一帧就擦一页写一页而是先写到一个暂存区全部收完后做整体CRC32校验通过了才搬移到APP区。如果边收边写APP区中途某帧错了要重传就得先把已写入的部分擦掉再重来Flash寿命直接损失一大截。Flash寿命也是要考虑的。HC32 Flash擦写寿命通常标注一万次以上虽然单台设备远程升级次数不会太多但批量验证、反复调试时如果协议设计得不好很快就能把Flash擦坏。所以我在协议层加了一个约束刷写失败重传时可以对同一页做读-改-写操作尽量复用已有数据减少整页擦除次数。5.3 掉电保护和双区切换升级中最怕的不是传输慢而是掉电。我在参数区维护一个状态字节0xA5APP区固件有效正常启动0x5A新固件已收完但未校验等待校验0xC3新固件已通过校验等待跳转Bootloader启动时读这个状态字配合APP区起始处的魔数和CRC做最终判断。这样即使升级过程中突然断电Bootloader也能根据状态字知道上次升级没完成自动停留在升级模式等待重新传固件而不是傻乎乎去跳转一个残缺固件。Flash容量够的话更稳妥的方案是A/B双区轮换这次升级写B区下次升级写A区Bootloader根据标志位选择启动哪一区。哪怕升级过程断电另一区还是好用的设备完全无感。HC32L136的128KB版本我强烈建议做双区体验和单区的工程复杂度不是一个量级。6. 应用程序侧必须做好的三处配合修改6.1 中断向量表偏移是APP工程的头等大事APP工程里第一个要改的就是向量表。如果用GCC确保链接脚本里APP段的起始地址就是0x2000以本文分区为例并且启动文件里向量表放在段首。如果是MDK分散加载文件里First保证向量表排列在段首。有些第三方库的启动文件会在main之前就把VTOR设置为0这种代码在Bootloader跳转场景下会把前面的重映射覆盖掉。要在库的启动文件里把VTOR操作注释掉统一交给Bootloader管理。6.2 Flash擦写函数要不要放RAM执行APP如果需要在运行过程中接收新固件并写入Flash需要考虑代码执行位置的问题。M0从Flash取指如果在擦写Flash的瞬间代码正好要从同一片Flash读取下一条指令行为是未定义的轻则死等重则产生总线错误。HC32的Flash驱动库里擦写过程本身会做一些等待处理但我还是建议把关键的擦写执行函数放到RAM里跑。GCC里用__attribute__((section(.ramfunc)))MDK里用__ramfunc前缀就这么简单。写Flash期间还需要关中断关中断的时间窗口必须尽可能短否则外部通信会丢数据。实际测试时要把接收一帧和擦写一页拆到不同的时间片里避免擦写阻塞导致UART接收缓冲区溢出。6.3 APP收到升级指令后的处理策略APP侧收到开始升级的指令后不要立刻跳进Bootloader而是先做三件事把当前运行状态保存好比如业务参数、计量数据到参数区。给上位机回复一个ACK确认已收到升级指令。发送软件复位指令让设备重启进入Bootloader。我见过有人试图在APP里不重启直接在后台把整个固件接收完再写Flash复杂度高不少还要处理Flash自擦写的坑完全没有必要。利用Bootloader做升级模式APP只需要把升级请求变成一个复位标志这是最不容易出错的架构。这个思想也是远程升级系统里最值得坚持的一条分工明确每个角色只干自己擅长的事。如果硬件上支持外部看门狗在Bootloader接收固件期间要记得喂狗不然传输时间一长设备自动复位升级永远无法完成。内部看门狗也要在Bootloader初始化阶段重新配置一个更长的超时或者干脆在升级期间关闭传输完成后再恢复。7. 从这套方案延伸出的几个经验回到开头那个卡死问题。排查到最后同事真正改的就三处跳转前清理NVIC挂起中断、确保SCB-VTOR写入不被后续代码覆盖、APP启动时强制完整执行SystemInit。改完之后同样的代码在HC32F030和HC32L136上跑了几十次反复升级都没有再出现跳转卡死。我自己做这套远程升级方案时还有一个习惯在Bootloader里留一个串口命令接口用PC端串口助手就能手动触发擦除、查询版本、强制跳转不需要专门写上位机。这个接口平时也是远程升级链路的底层调试通道排查问题的时候非常有用。最后再分享一个工作方法上的建议IAP升级这种功能一定要在项目早期就定好Flash分区和协议不要等产品量产了再塞进去。因为Bootloader一旦出厂就基本锁定后面想改分区只能连烧录器强刷那就失去远程的意义了。先把架构定好之后每次业务升级只是多传一份固件包的事成本低、风险小这套投入绝对值得。本文还有配套的精品资源点击获取