串口通信(5)-C#串口通信数据接收不完整解决方案

2023-12-13 16:39:05

本文讲解C#串口通信数据接收不完整解决方案。

目录

一、概述

二、Modbus RTU介绍

三、解决思路

四、实例


一、概述

串口处理接收数据是串口程序编写的关键,在实际应用中基本是哪个采用异步通信的方式,所以接收数据就需要考虑接收数据的完整性,同时需要考虑数据分包,粘包,数据包的错误的情况。

有些场合尤其是全自动化设备指令收发时,数据完整性十分必要。

首先想到在串口接收事件里面添加延时,这种方案能解决分包发送的情况,对其他情况帮助不大,同时对时效要求高的情况下,显得鸡肋,不建议使用。

常规的正确的做法使用缓存的编写方式,当然这种依赖协议的完整性,本文以接收modbus协议数据为例。

二、Modbus RTU介绍

数据帧格式

首先我们要知道一帧正常的MODBUS数据帧包含的内容有:地址域 + 功能码 + 数据 + 差错校验,再者无论是上述哪种协议版本,Modbus帧格式都是一样的:

?

其中地址域:一个字节,理论上代表256个设备,实际可用的只有247个,地址0是广播地址(0~247)

功能码:一个字节,具体含义有规范要求。

数据:N个字节,具体格式和大小跟功能码有关。Modbus信息帧所允许的最大长度为256个字节,所以数据最多252个字节。

差错校验:2个字节,低字节在前,高字节在后。RTU采用16位CRC校验,是从开头一直校验到此之前。在每个RTU数据帧之前和之后有不少于3.5个字符位作为帧的分隔。

Modbus RTU通讯协议在数据通讯上采用主从应答的方式进行。只能由主机(PC,HMI等)通过唯一从机地址发起请求,从机(终端设备)根据主机请求进行响应,即半双工通讯。该协议只允许主机发起请求,从机进行被动响应,因此从机不会主动占用通讯线路造成数据冲突。

类似Modbus RTU协议的主从应答协议还有西门子的PPI、电表常用的DL/T645-2007等协议。

如果想进一步学习可以使用仿真软件进行调试学习

Modbus Poll则可以仿真出ModbusRTU中的主站。

Modbus Slave 可以仿真出ModbusRTU中的从站。

下载地址;https://www.modbustools.com/download/ModbusSlaveSetup64Bit-822.exe

当然可以下载破解版

1、协议格式

信息传输为异步方式,使用16进制进行通讯,信息帧格式:

地址码

功能码

数据区

CRC校验码

1字节

1字节

N字节

2字节

?

地址码

地址码是每个通讯信息帧的第一个字节,一般支持1到247,部分设备也支持0地址,用于接收主机的广播数据,每个从机在总线上地址必须唯一,只有与主机发送的地址码相符的从机才能响应返回数据。

功能码

功能码是每个通讯信息帧的第二个字节。主机发送,通过功能码告知从机设备应当执行何种操作。

常见的八种功能码:

功能码

定义

操作

01H

读取线圈

读取一个或多个连续线圈状态

05H

写单个线圈

操作指定位置的线圈状态

0FH

写多个线圈

操作多个连续线圈状态

02H

读取离散量输入

读取一个或多个连续离散输入状态

04H

读取输入寄存器

读取一个或多个连续输入寄存器数据

03H

读保持寄存器

读取一个或多个保持寄存器数据

06H

写单个保持寄存器

把两个十六进制数据写入对应位置

10H

写多个保持寄存器

把4*N个十六进制数据写入N个连续保持寄存器

?

数据区

数据区随功能码以及数据方向的不同而不同,这些数据可以是“寄存器首地址+读取寄存器数量”、“寄存器地址+操作数据”、“寄存器首地址+操作寄存数量+数据长度+数据”等不同的组合,在“功能码分析”详解不同功能码的数据区。

Modbus CRC校验

Modbus RTU协议常用与工业现场对数据传输的稳定性和正确性有较高的要求,因此通过CRC校验保证数据传输的正确性和完整性。

2、错误反馈

地址与CRC校验错误并不会收到从机的数据反馈,其他错误将向主机返回错误码。数据帧的第二位加上0X80表示请求发生错误(非法功能码、非法数据值等),错误数据帧如下:

地址码

功能码

错误码

CRC校验码

1字节

1字节

1字节

2字节

常见错误码如下:

名称

说明

01H

非法的功能码

