西门子S7通讯协议引用整理
对于S7通讯协议,我觉得首先要搞清楚为什么要研究它。是兴趣吗?当然可以,那就不要太执着,了解各大概就行;是为了突破厂家的限制,开发自己的HMI吗?那就要好好的深入研究一下了。(如果设备硬件支持用OPC UA也是不错的选择)
一、西门子S7通讯协议概述。
1、S7协议结构:
借助WireShark抓包,可以看到,S7 以太网协议基于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之间建立桥梁;
第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端口,这是西门子开放的一个通讯端口;
2、第七层 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存储区写数据;在请求数据报文中此部分不包含任何数据。
3、S7Comm以太网通讯过程(以1500PLC为例)
计算机与1500PLC进行S7Comm以太网通讯,需经过三个过程:
<1>“握手”
当PC与PLC通过Socket建立链接时,会进行“三次握手”,这是标准的TCP连接方式;这个过程会有Socket自动完成;
<2> 通讯请求
在“握手”之后,并不能马上进行数据交换,还需要"通讯请求"过程。这个过程包含两次报文交换:
1> PC 发送COTP报文给PLC;在COTP报文中包含“连接请求”和“destination TSAP” - 明确CPU的机架号和槽号;
PLC反馈COTP报文,包含“连接确认”;
这样PLC就清楚了需要和那个CPU来进行数据通讯;
2> PC 发送S7Comm报文给PLC;在S7 communicaton报文中包含“通讯请求”;
PLC反馈S7Comm报文。
<3> 交换数据
数据读写就在这个过程内完成。我们可以组织报文来实现我们需要的功能。这个过程内的报文是S7Comm格式;
具体实现时,需要对S7Comm中的第5、6、7层进行编程。
*需要注意的是,如果不进行“第2步-通讯请求”而直接发送交换数据报文,则PLC会将连接断开。
西门子PLC通讯端口固定102,但是可以连接多个PC端(客户端),端口连接的时候,要做两个初始化指令交互后,才能正常读写处理; 如果中途有错误格式的指令,可能导致端口连接断开;
二、报文
2.报文的基本格式:
2.1 第1和第2个字节是:固定报文头03 00,这里我们就用到三种报文:
a.初始化 b. 读 c.写,都是这种格式;
2.2 第3和第4个字节是:整个报文的长度;
其它部分就是各种报文的个性化处理了;
下面分析大量报文的案例进行规律分析,为了便于对照,每种都用1200 和300 两种对照demo显示:
3.初始化报文
初始化报文分两个交互;
3.1 交互一
西门子1200:
PC发出报文 ( [A18]=0x01
=CPUSlot)
03 00 00 16 11 E0 00 00
00 01 00 C1 02 01 00 C2
02 01 01 C0 01 09
PLC回复报文( B[10]=0x06 可能 是西门子的小型号 B[22]=0x01=CPUSlot)
03 00 00
16 11 D0 00 01 00 06 00 C0 01 09 C1
02
01
00 C2 02 01 01
西门子300:
PC发出报文 ( A[18]=0x02
=CPUSlot)
03 00 00 16 11 E0 00 00
00 01 00 C1 02 01 00 C2
02 02 01 C0 01 09
PLC回复报文 (B[10]=0x04 可能 是西门子的小型号 B[22]=0x0=CPUSlot)
03 00 00
16 11 D0 00 01 00 04 00 C0 01 09
C1 02
01
00 C2 02 01 02
opc 对 1200 和 300 不用配置的不同点,就一个地方:前者 CPUSlot = 1 ,后者CPUSlot = 2;
所以可以摸索规律是:
a.pc发起第一个初始化报文的时候,第18个字节标识了CPUSlot
;
b.plc回复报文和读取报文长度一样都是22个字节长度;
c.plc回复报文的最后一个字节也是CPUSlot
,这个可以用来校验;
d. plc回复的第10个字节一个是06,一个是04,这个好像是小型号的区别;
细节摸索下来:1200该字节是06,314是04,315是03;咱写程序的时候,就不要考虑这个来校验了;
3.2交互二
PC发出报文
03 00 00 19 02
F0 80 32 01 00 00 FF FF 00 08 00
00 F0 00 00 01
00 01 07 80
PLC回复报文
03 00 00 1B 02
F0 80 32 03 00 00 FF FF 00 08 00
00 00 00 F0 00
00 01 00 01 00 F0
第二个初始化报文交互,通过1200 和314,315的对比,发现居然完全没有任何区别;
所以我们可以把这个交互完全固话;
到此,整个初始化处理就算结束了,正常在设计架构的时候,可以这么实现:
在ClentSocket的onConnect(即正常连接上)的瞬间,pc给plc发起第一个初始请求,得到回复后(为了简单,就仅仅判断长度为22即可);
立刻发起第二个固定的初始话请求,得到长度为24的报文后,就通过一个布尔变量通知整个系统可以正常读写;
4.读操作
读demo1:
西门子1200: 读取DB10, count=17
,offset=19
PC发出报文
(A[3]~A[4]=0x001F=31=读取报文总长度, A[12]~A[13]=0x001C=序列号,A[24]~A[25]=0x0011=17=读取请求count;
A[26]~A[27]=0x000A=10=DB10, A[28]=0x84=读取的数据类型为DB块,A[29]~A[31]=0x000098=152=19*8=读取偏移量offset(bit为单位) )
03 00 00 1F 02 F0 80 32 01 00 00 00 1C 00 0E
00
00 04 01 12 0A 10 02 00 11 00 0A 84 00
00 98
PLC回复报文:
(B[3]~B[4]=0x002A=42=回复报文总长度, B[12]~B[13]=0x001C=序列号,B[16]~B[17]=0x0015=21=读取请求count(17)+4
B[24]~B[25]=0x0088=17*8=请求数据长度(bit为单位), B[26]~最后=数据值)
03 00 00 2A 02 F0 80 32 03 00 00 00 1C 00
02 00
15 00 00 04 01 FF 04 00 88 13 14 15 16 17 00
00
00 00 00 00 00 00 00 00 00 00
读demo2:
西门子1200: 读取DB11, count=17
,offset=19
PC发出报文:
(A[3]~A[4]=0x001F=31=读取报文总长度, A[12]~A[13]=0x008E=序列号,A[24]~A[25]=0x0011=17=读取请求count;
A[26]~A[27]=0x000B=11=DB11, A[28]=0x84=读取的数据类型为DB块,A[29]~A[31]=0x000098=152=19*8=读取偏移量offset(bit为单位) )
03 00 00 1F 02 F0 80 32 01 00 00 00 8E 00 0E
00
00 04 01 12 0A 10 02 00 11 00 0B 84 00
00 98
PLC回复报文:
(B[3]~B[4]=0x002A=42=回复报文总长度, B[12]~B[13]=0x001C=序列号,B[16]~B[17]=0x0015=21=读取请求count(17)+4
B[24]~B[25]=0x0088=17*8=请求数据长度(bit为单位), B[26]~B[42]=数据值)
03 00 00 2A 02 F0 80 32 03 00 00 00 8E 00
02 00
15 00 00 04 01 FF 04 00 88 13 14 15 16 17 18 00
00 00 00 00 00 00 00 21 22 23
读demo3:
西门子1200:读取DB11, count=16
,offset=18
PC发出报文:
(A[3]~A[4]=0x001F=31=读取报文总长度, A[12]~A[13]=0x0013=序列号,A[24]~A[25]=0x0010=16=读取请求count;
A[26]~A[27]=0x000B=11=DB11, A[28]=0x84=读取的数据类型为DB块,A[29]~A[31]=0x000090=146=18*8=读取偏移量offset(bit为单位) )
03 00 00 1F 02 F0 80 32 01 00 00 00 13 00 0E 00
00 04 01 12 0A 10 02 00 10 00 0B 84 00
00 90
PLC回复报文:
(B[3]~B[4]=0x0029=41=回复报文总长度, B[12]~B[13]=0x0013=序列号,B[16]~B[17]=0x0014=20=读取请求count(16)+4
B[24]~B[25]=0x0080=16*8=请求数据长度(bit为单位), B[26]~B[41]=数据值)
03 00 00 29 02 F0 80 32 03 00 00 00 13 00
02 00
14 00 00 04 01 FF 04 00 80 00 13 14 15 16 17 18
00 00 00 00 00 00 00 00 21
读demo4:
西门子300 (314) 读取D50, count=20 ,offset=4000
PC发出报文:
(A[3]~A[4]=0x001F=31=读取报文总长度, A[12]~A[13]=0x0028=序列号,A[24]~A[25]=0x0014=20=读取请求count;
A[26]~A[27]=0x0032=50=DB50, A[28]=0x84=读取的数据类型为DB块,A[29]~A[31]=0x007D00=32000
=4000*8=读取偏移量offset(bit为单位) )
03 00 00 1F02 F0 80 32 01 00 00 00
28 00 0E 00
00 04 01 12 0A 10 02 00 14 00
32 8400 7D 00
PLC回复报文:
(B[3]~B[4]=0x002D=45=回复报文总长度, B[12]~B[13]=0x0028=序列号,B[16]~B[17]=0x0018=24=读取请求count(20)+4
B[24]~B[25]=0x00A0=20*8=请求数据长度(bit为单位), B[26]~B[45]=数据值)
03 00 00 2D02 F0 80 32 03 00 00 00
28 00 02 00
1800 00 04 01 FF 04 00 A0 00 04 0E AB 00 00
00
00 00 00 03 00 00 00 00 00 00 00 00 00
读demo5:
西门子300 (315) 读取D10, count=100 ,offset=2
PC发出报文:
(A[3]~A[4]=0x001F=31=读取报文总长度, A[12]~A[13]=0x0003=序列号,A[24]~A[25]=0x0064=100=读取请求count;
A[26]~A[27]=0x000A=10=DB10, A[28]=0x84=读取的数据类型为DB块,A[29]~A[31]=0x000010=16=2*8=读取偏移量offset(bit为单位) )
03 00 00 1F 02 F0 80 32 01 00 00 00 03 00 0E
00
00 04 01 12 0A 10 02 00 64 00 0A 84 00 00
10
PLC回复报文:
(B[3]~B[4]=0x007D=125=回复报文总长度, B[12]~B[13]=0x0003=序列号,B[16]~B[17]=0x0068=104=读取请求count(100)+4
B[24]~B[25]=0x0320=100*8=请求数据长度(bit为单位), B[26]~B[125]=数据值)
03 00 00 7D 02 F0 80 32 03 00 00 00 03 00
02 00
68 00 00 04 01 FF 04 03 20 00 00 00 01 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00
读demo6:
西门子1200 读取X输入(input)两个byte:
PC发出报文:
(A[3]~A[4]=0x001F=31=读取报文总长度, A[12]~A[13]=0x0002=序列号,A[24]~A[25]=0x0002=2=读取请求count;
A[26]~A[27]=0x000A=10=DB10[其实这里写什么都可以,因为input不属于DB块],
A[28]=0x81=读取的数据类型为Input,A[29]~A[31]=0x000000=0=0*8=读取偏移量offset(bit为单位) )
03 00 00 1F 02 F0 80 32 01 00 00 00 02 00
0E 00
00 04 01 12 0A 10 02 00 02 00 0A 81 00
00 00
PLC回复报文:
(B[3]~B[4]=0x001B=27=回复报文总长度, B[12]~B[13]=0x0002=序列号,B[16]~B[17]=0x0068=104=读取请求count(100)+4
B[24]~B[25]=0x0320=100*8=请求数据长度(bit为单位), B[26]~B[27]=数据值)
03 00 00 1B 02 F0 80 32 03 00 00 00 02 00
02 00
06 00 00 04 01 FF 04 00 10 08 00
读demo7:
西门子1200 读取Y输出(output)两个byte:
PC发出报文:
(A[3]~A[4]=0x001F=31=读取报文总长度, A[12]~A[13]=0x0001=序列号,A[24]~A[25]=0x0002=2=读取请求count;
A[26]~A[27]=0x000A=10=DB10[其实这里写什么都可以,因为input不属于DB块],
A[28]=0x82=读取的数据类型为Output,A[29]~A[31]=0x000000=0=0*8=读取偏移量offset(bit为单位) )
03 00 00 1F 02 F0 80 32 01 00 00 00 01 00 0E 00
00 04 01 12 0A 10 02 00 02 00 0A 82 00 00 00
PLC回复报文:
(B[3]~B[4]=0x001B=27=回复报文总长度, B[12]~B[13]=0x0002=序列号,B[16]~B[17]=0x0068=104=读取请求count(100)+4
B[24]~B[25]=0x0320=100*8=请求数据长度(bit为单位), B[26]~B[27]=数据值)
03 00 00 1B 02 F0 80 32 03 00 00 00 01 00
02 00
06 00 00 04 01 FF 04 00 10 05 00
读demo8:
西门子1200 读取flag两个byte:
PC发出报文:
03 00 00 1F 02 F0 80 32 01 00 00 05 65 00 0E
00
00 04 01 12 0A 10 02 00 02 00 09 83 00 00
00
PLC回复报文:
(B[3]~B[4]=0x001B=27=回复报文总长度, B[12]~B[13]=0x0565=序列号,B[16]~B[17]=0x0006=6=读取请求count(2)+4
B[24]~B[25]=0x0010=2*8=请求数据长度(bit为单位), B[26]~B[27]=数据值)
03 00 00 1B 02 F0 80 32 03 00 00 05 65 00
02 00
06 00 00 04 01 FF 04 00 10 FF 17
根据以上8个报文的demo,摸索出大致规律如下(未必完全正确,但是应付项目可以了);
A[1]~A[2]: 03 00 固定报文头;
A[3]~A[4]: 00 1F 整个读取请求长度为0x1F= 31 ;
A[5]~A[11]: 02 F0 80 32 01 00 00 固定6个字节;
A[12]~A[13]: 两个字节,标识序列号,回复报文相同位置和这个完全一样;范围是0~65535;
A[14]~A[23]:00 0E 00 00 04 01 12 0A 10 02 固定10个字节
A[24]~A[25]:两个字节,访问数据的个数,以byte为单位;
A[26]~A[27]: DB块的编号,比如DB50, 就是0x32=50, 两个字节,范围是0~65535(也许是一个1个字节,因为没有设置估DB255以上的数据块,所以不知道到底是几个字节,姑且认为是2个字节);
A[28] : 访问数据块的类型:0x81-input ,0x82-output ,0x83-flag , 0x84-DB(这个最常见);
A[29]~A[31]: 访问DB块的偏移量offset (地址+1以byte为单位); 3个字节,范围是0~16777216(一般 用不到这么大)
程序设计的时候,其实主要关注最后4个信息,即:
1. A[24]~A[25]: 访问byte个数
2. A[26]~A[27]: DB块编号
3. A[28] : 数据块类型
4.A[29]~A[31] :访问地址偏移量;相当于首地址编号
B[1]~B[2]: 03 00 固定报文头
B[3]~B[4]: 整个读取回复报文长度:25+读取长度;
B[5]~B[11]: 02 F0 80 32 03 00 00 固定6个字节,和读取请求相同的位置几乎一样,就 B[9]=0x03 ;A[9]=0x01;
B[12]~B[13]: 两个字节,标识序列号,回复报文相同位置和这个完全一样;范围是0~65535;
B[14]~B[15]: 两个字节,固定为00 02;对应读取位置是 00 0E;正好 02+0E=10 ;有点补码的感觉,其实不需要关注规律,反正是固定的;
B[16]~B[17]:两个字节,=请求读取的字节数+4;
B[18]~B[23]:6个字节,固定为:00 00 04 01 FF 04 ;
B[24]~B[25]:两个字节, 请求访问的byte个数*8 ;其实就是以二进制为单位的个数;由此可以看出,一口气最多访问的地址个数是8192;
B[26]~ 最后一个 :以offset作为首地址,所对应的各个byte的值;
程序设计的时候,其实只要关注两个信息:
1.校验B[3]~B[4]:校验长度正确;
2.B[26]~最后一个 :获取对应的值;
到这里读的处理就算结束了;几个小注意点:
1. 对于不同信号的PLC,除了初始化的CPUSolt不同;正常读/写指令是一样的;
2.读的时候,都是以byte为单位的,如果程序只需要bit,那么还是以Byte为单位去读,将读出的部分按bit再去分解;
3.flag类型到底是什么,不是很清楚,有点类似三菱里的M点;这个也不需要去深究,一般项目里主要就是用DB块;
4.读取的长度如果是N(以byte为单位),那么返回的长度就是N*8(以bit为单位);怎么判断长度是否要*8;主要看后面是不是紧挨着数据,
如果是数据,就需要*8;offset都是以bit为单位的;
5.正常读的操作都是DB块,所以在A[26]~A[27]这个字节写入DB块的编号,但是对于input,output,flags这三个类型,是不需要数据块编号的,
不过我们可以随便写一个DB编号;
4.写操作
写demo1:
西门子1200 写 db10.WORD18=0xFFFE=65534;
也就是: DB10.b18=0xFF; DB10.B19=0xFE;
PC发出报文:
(A[3]~A[4]=0x0025=37=读取报文总长度, A[12]~A[13]=0x0005=序列号,A[16]~A[17]=0x06=写入byte个数(2)+4 , A[23]=0x02=写入方式为byte,
A[24]~A[25]=0x0002=2=写入个数count;
A[26]~A[27]=0x000A=10=DB10,A[28]=0x84=写入的数据类型为DB块,A[29]~A[31]=0x000090=144=18*8=读取偏移量offset(bit为单位),
A[32]~A[33]=0x0004=写入方式为Byte
, A[34]~A[35]=0x0010=2*8=写入byte的个数(bit为单位) ,A[36]~A[37]= 写入数据)
03 00 00 25 02 F0 80 32 01 00
00 00 05 00 0E 00
06 05 01 12 0A 10 02 00
02 00 0A 84 00 00 90 00
04 00 10 FF FE
PLC回复报文:
( B[12]~B[13]=0x0565=序列号,最后一个B[14]=0xFF表示写入)
03 00 00 16 02 F0 80 32 03 00 00 00 05 00 02 00
01 00 00 05 01 FF
写demo2:
1200 写入 DB10. X2.6=1 (这里是按bit写入)
PC发出报文:
(A[3]~A[4]=0x0024=36=读取报文总长度, A[12]~A[13]=0x0008=序列号,A[16]~A[17]=0x05=写入bit个数(1)+4 A[26]~A[27]=0x000A=10=DB10,A[28]=0x84=写入的数据类型为DB块,A[29]~A[31]=0x000016=22=2*8+6=读取偏移量offset( bit为单位)
A[32]~A[33]=0x0003=写入方式为bit
, A[34]~A[35]=0x0001=写入bit的个数(bit为单位) ,A[36]= 写入数据[0或1])
03 00 00 24 02 F0 80 32 01 00 00 00 08 00
0E 00
05 05 01 12 0A 10 01 00 01 00
0A 84 00 00 16 00
03 00 01 01
PLC回复报文:
( B[12]~B[13]=0x0565=序列号,最后一个B[14]=0xFF表示写入)
03 00 00 16 02 F0 80 32 03 00 00 00 08 00 02 00
01 00 00 05 01 FF
写demo3:
1200 写 入:output0=4
PC发出报文:
(A[3]~A[4]=0x0024=36=读取报文总长度, A[12]~A[13]=0x0008=序列号,A[16]~A[17]=0x05=写入byte个数(1)+4 ,A[23]=0x02=写入方式为byte,A[24]~A[25]=0x0001=1=写入个数count;
A[26]~A[27]=0x0001=DB1(因为是output,所以DB块编号无所谓),A[28]=0x82=写入的数据类型为output,A[29]~A[31]=0x000000=读取偏移量offset( bit为单位)
A[32]~A[33]=0x0004=写入方式为byte
, A[34]~A[35]=0x0008=1*8=写入byte的个数 ,A[36]= 写入数据)
03 0000 24 02 F0 80 32 01 00 00 00 08 00 0E 00
05 05 01 12 0A 10 02 00 01 00 01 82 00 00 00 00
04 00 08 04
PLC回复报文:
( B[12]~B[13]=0x0565=序列号,最后一个B[14]=0xFF表示写入)
03 00 00 16 02 F0 80 32 03 00 00 00 08 00 02 00
01 00 00 05 01 FF
写demo4:
1200 写 输入:output 0.3=1
PC发出报文:
(A[3]~A[4]=0x0024=36=写入报文总长度, A[12]~A[13]=0x0003=序列号,A[16]~A[17]=0x05=写入bit个数(1)+4A[23]=0x01=写入方式为bit,A[24]~A[25]=0x0001=1=写入个数count;
A[26]~A[27]=0x000A=10=DB10(因为是output,所以DB块编号无所谓),A[28]=0x82=写入的数据类型为output,A[29]~A[31]=0x000003=读取偏移量offset( bit为单位)
A[32]~A[33]=0x0003=写入方式为bit
, A[34]~A[35]=0x0001=写入bit的个数(bit为单位) ,A[36]= 写入数据[0或1])
03 00 00 24 02 F0 80 32 01 00 00 00 03 00 0E
00
05 05 01 12 0A 10 01 00 01 00 01 82 00 00 03 00
03 00 01 01
PLC回复报文:
( B[12]~B[13]=0x0565=序列号,最后一个B[14]=0xFF表示写入)
03 00 00 16 02 F0 80 32 03 00 00 00 03 00 02 00
01 00 00 05 01 FF
根据以上4个报文的demo,摸索出大致规律如下(未必完全正确,但是应付项目可以了);
A[1]~A[2]: 03 00 固定报文头;
A[3]~A[4]: 整个报文长度:35+写入长度;
A[5]~A[11]: 02 F0 80 32 01 00 00 固定6个字节(和读取的完全一样)
A[12]~A[13]: 两个字节,标识序列号,回复报文相同位置和这个完全一样;范围是0~65535;
A[14]~A[15]:00 0E 固定2个字节;
A[16]~A[17]:写入长度+4;
A[18]~A[22]: 05 01 12 0A 10 固定5个自己
A[23] : 写入方式: 01-按bit写入; 02-按byte写入;
A[24]~A[25]:两个字节,写入数据的个数(可能是byte或bit, 按A[23]来区分)
A[26]~A[27]: DB块的编号
A[28] : 写入数据块的类型:0x81-input ,0x82-output ,0x83-flag , 0x84-DB(这个最常见);
A[29]~A[31]: 写入DB块的偏移量offset (地址+1以byte为单位); 3个字节,范围是0~16777216(一般 用不到这么大)A[32]~A[33]:写入方式为: 03-按bit写入; 04-按byte写入;
A[34]~A[35]:写入bit的个数(bit为单位)
A[36]~最后 : 连续的写入值;
B[1]~B[2]: 03 00 固定报文头;
B[14]: FF 标识写入正常;
到这里,初始化,读,写 这3种方式都摸索完了,未必都正确,应付开发应该绰绰余裕了;
在接下来的时间里,就需要把这些规律变成相应的程序;
注意点:
1.写入可以按byte和bit两种方法去操作;
2.对于byte,可以一口气写连续多个byte, 理论上一条指令连续写bit也可以,但是实践下来,发现有问题,所以对于bit操作,我们就一个一个写吧;
上述内容来自网络,以下是个人的实践心得
一、关于报文
1、上文中介绍了两种S7的报文,其中A格式的报文更适用于S7-300/400及其以下版本的PLC(当然也可以用于S7-1200/1500),B格式适用于S7-1200/1500
2、两种格式的报文最大的差别在于通讯效率,A格式报文的最大长度是256字节,除去报头一次能通讯的最大数据是220个字节,B格式报文理论上一次能通讯
8192个字节(前提是CPU型号支持)
二、应用
下面是一个C#的S7报文生成实例
这里讲报文定义成了一个长度可变的byet[]数组,通过修改数组各个字节的内容就可以组成S7报文,之后再通过通讯接口发送的PLC,并读取返回的数据就可以
实现上位机于PLC的数据交换了
_PLCCommand[0] = 0x03; // 报文头 -> Head
_PLCCommand[1] = 0x00;
_PLCCommand[2] = (byte)(_PLCCommand.Length / 256);
_PLCCommand[3] = (byte)(_PLCCommand.Length % 256);// 报文总长度 =[2]*256+[3]
_PLCCommand[4] = 0x02; // 固定 -> Fixed
_PLCCommand[5] = 0xF0;
_PLCCommand[6] = 0x80;
_PLCCommand[7] = 0x32; // 协议标识
_PLCCommand[8] = 0x01; // 命令:状态01=发
_PLCCommand[9] = 0x00; //冗余标识 (保留的): 0x0000;
_PLCCommand[10] = 0x00; // 协议数据单元引用;请求事件增加;
_PLCCommand[11] = 0x00;
_PLCCommand[12] = 0x01; // 参数命令数据总数:通常为1,也就是一次只读取一个数据区(M、I、Q、DB),每个参数命令的长度是12个字节,开始于第19个字节
也就是说要一次读取多个区域[12]的值大于1,PLC会从报文的第19字节开始,按这里指定的数量每12个字节一组去识别参数
_PLCCommand[13] = (byte)((_PLCCommand.Length - 17) / 256);
_PLCCommand[14] = (byte)((_PLCCommand.Length - 17) % 256);// 数据长度 =报文总长度-17(报头)
_PLCCommand[15] = 0x00; // 读取内部数据时为00,读取CPU型号为Data数据长度
_PLCCommand[16] = 0x00;
// =====================================================================================以上是报文头
_PLCCommand[17] = 0x04; // 读写指令,04读,05写
_PLCCommand[18] = (byte)readCount; // 读取数据块个数
for (int ii = 0; ii < readCount; ii++)
{
//===========================================================================================以下是12个字节的参数命令
// 指定有效值类型 -> Specify a valid value type
_PLCCommand[19 + ii * 12] = 0x12;
// 接下来本次地址访问长度 -> The next time the address access length
_PLCCommand[20 + ii * 12] = 0x0A;
// 语法标记,ANY -> Syntax tag, any
_PLCCommand[21 + ii * 12] = 0x10;
// 按字为单位 -> by word
_PLCCommand[22 + ii * 12] = 0x02; // (byte)(address[ii].Content1 == 0x1D ? 0x1D : address[ii].Content1 == 0x1C ? 0x1C : 0x02);
// 访问数据的个数 -> Number of Access data
_PLCCommand[23 + ii * 12] = (byte)(length[ii] / 256);
_PLCCommand[24 + ii * 12] = (byte)(length[ii] % 256);
// DB块编号,如果访问的是DB块的话 -> DB block number, if you are accessing a DB block
_PLCCommand[25 + ii * 12] = (byte)(address[ii].Content3 / 256);
_PLCCommand[26 + ii * 12] = (byte)(address[ii].Content3 % 256);
// 访问数据类型 -> Accessing data types
_PLCCommand[27 + ii * 12] = address[ii].Content1;
// 偏移位置 -> Offset position
_PLCCommand[28 + ii * 12] = (byte)(address[ii].Content2 / 256 / 256 % 256);
_PLCCommand[29 + ii * 12] = (byte)(address[ii].Content2 / 256 % 256);
_PLCCommand[30 + ii * 12] = (byte)(address[ii].Content2 % 256);
}