手摸手教你撕碎西门子S7通讯协议05--S7Read读取bool数据

1、S7通讯回顾


    - (1)建立TCP连接      Socket.Connect-》已实现

     - (2)发送访问请求     COTP-》已实现 

     - (3)交换通信信息     Setup Communication-》已实现

     - (4)执行相关操作     读、写、PLC启停、时间、上传下载-》本节实现读取bool数据

2、S7Read请求介绍

在成功完成上面的1,2,3的三个过程后,现在才是读、写、PLC启停、时间、上传下载等,读取PLC数据就是通过这个报文实现的,很明显它的报文复杂多了,结构复杂,内容复杂,参数复杂,难点在于读取后返回的报文解析数据得到PLC数据,不过没有关系,有大V在,一切so easy。先了解这个报文,后面代码中我们一条条拼起来。

从报文要求中,我们可以看到以下几点:

1》报文长度明显比前面的结构复杂,包括5个部分,发送总长是30个,响应总长是》30,因为这是请求的数据长度不一样,自然响应的数据长度也不是一样的,有必要对西门子PLC的存储区和数据类型回顾一下

1)西门子PLC存储区简介
I:数字量输入(DI)

Q:数字量输出

AI:模拟量输入

AQ:模拟量输出

V:变量存储区

M:位存储区

T:定时器存储区

C:计数器存储区

HC:高速计数器

AC:累加器

SM:特殊存储器

L:局部存储区

S:顺序控制继电器

DB:数据块 

2)访问规则:DataType:

0x01 - BIT:一个无符号的bit
0x02 - BYTE:一个8位的数字
0x03 - CHAR:一个字符
0x04 - WORD:两个字节宽的无符号整数
0x05 - INT:两个字节宽的有符号整数。
0x06 - DWORD:四字节宽的无符号整数
0x07 - DINT:四字节宽的有符号整数
0x08 - REAL:四个字节宽的IEEE浮点数
0x1c - COUNTER:PLC程序计数器使用的计数器类型
示例:变量的示例地址是DB123X 2.1,它访问DB块123的第3个Byte的第2个Bit。 

 3)C#与这些数据类型的关系

0x01 - BIT:一个无符号的bit,如bool或false
0x02 - BYTE:一个8位的数字,如0-255
0x03 - CHAR:一个字符,如ascii码,汉字也可以
0x04 - WORD:两个字节宽的无符号整数,如uint16或ushort
0x05 - INT:两个字节宽的有符号整数。如int32
0x06 - DWORD:四字节宽的无符号整数,如uint32
0x07 - DINT:四字节宽的有符号整数,如double
0x08 - REAL:四个字节宽的IEEE浮点数,如float类型数据
0x1c - COUNTER:PLC程序计数器使用的计数器类型

2》发送报文中的功能码是固定的0x04,序号17,它是S7Parameter->Function已知枚举值

 

3》序号21是Syntax Id: S7ANY (0x10),S7Parameter->Item->Syntax Id已知枚举值

4》序号22,它是Transport size: BYTE,表示传输的类型,S7Parameter->Item->Transport size常见值,特别注意一个极小的细小,稍不注意就被带坑里了,在读取数据时,只用到左边的parameterITEM,在写入数据时,就会用到左边的parameterITEM和右边的DATAItem,这个在读取和写入时要特别小心,否则会爆雷。另外这个Transport size: BYTE参数的值是变化的,因为请求不同的数据类型时,它的值是不同的,比如读取一个位bit,那么它的值是0x01,你要读取一个23这样的整形数据,那么它的值是0x04或0x05,理解不?它要随着你请求不同的数据类型来变化。

5》序号27表示存储区代号,这是固定的,S7Parameter->Item->Area常见值,比如报文使用了0x84,这就表示它读取的数据块里面的数据,当然可以是别的值,这要根据你的PLC的类型来确定,但它是支持很多PLC的CPU类型的,在本例子中使用的是1500的CPU,它支持很存储区,是一个比较先进的CPU,支持DB块,I区,Q区,M区等都可以。

 6》响应报文中的error class,error code跟上节的一样,回顾一下

S7Header->Error class已知枚举值

 S7Parameter->Error code已知枚举值

7》return code是响应报文中的参数,它在序号21和26中都出现了,很关键,示例报文中使用的是oxff,看到下图,明白了吗?用来判断报文是否正确,我们当然是希望得到0xff,S7Data->Item->Return code已知枚举值,具体如下:

 3、开搞啦

1)博途创建数据块db1

 添加4个变量,如图,注意这里,先保存再编译,bool类型与word类型,word占2个寄存器