不支持该功能码操作寄存器

02H

非法的寄存器地址

访问设备禁止访问的寄存器

03H

非法的数据值

写入不支持的参数值

04H

从机故障

设备工作异常

3、通讯信息传输过程

通讯命令由主机发送从机时,与主机发送的地址码相符的从机接收通讯命令,如果CRC校验无误,则执行相应的操作,然后把执行结果(数据)返回给主机。返回信息中包含地址码、功能码、执行后的数据以及CRC校验码。如果地址不匹配或者CRC校验出错就不返回任何信息。

功能码分析功能码01H:读线圈

例如:主机要读取从机地址为01H,起始线圈地址为00H的1个线圈状态,主机发送:

主机发送

发送数据(HEX)

地址码

01

功能码

01

起始线圈地址

高字节

00

低字节

00

线圈数量

高字节

00

低字节

01

CRC校验

低字节

FD

高字节

CA

如果从机寄存器00H线圈闭合,从机返回:

从机返回

发送数据(HEX)

地址码

01

功能码

01

字节数

01

线圈状态

01

CRC校验码

低字节

90

高字节

48

功能码05H:写单个线圈

例如:主机要控制从机地址为01H,线圈地址为0000H的线圈状态,主机发送:

主机发送

发送数据(HEX)

地址码

01

功能码

01

线圈地址

高字节

00

低字节

00

控制方式

高字节

00(断开)、FF(闭合)

低字节

01

CRC校验

低字节

XX

高字节

XX

从机返回与主机请求相同;

功能码0FH:写多个线圈

例如:主机要控制从机地址为01H,起始线圈地址为00H的4个线圈状态,主机发送:

主机发送

发送数据(HEX)

地址码

01

功能码

0F

起始线圈地址

高字节

00

低字节

00

线圈数量

高字节

00

低字节

04

写入字节数

01

控制方式

00(全部断开)、0F(全部闭合)

CRC校验

低字节

XX

高字节

XX

功能码0FH操作,从机返回:

从机返回

发送数据(HEX)

地址码

01

功能码

0F

起始线圈地址

高字节

00

低字节

00

线圈数量

高字节

00

低字节

04

CRC校验

低字节

54

高字节

08

功能码02H:读离散输入

例如:主机要读取从机地址为01H,起始离散量地址为00H的4个输入状态,主机发送:

主机发送

发送数据(HEX)

地址码

01

功能码

02

起始离散量地址

高字节

00

低字节

00

读取数量

高字节

00

低字节

04

CRC校验

低字节

79

高字节

C9

如果从机首地址00H开始的4离散输入全部检测到输入,从机返回:

从机返回

发送数据(HEX)

地址码

01

功能码

02

字节数

01

离散输入状态

0F

CRC校验码

低字节

E1

高字节

8C

功能码04H:读取输入寄存器

例如:主机要读取从机地址为01H,起始寄存器地址为02H的1个输入寄存器数据,主机发送:

主机发送

发送数据(HEX)

地址码

01

功能码

04

起始寄存器地址

高字节

00

低字节

02

寄存器数量

高字节

00

低字节

01

CRC校验

低字节

90

高字节

0A

如果从机输入寄存器02H的数据为3344H,从机返回:

从机返回

发送数据(HEX)

地址码

01

功能码

04

字节数

02

寄存器05H数据

高字节

33

低字节

44

CRC校验码

低字节

AD

高字节

F3

功能码03H:读保持寄存器

例如:主机要读取从机地址为01H,起始寄存器地址为05H的2个保持寄存器数据,主机发送:

主机发送

发送数据(HEX)

地址码

01

功能码

03

起始寄存器地址

高字节

00

低字节

05

寄存器数量

高字节

00

低字节

02

CRC校验

低字节

D4

高字节

0A

如果从机保持寄存器05H、06H的数据为1122H、3344H,从机返回:

从机返回

发送数据(HEX)

地址码

01

功能码

03

字节数

04

寄存器05H数据

高字节

11

低字节

22

寄存器06H数据

高字节

33

低字节

44

CRC校验码

低字节

4B

高字节

C6

功能码06H:写单个保持寄存器

例如:主机写入9988H的数据给从机地址为01H,寄存器地址为0050H的寄存器,主机发送:

主机发送

发送数据(HEX)

地址码

01

功能码

06

寄存器地址

高字节

00

低字节

50

写入值

高字节

99

低字节

88

CRC校验

