十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

S7-1200多台Modbus TCP从站轮询:连接ID重复是最隐蔽的坑

S7-1200多台Modbus TCP从站轮询:连接ID重复是最隐蔽的坑 1. 项目背景一台S7-1200四台Modbus TCP从站前阵子做车间数据采集改造一台S7-1200要同时轮询4台Modbus TCP设备。刚接到这个需求时我觉得难度不大Modbus TCP的报文格式早就烂熟于心S7-1200也自带MB_CLIENT指令无非是把设备地址、寄存器地址、数据长度填对再写个轮询逻辑。真正动手之后才发现单台设备怎么调都能通一旦要稳定地轮询多台设备藏在协议之外的坑立刻冒出来而且最深的那个坑我最后才发现。这次涉及的四台从站分别是一台变频器、两块智能电表、一台温控器。每个设备都支持标准的Modbus TCP从站功能所有设备都在同一个管理网段PLC和它们之间不需要经过路由器。理论上轮询四台设备就是一个for循环的事情可真跑起来后现场出现“第一台正常、第三台偶发超时、第四台彻底连不上”的怪现象。排到最后问题竟然不在Modbus报文里而在TIA Portal中那个很少有人会留意的连接结构。1.1 设备与点位清单先看这次实际要采集的点位。寄存器表本身并不复杂四台设备合计要读的保持寄存器大概60个另有少量线圈状态需要监控。设备IP地址主要读取内容寄存器类型变频器192.168.0.11运行频率、输出电流、母线电压、状态字保持寄存器 线圈电表1192.168.0.12三相电压、电流、有功功率、电度保持寄存器电表2192.168.0.13三相电压、电流、有功功率、电度保持寄存器温控器192.168.0.14当前温度、设定温度、运行模式保持寄存器如果只是单点调试这种点位量用Modbus TCP做起来很快。麻烦在于四台设备来自不同厂商寄存器地址编排方式不统一有的从40001开始有的从0开始有的32位浮点按高字在前有的按低字在前有的支持同时多连接有的只允许一个TCP客户端接入。这些细节单独看都不是问题组合到一起就变成一串连环坑。1.2 Modbus TCP看着简单难点都在组合之后Modbus TCP的协议层确实简洁请求报文就是“事务ID 协议ID 长度 单元ID 功能码 数据”响应报文在此基础上多一个异常码字段。抓包看非常直观比PROFINET、EtherNet/IP这些动辄几百页规范的总线协议友好太多。也正因为简单很多工程师容易忽略它背后的TCP连接管理。需要注意Modbus TCP虽然是“请求-响应”模式底层却是建立在TCP之上的。TCP不像UDP那样“发完就完事”连接需要建立、保活、关闭。S7-1200里用MB_CLIENT做客户端时一次完整的“请求-响应”结束后TCP连接默认并不会立刻断开而是保留复用。这个特性在单台设备场景下毫无存在感但一旦做多台设备轮询连接资源的分配、释放、ID唯一性就全都会成为问题。后面我会详细说最深的那个坑就在这里。2. 轮询架构怎么选单客户端动态改IP还是多客户端实例在做4台Modbus TCP设备轮询时我见过两种典型架构。第一种是用一个MB_CLIENT实例轮询前动态修改CONNECT结构里的IP地址第二种是创建4个MB_CLIENT实例每台设备一个独立连接。先别急着选两种方式各有各的坑我这次先用的是第一种后来才换成第二种。2.1 单客户端方案看似省事实际埋雷单客户端方案长这样程序里只放一个MB_CLIENT指令设备1读完后把CONNECT结构中的RemoteAddress改成设备2的IP再触发一次REQ。这样做的优点非常直观——代码量小背景DB少监控起来只有一组DONE、BUSY、ERROR信号。但这个方案有一个隐含前提每次切换设备前前一个TCP连接必须被彻底释放。很多从网上抄来的例程只会在REQ上做上升沿和下降沿却忽略了MB_CLIENT的TCP连接并不会因为一次请求完成就自动关闭。我实测下来的现象是第一轮轮询设备1、设备2都正常第二轮开始偶发报错到第三轮设备4直接超时。原因就是设备1的连接还挂在系统里当你把CONNECT改成设备4并再次触发REQ时旧的连接资源没有释放CPU无法建立新的连接。如果你实在想用单客户端方案必须补上“DISCONNECT”这一步请求完成后给DISCONNECT一个TRUE信号等待BUSY归零再修改CONNECT结构最后才能给REQ一个上升沿。这一套时序漏掉任何一环轮询都会不稳定。即便这样我还是不推荐4台以上设备用这种方式状态机写复杂之后排查成本远高于省下的几个DB块。2.2 多客户端方案每台设备一个MB_CLIENT这次项目改成4个MB_CLIENT实例后问题立刻少了八成。每台设备对应一个FB实例、一个背景DB、一个独立的CONNECT结构。四个实例的REQ可以按顺序触发也可以同时触发因为它们的TCP连接彼此独立不会互相抢占。这种方案的缺点是背景DB数量多程序里的调用代码看起来有一些重复。但换来的是逻辑简单设备1的问题不会波及其他设备某台设备掉线时只需要看它对应的STATUS码。也正因为每台设备有独立的MB_CLIENT实例调试时可以单独强制某一路的REQ不需要在复杂的切换逻辑里猜问题。2.3 两个方案的对比对比项单客户端动态切换多客户端独立实例程序量小中背景DB数量1个每设备1个连接管理复杂度高需手动处理DISCONNECT低天然隔离故障排查难度高问题互相影响低按设备定位适合设备数1-2台临时采集4台及以上固定轮询从长期运维角度我建议在设备数量超过2台时直接采用多客户端方案。后面要讲的“连接ID重复”这个深坑也多发生在多客户端方案里。3. 比报文更隐蔽的细节地址、长度、字序与连接ID项目中最容易踩的其实是下面这些细节每个单独拎出来都能写一篇排查记录。我把它们都列出来因为真正的“深坑”往往是在这些小问题全部排掉之后才会暴露出来。3.1 功能码和DATA_ADDR地址的错位S7-1200的MB_CLIENT指令里MODE参数用来指定读还是写DATA_ADDR参数用来指定Modbus地址。很多人直接照搬其他PLC的写法把DATA_ADDR填成0开头结果读出来的数值完全对不上。Modbus TCP在报文层面使用的是“数据模型地址”和“协议数据地址”而在S7-1200的MB_CLIENT里DATA_ADDR需要按带区号的形式填写。我建议先把这张表记牢DATA_ADDR范围对应Modbus数据区常见功能码DATA_LEN单位00001-09999线圈 Coil01/05/0F位10001-19999离散输入 Discrete Input02位30001-39999输入寄存器 Input Register04字40001-49999保持寄存器 Holding Register03/06/10字举例来说设备手册上写“电压存放在保持寄存器地址40001”在S7-1200里DATA_ADDR就填40001。如果手册上写的是“寄存器偏移地址0”那在Modbus TCP报文里对应的就是40001。千万不要看到手册里有个0就直接填0要根据地址类型换算。3.2 MB_DATA_LEN的单位陷阱第二个容易翻车的点是MB_DATA_LEN的单位。寄存器类操作功能码03/04的长度单位是“字”线圈和离散输入功能码01/02的单位是“位”。我第一次读电表时想把10个保持寄存器的数据装进一个“10字节”的缓冲区结果PLC反馈地址错误查了半天才发现DATA_LEN应该填10指的是10个字缓冲区至少要20字节。这里有一个很实用的经验把数据缓冲区定义成ARRAY[0..9] OF WORD而不是ARRAY[0..19] OF BYTE。这样DATA_LEN填10时缓冲区大小天然匹配。后续做32位浮点数运算时再用AT覆盖或者字节拼接的方式转成REAL不容易出现缓冲区越界。3.3 32位数值的词序问题Modbus TCP本质上只传输16位寄存器32位整数、32位浮点数都要靠两个连续寄存器拼出来。协议标准推荐使用大端字序也就是高字在前、低字在后。可惜现实世界不守规矩很多国产设备默认按小端字序输出低字在前、高字在后。现场表现很典型电表回传的电压值读出来变成几千万甚至负值。把两个WORD在监控表里手动交换一下数值就对了。解决办法有两种如果你读的是浮点数把收到的两个寄存器按设备实际字序重新排列再通过指针或AT覆盖转成REAL如果读的是32位整数同样先交换WORD再用左移右移拼出DINT。我建议在正式写逻辑前先用Modbus Poll之类的调试工具连一次设备确认它到底是“高字在前”还是“低字在前”并把结论写到项目文档里。这个动作能省下后面大量的试错时间。3.4 连接结构里藏得最深的ID现在说这次项目里藏得最深的一个坑TCON_IP_v4连接结构里的ID参数。S7-1200做Modbus TCP客户端时CONNECT参数要指向一个TCON_IP_v4结构里面至少包含连接ID、连接类型、远程IP、远程端口这些字段。很多工程师包括我在创建第二个、第三个设备时习惯把第一个CONNECT块复制一份只改IP地址ID就顺手保留成原来的值。我这次就是这样四个CONNECT结构全部复制自设备1ID全是1。问题在PLC运行时才暴露设备1正常设备2偶发失败设备3报错设备4几乎连不上。查IP、查端口、查防火墙全部正常。最后在监控表里逐个点开CONNECT结构才发现ID全是1。改成1、2、3、4之后四台设备立刻全部恢复正常。这个ID的全称是连接标识它在CPU内部是全局唯一的。你可以把它理解为每个TCP连接在本机系统里的“身份证号”。四台设备同时建立连接时如果ID重复后面建立连接的任务就会因为“标识已被占用”而失败。更要命的是ID不会出现在任何Modbus TCP报文中用Wireshark抓包根本看不见它所以排查时很难想到是这个参数的问题。这大概就是为什么说它是藏得最深的坑。在实际项目中我会把CONNECT结构统一放在一个全局DB里四个设备分别定义成CONNECT[1]到CONNECT[4]并规定ID从101开始往后排。这样即使以后加入其他TCP通信块也不至于和Modbus轮的连接ID撞车。3.5 连接不释放DISCONNECT和BUSY时序多客户端方案里每台设备一个连接看似不需要管连接释放。但如果某台设备重新上电、网络断掉重连原先残留的连接状态也会导致后续轮询失败。正确做法是轮询状态机里每次请求完成后需要把REQ拉低等BUSY归零再开始下一轮。如果从站支持断线重连可以周期性给DISCONNECT一个短暂TRUE信号主动清理连接避免TCP半开连接累积。这个时序听起来简单但我在现场见过很多程序只做“REQ上升沿”不处理BUSY导致请求发出去后还没收到响应下一次REQ又把请求塞进去了。Modbus TCP是请求响应协议同一时间一个客户端连接上只能有一个未完成的事务BUSY不归零就发下一个请求轻则丢帧重则连接错乱。所以轮询逻辑一定要做成状态机而不是简单的定时器加REQ脉冲。4. 四台设备轮询的实操步骤与程序骨架4.1 网络规划和TIA组态清单先做网络规划。PLC的IP设为192.168.0.10四台从站分别是192.168.0.11到192.168.0.14统一使用端口502。如果现场存在多个网段PLC要能路由到从站所在网段否则Modbus TCP压根建立不了连接。TIA Portal里的组态步骤大概是这样的新建项目添加S7-1200 CPU设置好PLC的IP地址。新建全局DBConnDB里面定义一个ARRAY[1..4] OF TCON_IP_v4的结构数组。在ConnDB里给每个元素赋值ID分别为101、102、103、104ConnectionType为16#11TCPActiveEstablished为TRUERemoteAddress分别填四台设备的IPRemotePort填502。新建数据DBDataDB里面定义四个ARRAY[0..9] OF WORD分别存放四台设备的原始寄存器数据。在OB1里调用4次MB_CLIENT指令每个指令分配独立的背景DBCONNECT参数分别绑定ConnDB.Conn[1]到ConnDB.Conn[4]DATA_PTR分别绑定DataDB.Regs[1]到DataDB.Regs[4]。需要特别提醒的是不要在一个全局DB里只建一个TCON_IP_v4结构然后让四个MB_CLIENT都指向它。那样和单客户端方案的连接复用问题本质上没区别ID就算改了CONNECT结构被多个实例同时读写也会造成莫名其妙的指针冲突。4.2 轮询状态机与关键逻辑四台设备各自独立调用MB_CLIENT后轮询逻辑不需要太花哨。用顺序触发即可设备1完成再触发设备2这样也能避免现场多台设备同时响应造成的网络小波动。下面是一段简化后的SCL伪代码重点是REQ、DONE、ERROR、BUSY四个信号之间的握手时序。// 伪代码四台设备顺序轮询 CASE #step OF 0: // 等待轮询周期 IF #poll_trigger THEN #mb1_req : TRUE; #step : 10; END_IF; 10: // 等待设备1完成 IF #mb1_done OR #mb1_error THEN #mb1_req : FALSE; #step : 11; END_IF; 11: // 确认设备1的BUSY归零 IF NOT #mb1_busy THEN #step : 20; END_IF; 20: // 触发设备2 #mb2_req : TRUE; #step : 30; 30: IF #mb2_done OR #mb2_error THEN #mb2_req : FALSE; #step : 31; END_IF; 31: IF NOT #mb2_busy THEN #step : 40; END_IF; // 设备3、设备4类似不展开 ... END_CASE;想再稳妥一点可以在每个设备完成后把STATUS码和当前时间戳写入故障日志DB。这样一旦某台设备异常后续翻日志就能看到是哪个时间点、哪个设备、错误码是什么不用一直盯着监控表。4.3 先用仿真器把流程跑通四台设备同时上电前强烈建议先在本机用Modbus TCP仿真器验证程序逻辑。Python的pymodbus库可以快速搭一个假的Modbus TCP从站把设备手册里的寄存器表提前填进去S7-1200这边直接按照正式程序跑轮询。这样能提前发现地址错位、长度单位、字序这类问题不用到现场反复断电重启。我这里给一个最简的仿真从站思路from pymodbus.server import StartTcpServer from pymodbus.datastore import ModbusSlaveContext, ModbusDataBlock from pymodbus.datastore import ModbusServerContext store ModbusSlaveContext( diModbusDataBlock.create([0]*100), coModbusDataBlock.create([0]*100), hrModbusDataBlock.create([0]*100), irModbusDataBlock.create([0]*100) ) context ModbusServerContext(slavesstore, singleTrue) StartTcpServer(context, address(0.0.0.0, 502))实际测试时给保持寄存器写入几个特征值比如12345、-6789用来验证S7-1200读到的字节序和字序是否一致。仿真通过后再去现场接入真实设备能省下大量调试时间。5. 现场排查实录与避坑速查5.1 我踩过的五个坑按出现频率排序我把这次项目中真正踩过的坑总结成下面五条每条都对应一个明确的处理动作。地址错位所有寄存器都按0偏移填写导致读出来的是相邻地址的数据。处理办法是统一按“40001带区号”方式填写DATA_ADDR。长度单位错DATA_LEN按字节填写缓冲区按字节数组定义导致状态报错。处理办法是寄存器操作直接按“字”为单位缓冲区用WORD数组。32位字序颠倒浮点数读出来数值异常。处理办法是用Modbus Poll确认设备字序再做WORD交换和字节拼接。CONNECT里的ID重复这个最隐蔽。四台设备复制同一个连接块ID全部一样导致部分设备连不上。处理办法是每个连接的ID全局唯一。轮询不等待BUSY归零REQ下降沿没做或下降沿没保持足够时间导致请求被重复触发。处理办法是改成状态机每台设备经历“触发-等待DONE/ERROR-等待BUSY归零”三个阶段。5.2 STATUS错误码速查MB_CLIENT执行异常时STATUS会输出一个16进制错误码。不同的错误码对应不同方向我没有办法把所有码都背下来但常用的几个排查方向可以记一下STATUS常见方向优先排查项16#80C8TCP连接建立失败IP地址、子网掩码、远程端口是否50216#818A连接标识占用或连接资源不足CONNECT结构里的ID是否唯一是否超过CPU连接数限制16#80D5连接超时或对端主动断开检查从站是否在线适当增大超时时间16#7002数据长度越界或不支持检查DATA_LEN单位以及从站是否支持该功能码注意STATUS码在不同固件版本里可能有差异遇到不确定的码最有效的办法是打开TIA Portal在线帮助按STATUS码原文搜索或者查CPU的诊断缓冲区。不要凭网上的碎片化经验硬猜。5.3 排查思路与工具箱遇到Modbus TCP轮询异常我的排查顺序一般是先看PLC侧STATUS码判定是连接层、参数层还是数据层问题再用Wireshark在PLC所接交换机上抓包看TCP三次握手是否完成、Modbus请求是否发出、从站是否有响应最后用Modbus Poll之类的独立工具直连从站验证从站本身是否正常。这里有一个容易忽略的小技巧当S7-1200作为Modbus TCP客户端时如果抓包发现PLC一直在发送SYN但TCP连接始终建不起来多半是IP路由不通、设备端口错误或者设备只允许一个连接且已经被占用。这时候不要折腾PLC程序先去查网络和设备连接许可。如果抓包看到TCP连接已经建立但Modbus响应迟迟不来再从站侧程序、寄存器地址、数据长度这些方向排查。这个思路几乎可以覆盖绝大多数Modbus TCP轮询问题。真正常见的坑反而不是协议本身而是连接管理、地址编排、数据长度单位这些“看似简单”的地方。尤其是CONNECT结构里的ID这个参数不抓包看不出、编译也不报错却能把整个轮询系统拖垮。我现在的习惯是新建工程后的第一件事就是把CONNECT结构做成全局DB把ID规划好每台设备一个固定编号。别小看这一步越到后期越值钱。
返回列表