1. 从一次“黑盒”调试说起:为什么我们需要理解S7协议报文
几年前,我接手一个老旧产线的数据采集项目。现场有一台西门子S7-300 PLC,负责控制几个关键阀门的开度。客户的需求很简单:把阀门的实时开度值(一个浮点数)采集上来,显示在新建的上位机监控系统里。听起来是个标准的OPC DA或者Modbus TCP就能搞定的小活儿。但到了现场才发现,这台PLC除了一个以太网口,没有任何开放的通信接口,原有的上位机是一台早已停产的工控机,里面的组态软件连厂家都找不到了。换句话说,PLC对我来说就是个“黑盒”——我知道里面有数据,但不知道用什么“语言”去问它要。
当时第一个念头是找找有没有现成的驱动库。试了几个号称支持S7协议的商业库和开源组件,要么连接不稳定,时不时断线,要么读取的数据全是乱码。在调试窗口里,我看到软件发送和接收着一串串十六进制的数据,但对面的PLC就像个沉默的巨人,偶尔回应一下,也完全看不懂它在“说”什么。那一刻我意识到,如果不想被各种封装好的、但可能并不可靠的“黑盒”驱动牵着鼻子走,就必须自己搞清楚西门子S7协议这套“语言”的语法。这不是为了炫技,而是在复杂的工业现场,当标准方案失效时,能亲手“掰开”数据流,定位问题根源的唯一途径。理解S7协议报文,就是拿到了与西门子PLC直接对话的钥匙,从被动的“使用者”变为主动的“沟通者”。
今天,我就把自己在多个项目中摸索、验证过的S7协议报文解析经验,进行一次彻底的梳理。这不是一份官方的协议手册(那东西既庞杂又枯燥),而是一份从实战出发的“解码指南”。我们会从最基础的连接建立开始,一步步拆解该协议的核心报文结构,重点聚焦在最常用的数据读写功能上,并用实际的代码片段展示如何构造和解析。无论你是正在开发自己的数据采集中间件,还是在调试第三方软件时遇到了令人困惑的通信问题,这篇文章都能帮你拨开迷雾,直击核心。
2. 协议基石:TPKT与ISO-COTP——通信的“信封”与“快递单”
在深入S7协议的核心内容之前,我们必须先理解它赖以传输的两层“包装”。你可以把整个通信过程想象成寄信:S7协议本身的读写指令是信纸上的具体内容(比如“请告诉我DB10.DBD20的值”),但这张信纸需要先装入一个标准信封(TPKT),然后在信封上贴上包含详细地址和流程信息的快递单(ISO-COTP),才能被网络投递。很多初学者直接去啃S7的“信纸内容”,却忽略了“信封”和“快递单”的格式,导致连最基本的连接都建立不起来。
2.1 TPKT:简单直接的传输包装
TPKT(ISO Transport Service on top of the TCP)是一个非常轻量级的头部,定义在RFC 1006中,它用于在TCP流中标识一个独立协议数据单元(PDU)的边界。其结构固定为4字节:
| 字节偏移 | 字段名 | 值(示例) | 说明 |
|---|---|---|---|
| 0 | 版本 | 0x03 | 固定为3,代表TPKT版本号。 |
| 1 | 保留 | 0x00 | 保留字段,始终为0。 |
| 2-3 | 长度 | 0x00, 0x1F | 大端字节序。表示整个TPKT数据包(包含这4字节头部)的总长度。例如0x001F表示后续长度为31字节。 |
关键点:这里的“长度”是整个包的长度。计算时,你需要将S7 PDU(或COTP PDU)的长度加上4(TPKT头自身的长度)。在解析时,先读取这4个字节,根据“长度”字段值,就能从TCP流中准确截取出一个完整的数据包,有效解决TCP的粘包问题。
2.2 ISO-COTP:面向连接的传输服务
COTP(Connection-Oriented Transport Protocol)位于TPKT之内,它负责管理连接的建立、释放以及数据的分片(虽然S7通信中通常不分片)。对于S7协议,我们主要关注两种类型的COTP PDU:连接请求(CR)和连接确认(CC),以及用于数据传输的数据包(DT)。
COTP连接请求(CR)报文结构分析:
这是客户端主动发起连接时发送的第一个有效载荷。一个典型的CR报文如下(包含在TPKT中):
TPKT Header: 03 00 00 16 COTP CR: 11 // 长度和PDU类型 e0 // 目标引用(高) 00 // 目标引用(低) 00 00 // 源引用 00 // 协议类(0=无显式流控) c1 // 参数:目标TSAP(高) 02 // 参数:目标TSAP(低) c2 // 参数:源TSAP(高) 02 // 参数:源TSAP(低)- 长度(1字节):0x11,表示COTP头部后续的字节数(不包括长度字段本身),这里是17字节。
- PDU类型(1字节):0xE0,代表连接请求(Connection Request)。
- 目标/源引用(各2字节):用于标识连接,可以自行设定,通常不重要。
- 参数:这是关键!
c1 02和c2 02分别定义了目标TSAP和源TSAP。- TSAP(Transport Service Access Point):可以理解为TCP端口之上的另一个逻辑端口号,用于在同一个物理连接上复用多个逻辑连接。西门子PLC使用TSAP来区分不同的通信服务(如PG/PC编程、S7基本通信、S7连接等)。
- 计算规则:对于S7-300/400/1200/1500,常见的TSAP由两个字节组成。第一个字节通常是
0x10 + 机架号,第二个字节是0x00 + 槽号。但更常见的简化设置是:目标TSAP(PLC端)固定为0x01, 0x00(对应机架0,槽2,但常被视为默认设置),源TSAP(客户端)则可以是0x01, 0x00或任意不冲突的值,如0x01, 0x01。上面示例中的c1 02和c2 02是另一种编码形式,c1表示目标TSAP标识,02是长度,后面跟两个字节的实际TSAP值。在实际构造时,我们通常直接使用01 00和01 01这样的简单形式。
COTP连接确认(CC)报文:PLC在收到CR后,会回复一个CC报文,其PDU类型为0xD0。结构类似CR,包含了PLC确认的参数。收到CC,意味着COTP连接层已经建立成功。
COTP数据(DT)报文:在连接建立后,所有S7协议PDU都包裹在COTP DT报文中发送。DT报文头极其简单:0x02(表示DT PDU) +0xF0(一个固定值,指示这是最后一个数据单元,无分片) +TPDU编号(1字节,用于流控,通常忽略)。所以,你看到的实际S7报文,前面都会有02 f0 80这三个字节的COTP DT头。
实操心得:90%的S7连接失败问题,都出在TPKT长度计算错误或TSAP设置不对上。务必使用网络抓包工具(如Wireshark,它内置了S7协议解析器)对比你的报文和成功通信的报文。一个快速验证TSAP的方法是:用西门子官方软件(如Step 7或TIA Portal)成功连接一次PLC,然后用Wireshark抓包,直接查看CR报文中的“Destination TSAP”和“Source TSAP”字段,照抄即可。
3. 核心对话:S7协议PDU的请求与响应结构
当COTP连接建立后,我们终于可以开始“对话”了。S7协议PDU是对话的具体内容。它遵循一个非常固定的“请求-响应”模式。无论是读、写、还是其他功能,请求和响应的报文结构都有清晰的层次。
3.1 通用PDU头部:协议、功能和冗余检查
每一个S7 PDU都以一个10字节(对于S7-300/400)或12字节(对于S7-1200/1500,多一个保留字)的头部开始。我们以经典的S7-300/400的10字节头部为例:
| 字段 | 字节数 | 典型值 | 说明 |
|---|---|---|---|
| 协议ID | 1 | 0x32 | 固定值,标识这是S7协议。 |
| PDU类型 | 1 | 0x01 (请求) / 0x03 (响应) | 0x01: Job (客户端请求),0x02: Ack,0x03: Ack-Data (服务器响应),0x07: Userdata。我们主要和0x01、0x03打交道。 |
| 冗余标识 | 2 | 0x0000 | 用于冗余系统,单机通常为0。 |
| 协议数据单元参考 | 2 | 自增序列号 | 客户端生成,用于匹配请求和响应。比如你发送的请求是0x0001,PLC返回的响应也应该是0x0001。这是排查“应答不对”问题的重要依据。 |
| 参数长度 | 2 | 大端 | 后续参数部分的字节数。 |
| 数据长度 | 2 | 大端 | 后续数据部分的字节数。对于读请求,数据长度为0。 |
参数部分紧随头部之后,其结构根据PDU类型(请求/响应)和功能码不同而变化。功能码是参数部分的第一个字节。
数据部分则存放着实际要读取或写入的值。
3.2 读请求的构造:你想要什么?
一个读请求(PDU类型为0x01)的核心在于其参数部分,它明确告诉PLC:“我要读哪个存储区、从哪个地址开始、读多少数据。”
读请求的参数部分固定格式如下:
- 功能码:
0x04代表读变量。 - 项目数量:
0x01表示本次请求只读取一个连续的数据块(一次请求可以读多个不连续区域,但常见的是单个)。 - 变量规格:
- 地址格式:
0x12。这是一个固定值,表示后续的地址信息遵循“S7-Any”指针格式。 - 读取长度:
0x0a0x00。2字节,大端。表示请求读取10个字节。注意:S7协议一次读取的最大长度受PLC型号和连接资源限制,通常为200字节左右。超限需要分多次读。 - DB号:
0x000x01。2字节,大端。如果非DB区,此处为0。这里表示DB1。 - 存储区与地址:
0x840x000x000x00。0x84:这是一个复合字节。高4位0x8代表存储区类型:0x1=I(输入),0x2=Q(输出),0x3=M(位存储器),0x4=DB(数据块),0x5=DI(背景数据块)。这里0x8是0x4的另一种表示(有时是0x84,有时是0x30|0x04,取决于协议细节,但Wireshark能帮你确认)。低4位0x4表示后续地址的字节数(这里是4字节)。0x000x000x00:3字节的偏移地址,按位寻址。需要乘以8转换为字节内的位偏移。这里全是0,表示从DB1.DBX0.0开始。
- 地址格式:
一个完整的读DB1.DBB0开始10个字节的请求报文示例(已包含TPKT+COTP DT头):
// TPKT Header 03 00 00 1f // 总长度31字节 // COTP DT Header 02 f0 80 // S7 PDU Header 32 01 00 00 00 01 00 0e 00 00 // 协议ID, Job, 序列号0x0001, 参数长14,数据长0 // S7 Parameter (读) 04 // 功能码:读 01 // 项目数:1 12 // 地址格式:S7-Any 0a 00 // 读取长度:10字节 00 01 // DB号:1 84 // 存储区:DB,地址长度4字节 00 00 00 // 偏移地址:0字节 * 8 = 位偏移0关键解析:
84 00 00 00如何对应到DB1.DBB0?84表示DB区,且地址信息占4字节。后3字节00 00 00是位偏移0。因为我们要读的是字节(BB),所以从位偏移0开始的连续8位(一个字节)就是DBB0。如果要读DB1.DBD4(一个双字,4字节),偏移地址需要是4 * 8 = 32,转换为3字节就是00 00 20(因为32的十六进制是0x20)。
3.3 读响应的解析:PLC给了你什么?
PLC对读请求的响应,PDU类型为0x03(Ack-Data)。其结构也包含头部、参数和数据部分。
响应参数部分很简单:
- 功能码:
0x04,与请求对应。 - 项目数量:
0x01。 - 返回码:
0xff。这是最关键的一个字节!0xff表示成功。任何其他值都代表错误,例如0x05表示地址错误,0x0a表示对象不存在等。解析响应时,必须首先检查这个返回码。
响应数据部分包含了实际读取到的值。其结构为:
- 数据类型:
0x04表示读取的是字节/字/块。 - 返回长度:2字节,大端。表示后续实际数据字节数。
- 实际数据:连续的数据字节。
接上例,一个成功的读响应报文可能如下:
// TPKT Header 03 00 00 25 // 总长度37字节 // COTP DT Header 02 f0 80 // S7 PDU Header 32 03 00 00 00 01 00 02 00 08 // 协议ID, Ack-Data, 序列号0x0001, 参数长2,数据长8 // S7 Parameter (读响应) 04 // 功能码:读 01 // 项目数:1 ff // 返回码:成功 // S7 Data 04 // 数据类型:字节/字/块 00 0a // 返回长度:10字节 01 02 03 04 05 06 07 08 09 0a // 实际数据:DB1.DBB0~DBB9的值解析时,我们首先确认返回码是0xff,然后从数据部分提取出00 0a后面的10个字节01 02 ... 0a,这就是DB1.DBB0到DBB9的值。
避坑指南:响应数据部分的“返回长度”字段,表示的是“实际数据字节数”。它不一定等于请求的长度。比如,如果你请求读10个字节,但DB块实际只有5个字节,PLC可能会返回错误,也可能只返回5个字节(返回长度=5)。你的解析程序必须能处理这种情况,不能僵化地按照请求长度去截取数据。
4. 写操作与复杂数据类型处理
理解了读操作,写操作就顺理成章了,它更像是读操作的“逆过程”。但这里涉及到如何将高级语言中的数据类型(如Int、Real、String)转换为S7协议能识别的字节序列,这是实际应用中的另一个核心难点。
4.1 写请求的构造:告诉PLC改什么
写请求(PDU类型仍为0x01)的参数部分与读请求高度相似,但功能码是0x05。最大的不同在于,它必须携带“数据部分”,这部分明确指出了要写入的数据类型、长度和具体的值。
一个写DB1.DBW2(字)值为0x1234的请求报文结构如下:
// ... TPKT, COTP, S7 Header 类似,注意数据长度不为0 ... // S7 Parameter (写) 05 // 功能码:写 01 // 项目数:1 12 // 地址格式:S7-Any 02 00 // 写入长度:2字节 (一个字) 00 01 // DB号:1 84 // 存储区:DB,地址长度4字节 00 10 00 // 偏移地址:2字节 * 8 = 16位偏移 = 0x0010 // S7 Data 04 // 数据类型:字节/字/块 00 02 // 写入数据长度:2字节 12 34 // 要写入的数据:0x1234地址计算:DBW2表示从第2个字节开始的一个字(2字节)。字节偏移为2,位偏移为2 * 8 = 16。16的十六进制是0x10,用3字节表示为00 10 00(大端表示,实际有效位是中间的0x10)。
4.2 写响应的解析:确认是否成功
写响应的结构与读响应类似,但更简单。参数部分包含功能码0x05和返回码0xff(成功)。数据部分通常很短,只包含一个简单的确认信息。
4.3 复杂数据类型的字节序与编码转换
这是协议解析中最容易出错的地方。西门子PLC使用的字节序(Byte Order)与我们的PC(x86架构)通常不同。
字节序(Endianness):
- PC(x86, ARM等):通常采用小端序(Little-Endian),即低位字节在前。例如,整数
0x1234在内存中存储为34 12。 - 西门子S7-300/400/1200/1500:采用大端序(Big-Endian),即高位字节在前。
0x1234存储为12 34。 - 影响:所有多字节数据类型(Word, Int, DWord, DInt, Real, Time等)在组包(写入)和解包(读取)时,都必须进行字节序转换。
- PC(x86, ARM等):通常采用小端序(Little-Endian),即低位字节在前。例如,整数
常见数据类型编码示例:
- Int (16位有符号整数):值
-100。- 内存表示(补码):
0xFF9C。 - S7报文中的字节序列:
FF 9C(大端)。你的程序发送或接收后,需要转换为9C FF才能被小端系统正确解释为-100。
- 内存表示(补码):
- Real (32位浮点数,IEEE 754):值
123.456。- 内存表示(小端):
0x79 E9 F6 42。 - S7报文中的字节序列:
42 F6 E9 79(大端)。你需要将收到的42 F6 E9 79重新排序为79 E9 F6 42才能得到正确的浮点数。
- 内存表示(小端):
- String (S7格式字符串):西门子有特定的字符串格式。第一个字节是最大长度,第二个字节是当前长度,后面才是字符数据(ASCII)。例如,定义
String[10]的变量,存储"Hello"。- 在DB块中可能表示为:
0A 05 48 65 6C 6C 6F 00 00 00 00(最大长度10,当前长度5,内容“Hello”,剩余用0填充)。 - 解析时,你需要根据这个特定格式来提取有效字符。
- 在DB块中可能表示为:
- Int (16位有符号整数):值
实战技巧:在代码中,不要手动拼接这些字节。为每种数据类型(Int, DInt, Real, Word等)编写专用的
ToBytes()和FromBytes()函数,内部处理好字节序转换。对于字符串,更要小心处理长度字节和填充字节。一个健壮的库应该封装好这些细节。
5. 实战演练:用Python构造与解析一个完整的读操作
理论说得再多,不如一行代码。下面我们用Python(使用socket库)演示如何手动构造一个读取DB1.DBD4(双字,4字节)的请求,并解析响应。这里假设PLC的IP是192.168.0.1,TSAP设置如前所述。
import socket import struct def build_read_request(sequence, db_number, byte_offset, read_length): """构造一个读DB块的请求报文""" # 1. TPKT Header tpkt_ver = 0x03 tpkt_reserved = 0x00 # 总长度稍后计算 # 2. COTP DT Header cotp_dt = bytes([0x02, 0xf0, 0x80]) # 3. S7 PDU Header (Job) protocol_id = 0x32 pdu_type = 0x01 # Job redundant_ident = 0x0000 pdu_ref = sequence & 0xFFFF param_length = 0x000E # 固定14字节 data_length = 0x0000 # 读请求无数据 s7_header = struct.pack('>BBHHHH', protocol_id, pdu_type, redundant_ident, pdu_ref, param_length, data_length) # 4. S7 Parameter (Read) func_code = 0x04 item_count = 0x01 addr_format = 0x12 # 计算位偏移 bit_offset = byte_offset * 8 # 注意:西门子地址是3字节,大端,但位偏移要放在中间字节?实际是打包成3字节。 # 更常见的做法是:将位偏移转换为一个4字节整数,取低3字节,按大端排列。 # 例如:byte_offset=4, bit_offset=32 (0x20) addr_bytes = struct.pack('>I', bit_offset)[1:] # 取后3字节,即 00 00 20 s7_param = struct.pack('>BBHHBB', func_code, item_count, read_length, db_number, 0x84, addr_bytes[0]) s7_param += addr_bytes[1:] # 加上后两个地址字节 # 5. 组装完整S7 PDU s7_pdu = s7_header + s7_param # 6. 组装COTP PDU cotp_pdu = cotp_dt + s7_pdu # 7. 计算总长度并组装TPKT total_length = len(cotp_pdu) + 4 tpkt_header = struct.pack('>BBH', tpkt_ver, tpkt_reserved, total_length) # 完整报文 full_packet = tpkt_header + cotp_pdu return full_packet def parse_read_response(packet, sequence): """解析读响应报文,返回数据字节或错误信息""" # 跳过TPKT头 (4字节) 和 COTP DT头 (3字节) s7_pdu = packet[7:] # 解析S7 Header proto_id, pdu_type, red_id, pdu_ref, param_len, data_len = struct.unpack_from('>BBHHHH', s7_pdu, 0) if pdu_ref != sequence: return None, f"序列号不匹配: 期望{sequence}, 收到{pdu_ref}" if pdu_type != 0x03: # 不是 Ack-Data return None, f"非期望的PDU类型: {pdu_type}" # 跳转到参数部分 param_offset = 10 func_code, item_count, return_code = struct.unpack_from('>BBB', s7_pdu, param_offset) if func_code != 0x04: return None, f"功能码错误: {func_code}" if return_code != 0xff: return None, f"PLC返回错误码: 0x{return_code:02x}" # 跳转到数据部分 data_offset = param_offset + param_len data_type, ret_len = struct.unpack_from('>BH', s7_pdu, data_offset) if data_type != 0x04: return None, f"数据类型错误: {data_type}" # 提取实际数据 actual_data = s7_pdu[data_offset + 3: data_offset + 3 + ret_len] return actual_data, None # 主程序 def main(): plc_ip = '192.168.0.1' plc_port = 102 # S7协议默认端口 sequence = 1 db_number = 1 byte_offset = 4 # 读取 DBD4 read_length = 4 # 读取4个字节 (一个双字) # 构造请求 request = build_read_request(sequence, db_number, byte_offset, read_length) # 建立TCP连接 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5.0) # 设置超时 try: sock.connect((plc_ip, plc_port)) # 发送连接请求 (COTP CR),这里为简化,假设已连接。实际需要先发CR。 # 发送读请求 sock.send(request) # 接收响应 response = sock.recv(1024) # 解析响应 data, error = parse_read_response(response, sequence) if error: print(f"读取失败: {error}") else: print(f"读取成功,原始数据: {data.hex()}") # 假设我们知道读回来的是一个DInt(32位有符号整数),大端序 # 需要转换为小端序再解析 value_le = int.from_bytes(data, byteorder='big', signed=True) # 注意:from_bytes with 'big' 直接解析大端序 # 但通常我们的系统是小端,所以如果直接按大端解析得到错误值,需要转换 # 更清晰的做法: data_be = data # 报文中的是大端数据 # 方法:将大端字节序转换为整数 value = struct.unpack('>i', data_be)[0] # '>i' 表示大端有符号32位整数 print(f"解析后的DInt值: {value}") except socket.timeout: print("连接或接收超时") except Exception as e: print(f"通信错误: {e}") finally: sock.close() if __name__ == '__main__': main()这段代码是一个高度简化的示例,它省略了COTP连接建立的过程和完整的错误处理。但它清晰地展示了从组包、发送到解包的核心流程。在实际项目中,你需要处理连接协商、序列号管理、超时重试、复杂地址计算等更多细节。
6. 高级话题与排错实战
掌握了基本读写,你已经能解决80%的问题。剩下的20%往往出现在更复杂的场景和诡异的故障中。
6.1 多项目读写与部分写入
S7协议支持在一个PDU内读写多个不连续的区域。参数部分中的“项目数量”字段可以大于1,后面跟随多个“变量规格”项。这在需要一次性读取散布在各处的多个变量时非常高效,能减少通信往返次数。数据部分的组织也会相应变化,每个读取项都有独立的返回头和返回码。实现此功能需要对协议有更精确的把握,建议在单项目稳定后再尝试。
部分写入(Partial Write)用于写入位(Bit)变量。其地址计算需要精确到位,并且数据部分需要指明写入的是位值(0或1)。例如,写入DB1.DBX0.5为1,地址偏移是(0*8)+5=5,数据部分会包含特定的位操作编码。
6.2 使用Wireshark进行深度协议调试
当你的代码不工作时,Wireshark是你的最佳搭档。按以下步骤操作:
- 过滤:在Wireshark中使用过滤器
tcp.port == 102只看S7通信流量。 - 对比:用你的程序发起一次通信,同时用西门子官方软件(如Step 7的“监控与强制表”或TIA Portal的“在线访问”)对同一个地址进行一次成功的读写操作。
- 分析:在Wireshark中对比两个通信流。
- 连接阶段:看CR报文的TSAP是否一致。
- 请求阶段:对比S7 PDU头部中的“协议数据单元参考”(序列号)是否正常递增?参数部分的地址编码(特别是DB号和偏移)是否完全一致?数据部分的字节序是否正确?
- 响应阶段:PLC是否返回了响应?返回码是
0xff吗?数据长度是否符合预期?
- 解码:Wireshark的S7协议解析器能帮你把十六进制报文翻译成可读的格式(如“Read Var, DB1, 4 bytes at offset 0”),极大提升调试效率。如果Wireshark能正确解析官方软件的报文,但不能解析你的报文,那问题一定出在你的报文构造上。
6.3 常见错误码与排查思路
- 0x05 - 地址错误:你请求的地址在PLC中不存在。检查DB号、字节偏移是否超出块长度,存储区标识符是否正确(例如,对于M区,存储区标识符是
0x83)。 - 0x0a - 对象不存在:通常是DB号错误,或者该DB块未被下载到PLC中。
- 0xd2 - 资源不可用:PLC的连接资源已满。S7-300/400的每个CPU有有限的通信连接数。需要检查PLC配置,或等待其他连接释放。
- 无响应或连接被重置:
- 检查物理网络和IP地址。
- 检查PLC的防火墙或访问保护设置(特别是S7-1200/1500,需要在“防护与安全”中勾选“允许来自远程对象的PUT/GET通信访问”)。
- 检查TSAP设置,确保与PLC配置一致。对于S7-1200/1500,机架/槽号通常为0,TSAP可能是
03.01或03.00,需要查看PLC属性中的连接机制。 - 确认PLC处于RUN模式,某些操作在STOP模式下被禁止。
理解S7协议报文,就像是掌握了与西门子PLC直接沟通的母语。它让你不再依赖那些可能封装得并不完美的第三方库,在出现通信故障时能够直击问题本质。从最基本的TPKT/COTP信封,到S7 PDU的具体指令,再到复杂数据类型的转换,每一步都需要耐心和细致的理解。我建议你在自己的测试环境中,从一个最简单的读操作开始,用Wireshark抓包,对照本文的解析,亲手构造和解析几个报文。这个过程可能会遇到各种意想不到的问题,但每一次解决问题的经历,都会让你对工业通信协议的理解更深一层。当你能够不借助任何高级库,仅凭socket和协议文档就稳定地与PLC交换数据时,那种对系统底层的掌控感,是使用现成工具无法比拟的。