低字节

E3

高字节

ED

从机返回与主机请求相同;

功能码10H:写多个保持寄存器

例如:主机要把数据0005H、2233H保存到从机地址为01H,起始寄存器地址为0020H的2个寄存器中,主机发送:

主机发送

发送数据(HEX)

地址码

01

功能码

10

起始寄存器地址

高字节

00

低字节

20

寄存器数量

高字节

00

低字节

02

写入字节数

04

0000H

寄存器待写入

高字节

00

低字节

05

0001H

寄存器待写入

高字节

22

低字节

33

CRC校验

低字节

B9

高字节

03

功能码10H操作,从机返回:

从机返回

发送数据(HEX)

地址码

01

功能码

10

起始寄存器地址

高字节

00

低字节

20

寄存器数量

高字节

00

低字节

02

CRC校验

低字节

40

高字节

02

三、解决思路

创建一个缓冲区用来存放串口每次接收到的数据,串口收到数据后,我们就直接判断缓冲区的头字节是否为头码内容,如果符合要求,则根据数据长度接收完这帧数据,之后进行CRC校验判断,若能满足,则表示这帧数据是对的。

若这帧数据不能满足校验,则说明这是一帧错误是数据,有可能我们拿到的头码是伪头码,挨个遗弃字节,直到再在缓冲区中重新找到头码,重新处理帧数据。

四、实例

准备

在modbus仿真软件中准备数据

?

?

设备地址为1

编写程序进行读取

发送为>>> 01 03 00 00 00 04 44 09

接收的为<<< 01 03 08 00 01 00 02 00 03 00 04 0D 14

实例

创建winform项目,添加一个按钮触发定时器读操作

?Cs文件代码

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Forms;

