做机器人项目的人应该都有过这种经历:现场调试到一半,甲方突然要求把机器人的坐标、扭矩、报警码实时传到PLC的触摸屏上。ABB机器人本身有Profinet和Profibus的选件板,但仓库没货、交期太长,或者柜子里根本没有预留扩展槽位。这时候ModbusTCP就成了最快能落地的方案——一根网线、一个IP、几行RAPID程序,就能把数据塞进PLC的保持寄存器。但是有个坎绕不过去:Modbus寄存器是16位的,而Float是32位的。怎么把Float拆成两个16位寄存器送过去、在PLC里再拼回来,是新手最容易卡住的地方。
这篇文章把完整实现过程拆开讲,包括ModbusTCP报文结构、ABB RAPID代码、西门子S7-1200/1500侧配置,以及我现场踩过的高字低字、字节序、地址偏移的坑。适合自己写过一点机器人或PLC、但没完整跑通过ModbusTCP通信的人,也适合那些临时被拉去现场救火的电气工程师——照着这篇文章抄,基本能把链路调通。
1. 项目背景与方案选型:为什么最终选了ModbusTCP
1.1 这个项目到底要做什么
我碰到的项目是一个机器人上下料工作站,ABB机器人负责从料台抓取工件放到机床上,加工完再取回来。甲方要求把机器人的状态实时显示在HMI上,包括当前TCP坐标(X、Y、Z)、姿态四元数、六个关节角度、当前报警代码,以及“正在抓取”、“加工中”、“已完成”这些任务状态标志。
数据量其实不大,加起来十几个Float加几个Bool,总共不超过六七十个字节。实时性要求也不是秒级毫秒级,100ms刷新完全够用。难点在于ABB机器人不是西门子生态内的设备,没有现成的Profinet IO方案摆在柜子里,甲方又不愿意等选件板到货。这种情况下,ModbusTCP几乎是唯一一个“零额外硬件成本”的选择——机器人控制柜上本身就有以太网口,西门子PLC也标配网口,两边配个IP就能通信。
1.2 几种通信方案的对比与取舍
我在方案阶段其实认真比过几种常见路子,这里直接给大家一张对比表,方便后续选型时参考。
| 方案 | 实时性 | 额外硬件 | 编程难度 | 适用场景 |
|---|---|---|---|---|
| Profinet IO | 毫秒级,强实时 | 需要机器人侧Profinet选件板/网关 | 中等,需GSD文件 | 运动控制、安全信号、高速数据交换 |
| Profibus DP | 毫秒级 | 需要机器人侧DP从站选件板 | 中等,需GSD文件 | 老柜子、DP总线存量系统 |
| ModbusTCP | 10~100ms级,软实时 | 无,两根网线即可 | 低,Socket编程或现成指令 | 坐标监控、状态上报、报警记录、参数下发 |
| 硬接线IO | 最快,微秒级 | 大量线缆、IO模块 | 低 | 急停、门联锁、简单启停信号 |
最终选了ModbusTCP,理由很直接:第一,不需要额外硬件,仓库里找两根超五类网线就能开工;第二,ABB的RAPID语言自带Socket指令,不用装任何选项包;第三,西门子S7-1200/1500从固件4.0开始原生支持ModbusTCP服务器,MB_SERVER指令拖出来就能用。Profinet实时性确实好,但光一个机器人侧选件板就得等一个月,现场等不起。这个项目的数据也就是在HMI上显示一下,ModbusTCP完全够用。
2. ModbusTCP协议与Float数据模型:先把底层看明白
2.1 ModbusTCP报文结构
写RAPID代码之前,我建议先把ModbusTCP的报文在纸上画一遍。这东西不难,就是固定格式的字节流,一次通信包含一个请求帧和一个响应帧。
ModbusTCP请求帧由MBAP Header加PDU组成。MBAP Header固定7个字节:
| 字节偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | 事务处理ID | 2字节 | 每次请求自增,用于匹配请求和响应 |
| 2 | 协议ID | 2字节 | ModbusTCP固定为0x0000 |
| 4 | 长度字段 | 2字节 | 表示后面还有多少字节(单元ID + PDU长度) |
| 6 | 单元ID | 1字节 | 从站地址,通常为1 |
PDU部分就是我们常说的功能码加数据。用写多个保持寄存器的FC16举例,请求报文结构如下:
| 字节偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 7 | 功能码 | 1字节 | 0x10(十进制16) |
| 8 | 起始地址 | 2字节 | 要写的第一个保持寄存器地址,0对应40001 |
| 10 | 寄存器数量 | 2字节 | 写几个寄存器,这里写2个 |
| 12 | 字节数 | 1字节 | 数据区总字节数,此处为4 |
| 13 | 寄存器数据 | 4字节 | 两个寄存器各占2字节,按大端排列 |
所以一个写单个Float的完整请求帧总共17字节。MBAP里的长度字段是单元ID 1字节加PDU 10字节,也就是11。很多人第一次算长度字段会出错,计数器从第7个字节开始数就行。
2.2 寄存器地址与功能码
Modbus里最常用的是保持寄存器(Holding Register),编号从40001开始,但报文中的起始地址是0-based,也就是说地址0对应PLC上显示的40001。这个偏移搞反了,数据就会跑到相邻的寄存器上,现场排查时特别容易困惑。
本项目用到的功能码其实就两个:FC06写单个寄存器、FC16写多个寄存器。读的话用FC03。很多老工程师的习惯是用FC06一个寄存器一个寄存器地写,写Float就发两次FC06,先写高字再写低字。这种做法能通,但不够优雅,现场也容易踩坑。我更推荐直接用FC16,一次把两个寄存器写掉,后面会细说原因。
2.3 Float在寄存器里怎么存:IEEE 754和字节序
这是全文最核心的问题:Modbus寄存器只有16位,Float是32位,怎么塞进去?
答案是拆成两个寄存器。一个32位Float在内存里是4个字节,两个寄存器正好4个字节,一个寄存器存高16位,一个存低16位。
拿123.456这个数举例。按照IEEE 754标准,123.456用32位浮点表示,十六进制是0x42F6E979,拆成两半就是:
- 高字(第一个寄存器):
0x42F6 - 低字(第二个寄存器):
0xE979
PLC收到这两个寄存器后,把0x42F6E979按IEEE 754规则还原,就得到123.456。这里必须强调字节序问题。Modbus协议规定数据按大端(Big-Endian)传输,也就是高字节在前。ABB机器人RAPID的PackRawBytes默认也是大端,所以正常写出来就是42 F6 E9 79这么一排字节。
如果哪个环节的字节序反了,比如PLC解析时按E9 79 42 F6处理,出来的数就完全不是123.456,而是一个接近2.33e-38的极小值,很多时候还会直接变成NAN。这是我见过最多的Float传输错误,没有之一。
关于“float和real”这两个叫法,这里顺便说一下:ABB的RAPID里叫num,西门子PLC里叫REAL,Python/C++里叫float,它们底层都是IEEE 754单精度浮点数,本质上是一个东西。如果传双精度(ABB的dnum、西门子的LREAL),那是64位,需要4个寄存器,方法一模一样,只是数据区从4字节变成8字节。
3. ABB机器人侧实现:RAPID代码拆解
3.1 通信环境准备
写代码之前,先把网络环境搭好。ABB机器人控制柜的网口可以在示教器的“控制面板—配置—Communication—Industrial Network”里设置IP,或者直接在RobotStudio里配置。机器人和PLC要处于同一网段,比如机器人192.168.0.10,PLC 192.168.0.10这种是不行的,要不同IP。
PLC侧建议固定IP,不要用DHCP,因为机器人重启之后如果PLC地址变了,SocketConnect会失败。端口默认是502,除非PLC侧改了端口,否则机器人侧就用502。
还需要确认RAPID的Socket指令可用。ABB的IRC5和OmniCore控制柜都自带SocketCreate、SocketConnect、SocketSend、SocketReceive这些指令,属于标准指令集,不需要额外购买选项包。如果用的是很老的S4C或者S4C Plus控制器,那就得另想办法,但市面上存量不多了,这里不展开。
3.2 发送Float的完整代码
下面直接给出我项目里用的完整RAPID模块。这个模块做了三件事:连接PLC、发送一个Float到指定起始地址、自动处理超时和断线重连。
MODULE ModbusTCPFloat CONST num MBT_PORT := 502; VAR socketdev mbt_socket; VAR num mbt_trans_id := 1; VAR bool mbt_connected := FALSE; PROC MBT_Connect() IF mbt_connected THEN RETURN; ENDIF SocketCreate mbt_socket; SocketConnect mbt_socket, "192.168.0.10", MBT_PORT; mbt_connected := TRUE; ENDPROC PROC MBT_Disconnect() IF mbt_connected THEN SocketClose mbt_socket; mbt_connected := FALSE; ENDIF ENDPROC ! 写一个Float到起始地址start_addr ! start_addr是寄存器的0-based地址,对应PLC的40001 PROC MBT_WriteFloat(num start_addr, num float_value) VAR raw_packet{17}; VAR bytes response{8}; VAR num payload_len := 11; IF NOT mbt_connected THEN MBT_Connect; ENDIF ! MBAP Header PackRawBytes mbt_trans_id, raw_packet, 1, \INT; PackRawBytes 0, raw_packet, 3, \INT; PackRawBytes payload_len, raw_packet, 5, \INT; PackRawBytes 1, raw_packet, 7, \BYTE; ! PDU:FC16写多个寄存器 PackRawBytes 16, raw_packet, 8, \BYTE; PackRawBytes start_addr, raw_packet, 9, \INT; PackRawBytes 2, raw_packet, 11, \INT; PackRawBytes 4, raw_packet, 13, \BYTE; PackRawBytes float_value, raw_packet, 14, \FLOAT; ! 发送请求 SocketSend mbt_socket \RawData:=raw_packet; ! 接收响应并丢弃内容(8字节) SocketReceive mbt_socket \RawData:=response \Time:=5; mbt_trans_id := mbt_trans_id + 1; IF mbt_trans_id > 65535 THEN mbt_trans_id := 1; ENDIF ERROR IF ERRNO = ERR_SOCK_TIMEOUT THEN TPWrite "MBT write timeout: check PLC Modbus server"; mbt_connected := FALSE; ELSEIF ERRNO = ERR_SOCK_CLOSED THEN TPWrite "MBT connection closed, retry next cycle"; mbt_connected := FALSE; ENDIF ENDPROC ENDMODULE这段代码里最关键的几行是PackRawBytes调用。PackRawBytes会把一个值按指定类型打包成字节,\INT是2字节有符号整数,\BYTE是1字节,\FLOAT是4字节浮点数。RAPID的PackRawBytes默认按大端处理,正好符合ModbusTCP的字节序要求。
raw_packet数组大小是17,对应前面算出来的完整请求帧长度。response数组大小是8,因为FC16的正常响应帧固定是8字节:事务ID 2字节、协议ID 2字节、长度字段2字节、单元ID 1字节、功能码1字节,后面没有数据。这里接收响应主要是为了把TCP缓冲区里的数据读掉,避免影响下一次请求的匹配。
3.3 封装成通用模块
单发一个Float够用了,但项目里往往要发十几个数据。不建议在机器人主程序里把PackRawBytes和SocketSend铺得到处都是,封装成通用函数是更好的做法。
我一般会把MBT_WriteFloat扩展成MBT_WriteFloatArray,一次写连续多个Float到PLC的连续寄存器区。核心改动就是报文里的寄存器数量变成2 * n,字节数变成4 * n,然后在PDU的数据区按顺序循环PackRawBytes多个Float即可。
PROC MBT_WriteFloatArray(num start_addr, num values_array{*}) VAR num n := Dim(values_array, 1); VAR raw_packet{1}; VAR bytes response{8}; VAR num i; VAR num payload_len := 7 + 4 * n; VAR num offset := 14; ! 注意:raw_packet要按实际长度动态处理 ! 这里为了演示,写死了最大情况 ! 实际项目中可以先计算好数组大小再定义 IF NOT mbt_connected THEN MBT_Connect; ENDIF ! 重新定义数组大小 raw_packet := [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; PackRawBytes mbt_trans_id, raw_packet, 1, \INT; PackRawBytes 0, raw_packet, 3, \INT; PackRawBytes payload_len, raw_packet, 5, \INT; PackRawBytes 1, raw_packet, 7, \BYTE; PackRawBytes 16, raw_packet, 8, \BYTE; PackRawBytes start_addr, raw_packet, 9, \INT; PackRawBytes 2 * n, raw_packet, 11, \INT; PackRawBytes 4 * n, raw_packet, 13, \BYTE; FOR i FROM 1 TO n DO PackRawBytes values_array{i}, raw_packet, offset, \FLOAT; offset := offset + 4; ENDFOR SocketSend mbt_socket \RawData:=raw_packet; SocketReceive mbt_socket \RawData:=response \Time:=5; mbt_trans_id := mbt_trans_id + 1; IF mbt_trans_id > 65535 THEN mbt_trans_id := 1; ENDIF ERROR IF ERRNO = ERR_SOCK_TIMEOUT THEN TPWrite "MBT array write timeout"; mbt_connected := FALSE; ENDIF ENDPROC主程序里调用就非常简单了:
VAR num robot_data{6} := [123.456, 30.5, -45.2, 10.1, 0.0, 88.8]; MBT_Connect; WHILE TRUE DO robot_data{1} := CPos( \Tool:=tool_1 ).trans.x; ! 实际坐标值 robot_data{2} := CPos( \Tool:=tool_1 ).trans.y; MBT_WriteFloatArray 0, robot_data; WaitTime 0.1; ! 100ms刷新一次 ENDWHILE间距保持100ms刷新,对于HMI显示完全够用,ModbusTCP的负载也很小。如果需要涂胶轨迹那种高实时性同步,那不该用ModbusTCP,而是老老实实上Profinet,这个后面再展开聊。
3.4 为什么用FC16而不是FC06
有朋友会问:一个Float拆两个寄存器,我写两次FC06不就行了,为什么要用FC16?
理论上能行,但实际项目里有三个问题。第一,两次FC06是两个独立事务,PLC在两次写入之间有可能会扫描到中间状态——高字是新的,低字还是旧的,拼出来的Float就是一个错误值。如果PLC侧恰好在那个瞬间去读取数据,显示就会出现毛刺。第二,两次独立事务的时序无法保证,如果网络抖动导致第一次成功第二次失败,数据就永久错位了。第三,FC16的报文结构并不复杂,一次写2个寄存器和写1个寄存器相比,只多了几个字节的开销,完全没必要省。
我在现场见过一个案例,前一位工程师用FC06写Float,结果PLC触摸屏上的数值每隔一会儿就会跳一个接近NAN的乱码。后来改成FC16,问题再也没出现。所以能用FC16就尽量用FC16,尤其涉及浮点数这种“整体性”数据的时候,一次事务搞定是最稳的。
4. 西门子PLC侧配置:接收并还原Float
4.1 TIA Portal中开启MB_SERVER
机器人侧发数据,PLC侧就要开一个ModbusTCP服务器。在西门子S7-1200/1500里,这条指令叫MB_SERVER,位于“通信—开放式用户通信”库中。
具体步骤如下:
- 打开TIA Portal,添加S7-1200或S7-1500 CPU,设置PLC的IP地址(必须和机器人同一网段,且固定IP)。
- 在左侧项目树的“程序块”里新建一个全局数据块,比如命名为“ModbusData”,用于存放保持寄存器数据。
- 在OB1(主组织块)中拖入MB_SERVER指令。
- 配置MB_SERVER参数:
- DISCONNECT:FALSE,表示一直监听,不主动断开
- CONNECT_ID:1,连接ID,自己定义
- IP_PORT:502,ModbusTCP默认端口
- MB_HOLD_REG:指向数据块中保持寄存器区域的指针
- 下载程序到PLC,激活后PLC就监听502端口了。
这里最容易出问题的是MB_HOLD_REG的指针定义。在S7-1200中,MB_HOLD_REG需要一个指向Word类型数组起始地址的指针。很多人第一次搞,会直接把一个普通的DB变量填进去,结果PLC报错。正确的做法是先在DB里定义一个Word数组,再把数组的第一个元素的地址填给MB_HOLD_REG。
4.2 数据区设计与AT覆盖法
现在到了接收Float最关键的一步:PLC怎么把两个Word还原成Real。
我在项目里最常用的方法是AT覆盖法。在S7-1200/1500的全局DB里,定义一个DWord变量和一个Real变量,然后用AT让它们共享同一个地址。这样Modbus写进来的两个寄存器会填充到DWord的地址空间,Real变量自动就能读到同一个位模式。
TIA Portal里AT覆盖声明的写法是这样的:
ModbusData RawDWord : DWord; FloatValue : Real AT RawDWord;注意AT覆盖在TIA里有一些限制,比如不能直接在数组元素上用AT,不能跨DB访问,这些用的时候查一下软件报错就行。但上面这种变量级AT覆盖在1200/1500上是支持的。
从Modbus寄存器映射关系看:
- PLC的MB_HOLD_REG指向RawDWord的起始地址
- 寄存器40001(地址0)映射到RawDWord的高16位
- 寄存器40002(地址1)映射到RawDWord的低16位
机器人侧发出的高字0x42F6会落到40001,低字0xE979落到40002,RawDWord拼出来就是0x42F6E979,FloatValue自动变成123.456。整个过程不需要写SCL转换逻辑,一个AT声明全搞定。
如果不想用AT,也可以用两个Word加一个SCL函数做组合:
#rawDWord := SHL(INT_TO_DWORD(#highWord), 16) OR INT_TO_DWORD(#lowWord); #floatValue := #rawDWord; // 或者用MOVE指令做位拷贝但这种方式要写代码、建临时变量,还要处理符号扩展问题,比AT覆盖麻烦。我在三个项目里都用AT,稳定、直接、省代码。
4.3 启动顺序与状态监控
通信调试时,启动顺序非常重要。我的经验是:先启动PLC的ModbusTCP服务器,等PLC程序跑起来监听502端口,再启动机器人的通信程序。如果机器人先跑起来,SocketConnect会连不上,RAPID代码里的ERROR处理就会触发,但要注意,如果错误处理逻辑不完善,机器人程序可能会卡在SocketConnect上,导致后续动作全部停掉。
建议在主程序里做一个连接状态监控。MB_SERVER指令有一个STATUS输出,还有一个NDR(New Data Received)输出。NDR每收到一次新数据就会置位一个扫描周期,可以用来做一个“通信心跳”光亮相PLC侧。如果心跳长期不亮,说明链路断了。
PLC侧还可以在DB里放一个Bool变量“CommActive”,每次收到NDR就置位TRUE,然后隔固定时间复位。这样HMI上就能显示通信是否正常,排查起来非常直观。
4.4 没有PLC怎么验证:Python模拟器
很多时候现场PLC程序还没写好,但机器人侧可以先调试。这时候用Python写一个简单的ModbusTCP服务器,充当PLC的角色,能在最短时间内验证机器人的RAPID代码是否正确。
用pymodbus库就能实现。先安装依赖:
pip install pymodbus然后写一个带打印功能的模拟服务器:
from pymodbus.server import StartTcpServer from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext from pymodbus.datastore import ModbusSequentialDataBlock class PrintDataBlock(ModbusSequentialDataBlock): """重写setValues方法,在数据写入时打印内容""" def setValues(self, address, values): super().setValues(address, values) # 将两个16位寄存器拼成32位Float if len(values) >= 2: b = bytes([ (values[0] >> 8) & 0xFF, values[0] & 0xFF, (values[1] >> 8) & 0xFF, values[1] & 0xFF, ]) import struct f = struct.unpack('>f', b)[0] print(f"[0x{address:04X}] -> {hex(values[0])} {hex(values[1])} -> {f}") store = ModbusSlaveContext(hr=PrintDataBlock(0, 100), zero_mode=True) context = ModbusServerContext(slaves=store, single=True) print("ModbusTCP server started on port 502...") StartTcpServer(context, address=("0.0.0.0", 502))运行这段脚本后,机器人只要向这个服务器写数据,终端就会直接打印Float的还原值。我在项目调试时就用这个办法,在PLC程序编好之前,已经确认机器人侧发送的报文和字节序完全正确,等PLC一上电,数据直接对上。
5. 现场问题排查:高低字、字节序与连接稳定性
5.1 问题速查表
现场跑通信,十次有八次要踩同样的坑。我把自己遇过的典型问题整理成了一张速查表,排查速度最快的时候,照着这个表十分钟定位问题。
| 现象 | 可能原因 | 解决措施 |
|---|---|---|
| PLC里收到0 | 机器人没执行发送、起始地址错误、字节序拼出来恰好是0 | 先看机器人侧SocketSend是否执行;用ModbusPoll读原始寄存器值 |
| PLC里数值是一个几十万的大数 | 高低字顺序反了,两个寄存器的位置互换 | 在PLC侧交换寄存器高低字,或机器人侧调换写寄存器顺序 |
| 显示NAN | 字节序完全颠倒,或者数据根本不是Float | 用Python将寄存器值struct.unpack('>f')验证;检查功能码是否正确 |
| 第一次能通,之后再也收不到 | Socket连接断开后没有重连 | 在ERROR处理里加重连逻辑,断开后Sleep再SocketCreate |
| 只有一个数据能收,第二个永远不对 | 机器人侧地址计算错误,起始地址偏移了 | 确认机器人写的起始地址和PLC的MODBUS_HOLD_REG偏移一致 |
| 上电重启后通信恢复不了 | 机器人程序启动顺序问题,SocketConnect早于PLC监听 | 机器人侧增加重试机制,或PLC先上电运行再启动机器人程序 |
| 通信偶尔断,且无规律 | 交换机端口协商问题、网线质量差 | 固定通信两端网口为百兆全双工,换好的工业网线 |
5.2 高低字反了怎么判断
高低字反了是Float传输头号问题,而且特别隐蔽。我教大家一个特别简单的判断方法:让机器人发送一个固定的已知数,比如123.456,然后去PLC里读寄存器原始值。
如果是123.456,它的十六进制是0x42F6E979。正常情况寄存器1应该是0x42F6,寄存器2应该是0xE979。如果读出来寄存器1是0xE979、寄存器2是0x42F6,那就是高低字反了。
现场没有ModbusPoll的时候,用Python也能快速验证。拿到寄存器原始值,比如寄存器1读出来是0xE979、寄存器2读出来是0x42F6,执行下面这段就能看到错在哪里:
import struct # 假设从PLC读到的两个寄存器原始值 reg1 = 0xE979 reg2 = 0x42F6 # 正常顺序 byte_normal = struct.pack('>HH', reg1, reg2) print("正常顺序解析:", struct.unpack('>f', byte_normal)[0]) # 如果顺序反了 byte_reversed = struct.pack('>HH', reg2, reg1) print("反序解析:", struct.unpack('>f', byte_reversed)[0])看到“反序解析”出来的值是个极小值,基本就能确定是高低字问题。修法很简单:机器人侧在写寄存器时把高字和低字交换,或者在PLC侧把两个Word变量交换再拼DWord,二选一。
5.3 连接断开与重连策略
ModbusTCP是TCP长连接,PLC重启、网线松动、机器人程序热启动,都可能导致连接断开。RAPID的Socket指令在连接断开后,如果还用原来的socketdescriptor发送数据,会直接报错。最稳妥的做法是:一旦发送失败或超时,立即标记mbt_connected := FALSE,下次调用时重新SocketCreate和SocketConnect。
重连时有一个细节:不要发得太快。PLC重启可能需要10到30秒,如果机器人每100ms就尝试重新连接,PLC日志会刷出一堆“连接失败”告警,而且频繁的连接尝试也会占用PLC资源。我的习惯是重连失败后WaitTime 2,等2秒再试。
另外一个坑是RAPID的SocketConnect本身也会超时。如果PLC没开机,SocketConnect会阻塞在那里,机器人主程序直接卡住。解决方法是把连接过程放到一个独立的任务里,或者用SocketConnect \Time:=5这样的超时参数控制。RAPID的SocketConnect是否支持超时参数跟系统版本有关,建议查一下当前版本手册;如果不支持,可以通过后台task连接,主程序继续执行其他逻辑。
5.4 通信周期与扫描周期匹配
ModbusTCP是典型的主从请求响应模式,机器人作为客户端发请求,PLC作为服务器响应。这里有一个需要理解的机制:PLC的OB1扫描周期和通信请求的到达不一定同步。
如果OB1扫描周期是10ms,而机器人每100ms发一次数据,那么PLC有些扫描周期能收到新数据,有些扫描周期收到的还是旧数据。对于HMI显示来说完全没问题,但如果用这个Float做PID控制或精确同步,就不行了。
根据我的经验,ModbusTCP适合做100ms以上周期、非控制类的数据交换,比如设备状态、坐标监控、报警信息、配方参数。如果要做运动同步、高速位置控制、实时力矩限制,老老实实上Profinet或EtherCAT,ModbusTCP的循环周期和抖动都不够看。这是方案阶段就要想清楚的事,别等项目上线了再头疼。
还有一个跟PLC扫描周期有关的注意点:如果机器人写数据的频率比PLC扫描快,PLC读取MB_HOLD_REG对应的DB变量可能读到“一半更新”的状态。虽然AT覆盖法在物理上是同一个32位地址,理论上读写原子性还行,但在S7-1200上,如果程序逻辑不是同一个OB里读完再处理,还是可能出现微小错位。稳妥做法是PLC侧在做数据快照上传HMI时,用一次MOVE把FloatValue转到一个临时变量再使用。
最后再分享一个小技巧
我每次做这类通信项目,第一次联调时都不会急着传真实坐标。手动给机器人固定一个已知常数,比如123.456,发到PLC,然后在PLC里监控收到的Real值是不是正好123.456。不是的话,先检查寄存器顺序和字节序,而不是去怀疑网络、怀疑模块、怀疑人生。这个习惯帮我省了非常多排查时间。
另外,如果现场有条件,带上ModbusPoll这个软件,免费版就够用。把ModbusPoll指向PLC的IP和502端口,手动写两个寄存器,发0x42F6和0xE979,马上就能从PLC端确认链路通不通。很多问题其实一两分钟就能定位,但前提是手里有一个趁手的调试工具。这套东西跑通之后,后面再传几十个Float、几组坐标和姿态数据,都是同一套代码抄来抄去的事,真正做过一遍就通了。