Modbus TCP通讯中的Unit ID之谜:一个字节导致的故障排查实录
前阵子跑现场处理一套“网口仪表PLC”的通讯问题。现象很典型网络通IP能ping通寄存器地址、波特率之类的老几样也全都核过但Modbus TCP的数据就是时好时坏偶尔还蹦出一个完全不可能的值。我在机柜前蹲了大半天网线、交换机、IP冲突挨个排最后发现罪魁祸首居然只是报文里一个最不起眼的字节——Unit ID单元标识符。这个字节就是标题里说的那个藏得最深的坑。今天我把这个坑从头到尾拆开讲透顺便把我在现场测过的各种设备行为、S7-1200轮询多台设备的配置记录、组态软件和触摸屏里对应的坑还有排查套路一起整理出来。搞Modbus TCP开发的工程师、做上位机的朋友、写触摸屏画面的电气工程师这篇文章应该都能用上。1. 先看懂Modbus TCP里那个“多余”的字节1.1 一份Modbus TCP报文到底长什么样Modbus TCP的报文结构比RTU简单核心就是在标准Modbus PDU外面套一个MBAP头。MBAP共7个字节事务处理标识符2字节、协议标识符2字节、长度2字节、单元标识符1字节。我随便写一条“读保持寄存器”的请求大家感受一下00 01 00 00 00 06 01 03 00 00 00 02字段拆开来看00 01事务处理标识符客户端生成服务器原样返回用来匹配请求和响应。00 00协议标识符Modbus协议固定为0。00 06长度表示后面还有6个字节Unit ID的01加PDU的03 00 00 00 02。01单元标识符Unit ID也就是本文的主角。03功能码读保持寄存器。00 00起始寄存器地址。00 02读取数量。注意这个长度字段它计的是“Unit ID PDU”的字节数不是整帧长度。很多第一次自己拼报文的人在这里算错后面所有解析全乱。我自己也在这上面栽过跟头。1.2 Unit ID从哪来RS-485时代留下的“历史包袱”Modbus最早是串口协议RS-485总线上挂着好多从站所以报文开头必须带一个从站地址用来区分“这条命令是发给谁的”。到了以太网时代Modbus TCP默认端口502一个TCP连接对应的就是一台具体设备IP地址已经把设备区分开了按理说不需要再从站地址了。但协议设计者为了让TCP客户端能通过一个连接去访问网关后面的多个串口从站就把RTU里的“从站地址”改名成了“单元标识符Unit ID”保留了下来。用法是客户端把Unit ID填成目标从站在串口侧的地址网关收到TCP请求后剥掉MBAP头把Unit ID写进RTU报文的地址字节再往对应的串口从站转发。问题就出在这。在直连以太网设备时Unit ID是“理论上多余”的于是不同设备厂商对这个字段的态度完全不一样有的直接忽略有的严格校验有的把它跟功能码搅在一起还有的网关用它做串口通道映射。这一层“历史包袱”没写进任何一本统一的手册就造成了后来的各种诡异步现象。1.3 为什么这个字节最容易藏坑因为它看起来“无关紧要”。排障的时候大家习惯于查IP、查端口、查寄存器地址、查功能码很少有人一上来就盯Unit ID。而且Modbus TCP的驱动配置界面里这个字段被放在各种五花八门的位置有的叫“站号”有的叫“设备地址”有的叫“从站编号”有的干脆藏在高级选项里默认值还各不相同。结果就是同一个上位机程序连A设备正常连B设备就是不通或者现场通讯“偶尔能用、经常超时”非常折磨人。我见过不止一个工程师排查到最后开始怀疑人生最后才发现就是驱动里那个默认的“1”跟设备要求的“255”对不上。2. 三种设备行为决定你会踩哪种坑2.1 完全忽略Unit ID的设备这类设备做Modbus TCP服务器时根本不在乎Unit ID填什么。你填0也好、1也好、255也好它都照常执行命令、正常返回。PLC的很多Modbus TCP功能块、部分以太网仪表、大多数仿真软件都是这个德行。这类设备用多了容易让人形成一个习惯性判断“Unit ID随便填不影响的。”这个判断一旦形成后面遇到严格校验的设备时排障方向就会跑偏。2.2 严格校验Unit ID的设备这一类以串口服务器、协议网关为典型。它们把TCP请求转换成串口RTU报文时必须把Unit ID映射成RTU从站地址所以收到Unit ID不等于后端从站地址的请求时有两种处理方式要么直接返回异常码“02”非法数据地址要么干脆不响应。举个例子你有一套RTU 485总线上面挂着5台仪表地址分别是1到5总线通过一个网关接到上位机。上位机访问2号仪表时Unit ID就必须填2。如果组态软件里的“站号”填的是默认1那1号仪表一切正常2到5号全部通讯失败。这就是“同一套配置换个从站就凉”的最常见原因。2.3 静默丢弃或映射怪异的设备比上面两种更阴的是第三种设备不按常理出牌。有些设备规定Unit ID必须为0有些规定必须为255有些只认低4位有些把Unit ID当作“数据通道号”还有些网关在配置了强制地址映射后会把Unit ID当成串口通道索引来用。这类设备的特点是不报错、不回异常码、也没有任何日志收到不匹配的请求直接装死表现就是“连接建立成功但读写总超时”。注意连接能建立不代表通讯正常因为TCP握手跟Modbus应用层是两码事。抓包一看请求发出去了对面就是安安静静不回应这种场景最容易让人误判成网络丢包或者防火墙问题。说个我自己的案例一套系统组态软件连A品牌变频器站号填1跑得好好的。后来变频器换成B品牌同样的IP、同样的功能码、同样的寄存器地址就是反复超时。我当时以为是变频器里Modbus参数没打开翻了一下午手册最后才发现B品牌要求Unit ID必须是255代表不限定从站而组态软件驱动配置里的默认站号还是1。改完通讯秒通。3. 实战复盘S7-1200轮询4台Modbus TCP设备的完整记录3.1 组态前的准备思路有一回做项目现场是一台S7-1200做Modbus TCP客户端轮询4台带以太网口的第三方设备。S7-1200从固件V4.0开始支持MB_CLIENT指令可以直接当主站用。博途里新建一个FB或者直接在OB1里调用然后给每台设备各建一个背景DB或者用一个DB复用。开始之前我习惯先把每台设备的Modbus参数表抄出来支持哪些功能码、寄存器地址范围、端口是不是默认502、Unit ID有没有要求。这一步看着啰嗦但能省掉后面大量现场调试时间。很多现场问题根本不是程序写得不对而是设备手册里的参数没看全。3.2 MB_CLIENT的UNIT_ID怎么填MB_CLIENT指令的引脚里有一个UNIT_ID这个就是Unit ID。很多教程里写“直连时填1就行”这句话害了不少人。正确的做法是看从站设备的要求直连第三方以太网设备大多数设备不校验填1可以但遇到严格要求255或0的设备必须按设备改。通过网关访问RTU从站Unit ID必须等于RTU从站地址比如访问2号仪表就填2访问3号就填3。如果4台设备里既有直连设备又有网关后面的串口仪表那不同设备的UNIT_ID可能完全不同千万别在程序里写死一个常量到处用。我的做法是建一个数组把每台设备的UNIT_ID、IP、寄存器映射、轮询周期都放在数据块里状态机轮到时按索引取参数。3.3 轮询节奏、连接资源与超时的配合S7-1200的MB_CLIENT有一个硬性限制同一时刻只允许一个实例处于激活状态。也就是说想轮询4台设备不能4个MB_CLIENT一起REQ必须做轮询状态机触发1号→等它完成或超时→再触发2号。否则会直接报错错误代码常见8184、8185。轮询周期也要控制好。MB_CLIENT的TIME_OUT参数默认1000毫秒如果现场有交换机、网关这类中间环节我一般会设到2000到3000毫秒。设太短设备响应稍慢就误判超时设太长4台设备轮一圈下来周期被拖得很大影响实时性。轮询周期里还得留出连接建立时间尤其是带网关的场景TCP连接断开后重新建立要额外耗时。另外REQ引脚建议用带时基的脉冲触发比如每200毫秒产生一个5Hz的上升沿而不是保持高电平。MB_CLIENT要求REQ上升沿触发一次请求如果REQ一直为TRUE同一个扫描周期里可能反复触发导致通讯混乱。3.4 我实际踩过的三次故障第一次是Unit ID写死。程序里一个CONNECT结构体被4台设备共用UNIT_ID全是1。1号设备正常2号设备要求255结果2号永远没响应。后来改成按设备索引取值才算解决。第二次是REQ保持高电平。我当时图省事直接把REQ接到了常ON上结果MB_CLIENT持续报错设备侧显示连接被频繁建立和断开。查了半天改成脉冲触发后一切正常。第三次是带网关的串口仪表。网关后面挂了3台仪表我在程序里把UNIT_ID填成PLC的站号结果3台仪表的地址映射全乱。后来对着网关说明书把UNIT_ID改成了各仪表的RTU地址才恢复正常。这三件事都不复杂但因为每个环节看起来都“没问题”排查时特别费时间。总结一句话S7-1200做Modbus TCP轮询多设备时Unit ID别写死REQ别保持超时要留余量。4. 组态软件和触摸屏背后的站号玄机4.1 KingSCADA连接Modbus TCP时容易忽略的设置用KingSCADA这类组态软件连Modbus TCP设备三个地方要看IP地址端口、设备地址站号、寄存器映射。不少工程师在设备配置界面看到“IP地址”就填了IP看到端口就填502唯独中间那个“设备地址/站号”留了默认值1。这个“设备地址/站号”在很多组态软件的Modbus TCP驱动里实际就是Unit ID。它跟寄存器地址没关系——寄存器地址是“40001”这类站号是“1、255”这类两者是不同维度的东西。如果通讯状态一直显示设备失败先别怀疑网线拉Wireshark抓包看看请求里的Unit ID到底是多少再跟设备手册核对。这里有个经验相同型号的设备和相同的驱动配置如果A厂区能用、B厂区不能用优先怀疑两个厂区设备固件版本或者设备配置参数不一致尤其是Unit ID的处理策略不一样。4.2 威纶通触摸屏新建工程时的“设备类”与元件地址威纶通触摸屏连Modbus TCP设备在“系统参数→设备→新增设备”里设备类选“MODBUS TCP/IP”然后会弹出一堆参数IP地址、端口、站号Station Number等。这个站号就是Unit ID默认1。很多人在这一步不看直接下一步等到画面上建元件地址时才发现问题。威纶通的元件地址格式是“4x-0001”这种前面的4x对应保持寄存器3x对应输入寄存器。如果从站数据在保持寄存器区元件地址却用成了3x读出来的数据自然不对。还有一个小坑威纶通的“32-bit”数据有字序设置如果从站返回的是浮点数还需要在“数据格式”里选择“32-bit Float”并确认高低字是否需要交换。这个放到下一章一起说。4.3 欧姆龙以太网通信单元与汇川AM系列Server编程的差异有朋友问过NX-CIF105这类通信单元怎么做Modbus TCP通讯。欧姆龙PLC本身的原生以太网协议是FINS不是Modbus TCP所以通常需要自己拼报文用Socket指令或通信单元来收发。自己拼报文的时候MBAP头里的Unit ID你填什么完全取决于对接的设备。很多例程里写的是“01”于是大家都照抄“01”碰到要求Unit ID0或255的从站就歇菜。汇川AM系列做Modbus TCP Server编程在InoProShop里配置好Modbus TCP服务器功能块之后重点同样在参数一致性上。实测AM系列的服务器功能块对Unit ID的校验行为在不同固件版本里有差异有的版本忽略Unit ID客户端随便填都能通讯有的版本则严格要求客户端填的值与功能块参数一致。遇到连不上的情况我一般建议在客户端侧把站号1、0、255各试一次通常就能试出来。如果都试不出来再回头查PLC功能块参数和IP端口。5. 第二个高频深坑寄存器字节序5.1 为什么读出来的浮点数像天文数字Unit ID排第一字节序我觉得能排第二它俩在现场出现的频率不相上下。现象是寄存器地址对了、功能码对了、数值也能读回来了但32位浮点数显示出来要么是几百万的天文数字要么是0.0000几要么是负数。原因是Modbus协议本身只规定了“字节传输顺序”是大端高字节在前但它没规定“两个16位字组成一个32位数据时哪个字在前”。于是各厂商就各搞一套有的高字在前有的低字在前有的字内还要交换字节。你按A厂商的习惯解析B厂商的设备数值自然会拧成一团。5.2 常见字节序排列和应对方法以32位浮点数占用两个寄存器四个字节为例常见排列有四种ABCD、CDAB、BADC、DCBA。实际项目里最常见的对立是ABCD和CDAB也就是高字在前还是低字在前。碰到数据不对先别改程序计算逻辑优先在触摸屏或组态软件里找“字序”“字节顺序”“Word Swap”这类选项。威纶通在元件的数据格式里有“Word Swap”可以勾KingSCADA等组态软件在变量类型里通常有“字节顺序”可选PLC作为主站时可以用SWAP指令把高低字交换之后再写进寄存器区。实在没有现成选项就在程序里手动做一次字交换把32位数据拆成两个字交换后写入Modbus地址区。注意交换的是“字”不是“字节”。很多新手在这里会把高低字节也拆开再拼回去结果越搞越乱。5.3 汇川AM系列做Modbus TCP Server的实测记录有一回我把汇川AM600配成Modbus TCP服务器给上位机提供一批32位浮点数据。上位机组态软件配置了“Float”类型地址也没错读出来却是乱码。后来在PLC程序里把提供到Modbus地址区的数据做了一次高低字交换上位机再读就正常了。说明AM系列寄存器区里32位数据的字序和上位机默认的字序不一致只是软件层面没暴露出来。这个问题的本质还是“字序对齐”只不过位置不在上位机而在Server侧的寄存器映射。遇到这种问题最快的方式是按“哪边有字序设置就设哪边”的原则处理两边都支持的话任选一边调整即可。6. 现场排查套路三步定位Unit ID问题6.1 先抓包看Unit ID的真实值现场一台笔记本电脑、一个Wireshark就能快速判断问题范围。抓包时过滤条件写tcp.port 502然后找一条客户端发出的Modbus请求看MBAP头里的Unit ID字段实际是多少再看服务器有没有返回异常码。如果请求里的Unit ID是1但设备要求是255那问题就锁定在上位机/PLC的驱动配置里不用再怀疑网线、交换机。如果在请求里看到的Unit ID是对的设备依然不响应那就需要进一步查网关映射、防火墙规则和设备日志。抓包还能看出TCP连接是否有反复重建的迹象——如果请求重发但每次都新建连接、或者连接数被占满多半是连接管理逻辑有问题而不是Unit ID的问题。6.2 再顺链路检查驱动侧与网关侧抓包确认Unit ID异常后检查顺序是驱动侧配置 → 网关映射 → 设备侧参数。驱动侧最常见的就是站号/设备地址/Unit ID这类字段填了默认值。网关侧要区分是透明转发还是协议转换很多串口服务器默认会把Unit ID透传到串口帧里但有些产品固件版本不同处理方式也不一样。设备侧则要重新翻手册确认它在Modbus TCP服务器模式下到底校验Unit ID还是忽略Unit ID。三步走完绝大部分Unit ID问题都能锁定。6.3 附带一份避坑速查表现象可能原因排查顺序能ping通但读写失败Unit ID/站号不匹配、端口不是502、功能码不支持抓包确认Unit ID→查端口→查设备文档通讯偶尔失败TCP连接资源被占满、超时时间过短、Unit ID映射不稳定抓包看连接数→加长TIME_OUT→核对网关映射数据读出来完全不对寄存器地址错、功能码用错03和04混了、字节序不对核对地址映射→确认功能码→调整字序上电后长时间连不上从站启动慢、主站重试间隔太短加长延时→确认网关重启时间→错开整体轮询时序换一台同型号设备就失败设备固件版本不同、参数没做一致性配置对比两台设备的Modbus参数→核对Unit ID策略表格里的这些情况我在现场都遇到过不能说100%都是Unit ID引起的但“能ping通但读写失败”和“换台设备就失败”这两条十个里有七个跟Unit ID有关。先把这一项排掉再往下查效率会高很多。说实话Unit ID这个坑之所以难缠是因为它在每一层看起来都“没毛病”从TCP层看连接是通的从IP层看地址是对的从寄存器层看地址是核对过的唯独中间这个一字节的Unit ID各家设备定义不同、驱动界面名字千奇百怪、默认值五花八门问题出现时又往往不报错、不提示全靠人一点点试。我自己现在已经养成一个习惯凡是接到Modbus TCP项目开工第一件事就是把所有从站的Unit ID要求写进项目文档明确标出“该设备是忽略型、校验型还是映射型”。这个动作花不了十分钟但后面调试时能少掉好几根头发。最后再分享一个小技巧如果现场实在查不动了把驱动里的站号按0、1、255依次试一遍再配合Wireshark抓包看响应大概率能绕过这个坑。技多不压身下次遇到Modbus TCP通讯诡异不稳的时候希望你也能想起这个一字节的“隐藏Boss”。