
简介这是一套面向华大单片机开发者的远程在线IAP升级参考方案聚焦HC32L136平台同时兼容HC32F030、HC32L130、HC32L(F)17X、HC32L(F)19X等芯片适合需要为量产设备加入固件远程更新能力的嵌入式工程师与项目团队。资源包内含BOOT引导程序、APP应用程序与上位机程序三部分完整呈现从下位机跳转逻辑到上位机通信的升级链路。压缩包共282个文件约3.29MB以57个h头文件、39个c源文件为核心代码辅以xcl、icf、sct等分散加载与链接配置以及bat批处理、jlink调试脚本、uvprojx与ewp工程文件、bin与hex固件、pdf说明文档等覆盖Keil与IAR双环境。目前已有3526人学习下载。读者可据此理解IAP分区设计、跳转与校验流程并参考多型号适配方式快速搭建自己的在线升级框架。1. 远程在线IAP升级为什么华大HC32系列的量产项目绕不开这套方案做过华大HC32L136、HC32F030、HC32L130这几个料号的工程师大概都有体会板子一旦装进现场设备固件迭代就成了最头疼的事。客户现场在异地设备装在机柜里拆机接仿真器重新烧录根本不现实。这时候远程在线IAP升级就不是“锦上添花”而是量产项目的刚需。这套方案要解决的核心问题很明确让设备通过已有通信链路串口、无线模组、4G模块等接收新固件由bootloader完成校验、搬运、跳转全程不需要拆机、不需要仿真器。适合谁正在做华大HC32系列产品、需要支持远程固件更新的嵌入式开发者以及拿这套结构做毕设或课设的学生——bootloader设计本身就是一个完整的、可答辩的嵌入式课题。2. 需求拆解与技术选型为什么是IAP而不是OTA模组2.1 先搞清楚IAP和OTA的区别很多需求文档里把IAP和OTA混着写实际落地时才发现选错了方向。IAPIn-Application Programming的本质是MCU自己有能力擦写自己的Flashbootloader驻留在Flash起始区域应用程序通过通信接口把新固件数据写入Flash的指定区域然后跳转执行。OTA则通常指通过无线模组WiFi、4G完成整个升级流程模组本身可能还要跑一套协议栈。对于HC32L136、HC32F030、HC32L130这类Cortex-M0内核的MCU资源有限Flash通常64KB到128KBRAM 4KB到16KB跑完整的OTA协议栈不现实。所以正确的做法是通信链路由外部模组或上位机负责MCU侧只做IAP——接收数据包、校验、写Flash、跳转。这样MCU侧的代码量可以控制在4KB以内RAM占用不超过1KB。2.2 存储空间怎么分这是整个方案最关键的决策点。HC32L136的Flash通常是64KBHC32F030是64KBHC32L130也是64KB。如果bootloader占8KB应用程序占48KB那剩下8KB做升级缓存区刚好够用。但要注意华大HC32系列的Flash擦写粒度是512字节一页不是常见的1KB或2KB。这个参数直接决定了升级缓存区的管理策略。我一般会这样划分区域起始地址大小用途Bootloader0x000000008KB驻留引导程序应用程序0x0000200048KB用户固件升级缓存0x0000E0008KB接收新固件数据参数区0x0000F8002KB升级标志、版本号、CRC这个划分的前提是应用程序不超过48KB。如果超了要么压缩bootloader要么换更大Flash的型号。HC32F030有些封装是128KB Flash的那就宽裕很多。2.3 通信接口选哪个远程升级的“远程”二字取决于你的设备现场有什么通信条件。常见的有三种第一种是串口透传。设备上预留UART接口现场用USB转串口工具接上位机上位机发固件包。这种方式最简单但需要人到现场。第二种是无线模组。比如设备上已经装了LoRa、NB-IoT或4G模组MCU通过UART和模组通信模组负责和服务器建立连接。MCU侧只需要实现一个简单的分包协议。第三种是CAN总线。工业设备常用但CAN的MTU只有8字节传输效率低适合小固件。选型原则现场有什么链路就用什么链路不要为了升级单独加模组。升级协议本身要设计成与物理层无关的这样换链路时只需要改底层驱动。3. Bootloader核心设计从Flash驱动到跳转逻辑3.1 Flash擦写驱动怎么写华大HC32系列的Flash操作有一套固定的流程解锁、擦除、写入、上锁。以HC32L136为例关键寄存器操作如下#include hc32l136.h #define FLASH_PAGE_SIZE 512 #define APP_START_ADDR 0x00002000 #define CACHE_START_ADDR 0x0000E000 #define PARAM_START_ADDR 0x0000F800 /* Flash解锁 */ void Flash_Unlock(void) { FLASH-KEYR 0x45670123; // 第一解锁密钥 FLASH-KEYR 0xCDEF89AB; // 第二解锁密钥 } /* 擦除一页 */ uint8_t Flash_ErasePage(uint32_t addr) { uint32_t timeout 0; FLASH-CR | FLASH_CR_PER; // 页擦除模式 FLASH-AR addr; // 目标地址 FLASH-CR | FLASH_CR_STRT; // 启动擦除 while ((FLASH-SR FLASH_SR_BSY) (timeout 0xFFFFF)) { timeout; } if (timeout 0xFFFFF) return 1; // 超时 FLASH-SR FLASH_SR_EOP; // 清除完成标志 return 0; } /* 写入半字16位 */ uint8_t Flash_WriteHalfWord(uint32_t addr, uint16_t data) { uint32_t timeout 0; FLASH-CR | FLASH_CR_PG; // 编程模式 *(volatile uint16_t*)addr data; while ((FLASH-SR FLASH_SR_BSY) (timeout 0xFFFFF)) { timeout; } if (timeout 0xFFFFF) return 1; FLASH-SR FLASH_SR_EOP; return 0; }这段代码有几个关键点。第一解锁必须连续写两个密钥中间不能被打断所以解锁前最好关中断。第二擦除和写入都有超时保护实际项目中Flash操作卡死是常见故障没有超时机制会导致设备变砖。第三华大HC32的Flash写入是按半字16位操作的不是按字节所以数据缓冲区要按16位对齐。3.2 升级标志和版本管理bootloader启动后第一件事是检查参数区判断是否需要执行升级。参数区的数据结构通常这样设计typedef struct { uint32_t magic; // 固定值0x5A5A5A5A标识参数区有效 uint32_t upgrade_flag; // 0x01表示需要升级0x00表示正常启动 uint32_t app_version; // 应用程序版本号 uint32_t app_size; // 应用程序大小字节 uint32_t app_crc; // 应用程序CRC32校验值 uint32_t cache_crc; // 缓存区固件CRC32 uint8_t reserved[40]; // 预留 } ParamStruct;升级流程是这样的上位机先发一个“准备升级”命令bootloader收到后把upgrade_flag置1然后复位。复位后bootloader检测到flag为1进入接收模式把收到的数据写入缓存区。全部接收完成后计算CRC和上位机发来的CRC比对。如果一致把缓存区的数据搬运到应用程序区清除flag跳转执行。如果不一致清除flag跳转回旧应用程序。这里有个血泪经验搬运过程中如果断电应用程序区就是半新半旧的状态设备直接变砖。所以搬运前要先擦除应用程序区搬运后再写一个“搬运完成”标志。下次启动时如果检测到“搬运中”标志但没有“搬运完成”标志说明上次搬运失败了需要重新搬运或者回滚。3.3 跳转逻辑与中断向量表重映射bootloader跳转到应用程序之前必须做三件事关闭所有外设中断、设置主栈指针MSP、跳转到应用程序的复位向量。typedef void (*pFunction)(void); void JumpToApp(uint32_t appAddr) { uint32_t jumpAddr; pFunction jump; /* 检查栈顶地址是否合法必须在RAM范围内 */ if (((*(volatile uint32_t*)appAddr) 0x2FFE0000) 0x20000000) { jumpAddr *(volatile uint32_t*)(appAddr 4); // 复位向量 jump (pFunction)jumpAddr; __disable_irq(); // 关总中断 SysTick-CTRL 0; // 关SysTick SysTick-LOAD 0; SysTick-VAL 0; SCB-VTOR appAddr; // 重映射中断向量表 __set_MSP(*(volatile uint32_t*)appAddr); // 设置主栈指针 jump(); // 跳转 } }SCB-VTOR这行是关键。Cortex-M0的中断向量表默认在0x00000000应用程序的向量表在0x00002000不重映射的话应用程序里所有中断都会跳到bootloader的向量表直接跑飞。HC32L136支持VTOR寄存器但有些M0内核的MCU不支持那就需要在应用程序的链接脚本里把向量表偏移到正确位置或者用bootloader做中断转发。4. 上位机与通信协议分包、校验、重传怎么设计4.1 帧格式定义远程升级的通信协议不需要太复杂但必须可靠。我一般用这样的帧结构字段长度说明帧头2字节0xAA 0x55命令1字节0x01握手 0x02数据 0x03结束 0x04应答包序号2字节从0开始递增数据长度2字节本包有效数据字节数数据N字节固件数据最大1024字节CRC162字节从命令到数据的CRC16数据包最大1024字节是因为HC32L136的RAM只有8KBbootloader本身还要占一部分缓冲区不能太大。实际测试下来512字节一包比较稳传输效率也不差。4.2 上位机发送逻辑上位机用Python写最方便核心逻辑就是读固件文件、分包、发一包等一包应答、超时重传import serial import struct import time def send_firmware(port, baudrate, firmware_path): ser serial.Serial(port, baudrate, timeout2) with open(firmware_path, rb) as f: firmware f.read() total_size len(firmware) packet_size 512 seq 0 # 握手 handshake bytes([0xAA, 0x55, 0x01, 0x00, 0x00, 0x00, 0x00]) ser.write(handshake) resp ser.read(7) if len(resp) 7 or resp[2] ! 0x04: print(握手失败) return False # 分包发送 offset 0 while offset total_size: chunk firmware[offset:offset packet_size] length len(chunk) header struct.pack(BBHH, 0xAA, 0x55, 0x02, seq) header struct.pack(H, length) crc calc_crc16(header[2:] chunk) frame header chunk struct.pack(H, crc) retry 0 while retry 3: ser.write(frame) resp ser.read(7) if len(resp) 7 and resp[2] 0x04 and resp[3:5] struct.pack(H, seq): break retry 1 time.sleep(0.1) if retry 3: print(f包{seq}发送失败) return False offset length seq 1 # 发送结束命令附带总CRC end_frame struct.pack(BBHH, 0xAA, 0x55, 0x03, seq) end_frame struct.pack(H, 0) total_crc calc_crc32(firmware) end_frame struct.pack(I, total_crc) end_frame struct.pack(H, calc_crc16(end_frame[2:])) ser.write(end_frame) resp ser.read(7) return len(resp) 7 and resp[2] 0x04这段代码里超时重传是必须的。现场通信链路质量参差不齐没有重传机制升级成功率会很低。重传次数一般设3次超过3次就报错退出让用户检查链路。4.3 MCU侧接收状态机bootloader不能一直阻塞等数据要用状态机来管理接收流程typedef enum { STATE_IDLE, // 空闲等待握手 STATE_RECEIVING, // 接收数据中 STATE_VERIFY, // 校验中 STATE_COPY, // 搬运中 STATE_DONE // 完成 } UpgradeState; UpgradeState g_state STATE_IDLE; uint32_t g_cache_offset 0; void Bootloader_Process(void) { switch (g_state) { case STATE_IDLE: if (UART_GetFrame(g_frame)) { if (g_frame.cmd 0x01) { Flash_EraseCache(); // 擦除缓存区 g_cache_offset 0; g_state STATE_RECEIVING; UART_SendAck(0); } } break; case STATE_RECEIVING: if (UART_GetFrame(g_frame)) { if (g_frame.cmd 0x02) { Flash_WriteData(CACHE_START_ADDR g_cache_offset, g_frame.data, g_frame.len); g_cache_offset g_frame.len; UART_SendAck(g_frame.seq); } else if (g_frame.cmd 0x03) { g_state STATE_VERIFY; UART_SendAck(g_frame.seq); } } break; case STATE_VERIFY: if (VerifyCacheCRC(g_frame.total_crc)) { g_state STATE_COPY; } else { g_state STATE_IDLE; // 校验失败回到空闲 } break; case STATE_COPY: CopyCacheToApp(); ClearUpgradeFlag(); g_state STATE_DONE; break; case STATE_DONE: JumpToApp(APP_START_ADDR); break; } }这个状态机每轮循环执行一次不阻塞。UART_GetFrame是非阻塞的收到完整帧返回1否则返回0。这样bootloader在等待数据的时候还能处理超时逻辑。5. 避坑与常见问题那些让设备变砖的细节5.1 中断向量表没重映射应用程序跑飞现象bootloader跳转后应用程序的main函数能执行但一进中断就死机。原因Cortex-M0的中断向量表默认在0x00000000应用程序的向量表在0x00002000没有设置VTOR或者应用程序的链接脚本没把向量表放到正确位置。解决bootloader跳转前设置SCB-VTOR APP_START_ADDR。如果MCU不支持VTOR在应用程序的链接脚本里把ROM起始地址改成0x00002000并在启动文件里把向量表放到该地址。5.2 Flash擦除时断电参数区丢失现象升级过程中断电重新上电后设备不启动bootloader也进不去。原因参数区和应用程序区在同一块Flash上擦除应用程序区时如果断电参数区的数据可能被破坏。解决参数区单独放在最后一页擦除应用程序区时不碰参数区。升级标志的写入要用“先写备份、再写主份”的方式两份都写成功才认为标志有效。5.3 缓存区大小不够大固件升级失败现象小固件升级正常固件超过一定大小后升级失败。原因缓存区只有8KB固件超过8KB时接收完的数据还没搬运就被新数据覆盖了。解决两种方案。一是分段升级接收8KB搬运8KB再接收下一段。二是外挂SPI Flash做缓存MCU通过SPI把数据写到外部Flash全部接收完再搬运到内部Flash。第一种方案不需要额外硬件但搬运次数多升级时间长。第二种方案需要外挂Flash但升级流程更简单。5.4 通信超时时间设太短误判失败现象升级过程中偶尔报“包发送失败”但重试后又成功。原因MCU擦写Flash需要时间HC32L136擦除一页512字节大约需要20ms到40ms写入一页大约需要10ms。如果上位机超时设成50msMCU还在擦写的时候上位机就判超时了。解决上位机超时时间至少设200ms重传间隔设100ms。MCU侧在擦写Flash时可以先发一个“忙”应答上位机收到“忙”就延长等待。5.5 跳转前没关外设应用程序初始化冲突现象bootloader里开了UART和定时器跳转到应用程序后应用程序初始化同样的外设时死机。原因bootloader没有把外设恢复到复位状态应用程序初始化时寄存器状态不对。解决跳转前把bootloader用到的外设全部DeInit关时钟。或者更彻底的做法跳转前执行一次软复位让所有外设回到默认状态但软复位后要能区分是正常启动还是升级后启动这需要参数区配合。6. 验收与答辩怎么证明这套IAP方案真的可靠验收这套方案不能只看“升级成功一次”就完事。我一般会按下面这个清单逐项过验收项测试方法合格标准正常升级发送完整固件观察跳转升级后版本号正确功能正常断点续传发送到一半断开重新连接能从断点继续不重复擦写断电恢复升级过程中断电重新上电设备能启动bootloader能重新进入升级模式CRC校验故意发一包错误数据bootloader拒绝写入报校验失败超时重传模拟链路丢包重传3次内成功超过3次报错回滚发送错误固件校验失败跳回旧应用程序设备正常工作大固件发送接近Flash容量的固件分段搬运正常升级后功能正常答辩的时候评委最常问的三个问题第一升级过程中断电怎么办第二怎么保证新固件是完整的第三bootloader本身怎么升级前两个问题上面的验收项已经覆盖了。第三个问题是bootloader的自升级通常的做法是在bootloader里预留一个“更新bootloader”的命令把新的bootloader写到缓存区然后跳转到缓存区执行由缓存区的代码擦写bootloader区。这个功能一般项目用不到但答辩时能说出来说明你想过边界情况。还有一个技巧答辩时现场演示一次完整的升级流程从发送固件到跳转成功全程不超过2分钟。演示前把设备断电证明是从零开始。演示后再故意发一个错误固件展示回滚机制。这两步做完基本不用再解释什么了。从那以后我每次做IAP项目都会在参数区里多留一个“升级次数”计数器每次升级成功加一。这个计数器在答辩时特别有用——评委问“你怎么证明升级了”直接读计数器比任何解释都直观。希望帮到你。本文还有配套的精品资源点击获取