公司给的一个小任务,这篇文章进行详细讲解编程
主要内容以下:
一、实现使用modbus通信规约的测试软件;
二、具备通讯超时功能;
三、分主站从站,并能编辑报文、生成报文等;
四、计算发送报文次数,接收报文次数,失败通讯次数;
五、对接收的数据进行解析。数组
下面图片能够看出具体的内容:
缓存
该小软件使用的知识以下:
一、modbus通讯规约;
二、串口通信;
三、定时器;
四、多线程;多线程
modbus是一个工业上经常使用的通信协议,一个通信约定,包括RTU,ASCII,TCP。该软件使用的RTU。函数
主站设备查询:
查询消肿的功能号告知被选中的设备要执行何种功能。数据段包括了从站设备要执行的功能的任何附加信息。测试
从站设备回应:
当从站设备正常回应后,在回应数据里也包括这功能号,并直接截取从站设备收集的数据。若是发生错误,功能号将被修改成用于指出回应消息为错误消息。并在数据段包括该描述的错误信息。错误校测域容许主设备确认消息的内容是否可用,是否正确。线程
下面的图片解释了modbus的规约的组成:设计
mobus通信规约是由从机地址+功能号+数据地址+数据+CRC校验。指针
从机地址:该规约是单主站/多从站,主站轮询向从站请求的方式进行传输数据,并使用从机地址的方式区分从机。code
功能号: 某指令是干啥,一目了然。接收方将经过功能号进行相应的执行功能。
下面为经常使用功能号:
数据地址:意思是数据存储的地址,从该存储的地址的获取数据。
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的器件,也就是说硬件称为串行接口电路。
串口通信重要的参数有波特率,数据位,中止位,奇偶校验。
一、波特率,这是一个衡量符号传输速率的参数,指的是信号被调制之后在单位时间内的变化,即单位时间内载波参数变化的次数,如每秒钟传960个字符,而每一个字符格式包含10位(1个起始位,1个中止位,8个数据位)这是波特率为960Bd,比特率就是9600bps,
二、数据位:这是衡量通讯中实际数据位的参数,当计算机发送一个信息包,实际的数据每每不会是8位,标准的是六、7和8位,标准的ASCII码是0~127(7位),扩展的ASCII码是0~255(8位),
三、中止位:用于表示单个包的最后几位,典型的值为1,1.5和2位。做用就是数据在传输线上定时的,而且每个有其本身的时钟,极可能在通讯中两台设备出现不一样步的状况,中止位能够解决这个问题,它不只表示传输结束,还能够提供计算机矫正同步时钟的机会。
四、校验位:在串口通讯中一种简单的检错方式,有四种检错方式:奇,偶,高、低。
下面是我写的串口通信的代码:
一、加载串口配置
#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
二、处理数据的定时器,在定时器里面对接收到的数据进行压到队列里面,后期对队列进行再次的处理。
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次定时都尚未将数据传输完毕,则认为数据传输完毕。
三、发送数据
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事件; }
定时器是在通信方面是常用到的,下面我讲解一下我这个小软件使用到定时器位置
一、超时通讯功能
二、定时发送功能
三、接收功能
三、定时显示某些数据,好比发送次数,接收次数,失败通讯次数等。
在定时器使用过程当中,也会使用到线程。好比,有些地方为了与其余功能分离开来。
下面给出开启线程的代码
public void StartSendThread() { Thread SendThread = new Thread(SendMsg); SendThread.Start(); }
一、界面
因为须要作两个软件(主从站),我将两个软件融合在一块儿,使用选择站点的方式进行开启主站或者从站。 主站的界面和从站的界面很类似,为了让用户操做一致。
二、在上述给出了知识讲解中,基本包含了软件的设计思路,主从站之分在于报文拟制不一样,串口发送过程相同,所使用的方式也相同,就不具体讨论,下面对重要设计思想进行描述。
(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之间作转换。
一、string转byte[]
byte[] buf = BitConverter.GetBytes(short.Parse(str));
二、byte[]转string
System.Text.Encoding.Default.GetString(buf);
三、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(); }
四、int 转 string
str = i.ToString()
五、string转int
UInt16 i= UInt16.Parse(str)
通过这个软件的练习,我对C#语言有必定的了解,须要多实践,多编程。
C#语言和C++语言仍是有不少不同的地方,C#没有指针,用的怪怪的,没有从地址角度去考虑数据,数据容易管理很差,我的以为。
最后一点就是学到了不少东西,文章也慢慢开始写,须要多积累,多运用,才是属于本身的。