串口数据被“拆包”问题

前言

写串口助手、上位机等与串口打交道的程序的时候,可能会遇到这么一个问题:串口数据“被“拆包”(“粘包”也是一样的原因)。

这种情况多发生在使用“串口转 USB ”线材的时候,我猜测与转换芯片或者硬件电路之类的有很大的关系。

因为用同一个串口助手软件,“ RS485/232 转 USB ”的线材会导致数据被“拆包”,而“ TTL 转 USB ”的线材则不会(我的实验现象)。

“拆包”现象如下图所示,下位机一次性向串口发送了 10 个字节的数据,但是串口助手(或者上位机)却分两次接收,实际上是读了两次缓冲区。为了使肉眼可见,下图放慢了速度:

data_interrupt
上图只是演示效果,实际串口助手做了延时,很大程度上避免了这个问题

这种现象导致本来完整的数据被拆成了两段,会对高速数据采集、处理产生困扰,那么出现这种问题的时候,应该怎么处理呢?

产生这种现象的原因是接收事件触发了两次(或者多次),但这并不是.Net 里 serialPort 类的问题。

我总结了三个解决办法:

  1. 按照字节数将数据拼接起来 (简单易用,但是适用面窄,而且不靠谱)
  2. 在接收事件里加延时 (简单易用,但是不能照顾所有的情况)
  3. 使用队列 (适用所有的情况,但是代码稍微多一点点,使用条件多)

一、按照字节数将数据拼接起来

将数据拼接起来,只适用一种长度的数据,如果要接收多种长度的数据,就无法使用,所以说适用面窄。如果多种数据之间有很明显的差别,还是可以区分开然后各自拼接。

这个方案不靠谱的原因是,如果数据在传输的过程中受到外部电磁等干扰,出现丢失或者增多,长度条件永远不会满足,后面就再也“收不到”数据了。

下面演示 10 个字节一个包的接收方法(只演示 16 进制接收,ASCLL 接收方法是一样的):

static string rec_data = ""; //静态变量
private void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    try
    {
        //实例化相应长度的数组
        byte[] ReDatas = new byte[serialPort.BytesToRead]; 
        serialPort.Read(ReDatas, 0, ReDatas.Length);
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < ReDatas.Length; i++)
        {
           sb.AppendFormat("{0:x2}" + " ", ReDatas[i]);
        }
        //将数据拼接起来
        rec_data = rec_data + sb.ToString().ToUpper();
        
        //如果数据长度满足条件
        if (rec_data.Replace (" ","").Length == 20)
        {
          textBox_Received.AppendText(rec_data);
          rec_data = ""; //记得清除
        }
        sb.Clear();
     }
     catch(Exception message)
     {
         MessageBox.Show(message .ToString ());
         return;
     }
}

二、在接收事件里加延时

在接收事件里加延时,原理是等待下位机的数据完全传输到缓冲区后,再一次性读取,这样就能得到完整的数据。

这个解决方案的关键是延时时间长短,延时时间要根据波特率、数据长度而定。波特率越高、数据长度越短,延迟时间就可以越短。后面会附上常用波特率下的延时时间。

下面演示加延时方案,

private void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
     try
     {
        Thread.Sleep(2); //延时2ms
        //实例化相应长度的数组
        byte[] ReDatas = new byte[serialPort.BytesToRead]; 
        serialPort.Read(ReDatas, 0, ReDatas.Length);//读取数据
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < ReDatas.Length; i++)
        {
           sb.AppendFormat("{0:x2}" + " ", ReDatas[i]);
        }
        textBox_Received.AppendText(sb.ToString().ToUpper()); //十六进制默认大写吧
        sb.Clear();
     }
     catch(Exception message)
     {
        MessageBox.Show(message .ToString ());
        return;
     }
}

常见波特率延时时间参考表

2400波特率 9600波特率 115200波特率
1字节 4.17ms 1.04ms 0.087ms
5字节 20.85ms 5.2ms 0.44ms
10字节 41.7ms 10.4ms 0.88ms
20字节 83.4ms 20.8ms 1.76ms
计算公式:延时时间(单位:毫秒) = 字节数 / (波特率/10/1000)

