
简介这套源码以C语言在Linux环境下实现Ymodem串口文件传输协议面向嵌入式系统开发者、串口通信研究人员以及需要了解经典传输协议的工程人员尤其适合需要实现串口文件传输或深入研究协议栈细节的学习者。Ymodem支持一次传输最多16KB数据块并具备校验机制适合远程监控、设备固件升级及无网络环境下的数据交换。压缩包内共4个文件包括3个C程序文件与1个头文件整体仅10KB程序文件按发送、接收等模块组织头文件定义协议接口及错误码便于阅读移植。目前已有616人学习下载。通过阅读源码既可以梳理Ymodem的握手流程、错误恢复策略也能根据实际需求调整数据块大小、补充安全特性为后续在ARM等异构平台上的串口通信开发提供了可复用的参考实现。1. 串口升级卡在协议栈里我翻出了这份 Ymodem 源码一个嵌入式开发最常见的画面bootloader 烧好后拿着一条 USB 转串口线给板子上传固件。minicom 的「发送文件」菜单里摆了 Zmodem、Ymodem、Xmodem我选了 Zmodem结果板子那边没有任何反应。后来才发现Bootloader 只实现了 Ymodem而 PC 方默认发了 Zmodem。手边这份 ymodem.rar 里正好有一套 C 语言写的 Ymodem 实现ymodem.c、ymodem.h、send.c、receive.c 四个文件就能组成收发闭环。下面按串口传输的实际过程把帧格式、Linux 串口配置、错误码和交叉编译的坑逐个拆开适合既想理解协议原理又急着把固件跑起来的开发者。2. 帧格式与会话状态机把 ymodem.c 拆开看2.1 Ymodem 与 Xmodem 的差异决定你选哪份代码Ymodem 是 Xmodem 的增强版最直观差异在两点数据块长度最大到 1024 字节而 Xmodem 只有 128 字节同时在文件内容前多了一个编号为 0 的块用来传文件名、长度和时间戳。正是块 0 的存在让 Ymodem 特别适合嵌入式 IAP 升级bootloader 可以从块 0 里拿到固件名和长度不必预先在编译期写死固件大小。源码里ymodem.c是协议纯状态机ymodem.h是唯一公开头文件send.c和receive.c分别作为发送端与接收端示例。我打开ymodem.h后先看帧结构定义它决定了后面所有解析逻辑#define YMODEM_CRC 0x01 #define YMODEM_ACK 0x06 #define YMODEM_NAK 0x15 #define YMODEM_EOT 0x04 #define YMODEM_C 0x43 typedef struct { uint8_t seq; /* 帧序号 */ uint8_t seq_comp; /* 序号按位取反 */ uint8_t data[1024 1]; /* 数据区首字节是实际数据长度 */ uint16_t crc; /* CRC16-XMODEM */ } ymodem_frame;这是一份简化后的帧结构实际传输时外层还要加 SOH/STX 控制字节不过程序里收发双方都保持一致就行。data[10241]里的 1 字节是长度字段如果实际数据不满 1024接收端只取长度个字节计入 CRC其余丢弃。正因有这个长度字段低速场景下可以主动把每次发送的数据压到 128 字节协议依然兼容。块 0 的数据区不是裸文件而是一个以 NULL 分隔的字符串例如app.bin\0后跟文件大小十进制文本再接修改时间。接收端解析完块 0 后才创建文件所以任何想移植到 bootloader 的变体都必须保留对块 0 的解析否则第一包会被当成固件头写进 Flash。2.2 发送端状态机从发起握手到 EOT 结束send.c 的入口逻辑不复杂先等待接收方发出字符C0x43表示接收方准备按 CRC 模式握手如果收到NAK0x15则退化为校验和模式。这里有个实际容易踩的坑很多老式串口助手点完「接收」后只会发NAK发送端会被迫切回校验和模式而目标板又不支持校验和双方就僵持到超时。调试时最好用支持自定义握手字符的工具或者直接让接收端主动发C。把发送端状态机收敛成一张表更直观发送状态发送方动作接收方正常响应异常分支HANDSHAKE等待C/NAK发C10 次超时退出SEND_HEADER发块 0 文件名/大小ACKC重发包 0SEND_DATA发数据帧ACKNAK则重传当前帧SEND_EOT发第一个EOTACK超时重发 EOTSEND_END发空块 0 结束会话ACK超时结束这张表里的 SEND_END 状态最容易被简化。有些人为了省时间只发文件内容不发送空块 0接收端第一次收到EOT后还会再等一个空块 0 才算完整结束。如果对方 bootloader 严格实现协议省略这个空块会导致第二次EOT被当成新文件头文件末尾会出现异常填充。2.3 CRC16 的实现位置与位序陷阱ymodem.c 里通常有一个独立的crc16()函数按 CRC16-XMODEM 实现多项式0x1021初值0x0000。接头时必须单独核对这段代码因为串口链路里每个字节都要靠它校验位序不对做什么都白搭。uint16_t crc16_xmodem(const uint8_t *p, size_t len) { uint16_t crc 0x0000; while (len--) { crc ^ (*p 8); for (int i 0; i 8; i) { if (crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } } return crc; }这是移位型软件实现不查表、不占额外内存。调用前的关键点是确认串口驱动没有翻转字节位序否则同一包0x1234双方算出来的 CRC 完全不同。标准验证向量是字符串123456789期望算出0x31C3。移植完先打印一遍结果不符先查输入顺序和初值。接收端收到数据帧后对data[0]到data[1长度-1]重新计算 CRC并与帧尾的 2 字节比较。这里有个细节1024 字节块和 128 字节块的 CRC 计算方法完全一致只是长度不同源码在构造帧时不需要区分块类型。提示跨平台移植时别忘uint8_t依赖stdint.h。如果代码里直接用unsigned char在某些编译器下unsigned char不是 8 位CRC 移位会多出高位噪声。3. 在 Linux 上编译、回环测试与串口调通3.1 最小可编译配置Makefile 与串口设备拿到这份源码后先确认 send.c 和 receive.c 是独立 main 程序它们通过ymodem.c复用协议函数。我在 Ubuntu 20.04 上直接编译唯一要补的是 POSIX 串口层。源码若没有串口封装需要自己建一个头文件用termios.h文件描述符来自open(/dev/ttyS0, O_RDWR | O_NOCTTY)。一个能直接编译的 Makefile 长这样CC gcc CFLAGS -Wall -O2 -D_GNU_SOURCE TARGET ysend yrecv all: $(TARGET) ysend: send.c ymodem.c ymodem.h $(CC) $(CFLAGS) -o $ send.c ymodem.c yrecv: receive.c ymodem.c ymodem.h $(CC) $(CFLAGS) -o $ receive.c ymodem.c clean: rm -f $(TARGET)-D_GNU_SOURCE必须写上cfmakeraw在 glibc 中需要该宏才会显式声明。如果只做交叉编译把第一行的CC换成arm-linux-gnueabihf-gccCFLAGS追加-marcharmv7-a其余不用改。编译完成后./ysend和./yrecv会把第一个参数当作串口设备名。有人会在板子上遇到设备名不是/dev/ttyS0而是/dev/ttyUSB0或/dev/ttyAMA0那只是驱动注册名不同协议层完全不感知。3.2 用 socat 创建虚拟串口在 PC 上做无硬件验证没有开发板时用socat建一对虚拟串口就能在 PC 上完整跑一遍 Ymodem 收发。这对调试协议逻辑特别有用socat -d -d pty,raw,echo0 pty,raw,echo0命令执行后输出两个伪终端例如/dev/pts/3和/dev/pts/4。raw模式避免 socat 做任何行缓冲或信号字符处理行为接近真实串口。然后在两个终端分别启动接收端和发送端./yrecv /dev/pts/3 -o /tmp/received.bin ./ysend /dev/pts/4 /home/user/firmware.bin传完后执行diff firmware.bin /tmp/received.bin输出为空说明协议栈工作正常。这一步是交叉编译到 ARM 板之前最廉价的验证手段我通常先在这里把改动过的 CRC 和状态机跑过一遍再烧板。3.3 串口参数设置波特率、数据位和流控Ymodem 对串口参数要求严格收发双方必须一致。最常用的组合是115200 8N1无硬件流控。termios 的关键配置如下struct termios tio; tcgetattr(fd, tio); cfsetispeed(tio, B115200); cfsetospeed(tio, B115200); tio.c_cflag ~PARENB; /* 无校验位N */ tio.c_cflag ~CSTOPB; /* 1 位停止位 */ tio.c_cflag ~CSIZE; tio.c_cflag | CS8; /* 8 位数据 */ tio.c_cflag ~CRTSCTS; /* 关闭硬件流控 */ tio.c_iflag ~(IXON | IXOFF | IXANY); /* 关闭软件流控 */ tcsetattr(fd, TCSANOW, tio);重点是清掉IXON/IXOFF/IXANY。串口线上的 XON/XOFF 流控可能吞掉 Ymodem 的数据字节0x13造成接收端等待一个永远收不到的帧。关闭后软件层由 Ymodem 自身的 ACK/NAK 机制保证可靠传输。参数项推荐值说明波特率115200也可以 57600双方需一致数据位8Ymodem 按字节流处理不支持 7 位模式停止位18N1 是默认组合校验位None数据完整性交给协议层 CRC16硬件流控off目标板常未接 RTS/CTS 线软件流控off防止0x13被流控吞掉3.4 用 4MB 固件做压力测试观察全过程前面 socat 测试只传小文件很难暴露出超时问题。建议直接用 4MB 左右的固件压测观察是否有偶发 CRC 错误dd if/dev/urandom of/tmp/test_4m.bin bs1M count4 ./yrecv /dev/pts/3 -o /tmp/out.bin ./ysend /dev/pts/4 /tmp/test_4m.bin如果中途出错在发送端前面加strace跟踪读写调用strace -xx -e traceread,write -o /tmp/trace.log ./ysend /dev/ttyUSB0 /tmp/test_4m.binstrace 日志里能看到每次 read 返回的时间间隔和字节数。如果 read 长时间返回 0说明串口超时配置不对去查VMIN和VTIME。这两个参数会在下一章错误码排查里反复遇到。4. 错误码分类与四个典型的排错现场4.1 错误码和实际原因怎么对应Ymodem 的 Linux 实现习惯把错误码定义在头文件里这份源码至少包含下面的映射关系宏数值实际含义排查方向YMODEM_OK0x00正常无YMODEM_ERR_CRC0x01CRC 校验失败位序、波特率、信号干扰YMODEM_ERR_SEQ0x02帧序号不匹配重传逻辑缺陷YMODEM_ERR_TIMEOUT0x03等待超时termios 里 VMIN/VTIMEYMODEM_ERR_CANCEL0x04收到 CAN 取消另一端异常退出或人为中断YMODEM_ERR_BUSY0x05串口被占用其他进程占用同一设备YMODEM_ERR_TIMEOUT最值得注意。很多从 Windows 移植过来的代码在 Linux 串口上直接调read()由于 termios 默认VMIN1read()会阻塞到至少收到 1 个字节协议层的超时永远走不到。正确做法是把VMIN设为 0VTIME设为 1 到 20让read()允许空超时返回再把返回值 0 当作一次超时事件喂给状态机。tio.c_cc[VMIN] 0; tio.c_cc[VTIME] 10; /* 100ms * 10 1s */VTIME单位是十分之一秒VTIME10表示 1 秒无数据就返回 0。若调得太小发送端连续发两帧时接收端会误判超时重传风暴就来了。4.2 常见故障一CRC 错但数据又差不多出现的画面是传输进行到接近末尾突然报YMODEM_ERR_CRC把收到的文件跟原文件比较只有末尾几十个字节不一样。这类问题大多不是 CRC 算法错而是串口线路或流控配置丢掉了尾包。我一般按下列顺序排查降低波特率到 57600 或 38400排除极限速率下的信号质量问题。检查CRTSCTS是否真的被清零有些 USB 转串口驱动会强制占用 RTS/CTS。在发送端打印每一帧的序号和 CRC定位是哪一个字节位翻转。发送端加-v参数后会输出类似frame seq3 size1024 crc0x31C3的日志./ysend /dev/ttyUSB0 /tmp/test_4m.bin -v 21 | grep crc如果错误集中在某个固定字节偏移就怀疑PARENB没清干净偶校验位顶掉了第 8 位。还有一种是系统时钟不准导致波特率漂移。ARM 板外部晶振偏差超过 2% 时115200 波特率下会持续产生比特错误。判断方法先发 100 个0x55特征字节接收端统计错误位位置。这种情况只能降波特率协议栈本身救不回来。4.3 常见故障二第二个 EOT 处理不完整Ymodem 会话结束包含两个EOT。第一个EOT表示当前文件结束接收端回复ACK紧接着发送端还要发一个空块 0表示整个会话结束。很多接收端实现只处理了第一个EOT第二个EOT被当成新文件头解析造成最后一包被错误覆盖。我一般会在 receive.c 里检查 EOT 状态判断if (ch YMODEM_EOT) { if (session_done) { return YMODEM_OK; /* 第二次 EOT整个会话结束 */ } else { send_ack(fd); session_done 1; continue; /* 继续等待第二个 EOT */ } }关键是把session_done状态位维护好不要在两个EOT之间重置文件写入指针。这个 bug 的典型表现是文件开头一致、末尾多出 1024 字节的全0或0xFF填充长度由写入策略决定。4.4 交叉编译后找不到符号或直接崩溃如果目标板跑 Linux用arm-linux-gnueabihf-gcc替代 gcc 就行。问题常出现在目标板 C 库版本旧所以编译时最好静态链接arm-linux-gnueabihf-gcc -Wall -O2 -marcharmv7-a -static \ -D_GNU_SOURCE -o yrecv_arm receive.c ymodem.c如果目标是裸机比如 Cortex-M 单片机就不能直接链termios。需要把 Ymodem 状态机与串口驱动解耦。做法是给 ymodem.c 增加一个ymodem_port.h声明port_read_byte和port_write_byte在硬件平台上实现为查询串口寄存器或中断缓冲。裸机环境没有printf日志时用 GPIO 翻转电平来观察状态机卡在哪一步。注意裸机编译器通常不支持_GNU_SOURCE别定义进去文件里若用到usleep要换成硬延时或系统 tick。5. 把这份 Ymodem 用到 STM32F411 IAP块 0 里塞进 CRC结束前多验一道如果你最终要解决的是「stm32f411 ymodem固件升级」用官方 Bootloader 里的 Ymodem 命令是最省事的但想在自定义 IAP 里复用这份源码有个小改动可以显著提升可靠性在块 0 的文件信息串里追加一列固件 CRC32。具体做法是发送端在读取固件文件后同时算出一个 CRC32写进块 0 的元数据中unsigned long crc32 compute_crc32(file_buf, file_len); snprintf(block0_data, sizeof(block0_data), app.bin%c%lu%c%lu, 0, file_len, 0, crc32);接收端等第一个ACK后解析文件名、长度并记录这个crc32。在所有数据帧接收完成前不着急写 Flash 应用区先放到 SRAM 缓冲或内部串行 Flash 的临时段等最后一个数据帧结束后对缓冲算一次同样的 CRC32两者一致才做 erase/program不一致直接发NAK终止升级。这样 CRC16 负责每一帧的比特错误CRC32 负责整个文件的完整性。很多官方 Bootloader 不做文件级校验传输中断就会烧出半块固件。加上这层后升级失败至少能保住旧固件。另一个实用技巧是调整数据块长度。Ymodem 默认最大 1024但在低速或无线串口上一帧 1024 字节的出错率高重传代价大。可以把发送端构造帧时的最大值改成 128协议依然兼容缺点是握手开销增加。实测 115200 波特率下4MB 固件用 1024 块比 128 块快约 40%但无线串口环境里 128 块明显更稳。我会留一个编译期宏YMODEM_DATA_SIZE让不同产品取不同值#ifndef YMODEM_DATA_SIZE #define YMODEM_DATA_SIZE 1024 #endif把帧构造和 CRC 计算长度都换成YMODEM_DATA_SIZE再跑一遍第 3 章的 socat 回环测试。如果 bootloader 固定从 Flash 起始地址读固件记得把块 0 里的文件名改成与 IAP 约定的分区名接收端按分区名找目标地址不要硬编码文件名。这样板子既能收 app.bin 也能收 boot.bin升级流程就不用额外定制了。本文还有配套的精品资源点击获取