公司动态
Ymodem协议详解:从帧格式到串口在线升级实战
简介Ymodem协议由Xmodem增强而来适合在低带宽环境下传输文件它把文件分割为1024字节的数据块通过双重CRC校验保证完整性接收方发现错误可请求重传并支持多文件连续传输。这份Qt工程示例专门面向串口通信开发者和嵌入式工程师演示如何在QT框架下实现可靠的批量文件传输。压缩包内共48个文件约1.69MB包含C源代码、Qt界面设计文件、pro工程配置、Makefile以及可直接运行的exe程序结构完整清晰资源上线以来已有377人浏览学习。代码按Ymodem发送与接收两个核心模块组织涵盖串口交互、数据包解析与构建、QFile文件读写等关键实现借助Qt信号槽机制还能直观理解异步串口通信和事件驱动编程的落地方式。工程可在Qt Creator中直接打开编译配合源码注释逐行调试适合用于串口升级、固件下载等实用场景的二次扩展。 做嵌入式开发的朋友对“串口在线升级”这个需求应该都不陌生。板子已经装到客户现场程序发现Bug总不能拆壳、上编程器重新烧录吧所以bootloader里加一个串口升级功能几乎是量产设备的标准动作。而Ymodem协议就常年出现在这种场景里。前面说Xmodem后面说Zmodem实际项目中嵌得最多的反而是Ymodem。它是Xmodem的扩展版最大的特点是传输的是“文件”而不是裸数据——文件名、文件大小都会一起发过来还支持一次连续传多个文件。这在bootloader升级APP、批量烧写字库或者参数分区的时候特别有用。这篇文章不绕弯子直接讲Ymodem的帧格式、传输状态机、上位机/下位机实现细节以及我在实际调试中踩过的一堆坑。正在做串口升级或者想把Ymodem彻底搞明白的开发者建议收藏。1. Ymodem协议原理与整体设计思路1.1 为什么串口在线升级认准Ymodem串口文件传输协议里经常被拉出来对比的就是Xmodem、Ymodem和Zmodem这三个。Xmodem出现最早协议最简单但它一次只能传一个文件而且标准模式用128字节块传输发一个几百KB的固件能等到怀疑人生。Zmodem功能最强支持断点续传和更复杂的命令交互但在MCU端实现成本高bootloader里放不下那么多代码也没必要。Ymodem正好卡在中间它兼容Xmodem的1K块模式1024字节一块还加入了对文件名的传输。接收端拿到固件之前就能知道这个文件叫什么、多大方便做固件版本判断和大小预校验。相比之下Xmodem传过去的东西没有文件名你都不知道烧进去的是哪一版固件。我把几个协议放在一起对比过直接看表更清楚协议最大块大小文件名/大小多文件批处理断点续传MCU实现难度Xmodem128B不支持不支持不支持低Xmodem-1K1024B不支持不支持不支持低Ymodem1024B支持支持不支持中Zmodem32KB支持支持支持高所以在“MCU资源有限需要升级固件还想知道文件名和大小”这个组合下Ymodem几乎是唯一优雅的选择。它不需要复杂的协议栈几百行C代码就能实现接收端。1.2 帧格式拆解协议的基本单元要把Ymodem谈透先得把帧格式搞明白。Ymodem传输过程中所有数据都以“块”Block为单位块分两种128字节块和1024字节块。128字节块格式SOH0x011字节表示这是一个128字节块块号1字节从0开始递增块号取反1字节即0xFF - 块号用于校验块号正确性数据区128字节CRC16高字节、低字节各1字节1024字节块格式STX0x021字节表示这是一个1024字节块块号、块号取反数据区1024字节CRC16高字节、低字节除了块之外Ymodem还有一组单字节控制字符接收端和发送端靠它们握手。我经常让工程师把这些字符记在代码注释里字符值含义SOH0x01128字节块起始STX0x021024字节块起始EOT0x04传输结束ACK0x06数据确认NAK0x15请求重发/否定确认CAN0x18取消传输C0x43请求传输/下一个包这里最容易忽略、也最重要的其实是“块0”的设计。Ymodem在传输第一个数据块之前会先发一个特殊的块0数据区放的不再是固件内容而是ASCII字符串形式的文件名、文件大小用空格分隔。比如发送端传一个firmware.bin、大小131072字节的文件块0数据区就是firmware.bin 131072接收端收到块0解析出文件名和大小继续后面的数据块。这就是Ymodem和Xmodem的核心差异所在——Xmodem没有这个“前导信息帧”Ymodem有而且这个设计直接支持了多文件批处理传完一个文件后发送端再一次发块0接收端就能知道属于下一个文件。2. Ymodem传输时序与状态机2.1 一次完整升级的流程拆解写Ymodem接收端本质是写一个状态机。我把一次完整传输掰开揉碎按接收端视角列一下接收端上电初始化发送一个C0x43表示“我已经准备好用CRC方式接收”。发送端收到C发送块0文件名文件大小。接收端收到块0校验CRC16正确、解析出文件名和大小后回复ACK然后再次发送C请求数据。发送端收到C开始发送数据块。块号从1开始递增每块发完后等待接收端响应。接收端每收到一个数据块做CRC16校验。校验通过回复ACK失败则回复NAK发送端收到NAK会重发当前块。数据全部发送结束后发送端发送EOT0x04。接收端收到EOT这里不同实现有差异标准Ymodem会回复NAK发送端收到NAK后再发一个EOT接收端再回复ACK确认结束。也有实现是直接回复ACK。结束确认后接收端再发C等待下一个文件或者结束块。如果还有下一个文件发送端发送新的块0重复步骤4-8。如果所有文件都传完了发送端会发送一个文件名为空的块0数据区全是0x00填充接收端收到后回复ACK本轮升级结束。重点提醒一下步骤7到步骤10不同上位机行为很不一样。比如SecureCRT发单个文件时往往在第一次EOTACK后就认为传输结束了不会再发空文件名块0。而一些国产串口工具会严格按照批处理流程走完空块0才算完。所以MCU接收端要兼容这两种结束方式——既要能处理EOT之后直接结束也要能处理EOT之后再来一个空块0。2.2 Ymodem与Ymodem-G的选择问题Ymodem还有一个变体叫Ymodem-G很多初学者会踩坑。Ymodem-G和标准Ymodem的最大区别是它不要求接收端对每个数据块回复ACK/NAK而是连续发送数据只在对方发来CAN取消时才中断重发。这个设计看起来更高效但有个隐含前提通信链路必须可靠。所以Ymodem-G一般配合硬件流控RTS/CTS使用防止UART缓冲区溢出。在没有流控的普通USB转串口上直接上Ymodem-G很容易丢包而且发送端因为收不到NAK根本不会重传文件就永久性地错下去了。我自己的习惯是只要是普通串口调试、没有硬件流控就用标准Ymodem慢一点但稳如果链路质量有保证、波特率也高而且两端都支持RTS/CTS才考虑Ymodem-G。3. 实操从零搭建Ymodem在线升级链路3.1 上位机发送端SecureCRT和Python脚本PC端发送固件最省事的办法是用现成工具。如果你用的是SecureCRT操作流程是设备端进入bootloader并发出C之后在SecureCRT菜单栏选择“Transfer” - “YMODEM” - “Send File”选中固件bin文件即可。Xshell也差不多传输方式里选YMODEM。但工具是图形界面的不适合自动化测试和产线场景。我一般会写Python脚本用python-xmodem这个库来发固件。它的用法非常直接import serial from xmodem import YMODEM def send_firmware(port, baudrate, filepath): ser serial.Serial(port, baudrate, timeout1) def getc(size, timeout1): return ser.read(size) or None def putc(data, timeout1): return ser.write(data) modem YMODEM(getc, putc, modey) with open(filepath, rb) as f: modem.send(f) if __name__ __main__: send_firmware(COM10, 115200, firmware.bin)getc和putc是YMODEM库的底层读写钩子库内部会自动处理握手、CRC校验和重传。需要注意的是modey如果不写这个参数默认是标准Xmodem块0文件名信息就发不出去了。这个脚本在Windows和Linux下都能跑放到产线工具里也完全够用。3.2 MCU接收端状态机实现接收端的核心是状态机我直接给一个框架性的C代码片段。它不依赖具体硬件平台串口发送和接收函数改成你自己平台的API就行typedef enum { YM_WAIT_C, /* 初始状态发送 C 等待块0 */ YM_WAIT_BLOCK0, /* 收到块0解析文件名和大小 */ YM_WAIT_DATA, /* 接收数据块 */ YM_WAIT_EOT, /* 收到EOT处理结束流程 */ YM_DONE /* 传输完成 */ } ym_state_t; static ym_state_t state; static uint8_t rx_buf[1024 16]; static uint16_t block_no; /* 串口中断里收到一帧数据后调用data和len是完整一帧 */ void ymodem_on_frame(uint8_t *data, uint16_t len) { switch (state) { case YM_WAIT_BLOCK0: if (data[0] SOH data[1] 0x00 data[2] 0xFF) { /* 解析文件名和大小保存到全局变量 */ uint8_t *file_info data[3]; // parse_filename_size(file_info); uart_send(ACK); state YM_WAIT_DATA; block_no 1; } break; case YM_WAIT_DATA: if (data[0] EOT) { uart_send(ACK); /* 有的上位机这里会继续发空块0所以回到等待块0 */ state YM_WAIT_BLOCK0; break; } if (data[0] SOH || data[0] STX) { uint16_t expect_len (data[0] SOH) ? 128 : 1024; uint8_t cur_no data[1]; uint8_t cur_neg data[2]; if (cur_no ! block_no || cur_neg ! (uint8_t)(~block_no)) { uart_send(NAK); /* 块号不对请求重发 */ break; } if (crc16_buf(data[3], expect_len) ! (data[3 expect_len] 8 | data[4 expect_len])) { uart_send(NAK); /* CRC错误请求重发 */ break; } /* 先把数据存到临时缓冲区不要直接在中断里擦写flash */ memcpy(flash_buf, data[3], expect_len); // write_flash_pending(flash_buf, expect_len); uart_send(ACK); block_no; } break; default: break; } }这段代码只展示了核心逻辑。实际工程里串口接收建议用“串口字节中断 环形缓冲区”或者“DMA 空闲中断”先完整接收一帧再交给协议层解析。块号处理上要注意块号是8位传完255会回绕到0虽然固件一般到不了这个大小但状态机要保持无符号8位递增的写法。3.3 CRC16校验与Flash写入避坑点Ymodem的CRC用的是CRC-CCITT多项式0x1021初值0x0000。网上有很多现成实现表驱动版速度快适合MCUstatic uint16_t crc16_ccitt(uint8_t *buf, uint16_t len) { uint16_t crc 0x0000; for (uint16_t i 0; i len; i) { crc ^ (uint16_t)buf[i] 8; for (uint8_t bit 0; bit 8; bit) { if (crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } } return crc; }这里有个常见的坑Ymodem协议里CRC16是“高字节在前、低字节在后”发送的不少人在接收端比对时写反了高低字节导致明明数据是对的CRC却一直错误。排查时先用固定的测试数据比如发128字节全0x00的包在PC上算出CRC值再对比MCU收到的字节序。Flash写入比CRC更隐蔽。很多工程师踩过的坑是协议层一收到数据块就在串口中断或者协议处理函数里直接调用Flash擦写操作。擦写一页几百毫秒甚至更久这时候UART接收缓冲早就溢出了下一块数据直接丢。正确做法是先把数据复制到RAM缓冲区等协议收完一帧、返回主循环或者低优先级任务后再执行Flash写入。如果硬件平台支持ping-pong缓冲可以在后台写上一块数据的同时串口收下一块升级速度能提升不少。4. 常见问题与排查技巧实录4.1 接收端发了一堆‘C’上位机就是没反应这个问题90%出在串口参数上。Ymodem要求数据位8位、无校验、停止位1位8N1波特率两端必须一致。其次是硬件流控上位机里如果误开了RTS/CTS而板子没有接流控线发送端会一直等RTS电平自然不发数据。还有少量情况是USB转串口芯片驱动有问题建议先用串口助手手动发一个C确认PC能收发再跑协议。4.2 块0能收到但文件名解析出来是乱码块0里文件名是ASCII字符串如果解析乱码一般不是Ymodem的问题而是接收端在块0处理时把块0前面的SOH、块号、取反这些头字节也算进数据区了。注意块0的数据区从第4个字节开始data[3]文件名和大小之间是一个空格如果实现里按\0截断字符串先确认发送端是用的空格分隔而不是\0。SecureCRT发送时文件名和大小之间就是空格。4.3 传输中途频繁NAK、或者进度卡在某个百分比不动这几乎是嵌入式升级最典型的故障。先看波特率115200以下一般问题不大上了460800或921600如果线材质量差、地线没接好误码率会明显升高。Ymodem有重传机制但重传超过一定次数就放弃了。另一个被忽略的点是在串口中断里做Flash擦写导致串口数据丢失。前面说过擦写Flash要放到后台执行。还有些平台串口中断优先级不够被高优先级定时器长期抢占也会丢字节。你可以用串口助手抓数据如果发现NAK重传后发送端重发的数据长度变短或帧头不对基本就是接收端丢字节了。4.4 传完EOT之后设备不结束、一直等这个问题在文章前面提过是结束流程兼容性造成的。处理办法是把结束状态机写得宽容一些收到第一次EOT后回复ACK开启一个超时窗口如果上位机继续发空块0就处理空块0后结束如果超时没有新数据也直接结束整个升级流程跳转到APP执行。这种兼容逻辑能同时适配SecureCRT、Xshell和Python脚本。4.5 问题排查速查表现象常见原因解决方向上位机不传文件串口参数不对、硬件流控开启改8N1关闭流控块0收不到数据位/校验位配置不一致检查UART配置文件名乱码数据区起始地址错误确认偏移为data[3]频繁NAK波特率太高、丢字节、CRC实现不对降波特率、后台写Flash、检查CRC字节序收完EOT不结束结束流程兼容问题用超时窗口兼容空块0流程升级后程序不跑APPCRC校验失败、跳转地址错误全片校验固件CRC检查跳转条件最后说一点我自己的心得Ymodem这东西看协议文档总觉得简单真正调试起来全是细节。我记得第一次调升级功能卡在CRC字节序上整整两天最后还是用逻辑分析仪抓出来的那一刻才真正理解什么叫“协议规范写得很清楚但实现各有各的脾气”。如果你也在做串口升级我的建议是先把状态机画清楚再写代码上位机工具多备几个SecureCRT、Xshell、Python脚本轮流测别只盯着一个工具的行为。最后在接收端加一套日志打印把收到的帧头、块号、CRC都打出来对比协议文档慢慢核对Ymodem其实没有想象中那么难缠。本文还有配套的精品资源点击获取