namespace ModbusReadDemo
{
    public partial class Form1 : Form
    {
        List<byte> btBuf = new List<byte>(4096);
        //读返回数据
        int iReadData1 = 0;
        int iReadData2 = 0;
        int iReadData3 = 0;
        int iReadData4 = 0;
        public Form1()
        {
            InitializeComponent();
        }
        /// <字节数组转16进制字符串>
        /// <param name="bytes"></param>
        /// <returns> String 16进制显示形式</returns>
        public static string byteToHexStr(byte[] bytes)
        {
            string returnStr = "";
            try
            {
                if (bytes != null)
                {
                    for (int i = 0; i < bytes.Length; i++)
                    {
                        returnStr += bytes[i].ToString("X2");
                        returnStr += " ";                       //两个16进制用空格隔开,方便看数据
                    }
                }
                return returnStr;
            }
            catch (Exception)
            {
                return returnStr;
            }
        }
        private void spt_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
        {
            try
            {
                if (spt.IsOpen)
                {

                    int iLength = spt.BytesToRead;
                    byte[] btData = new byte[iLength];//读数据暂存
                    CRC CRC = new CRC();
                    byte[] crc = new byte[2];
                    spt.Read(btData, 0, iLength);
                    btBuf.AddRange(btData);//缓存数据
                    if (btBuf.Count < 8) //数据区尚未接收完整
                    {
                        return;
                    }
                    while (btBuf.Count >= 8) //至少包含帧头(地址)(1字节)、功能码(1字节)、校验位(2字节)等;最少返回8个字节
                    {
                        if (btBuf[0] == 0x01)//判定帧头为01
                        {

                            #region 读返回
                            //btBuf[1] 功能码 读 btBuf[2]字节长度
                            if (btBuf[1] == 0X03 & btBuf[2] == 0X08) //传输数据有帧头,用于判断
                            {
                                int len = btBuf[2];
                                if (btBuf.Count < len + 5) //数据区尚未接收完整
                                {
                                    break;
                                }
                                byte[] ReceiveBytes = new byte[len + 5];//读数据暂存
                                btBuf.CopyTo(0, ReceiveBytes, 0, len + 5);
                                CRC.CalculateCrc16(ReceiveBytes, out crc[1], out crc[0]).ToString("X");//生成验证码
                                if ((btBuf[(len + 5) - 2] == crc[0]) & (btBuf[(len + 5) - 1] == crc[1]))//校验码验证
                                {
                                    string strRecv = byteToHexStr(ReceiveBytes);
                                    Trace.Write("串口接收" + strRecv + "\n");
                                    try
                                    {
                                        #region  读地址的数据转化为int类型
                                        byte[] btR1 = { btBuf[4], btBuf[3], 0, 0 };//byte转化为int需要4个字节,btData[4]数据低字节, btData[3]数据高字节
                                        iReadData1 = BitConverter.ToInt32(btR1, 0);
                                        byte[] btR2 = { btBuf[6], btBuf[5], 0, 0 };
                                        iReadData2 = BitConverter.ToInt32(btR2, 0);
                                        byte[] btR3 = { btBuf[8], btBuf[7], 0, 0 };//byte转化为int需要4个字节,btData[4]数据低字节, btData[3]数据高字节
                                        iReadData3 = BitConverter.ToInt32(btR3, 0);
                                        byte[] btR4 = { btBuf[10], btBuf[9], 0, 0 };
                                        iReadData4 = BitConverter.ToInt32(btR4, 0);
                                        Trace.Write("第一个数" + iReadData1.ToString()+"\n");
                                        Trace.Write("第二个数" + iReadData2.ToString() + "\n");
                                        Trace.Write("第三个数" + iReadData3.ToString() + "\n");
                                        Trace.Write("第四个数" + iReadData4.ToString() + "\n");
                                        #endregion
                                        btBuf.RemoveRange(0, len + 5);//数据清除
                                    }
                                    catch
                                    {
                                        btBuf.RemoveAt(0);
                                    }
                                }
                                else
                                {
                                    btBuf.RemoveAt(0);
                                }
                            }
                            else
                            {
                                btBuf.RemoveAt(0);
                            }
                            #endregion
                        }
                        else
                        {
                            btBuf.RemoveAt(0);

                        }

                    }
                }
            }
            // catch (Exception ex)
            catch
            {
                // MessageBox.Show("数据失败!"+ex, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }
        //开始读取
        private void button1_Click(object sender, EventArgs e)
        {
            /* 从D55-D71寄存器读 17个寄存器,30个字节,地址默认1,功能码(03为读寄存器单元),
                                * 起始地址高位(初始化地址38,寄存器D55),起始地址低位,位数高字节,位数低字节,低位CRC校验,高位CRC校验 */
            spt.Open();
            timer1.Enabled = true;
        }
        //定时发送查询寄存器数据
        private void timer1_Tick(object sender, EventArgs e)
        {
            
            if (spt.IsOpen == true)
            {
                Byte[] bt = new byte[8] { 0X01, 0X03, 0X00, 0X00, 0X00, 0X04, 0X44, 0X09 };
                spt.Write(bt, 0, 8);
                //Trace.Write("串口发送");
            }
            else
            {
                //Trace.Write("串口未打开");
            }
        }
    }
}

?CRC代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ModbusReadDemo
{
    public class CRC
    {
        private readonly byte[] _auchCRCHi = new byte[]//crc高位表
        {
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
            0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
            0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
            0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
            0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
            0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
            0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
            0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
            0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
            0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
            0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
            0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
            0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
            0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
            0x80, 0x41, 0x00, 0xC1, 0x81, 0x40
        };

        private readonly byte[] _auchCRCLo = new byte[]//crc低位表
        {
            0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
            0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
            0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
            0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
            0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
            0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
            0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
            0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
            0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
            0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
            0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
            0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
            0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
            0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
            0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
            0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
            0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
            0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
            0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
            0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
            0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
            0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
            0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
            0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
            0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
            0x43, 0x83, 0x41, 0x81, 0x80, 0x40
        };

        public ushort CalculateCrc16(byte[] buffer, out byte crcLo, out byte crcHi)//协议默认低位在前
        {
            crcHi = 0xff;  // high crc byte initialized
            crcLo = 0xff;  // low crc byte initialized 

            for (int i = 0; i < buffer.Length - 2; i++)
            {
                int crcIndex = crcHi ^ buffer[i]; // calculate the crc lookup index

                crcHi = (byte)(crcLo ^ _auchCRCHi[crcIndex]);
                crcLo = _auchCRCLo[crcIndex];
            }

            return (ushort)(crcHi << 8 | crcLo);
        }
    }
}

?

丛机的仿真器打开,设置串口端口号等参数,开启连接,运行软件

点击读按钮,定时器触发读。

调试输出如下:

串口接收01 03 08 00 01 00 02 00 03 00 04 0D 14

第一个数1

第二个数2

第三个数3

第四个数4

?总结:通过上述实例很好的演示了接收数据不全的问题。

文章来源:https://blog.csdn.net/qq_30725967/article/details/134974211
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。