
1. 从一条产线调试日志说起为什么Socket通信值得单独拎出来讲去年冬天我在一个包装码垛项目上做联调。现场有一台S7-1200做主控上位机是一台工控机跑着C#写的MES对接程序中间还挂着一台视觉相机做垛型检测。按照常规做法视觉相机走Profinet、MES走OPC UA各走各的协议互不干扰。但客户临时提了一个需求MES那边要实时拿到每一垛的码垛坐标和节拍数据而且他们的软件团队只会用Socket不想再引入OPC UA的SDK。这个需求听起来简单做起来却让我在三天里反复改了四版程序。问题不在于Socket本身有多难而在于PLC侧的Socket通信和PC侧的Socket通信在思维模型上完全是两回事。PC程序员习惯了建立连接—收发数据—关闭连接的线性流程而PLC的扫描周期机制决定了它必须把Socket操作拆散到不同的扫描周期里用状态机的方式去驱动。这个认知差是绝大多数初次接触PLC Socket通信的工程师踩坑的根源。所以这篇内容我想把基于西门子PLC的Socket通信从头到尾拆一遍。不是照着手册念指令而是把为什么这么设计、参数怎么算、状态机怎么搭、现场会遇到什么幺蛾子这些真正影响项目成败的东西讲透。适合已经会写基本梯形图、但一碰到开放式通信就发怵的工程师也适合做上位机开发、需要和PLC对接Socket的朋友。关键词先摆出来西门子PLC、Socket通信、TCP连接、TCP长连接与短连接、TCP三次握手、Modbus TCP、S7-1200、开放式用户通信。这些词背后对应的是一整套通信架构的选型逻辑我会在后面的章节里逐一展开。2. 西门子PLC做Socket通信到底用的是哪套机制2.1 开放式用户通信指令族TCON、TSEND、TRCV、TDISCON西门子把Socket通信归在开放式用户通信Open User Communication这个大类下面。S7-1200和S7-1500系列提供的核心指令就四个TCON建立连接、TSEND发送数据、TRCV接收数据、TDISCON断开连接。如果是S7-300/400对应的指令是AG_SEND和AG_RECV走的是不同的库这里不展开。这四个指令的工作方式和PC上的Socket API有本质区别。PC上你调用一个connect()线程会阻塞在那里直到连接建立或超时。但PLC不行PLC的OB1扫描周期可能只有几毫秒你不可能让CPU停在那里等网络握手。所以TCON指令的设计是调用一次发起连接请求然后立刻返回连接是否建立成功通过指令的DONE、BUSY、ERROR三个输出位来反馈你需要在后续的扫描周期里反复调用TCON直到DONE或ERROR置位。这就引出了第一个关键认知PLC的Socket通信必须用状态机来驱动。你不能像写PC程序那样顺序执行必须把连接—发送—接收—断开这几个阶段拆成状态每个扫描周期根据当前状态和指令反馈来决定下一步做什么。2.2 TCP和ISO-on-TCP的区别以及为什么大多数场景选TCP西门子的开放式用户通信支持两种协议TCP和ISO-on-TCP也叫RFC1006。两者的区别在于ISO-on-TCP在TCP之上加了一层ISO协议头提供了更严格的数据边界标识。而纯TCP是字节流协议没有消息边界的概念。那为什么大多数项目还是选TCP因为上位机侧的兼容性。你用C#、Python、Java写Socket客户端默认都是纯TCP。如果要对接ISO-on-TCP上位机也得实现RFC1006的封装工作量陡增。所以除非是西门子PLC之间互连、且对数据边界有严格要求否则一律选TCP。选TCP带来的代价是你必须自己处理消息边界。TCP是字节流发送方调了三次TSEND发三包数据接收方可能一次TRCV就把三包全收上来了也可能一包数据被拆成两次才收完。这个问题在PC编程里也存在但PC侧有成熟的框架帮你处理PLC侧你得自己写逻辑。2.3 连接资源的上限S7-1200到底能开几条Socket这是现场最容易被忽略的问题。S7-1200的开放式用户通信连接数是有限制的具体数值取决于CPU型号和固件版本。以CPU 1214C DC/DC/DC为例它的开放式用户通信连接资源是8条其中主动连接和被动连接共享这个池子。S7-1500的资源更多但也不是无限的。这意味着什么如果你要做一个PLC同时和32台设备通信的项目——比如热搜词里提到的一个西门子PLC与32个变频器Modbus通讯控制——你不可能给每台变频器开一条独立的Socket连接。32条连接远超S7-1200的上限。这种场景必须用轮询的方式用一条或少数几条连接分时和不同设备通信。提示在博途里查看PLC资源使用情况可以在在线和诊断里看连接资源选项卡它会显示当前已用的连接数和剩余可用数。调试阶段养成定期查看的习惯能避免很多莫名其妙连不上的问题。3. 用状态机驯服PLC的Socket通信从连接建立到数据收发3.1 为什么不能用顺序逻辑写Socket程序前面提到了状态机的必要性这里展开说清楚原因。假设你写了一段顺序逻辑先调TCON等连接建立再调TSEND发数据再调TRCV收数据。在PC上这么写没问题因为PC程序是顺序执行的。但在PLC里OB1每个扫描周期都会从头到尾执行一遍如果你不加状态判断TCON会被反复调用TSEND也会在连接还没建立时就被调用结果就是指令报错、连接混乱。正确的做法是定义一个整数变量作为状态字比如iState然后用CASE语句或比较指令来分支。典型的状态划分是这样的状态值状态名称动作转移条件0空闲等待触发收到启动信号转到1010建立连接调用TCONDONE1转到20ERROR1转到9920发送数据调用TSENDDONE1转到30ERROR1转到9930接收数据调用TRCVNDR1处理数据后转回20ERROR1转到9999错误处理调用TDISCON记录错误码延时后转回0这个状态机是骨架实际项目里还要根据业务逻辑增加状态比如等待响应超时、重连计数等。但核心思路就是每个扫描周期只执行当前状态对应的动作根据指令反馈决定是否转移状态。3.2 TCON指令的参数配置连接描述数据结构TCON指令需要一个连接描述参数在博途里通常是一个TCON_IP_v4类型的结构体。这个结构体里几个关键字段必须配对正确InterfaceId硬件标识符在设备组态的系统常数里能找到对应PLC的以太网口。ID连接ID自己分配范围1~4095同一PLC内不能重复。ConnectionType16#0B表示TCP16#0C表示ISO-on-TCP。ActiveEstablishedTRUE表示主动连接PLC做客户端FALSE表示被动连接PLC做服务器。RemoteAddress对方IP地址四个字节分别填。RemotePort对方端口号。LocalPort本地端口号主动连接时可以填0让系统自动分配。这里有个坑RemoteAddress的字节顺序。在博途里这个字段是Array[1..4] of Byte填的时候要按正常IP顺序填比如192.168.1.100就填192、168、1、100。但如果你是用SCL动态赋值要注意字节序问题别搞反了。另一个坑是LocalPort。做主动连接时填0没问题但做被动连接PLC当服务器时LocalPort必须填一个明确的端口号而且这个端口号不能和PLC上其他服务冲突。西门子自己占用了不少端口比如102是ISO-on-TCP的默认端口80是Web服务器。选端口时避开这些。3.3 TSEND和TRCV的数据区管理别让缓冲区溢出TSEND发送的数据放在一个Data块或M区的数组里TRCV接收的数据也放在一个数组里。这两个数组的长度需要根据你的协议来定。如果对方发来的数据长度超过了你TRCV的缓冲区长度TRCV会报错错误码是16#8085缓冲区太小。我的经验是接收缓冲区至少要比协议约定的最大报文长度多留20%的余量。比如你的协议规定最大报文是1024字节那缓冲区就开到1280字节。多出来的空间不是为了浪费而是为了应对对方程序异常时可能发出的超长报文。现场调试时遇到过对方上位机程序bug一次发了4KB数据过来直接把PLC的TRCV干报错了整个通信中断。后来把缓冲区加大到2KB虽然还是收不全但至少不会报错中断PLC可以丢弃超长部分继续工作。发送侧要注意的是TSEND的LEN参数。这个参数指定本次发送多少字节。如果你每次发送的长度不固定就需要动态计算LEN的值。在SCL里可以用LEN : iDataLength这种方式。但要注意LEN的值不能超过发送缓冲区的实际长度否则TSEND会报错。3.4 接收数据的边界处理TCP粘包问题的PLC解法TCP粘包是Socket通信的经典问题。PC侧通常用长度字段数据体的协议格式来解决报文头里用2或4个字节标明数据体长度接收方先读头、解析出长度、再读对应长度的数据体。在PLC侧实现这个逻辑需要在状态机里增加一个解析报文头的状态。具体做法是TRCV收到数据后先判断接收到的字节数是否大于等于报文头长度。如果是从缓冲区里取出长度字段计算出完整报文的总长度。然后判断当前收到的字节数是否达到总长度如果没达到继续TRCV接收如果达到了处理完整报文然后把缓冲区里剩余的数据前移准备处理下一包。这个逻辑用SCL写会比较清晰用梯形图写会非常痛苦。所以强烈建议PLC侧的Socket通信程序用SCL编写梯形图只适合做状态机的简单分支涉及数组操作、循环、指针的地方SCL的效率高得多。4. 长连接还是短连接一个被低估的架构决策4.1 短连接的工作模式与适用场景短连接的意思是每次需要通信时建立连接通信完成后立刻断开。对应到PLC指令就是TCON→TSEND/TRCV→TDISCON一个完整的周期。短连接的优点是资源占用时间短。如果你的PLC需要和很多台设备通信但每台设备的通信频率很低比如每分钟一次用短连接可以让连接资源快速释放轮询更多设备。缺点是每次通信都要经历TCP三次握手增加了延迟。在局域网内三次握手的延迟通常在几毫秒到十几毫秒对于慢速轮询场景可以接受。短连接还有一个隐性优点故障恢复简单。如果连接断了下次通信时重新TCON就行不需要复杂的重连逻辑。这在现场网络环境不稳定时反而更可靠。4.2 长连接的维持成本与断线重连策略长连接是建立一次连接后保持不断后续通信直接TSEND/TRCV。优点是省去了反复握手的开销延迟低、吞吐高。适合通信频率高、数据量大的场景比如PLC和上位机之间的实时数据同步。但长连接的麻烦在于断线检测和重连。TCP连接可能因为网络抖动、对方程序重启、交换机重启等原因断开而PLC侧不一定能立刻感知到。如果PLC继续往一个已经断开的连接上TSEND会收到错误码。这时候需要状态机检测到错误调用TDISCON清理然后重新TCON。我的做法是在长连接状态机里加一个心跳机制每隔固定时间比如5秒发送一个心跳报文如果连续3次发送都报错就判定连接断开进入重连流程。重连时不要立刻重试加一个递增的延时比如第一次等1秒第二次等2秒第三次等4秒避免在网络故障时疯狂重连把CPU资源耗尽。4.3 连接数受限时的轮询架构设计回到那个一个PLC控制32台变频器的场景。如果用Modbus TCP每台变频器是一个Modbus TCP服务器PLC作为客户端去轮询。32台设备PLC不可能同时开32条连接必须轮询。轮询架构的设计要点连接复用用一条TCP连接依次和不同IP的设备通信。但这里有个问题TCP连接是绑定IP的你不能用一条连接去连不同IP。所以实际上还是要为每台设备建立连接只是可以分批建立、分批断开。分组轮询把32台设备分成4组每组8台同一时间只保持一组设备的连接。轮询完一组后断开再连下一组。这样任意时刻最多占用8条连接资源。优先级队列如果某些设备的数据需要更高频率采集给它们更高的轮询优先级在队列里排前面。这个架构用SCL写大概需要200~300行代码核心是一个设备列表数组和一个轮询指针。每完成一台设备的通信指针加一指向下一台。指针超过数组长度就归零同时切换连接组。注意Modbus TCP和原生Socket通信在PLC侧的实现方式不同。Modbus TCP有专门的MB_CLIENT指令不需要自己调TCON/TSEND/TRCV。但MB_CLIENT底层也是基于开放式用户通信实现的理解Socket通信的原理有助于排查MB_CLIENT的故障。5. 现场调试实录那些手册上不会写的坑5.1 连接建立成功但收不到数据防火墙与端口排查有一次在现场PLC和上位机的TCON都显示DONE1连接建立成功了但TRCV一直收不到数据NDR位始终为0。排查了半天最后发现是上位机那台工控机的Windows防火墙把入站规则给拦了。PLC发过去的数据被防火墙丢弃上位机根本没收到。排查这类问题的顺序应该是确认物理链路ping一下对方IP通不通。确认端口监听在上位机上用netstat -an看目标端口是否处于LISTENING状态。确认防火墙临时关闭防火墙测试如果通了就是防火墙问题然后加例外规则而不是一直关着。确认PLC侧发送在PLC侧监控TSEND的DONE位确认数据确实发出去了。抓包如果以上都正常用Wireshark抓包看数据到底走到哪一步了。5.2 TRCV报16#8085接收缓冲区溢出的根因分析前面提到了16#8085错误这里展开说排查思路。这个错误码的含义是接收缓冲区太小无法容纳完整报文。但有时候你的缓冲区明明够大还是报这个错原因可能是对方发送的数据长度超过了协议约定。比如协议说好最大1024字节对方程序bug发了2048字节。TRCV的LEN参数设置不当。TRCV的LEN参数指定本次最多接收多少字节如果设得比缓冲区实际长度小也会报错。数据区被其他程序改写。如果接收缓冲区所在的DB块被其他逻辑意外写入可能导致长度信息混乱。我的处理方式是在TRCV报错时把错误码和当前接收到的字节数记录到一个诊断DB里同时把接收缓冲区的前64字节也存下来。这样事后分析时能看到对方到底发了什么快速定位是协议问题还是对方程序问题。5.3 扫描周期被通信拖慢OB1里的通信指令该放多少Socket通信指令放在OB1里执行会占用扫描周期时间。TCON、TSEND、TRCV这些指令的执行时间取决于网络状态如果网络拥塞指令的BUSY位可能持续多个扫描周期导致OB1的循环时间变长。如果通信逻辑比较复杂建议把Socket状态机放到一个循环中断OB里执行比如OB30设置循环时间为10ms或20ms。这样通信逻辑的执行频率是固定的不会因为OB1里其他逻辑的耗时波动而受影响。同时循环中断OB的优先级比OB1高能保证通信的实时性。但要注意循环中断OB里不能执行耗时太长的操作否则会触发时间错误。如果状态机里有一次处理大量数据的循环要把它拆成多次小批量处理分散到多个中断周期里完成。5.4 上位机重启后PLC连不上连接状态残留问题这是一个很隐蔽的坑。上位机程序重启后PLC侧的TCON可能还认为连接是建立状态不会主动重新连接。而上位机侧已经是一个全新的Socket了双方状态不一致通信就卡住了。解决办法有两个一是在PLC侧加连接超时检测如果一段时间内没有收到对方数据主动TDISCON再重连二是在协议里加一个握手报文上位机重启后先发一个握手包PLC收到后重置状态机。我通常两个都做双保险。6. 从Socket到Modbus TCP协议选型的边界在哪里6.1 什么时候该用原生Socket什么时候该用Modbus TCP原生Socket通信给你最大的自由度你可以定义任何协议格式。但自由度也意味着你要自己处理所有细节报文边界、校验、重传、超时。适合的场景是上位机是你自己开发的协议可以双方约定或者对接的是非标设备没有标准协议。Modbus TCP是标准协议有成熟的指令库MB_CLIENT/MB_SERVER报文格式固定调试工具多比如Modbus Poll。适合对接标准Modbus设备比如变频器、仪表、远程IO。缺点是协议开销大传输效率不如自定义协议。选型的原则很简单能用标准协议就用标准协议标准协议搞不定的再用原生Socket。标准协议意味着更少的bug、更好的可维护性、更容易找到调试工具。6.2 Modbus TCP在PLC侧的底层实现与Socket的关系MB_CLIENT指令底层就是基于开放式用户通信实现的。你在博途里调用MB_CLIENT时它内部会管理TCON、TSEND、TRCV的调用。但MB_CLIENT有自己的连接管理机制一个MB_CLIENT实例对应一条连接。如果你要轮询32台设备就需要32个MB_CLIENT实例或者用多个实例分时复用。这里有个容易混淆的点MB_CLIENT的实例数量不等于连接数量。你可以用同一个MB_CLIENT实例通过改变MB_CLIENT的MB_UNIT_ID和IP参数去轮询不同设备。但每次改变参数前需要先断开当前连接。所以本质上还是轮询。6.3 混合架构Socket做实时数据Modbus TCP做设备控制在实际项目里我经常用混合架构PLC和上位机之间用原生Socket传实时数据因为数据量大、频率高、协议可以自定义优化PLC和变频器之间用Modbus TCP做控制因为变频器支持标准Modbus协议用现成指令省事。这种架构的关键是做好资源分配。S7-1200的连接资源有限要算清楚上位机Socket占几条、Modbus TCP占几条、留几条余量给调试和备用。如果资源不够就要考虑升级到S7-1500或者减少同时连接的设备数。7. 写在最后几个我反复验证过的实操建议关于PLC的Socket通信我踩过的坑远不止上面这些。最后再分享几条用血泪换来的经验。第一条永远在PLC侧做超时处理。不要假设对方会及时响应。TCON有超时但TSEND和TRCV没有内置超时。你需要在状态机里自己计时如果发出请求后N个扫描周期内没收到响应就判定超时进入错误处理。超时时间设多少局域网内建议3~5秒跨网段建议10~15秒。第二条诊断信息要存够。在DB块里专门开一个诊断区记录每次通信的错误码、时间戳、收发字节数。现场出问题时这些数据比任何猜测都有用。我习惯用环形缓冲区的方式存最近100条通信记录PLC重启也不丢失用保持性DB。第三条先用调试工具跑通协议再写PLC程序。在写PLC代码之前先用Modbus Poll、Socket调试助手之类的工具把通信协议跑通。确认对方设备或上位机的收发格式、字节序、超时行为都符合预期再动手写PLC程序。这样能把问题隔离在协议层面而不是PLC代码层面。第四条SCL比梯形图更适合写通信程序。这句话我说了很多遍但还是要强调。通信程序涉及大量的数组操作、循环、条件分支SCL的表达能力比梯形图强太多。博途里SCL和梯形图可以混用通信状态机用SCL写其他逻辑用梯形图写各取所长。第五条版本兼容性要提前确认。不同固件版本的S7-1200开放式用户通信指令的行为可能有细微差异。比如早期固件的TCON不支持某些参数组合升级固件后才行。项目开始前确认PLC的固件版本查对应的手册别等调试时才发现指令不支持。这些经验不一定每条都适用于你的项目但至少能帮你少走一些弯路。Socket通信本身不复杂复杂的是把它塞进PLC的扫描周期模型里还要保证稳定可靠。把状态机搭好、把超时和诊断做足、把资源算清楚剩下的就是耐心调试了。