ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ABB机器人与PLC的ModbusTCP通信:Float拆分寄存器与字节序实战

2026/10/5 6:41:07 拓冰建站 浏览量
ABB机器人与PLC的ModbusTCP通信:Float拆分寄存器与字节序实战 做机器人项目的人应该都有过这种经历现场调试到一半甲方突然要求把机器人的坐标、扭矩、报警码实时传到PLC的触摸屏上。ABB机器人本身有Profinet和Profibus的选件板但仓库没货、交期太长或者柜子里根本没有预留扩展槽位。这时候ModbusTCP就成了最快能落地的方案——一根网线、一个IP、几行RAPID程序就能把数据塞进PLC的保持寄存器。但是有个坎绕不过去Modbus寄存器是16位的而Float是32位的。怎么把Float拆成两个16位寄存器送过去、在PLC里再拼回来是新手最容易卡住的地方。这篇文章把完整实现过程拆开讲包括ModbusTCP报文结构、ABB RAPID代码、西门子S7-1200/1500侧配置以及我现场踩过的高字低字、字节序、地址偏移的坑。适合自己写过一点机器人或PLC、但没完整跑通过ModbusTCP通信的人也适合那些临时被拉去现场救火的电气工程师——照着这篇文章抄基本能把链路调通。1. 项目背景与方案选型为什么最终选了ModbusTCP1.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总线存量系统ModbusTCP10~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事务处理ID2字节每次请求自增用于匹配请求和响应2协议ID2字节ModbusTCP固定为0x00004长度字段2字节表示后面还有多少字节单元ID PDU长度6单元ID1字节从站地址通常为1PDU部分就是我们常说的功能码加数据。用写多个保持寄存器的FC16举例请求报文结构如下字节偏移字段长度说明7功能码1字节0x10十进制168起始地址2字节要写的第一个保持寄存器地址0对应4000110寄存器数量2字节写几个寄存器这里写2个12字节数1字节数据区总字节数此处为413寄存器数据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低字第二个寄存器0xE979PLC收到这两个寄存器后把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里叫REALPython/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.10PLC 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; ! PDUFC16写多个寄存器 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侧配置接收并还原Float4.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参数DISCONNECTFALSE表示一直监听不主动断开CONNECT_ID1连接ID自己定义IP_PORT502ModbusTCP默认端口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落到40002RawDWord拼出来就是0x42F6E979FloatValue自动变成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输出还有一个NDRNew 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(hrPrintDataBlock(0, 100), zero_modeTrue) context ModbusServerContext(slavesstore, singleTrue) 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或EtherCATModbusTCP的循环周期和抖动都不够看。这是方案阶段就要想清楚的事别等项目上线了再头疼。还有一个跟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、几组坐标和姿态数据都是同一套代码抄来抄去的事真正做过一遍就通了。