简介

公司给的一个小任务,这篇文章进行详细讲解

题目: modbus串口通讯

主要内容如下:

1、实现使用modbus通讯规约的测试软件;

2、具有通信超时功能;

3、分主站从站,并能编辑报文、生成报文等;

4、计算发送报文次数,接收报文次数,失败通信次数;

5、对接收的数据进行解析。

下面图片可以看出具体的内容:

知识点讲解

该小软件使用的知识如下:

1、modbus通信规约;

2、串口通讯;

3、定时器;

4、多线程;

1、modbus通讯规约

modbus是一个工业上常用的通讯协议,一个通讯约定,包括RTU,ASCII,TCP。该软件使用的RTU。

主站设备查询:

查询消肿的功能号告知被选中的设备要执行何种功能。数据段包括了从站设备要执行的功能的任何附加信息。

从站设备回应:

当从站设备正常回应后,在回应数据里也包括这功能号,并直接截取从站设备收集的数据。如果发生错误,功能号将被修改为用于指出回应消息为错误消息。并在数据段包括该描述的错误信息。错误校测域允许主设备确认消息的内容是否可用,是否正确。

下面的图片解释了modbus的规约的组成:



mobus通讯规约是由从机地址+功能号+数据地址+数据+CRC校验。

从机地址:该规约是单主站/多从站,主站轮询向从站请求的方式进行传输数据,并使用从机地址的方式区分从机。

功能号: 某指令是干啥,一目了然。接收方将通过功能号进行相应的执行功能。

下面为常用功能号:

数据地址:意思是数据存储的地址,从该存储的地址的获取数据。

CRC校验:循环冗余校验码,是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定。

对于校验,网上资料很多,这里直接上代码:

    #region  CRC16
public static byte[] CRC16(byte[] data)
{
int len = data.Length;
if (len > 0)
{
ushort crc = 0xFFFF; for (int i = 0; i < len; i++)
{
crc = (ushort)(crc ^ (data[i]));
for (int j = 0; j < 8; j++)
{
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
}
byte hi = (byte)((crc & 0xFF00) >> 8); //高位置
byte lo = (byte)(crc & 0x00FF); //低位置 return new byte[] { hi, lo };
}
return new byte[] { 0, 0 };
}
#endregion

串口通讯

在C#中实现串口通讯,由于C#微软封装的很好,提供了SerialPort类,命名空间为system.IO.Ports.


下面解释serialPort类编程中常用到的关键字和方法:

常用字段:

PortName 获取或设置通信端口

BaudRate 获取或设置串行波特率

DataBits 获取或设置每个字节的标准数据位长度

Parity 获取或设置奇偶校验检查协议

StopBits 获取或设置每个字节的标准停止位数

常用方法:

Close 关闭端口连接,将IsOpen 属性设置为false,并释放内部 Stream 对象

GetPortNames 获取当前计算机的串行端口名称数组

Open 打开一个新的串行端口连接

Read 从 SerialPort 输入缓冲区中读取

Write 将数据写入串行端口输出缓冲区


串口通信简介

串口是一种可以接受来自CPU的并行数据字符转换为连续的的串行数据流发送出去,同时可将接受的串行数据流转换为并行的数据字符供给CPU的器件,也就是说硬件称为串行接口电路。

串口通讯重要的参数有波特率,数据位,停止位,奇偶校验。

1、波特率,这是一个衡量符号传输速率的参数,指的是信号被调制以后在单位时间内的变化,即单位时间内载波参数变化的次数,如每秒钟传960个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位)这是波特率为960Bd,比特率就是9600bps,

2、数据位:这是衡量通信中实际数据位的参数,当计算机发送一个信息包,实际的数据往往不会是8位,标准的是6、7和8位,标准的ASCII码是0127(7位),扩展的ASCII码是0255(8位),

3、停止位:用于表示单个包的最后几位,典型的值为1,1.5和2位。作用就是数据在传输线上定时的,并且每一个有其自己的时钟,很可能在通信中两台设备出现不同步的情况,停止位可以解决这个问题,它不仅表示传输结束,还可以提供计算机矫正同步时钟的机会。

4、校验位:在串口通信中一种简单的检错方式,有四种检错方式:奇,偶,高、低。


下面是我写的串口通讯的代码:

1、加载串口配置

   #region 加载串口配置   

    public bool LoadSerialConfig(string com, string BAUDRATE, string DATABITS, string STOP, string PARITY)
{
if (!sp1.IsOpen) //没打开
{
try
{
//设置串口号
string serialName = com;
sp1.PortName = serialName; //设置各“串口设置”
string strBaudRate = BAUDRATE;
string strDateBits = DATABITS;
string strStopBits = STOP;
Int32 iBaudRate = Convert.ToInt32(strBaudRate);
Int32 iDateBits = Convert.ToInt32(strDateBits); sp1.BaudRate = iBaudRate; //波特率
sp1.DataBits = iDateBits; //数据位
switch (STOP) //停止位
{
case "1":
sp1.StopBits = StopBits.One;
break;
case "1.5":
sp1.StopBits = StopBits.OnePointFive;
break;
case "2":
sp1.StopBits = StopBits.Two;
break;
default:
//MessageBox.Show("Error:参数不正确!", "Error");
break;
} switch (PARITY) //校验位
{
case "NONE":
sp1.Parity = Parity.None;
break;
case "奇校验":
sp1.Parity = Parity.Odd;
break;
case "偶校验":
sp1.Parity = Parity.Even;
break;
default:
//MessageBox.Show("Error:参数不正确!", "Error");
break;
} //如果打开状态,则先关闭一下
if (sp1.IsOpen == true)
{
sp1.Close();
}
sp1.Open(); //打开串口
return true;
}
catch (System.Exception ex)
{
SetSerialOpenFlag(false);
Form1.ShowThrow(ex);
return false;
}
}
else //已经打开
{
return true;
}
}
#endregion

2、处理数据的定时器,在定时器里面对接收到的数据进行压到队列里面,后期对队列进行再次的处理。

public void StartTimeOutTimer( UInt16 SendDataShowTimer,bool autoFlag)
{
//实例化Timer类,设置间隔时间为10000毫秒;
timeOutTimer = new System.Timers.Timer(SendDataShowTimer);
timeOutTimer.Elapsed += new System.Timers.ElapsedEventHandler(EndTimeProcess);
timeOutTimer.AutoReset = autoFlag;//设置是执行一次(false)还是一直执行(rtue);
timeOutTimer.Enabled = true;//是否执行System.Timers.Timer.Elapsed事件;
} private void EndTimeProcess(Object sender, EventArgs e)
{
if (GetSerialOpenFlag())
{
recvBytesNum = (UInt16)sp1.BytesToRead; if (recvBytesNum == 0 && delayTime <= TimeOutFailMaxTime)
{
delayTime++;
timeOutTimer.Start(); //定时器应该执行一次,然后在这从新开始,比如100毫秒后还未接收到数据,就记下数后重新开始定时器
}
else //通过sp1.BytesToRead已经知道串口接收缓存区的大小,使用read函数直接取数,
{
if (sp1.BytesToRead > 0) //有数据,下面接收数据并校验数据
{
//接收16进制
try
{
lock (Recvlock) //加锁
{
Byte[] receiveddata = new Byte[sp1.BytesToRead]; //创接建收字节数组
sp1.Read(receiveddata, 0, receiveddata.Length); //读取数据
sp1.DiscardInBuffer(); //清空SerialPort控件的Buffer
if (receiveddata.Length <= 0)
return;
DataProcessorQueue.Enqueue(receiveddata);
}
delayTime = 0;
recvBytesNum = 0;
}
catch (Exception ex)
{
Form1.ShowThrow(ex);
return;
}
}
else //超过多次定时都为串口缓冲区的数据都为空,则说明通讯超时
{
ConnectFailCount += 1;
}
}
} } #region 串口数据接收
void sp1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (GetSerialOpenFlag()) //此处可能没有必要判断是否打开串口,但为了严谨性,我还是加上了
{
timeOutTimer.Stop();
timeOutTimer.Start();
}
else
{
Form1.ShowThrow("串口没有成功打开");
return;
}
}
#endregion

在这里讲解一下为什么需要用到定时器?

由于要实现通讯超时功能,所以我这里使用定时器的方式,开始接收到数据后开始定时,直到我的数据在定时间内发送过来,我设定了小于3次的定时,如果3次定时都还没有将数据传输完毕,则认为数据传输完毕。

3、发送数据

public void SendTextdelegate(byte[] buf)
{
SetSendText( buf);
StartSendThread();
}
public void SetSendText(byte[] buf)
{
strSend = System.Text.Encoding.Default.GetString(buf);
}

为了自己封装一个类,并与UI进行分离,我使用的是C#常用的委托方式,从Form类中传入数据,

定时器

先上代码

public System.Timers.Timer timeOutTimer;    //定义定时器

