C#实现Xmodem协议数据传输

前言

Xmodem是一种广泛使用的文件传输协议,特别适用于串口通信中。它通过简单而有效的错误检测和重传机制,确保文件在不可靠的通信信道上能够正确传输。在嵌入式开发、工业自动化等领域,Xmodem协议被广泛用于固件更新、配置文件传输等场景。本文将分享如何在C#中实现Xmodem协议数据传输。

Xmodem协议简介

Xmodem协议是一种异步文件传输协议,由Ward Christensen于1977年开发。它的主要特点包括:

  1. 简单可靠:协议设计简单,易于实现,同时提供了基本的错误检测和重传机制
  2. 块传输:将文件分成固定大小的数据块进行传输
  3. 校验机制:支持校验和(Checksum)和循环冗余校验(CRC)两种错误检测方式
  4. 确认与重传:通过 ACK(确认)和 NAK(否定确认)实现简单的错误恢复与停等式传输控制

Xmodem协议有多个变种,包括:

  • Xmodem:使用128字节数据块
  • Xmodem-1K:使用1024字节数据块,提高传输效率
  • Xmodem-CRC:使用CRC校验替代校验和,提高错误检测能力

实现Xmodem协议的挑战

在实现Xmodem协议时,我们通常会面临以下挑战:

  1. 协议状态管理:需要正确处理协议的各种状态和转换
  2. 数据分块与重组:将文件分成适当大小的数据块,并在接收端重组
  3. 错误检测与重传:实现有效的错误检测机制,并在出错时进行重传
  4. 超时处理:处理通信超时情况,避免长时间阻塞
  5. 串口通信:与底层串口通信的无缝集成

设计思路

为了应对上述挑战,我设计了一套完整的Xmodem协议实现方案,主要包括以下设计思路:

1. 模块化设计

