【嵌入式通信】零基础入门Xmodem协议

前言: 在嵌入式开发中,一定要区分“打印调试”和“数据传输”的区别。 很多新人习惯了用 printf 和串口助手交互,觉得串口很简单。但当你面临“固件升级(IAP)”或“传输图片”等需求时,你会发现直接发数据(Raw Data)简直是灾难:丢一个字节,整个固件报废;没有校验,干扰导致花屏。

怎么解?这时候你需要给数据穿上一层“铠甲”,这层铠甲就是——通信协议。今天我们来拆解最经典、最纯粹的文件传输协议:Xmodem

一、 为什么是 Xmodem?

在 TCP/IP 满天飞的今天,为什么还要学这个几十年前的协议?

  1. 极简主义:它不需要复杂的协议栈,几百行 C 代码就能搞定,非常适合资源受限的 MCU(如 STM32F0/F1 系列)。

  2. 通用性:几乎所有的终端软件(SecureCRT, TeraTerm, Xshell)都内置了 Xmodem 支持。

  3. 基石作用:搞懂了 Xmodem,你再去学 Ymodem(多文件传输)、Zmodem(流式传输),甚至是自定义协议,都只是换汤不换药。

简单来说,Xmodem 是嵌入式工程师从“写代码”进阶到“搞系统”的必修课。

二、 协议解剖:数据的“铠甲” (Packet Structure)

Xmodem 是一个半双工(Half-Duplex)协议,采用“一问一答”的模式。它不允许数据“裸奔”,所有数据必须打包成固定 132 字节 的格式。

我们可以把它想象成一列标准化的货运火车,每一节车厢(Packet)都长得一样:

字节偏移字段名称值/范围说明
0SOH (Start of Header)0x01包头。接收端看到它,就知道新包来了。
1Seq (Packet Number)0x01 ~ 0xFF包序号。从1开始累加,溢出后归0。
2~Seq (1's Complement)0xFE ~ 0x00包序号反码。这是 Xmodem 的精髓设计之一。
3 ~ 130Data (Payload)128 Bytes数据载荷。不管你要传什么,必须凑够128字节。
131CheckSum0x00 ~ 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 收到数据包后,进行三步检查:

    1. 检查包头是不是 SOH

    2. 检查 Seq~Seq 对应关系是否正确,且 Seq 是否是期望的下一包。

    3. 计算 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, 0x030x02, 0x02 的和是一样的)。

  • 改进:在握手阶段,如果 STM32 发送 'C' (0x43) 而不是 NAK,就代表请求 CRC-16 校验模式。这能极大地降低数据出错的概率。现在的 Bootloader 开发中,CRC 模式是标配

结语

Xmodem 协议虽老,但它五脏俱全。 从包结构设计(序号反码)到交互逻辑(超时重传),再到工程实现(状态机),它包含了嵌入式通信的全部核心要素。

建议大家拿起手边的 STM32 开发板,短接串口 TX 和 RX 进行回环测试,或者写一个 Bootloader 试着给自己升级一次固件。当你看到进度条跑完的那一刻,你对通信协议的理解将不再停留在纸面上。

Happy Coding!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值