在实际工业自动化项目中,西门子 S7-1500R/H 冗余系列 PLC 因其高可用性而被广泛应用于关键流程控制。然而,当需要与第三方设备(如仪表、变频器或非西门子 PLC)通过 Modbus TCP 协议进行数据交换时,一个常见且棘手的问题随之而来:标准 Modbus TCP 通信功能块(如MB_CLIENT和MB_SERVER)在冗余 CPU(如 CPU 1515R)上运行时,通常需要额外的“冗余通信块”许可证(Redundant Communication Blocks License)。如果没有此许可证,直接调用这些块可能导致程序无法下载或运行时出错。本文将深入解析这一限制的根源,并提供一套无需额外许可证的、基于标准 TCP 连接自行实现 Modbus TCP 客户端/服务器功能的完整工程方案。无论你是面临预算限制、紧急调试,还是希望深入理解底层通信机制,本文都将引导你从概念理解、环境搭建、代码实现到问题排查,完成一个可在 CPU1515R 上稳定运行的 Modbus TCP 通信程序。
1. 理解冗余 CPU 的 Modbus TCP 限制与替代方案原理
在深入代码之前,必须清楚为什么标准方案在冗余 CPU 上需要额外授权,以及我们自行实现的可行性边界在哪里。
1.1 标准方案的限制:冗余通信块许可证
西门子 TIA Portal 中集成的 Modbus TCP 功能块(位于“通信 > 通信处理器 > Modbus”下)是经过高度封装和优化的库。对于非冗余的标准 S7-1500 CPU,这些块可以直接使用。但对于 CPU 1515R、CPU 1517H 等冗余/容错型 CPU,西门子的设计理念是:为确保冗余系统间通信的绝对可靠性与一致性,任何涉及跨冗余单元的网络通信都应使用经过特殊冗余处理的专用功能块。这些专用块被封装在“冗余通信块”许可证包中。如果没有这个许可证,TIA Portal 会阻止你使用MB_CLIENT或MB_SERVER等功能块在冗余 CPU 的程序中调用。
这个限制的本质是商业授权策略,而非技术不可行性。Modbus TCP 协议本身是标准的、开放的基于 TCP 的应用层协议。
1.2 自行实现的核心思路:使用标准 TCP 连接
既然协议是开放的,我们就可以绕过需要许可证的专用库,使用西门子 PLC 提供的更底层的、通用的开放式用户通信(Open User Communication)功能块。这些块(如TSEND_C,TRCV_C,TCON,TDISCON)用于建立和管理原始的 TCP 连接,并对数据传输进行控制。它们本身不包含任何应用层协议(如 Modbus)的逻辑,因此不受“冗余通信块”许可证的限制。
我们的任务就是:利用这些标准的 TCP 通信块,在 PLC 中手动构建和解析 Modbus TCP 协议帧,从而实现完整的 Modbus 客户端(主站)或服务器(从站)功能。
可行性分析:
- 协议层面:Modbus TCP 协议简单。一个请求/响应报文由 MBAP 头(7字节)和 Modbus PDU(功能码+数据)构成。我们完全可以在 PLC 中组态这些字节数组。
- PLC 能力层面:S7-1500 支持强大的字节、字、数组操作指令(如
MOVE_BLK,Serialize),以及 TCP 通信块,具备实现条件。 - 性能与稳定性:对于中等数据量、非极高速率的通信场景,自行实现的方案完全能够满足要求。关键在于程序的健壮性设计(连接管理、超时处理、错误恢复)。
1.3 方案选型对比
| 特性 | 标准 Modbus 库 (MB_CLIENT/MB_SERVER) | 自行实现方案 (基于 TCON/TSEND/TRCV) |
|---|---|---|
| 是否需要冗余许可证 | 是 | 否 |
| 开发复杂度 | 低,配置简单 | 高,需处理协议组帧、解析、状态机 |
| 代码可控性 | 低,内部逻辑封闭 | 高,完全自主控制 |
| 灵活性 | 一般,遵循库的接口 | 极高,可定制任何协议变体 |
| 性能 | 优化好,效率高 | 取决于实现质量,通常足够 |
| 适用场景 | 有许可证的冗余/非冗余项目 | 无许可证的冗余 CPU、特殊协议需求、学习研究 |
对于 CPU1515R 无许可证的场景,我们别无选择,只能采用自行实现方案。接下来的章节将围绕实现一个 Modbus TCP 客户端(主站)来展开。
2. 环境准备与 TIA Portal 项目配置
在开始编程前,必须确保软件、硬件和项目配置正确。
2.1 软硬件要求清单
- 软件:
- Siemens TIA Portal V16 或更高版本(本文以 V17 为例)。
- 已安装 S7-1500 的硬件支持包。
- 硬件:
- CPU 1515R-2 PN (6ES7 515-2RM00-0AB0)。
- 编程用 PC,与 PLC 处于同一以太网网络。
- 知识准备:
- 基本了解 S7-1500 编程(LAD/FBD/SCL)。
- 了解 Modbus TCP 协议格式(MBAP 头 + PDU)。
- 了解 TCP/IP 基础概念(IP地址、端口)。
2.2 创建新项目与添加硬件
- 打开 TIA Portal,创建一个新项目,命名为“ModbusTCP_NoLicense”。
- 进入“项目视图”,在“设备”区域,点击“添加新设备”。
- 在控制器列表中找到“SIMATIC S7-1500” -> “CPU” -> “CPU 1515R-2 PN (6ES7 515-2RM00-0AB0)”。选择合适的固件版本(如 V2.9),点击“确定”。
- 设备组态视图打开后,可以看到 CPU1515R 的两个接口:X1 (系统总线) 和 X2 (PROFINET)。我们使用X2接口进行 Modbus TCP 通信。
- 在设备概览中,点击 CPU 的 X2 接口,在下方属性窗口中,进入“以太网地址”子项。
- 为 PLC 分配一个固定的 IP 地址、子网掩码和路由器(网关)。例如:
- IP 地址:
192.168.0.10 - 子网掩码:
255.255.255.0
- IP 地址:
- (可选)在“操作模式”子项,确认接口已设置为“使用 IP 协议”,而不是“使用 PROFINET”。
2.3 添加必要的全局数据块与背景数据块
我们将使用以下关键功能块,它们需要对应的背景数据块(Instance DB)和存储数据的全局数据块(Global DB)。
- 创建用于通信控制的全局数据块:
- 在项目树中,右键点击“程序块” -> “添加新块”。
- 选择“数据块”,命名为“DB_ModbusClient”,类型为“全局数据块”。
- 打开 DB,创建以下变量:
STRUCT // 连接参数 bConnect : Bool; // 连接使能 bDisconnect : Bool; // 断开使能 nConnID : Int; // 连接ID,由TCON分配 sRemoteIP : String[15]; // 远程设备IP,如 '192.168.0.20' nRemotePort : Int; // 远程设备端口,Modbus TCP通常为502 nLocalPort : Int; // 本地端口,0表示由系统分配 // 发送数据 aSendBuffer : Array[0..255] of Byte; // 发送缓冲区 nSendLength : Int; // 本次发送数据长度 bSendTrigger : Bool; // 发送触发(上升沿有效) // 接收数据 aRecvBuffer : Array[0..255] of Byte; // 接收缓冲区 nRecvLength : Int; // 实际接收数据长度 bRecvReady : Bool; // 接收完成标志 // 状态与错误 bConnected : Bool; // 连接状态 nErrorCode : Word; // 错误代码 sErrorText : String[255]; // 错误文本 // Modbus 特定 nTransactionID : Word; // 事务标识符,每次请求递增 nUnitID : Byte; // 从站地址(Modbus TCP中常为0xFF或设备地址) END_STRUCT
- 创建用于功能块实例的数据块:
- 我们将使用
TCON,TDISCON,TSEND,TRCV等块。通常为每个连接创建独立的背景数据块更清晰。在“程序块”下添加新的“数据块”,命名为“DB_Conn_Instance”,但这次在创建时,不要选择“全局数据块”,而是在“类型”下拉框中选择“系统块” -> “通信” -> “TCON_IP_v4”。这将创建一个具有TCON所需所有参数的结构化 DB。用同样的方法为TSEND,TRCV创建实例 DB(如DB_Send_Instance,DB_Recv_Instance)。
- 我们将使用
3. 使用开放式用户通信实现 Modbus TCP 客户端
核心逻辑是:建立 TCP 连接 -> 组态 Modbus 请求帧 -> 发送 -> 等待并接收响应 -> 解析响应数据。
3.1 建立与断开 TCP 连接
我们将使用TCON(建立连接) 和TDISCON(断开连接) 功能块。在 OB1(主循环组织块)或一个专用的 FB/FC 中调用它们。
调用 TCON 建立连接:
// 假设在某个循环调用的FB或FC中 // 准备连接参数 #dbClient.sRemoteIP := '192.168.0.20'; // 远程Modbus服务器IP #dbClient.nRemotePort := 502; #dbClient.nLocalPort := 0; // 本地端口由系统分配 // 触发连接 IF #dbClient.bConnect AND NOT #dbClient.bConnected THEN // 填充TCON的背景数据块(DB_Conn_Instance)中的连接参数 #instConn.Connect.Interface := 'LocalPrognetInterface'; // 指向X2接口 #instConn.Connect.ID := 1; // 本地连接ID,用于TSEND/TRCV引用 #instConn.Connect.ConnectionType := 11; // B#16#0B, 表示TCP #instConn.Connect.ActiveEstablished := true; // TRUE表示PLC作为客户端主动连接 #instConn.Connect.RemoteAddress := #dbClient.sRemoteIP; #instConn.Connect.RemotePort := #dbClient.nRemotePort; #instConn.Connect.LocalPort := #dbClient.nLocalPort; // 调用TCON #tconBlock( REQ:=#dbClient.bConnect, ID:=1, CONNECT:=#instConn.Connect, DONE=>#tempDone, BUSY=>#tempBusy, ERROR=>#tempError, STATUS=>#tempStatus); IF #tempDone THEN #dbClient.bConnected := TRUE; #dbClient.nConnID := 1; // 记录连接ID #dbClient.bConnect := FALSE; // 复位触发 ELSIF #tempError THEN #dbClient.nErrorCode := #tempStatus; // 可以添加错误处理 END_IF; END_IF;关键解释:
ConnectionType:=11和ActiveEstablished:=true是核心配置,表明 PLC 作为 TCP 客户端主动连接服务器。ID参数必须与后续TSEND/TRCV调用时使用的ID一致。调用 TDISCON 断开连接:
IF #dbClient.bDisconnect AND #dbClient.bConnected THEN #tdisconBlock( REQ:=#dbClient.bDisconnect, ID:=#dbClient.nConnID, DONE=>#tempDoneD, BUSY=>#tempBusyD, ERROR=>#tempErrorD, STATUS=>#tempStatusD); IF #tempDoneD THEN #dbClient.bConnected := FALSE; #dbClient.nConnID := 0; #dbClient.bDisconnect := FALSE; END_IF; END_IF;
3.2 组态 Modbus TCP 请求帧并发送
Modbus TCP 帧格式:MBAP 头(7字节) + PDU。
假设我们要读取远程设备(Unit ID=1)的保持寄存器(功能码03),起始地址为40001(对应Modbus协议地址0),寄存器数量为2。
- 构建请求帧:
// 在触发发送前,组装数据到 #dbClient.aSendBuffer #dbClient.nTransactionID := #dbClient.nTransactionID + 1; // 事务ID递增 // MBAP Header (7 Bytes) #dbClient.aSendBuffer[0] := BYTE#(SHL(WORD_TO_BYTE(#dbClient.nTransactionID), 8)); // 事务ID高字节 #dbClient.aSendBuffer[1] := BYTE#(#dbClient.nTransactionID AND 16#FF); // 事务ID低字节 #dbClient.aSendBuffer[2] := 0; // 协议标识符高字节 (Modbus=0) #dbClient.aSendBuffer[3] := 0; // 协议标识符低字节 (Modbus=0) #dbClient.aSendBuffer[4] := 0; // 长度高字节 (后面填充) #dbClient.aSendBuffer[5] := 0; // 长度低字节 (后面填充) #dbClient.aSendBuffer[6] := #dbClient.nUnitID; // 单元标识符 (从站地址) // PDU (功能码03请求) #dbClient.aSendBuffer[7] := 16#03; // 功能码: 读保持寄存器 #dbClient.aSendBuffer[8] := 0; // 起始地址高字节 (40001 -> 地址0) #dbClient.aSendBuffer[9] := 0; // 起始地址低字节 #dbClient.aSendBuffer[10] := 0; // 寄存器数量高字节 (2个) #dbClient.aSendBuffer[11] := 2; // 寄存器数量低字节 // 计算长度字段:单元标识符(1) + PDU长度(6) = 7 #dbClient.aSendBuffer[4] := 0; // 长度高字节为0 #dbClient.aSendBuffer[5] := 7; // 长度低字节为7 #dbClient.nSendLength := 12; // 整个帧长度:MBAP(7) + PDU(5) = 12 - 调用 TSEND 发送数据:
// 使用上升沿触发发送,避免重复发送 IF #dbClient.bSendTrigger AND NOT #lastSendTrigger THEN #tsendBlock( REQ:=TRUE, ID:=#dbClient.nConnID, // 使用TCON建立的连接ID LEN:=#dbClient.nSendLength, DATA:=P#DB_ModbusClient.aSendBuffer[0] BYTE 256, // 指向发送缓冲区 DONE=>#sendDone, BUSY=>#sendBusy, ERROR=>#sendError, STATUS=>#sendStatus); #lastSendTrigger := #dbClient.bSendTrigger; END_IF; #lastSendTrigger := #dbClient.bSendTrigger; IF #sendDone THEN // 发送成功,可以准备接收或复位触发 #dbClient.bSendTrigger := FALSE; ELSIF #sendError THEN #dbClient.nErrorCode := #sendStatus; END_IF;
3.3 接收并解析 Modbus TCP 响应帧
发送请求后,需要异步接收响应。通常在一个独立的循环或中断中处理接收。
- 调用 TRCV 接收数据:
// 在连接建立后持续尝试接收 IF #dbClient.bConnected THEN #trcvBlock( EN_R:=TRUE, // 始终使能接收 ID:=#dbClient.nConnID, LEN:=SIZEOF(#dbClient.aRecvBuffer), // 缓冲区最大长度 DATA:=P#DB_ModbusClient.aRecvBuffer[0] BYTE 256, RCVD_LEN=>#dbClient.nRecvLength, NDR=>#rcvNDR, // New Data Received, 新数据接收完成 BUSY=>#rcvBusy, ERROR=>#rcvError, STATUS=>#rcvStatus); IF #rcvNDR THEN #dbClient.bRecvReady := TRUE; // 设置接收完成标志 // 可以在此处或另一个地方调用解析函数 // #parseModbusResponse(DB_ModbusClient); ELSIF #rcvError THEN #dbClient.nErrorCode := #rcvStatus; #dbClient.bRecvReady := FALSE; END_IF; END_IF; - 解析响应帧:
// 在函数FC_ParseResponse中实现 IF #dbClient.bRecvReady AND #dbClient.nRecvLength >= 9 THEN // 最小响应长度 // 检查事务ID和协议ID是否匹配请求(简单验证) IF MAKE_WORD(#dbClient.aRecvBuffer[1], #dbClient.aRecvBuffer[0]) = #dbClient.nTransactionID AND MAKE_WORD(#dbClient.aRecvBuffer[3], #dbClient.aRecvBuffer[2]) = 0 THEN // 检查单元标识符 IF #dbClient.aRecvBuffer[6] = #dbClient.nUnitID THEN // 检查功能码 (最高位为1表示异常) IF #dbClient.aRecvBuffer[7] = 16#03 THEN // 正常响应 #byteCount := #dbClient.aRecvBuffer[8]; // 提取寄存器数据 (假设读2个寄存器,共4字节) #register1 := MAKE_WORD(#dbClient.aRecvBuffer[10], #dbClient.aRecvBuffer[9]); #register2 := MAKE_WORD(#dbClient.aRecvBuffer[12], #dbClient.aRecvBuffer[11]); // 将数据存储到需要的DB区域,例如: #targetDB.RealValue1 := WORD_TO_INT(#register1); #targetDB.RealValue2 := WORD_TO_INT(#register2); ELSIF #dbClient.aRecvBuffer[7] >= 16#80 THEN // 异常响应 #exceptionCode := #dbClient.aRecvBuffer[8]; // 处理异常,如记录日志 #dbClient.nErrorCode := SHL(16#1000, 16) OR #exceptionCode; // 自定义错误码 END_IF; END_IF; END_IF; #dbClient.bRecvReady := FALSE; // 解析完成,复位标志 END_IF;
4. 程序架构设计与关键参数说明
一个健壮的 Modbus TCP 客户端需要良好的状态机管理,而不仅仅是顺序调用功能块。
4.1 推荐的程序架构
主状态机(在 FB 或 FC 中实现):
IDLE: 初始状态。CONNECTING: 正在建立 TCP 连接。CONNECTED: 连接已建立,等待发送或接收。SENDING: 正在发送请求。WAITING_RESPONSE: 已发送请求,等待响应。RECEIVING: 正在接收/解析响应。DISCONNECTING: 正在断开连接。ERROR: 发生错误,等待恢复或复位。 状态机根据TCON,TSEND,TRCV块的DONE,ERROR,BUSY信号进行跳转。
数据流分离:
- 使用独立的全局数据块(如之前的
DB_ModbusClient)存储通信参数、缓冲区和状态。 - 将组帧(
BuildRequest)、解析(ParseResponse)逻辑封装成可复用的 FC 或 FB。 - 连接管理(
Connect,Disconnect)和收发控制(Send,Receive)也封装成独立的逻辑块。
- 使用独立的全局数据块(如之前的
调用方式:
- 在循环中断组织块(如
OB30)或低速循环 OB(如OB1但需控制扫描周期)中调用主通信 FB,以确保定时执行和响应超时判断。
- 在循环中断组织块(如
4.2 关键参数与超时设置
开放式用户通信块本身不直接提供应用层超时。超时需要我们在程序逻辑中实现。
| 参数/变量 | 典型值/设置 | 说明 |
|---|---|---|
TCON.Connect.ID | 1, 2, ... | 连接标识符,必须与TSEND.ID/TRCV.ID一致。 |
TCON.Connect.ConnectionType | B#16#0B (11) | 表示 TCP 协议。 |
TCON.Connect.ActiveEstablished | TRUE | PLC 作为客户端主动连接。 |
TSEND.LEN | 动态计算 | 发送数据的实际字节长度。 |
TRCV.LEN | 缓冲区大小 | 接收缓冲区的最大容量。 |
TRCV.EN_R | TRUE | 在连接期间应始终使能,以持续监听。 |
| 应用层超时 | 3000 ms | 从发送完成到收到响应头的最长等待时间。需用TON定时器在状态机中实现。 |
| 连接重试间隔 | 5000 ms | 连接失败后,再次尝试连接的间隔时间。 |
| 发送间隔 | 100 ms | 两次 Modbus 请求之间的最小间隔,避免对方处理不过来。 |
实现发送超时的伪代码逻辑:
// 在 SENDING -> WAITING_RESPONSE 状态转换时启动定时器 IF #state = SENDING AND #sendDone THEN #state := WAITING_RESPONSE; #responseTimer(IN:=TRUE, PT:=T#3000MS); // 启动3秒定时器 END_IF; // 在 WAITING_RESPONSE 状态检查 IF #state = WAITING_RESPONSE THEN IF #dbClient.bRecvReady THEN #responseTimer(IN:=FALSE); // 收到响应,停止定时器 #state := RECEIVING; ELSIF #responseTimer.Q THEN // 超时处理 #state := ERROR; #dbClient.nErrorCode := 16#8888; // 自定义超时错误码 #responseTimer(IN:=FALSE); END_IF; END_IF;5. 下载、测试与运行验证
5.1 编译与下载
- 在 TIA Portal 中,点击项目树顶部的“编译”按钮,确保没有错误,只有少量警告(如未使用的变量)。
- 通过网线将 PC 与 CPU1515R 的 X2 口连接,并设置 PC 的 IP 地址在同一网段(如
192.168.0.100)。 - 点击“下载到设备”,选择正确的接口和 IP,将硬件配置和程序块下载到 PLC。
- 下载完成后,将 PLC 切换到 RUN 模式。
5.2 测试与验证方法
- 使用网络调试助手模拟服务器:
- 在 PC 上运行一个 Modbus TCP 服务器模拟软件(如 Modbus Slave 模拟器或简单的网络调试助手)。
- 将模拟服务器的 IP 设置为
192.168.0.20,端口502。 - 在模拟器中设置一些保持寄存器(如地址 0, 1)的值。
- 触发 PLC 程序:
- 通过 TIA Portal 的“监控表”或 HMI,将
DB_ModbusClient.bConnect置为TRUE。 - 观察
bConnected是否变为TRUE。 - 将
bSendTrigger置为TRUE一次,触发一次读请求。
- 通过 TIA Portal 的“监控表”或 HMI,将
- 观察数据流:
- 在网络调试助手中,应能看到 PLC 发来的 Modbus TCP 请求帧(12字节)。
- 模拟器应自动回复响应帧。
- 在 PLC 的监控表中,观察
DB_ModbusClient.bRecvReady是否置位,以及解析后的数据(如#register1,#register2)是否正确。 - 同时,观察你程序中用于存储最终数据的 DB 块(如
#targetDB)中的值是否更新为模拟器设置的值。
- 使用 PLC 变量表监控:
- 添加
DB_ModbusClient中的关键变量(bConnected,nErrorCode,aRecvBuffer)到变量表进行在线监控,可以直观看到通信状态和原始字节数据。
- 添加
5.3 预期结果与成功标志
- 连接成功:
bConnected为TRUE,nErrorCode为0。 - 发送成功:触发
bSendTrigger后,网络调试助手收到符合格式的请求报文。 - 接收解析成功:
bRecvReady在发送后一段时间内跳变为TRUE然后被程序复位,且解析出的寄存器数据与模拟器设置一致。 - 数据落地成功:最终目标数据块(如存放过程值的 DB)中的数值随模拟器的修改而变化。
6. 常见问题排查与调试技巧
自行实现通信,排查问题是必备技能。以下是典型问题的排查路径。
6.1 连接建立失败
| 现象 | 可能原因 | 检查点与解决方案 |
|---|---|---|
TCON块ERROR置位,STATUS有错误码。 | 1. IP地址/端口错误。 2. 远程设备未启动或防火墙阻止。 3. 本地端口被占用(如果指定了非0端口)。 4. ActiveEstablished设置错误。 | 1. 核对sRemoteIP和nRemotePort。2. 用 PC 的 ping和telnet命令测试远程设备可达性和端口开放性。3. 尝试将 nLocalPort设为 0。4. 确认 PLC 是客户端,应设 ActiveEstablished:=TRUE。 |
TCON一直BUSY,不进入DONE或ERROR。 | 1. 网络物理链路不通。 2. PLC 硬件接口(X2)未激活或 IP 冲突。 3. ID冲突,同一 ID 被其他连接使用。 | 1. 检查网线、交换机。 2. 在 TIA Portal 在线诊断中查看 PLC X2 接口状态和 IP。 3. 确保项目中所有 TCON/TSEND/TRCV使用的ID唯一。 |
6.2 数据发送失败或对方无响应
| 现象 | 可能原因 | 检查点与解决方案 |
|---|---|---|
TSEND报错或DONE不置位。 | 1. 连接未成功建立 (bConnected为假)。2. TSEND.ID与TCON.ID不匹配。3. LEN为 0 或大于缓冲区。4. DATA指针错误。 | 1. 确保先连接成功再发送。 2. 核对 ID值。3. 检查 nSendLength计算是否正确。4. 检查 DATA参数指向的缓冲区地址是否正确。 |
| 网络调试助手收到乱码或非 Modbus 报文。 | 1. 发送缓冲区数据组装错误。 2. 字节序问题。 | 1. 在线监控aSendBuffer数组,与标准的 Modbus TCP 报文格式逐字节对比。2. 注意 S7-1500 为大端序(高字节在前),与 Modbus TCP 一致,但组帧时 WORD拆高低字节要小心。 |
| 对方收到请求但不回复,或回复后 PLC 收不到。 | 1. PLC 的TRCV未使能 (EN_R不为 TRUE)。2. 接收缓冲区太小。 3. 对方回复的报文格式错误。 | 1. 确保在连接建立后,TRCV的EN_R引脚一直为 TRUE。2. 确保 aRecvBuffer足够大(至少 256 字节)。3. 用 Wireshark 抓包,分析对方回复的报文是否符合 Modbus TCP 规范。 |
6.3 数据解析错误
| 现象 | 可能原因 | 检查点与解决方案 |
|---|---|---|
bRecvReady置位,但解析出的数据不对。 | 1. 事务 ID、协议 ID 或单元 ID 校验失败。 2. 响应长度字段解析错误。 3. 寄存器数据字节顺序与预期不符。 | 1. 在线监控aRecvBuffer,与预期的响应报文对比。2. 确认计算长度字段的公式: 长度 = 单元标识符(1) + PDU长度。3. Modbus 寄存器数据是高字节在前,与 S7-1500 一致,直接使用 MAKE_WORD即可。 |
| 收到异常响应(功能码最高位为1)。 | Modbus 从站报告异常,如非法地址、非法功能码等。 | 检查aRecvBuffer[8](异常码)。01-非法功能码,02-非法数据地址,03-非法数据值。根据异常码检查请求报文中的功能码和地址。 |
6.4 通信不稳定(偶发中断/超时)
| 现象 | 可能原因 | 检查点与解决方案 |
|---|---|---|
| 运行一段时间后连接断开,或频繁超时。 | 1. 网络波动或负载过高。 2. PLC 扫描周期过长,导致响应处理不及时。 3. 未处理 TRCV的ERROR状态,连接异常后未恢复。 | 1. 检查网络硬件。 2. 将通信 FB 放在一个固定周期的循环中断 OB(如 OB30,周期100ms)中调用,确保定时执行。3. 在状态机中增加对 TRCV.ERROR的处理,一旦出错,进入ERROR状态,尝试断开重连。 |
调试黄金法则:始终先在线查看原始字节数组(aSendBuffer和aRecvBuffer),与已知正确的 Modbus 报文格式对比。使用 Wireshark 抓包是定位网络层问题的终极手段。
7. 生产环境最佳实践与扩展方向
将实验代码转化为生产可用的程序,还需要考虑更多因素。
7.1 健壮性增强建议
- 完整的连接生命周期管理:实现自动重连机制。在
ERROR状态或连接意外断开后,延迟一段时间自动触发bDisconnect然后bConnect。 - 发送队列与流控:不要在每个 PLC 扫描周期都触发发送。使用一个队列(数组)存储待发送的请求,按顺序处理,并确保前一个请求收到响应或超时后再发送下一个。
- 超时与重试:为连接建立、发送、接收响应分别设置超时。发送失败或响应超时后,可以进行有限次重试。
- 错误分类与记录:将不同的错误码(
TCON_STATUS,TSEND_STATUS, 自定义超时码、解析异常码)分类,并记录到专门的诊断数据块或发送到 HMI,便于运维。 - 资源清理:在 PLC 停止(
OB100)或程序出错时,确保调用TDISCON主动断开连接。
7.2 性能优化建议
- 使用背景数据块与多重实例:为每个 Modbus 连接创建独立的 FB 背景数据块,实现多连接并行通信。
- 优化缓冲区与数据拷贝:避免频繁使用
MOVE_BLK在大数组间拷贝数据。尽量直接操作原缓冲区。 - 合理设置扫描周期:通信 FB 的调用周期应与通信需求匹配。对于慢速设备(秒级),放在主 OB1 即可;对于需要快速响应的,使用循环中断 OB。
7.3 功能扩展方向
- 实现 Modbus TCP 服务器:原理相同,角色互换。PLC 作为服务器,
TCON的ActiveEstablished设为FALSE,监听端口。在TRCV收到请求后,解析请求,生成响应,再用TSEND发回。 - 支持更多功能码:当前示例仅实现了功能码 03(读保持寄存器)。可以扩展支持 06(写单个寄存器)、16(写多个寄存器)、01(读线圈)、05(写单个线圈)等。
- 封装为可复用库:将整个 Modbus TCP 客户端逻辑封装成一个功能块(FB),并配以对应的输入/输出参数和静态变量,使其像一个简化的
MB_CLIENT一样易于调用。 - 添加 JSON 或自定义协议:掌握了基于 TCP 的原始数据收发能力后,可以轻松实现与其他自定义协议或轻量级协议(如简单的 JSON)的设备通信。
通过以上步骤,你不仅解决了 CPU1515R 无冗余许可证下的 Modbus TCP 通信问题,更获得了一套深入理解西门子 PLC 开放式通信和协议实现的宝贵经验。这种能力让你在面对其他非标准协议或特殊通信需求时,能够从容应对,不再受限于预置功能库的约束。