注:

1,上面的公式中,波特率除以 10,而没有除以 8 ,是因为串口数据在发送的时候还会有起始位、结束位,这个时间不能忽略。如果数据传输还包含校验位等信息,则要除以比 10 更大的数。

2,公式只是大致的推算,只能给出延时时间的大致长度,具体延时时间还需要具体调试,因为下位机或者其他设备存在反应时间等其他特性。

三、使用队列

使用队列解决串口数据 “拆包” 问题,优点有:

  1. 适应所有的波特率、所有的数据长度
  2. 适应所有数据格式
  3. 无惧数据丢失或异常

使用队列解决串口数据 “拆包” 的前提条件是串口数据有明确的规律,以便于将数据正确的拆、合。一般情况下只有上位机软件可以使用这个方案。

比如某设备不停的向上位机监控软件发送温度数据,以 16 进制发送,数据格式为 :
“ 2E + 1 字节地址 + 1 字节关键字 + 3字节数据位 + 1 字节和校验位 ”
2E 00 AA 01 11 23 63
“2E” 代表数据起始,“00” 表示设备地址为0,   “AA” 表示这段数据是温度,“01” 表示温度数据为正数,“11 23” 表示温度为 11.23 度,“63” 是校验位。

类似这样的情况,就可以使用队列。接收事件收到数据就往队列里添加,数据处理线程从队列里取数据,取到 “2E” 就作为数据开始,直到取到下一个 “2E” ,就得到了一个完整的数据段,然后再验证校验确保数据正确。

由以上的例子可以看出,地址位、数据位、校验位不能出现 “2E” ,否则就会错误处理至少 2 个数据段,当然还可以有更好的办法解决,比如以 “2E” + 地址 两个字节作为起始标志,以减少这种情况造成的干扰,甚至可以用 “2E” + 地址 + 关键字 三个字节作为起始。

这个方案我就不贴代码了,因为不具有通用性。总之,使用队列解决 “拆包” 问题的核心思路就是串口数据要有规律可循,只要从队列取数据,根据规律识别出数据段即可。

 

最后附上我写的一个串口助手,简单易用,界面简洁,功能丰富,稳定靠谱。部分功能:

  1. 自定义波特率
  2. 定时存储
  3. 定时发送
  4. 接收频率显示
  5. HEX 自动换行
  6. 多条循环发送
  7. 和校验、RCR校验、LRC校验计算器
  8. 退出参数保存
  9. 一键恢复默认设置

附件:iCOM串口调试助手

提取码:k25f

 

2018/3/16 更新

修复多条发送最后一条指令重复发送的bug,最新版本号:2.0.9

2018/6/13 更新

新增自动换行延迟时间选择,更好的适应自动换行的不同情况。最新版本号:2.1.1

2018/10/30 更新

新增多条发送可选只循环一次。最新版本号:2.1.2

2019/5/2 更新

修复重启后,ASCII发送的情况下“发送新行”未使能的bug;

界面显示更多信息;

新增自动存储文件名自定义

新增版本更新,再也不用担心新版本从哪里找啦!  (o゜▽゜)o☆

最新版本号:2.1.6

5 thoughts on “串口数据被“拆包”问题

  1. 一箩筐车厘子

    博主的串口助手的确不错 功能很全 特别是定时存储的小加载条 细节到位

    持续关注!

    • Bluesummer

      开心 (≧∀≦)ゞ

  2. liang

    网上下载您的大作,使用起来有两点不顺手。
    1,您的串口波特率不支持我们高速率的自定义,3000000即3M速率,有些设备还有5M的通信速率
    2,存储的文件可以设置名称么?还是暂不支持。
    希望大神们,继续更新,我也会持续关注您的博客。里面硬货不少,再次感谢作者的无私奉献。

    • Bluesummer

      感谢亲的支持!快试试最新版本2.1.6吧,可以自定义存储文件名了,但是高波特率还没能解决。

  3. xiaoliang

    学习了

发表评论