简介:这份资源是面向华大单片机开发者的远程在线IAP升级完整DEMO,针对HC32L136芯片实现,同时可参考移植到HC32F030、HC32L130、HC32L(F)17X、HC32L(F)19X等型号,适合需要为产品加入固件远程升级能力的嵌入式工程师与学习者。压缩包共282个文件,约3.29MB,包含57个h头文件、39个c源文件,以及xcl、icf、sct等分散加载与链接配置,bat批处理、jlink调试脚本、uvprojx与ewp工程文件、bin与hex固件、flm烧录算法、svd寄存器描述等,覆盖BOOT、APP与上位机三部分。资源中已整理好各芯片型号的启动与编译脚本,并附带删除中间文件的辅助批处理,便于快速搭建与切换工程。目前已有3526人学习下载,读者可据此掌握IAP分区设计、跳转逻辑与上位机通信流程,并迁移到同系列芯片项目中。
1. 从一次现场返工说起:为什么 HC32L136 这类片子必须要有远程 IAP
去年冬天,一个做远程监测终端的客户找我救火。设备已经装在几十米高的塔上,现场有 200 多台,用的是 HC32L136 做主控。问题出在固件里一个计量系数的符号写反了,数据全偏。按老办法,得派人带着 J-Link 一台一台爬上去烧。客户算了一笔账:人工加停工,够买一辆车。那天晚上我们连夜把 bootloader 方案重新捋了一遍,用 HC32L136 的 Flash 自编程能力加一路串口,把升级通道打通,第二天在办公室点了一下按钮,200 台设备自己完成了固件替换。
这就是远程在线 IAP 升级要解决的事。它不是什么高深技术,本质是让单片机自己擦写自己的程序区。HC32L136、HC32F030、HC32L130 这几颗华大单片机都支持 Flash 自编程,配合一个常驻的 bootloader,就能通过串口、无线模块或者 4G 模组把新固件喂进去。适合谁?适合那些设备已经部署出去、拆机成本高、又需要持续修 bug 或加功能的嵌入式开发者。如果你手上正好有这几颗片子,或者准备选型做带升级需求的低功耗产品,这套东西值得花时间吃透。
2. 方案选型与 Flash 分区:bootloader 和 app 怎么切蛋糕
2.1 为什么选 IAP 而不是 ICP 或 ISP
先分清三个概念。ICP 是离线烧录,产线用;ISP 是芯片出厂固化的 boot 区,通常只能通过特定引脚进;IAP 是用户程序自己实现的在线升级,灵活度最高。HC32L136 这类片子没有开放的原厂 ISP 协议给用户远程用,所以远程升级只能走 IAP。
IAP 的核心是两段程序:一段是 bootloader,永远不动,负责接收新固件、校验、写 Flash、跳转;另一段是 app,业务逻辑,可以被替换。bootloader 必须足够小、足够稳,因为它一旦坏了,设备就真成砖了。我一般把 bootloader 控制在 8KB 以内,剩下的 Flash 全给 app。
2.2 Flash 分区设计:以 HC32L136 为例
HC32L136 的 Flash 通常是 64KB,扇区大小 512 字节。分区方案直接决定升级能不能回滚、能不能断点续传。下面是我在项目里用的分区表:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x00000000 | 8KB | 常驻升级程序 |
| App | 0x00002000 | 40KB | 业务固件 |
| 升级缓存区 | 0x0000C000 | 12KB | 暂存新固件 |
| 参数区 | 0x0000F000 | 4KB | 版本号、标志位、CRC |
这个分法的好处是:新固件先写到缓存区,校验通过后再由 bootloader 搬运到 App 区。搬运过程中如果断电,App 区可能被破坏,但缓存区还在,下次上电可以重试。参数区单独放,擦写次数少,寿命长。
注意:HC32F030 和 HC32L130 的 Flash 容量和扇区划分可能不同,移植时先查对应型号的数据手册,别直接抄地址。
2.3 升级标志与跳转逻辑
bootloader 上电后第一件事是读参数区的升级标志。如果标志是「有新固件」,就进升级流程;否则直接跳 app。跳转前要关中断、重设向量表偏移。HC32 的 Cortex-M0+ 内核,向量表偏移寄存器是 SCB->VTOR。
/* 跳转到 app 的典型写法,地址按实际分区改 */ typedef void (*app_entry_t)(void); void jump_to_app(uint32_t app_addr) { uint32_t sp = *(volatile uint32_t *)app_addr; /* 取栈顶 */ uint32_t pc = *(volatile uint32_t *)(app_addr + 4); /* 取复位向量 */ /* 检查栈顶是否在 RAM 范围内,防止跳飞到非法地址 */ if ((sp & 0x2FFE0000) != 0x20000000) { return; /* 非法,留在 bootloader */ } __disable_irq(); SCB->VTOR = app_addr; /* 重定向向量表 */ __set_MSP(sp); /* 重设主栈指针 */ ((app_entry_t)pc)(); /* 跳过去,不再返回 */ }这段代码的关键在 sp 和 pc 的取值。Cortex-M 的向量表前两个字分别是初始栈顶和复位向量。SCB->VTOR 必须设成 app 的起始地址,否则 app 里的中断会跑到 bootloader 的向量表去,直接硬件异常。__set_MSP要在关中断之后调用,顺序反了会翻车。
3. 通信协议与固件接收:一包数据怎么从串口进到 Flash
3.1 自定义帧格式:简单但够用
远程升级的通信层可以跑在 UART、SPI 或者无线透传上。我习惯用一套轻量帧协议,不依赖任何现成库,方便移植到 HC32F030 这种资源紧张的片子上。帧结构如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2B | 0xAA 0x55 |
| 命令 | 1B | 0x01 握手 / 0x02 数据 / 0x03 结束 |
| 序号 | 2B | 从 0 递增,用于丢包检测 |
| 长度 | 2B | 数据段字节数 |
| 数据 | N | 固件分片 |
| CRC16 | 2B | 从命令到数据的校验 |
这套协议没有花哨的流控,但配合重传机制足够稳定。每包数据我一般取 512 字节,正好一个扇区,写 Flash 时不用跨扇区处理。
3.2 接收状态机:别在中断里写 Flash
串口中断只负责把字节塞进环形缓冲区,主循环里跑状态机解析帧。Flash 写操作耗时且不能被打断,放在中断里必出玄学问题。
/* 简化的接收状态机,主循环调用 */ typedef enum { WAIT_HEAD, WAIT_CMD, WAIT_SEQ, WAIT_LEN, WAIT_DATA, WAIT_CRC } rx_state_t; void iap_rx_poll(void) { uint8_t byte; while (ringbuf_get(&byte)) { switch (state) { case WAIT_HEAD: if (byte == 0xAA) state = WAIT_HEAD2; break; case WAIT_HEAD2: state = (byte == 0x55) ? WAIT_CMD : WAIT_HEAD; break; case WAIT_CMD: frame.cmd = byte; state = WAIT_SEQ; break; /* ... 依次收完序号、长度、数据 ... */ case WAIT_CRC: frame.crc = (frame.crc << 8) | byte; if (crc16_check(&frame)) { handle_frame(&frame); /* 校验通过才处理 */ } state = WAIT_HEAD; break; } } }状态机的每个分支只做一件事,收完一帧再统一处理。handle_frame里根据命令字决定是回握手、写缓存还是触发搬运。CRC 校验失败直接丢弃,等上位机重传,不要试图修复。
3.3 Flash 写入:HC32 的解锁与扇区擦除
HC32L136 的 Flash 控制器需要先解锁才能写。擦除按扇区,写入按字或半字。下面是对缓存区写入一个分片的流程:
/* 写一包数据到升级缓存区,addr 需 4 字节对齐 */ int flash_write_packet(uint32_t addr, uint8_t *data, uint16_t len) { uint32_t *p = (uint32_t *)data; uint16_t i; Flash_Unlock(); /* 解锁 Flash 控制器 */ for (i = 0; i < len / 4; i++) { if (Flash_WriteWord(addr + i * 4, p[i]) != Ok) { Flash_Lock(); return -1; /* 写入失败,上报重传 */ } } Flash_Lock(); /* 写完立刻上锁,防误擦 */ return 0; }擦除要在写之前做,而且只能整扇区擦。我一般在上位机发「开始升级」命令时,先把缓存区所有扇区擦一遍,后面每包只写不擦。Flash_Unlock和Flash_Lock必须成对出现,中间不要插入耗时操作,否则可能被意外中断打断导致锁状态异常。
4. 避坑与排查:那些让我熬夜的升级故障
4.1 跳转后进 HardFault
现象:bootloader 跳转成功,但 app 跑几毫秒就进 HardFault。原因通常是向量表没重定向,或者 app 编译时链接地址还是 0x00000000。解决:在 app 的链接脚本里把 ROM 起始改成 0x00002000,同时在 app 的 main 开头再设一次 SCB->VTOR,双保险。
4.2 升级到一半断电,设备变砖
现象:搬运过程中掉电,App 区被擦了一半,重启后 bootloader 跳过去直接死。原因是没有做搬运保护。解决:搬运前先把参数区的标志改成「搬运中」,搬运完成再改成「正常」。bootloader 上电看到「搬运中」就重新从缓存区搬,不跳 app。
4.3 CRC 校验总是不通过
现象:上位机算的 CRC 和单片机算的对不上。原因多半是 CRC 初值和多项式不一致,或者数据段长度算错了。解决:双方约定死 CRC16 的初值 0xFFFF、多项式 0xA001,数据段从命令字开始算,不含帧头。调试时先把原始字节打印出来对比。
4.4 串口接收丢包严重
现象:高速传输时状态机经常卡在 WAIT_DATA。原因是环形缓冲区太小,或者主循环里其他任务耗时太长。解决:缓冲区至少开到 1KB,状态机放在主循环最前面,Flash 写操作分片进行,每写一包让出 CPU 喂一次看门狗。
4.5 HC32F030 上跑同样的代码写 Flash 失败
现象:HC32L136 上正常的写函数,换到 HC32F030 返回错误。原因是两颗片子的 Flash 控制器寄存器地址和时序不同。解决:不要直接复用底层驱动,把 Flash 操作抽象成flash_erase_sector、flash_write_word两个接口,不同型号各写各的实现,上层协议和状态机完全复用。
5. 二次开发与移植:把升级能力塞进你自己的项目
5.1 移植清单:先改这三个地方
拿到这套代码想用到自己的板子上,按顺序改三处:第一,Flash 分区地址,根据你的芯片容量和 app 大小重新划;第二,通信外设初始化,把 UART 换成你的实际通道,中断优先级别设太高;第三,参数区读写接口,如果你外挂了 EEPROM,可以把标志位挪过去,省一次 Flash 擦写。
5.2 加一个版本号比对,避免重复升级
上位机每次连接先读参数区的版本号,和待升级固件的版本比对,相同就跳过。这个逻辑放在 bootloader 的握手阶段,省得每次上电都白搬一遍。
/* 版本号存在参数区固定偏移,格式主版本.次版本.修订号 */ typedef struct { uint8_t major; uint8_t minor; uint8_t patch; uint8_t reserved; } fw_version_t; int need_upgrade(fw_version_t *new_ver) { fw_version_t cur; flash_read(PARAM_ADDR + VER_OFFSET, (uint8_t *)&cur, sizeof(cur)); if (new_ver->major != cur.major) return 1; if (new_ver->minor != cur.minor) return 1; if (new_ver->patch > cur.patch) return 1; /* 只升不降,防误操作 */ return 0; }版本号比较用「主版本不同必升、次版本不同必升、修订号只升不降」的策略,能挡住大部分误操作。如果你想支持回滚,把判断反过来,同时保留旧固件在缓存区。
5.3 用无线模组替代串口:注意流控
很多项目最终要过 4G 或 LoRa 升级。无线链路丢包率高,帧协议里的序号和重传就派上用场了。我一般把每包数据缩到 256 字节,超时时间设 2 秒,连续重传 3 次失败就中止升级并上报。另外无线模组通常有硬件流控引脚,能接就接上,比软件流控可靠。
5.4 一个我踩过的坑:别在 bootloader 里用 printf
早期调试时我在 bootloader 里加了串口打印,结果正式版忘了删。打印函数依赖的缓冲区占了几百字节 RAM,和 app 的栈空间冲突,导致 app 跑起来后随机死机。从那以后我每次发布 bootloader 前都强制走一遍 map 文件检查,确认没有多余的库函数被链进来。希望这个习惯能帮到你,少熬一个通宵。
本文还有配套的精品资源,点击获取