一.串口通讯方式

首先,串口、UART口、COM口、USB口是指的物理接口形式(硬件)。而TTL、RS-232、RS-485是指的电平标准(电信号)。

串口:串口是一个泛称,UART、TTL、RS232、RS485都遵循类似的通信时序协议,因此都被通称为串口

工控中常用的协议:RS232 RS485

RS232:是电子工业协会(Electronic Industries Association,EIA) 制定的异步传输标准接口,同时对应着电平标准和通信协议(时序),其电平标准: 3V~ 15V对应0,-3V~-15V对应1。rs232 的逻辑电平和TTL 不一样但是协议一样

RS485:RS485是一种串口接口标准,为了长距离传输采用差分方式传输,传输的是差分信号,抗干扰能力比RS232强很多。两线压差为-(2~6)V表示0,两线压差为 (2~6)V表示1

常见的D型9针串口(通俗说法)。在台式电脑后边都可以看到。这种接口的协议只有两种:RS-232和RS-485

二.网口通讯方式

TCP/IP 是互联网相关的各类协议族的总称,比如:TCP,UDP,IP,FTP,HTTP,ICMP,SMTP 等都属于 TCP/IP 族内的协议。像这样把与互联网相关联的协议集合起来总称为 TCP/IP。也有说法认为,TCP/IP 是指 TCP 和 IP 这两种协议。还有一种说法认为,TCP/IP 是在 IP 协议的通信过程中,使用到的协议族的统称。

工控中常用的通讯有TCP和UDP

这两个区别就是:TCP 用于在传输层有必要实现可靠传输的情况。由于它是面向有链接并具备顺序控制、重发控制等机制的,所以他可以为应用提供可靠的传输。 而在一方面,UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。 我们举一个通过 IP 电话进行通话的例子。如果使用 TCP,数据在传送途中如果丢失会被重发,但这样无法流畅的传输通话人的声音,会导致无法进行正常交流。而采用 UDP,他不会进行重发处理。从而也就不会有声音大幅度延迟到达的问题。即使有部分数据丢失,也支持会影响某一小部分的通话。此外,在多播与广播通信中也是用 UDP 而不是 TCP。

三.确定PLC硬件

首先要确定PLC是否支持网口或者串口通信,大多数都支持RS232,TCP或者UDP,有的可能要购买拓展模块才能进行通信

四.确定PLC通信协议

通信协议:是指双方实体完成通信或服务所必须遵循的规则和约定。通过通信信道和设备互连起来的多个不同地理位置的数据通信系统,要使其能协同工作实现信息交换和资源共享,它们之间必须具有共同的语言。交流什么、怎样交流及何时交流,都必须遵循某种互相都能接受的规则。这个规则就是通信协议。

以下以欧姆龙FINS通信协议为例

一,握手命令1、客户端向服务器发送命令00000000。这个命令长20字节,分成5组4字节。分别是:头(FINS) 长度(Hex0C) 命令(00000000) 错误码(00000000) 客户机节点地址。46494E53是FINS的ASCII码值,即命令头。0000000C是命令长度20。00000000是命令码。00000000是错误码。00000005是客户节点地址,即电脑IP地址的末位。在发送区输入:46494E53 0000000C 00000000 00000000 00000005点击发送,PLC立即回应:46494E53 00000010 00000001 00000000 00000005 00000020到此我们已经成功地完成了第一步!接下来需要的就是之前介绍过的HostLink协议里面FINS的知识了。

工控机与plc通讯接口(工控C中的串口)(1)

