前言: 在嵌入式开发中,一定要区分“打印调试”和“数据传输”的区别。 很多新人习惯了用 printf 和串口助手交互,觉得串口很简单。但当你面临“固件升级(IAP)”或“传输图片”等需求时,你会发现直接发数据(Raw Data)简直是灾难:丢一个字节,整个固件报废;没有校验,干扰导致花屏。
怎么解?这时候你需要给数据穿上一层“铠甲”,这层铠甲就是——通信协议。今天我们来拆解最经典、最纯粹的文件传输协议:Xmodem。
一、 为什么是 Xmodem?
在 TCP/IP 满天飞的今天,为什么还要学这个几十年前的协议?
-
极简主义:它不需要复杂的协议栈,几百行 C 代码就能搞定,非常适合资源受限的 MCU(如 STM32F0/F1 系列)。
-
通用性:几乎所有的终端软件(SecureCRT, TeraTerm, Xshell)都内置了 Xmodem 支持。
-
基石作用:搞懂了 Xmodem,你再去学 Ymodem(多文件传输)、Zmodem(流式传输),甚至是自定义协议,都只是换汤不换药。
简单来说,Xmodem 是嵌入式工程师从“写代码”进阶到“搞系统”的必修课。
二、 协议解剖:数据的“铠甲” (Packet Structure)
Xmodem 是一个半双工(Half-Duplex)协议,采用“一问一答”的模式。它不允许数据“裸奔”,所有数据必须打包成固定 132 字节 的格式。
我们可以把它想象成一列标准化的货运火车,每一节车厢(Packet)都长得一样:
| 字节偏移 | 字段名称 | 值/范围 | 说明 |
| 0 | SOH (Start of Header) | 0x01 | 包头。接收端看到它,就知道新包来了。 |
| 1 | Seq (Packet Number) | 0x01 ~ 0xFF | 包序号。从1开始累加,溢出后归0。 |
| 2 | ~Seq (1's Complement) | 0xFE ~ 0x00 | 包序号反码。这是 Xmodem 的精髓设计之一。 |
| 3 ~ 130 | Data (Payload) | 128 Bytes | 数据载荷。不管你要传什么,必须凑够128字节。 |
| 131 | CheckSum | 0x00 ~ 0xFF | 校验和。所有数据字节累加后的低8位。 |
老手视点:为什么要传“反码”?
新手可能会觉得第 2 个字节是浪费。其实不然,这是一个极低成本的容错设计。 在信道极其恶劣的情况下,噪音可能把“包序号”修改了。如果没有反码,接收端可能把第 5 包误判为第 6 包,导致拼装文件错误。 有了反码,接收端必须满足
Seq + (~Seq) == 0xFF才会收货。这是一个双重保险。
三、 通信握手:优雅的“一问一答”
Xmodem 的通信流程极其讲究“同步”。发送方(PC)和接收方(STM32)就像在进行一场严谨的对话。
我们用一张时序图来描述这个过程:
1. 启动阶段(The Startup)
-
STM32 (RX):上电初始化后,并不知道对方什么时候发数据。于是每隔几秒发送一个
NAK (0x15)。-
潜台词:“有人吗?我准备好接收 Checksum 格式的数据了,快发吧!”
-
-
PC (TX):一直在监听。一旦收到
NAK,立刻发送第一包数据。
2. 传输阶段(The Transfer)
-
STM32 收到数据包后,进行三步检查:
-
检查包头是不是
SOH。 -
检查
Seq和~Seq对应关系是否正确,且Seq是否是期望的下一包。 -
计算 128 字节数据的 Checksum,看是否与第 132 字节一致。
-
-
判定:
-
全对,回复
ACK (0x06)。PC 收到后发下一包。 -
有错,回复
NAK (0x15)。PC 收到后重发当前包。
-
3. 结束阶段(The Teardown)
-
PC 发完所有数据后,发送一个
EOT (0x04)(End of Transmission)。 -
STM32 收到 EOT 后,通常会先回一个
NAK(这是协议里的防误触机制,假装没听清)。 -
PC 再次发送
EOT。 -
STM32 这次回复
ACK。通信正式关闭。
四、 STM32 代码实战:状态机的艺术
在 STM32 上实现 Xmodem,切忌写成线性的 while 循环嵌套,那样会让代码陷入死局。状态机(State Machine)是处理通信协议的最佳范式。
以下是核心逻辑的伪代码提炼(适用于 HAL 库):
/* 协议常量定义 */
#define SOH 0x01
#define EOT 0x04
#define ACK 0x06
#define NAK 0x15
#define CAN 0x18
/* 接收主逻辑 */
int Xmodem_Receive(void)
{
uint8_t packet_buf[132];
uint8_t next_packet_num = 1; // 期望接收第1包
uint8_t retries = 0;
// 1. 启动握手:发送 NAK 拉起传输
UART_SendByte(NAK);
while (1)
{
// 2. 读取首字节(关键:必须带超时机制!)
// 如果这里死等,程序就卡死了
uint8_t header;
if (UART_Receive(&header, 1, TIMEOUT_1SEC) != OK) {
if (++retries > MAX_RETRY) return ERROR;
UART_SendByte(NAK); // 超时了,催一下
continue;
}
// --- 分支 A: 收到数据包头 ---
if (header == SOH)
{
// 接收剩余 131 字节
UART_Receive(packet_buf + 1, 131, TIMEOUT_1SEC);
// 校验环节
uint8_t seq = packet_buf[1];
uint8_t seq_inv = packet_buf[2];
uint8_t checksum = packet_buf[131];
// 逻辑严密的校验链
if ((seq + seq_inv) != 0xFF) { // 序号反码错误
UART_FlushRx(); // 清空缓存,防错位
UART_SendByte(NAK);
}
else if (seq != next_packet_num) { // 重复包或乱序
// 如果是上一包重发,回 ACK;如果是乱序,回 CAN 或 NAK
if (seq == next_packet_num - 1) UART_SendByte(ACK);
else UART_SendByte(CAN);
}
else if (CalcChecksum(&packet_buf[3], 128) != checksum) { // 数据校验错误
UART_SendByte(NAK);
}
else {
// === 数据完美 ===
WriteToFlash(&packet_buf[3], 128); // 写入 Flash
next_packet_num++; // 准备收下一包
retries = 0; // 重置重试计数
UART_SendByte(ACK);
}
}
// --- 分支 B: 收到结束信号 ---
else if (header == EOT)
{
UART_SendByte(ACK);
return SUCCESS; // 传输完成
}
// --- 分支 C: 收到取消信号 ---
else if (header == CAN)
{
return ERROR; // 上位机取消了
}
}
}
五、 进阶心法:给老手的三个锦囊
如果你已经跑通了上面的代码,这里有三个“坑”和优化方向,能体现你对协议理解的深度:
1. 尾部填充的处理 (Padding)
文件大小很少正好是 128 的倍数。标准 Xmodem 规定,如果最后一包不足 128 字节,发送端通常用 0x1A (CTRL-Z, DOS 时代的 EOF) 进行填充。
-
坑点:如果你的固件是二进制
.bin文件,0x1A可能就是代码的一部分! -
解法:Xmodem 本身无法解决这个问题(它不知道文件实际长度)。通常的做法是:接收端不管填充,直接写入 Flash。由上层业务逻辑(比如固件头部包含的 Length 字段)来决定实际读多少。
2. 为什么需要 Xmodem-1K?
标准 Xmodem 一次传 128 字节。对于 STM32 而言,Flash 的写入通常是按 Page(页) 来的,一页往往是 1KB 或 2KB。
-
低效:收 128 字节写一次 Flash,不仅慢,而且可能因为频繁操作 Flash 导致寿命损耗(虽然很少见)。
-
优化:使用 Xmodem-1K。包头变为
STX (0x02),数据区扩大到 1024 字节。这正好对应大多数 MCU 的 1KB 页大小,收一包、写一页,效率和逻辑完美契合。
3. 校验算法的演进
最初的 Xmodem 使用累加和(Checksum),这其实很不安全(比如 0x01, 0x03 和 0x02, 0x02 的和是一样的)。
-
改进:在握手阶段,如果 STM32 发送
'C' (0x43)而不是NAK,就代表请求 CRC-16 校验模式。这能极大地降低数据出错的概率。现在的 Bootloader 开发中,CRC 模式是标配。
结语
Xmodem 协议虽老,但它五脏俱全。 从包结构设计(序号反码)到交互逻辑(超时重传),再到工程实现(状态机),它包含了嵌入式通信的全部核心要素。
建议大家拿起手边的 STM32 开发板,短接串口 TX 和 RX 进行回环测试,或者写一个 Bootloader 试着给自己升级一次固件。当你看到进度条跑完的那一刻,你对通信协议的理解将不再停留在纸面上。
Happy Coding!

1万+

被折叠的 条评论
为什么被折叠?



