简介:面向华大半导体HC32L130/HC32L136系列单片机,这是一套基于YModem协议的IAP在线升级完整方案,内含BOOTLOADER源码与PC端上位机源码,适合需要实现固件远程更新、降低现场维护成本的嵌入式开发人员,也适合有一定C语言与单片机基础的开发者对照学习。资源共317个文件、压缩包仅3.38MB,以C源码(39个.c、57个.h)与IAR/Keil双平台工程文件为主,辅以xcl/icf链接脚本、hex/bin/axf编译输出、J-Link烧录脚本、PDF说明及SVD外设描述文件,覆盖从工程配置、编译链接、烧录调试到固件升级的完整链路。已有1057人学习下载。这份资料的核心价值在于给出了可直接参考的IAP实现:Bootloader与APP分区管理、YModem分块传输与CRC校验、Flash擦写与跳转流程、上位机交互界面等关键代码均清晰可查;同时工程结构预留了移植接口,稍作配置即可迁移至华大ARM0系列其他型号,对深入理解串口协议、在线升级机制与嵌入式软件架构都很有帮助。 做单片机开发的,迟早会碰上一次“产品已经铺出去了,固件却还要改”的尴尬。以前要么把设备拆回来,要么带着JTAG/SWD现场刷,费时费力,客户还不一定愿意配合。我最近把华大(小华)HC32L130和HC32L136两个系列低功耗MCU的项目整理了一遍,用一套基于YModem协议的IAP Bootloader加上配套上位机源码,把串口升级这条链路彻底打通了。这篇文章就把这套方案的设计思路、Bootloader端实现、上位机联调方法,以及我实际踩过的坑全部写出来,给正在做同类IAP开发的朋友一个能直接参考的样板。
这套东西适合谁?如果你是做华大HC32L130/L136产品开发,或者刚接触IAP想搞懂YModem协议到底怎么落地,又或者已经写了一个Bootloader但总觉得不够稳,那这篇文章基本都能对上你的需求。整个过程不涉及复杂算法,但协议状态机、Flash分区、中断向量偏移、CRC校对这些细节,一个都躲不掉。
1. 项目整体设计与思路拆解
1.1 为什么选YModem而不是XModem或自研协议
很多人第一次做IAP时,第一反应是“协议不就这么几行吗,自己定一个算了”。我也这么干过——帧头、长度、数据、校验和,看起来简单,真用到产品上升级几十KB固件就开始难受了:文件名没地方放、文件大小要额外定义、分包序号得自己维护、传输中断后不知道传到哪一包。这些坑YModem协议早就替你填好了。
YModem是XModem的扩展版,经典串口传输协议。和自研协议、XModem放在一起对比,差距很明显:
| 特性 | 自研简单协议 | XModem | YModem |
|---|---|---|---|
| 包长 | 自定义 | 128字节 | 128/1024字节 |
| 文件名和大小 | 没有 | 没有 | 第0包携带 |
| 校验方式 | 自定义 | CRC16或校验和 | CRC16 |
| 分包序号管理 | 自己写 | 有,简单 | 有,完善 |
| 标准结束流程 | 自己定义 | 简单 | 二次EOT握手 |
| 现成调试工具 | 无 | 部分支持 | Tera Term、SecureCRT都支持 |
YModem最大的价值不是性能,而是生态。开发Bootloader阶段,我直接用Tera Term的YModem发送功能来验证协议,不用自己写上位机,先把Bootloader这边跑通;万一之后自己写的上位机出了Bug,还能用现成工具把固件刷进去救砖。如果当初选自研协议,这些方便全都没有。
1.2 Bootloader Flash分区方案怎么设计更稳
华大HC32L130和HC32L136都是Cortex-M0+内核的低功耗MCU,不同型号Flash和RAM容量有差异。以我项目里用的HC32L136K8TA为例,内置64KB Flash、8KB RAM,分区我是这样安排的:
0x00000000 ~ 0x00001FFF:Bootloader区,8KB,足够放下YModem协议、串口驱动和Flash操作代码。0x00002000 ~ 0x0000EFFF:App区,52KB左右,这是产品主程序的位置。0x0000F000 ~ 0x0000FFFF:参数区,存放升级标志、App CRC校验值等。
具体起始地址要根据你手里的型号Flash大小调整,但有两个原则必须守住:第一,Bootloader不要贪大,够用就行,把空间尽量留给App;第二,App起始地址一定要对齐Flash扇区边界。有些型号扇区是1KB,有些是2KB或更大,不对齐的话擦除操作会误伤前一个分区。
App工程里的链接脚本(.ld文件或分散加载文件)也要同步改:Flash起始地址设置成0x2000,长度改成剩余大小。编译出来的bin文件就是App区镜像。这里有个M0+平台共性问题——中断向量表偏移。如果芯片支持VTOR,在App启动早期设置好偏移;如果找不到VTOR或者华大固件库版本较老,就需要查芯片手册,用启动文件或库里提供的向量表重映射方式处理。跳转前没搞定向量表偏移,App一进中断就死给你看。
1.3 上位机源码用什么框架写
这套源码里的上位机我用的是C# WinForms,原因很简单:Windows下部署方便,串口操作封装成熟,做调试工具UI效率也高。界面分四块:串口配置区(串口号、波特率、连接按钮)、固件文件选择区、下载控制区(开始、取消、进度条)、日志显示区(RichTextBox带时间戳)。
代码结构上我把协议层单独抽出来,不跟UI混在一起。上位机内部拆成三层:
- UI层:只负责按钮事件、刷新进度、显示日志。
- YModem协议层:构造文件头包、数据包、解析ACK/NAK/CRC。
- 串口通讯层:封装SerialPort读写、超时控制。
这样以后如果客户机器要换WPF界面,或者想把上位机移植成Qt版本,协议层和通讯层可以直接复用,不用重写核心逻辑。
2. Bootloader端核心细节与实现要点
2.1 YModem协议状态机到底怎么转
YModem的传输过程配得上“经典”两个字,但第一次看协议文档容易懵。我用最直白的方式描述一遍:
- Bootloader上电,检查升级标志或者按键,决定是否进入IAP模式。
- Bootloader作为接收方,先发一个字符
C,告诉发送端“我准备好了,支持CRC16”。 - 上位机收到
C后,发送第0包:SOH + 00 + FF + 文件名 文件大小 + 填充 + CRC16。 - Bootloader解析出文件名和大小,回复
ACK + C。 - 上位机开始发数据包:
STX + 序号 + (255-序号) + 1024字节数据 + CRC16。 - Bootloader每收一包,CRC校验通过就回
ACK,校验失败就回NAK,上位机收到NAK会重发当前包。 - 数据全部发完后,上位机发
EOT,Bootloader回NAK;上位机再发一个EOT,这次Bootloader回ACK。 - 上位机接着发结束包:
SOH + 00 + FF + 全0填充 + CRC16,Bootloader回ACK。 - 全部结束,Bootloader校验App区数据,跳转。
第一次搞YModem的人基本都会疑惑:为什么EOT要发两次?这不是冗余,是协议规定的握手流程。第一次EOT表示“数据文件传完了”,接收方回NAK是为了告诉发送端“我确认了,但还没收到会话结束包”;第二次EOT之后回ACK,表示可以发送结束包了。这个设计能有效避免半包关闭连接被误判成正常结束。
协议解析代码我建议写成事件驱动状态机,不要在接收函数里用阻塞式等待。核心状态枚举大概是这样的:
typedef enum { YM_STATE_WAIT_C, YM_STATE_RECV_FILENAME, YM_STATE_RECV_DATA, YM_STATE_WAIT_EOT, YM_STATE_WAIT_END_PACKET, YM_STATE_FINISH } ym_state_t;串口中断或者DMA收到一字节后,喂给状态机;超时用定时器单独管。好处是接收过程中还能响应超时重发、取消升级等操作,代码也不容易堆成一坨。
2.2 Flash擦写与跳转的代码逻辑
华大官方固件库提供了Flash操作接口,逻辑上分三步:解锁、擦除、编程。典型代码如下:
void flash_write_app(uint32_t addr, uint8_t *buf, uint32_t len) { FLASH_Unlock(); FLASH_SectorErase(addr); // 按扇区擦除,不支持随机写之前不擦 for (uint32_t i = 0; i < len; i += 2) { uint16_t data = buf[i] | (buf[i + 1] << 8); FLASH_Program(addr + i, data); // 按半字编程 } FLASH_Lock(); }Flash操作有几个硬性要求:写之前必须擦,编程通常按半字或字对齐,擦写期间不能进中断。实际项目里我会先把整个扇区数据缓存到RAM,关串口中断,一次性把该扇区擦完再写入,再把中断打开。否则边擦边收串口数据,很容易丢包。
跳转App的代码是另一个关键点:
#define APP_START_ADDR 0x00002000 void jump_to_app(void) { uint32_t app_sp = *(volatile uint32_t *)APP_START_ADDR; uint32_t app_pc = *(volatile uint32_t *)(APP_START_ADDR + 4); if ((app_sp & 0xFFF00000) == 0x20000000) { __disable_irq(); SysTick->CTRL = 0; // 根据具体型号决定是否设置向量表偏移 // SCB->VTOR = APP_START_ADDR & (uint32_t)0xFFFFF800; __set_MSP(app_sp); ((void (*)(void))app_pc)(); } }检查app_sp是否落在RAM地址范围,这一步不能省。Flash空白时读出来全是0xFF,直接跳转只会进HardFault。跳转前把所有中断关掉,关闭SysTick和外设,不然App启动过程中突然来一个串口中断,向量表还没切好,同样要翻车。
2.3 CRC16校验函数必须和上位机对齐
YModem标准使用CRC16-CCITT,也叫XMODEM CRC16,多项式0x1021,初始值0x0000。网上流传的CRC16变种很多,有的初值、结果异或不一样,上位机和Bootloader各用一套,协议就根本对不上。
我在Bootloader里用查表法版本,稳定且速度快:
static uint16_t crc16_update(uint16_t crc, uint8_t byte) { crc ^= (uint16_t)byte << 8; for (int i = 0; i < 8; i++) { if (crc & 0x8000) crc = (crc << 1) ^ 0x1021; else crc <<= 1; } return crc; }上位机C#里的实现,多项式、初始值、字节处理顺序必须和这个完全一致。联调时最容易出问题的地方就是这里。经验是:先在PC上用两个串口工具互发YModem文件,验证协议层CRC实现是否正确,再往单片机上下载,能省下大量排查时间。
3. 上位机源码与通讯调试
3.1 C#串口上位机界面和防卡死处理
上位机界面不算复杂,关键的控件是这几个:
- 串口选择下拉框、波特率下拉框,波特率默认给115200或者57600,但产品上我不建议一上来就用太高。
- 连接/断开按钮、选择文件按钮、开始升级按钮、取消按钮。
- 进度条加百分比文本框。
- 日志RichTextBox,每一行都带时间戳。
C#里用SerialPort类操作串口很简单,但有个坑:DataReceived事件是在后台线程触发的,直接在事件里更新UI会报线程间操作异常,必须用Invoke或者async/await。我通常用BaseStream.ReadAsync配合异步方法,代码更清爽:
using (var port = new SerialPort("COM3", 115200, Parity.None, 8, StopBits.One)) { port.Open(); await Task.Delay(50); byte[] cmd = new byte[] { (byte)'C' }; port.Write(cmd, 0, 1); }升级动作放到BackgroundWorker里执行,文件读取、分包、串口写入这些可能在几十毫秒到几秒的操作不会把界面卡死。日志框统一走一个AppendLog方法,内部用锁保证多线程写入安全。
3.2 文件分包和YModem发送流程实现
上位机把bin文件读进来之后,按1024字节拆包,最后不足1024字节的部分用0x1A填充。YModem标准填充符是0x1A(SUB),虽然很多Bootloader不关心填充值,但既然协议有约定,就按标准来。
第0包文件名格式是ASCII字符串,形如:
firmware.bin 53248文件名和文件大小之间是一个空格,后面跟上十进制文件长度。分包和发送核心逻辑大致如下:
byte seq = 1; while (remaining > 0) { byte[] packet = BuildDataPacket(seq, dataChunk); port.Write(packet, 0, packet.Length); // 等待ACK或NAK,超时则重发当前包 bool ack = WaitForAck(1000); if (ack) { seq++; offset += chunkLen; UpdateProgress(offset, fileSize); } else { RetryCount++; } }数据包序号从1开始,最大255,超过后回绕到0。每个数据包都要带255 - seq作为序号取反,接收端用这个判断包是否重复或乱序。别小看这个字节,它专门用来应对串口丢包后的重传场景。
3.3 联调时最值得先做的三件事
第一,先不碰板子,用Tera Term的YModem发送功能把Bootloader调通。Tera Term能直接选YModem协议发送文件,如果Bootloader能正常下载并跳转,说明协议栈本身没问题,剩下要看的就是自己上位机的锅。这一步能极大缩小排查范围。
第二,用逻辑分析仪或串口监听工具抓一次完整交互。重点看三处:Bootloader发出C之后,上位机有没有立刻回第0包;上位机发完数据包后,Bootloader回的是ACK还是NAK;传输结束后二次EOT流程有没有走完整。YModem是半双工一问一答,只要看一个交互循环,问题卡在哪立刻清楚。
第三,波特率先降到9600或者19200跑通全流程,再逐步提上去。很多低成本板子用内部RC振荡器,标称24MHz实际误差不小,115200下容易错一两个bit,表现为包序号乱跳、CRC错。等到协议稳定了,再评估能不能用更高的波特率。
4. 踩坑记录与排查技巧
4.1 常见问题速查表
我把这次项目里遇到的高频问题整理成一张表,排查时直接对照:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 上位机点了发送,板子没反应 | Bootloader没进入接收模式;C没发出 | 检查升级标志/按键逻辑;用串口助手手动发C测试 |
| 一直卡在文件头 | 第0包CRC算错;文件名格式不对 | 抓包对比Tera Term发出的第0包内容 |
| 数据包传一半回NAK | Flash擦写时间超过串口超时 | 分扇区擦除并缓存数据;上位机加大响应超时 |
| 跳转后App跑飞 | 向量表偏移没设;App链接地址错误 | 查App工程FLASH起始地址;确认跳转前关中断 |
| 整包下载成功但App不运行 | Flash编程参数或写入地址不对 | 用调试器读回Flash内容,和bin文件逐字节比对 |
| 偶尔下载失败,重试又成功 | 波特率偏高或供电不稳 | 降波特率;检查板子电源纹波;串口线缩短 |
4.2 升级失败的容错和恢复机制
产品级IAP最怕什么?传输到一半断电,设备变成砖头。所以Bootloader里一定要有“升级标志位”机制。思路是这样的:参数区放一个固定值,比如0xA5A5表示“升级中”。上位机开始发送前,先通过一条命令让Bootloader把这个标志写成升级中;全部数据接收完成,并且App区CRC校验通过后,Bootloader把标志清掉再跳转。
Bootloader每次上电都检查这个标志:
- 标志不是升级中,且App区CRC校验通过,直接跳App。
- 标志是升级中,说明上次升级没完成,不跳App,留在IAP模式继续等待接收新固件。
- App区校验失败也留在IAP模式。
这样即使升级过程中突然断电,下次上电Bootloader依然会进入IAP等待状态,不会变砖。有些产品还想做双Bank备份(A区坏了从B区启动),但对HC32L130/L136这种小Flash芯片来说,双Bank占用的空间实在太大,我用“升级标志+App区CRC校验”就足够了,关键是可靠。
4.3 实际测试中我总结的几条经验
串口缓冲区一定要开大。1024字节的YModem数据包,如果只用串口中断逐字节接收,在低优先级中断频繁被打断的情况下很容易丢字节。我建议Bootloader端用DMA接收配合空闲中断,一包数据到齐后统一处理,效果最好。如果固件库版本不支持空闲中断,也要保证接收缓冲区和环形队列足够大。
上位机超时重发不能太急。我把响应超时设到1000ms,因为Flash擦一个扇区在低功耗芯片上可能耗时几十毫秒到上百毫秒,加上波特率低时包传输本身就要几百毫秒,超时设太短会导致发送端和接收端状态叠加混乱。
测试固件准备三份:一个正常的App、一个故意加了打印的App、一个全空的bin。正常的用来验证下载和跳转;加打印的用来验证跳转后App是否真正接管外设;全空的用来验证Bootloader对非法App的拦截逻辑。
开发阶段保留UART日志,等整套方案稳定后再把日志关掉,或者换成加密固件。日志虽然占Flash空间,但现场出问题时,一串清晰的日志比什么调试工具都管用。
最后再分享一个个人体会:这套IAP从画分区到完全跑通,我前后花了将近三周,一半时间耗在协议对齐和Flash驱动细节上,协议本身反而是最快写完的。做Bootloader,最重要的不是功能多花哨,而是稳定、可恢复、可兜底。只要把YModem协议、CRC校验、掉电保护和跳转逻辑都验证透了,后面的App迭代基本就是打开上位机、点一下下载的事。
本文还有配套的精品资源,点击获取