图3 网络调试助手 握手成功2、这个是服务器端(PLC)向客户端(电脑)发送的命令00000001。这个命令长24字节,分成6组4字节。分别是:头(FINS) 长度(Hex10) 命令(00000001) 错误码 客户机节点地址 服务器地址。上面的命令错误代码为0,客户端ip地址05已被服务器32(hex20)成功记录。如果发生错误,服务器回应的命令会包含错误码,连接断开,端口立刻关闭。当连接建立之后,不要再次发送这个命令,否则服务器会返回03错误码,即不支持的命令。全部的错误代码如下:十六进制错误码 含义00000000 正常00000001 头不是‘FINS’ (ASCII code)。00000002 数据太长。00000003 不支持的命令。00000020 所有的连接被占用。00000021 制定的节点已经连接。00000022 未被指定的IP地址试图访问一个被保护的节点。00000023 客户端FINS节点地址超范围。00000024 相同的FINS节点地址已经被使用。00000025 所有可用的节点地址都已使用。二、FINS帧发送命令如果向服务器发送FINS帧,就要用到这个命令。由于FINS帧长度是12-2012,因此命令长度可变,头(FINS) 长度 命令(00000002) 错误码 FINS帧。FINS命令帧内容可参考欧姆龙OMRON PLC之HostLink通讯协议-FINS命令W字/位操作篇,里面有存储区代码和操作代码的内容。例2-1、读DM0开始的2个通道:发送: 46494E53 0000001A 00000002 00000000 80000200 20000005 00FF0101 82000000 000220000005:20是目标地址,05是源地址;00FF0101 :0101是读操作;82000000:82是DM存储区代码,000000是起始地址;0002:是数量。返回: 46494E53 0000001A 00000002 00000000 C0000200 05000020 00FF0101 00001234 567800001234:0000代表操作成功,1234是读回的第一个字,即D0=Hex1234,5678:D1=Hex5678例2-2、W210寄存器写入Hex0388:发送: 46494E53 0000001C 00000002 00000000 80000200 20000005 00FF0102 B100D200 0001038820000005:20是目标地址,05是源地址;00FF0102:0102是写操作代码;B100D200:B1是W字代码,00D2是起始地址,Hex00D2=212,;00010388:是写入数量,0388是写入首个内容;回应: 46494E53 00000016 00000002 00000000 C0000200 05000020 00FF0102 00000102后面紧跟的0000代表写入成功。例2-3、W210寄存器读取:发送: 46494E53 0000001A 00000002 00000000 80000200 20000005 00FF0101 B100D200 000120000005:20是目标地址,05是源地址;00FF0101:0101是读操作代码;B100D200:B1是W字代码,00D2是起始地址,Hex00D2=212,;0001:是读取数量。回应: 46494E53 00000018 00000002 00000000 C0000200 05000020 00FF0101 000003880102后面紧跟的0000代表读取成功,W210=Hex0388例2-4、强制W212.01=On:发送: 46494E53 0000001C 00000002 00000000 80000200 20000005 00FF2301 00010001 3100D40120000005:20是目标地址,05是源地址;00FF2301:2301是强制操作代码;00010001:前面的0001是数量,后面的0001代表强制置位操作;3100D401:31是W位代码,00D401是起始地址,Hex00D4.01=212.01。回应: 46494E53 00000016 00000002 00000000 C0000200 05000020 00FF2301 00002301后面紧跟的0000表示操作成功。注意在CX-Programmer查看窗口中W212.01的值1后面的(强制)字样。

工控机与plc通讯接口(工控C中的串口)(2)

图4 网络调试助手 强制置位

工控机与plc通讯接口(工控C中的串口)(3)

图5 CX-Programmer 强制置位成功例2-5、强制W212.01=Off:发送: 46494E53 0000001C 00000002 00000000 80000200 20000005 00FF2301 00010000 3100D40120000005:20是目标地址,05是源地址;00FF2301:2301是强制操作代码;00010000:0001是数量,0000代表强制复位操作;3100D401:31是W位代码,00D401是起始地址,Hex00D4.01=212.01。回应: 46494E53 00000016 00000002 00000000 C0000200 05000020 00FF2301 00002301后面紧跟的0000表示操作成功。例2-6、取消W212.01强制:发送: 46494E53 0000001C 00000002 00000000 80000200 20000005 00FF2301 0001FFFF 3100D40120000005:20是目标地址,05是源地址;00FF2301:2301是强制操作代码;0001FFFF:0001是数量,FFFF代表取消强制操作;3100D401:31是W位代码,00D401是起始地址,Hex00D4.01=212.01。回应: 46494E53 00000016 00000002 00000000 C0000200 05000020 00FF2301 00002301后面紧跟的0000表示操作成功。注意在CX-Programmer查看窗口中W212.01的值0后面的(强制)字样不见了,表示已经成功地取消了强制。

工控机与plc通讯接口(工控C中的串口)(4)

图6 网络调试助手 取消强制

工控机与plc通讯接口(工控C中的串口)(5)

图7 CX-Programmer 取消强制成功

与CIO不同,对于W、A、H及D 这样的寄存器进行位操作,其实不用强制操作,直接写入更简洁,可以减少操作步骤,下面以W位操作为例介绍。

例2-7、W212.01 按位置位:

发送: 46494E53 0000001B 00000002 00000000 80000200 20000005 00FF0102 3100D401 00010120000005:20是目标地址,05是源地址;00FF0102:0102是寄存器写操作代码;3100D401:31是W位代码,00D401是地址,Hex00D4.01=212.01;

000101:0001是数量,01代表写入值1;回应:

46494E53 00000016 00000002 00000000 C0000200 20000005 00FF0102 0000

