做倍福PLC的工程师,迟早会遇到这么一件事:客户现场的SCADA、上位机、或者别的品牌PLC,要跟你的TwinCAT3系统通数据。协议五花八门,但十有八九会先要求你开放一个Modbus-TCP Server口。我最初也被这句"你们倍福能不能做从站?"问住过,后来把流程完整走了一遍,才发现它并没有想象中复杂。这篇博文就是我从零把一个倍福PLC配成Modbus-TCP Server的完整过程,包含环境准备、程序编写、仿真调试、常见坑点,适合刚接触TwinCAT3、或者还没怎么碰过通讯功能的人照着做。
1. 整体思路与方案选型
1.1 为什么要在倍福PLC上做Modbus-TCP Server
很多人有个误区:倍福PLC的强项是运动控制、EtherCAT、实时以太网,觉得它跟传统Modbus设备打交道会很麻烦。实际上,TwinCAT3通过附加的TF6250、TF6255通信函数库,既能做Modbus-TCP Master,也能做Server,而且库函数封装得相当干净。
实际工程里最常见的场景是这三种:
- 第三方SCADA(WinCC、组态王、力控等)通过Modbus-TCP读取倍福PLC的数据点。
- 另一个品牌的PLC(比如三菱FX5U、西门子S7-1200)作为Master,把倍福PLC当远程IO站用。
- 现场触摸屏、DTU、边缘网关这类设备,普遍原生支持Modbus-TCP,想直连控制器。
这三种场景都指向同一个需求:让倍福PLC提供一个标准Modbus-TCP服务端,外部设备通过功能码读写寄存器。这就是本文要解决的核心问题。
1.2 硬件、软件和许可证清单
先说结论:做这套东西,不一定要有真实PLC,TwinCAT3支持本地仿真模式,一台普通Windows电脑就能把Server跑起来。需要的软件和条件如下:
- TwinCAT3开发环境,建议装3.1.4024.xx以上的版本,老版本对TF6255的支持和新版本稍有差异。
- 许可证:TF6250(Modbus-TCP Master)和TF6255(Modbus-TCP Slave/Server)。没有正式授权时,TwinCAT3提供7天试用模式,足够做开发验证。
- 如果接真实PLC,需要倍福的CX系列工控机或带板载网卡的PC,安装TC3实时扩展,并配置好ADS路由。
- 调试工具:Modbus Poll(Windows)、Python的pymodbus库,或者直接用ModScan。
这里要特别提醒:很多人装完TwinCAT3后找不到TF6255,是因为许可证类型没选对。在TwinCAT3环境里,如果你没有TF6255的试用授权,程序能编译通过,但运行时会报"license not found"之类的错误。所以动手之前,先把许可证检查一遍,这是最容易忽略的第一步。
2. 环境搭建与项目初始化
2.1 装好TwinCAT3并激活试用许可
我见过太多人卡在第一步:装好TwinCAT3之后,打开工程提示许可证不可用。在开始做Modbus-Server之前,先确认环境是健康的。
安装过程这里不展开,只说几个要点:
- 安装路径不要带中文,不要装在C盘Program Files以外的奇怪位置,否则实时驱动和路由配置容易出问题。
- 安装完成后,系统托盘会有TwinCAT图标,右键选择Router,确认本机AMS NetId能正常显示。如果没有显示,需要先以管理员权限运行TcXaeShell,再启动Router服务。
- 没有许可证时,在TwinCAT System Manager里可以看到7天试用剩余时间。这个试用会覆盖大部分通讯库,包括TF6255。
如果你安装的是3.1.4024.xx之后的新版本,重装时遇到过"检测到高版本残留"的提示,处理思路是这样的:先通过控制面板正常卸载TwinCAT3,然后手动删除C:\TwinCAT\3.1目录下的残余文件,再清掉注册表中的TwinCAT项,最后重装。注意,不到万不得已不要去动注册表,备份好项目文件再操作。
2.2 新建工程并添加Modbus函数库
打开TwinCAT XAE(也就是Visual Studio界面),新建一个TwinCAT XAE Project。具体的操作路径:
- 新建项目,选择TwinCAT Projects下的TwinCAT XAE Project,名称建议用英文,比如Tc3ModbusServer。
- 项目创建后,在Solution Explorer里找到Plc项目,右键Add New Item,新建一个Standard PLC Project。
- 编译一次空项目,确保PLC工程能生成,这说明开发环境已经正常。
接下来是关键步骤:在PLC工程的References节点上右键,选择Add Library,然后在搜索框里输入Modbus。你会看到跟Modbus相关的几个库,其中Server侧需要的是Tc2_ModbusSrv,Master侧是Tc2_ModbusTcp。如果你要做的是Server功能,添加Tc2_ModbusSrv就对了。
需要注意,库的引用版本跟你装的TF6255版本要对应。如果References里找不到Tc2_ModbusSrv,说明许可证或者安装的TF6255组件不完整,需要重新运行TwinCAT3安装程序,在功能组件里勾选对应的通讯组件。
2.3 让系统跑在本地仿真模式
没有真实PLC也能验证绝大部分功能。TwinCAT3支持把开发电脑当成目标系统,也就是本地仿真模式。很多初学者不明白怎么让"仿真程序跑起来",其实操作并不复杂:
- 在Solution Explorer里找到项目根节点(比如Tc3ModbusServer),右键选择Set as StartUp Project。
- 在顶部工具栏的目标系统下拉框里,选择Local(或者本机对应的AMS NetId)。
- 激活配置:点击Activate Configuration,系统会提示是否切换到Run Mode,选择是。
- 此时TLSM(TwinCAT System Manager)会自动启动,PLC工程会在本地运行,你会看到模块状态变成绿色。
如果你看到的是黄色或者红色,最常见的两个原因:一是没有切到Run Mode,二是没有管理员权限。把TcXaeShell以管理员身份重新打开,再激活一次就好。
仿真模式跑起来后,我们就把焦点回到PLC程序上,开始写Modbus Server的核心逻辑。
3. Server程序设计与核心代码实现
3.1 先画一张寄存器分配表
写代码之前,我强烈建议你先把Modbus寄存器分配表画出来。Modbus协议本身的数据模型很简单:线圈(Coil)、离散输入(Discrete Input)、输入寄存器(Input Register)、保持寄存器(Holding Register)。在Modbus-TCP Server场景里,用得最多的是保持寄存器,因为上位机可以读它,也可以写它,非常适合做参数下发和数据上报。
我一般用一个WORD数组作为Modbus的缓冲区,然后定义一张地址映射表,把数组元素跟实际的PLC变量对应起来。下面是一个典型例子:
| Modbus地址 | 寄存器偏移(相对40001) | 缓冲区索引 | PLC变量 | 含义 | 读写属性 |
|---|---|---|---|---|---|
| 40001 | 0 | arrModbusBuf[0] | wDeviceStatus | 设备状态字 | 只读 |
| 40002 | 1 | arrModbusBuf[1] | wRemoteCmd | 远程命令 | 读写 |
| 40003 | 2 | arrModbusBuf[2] | rFrequencySet | 频率设定值(放大100倍) | 读写 |
| 40004 | 3 | arrModbusBuf[3] | rCurrentFeedback | 电流反馈(放大100倍) | 只读 |
这里有个经验:现场值经常是小数,而Modbus寄存器是16位整数,所以需要约定小数放大倍数。比如频率25.5Hz,就存2550,上位机自己除以100。这个约定一定要写进通讯文档里,不然联调时必然扯皮。
3.2 写Server核心程序:初始化加周期运行
打开PLC工程里的MAIN程序,先声明变量:
PROGRAM MAIN VAR fbModbusServer : FB_MBServer; arrModbusBuf : ARRAY[0..255] OF WORD; bStartServer : BOOL := TRUE; bServerBusy : BOOL; bServerError : BOOL; iErrorId : UDINT; nState : UDINT; bInitDone : BOOL := FALSE; END_VAR然后在程序体里写两段逻辑:第一段做Server启动,第二段做周期轮询。
// 启动Modbus Server IF NOT bInitDone THEN bInitDone := TRUE; fbModbusServer( bStart := bStartServer, // 启动请求 sSrvNetID := '', // 使用本地NetId,也可以填本机AMS NetId nTCPPort := 502, // 默认Modbus端口 cbBufSize := SIZEOF(arrModbusBuf), // 缓冲区字节数 pBuffer := ADR(arrModbusBuf), // 缓冲区首地址 bAutoRestart := TRUE, // 异常退出后自动重启 bBusy => bServerBusy, bError => bServerError, iErrorId => iErrorId ); ELSE // 周期调用FB_MBServer,让后台处理TCP请求 fbModbusServer( bStart := bStartServer, sSrvNetID := '', nTCPPort := 502, cbBufSize := SIZEOF(arrModbusBuf), pBuffer := ADR(arrModbusBuf), bAutoRestart := TRUE, bBusy => bServerBusy, bError => bServerError, iErrorId => iErrorId ); END_IF这段程序需要注意几个地方的细节:
第一个是sSrvNetID参数。在真实PLC上,这个值填目标系统的AMS NetId,在本地仿真时留空或者填127.0.0.1.1.1都可以。如果你在真机上运行发现Client连接后立即断开,先检查这里。
第二个是bStart参数的触发方式。功能块内部对bStart的上升沿敏感,所以我用一个bStartServer常TRUE并放在周期调用里,让它满足持续运行的需要。如果你希望Server可以被外部条件启停,就把这个变量换成可写变量。
第三个是cbBufSize和pBuffer。这两个参数决定了Modbus寄存器缓冲区的大小和位置。上位机读写保持寄存器时,会直接作用于这个数组。所以,要让"寄存器地址"和"数组索引"建立对应关系,然后通过数组操作把数据搬进搬出。
3.3 把PLC变量挂到Modbus地址空间
缓冲区建好后,剩下的问题就是"怎么让外部数据真正读写到PLC变量"。最简单的做法是直接对数组元素赋值:
// 写入方向:PLC变量 -> Modbus缓冲区 arrModbusBuf[0] := wDeviceStatus; arrModbusBuf[2] := REAL_TO_WORD(rFrequencySet * 100.0); arrModbusBuf[3] := REAL_TO_WORD(rCurrentFeedback * 100.0); // 读取方向:Modbus缓冲区 -> PLC变量 wRemoteCmd := arrModbusBuf[1];这样写直白,但有一个隐患:如果上位机高频写入,而PLC程序里你也高频读取,可能会出现数据撕裂。毕竟FB_MBServer处理TCP报文是在后台任务里,和你的应用任务并不同步。更稳妥的做法是使用循环缓冲或者双缓冲:程序周期开始时把缓冲区整体复制到工作区,读写完成后整体复制回去。对于初学者,先用直接赋值的方式把逻辑跑通,后面再优化也不迟。
另外,如果你用的是TwinCAT3较新版本,TF6255还支持通过ADS设备直接映射到符号变量,这样就不需要手动维护映射表。但这种方式配置相对复杂,我建议先用缓冲区映射法,理解了数据通路之后再玩高级功能。
4. 联调测试:从仿真到真机
4.1 用Modbus Poll做快速验证
程序写完后,点击激活配置,让PLC工程运行起来。然后打开Modbus Poll,新建连接:
- Protocol选择Modbus TCP/IP。
- IP地址填127.0.0.1(本地仿真),端口502,Unit ID填1。
- 功能码选择03(Read Holding Registers),起始地址0,寄存器数量10。
点击OK后,如果一切正常,你会看到寄存器区开始刷新数据。此时在PLC程序里改一下wDeviceStatus的值,Modbus Poll里对应的寄存器应该跟着变化。能读到数据,就说明Server已经通了。
用Modbus Poll测试时有一个细节:地址基址的设定。有些工具显示地址是从40001开始的,有些是从0开始的,实际上都指向同一个寄存器。联调时如果遇到"明明地址对了,数据就是读不到",可以先试一下把地址偏移1位,这个问题多半是基址差异造成的。
4.2 用Python自动化脚本批量读写
Modbus Poll适合人工调试,但要做批量读写测试、压力测试,建议用Python直接跑pymodbus。我一般会写一个几十行的脚本,验证读保持寄存器、写单个寄存器、写多个寄存器这三个基本操作。
from pymodbus.client import ModbusTcpClient client = ModbusTcpClient('127.0.0.1', port=502) client.connect() # 读保持寄存器,从地址0开始读5个 result = client.read_holding_registers(0, 5, unit=1) print(result.registers) # 写单个保持寄存器,地址1写入1000 client.write_register(1, 1000, unit=1) # 写多个保持寄存器,地址2到4一次性写入 client.write_registers(2, [1500, 2000, 2500], unit=1) client.close()运行后回到PLC程序里查看arrModbusBuf数组的值,确认写入是否生效。我在测试时发现,有的版本pymodbus默认的Unit ID是0,而TF6255默认监听时对Unit ID不敏感,但上位机那边如果开了严格的Unit ID校验,就要保持一致。
4.3 真机联调的差异点
仿真是开发阶段的好帮手,但真机联调还是有几个地方完全不同:
第一,网卡绑定。真实PLC上,Modbus Server监听的是Windows系统的IP,不是实时网卡的IP。如果PLC有两个网口,一个接EtherCAT,一个接上位机局域网,必须在Windows网卡配置里把对方的IP段加到同一网络下,否则TCP包根本到不了应用层。
第二,路由配置。上位机如果是通过路由器跨网段访问PLC,需要在TwinCAT路由表里加入路由信息,或者直接关闭Windows防火墙。
第三,响应时间。仿真模式下实时任务和非实时任务都在一个进程里跑,响应速度不代表真机水平。真机上EtherCAT周期建议设在1ms以上,Modbus Server任务也会占用CPU,需要观察CPU负载,必要时把Server调用放到独立任务周期。
5. 常见问题与排查技巧实录
5.1 Server起不来、端口502占用怎么办
FB_MBServer如果没启动成功,最常见的原因是端口被占用。Modbus-TCP默认用502端口,这个端口在Windows上属于受保护端口,有时候其他软件会抢着绑。可以用下面的命令查:
netstat -ano | findstr :502如果看到有进程占用502,记下PID,然后在任务管理器里找到对应进程。很多时候是之前残留的调试程序没退出,杀掉就好了。如果是自己开发的程序占用,就改个端口号,比如1502,然后在客户端那边同步修改。
另外,Windows防火墙默认会拦截外部访问502端口的TCP连接。如果本机测试没问题、局域网设备连不上,十有八九是防火墙。解决办法是添加入站规则,放行TCP 502端口。注意,如果你修改了端口号,防火墙规则也要对应修改。
5.2 上位机连不上,先按这个顺序排查
我总结了一套排查顺序,联调遇到连不上时,从第一个开始查:
- 先ping:上位机能ping通PLC的IP吗?ping不通是网络问题,先解决到二层三层。
- 再测试端口:telnet PLC的IP 502,看端口通不通。如果不通,检查防火墙和Server是否启动。
- 看功能块状态:在PLC程序里监控bServerError和iErrorId,有没有报错。
- 看PLC日志:Windows事件查看器里TwinCAT相关日志,有没有异常记录。
- 换工具再测:同一时间只保留一个客户端连502,有些Server实现不支持多个连接同时访问。
按照这个顺序查,一般都能定位问题。我见过有人纠结了半天功能码,结果发现是防火墙没放行,这种低级错误其实最浪费时间。
5.3 数据错乱、字节序不对的坑
数据读到了但值不对,这是Modbus联调里最常见的坑。原因基本是字节序和字序问题。Modbus协议规定一个寄存器是16位,但实际工程里经常要传32位浮点数或者32位整数。
比如倍福PLC里一个REAL变量占32位,写进缓冲区时要占两个寄存器。上位机读出来之后,如果高低寄存器顺序不对,数值就会变成一个巨大或者接近0的数。解决办法是约定好字序:高字在前还是低字在前。
我建议在通讯文档里明确写清楚:所有REAL类型变量,按高字在前、低字在后排列,并按整数放大倍数处理。上位机组态时,针对每个数据点设置对应的字交换方式。如果你发现数值大约是正确值乘以65536,那就是高低字反了。
再有一个是ASCII字符串数据:PLC里STRING类型是单字节编码,如果上位机按Unicode解析,会看到乱码。这类问题不是代码错了,而是协议约定不完整。所以寄存器分配表里要写清楚数据类型、字节长度、小数位数、字序规则,联调才能有序推进。
5.4 TwinCAT3重装与版本残留的注意事项
热词里有条"twincat3 4024重装时检测到高版本残留",这个问题挺典型。TwinCAT3装过一次后,如果直接覆盖安装旧版本,经常会提示检测到高版本残留。解决办法是卸载当前版本后,删除C:\TwinCAT\3.1整个目录,再检查Windows服务列表里有没有TwinCAT相关服务处于停止状态,把它们都删除或禁用后再安装。
这里特别提醒一句:重装之前备份好项目的源代码,因为卸载TwinCAT3不会动你的项目工程,但如果你在安装过程中选择了清理配置,AMS NetId和路由表会重置,重新激活配置时要留意。
6. 进阶与扩展建议
6.1 多客户端与性能调优
默认情况下,FB_MBServer支持多客户端并发连接,但TCP连接数上来之后,如果PLC的CPU负载过高,客户端会感受到明显的响应延迟。我在一个项目中遇到过8台上位机同时轮询,每个扫描周期100ms,结果PLC的CPU占用率涨到30%多。后来做两个优化:一是把Modbus Server的调用放到独立的5ms任务里,不占用运动控制任务的时间片;二是减少缓冲区轮询次数,上位机设置为500ms轮询一次,数据完全够用。
如果你的系统对实时性要求很高,可以考虑用TwinCAT3的实时网卡配合专用Ethernet端口监听502,避免Windows协议栈的调度抖动。不过大多数项目没必要这么激进。
6.2 安全与生产部署建议
Modbus-TCP本身没有加密和认证功能,只要知道IP和端口,任何人都能读写寄存器。在生产环境部署时,至少做三件事:
- 把PLC放到独立的工业网段里,通过防火墙或工业网关隔离外部访问。
- 在PLC程序里做数据校验:上位机写命令寄存器时,可以用命令字加校验字的方式,避免误写。
- 对关键寄存器做预设范围检查:写入值超出合理范围就直接拒绝,防止异常数据流入控制系统。
另外,如果客户要求数据加密,Modbus-TCP是做不到的,可以考虑在应用层封装,或者改用OPC UA。倍福的TF6100库就是干这个的,但那是另一个话题了。
最后说一点个人体会。做Modbus-TCP Server这件事本身不复杂,真正复杂的是通讯现场的"预期管理"。跟SCADA联调时,两边一定要有一份清晰的寄存器地址表,数据类型、大小端、缩放系数全部写清楚。先联调通过一个寄存器,再扩大到全部变量,这样能少走很多弯路。
如果你后面需要跟三菱FX5U、西门子S7那样的设备做Modbus-TCP通讯,Master侧用的TF6250函数库思路也差不多,只是方向反过来。这套Server经验打底之后,再玩Master、RTU、OPC UA都会顺手很多。