将Xmodem协议的实现分解为多个职责明确的模块:

  • XmodemData:数据分块与重组,负责将文件转换为Xmodem数据块
  • CRC:错误检测,实现校验和和CRC校验算法
  • SerialPortHelper:串口通信封装,提供统一的串口操作接口(引用自上一篇文章《C#实现高效稳定的串口通讯》)

2. 面向对象设计

采用面向对象的设计方法,将协议的各个部分抽象为类和接口:

  • CRC接口:定义CRC计算的通用方法
  • CRC8和CRC16类:实现不同的CRC算法
  • XmodemData类:处理文件分块和数据块管理

3. 事件驱动模型

利用SerialPortHelperLib的事件驱动模型处理串口数据和错误:

  • SerialPortDataReceivedProcessEvent:数据接收事件,处理Xmodem协议逻辑
  • SerialPortErrorEvent:错误处理事件,处理串口通信错误

4. 灵活性与可扩展性

设计时考虑了协议的灵活性和可扩展性:

  • 支持多种Xmodem变种:包括Xmodem和Xmodem-1K
  • 支持多种校验方式:包括校验和和CRC
  • 可配置的数据块大小:根据需要调整数据块大小

核心实现

1. 数据分块实现

public static void XmodemDataList(string _path, bool useBlock1K) {
    // 初始化数据块列表
    if (useBlock1K) {
        xmodem1k_C = new List<List<byte>>();
        xmodem1k_N = new List<List<byte>>();
        xmodem_C = null;
        xmodem_N = null;
    } else {
        xmodem_C = new List<List<byte>>();
        xmodem_N = new List<List<byte>>();
        xmodem1k_C = null;
        xmodem1k_N = null;
    }

    CRC crc8 = new CRC8();
    CRC crc16 = new CRC16();

    using (FileStream plik = new FileStream(_path, FileMode.Open)) {
        using (BinaryReader czytnik = new BinaryReader(plik)) {
            List<byte> c_list = new List<byte>();
            List<byte> n_list = new List<byte>();
            //包序号
            byte PackageTemp = 1;

            while (czytnik.BaseStream.Position != czytnik.BaseStream.Length) {
                c_list = new List<byte>();
                n_list = new List<byte>();
                if (useBlock1K) {
                    c_list.Add(0x02); // STX,1024字节数据块
                    n_list.Add(0x02);
                } else {
                    c_list.Add(0x01); // SOH,128字节数据块
                    n_list.Add(0x01);
                }
                // 添加包序号和反码
                c_list.Add(Convert.ToByte((PackageTemp).ToString()));
                n_list.Add(Convert.ToByte((PackageTemp).ToString()));
                c_list.Add(Convert.ToByte((255 - PackageTemp).ToString()));
                n_list.Add(Convert.ToByte((255 - PackageTemp).ToString()));

                if (useBlock1K) {
                    // 处理1024字节数据块
                    byte[] byteTemp = new byte[1024];
                    // 初始化数据块
                    for (int z = 0; z < 1024; z++) {
                        byteTemp[z] = 0x1A;
                    }
                    // 读取文件数据
                    for (int j = 0; j < 1024; j++) {
                        byteTemp[j] = (byte)plik.ReadByte();
                    }
                    // 添加数据
                    foreach (var itemB in byteTemp) {
                        c_list.Add(itemB);
                        n_list.Add(itemB);
                    }
                    // 添加CRC校验
                    var crcbit = BitConverter.GetBytes(crc16.calcCRC(byteTemp));
                    c_list.Add(crcbit[1]);
                    c_list.Add(crcbit[0]);
                    // 添加校验和
                    n_list.Add(BitConverter.GetBytes(crc8.calcCRC(byteTemp))[0]);

                    xmodem1k_C.Add(c_list);
                    xmodem1k_N.Add(n_list);
                } else {
                    // 处理128字节数据块
                    byte[] byteTemp = new byte[128];
                    // 初始化数据块
                    for (int z = 0; z < 128; z++) {
                        byteTemp[z] = 0x1A;
                    }
                    // 读取文件数据
                    for (int j = 0; j < 128; j++) {
                        byteTemp[j] = (byte)plik.ReadByte();
                    }
                    // 添加数据
                    foreach (var itemB in byteTemp) {
                        c_list.Add(itemB);
                        n_list.Add(itemB);
                    }
                    // 添加CRC校验
                    var crcbit = BitConverter.GetBytes(crc16.calcCRC(byteTemp));
                    c_list.Add(crcbit[1]);
                    c_list.Add(crcbit[0]);
                    // 添加校验和
                    n_list.Add(BitConverter.GetBytes(crc8.calcCRC(byteTemp))[0]);

                    xmodem_C.Add(c_list);
                    xmodem_N.Add(n_list);
                }
                PackageTemp++;
            }
        }
    }
}

2. 基于SerialPortHelper的Xmodem协议实现

在XModem_XChip项目中,我们直接在SerialPortDataReceivedProcessEvent事件处理函数中实现了Xmodem协议的核心逻辑:

// 初始化SerialPortHelper
private SerialPortHelper serialPort1;
private SerialPortHelper serialPort2;
private int PackageTemp = 0;
private int useCRC = -1;
private bool useBlock1K = true;

 serialPort1 = new SerialPortHelper();
 //发送使用的串口通讯
 serialPort1.BindSerialPortDataReceivedProcessEvent(new SerialPortHelper.DelegateSerialPortDataReceivedProcessEvent(SerialPortDataReceivedProcess1));
 serialPort1.BindSerialPortErrorEvent(new SerialPortHelper.DelegateSerialPortErrorEvent(SerialPortErrorProcess));
 serialPort1.SerialReceviedTimeInterval = 10; //接收数据时间
 serialPort1.SerialReceviedTimeInterval = 1;  //发送数据时间

 serialPort2 = new SerialPortHelper();
 //接收使用的串口通讯
 serialPort2.BindSerialPortDataReceivedProcessEvent(new SerialPortHelper.DelegateSerialPortDataReceivedProcessEvent(SerialPortDataReceivedProcess2));
 serialPort2.BindSerialPortErrorEvent(new SerialPortHelper.DelegateSerialPortErrorEvent(SerialPortErrorProcess));
 serialPort2.SerialReceviedTimeInterval = 1; //接收数据时间
 serialPort2.SerialReceviedTimeInterval = 1;  //发送数据时间

// 发送数据接收处理函数
private void SerialPortDataReceivedProcess1(object sender, byte[] arrDataReceived) {
    try {
        // 处理接收到的数据
        foreach (byte b in arrDataReceived) {
            switch (b) {
                case 67: // 'C',请求使用CRC校验
                    {
                        useCRC = 0;
                        // 发送数据块
                        if (useBlock1K) {
                            serialPort1.WriteByte(XmodemData.xmodem1k_C[PackageTemp].ToArray());
                        } else {
                            serialPort1.WriteByte(XmodemData.xmodem_C[PackageTemp].ToArray());
                        }
                    }
                    break;
                case 21: // NAK,请求使用校验和
                    {
                        useCRC = 1;
                        // 发送数据块
                        if (useBlock1K) {
                            if (PackageTemp > 0) {
                                serialPort1.WriteByte(XmodemData.xmodem1k_N[PackageTemp - 1].ToArray());
                            } else {
                                serialPort1.WriteByte(XmodemData.xmodem1k_N[PackageTemp].ToArray());
                            }
                        } else {
                            if (PackageTemp > 0) {
                                serialPort1.WriteByte(XmodemData.xmodem_N[PackageTemp - 1].ToArray());
                            } else {
                                serialPort1.WriteByte(XmodemData.xmodem_N[PackageTemp].ToArray());
                            }
                        }
                    }
                    break;
                case 06: // ACK,确认收到
                    {
                        // 检查是否传输完成
                        if (useBlock1K) {
                            if (PackageTemp > XmodemData.xmodem1k_C.Count - 1) {
                                // 发送结束命令 EOT 0x04
                                serialPort1.Write(new byte[] { 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 });
                                // 关闭通讯
                                Thread.Sleep(1000);
                                string msg = "烧写完成!";
                                Fb_SendEvent(msg);
                                serialPort1.CloseCom(out msg);
                                Fb_SendEvent(msg);
                                return;
                            }
                        } else {
                            if (PackageTemp > XmodemData.xmodem_C.Count - 1) {
                                // 发送结束命令 EOT 0x04
                                serialPort1.Write(new byte[] { 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 });
                                // 关闭通讯
                                Thread.Sleep(1000);
                                string msg = "烧写完成!";
                                Fb_SendEvent(msg);
                                serialPort1.CloseCom(out msg);
                                Fb_SendEvent(msg);
                                return;
                            }
                        }

                        // 发送下一个数据块
                        if (useCRC == 0) {
                            if (useBlock1K) {
                                serialPort1.WriteByte(XmodemData.xmodem1k_C[PackageTemp].ToArray());
                            } else {
                                serialPort1.WriteByte(XmodemData.xmodem_C[PackageTemp].ToArray());
                            }
                        } else if (useCRC == 1) {
                            if (useBlock1K) {
                                serialPort1.WriteByte(XmodemData.xmodem1k_N[PackageTemp].ToArray());
                            } else {
                                serialPort1.WriteByte(XmodemData.xmodem_N[PackageTemp].ToArray());
                            }
                        }
                        // 更新包序号和进度
                        PackageTemp++;
                    }
                    break;
                case 24: // CAN,取消传输
                    {
                        serialPort1.Write(new byte[] { 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 });
                        string msg = "烧写失败";
                        Fb_SendEvent(msg);
                        serialPort1.CloseCom(out msg);
                        Fb_SendEvent(msg);
                    }
                    break;
                default:
                    break;
            }
        }
    } catch (Exception ex) {
        string msg = "烧写失败";
        Fb_SendEvent(msg);
        serialPort1.CloseCom(out msg);
        Fb_SendEvent(string.Format("烧写错误信息:{0}", ex.Message.ToString()));
    }
}

//接收数据接收处理函数
private void SerialPortDataReceivedProcess2(object sender, byte[] arrDataReceived)
{
    this.Invoke(new Action(() =>
    {

        try
        {
            if (arrDataReceived.Length != (useBlock1K ? 1028 : 132))
            {
                if (arrDataReceived.First() == 04)//表示结束接收
                {
                    serialPort2.Write(new byte[] { 0x06, 0x06, 0x06, 0x06 }); //告诉终端结束这次发送
                    Thread.Sleep(1000);
                    receivePath();
                    binPBC.Position = binPBC.Properties.Maximum;//设置进度条100%
                    string msg = "导入完成完成!";
                    Fb_SendEvent(msg);
                    serialPort2.CloseCom(out msg);
                    Fb_SendEvent(msg);
                    simpleButton4.Text = "导出";

                }
                else {
                    Fb_SendEvent(string.Format("->获取数据长度不够,重新获取!"));
                    //serialPort2.Write(new byte[] { 0x15 }); //重新NAk获取
                }
                return;
            }

            if (arrDataReceived.First() == 04)//表示结束接收
            {
                serialPort2.Write(new byte[] { 0x06, 0x06, 0x06, 0x06 }); //告诉终端结束这次发送
                                                                          //休眠下 导入到文件
                Thread.Sleep(1000);
                receivePath();
                binPBC.Position = binPBC.Properties.Maximum;//设置进度条100%
                string msg = "导入完成完成!";
                Fb_SendEvent(msg);
                serialPort2.CloseCom(out msg);
                Fb_SendEvent(msg);
                simpleButton4.Text = "导出";

                return;
            }
            //接收数据业务处理部分
            else if (arrDataReceived.First() == (useBlock1K ? 2 : 1)) //如果是1k 等于2 如果是k 就是1 处理头部分文件
            {
                List<byte> tembyte = new List<byte>();
                XmodemRequest xRequest = new XmodemRequest();
                xRequest.ControlCharacter = arrDataReceived.First();
                xRequest.PackageNo = arrDataReceived[1];
                var tempIndex = Convert.ToInt32(xRequest.PackageNo) - 1; //接收第二位
                xRequest.PackageInverseNo = arrDataReceived[2];              

                byte lastCrc;
                //判断校验位
                if (useBlock1K)
                {
                    xRequest.DataList = arrDataReceived.Skip(3).Take(1024).ToList();
                    tembyte.AddRange(arrDataReceived.Skip(3).Take(1024));
                    xRequest.CheckBit = new byte[] { arrDataReceived.Skip(1027).Take(1).First() };
                    lastCrc = arrDataReceived.Skip(1027).Take(1).First(); //获取校验位
                }
                else
                {
                    xRequest.DataList = arrDataReceived.Skip(3).Take(128).ToList();
                    tembyte.AddRange(arrDataReceived.Skip(3).Take(128));
                    xRequest.CheckBit =new byte[] { arrDataReceived.Skip(131).Take(1).First() };
                    lastCrc = arrDataReceived.Skip(131).Take(1).First(); //获取校验位
                }
                if (lastCrc != BitConverter.GetBytes(Port.GetCkSum(tembyte.ToArray()))[0]) //判断校验位是否正确
                {
                    Fb_SendEvent(string.Format("校验位不正确重新发送NAK"));
                    serialPort2.Write(new byte[] { 0x15 }); //重新NAk获取
                }
                else
                {
                    serialPort2.Write(new byte[] { 0x06 }); //获取下一个                 
                    if (tempIndex != tempReceived)
                    {
                        tempSize++;
                        binPBC.Position += 1;
                    }

                    xRequest.No = tempSize;
                    Fb_SendEvent(string.Format("->导出数据{0}包", tempSize));                   
                   
                    rxtemplists.Add(xRequest);
                    tempReceived = tempIndex;                    
                }
            }
            else if (arrDataReceived.First() == 24)
            {
                string msg = "连接超时!";
                Fb_SendEvent(msg);
                serialPort2.CloseCom(out msg);
                Fb_SendEvent(msg);
            }
            else
            {
                serialPort2.Write(new byte[] { 0x15 }); //重新NAk获取
            }

            if (!isSend)
            {
                Thread.Sleep(1000);
                binPBC.Position = binPBC.Properties.Maximum;//设置进度条100%
                simpleButton4.Text = "导出";
                serialPort2.Write(new byte[] { 0x18 }); //告诉终端结束这次发送
                Thread.Sleep(50);
                serialPort2.Write(new byte[] { 0x18 }); //告诉终端结束这次发送
                Thread.Sleep(50);
                serialPort2.Write(new byte[] { 0x18 }); //告诉终端结束这次发送
                Thread.Sleep(50);
                serialPort2.Write(new byte[] { 0x18 }); //告诉终端结束这次发送
                Thread.Sleep(50);
                string msg = "取消命令已发送!";
                Fb_SendEvent(msg);
                serialPort2.CloseCom(out msg);
                Fb_SendEvent(msg);
            }
        }
        catch (Exception ex)
        {
            Fb_SendEvent(string.Format("导出错误信息:{0}",ex.Message.ToString()));
        }
    }));
}

3. CRC校验实现

// CRC接口
public interface CRC {
    int getCRCLength();
    long calcCRC(byte[] block);
    long calcCRC(byte[] buf, int index, int len);
}

// CRC8实现(校验和)
public class CRC8 : CRC {
    public int getCRCLength() {
        return 1;
    }
    public long calcCRC(byte[] block) {
        byte checkSumma = 0;
        for (int i = 0; i < block.Count(); i++) {
            checkSumma += block[i];
        }
        return checkSumma;
    }

    public long calcCRC(byte[] buf, int index, int len) {
        throw new NotImplementedException();
    }
}

// CRC16实现
public class CRC16 : CRC {
    public static ushort[] _crc16LookupTable = {
        0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C,
        0xD1AD, 0xE1CE, 0xF1EF,
        // 省略部分查表数据...
    };
    public int getCRCLength() {
        return 2;
    }

    public long calcCRC(byte[] buf, int index, int len) {
        int counter;
        ushort crc = 0;
        for (counter = 0; counter < len; counter++)
            crc = (ushort)((crc << 8) ^ _crc16LookupTable[((crc >> 8) ^ buf[index + counter]) & 0x00FF]);
        return crc;
    }

    public long calcCRC(byte[] block) {
        int crc = 0x0000;
        foreach (var item in block) {
            crc = ((crc << 8) ^ _crc16LookupTable[((crc >> 8) ^ (0xff & item))]) & 0xFFFF;
        }
        return crc;
    }
}

参考资料

posted @ 2026-02-13 16:44  (*_^)?  阅读(16)  评论(0)    收藏  举报