ref.1
正常项目,一般都是网线tcp通讯的那种,官方叫 iso on tcp 这种方式 西门子的做法和 现在的大众软件方法类似,开放一个默认端口,是102,可以同时连接多个客户端; tcp 客户端连上西门子的102端口后,必须做一个特定报文的交互,然后才能做正常的读取操作;
可能由于西门子的102端口是1对多的,所以它比较拽,一旦发现通讯数据格式或内容不正确,不但不回复,而且直接跟你把通讯通道断开, 不理你了; 三菱遇到异常报文时,通讯不会断的,只是回复你异常数据,并且有异常代码;
解析的是以 0x32 开始的报文结构
借助WireShark抓包,可以看到,S7Comm 以太网协议基于OSI模型:
OSI layer
Protocol
7 Application Layer
S7 communication
6 Presentation Layer
S7 communication(COTP)
5 Session Layer
S7 communication(TPKT)
4 Transport Layer
ISO-on-TCP (RFC 1006)
3 Network Layer
IP
2 Data Link Layer
Ethernet
1 Physical Layer
Ethernet
其中,第1~4层会由计算机自己完成(底层驱动程序); 第5层TPKT,应用程数据传输协议,介于TCP和COTP协议之间;这是一个传输服务协议,主要用来在COTP和TCP之间建立桥梁;
“TPKT is an”encapsulation” protocol. It carries the OSI packet in its ownpacket’s data payload and then passes the resulting structure to TCP, from thenon, the packet is processed as a TCP/IP packet. The OSI programs passing datato TPKT are unaware that their data will be carried over TCP/IP because TPKTemulates the OSI protocol Transport Service Access Point (TSAP).”
第6层COTP,按照维基百科的解释,COTP 是 OSI 7层协议定义的位于TCP之上的协议。COTP 以“Packet”为基本单位来传输数据,这样接收方会得到与发送方具有相同边界的数据;
第7层,S7 communication,这一层和用户数据相关,对PLC数据的读取报文在这里完成;
刚看到TPKT和COPT也许会很迷惑,其实在具体的报文中,TPKT的作用是包含用户协议(5~7层)的数据长度(字节数);COTP的作用是定义了数据传输的基本单位(在S7Comm中 PDU TYPE:DT data);
S7Comm与标准TCP/IP比较:S7Comm是一个7层协议;TCP/IP是四层协议,用户数据在第四层TCP层完成;
计算机与PLC进行通讯,可以连接102端口,这是西门子开放的一个通讯端口;
第七层 S7 communication协议
S7 communication包含三部分:1-Header;2-Parameter;3 - Data。
根据实现的功能不同,S7 communication协议的结构会有所不同;例如,请求数据报文只包含前两部分;
<1>Header
*01(1 byte): protocol Id: 0x32;
*02a(1 byte): ROSCTR: Job (01);
*02b(2 byte): redundancy identification (reserved): 0x0000;
*2c(2 byte): protocol data unit reference; it’s increased by request event;
*2d(2 byte): parameter length - the total length (bytes) of parameter part;
*2e(2 byte): data length; 读取PLC内部数据,此处为00 00;对于其他功能,例如:读取CPU的型号,此处为Data部分的数据长度;
<2>Parameter(读取数据)
*3(1 byte): function code: Read Var (0x04);writeVar (0x05);
*4(1 byte): item count;
*5(1 byte): variable specification: 0x12;
*6(1 byte): length of following address specification – is 7~12length in byte;
*7(1 byte): syntax Id: S7ANY (0x10);
*8(1 byte):transport size: BYTE(2);
*9(2 byte): requested data length;
*10(2 byte): DB number; 如果访问的不是DB区域,此处为00 00;
*11(1 byte): Area: 0x84= data block(DB); 0X82= outputs(Q); 0x81=inputs(I); 0x83= Flags(M); 0x1d= S7 timers(T); 0x1c= S7counters(C);
*12(3 byte):address- start address from zero bit
*5~*12构成了一个基本的数据请求单元[Item],对多个不同地址区域的数据请求,就是有多个[Item]构成的。
Parameter部分的数据结构可以总结为:
[Function code ]+ [Item count] + Item[1] + Item[2] . . . Item[n]
<3>Data
这一部分与功能有关,例如:读取CPU型号、向CPU存储区写数据;在请求数据报文中此部分不包含任何数据。
S7Comm以太网通讯过程
需要”通讯请求”过程。这个过程包含两次报文交换
1> PC 发送COTP报文给PLC;在COTP报文中包含“连接请求”和“destination TSAP” - 明确CPU的机架号和槽号; PLC反馈COTP报文,包含“连接确认”; 这样PLC就清楚了需要和那个CPU来进行数据通讯;
握手1 03 00 00 16 11 e0 00 00 00 04 00 c1 02 01 00 c2 02 01 02 c0 01 0a 握手2 03 00 00 19 02 f0 80 32 01 00 00 02 00 00 08 00 00 f0 00 00 01 00 01 01 e0
2> PC 发送S7Comm报文给PLC;在S7 communicaton报文中包含“通讯请求”; PLC反馈S7Comm报文。
交换数据 数据读写就在这个过程内完成。我们可以组织报文来实现我们需要的功能。这个过程内的报文是S7Comm格式; 具体实现时,需要对S7Comm中的第5、6、7层进行编程。
数据类型 wchar 和 wstring 的定义: 1、数据类型为 wchar(宽字符)的变量长度为 16 位,占用2个 byte 的内存。 wchar 数据类型将扩展字符集中的单个字符保存为 UFT-16 编码形式。 2、数据类型为 wstring (宽字符串)的操作数用于在一个字符串中存储多个数据类型为 wchar 的 Unicode 字符。如果未指定长度,则字符串的长度为预置的 254 个字。
西门子 s7 通信库,读写操作需要手动同步,同时发会导致连接失败 // true signaled; false non-signaled ManualResetEvent manualResetEventPlcHeartbeat = new ManualResetEvent(true); // 可以发送 数据相关命令 ManualResetEvent manualResetEventPlcOn = new ManualResetEvent(false); private void taskMelsecPLCHeartbeatProc() { Log.Information("taskPLCHeartbeatProc enter..."); int iRet = 0; bool setStatus = false; while (FLAG_TASK_EXIT != (flag & FLAG_TASK_EXIT)) { manualResetEventPlcHeartbeat.WaitOne(); manualResetEventPlcOn.Reset(); Console.WriteLine("send heartbeat"); iRet = s7Helper.Open(App.db.m_config[0].PLCIP, 0, 0); if (iRet != 0) { setStatus = true; Log.Error($"open plc failed. err={iRet}"); Dispatcher.Invoke(new Action(() => { tb_status.Text = $"连接PLC失败 code={iRet}"; WindowState = WindowState.Normal; })); System.Threading.Thread.Sleep(500); continue; } Dispatcher.Invoke(new Action(() => { tb_status.Text = "状态:PLC 监控中"; })); byte[] rData = new byte[10]; iRet = s7Helper.ReadMBlockByte(0, App.db.m_config[0].PLCData, 1, ref rData); if (iRet != 0) { Dispatcher.Invoke(new Action(() => { tb_status.Text = $"读取PLC数据失败 code={iRet}"; })); } s7Helper.SetBit(ref rData, 0, 0, true); iRet = s7Helper.WriteMBlockByte(0, App.db.m_config[0].PLCData, 1, ref rData); if (iRet != 0) { Dispatcher.Invoke(new Action(() => { tb_status.Text = $"写入PLC数据失败 code={iRet}"; })); } manualResetEventPlcOn.Set(); for (var i = 0; i < 10; ++i) { if (FLAG_TASK_EXIT == (flag & FLAG_TASK_EXIT)) { goto skip_return; } System.Threading.Thread.Sleep(100); } } skip_return: Log.Information("taskPLCHeartbeatProc exit..."); } public void doBusi(string fileFullPath) { // 停止心跳 manualResetEventPlcOn.WaitOne(); manualResetEventPlcHeartbeat.Reset(); // PLC 读写操作 ... // 开始心跳 manualResetEventPlcHeartbeat.Set(); }
写入 4 字节的浮点数 { float t1 = (float)44.7; int it1 = 0; unsafe { it1 = *(int*)&t1; } float fVal = (float)0.0; unsafe { fVal = *((float*)&it1); } } // 西门子是大端模式,电脑是小端模式,所以需要转换一下 if (BitConverter.IsLittleEndian) { Array.Reverse(rData); } int iVal = BitConverter.ToInt32(rData, 0); float fVal = (float)0.0; unsafe { fVal = *((float*)&iVal); }
读取PLC var param = tb_test_param.Text.Trim(); if (param.Length > 0) { string[] lines = param.Split( new[] { "," }, StringSplitOptions.None ); int iRet = 0; int addr = int.Parse(lines[1]); int startPos = int.Parse(lines[2]); int count = int.Parse(lines[3]); // 分区类型:[DB,M],地址,起始位置,字符数量 byte[] rData = new byte[count]; if (lines[0] == "DB") { iRet = s7Helper.ReadBlockByte(addr, startPos, count, ref rData); } else { iRet = s7Helper.ReadMBlockByte(addr, startPos, count, ref rData); } if (iRet != 0) { Log.Error($"read plc failed. err={iRet}"); Dispatcher.Invoke(new Action(() => { tb_status.Text = $"读取PLC失败1 code={iRet}"; })); }else { var ss = BitConverter.ToString(rData).Replace("-", ""); tb_test.Text = ss; Log.Information($"test_read:{ss}"); } }
设置 plc 位 /* * 设置指定 dbxx 的指定字节的指定位 */ private void SetPLCBit(int dbNumber,int bytePos,int bitPos, bool bitValue) { manualResetEventPlcOn.WaitOne(); manualResetEventPlcHeartbeat.Reset(); int retryCnt = 0; Log.Information($"ClearPLCFlag enter... dbNumber={dbNumber},bytePos={bytePos},bitPos={bitPos},bitValue={bitValue}"); byte[] rData = new byte[30]; skip_retry_read: // 通知 PLC 保存工位参数 int iRet = s7Helper.ReadBlockByte(dbNumber, bytePos, 1, ref rData); if (iRet != 0) { Log.Error($"ClearPLCFlag plc flag failed. code={iRet},retryCnt={retryCnt}"); if (retryCnt < 3) { retryCnt += 1; goto skip_retry_read; } Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => { MessageBox.Show(Application.Current.MainWindow, "清除PLC标志位失败", "错误"); })); } else { s7Helper.SetBit(ref rData, 0, bitPos, bitValue); retryCnt = 0; skip_retry: iRet = s7Helper.WriteBlockByte(dbNumber, bytePos, 1, ref rData); if (iRet != 0) { Log.Error($"write plc flag failed. code={iRet},retryCnt={retryCnt}"); if (retryCnt < 3) { retryCnt += 1; goto skip_retry; } Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => { MessageBox.Show(Application.Current.MainWindow, "设置PLC标志位失败", "错误"); })); } else { Log.Information($"write standby flag ok. data={rData[0]}"); } } manualResetEventPlcHeartbeat.Set(); }