2)添加监控表

 

 

 确保上面操作成功后,进行下步代码

3)读取代码

完整代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace west.siemenscomm
{
    internal class Program
    {
        /// <summary>
        /// plc的ip地址
        /// </summary>
        static string _ip = "192.168.1.66";
        /// <summary>
        /// 端口号
        /// </summary>
        static int _port = 102;
        /// <summary>
        /// 机柜号,插槽号
        /// </summary>
        static byte _rack = 0, _slot = 1;
        /// <summary>
        /// socket对象
        /// </summary>
        static Socket socket = null;
        /// <summary>
        /// 时间事件
        /// </summary>
        static ManualResetEvent TimeoutObject = new ManualResetEvent(false);
        /// <summary>
        /// 连接状态 
        /// </summary>
        static bool connectState = false;
        /// <summary>
        /// 通讯连接的pdu长度
        /// </summary>
        static short _pduSize = 240;

        static void Main(string[] args)
        {
            Connect();
            if (connectState)
            {
                COTPConnection();
                if (connectState)
                {
                    SetupCommunication();
                    if (connectState)
                    {
                         Console.WriteLine(ReadBool());
                    }
                }
            }

            Console.ReadKey();
        }

        /// <summary>
        /// 读取bool
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
       private static bool ReadBool()
        { 
            // TPKT,占4个字节
            List<byte> tpktBytes = new List<byte>();
            tpktBytes.Add(0x03);//Version,版本默认3
            tpktBytes.Add(0x00);//Reserved,保留默认
            // --------整个字节数组的长度,这个稍留着,要等到后面计算出来 

            // COTP,占3个字节
            List<byte> cotpBytes = new List<byte>();
            cotpBytes.Add(0x02);//当前字节以后的字节数
            cotpBytes.Add(0xf0);//PDU Type,数据传输
            cotpBytes.Add(0x80);//TPDU number,固定值

            // Header,占10个字节
            List<byte> headerBytes = new List<byte>();
            headerBytes.Add(0x32); // Protocol Id,默认
            headerBytes.Add(0x01); // ROSCTR:JOB
            headerBytes.Add(0x00); // Redundancy Identification (Reserved
            headerBytes.Add(0x00); // 
            headerBytes.Add(0x00); // Protocol Data Unit Reference
            headerBytes.Add(0x00); // 
            // 添加Parameter字节数组的长度,这个稍留着,要等到后面计算出来 
            // 添加Data字节数组的长度,这个稍留着,要等到后面计算出来 


            // Parameter,占2个字节
            List<byte> paramBytes = new List<byte>();
            paramBytes.Add(0x04); // Function: Read Var (0x04)[
            paramBytes.Add(0x01);// Item count: 1如果有多个区域请求的情况下,这里需要计算,计算Item的个数


            // Item Bytes  ,占12个字节 
            List<byte> itemBytes = new List<byte>();
            itemBytes.Add(0x12); // 结构标识,一般默认0x12
            itemBytes.Add(0x0a); // 此字节往后的字节长度
            itemBytes.Add(0x10); // Syntax Id: S7ANY (0x10) 
            itemBytes.Add(0x01); // Transport size: bit (2) 
            // 读取长度
            itemBytes.Add(0x00); // 高位
            itemBytes.Add(0x01); // 低位
            // DB块编号
            itemBytes.Add(0x00); // 高位
            itemBytes.Add(0x01); // 低位
            // 数据区域
            itemBytes.Add(0x84); // 数据区域
            // 地址,DB1.DBX0.0 
            //计算公式:int address = startAddr * 8 + bitAddr;
            int addr = (0 << 3) +0;
            //字节地址
            itemBytes.Add((byte)(addr / 256 / 256 % 256));
            itemBytes.Add((byte)(addr / 256 % 256));
            //位地址
            itemBytes.Add((byte)(addr % 256)); 

            // 拼装Parameter&Item
            paramBytes.AddRange(itemBytes);
            // 拼装Header&Parameter
            headerBytes.Add((byte)(paramBytes.Count / 256 % 256));//Parameter长度
            headerBytes.Add((byte)(paramBytes.Count % 256));
            headerBytes.Add(0x00);//Data length,读取操作没有数据,这里自然是0
            headerBytes.Add(0x00);
            headerBytes.AddRange(paramBytes);
            // 拼装COTP&Header
            cotpBytes.AddRange(headerBytes);
            //拼装 TPKT&COTP
            // tpkt现有长度+报文总长度2个字节+COTP长度
            int count = tpktBytes.Count + 2 + cotpBytes.Count;
            tpktBytes.Add((byte)(count / 256 % 256));
            tpktBytes.Add((byte)(count % 256));
            tpktBytes.AddRange(cotpBytes); 
            //发送
            socket.Send(tpktBytes.ToArray());
             
            //响应数据处理
            byte[] bytes = new byte[4];
            socket.Receive(bytes, 0, 4, SocketFlags.None); // TPKT
            byte[] lenBytes = new byte[2];//整个响应长度的字节数 
            lenBytes[0] = bytes[3];
            lenBytes[1] = bytes[2];
            short len = BitConverter.ToInt16(lenBytes,0);
            len -= 4;//减去 TPKT部分,就是剩下的长度

            byte[] buffer = new byte[len];
            socket.Receive(buffer, 0, len, SocketFlags.None);//接收剩下的全部数据
            // 判断是否有异常,buffer[13]是error class,buffer[14]是error code,buffer[17]是return code
            int index = 17;
            if (buffer[13] == 0x00 && buffer[14] == 0x00 && buffer[index] == 0xff)
            {
                //数据响应长度所在位置 
                lenBytes[0] = buffer[index + 3];
                lenBytes[1] = buffer[index + 2];
                //得到响应的数据长度
                ushort dataLen = BitConverter.ToUInt16(lenBytes,0); 
                byte[] dataBuffer = new byte[dataLen];
                //获取响应的数据
                Array.Copy(buffer, index + 4, dataBuffer, 0, dataLen); 
                return dataBuffer[0] == 0x01;
            }
            return false;
        }

        /// <summary>
        /// 通讯连接
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
        private static bool SetupCommunication()
        {
            //s7comm连接包括4个部分,共25个字节,即25=4+3+10+8 
            byte[] setupBytes = new byte[] {
                // 1)TPKT包括4个字节
                0x03,//版本默认3
                0x00,//保留默认0
                0x00,//整个请求字节数高位
                0x19,//整个请求字节数低位,0x19转换成10进制就是25
               //2)COTP包括3个字节
                0x02,//当前字节以后的字节数(不包括自已,0x02转换成10进制就是2),注意这个“当前字节以后的字节数”是指COTP这部分,而不是整个字节部分
                0xf0,//PDU Type,0xe0 连接请求,0xd0 连接确认,0x08 断开请求,0x0c 断开确认,0x05 拒绝访问,0x01 加急数据,0x02 加急数据确认,0x04 用户数据,0x07 TPDU错误,0xf0 数据传输
                0x80,//TPDU number,固定值
                // 3)Header包括10个字节
                0x32,//默认值,协议id
                0x01,//ROSCTR,0x01 Job request。主站发送请求,0x02 Ack。从站响应请求不带数据,0x03 Ack_Data。从站响应请求并带有数据,0x07 Userdata。原始协议的扩展。读取编程/调试、SZL读取、安全功能、时间设置等
                0x00,//Redundancy Identification (Reserved)固定值,占2个字节
                0x00,
                0x00,//Protocol Data Unit Reference固定值,占2个字节
                0x00,
                0x00,//Parameter length参数长度,占2个字节
                0x08,
                0x00,//Data length数据长度,占2个字节
                0x00,
                // 4)Parameter包括8个字节
                0xf0,//Function功能码,具体是:0x00 CPU服务,0xF0 设置通信,0x04 读取变量,0x05 写变量,0x1A 请求下载,0x1B 下载块,0x1C 下载结束,0x1D 开始上传,0x1E 上传,0x1F 结束上传,0x28 PLC 控制,0x29 PLC 停止
                0x00,//保留默认值
                0x00,//Max AmQ(parallel jobs with ack) calling,占2个字节
                0x03,
                0x00,//Max AmQ(parallel jobs with ack) called,占2个字节
                0x03,
                0x03,//PDU length,占2个字节,0x03co转换成10进制就是960
                0xc0
            };
            try
            {
                socket.Send(setupBytes);
                //响应报文的长度就是固定的27个字节
                byte[] respBytes = new byte[27];
                int count = socket.Receive(respBytes);
                // 拿到PDU长度   后续进行报文组装和接收的时候可以参考
                byte[] pdu_size = new byte[2];
                pdu_size[0] = respBytes[26];
                pdu_size[1] = respBytes[25];
                _pduSize = BitConverter.ToInt16(pdu_size,0);
                if (respBytes[17] != 0x00&& respBytes[18] != 0x00)
                {
                    Console.WriteLine("粗问题,COMM连接响应异常");
                    connectState = false;
                }
                else
                {
                    Console.WriteLine("太好了,COMM连接响应正常");
                    connectState = true;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Setup通信未建立!" + ex.Message);
                connectState = false;
            }
            return connectState;
        }

        /// <summary>
        /// cotp连接
        /// </summary>
        private static bool COTPConnection()
        {
            //COTP连接包括2个部分,共22个字节),22=4+18
            byte[] cotpBytes = new byte[] {
                //1)TPKT包括4个字节
                0x03,//版本号,版本默认3
                0x00,//默认保留为0
                0x00,//整个请求字节高位
                0x16,//整个请求字节低位(0x16转换成为10进制就是22)
                //2)COTP包括18个字节 
                0x11,//当前字节以后的字节数(不包括自已,0x11转换成10进制就是17)
                0xe0,//PDU type,0xe0 连接请求,0xd0 连接确认,0x08 断开请求,0x0c 断开确认,0x05 拒绝访问,0x01 加急数据,0x02 加急数据确认,0x04 用户数据,0x07 TPDU错误,0xf0 数据传输
                0x00,//DST reference(2个字节)
                0x00,//
                0x00,//SRC reference(2个字节)
                0x00,//
                0x00,//class(固定的) 
                0xc1, //Parameter-code  src-tsap 上位机
                0x02,  //Parameter-Len   
                0x10 ,   //Source TSAP:01->PG;02->OP;03->S7单边(服务器模式);0x10->S7双边通 
                0x00,   //机架与插槽号为0   
                0xc2,//Parameter-code  dst-tsap PLC 
                0x02,//Parameter len  
                0x03,//Destination TSAP  
                (byte)(_rack*32+_slot),//机架与插槽号: 
                0xc0,  //    Parameter code:tpdu-size 
                0x01,   //   Parameter length
                0x0a  //    TPDU size 
            };
            try
            {
                socket.Send(cotpBytes);
                //响应报文的长度是固定的22个字节
                byte[] respBytes = new byte[22];
                int count = socket.Receive(respBytes, 0, 22, SocketFlags.None);
                //第5个字节是pdu type,具体是:0xe0 连接请求,0xd0 连接确认,0x08 断开请求,0x0c 断开确认,0x05 拒绝访问,0x01 加急数据,0x02 加急数据确认,0x04 用户数据,0x07 TPDU错误,0xf0 数据传输
                if (respBytes[5] != 0xd0)
                {
                    Console.WriteLine("粗问题,COTP连接响应异常");
                    connectState = false;
                }
                else
                {
                    Console.WriteLine("太好了,COTP连接响应正常");
                    connectState = true;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("COTP连接未建立!" + ex.Message);
                connectState = false;
            }
            return connectState;
        }
        /// <summary>
        /// tcp连接 
        /// </summary>
        /// <param name="timeout"></param>
        private static void Connect(int timeout = 50)
        {
            TimeoutObject.Reset();
            try
            {
                socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                socket.BeginConnect(_ip, _port, callback =>
                {
                    connectState = false;
                    var cbSocket = callback.AsyncState as Socket;
                    if (cbSocket != null)
                    {
                        connectState = cbSocket.Connected;
                        if (cbSocket.Connected)
                            cbSocket.EndConnect(callback);
                    }
                    TimeoutObject.Set();
                }, socket);
                TimeoutObject.WaitOne(2000, false);
            }
            catch (SocketException ex)
            {
                if (ex.ErrorCode == 10060)
                    Console.WriteLine(ex.Message);
            }
            if (socket == null || !socket.Connected || ((socket.Poll(200, SelectMode.SelectRead) && (socket.Available == 0))))
            {
                Console.WriteLine("网络连接失败");
            }
            Console.WriteLine(connectState == true ? "连接成功" : "连接失败");
        }
    }
}

 这里的代码是比较难的,伙伴们仔细看下,一片不会,来二片,三片

4、运行程序 

 

 注意一个问题,什么是字节地址,什么是位地址?字节地址占2个字节,位地址占1个字节,总共地址=字节地址+位地址,占3个字节

比如DB1.DBX0.0地址中,第一个0是字节地址,第二个0是位地址

 如果读取DB1.DBX0.1,那代码的地址部分怎么改,应该能明白吧

5、小结

有人肯定会问,那地址是多变的,那代码不要改来改去吗,别急,现在先走通报文组装,熟悉报文结构,后面会利用OOP思想进行操作,现在只要走通报文组装才是核心重点,万丈高楼打好基础,后面的继续干。小伙伴们,明白了不?下节继续顶起来

原创不易,打字截图不易,走过路过,不要错过,欢迎点赞,收藏,转载,复制,抄袭,留言,动动你的金手指,早日实现财务自由

  • 31
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hqwest

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值