C#实现Xmodem协议数据传输
前言
Xmodem是一种广泛使用的文件传输协议,特别适用于串口通信中。它通过简单而有效的错误检测和重传机制,确保文件在不可靠的通信信道上能够正确传输。在嵌入式开发、工业自动化等领域,Xmodem协议被广泛用于固件更新、配置文件传输等场景。本文将分享如何在C#中实现Xmodem协议数据传输。
Xmodem协议简介
Xmodem协议是一种异步文件传输协议,由Ward Christensen于1977年开发。它的主要特点包括:
- 简单可靠:协议设计简单,易于实现,同时提供了基本的错误检测和重传机制
- 块传输:将文件分成固定大小的数据块进行传输
- 校验机制:支持校验和(Checksum)和循环冗余校验(CRC)两种错误检测方式
- 确认与重传:通过 ACK(确认)和 NAK(否定确认)实现简单的错误恢复与停等式传输控制
Xmodem协议有多个变种,包括:
- Xmodem:使用128字节数据块
- Xmodem-1K:使用1024字节数据块,提高传输效率
- Xmodem-CRC:使用CRC校验替代校验和,提高错误检测能力
实现Xmodem协议的挑战
在实现Xmodem协议时,我们通常会面临以下挑战:
- 协议状态管理:需要正确处理协议的各种状态和转换
- 数据分块与重组:将文件分成适当大小的数据块,并在接收端重组
- 错误检测与重传:实现有效的错误检测机制,并在出错时进行重传
- 超时处理:处理通信超时情况,避免长时间阻塞
- 串口通信:与底层串口通信的无缝集成
设计思路
为了应对上述挑战,我设计了一套完整的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;
}
}

浙公网安备 33010602011771号