public void StartDataProcessorTimer( bool autoFlag)
{
//实例化Timer类,设置间隔时间为10000毫秒;
timeOutTimer = new System.Timers.Timer(DataProcessorTimer);
timeOutTimer.Elapsed += new System.Timers.ElapsedEventHandler(DataProcess);
timeOutTimer.AutoReset = autoFlag;//设置是执行一次(false)还是一直执行(true);
timeOutTimer.Enabled = true;//是否执行System.Timers.Timer.Elapsed事件;
}

定时器是在通讯方面是经常使用到的,下面我讲解一下我这个小软件使用到定时器位置

1、超时通信功能

2、定时发送功能

3、接收功能

3、定时显示某些数据,比如发送次数,接收次数,失败通信次数等。

多线程

在定时器使用过程中,也会使用到线程。比如,有些地方为了与其他功能分离开来。

下面给出开启线程的代码

public void StartSendThread()
{
Thread SendThread = new Thread(SendMsg);
SendThread.Start();
}

软件思路

1、界面

由于需要做两个软件(主从站),我将两个软件融合在一起,使用选择站点的方式进行开启主站或者从站。 主站的界面和从站的界面很相似,为了让用户操作一致。

2、在上述给出了知识讲解中,基本包含了软件的设计思路,主从站之分在于报文拟制不同,串口发送过程相同,所使用的方式也相同,就不具体讨论,下面对重要设计思想进行描述。

(a)使用锁,由于某些数据需要进行同步,我选择的是加锁的方式。

给出一部分的代码如下:

lock (Recvlock)    //加锁
{
Byte[] receiveddata = new Byte[sp1.BytesToRead]; //创接建收字节数组
sp1.Read(receiveddata, 0, receiveddata.Length); //读取数据
sp1.DiscardInBuffer(); //清空SerialPort控件的Buffer
if (receiveddata.Length <= 0)
return;
DataProcessorQueue.Enqueue(receiveddata);
}

实现数据同步的方式很多,数据同步,为了让多线程同时操作同一个缓存区时,能够保证数据一致性,

(b)队列,由于考虑到发送方发送数据过快时,我使用的是队列将接收的数据进行存储下来,然后再开启另外一个定时器和线程去队列取数,并将数据,分析,校验以及显示等等。这样的方式可以不用考虑对方何时发送,发送速度的问题,但有一个问题就是队列的大小有限制,我选择的队列是System.Collections.Generic.Queue,C#中队列很多,这种队列可以解决队列大小限制的问题。

(C)配置文件

为了让软件在初始化串口参数,我使用的是配置文件对串口参数进行设置。

下面为配置文件的代码:

private static IniFile _file;//内置了一个对象

public static void LoadProfile_Serial()
{
string strPath = AppDomain.CurrentDomain.BaseDirectory;
_file = new IniFile(strPath + "Cfg.ini");
G_BAUDRATE = _file.ReadString("CONFIG", "BaudRate", "4800"); //读数据,下同
G_DATABITS = _file.ReadString("CONFIG", "DataBits", "8");
G_STOP = _file.ReadString("CONFIG", "StopBits", "1");
G_PARITY = _file.ReadString("CONFIG", "Parity", "NONE"); }

(d)数据转换

下面对数据转换做一个总结:

做通讯软件,数据转换是必要的,在string,2进制,10进制,16进制,byte之间做转换。

1、string转byte[]

byte[] buf = BitConverter.GetBytes(short.Parse(str));

2、byte[]转string

System.Text.Encoding.Default.GetString(buf);

3、byte[]转16进制的string

public static string ByteToString(byte[] InBytes)
{
string StringOut = "";
foreach (byte InByte in InBytes)
{
StringOut = StringOut + String.Format("{0:X2}", InByte) + " ";
}
return StringOut.Trim();
}

4、int 转 string

str = i.ToString()

5、string转int

UInt16 i= UInt16.Parse(str)

总结

经过这个软件的练习,我对C#语言有一定的了解,需要多实践,多编程。

C#语言和C++语言还是有很多不一样的地方,C#没有指针,用的怪怪的,没有从地址角度去考虑数据,数据容易管理不好,个人觉得。

最后一点就是学到了很多东西,文章也慢慢开始写,需要多积累,多运用,才是属于自己的。

