S7-1200多台Modbus TCP从站轮询:连接ID重复是最隐蔽的坑
2026/9/21 1:41:19 网站建设 项目流程

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结构全部复制自设备1,ID全是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里的组态步骤大概是这样的:

  1. 新建项目,添加S7-1200 CPU,设置好PLC的IP地址。
  2. 新建全局DB:ConnDB,里面定义一个ARRAY[1..4] OF TCON_IP_v4的结构数组。
  3. ConnDB里给每个元素赋值:ID分别为101、102、103、104;ConnectionType为16#11(TCP);ActiveEstablished为TRUE;RemoteAddress分别填四台设备的IP;RemotePort填502。
  4. 新建数据DB:DataDB,里面定义四个ARRAY[0..9] OF WORD,分别存放四台设备的原始寄存器数据。
  5. 在OB1里调用4次MB_CLIENT指令,每个指令分配独立的背景DB,CONNECT参数分别绑定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( di=ModbusDataBlock.create([0]*100), co=ModbusDataBlock.create([0]*100), hr=ModbusDataBlock.create([0]*100), ir=ModbusDataBlock.create([0]*100) ) context = ModbusServerContext(slaves=store, single=True) StartTcpServer(context, address=("0.0.0.0", 502))

实际测试时,给保持寄存器写入几个特征值,比如12345、-6789,用来验证S7-1200读到的字节序和字序是否一致。仿真通过后,再去现场接入真实设备,能省下大量调试时间。

5. 现场排查实录与避坑速查

5.1 我踩过的五个坑

按出现频率排序,我把这次项目中真正踩过的坑总结成下面五条,每条都对应一个明确的处理动作。

  1. 地址错位:所有寄存器都按0偏移填写,导致读出来的是相邻地址的数据。处理办法是统一按“40001带区号”方式填写DATA_ADDR。
  2. 长度单位错:DATA_LEN按字节填写,缓冲区按字节数组定义,导致状态报错。处理办法是寄存器操作直接按“字”为单位,缓冲区用WORD数组。
  3. 32位字序颠倒:浮点数读出来数值异常。处理办法是用Modbus Poll确认设备字序,再做WORD交换和字节拼接。
  4. CONNECT里的ID重复:这个最隐蔽。四台设备复制同一个连接块,ID全部一样,导致部分设备连不上。处理办法是每个连接的ID全局唯一。
  5. 轮询不等待BUSY归零:REQ下降沿没做或下降沿没保持足够时间,导致请求被重复触发。处理办法是改成状态机,每台设备经历“触发-等待DONE/ERROR-等待BUSY归零”三个阶段。

5.2 STATUS错误码速查

MB_CLIENT执行异常时,STATUS会输出一个16进制错误码。不同的错误码对应不同方向,我没有办法把所有码都背下来,但常用的几个排查方向可以记一下:

STATUS常见方向优先排查项
16#80C8TCP连接建立失败IP地址、子网掩码、远程端口是否502
16#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规划好,每台设备一个固定编号。别小看这一步,越到后期越值钱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询