简介:这是一套面向华大单片机开发者的远程在线IAP升级参考方案,聚焦HC32L136芯片,同时兼容HC32F030、HC32L130、HC32L(F)17X、HC32L(F)19X等型号。资源内含BOOT引导程序、APP应用程序与上位机程序三部分,可帮助嵌入式工程师理解并搭建远程固件升级链路,适合具备一定单片机与通信基础的开发者参考移植。压缩包共282个文件,约3.29MB,以h头文件、c源文件、xcl链接配置、bat批处理脚本、jlink调试配置及bin固件为主,另含Keil与IAR工程文件、SVD寄存器描述、FLM烧录算法等,覆盖编译、下载、调试全流程。目前已有3526人学习下载。通过该DEMO,读者可掌握BOOT与APP分区跳转、固件接收与校验、上位机下发升级包等关键环节,并借助现成工程快速验证与二次开发,缩短远程升级功能的落地周期。
1. 远程在线IAP升级:为什么HC32L136这类低功耗MCU也需要一套自己的Bootloader
做过华大HC32L136、HC32F030、HC32L130这几个片子的工程师大概都有过类似经历:板子已经装进现场设备,客户突然提了一个功能修改,或者发现一个通信协议解析的边界Bug,最原始的做法是让现场人员把设备拆回来、插上仿真器重新烧录。一次两次还能忍,设备铺开几十上百台之后,这套流程基本就崩了。远程在线IAP升级要解决的就是这件事——让设备在不动外壳、不接仿真器的前提下,通过已有的通信链路把新固件写进Flash并跳转运行。
这套方案面向三类人:正在做HC32L136/HC32F030/HC32L130相关毕设或课设、需要把Bootloader和App分区讲清楚的学生;手上有多台现场设备、想搭一套可维护升级通道的嵌入式从业者;以及需要给低功耗产品补上OTA能力、但不想引入复杂RTOS和外部Flash的开发者。华大这几个片子的共同点是Flash容量不大、RAM有限、低功耗场景多,所以Bootloader不能照搬STM32那套大而全的思路,得在分区、跳转、校验、断电保护上做减法。
2. 需求与选型:IAP升级要满足哪些硬指标,为什么选片内Flash双区方案
2.1 从现场场景倒推功能需求
先把需求立住,后面所有设计才有依据。远程在线IAP升级不是"能升级就行",它至少要满足下面几条:
- 升级通道复用现有接口:设备现场已经跑着UART或RS485,升级指令就走这条链路,不额外加线。HC32L136这类片子UART资源够用,没必要为升级单独开一路。
- 断电可恢复:升级过程中掉电是现场最常见的翻车场景。方案必须保证掉电后设备还能起来,要么跑旧固件,要么进Bootloader等待重传,绝不能变砖。
- 固件完整性校验:传输过程可能受干扰,写入前必须校验,校验不过不跳转。
- App与Bootloader解耦:App升级不影响Bootloader本身,Bootloader尽量小、尽量稳定,写完就不再动。
- 低功耗兼容:HC32L130/L136常用于电池或低功耗场景,Bootloader不能长时间占用主循环,升级完要能正常进低功耗。
这几条里,断电可恢复和完整性校验是验收时的重点,也是学生答辩最容易被问的地方。
2.2 为什么用片内Flash双区,而不是外部Flash或单区
选型上常见三条路:外部SPI Flash缓存固件、片内Flash单区覆盖、片内Flash双区(Bootloader+App)。前两条在这个场景下都有明显短板。
外部Flash方案要多一颗芯片、多一组SPI走线,BOM和PCB都变复杂,对成本敏感的现场设备不划算。单区覆盖方案是App直接擦自己、写新固件,一旦中途断电,旧固件也没了,直接变砖,这是血泪经验里最常见的一种。
片内双区方案把Flash切成Bootloader区和App区,Bootloader负责接收、校验、写入App区,App区自己不做擦写。HC32L136的Flash容量足够划出这两块,代价是App可用空间变小,但换来的是断电可恢复。我一般会按下面的思路分区:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x00000000 | 8KB | 升级逻辑、跳转、校验 |
| App | 0x00002000 | 剩余空间 | 用户固件 |
| 升级标志区 | App末尾预留 | 1KB | 记录升级请求、固件长度、CRC |
具体地址要按实际片子Flash大小调整,HC32F030和HC32L130容量不同,分区表要跟着改。升级标志区放在App区末尾,是为了让Bootloader和App都能读写,又不会和代码段冲突。
提示:分区地址一旦定下来,Bootloader和App的链接脚本必须同步改,App的起始地址要偏移到0x00002000,否则跳转后跑飞。
3. 总体架构设计:Bootloader、App与上位机三方的职责划分
3.1 三方交互流程
整套升级链路是上位机、Bootloader、App三方配合。App在正常运行时收到升级指令,写升级标志、复位;复位后先跑Bootloader,Bootloader读标志判断是否进入升级模式,是则通过UART接收固件、校验、写入App区,写完清标志、跳转App;否则直接跳转App。
这个流程的关键在于状态机清晰。Bootloader不能既想升级又想跳转,必须靠升级标志区里的字段做唯一判断。我通常会在标志区放一个结构体:
/* 升级标志区结构,放在App区末尾预留的1KB内 */ typedef struct { uint32_t magic; /* 固定值0x5A5A5A5A,标识标志区有效 */ uint32_t upgrade_flag; /* 1=请求升级,0=正常启动 */ uint32_t fw_len; /* 待写入固件长度 */ uint32_t fw_crc; /* 固件CRC32,用于校验 */ uint32_t reserved[4]; /* 预留,方便后续扩展 */ } iap_flag_t;magic用来判断标志区是否被初始化过,避免首次上电读到随机值误判。upgrade_flag是核心开关,App置1,Bootloader处理完清0。fw_len和fw_crc让Bootloader知道要收多少数据、校验标准是什么。reserved留几个字,后面加版本号、升级时间戳都方便,不用改结构体布局。
3.2 Bootloader的启动判断逻辑
Bootloader一上电先做三件事:读标志区、判断magic、判断upgrade_flag。逻辑不复杂,但顺序不能乱:
void bootloader_main(void) { iap_flag_t *flag = (iap_flag_t *)IAP_FLAG_ADDR; /* 标志区magic不对,说明从未初始化,直接跳App */ if (flag->magic != IAP_MAGIC) { jump_to_app(); } /* 请求升级,进入接收流程 */ if (flag->upgrade_flag == 1) { if (iap_receive_firmware() == IAP_OK) { flag->upgrade_flag = 0; /* 升级成功,清标志 */ jump_to_app(); } else { /* 升级失败,保持标志,等待上位机重传 */ while (1) { iap_wait_retry(); } } } /* 无升级请求,正常跳转 */ jump_to_app(); }这里有个容易忽略的点:升级失败后不能清标志,也不能跳App,要让设备停在Bootloader等待重传。如果失败就跳App,App可能已经被写坏,跳过去也是跑飞。所以失败分支里用死循环等重传,配合看门狗喂狗,保证设备不会彻底失联。
3.3 跳转App的关键动作
跳转不是简单一个函数指针调用,中间要做几件事:关中断、设栈顶、取复位向量、跳转。HC32系列是Cortex-M0+内核,跳转逻辑和STM32类似:
void jump_to_app(void) { uint32_t app_stack = *(volatile uint32_t *)APP_ADDR; uint32_t app_entry = *(volatile uint32_t *)(APP_ADDR + 4); void (*app_reset)(void) = (void (*)(void))app_entry; /* 关全局中断,避免跳转过程中断向量错乱 */ __disable_irq(); /* 重设栈顶,App的栈顶存在起始地址前4字节 */ __set_MSP(app_stack); /* 跳转到App复位向量 */ app_reset(); }APP_ADDR就是分区表里App的起始地址。App的链接脚本要把中断向量表偏移到APP_ADDR,同时App里要重设SCB->VTOR(M0+支持的话)或者用偏移方式处理中断。这一步没做对,App里一进中断就跑到Bootloader的向量表,现象是"能跑但一中断就死",属于典型玄学问题。
4. 模块实现:UART接收、Flash写入与CRC校验的落地细节
4.1 UART接收与分包协议
升级数据走UART,不能裸传固件,要有简单协议做分包和校验。我一般用"帧头+长度+序号+数据+CRC"的结构:
/* 升级帧格式:2字节帧头 + 2字节序号 + 2字节长度 + N字节数据 + 2字节CRC */ #define IAP_FRAME_HEAD 0xA55A #define IAP_DATA_MAX 256 typedef struct { uint16_t head; uint16_t seq; uint16_t len; uint8_t data[IAP_DATA_MAX]; uint16_t crc; } iap_frame_t;seq用来检测丢包和重复包,上位机按序发,Bootloader按序收,序号不对就请求重传。len限制单帧数据长度,256字节是个折中,太小传输效率低,太大RAM吃紧。crc对整帧做校验,比单纯校验数据段更稳。
接收端用状态机解析,不要用阻塞式while等数据,否则升级过程中喂不了狗。状态机按字节推进:等帧头、收序号、收长度、收数据、收CRC、校验、写Flash。
4.2 Flash写入的页对齐与擦除
HC32L136的Flash按页擦除,写入前必须先擦。擦除单位通常是一页(比如512字节或1KB,具体看手册),写入可以按字或半字。这里最容易翻车的是跨页写入:一帧数据可能横跨两个页,如果只擦了一页就写,第二页写进去是脏数据。
处理办法是按页缓存:收满一页再擦、再写。或者更简单,升级开始前先把整个App区擦干净,再按帧顺序写。前者省擦除次数,后者逻辑简单。我一般选后者,因为升级不是高频操作,擦除寿命不是瓶颈,逻辑简单更不容易出错。
/* 按页擦除App区,page_size按实际手册取值 */ void iap_erase_app_area(uint32_t app_addr, uint32_t total_len) { uint32_t page_size = FLASH_PAGE_SIZE; uint32_t erase_addr = app_addr; uint32_t erase_end = app_addr + total_len; while (erase_addr < erase_end) { flash_page_erase(erase_addr); /* 底层擦除,需按手册解锁 */ erase_addr += page_size; } }擦除前要解锁Flash控制器,擦完再上锁,这步华大的库函数里有对应接口,别自己写寄存器,容易漏掉时序。
4.3 CRC32校验与跳转前确认
固件全部写完,最后一步是校验。上位机在升级开始时把整包固件的CRC32发过来,Bootloader写完后对App区做一次CRC32,两者一致才允许跳转。
/* 对App区做CRC32,和上位机下发的fw_crc比对 */ uint32_t iap_calc_crc(uint32_t addr, uint32_t len) { uint32_t crc = 0xFFFFFFFF; uint8_t *p = (uint8_t *)addr; for (uint32_t i = 0; i < len; i++) { crc ^= p[i]; for (uint8_t b = 0; b < 8; b++) { crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1)); } } return ~crc; }校验不过就保持升级标志、停在Bootloader等重传。校验过了再清标志、跳转。这一步是验收时的硬指标,答辩时老师大概率会让你演示"故意传错一包,看设备怎么处理"。
5. 避坑与常见问题:升级过程中最容易翻车的五个点
5.1 跳转后一进中断就死
现象:App能跑起来,主循环正常,但一触发中断就卡死或复位。原因:App的中断向量表没有偏移到APP_ADDR,中断发生时CPU还是去Bootloader的向量表取地址,取到的是Bootloader的中断服务函数,和App对不上。解决:App的链接脚本把向量表基址改到APP_ADDR,启动代码里重设SCB->VTOR(若内核支持),或者用向量表偏移寄存器。HC32F030和HC32L136在这块的寄存器名可能不同,查对应手册。
5.2 升级中途断电后设备起不来
现象:升级到一半断电,重新上电设备无响应。原因:App区被擦了一部分,旧固件不完整,Bootloader又没进升级模式,直接跳了App。解决:升级标志在擦除前就置位,且擦除、写入、校验全流程走完才清标志。这样断电后重新上电,Bootloader读到标志还是1,继续等重传。标志区本身不能被升级流程擦掉,要放在App区之外。
5.3 串口丢包导致固件写错
现象:升级完成但App跑飞,CRC校验不过。原因:UART接收没有做序号校验和重传,丢了一包数据,后续数据整体错位。解决:协议里加seq字段,Bootloader收到序号不连续就回NAK,上位机重发。别指望UART不丢包,现场电磁环境复杂,丢包是常态。
5.4 Flash写入前忘了解锁
现象:擦除和写入函数调用返回成功,但读回来数据没变。原因:华大Flash控制器有写保护,不解锁直接写会被忽略,库函数可能不报错。解决:擦写前调用解锁接口,擦写完上锁。调试时可以在写入后立刻读回比对,确认真的写进去了。
5.5 看门狗在升级过程中复位
现象:升级到一半设备复位,升级失败。原因:Bootloader接收数据时用了阻塞等待,长时间没喂狗,看门狗超时。解决:接收用状态机+超时机制,主循环里定期喂狗。或者升级期间临时关闭看门狗,升级完再开,但这样失去保护,不推荐。
6. 验收与答辩:怎么证明这套IAP方案真的可靠
验收这套远程在线IAP升级方案,不能只看"升级成功一次"。我一般会按下面几个场景逐个过,每个场景都要能复现、能解释:
| 验收项 | 操作 | 预期结果 |
|---|---|---|
| 正常升级 | 上位机发完整固件 | 升级成功,App版本号更新 |
| 断电恢复 | 升级到50%断电,重新上电 | 设备进Bootloader,等待重传 |
| 数据出错 | 故意改一包数据 | CRC校验不过,不跳转,等重传 |
| 丢包重传 | 拔插串口线模拟丢包 | 序号不连续,触发重传,最终成功 |
| 重复升级 | 连续升级两次 | 第二次正常覆盖,无残留 |
| 低功耗验证 | 升级完成后进低功耗 | 功耗回到正常水平 |
答辩时老师最爱问的三个问题,提前准备好答案:为什么分区地址这么定(答:按Flash容量和App体积估算,留足余量);断电为什么不会变砖(答:标志区在App区外,擦写前先置标志,全流程走完才清);CRC为什么用CRC32不用校验和(答:CRC32检错能力强,能发现突发错误,校验和太弱)。
还有一个具体技巧:答辩演示时,别只演示成功路径。主动演示一次"传错数据→校验失败→重传→成功"的完整过程,比讲十页PPT都有说服力。老师看到你能处理异常,基本就认可这套方案落地了。
从那以后我每次做IAP方案,都会强制走一遍"断电+丢包+错包"三个异常场景,跑通了才敢说这套Bootloader能用。希望这套结构能帮到正在做HC32L136、HC32F030、HC32L130远程升级的你,把分区、跳转、校验这三件事做扎实,剩下的就是按验收表逐项过。
本文还有配套的精品资源,点击获取