modbus串口通讯C#的更多相关文章

  1. .NET Core 跨平台 串口通讯 ,Windows/Linux 串口通讯,flyfire.CustomSerialPort 的使用

    目录 1,前言 2,安装虚拟串口软件 3,新建项目,加入 flyfire.CustomSerialPort 4,flyfire.CustomSerialPort 说明 5,开始使用 flyfire.C ...

  2. 基于STM32和W5500的Modbus TCP通讯

    在最近的一个项目中需要实现Modbus TCP通讯,而选用的硬件平台则是STM32F103和W5500,软件平台则选用IAR EWAR6.4来实现. 1.移植千的准备工作 为了实现Modbus TCP ...

  3. C#串口通讯实例

    本文参考<C#网络通信程序设计>(张晓明  编著) 程序界面如下图: 参数设置界面代码如下: using System; using System.Collections.Generic; ...

  4. delphi之动态库调用和串口通讯

    串口通讯: Spcomm 控件属性: CommName  :表示COM1,COM2等串口的名字: BaudRate:设定波特率9600,4800等 StartComm StopComm 函数Write ...

  5. 用SPCOMM 在 Delphi中实现串口通讯 转

      用Delphi 实现串口通讯,常用的几种方法为:使用控件如MSCOMM和SPCOMM,使用API函数或者在Delphi 中调用其它串口通讯程序.利用API编写串口通信程序较为复杂,需要掌握大量通信 ...

  6. 西门子plc串口通讯方式

    西门子plc串口通讯的三种方式 时间:2015-10-25 14:31:55编辑:电工栏目:西门子plc 导读:西门子plc串口通讯的三种方式,分为RS485 串口通信.PPI 通信.MPI 通信,自 ...

  7. 教程-Delphi MSComm 实时串口通讯

    Delphi  MSComm 实时串口通讯 MSComm控件具有丰富的与串口通信密切相关的属性,提供了对串口进行的多种操作,进而使串行通信变得十分简便.MSComm的控件属性较多,常用的属性如下:1) ...

  8. 浅析PC机串口通讯流控制

    转自浅析PC机串口通讯流控制 我们在串行通讯处理中,常常看到RTS/CTS和XON/XOFF这两个选项,这就是两个流控制的选项,目前流控制主要应用于调制解调器的数据通讯中,但对普通RS232编程,了解 ...

  9. STM32F10x 学习笔记6(USART实现串口通讯 2)

    这次讲讲利用串口收发中断来进行串口通讯.STM32 上为每个串口分配了一个中断.也就是说无论是发送完成还是收到数据或是数据溢出都产生同一个中断.程序需在中断处理函数中读取状态寄存器(USART_SR) ...

随机推荐

  1. Java之mybatis详解

    文章大纲 一.mybatis介绍二.mybatis代码实战三.项目源码下载四.参考文章   一.mybatis介绍 1. mybatis是什么?   mybatis是一个持久层的框架,是apache下 ...

  2. xmlString和map互转Util

    目录 XmlAndMapUtil类 XmlAndMapUtil类 import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org ...

  3. Mac OS X中开启或关闭显示隐藏文件命令

    前言:之前一直用的都是 windows 系统的电脑,刚接触 Mac ,很多功能都不熟悉,写下博客记录一下,以防以后忘记,也给后来者提供方便. 命令行方式:显示隐藏文件: defaults write ...

  4. dart 如何优雅的避空

    前言 对于每一个程序员来说,空指针异常应该是基本都会遇到过的异常,而且这个异常出现的概率还比较大. 但是,空指针异常又是最容易解决的异常,因为只要加个非空判断就可以避免了. 本篇通过对比一般非空判断和 ...

  5. Git常用简介

    Git是什么 git是目前最先进的分布式版本控制系统,它的核心架构如下图所示,分为四个核心区域.git的常用命令主要是关于这四个区域. 本地工作区-work 本地工作区就是我们实际电脑中的文件夹以及文 ...

  6. 【English EMail】2019 Q2 Public Holiday Announcement

    Hi all, According to 2019 public holiday announcement released by Chinese government, this is to ann ...

  7. Linux2:Linux目录结构

    Linux目录图 进入根目录,使用ll命令看一下Linux整个根目录图: 这里面所有的目录都是买完服务器之后最初始的目录,没有进过任何加工.Linux以树的结构组织所有目录,用一张图表示一下Linux ...

  8. Quartz简单实现定时任务管理(SSM+Quartz)

    首先你得有一个用Maven搭好的SSM框架,数据库用的Mysql,这里只有关于Quartz的部分.其实有大神总结的很好了,但做完后总有些地方不一样,所以写这篇作为笔记.这里先把大神的写的分享给大家:h ...

  9. Java Socket聊天室编程(二)之利用socket实现单聊聊天室

    这篇文章主要介绍了Java Socket聊天室编程(二)之利用socket实现单聊聊天室的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下 在上篇文章Java Socket聊天室编程(一)之 ...

  10. PHP中$GLOBALS和global的区别

    很多人都认为$GLOBALS['var']和global $var只是写法上不同,其实并不是这样 根据官方的解释是  $GLOBALS['var']是外部全局变量$var的本身, 而global $v ...