0102后面紧跟的0000表示操作成功。

例2-8、W212.01 按位复位:发送: 46494E53 0000001B 00000002 00000000 80000200 20000005 00FF0102 3100D401 000100

20000005:20是目标地址,05是源地址;00FF0102:0102是寄存器写操作代码;3100D401:31是W位代码,00D401是地址,Hex00D4.01=212.01;

000100:0001是数量,00代表写入值0;

回应:

46494E53 00000016 00000002 00000000 C0000200 20000005 00 FF0102 0000

0102后面紧跟的0000表示操作成功。

五.使用C#进行通信

1.串口

串口在C#中使用的是SerialPort这个类

要注意通讯协议中报文内容是16进制,还是ASCII码,还是就是普通的字符串,有格式的话注意发送和接收的报文需要转换

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO.Ports; using System.Threading; using System.Windows.Forms; public class SerialOP : IDisposable { private SerialPort Comport = new SerialPort(); private AutoResetEvent _manualEvent = new AutoResetEvent(false); private object _lockRead = new object(); private object _lockWrite = new object(); private byte[] _reivedData; private string reive=null; private object onRecive =new object(); public delegate void UpdateByteDelegate(byte[] reive); public event UpdateByteDelegate UpdateByte; /// <summary> /// 打开串口 /// </summary> /// <param name="com"></param> /// <param name="bps"></param> /// <param name="databit"></param> /// <param name="stopbit"></param> /// <param name="check"></param> public bool Open(string com, string bps, string databit, string stopbit, string check) { try { if (Comport.IsOpen) Comport.Close(); Thread.Sleep(100); //串口端口号 Comport.PortName = com; //串口波特率 Comport.BaudRate = int.Parse(bps); //串口数据位 Comport.DataBits = int.Parse(databit); //串口停止位 switch (stopbit) { case "0": Comport.StopBits = StopBits.None; break; case "1": Comport.StopBits = StopBits.One; break; case "1.5": Comport.StopBits = StopBits.OnePointFive; break; case "2": Comport.StopBits = StopBits.Two; break; default: Comport.StopBits = StopBits.None; break; } //串口奇偶校验 switch (check) { case "无": Comport.Parity = Parity.None; break; case "奇校验": Comport.Parity = Parity.Odd; break; case "偶校验": Comport.Parity = Parity.Even; break; default: Comport.Parity = Parity.None; break; } //Comport.ReceivedBytesThreshold = 1;//1字节触发数据获取 //Comport.DataReceived = new SerialDataReceivedEventHandler(OnReceived); Comport.Open(); Comport.DtrEnable = true; Comport.RtsEnable = true; return true; } catch (Exception ex) { LogHelper.error(ex.ToString()); MessageBox.Show("连续打开关闭串口出现错误:" ex.Message,"SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); return false; } } /// <summary> /// 检测串口是否打开 /// </summary> /// <returns></returns> private bool IsOpen() { try { if (Comport == null) return false; if (Comport.IsOpen) return true; else return false; } catch (Exception ex) { LogHelper.error(ex.ToString()); MessageBox.Show("检测串口是否打开出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); return false; } } private void OnReceived(object sender, SerialDataReceivedEventArgs e) { try { lock (onRecive) { byte[] reive = new byte[Comport.BytesToRead]; Comport.Read(reive, 0, Comport.BytesToRead); UpdateByte.Invoke(reive); } } catch (Exception ex) { LogHelper.error(ex.ToString()); MessageBox.Show("串口接收数据出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } finally { _manualEvent.Set(); } } /// <summary> /// 字符串转ASII码发送 /// </summary> /// <returns></returns> public void Send_ASCII(string str) { try { lock (_lockWrite) { var ByteSendW = Encoding.ASCII.GetBytes(str );//把发送数据转换为ASCII数组 Comport.Write(ByteSendW, 0, ByteSendW.Length); } } catch (Exception ex) { LogHelper.error(ex.ToString()); MessageBox.Show("串口发送数据出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } } /// <summary> /// 发送16进制数组,需要用,隔开 /// </summary> /// <param name="Hex"></param> /// <param name="check"></param> public void Send_HEX(string Hex) { try { lock (_lockWrite) { //16进制字符串转换成16进制数组 var HEX = Hex.Split(',').Select(temp => "0x" temp).Select(temp => (byte)Convert.ToInt32(temp, 16)).ToArray(); Comport.Write(HEX, 0, HEX.Length); } } catch (Exception ex) { LogHelper.error(ex.ToString()); MessageBox.Show("串口发送数据出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } } /// <summary> /// 发送字符串不转换 /// </summary> /// <param name="str"></param> public void Send_String(string str) { try { lock (_lockWrite) { Comport.Write(str); } } catch (Exception ex) { LogHelper.error(ex.ToString()); MessageBox.Show("串口发送数据出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } } /// <summary> /// 接收ASCII码,并转换成字符串 /// </summary> /// <param name="str"></param> public void Read_ASCII(out string str) { str = "error"; try { lock (_lockRead) { if (Comport.BytesToRead == 0) { LogHelper.error("缓存区没有数据"); return; } for (int i = 0; i < 100; i )//读取100个字节 { if (Comport.BytesToRead == 0) { str = reive; reive = null; break; } string reivestring = ""; _reivedData = new byte[1]; Comport.Read(_reivedData, 0, _reivedData.Length); reivestring = Encoding.ASCII.GetString(_reivedData); reive = reivestring; //Comport.DiscardInBuffer(); } } } catch (Exception ex) { str = ex.ToString(); LogHelper.error(ex.ToString()); MessageBox.Show("串口读取数据出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } } /// <summary> /// 接收字符串,不进行任何转换 /// </summary> /// <param name="str"></param> public void Read_String(out string str) { str = "error"; try { lock (_lockRead) { if (Comport.BytesToRead == 0) { LogHelper.error("缓存区没有数据"); return; } for (int i = 0; i < 100; i )//读取100个字节 { if (Comport.BytesToRead == 0) { str = reive; reive = null; break; } _reivedData = new byte[1]; Comport.Read(_reivedData, 0, _reivedData.Length); reive = _reivedData[0].ToString() " "; } } } catch (Exception ex) { str = ex.ToString(); LogHelper.error(ex.ToString()); MessageBox.Show("串口读取数据出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } } /// <summary> /// 接收到16进制,转成字符串 /// </summary> /// <param name="str"></param> public void Read_HEX_String(out string str) { str = "error"; try { lock (_lockRead) { if (Comport.BytesToRead == 0) { LogHelper.error("缓存区没有数据"); return; } for (int i = 0; i < 100; i )//读取100个字节 { if (Comport.BytesToRead == 0) { reive = reive.Substring(0, reive.Length - 1);//移除最后一位逗号 str = reive; reive = null; break; } _reivedData = new byte[1]; Comport.Read(_reivedData, 0, _reivedData.Length); reive = _reivedData[0].ToString("x2").ToUpper() ","; } } } catch (Exception ex) { str = ex.ToString(); LogHelper.error(ex.ToString()); MessageBox.Show("串口读取数据出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } } /// <summary> /// 关闭串口 /// </summary> public void Close() { try { if (Comport.IsOpen) Comport.Close(); } catch (Exception ex) { LogHelper.error(ex.ToString()); MessageBox.Show("关闭串口出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } } /// <summary> /// 释放串口 /// </summary> public void Dispose() { try { if (Comport.IsOpen) Comport.Close(); Comport.Dispose(); } catch (Exception ex) { LogHelper.error(ex.ToString()); MessageBox.Show("释放串口出现错误:" ex.Message, "SerialPort---Message", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); } //Comport.DataReceived -= new SerialDataReceivedEventHandler(Comport_DataReceived); } /// <summary> /// CRC16校验 /// </summary> /// <param name="data"></param> /// <returns></returns> public string CRCCalc(string data) { string[] datas = data.Split(' '); List<byte> bytedata = new List<byte>(); foreach (string str in datas) { bytedata.Add(byte.Parse(str, System.Globalization.NumberStyles.AllowHexSpecifier)); } byte[] crcbuf = bytedata.ToArray(); //计算并填写CRC校验码 int crc = 0xffff; int len = crcbuf.Length; for (int n = 0; n < len; n ) { byte i; crc = crc ^ crcbuf[n]; for (i = 0; i < 8; i ) { int TT; TT = crc & 1; crc = crc >> 1; crc = crc & 0x7fff; if (TT == 1) { crc = crc ^ 0xa001; } crc = crc & 0xffff; } } string[] redata = new string[2]; redata[1] = Convert.ToString((byte)((crc >> 8) & 0xff), 16); redata[0] = Convert.ToString((byte)((crc & 0xff)), 16); return redata[0].ToUpper() " " redata[1].ToUpper(); } }

2.网口

网口分客户端和服务器,协议有TCP和UDP

以下以TCP为例

TCP客户端

public class TcpClient { public bool connected =false ; Socket socketClient;//客户端接口 Task threadAcceptClient,//客户端接收线程 threadClient; //客户端线程 private string IpAddress = string.Empty; private int Port = 0; public TcpClient(string IpAddress, int Port) { this.IpAddress = IpAddress; this.Port = Port; ClientConnectSever(); } /// <summary> /// 客户端连接服务器 /// </summary> public bool ClientConnectSever() { IPAddress ip = IPAddress.Parse(IpAddress); IPEndPoint port = new IPEndPoint(ip, Port);//服务器port //创建TCP类型端口 socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { socketClient.Connect(port); connected = true; return true; } catch (Exception ex) { LogHelper.error(IpAddress "连接服务器失败:" ex.Message); return false; } } public void ColseConnect() { socketClient.Close(); } public bool ClientSendData(string data) { if (!connected) { LogHelper.error(IpAddress "未连接到服务器"); return false; } byte[] msg = Encoding.Default.GetBytes(data); try { NetworkStream netStream = new NetworkStream(socketClient); netStream.Write(msg, 0, msg.Length); netStream.Flush(); return true; } catch (Exception ex) { LogHelper.error(IpAddress "发送失败:" ex.Message); return false; } } public string ClientReadData() { try { if (!connected) { LogHelper.error(IpAddress "未连接到服务器"); return "error"; } NetworkStream netStream = new NetworkStream(socketClient); byte[] dataSize = new byte[1024]; netStream.Read(dataSize, 0, dataSize.Length); var Result = Encoding.Default.GetString(dataSize).TrimEnd('\0'); if (Result.Length > 1) return Result; else return string.Empty; //this.rtb_accept1.Rtf = Encoding.Unicode.GetString(message); } catch (Exception ex) { LogHelper.error(IpAddress "读取数据失败:" ex.Message); return "error"; } } }

TCP服务器

public class TcpServer { public delegate void UpdateObjectDelegate(object sender); public event UpdateObjectDelegate UpdataDataString; public event UpdateObjectDelegate UpdateMessage; private string IpAddress = string.Empty; private int Port = 0; public TcpServer(string IpAddress, int Port) { this.IpAddress = IpAddress; this.Port = Port; } static Socket serverSocket; List<Socket> ClientList = new List<Socket>(); //客户端列表 public bool connected = false; public void StartListen() { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); serverSocket = socket; //ip port socket.Bind(new IPEndPoint( IPAddress.Parse(IpAddress), Port)); //listen socket.Listen(10);//连接等待队列 ThreadPool.QueueUserWorkItem(new WaitCallback(this.AcceptClientConnect), socket); } private void AcceptClientConnect(object socket) { var serverSocket = socket as Socket; UpdateMessage.Invoke("服务器开始监听"); while (true) { try { var proxSocket = serverSocket.Accept(); ClientList.Add(proxSocket); UpdateMessage.Invoke((object)("客户端" proxSocket.RemoteEndPoint.ToString() "连接上了")); //接受消息 ThreadPool.QueueUserWorkItem(new WaitCallback(this.ReceiveData), proxSocket); connected = true; } catch (Exception e) { UpdateMessage.Invoke("服务器监听错误:" e.ToString()); connected = false; } } } private void ReceiveData(object obj) { Socket proxSocket = obj as Socket; byte[] data = new byte[1024 * 1024]; while (true) { int readLen = 0; try { readLen = proxSocket.Receive(data, data.Length,0); //if (readLen <= 0) //{ // //客户端正常退出 // UpdateMessage.Invoke(string.Format("客户端{0}正常退出", proxSocket.RemoteEndPoint.ToString())); // ClientList.Remove(proxSocket); // CloseListen(proxSocket); // connected = false; // return;//方法结束->终结当前接受客户端数据的异步线程 //} string txt = Encoding.Default.GetString(data, 0, readLen).TrimEnd('\0'); if(txt.Length>1) UpdataDataString.Invoke(txt); } catch (Exception ex) { //异常退出时 UpdateMessage.Invoke(string.Format("客户端{0}非正常退出,原因{1}", proxSocket.RemoteEndPoint.ToString(), ex.ToString())); ClientList.Remove(proxSocket); CloseListen(proxSocket); connected = false; return; } } } private void CloseListen(Socket proxSocket) { try { if (proxSocket.Connected) { proxSocket.Shutdown(SocketShutdown.Both); proxSocket.Close(100); connected = false; } } catch (Exception) { UpdateMessage("服务器关闭发生异常"); connected = false; } } public bool ServerSendData(string msg) { try { foreach (Socket s in this.ClientList) { //服务端广播式发送给客户端 (s as Socket).Send(Encoding.Default.GetBytes(msg)); } return true; } catch { return false; } } }

,