去年做设备数据采集时遇到一个典型需求:工厂里有几台倍福控制器,产线系统需要实时读取设备状态、产量和报警信息,但又不愿意在生产网里部署复杂的数据中间件,点名要往MQTT Broker上推。那会儿TwinCAT和MQTT的现成资料不多,网上搜到的要么是OPC UA方案,要么是拿树莓派做硬件网关,真正把TwinCAT自带MQTT功能库跑通的案例很少。折腾了差不多一周,才把整个链路彻底打通。这篇文章就是那次部署的完整复盘,从方案选型、环境准备、功能块调用到数据上送和排错思路都有,适合正在用TwinCAT 3做设备数据上云、又不想额外引入硬件网关的工程师参考。
1. 为什么倍福PLC需要MQTT网关:一个数采场景的选型思考
1.1 直接采PLC数据的三条路
倍福控制器的数据接口其实不少,最传统的是ADS,工业现场用得最多的是OPC UA,还有现在我们说的MQTT。先说清楚这三者的区别,你才知道为什么偏偏选MQTT。
ADS是倍福自家协议,走的是TCP 48898端口,读取速度极快,适合TwinCAT之间通信或者本机上位机读取。但它有个限制:非Windows平台对接ADS很麻烦,而且ADS组件基本只有倍福生态里有,外部系统要用得额外封装。
OPC UA是工业互联的通用语言,倍福的TF6100就是干这个的。数据建模能力强,安全性好,但有个比较现实的问题:OPC UA服务器通常要常驻运行,客户端连上来之后如果网络抖动,重连机制需要自己处理。对IT那边的同事来说,OPC UA的复杂度也偏高,他们更希望直接拿到一个JSON或者简单消息。
MQTT的优势在于轻量、异步、一对多。设备端只负责往Broker推消息,谁要消费数据自己去订阅,Broker不关心生产者和消费者的关系。这种解耦方式非常适合产线数据往MES、云平台或可视化大屏送。现场实施的时候,产线PLC一般只负责干活,数据上报的节奏完全可以按需设计,不占用控制任务的执行时间。
1.2 网关在架构里的位置
整个数据流的架构是这样的:倍福控制器运行TwinCAT 3,PLC程序里面跑MQTT功能块。功能块把PLC内部变量(比如DeviceState、TotalCount、AlarmCode)打包成MQTT消息,推送到局域网内的Broker。Broker后面可以挂MES系统、数据库写入服务、WebSocket看板、甚至云平台的桥接网关。
这里有个容易搞混的点:MQTT网关不等于硬件网关。很多人一听说PLC要上MQTT,第一反应是买一个硬件协议转换器。硬件网关当然可以,但它多了一层设备,多一个故障点,而且配置方式各家不一样。TwinCAT 3从某个版本开始直接提供MQTT功能库,PLC里声明一个功能块就能通信,完全不需要额外硬件。这篇博文讲的就是这种纯软件网关的部署方式。
从部署角度看,MQTT网关工程可以运行在倍福的工控机上,也可以运行在独立PC上通过ADS访问PLC数据。如果PLC和网关在同一台硬件上,直接读本机变量最省事;如果网关需要对接远程控制器,就要配置ADS路由。两种方式都可行,选择依据主要是现场的网络结构和安全要求。
1.3 什么时候不建议上MQTT
不是所有场景都适合MQTT。如果数据量特别大、要求毫秒级实时同步,MQTT的异步机制反而会拖后腿。比如两个控制器之间需要轴同步,或者要求绝对实时的事件联锁,这种场合老老实实走ADS或者EtherCAT,不要没事找事。
另外,如果只是本地SCADA系统读数据,没有跨系统分发需求,直接写数据库或者OPC UA更直接。MQTT的价值在于多系统分发和跨网络传输,单一消费端场景引入它属于过度设计。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 部署前的环境准备:版本、授权与Broker搭建
2.1 TwinCAT版本与MQTT功能库选型
TwinCAT 3的MQTT功能有两条产品线,一个叫TF6710,另一个叫TF6701。TF6710是早期版本,名字叫TwinCAT MQTT,功能比较简单,现在基本不再推荐。TF6701是后来推出的TwinCAT MQTT,功能更完整,支持TLS加密、多个连接实例、遗嘱消息等特性。我用的是TF6701,踩坑也主要踩在它上面。
安装TF6701之后,在TwinCAT工程的References里添加Tc3_Mqtt库(具体库名可能随版本叫法略有差异),就能在程序里使用FB_MQTTClient这个功能块。如果你的TwinCAT版本比较老,可能需要在Beckhoff官网下载对应的库文件包,或者在TcXaeShell里通过包管理器安装。
一个容易被忽略的点:TF6701是收费产品,需要License。不过在开发阶段,可以先用30分钟试运行授权来调试,TwinCAT XAE的Run-Time授权里能找到临时License选项。我一开始没注意到这个,装完库之后发现功能块报没有许可的错误,折腾了半小时才搞明白。
提示:安装TF6701后,务必在TwinCAT的License页面确认MQTT功能已经激活。否则即使编译通过,运行时会返回类似0x10000001的授权错误。
2.2 MQTT Broker选型与本地搭建
Broker的选择很多,生产环境我推荐EMQX或者Mosquitto。
EMQX功能丰富,带Web管理界面,集群、认证、规则引擎都有,适合对管理和扩展性有要求的场景。
Mosquitto轻量、稳定,安装简单,适合边缘侧小规模部署。
如果只是开发调试,用EMQX最省心,因为自带Dashboard,可以可视化查看消息收发。我在测试环境就用Docker快速拉了一个EMQX:
bash复制docker run -d --name emqx -p 1883:1883 -p 18083:18083 emqx/emqx:latest
1883是MQTT标准端口,18083是Dashboard端口,浏览器打开http://localhost:18083,默认账号admin/public。生产环境记得改密码。
Mosquitto的Docker部署也差不多:
bash复制docker run -d --name mosquitto -p 1883:1883 eclipse-mosquitto:2.0
如果要用用户名密码认证,需要挂载配置文件。这个后面讲安全配置的时候再展开。
2.3 网络与防火墙侧的准备工作
MQTT走TCP 1883端口,所以TwinCAT运行所在的Windows机器要放行这个端口。很多人在这里翻车:TwinCAT程序明明显示连接成功,但Broker那边收不到消息,最后发现是Windows防火墙把出站连接给拦了。
具体操作:控制面板 -> Windows Defender防火墙 -> 高级设置 -> 出站规则,允许TCP 1883目标端口。或者更省事,直接把TwinCAT的运行时进程加到防火墙例外列表。注意,如果Broker设在远程服务器,还要确认服务器安全组放行1883端口。
另一个我踩过的坑是时间同步。MQTT的Last Will、KeepAlive等功能依赖客户端和Broker的时间一致性,如果两个设备时间差太多,会出现连接被Broker强制断掉的怪问题。所以部署前把倍福工控机和Broker服务器的NTP时间同步做好,省得后面排查时怀疑人生。
3. 核心实现:FB_MQTTClient调用与连接状态管理
3.1 功能块实例化与参数说明
库添加好之后,在PLC程序中声明一个FB_MQTTClient实例:
iecst复制PROGRAM MAIN
VAR
fbMqttClient : FB_MQTTClient;
bConnect : BOOL;
bPublish : BOOL;
sTopic : STRING := 'factory/line1/dev001/status';
sPayload : STRING := '{"ts":"2025-01-01T12:00:00","state":1}';
nQoS : BYTE := 1;
bConnected : BOOL;
bPubBusy : BOOL;
bPubErr : BOOL;
nPubErrId : UDINT;
END_VAR
这个功能块的输入输出参数不少,最核心的几项列一下:
| 参数名 | 类型 | 说明 |
|---|---|---|
| sServerHost | STRING | Broker的IP或域名 |
| nServerPort | UDINT | Broker端口,默认1883 |
| sClientId | STRING | 客户端ID,同一Broker下不能重复 |
| sUserName / sPassword | STRING | 认证信息,按需填写 |
| nKeepAliveTime | UDINT | 心跳间隔,单位秒,默认60 |
| bExecute | BOOL | 上升沿触发连接 |
| bConnected | BOOL | 输出,是否已连接 |
| bErr / nErrId | BOOL / UDINT | 错误标志和错误码 |
这些参数的配置逻辑比较直观,但有两点要注意。第一,sClientId一定不能写死成固定值,尤其是多台设备共用一个Broker的时候,重复的ClientId会导致后登录的连接把先登录的踢下线。我习惯用设备编号拼时间戳,确保唯一性。第二,nKeepAliveTime不要设太短,也不要太长。太短会增加网络流量,太长会导致Broker无法及时发现设备离线,一般60秒就够。
3.2 连接与断开的状态机处理
FB_MQTTClient不是一调用就自动连接的,需要触发Connect方法。典型的控制逻辑是每次扫描周期检测连接状态,如果未连接则触发连接:
iecst复制IF NOT bConnected AND NOT bConnBusy THEN
bConnect := TRUE;
END_IF
fbMqttClient.Connect(
bExecute := bConnect,
sServerHost := '192.168.0.10',
nServerPort := 1883,
sClientId := 'PLC_DEV001_001',
sUserName := 'plcuser',
sPassword := 'password',
nKeepAliveTime := 60,
bConnected => bConnected,
bBusy => bConnBusy,
bErr => bConnErr,
nErrId => nConnErrId
);
这个模式有个关键点:Connect的bExecute是上升沿触发,所以bConnect在连接成功后马上复位。如果不复位,功能块会反复执行连接指令,导致Broker那边频繁出现Client连接断开重连的记录。
断开同理,用Disconnect方法,一般在PLC停机时调用,把连接优雅关闭。
3.3 发布数据的方法调用
连接成功后,发布消息就变得非常简单:
iecst复制IF bConnected AND bPubTrigger THEN
bPublish := TRUE;
END_IF
fbMqttClient.Publish(
bExecute := bPublish,
sTopicName := sTopic,
sPayload := sPayload,
nQoS := nQoS,
bBusy => bPubBusy,
bErr => bPubErr,
nErrId => nPubErrId
);
Publish方法有bExecute上升沿触发机制,所以bPublish在触发后也要复位,避免重复发布同一条消息。
QoS级别的选择要理解一下。QoS 0是至多一次,可能丢消息,适合不重要的状态量。QoS 1是至少一次,确保消息到达但可能重复,适合一般数据采集。QoS 2是恰好一次,最可靠但效率最低,PLC场景基本用不到。我用QoS 1居多,Broker端做幂等处理就行。
3.4 用订阅端验证通信链路
PLC写完后,先用第三方MQTT客户端工具验证链路。我喜欢用MQTTX,安装简单,界面直观。打开MQTTX,新建连接,填Broker地址,然后订阅Topicfactory/#,跑PLC程序触发发布,如果能看到JSON消息,说明链路已经通了大半。
如果是命令行环境,Mosquitto自带的命令行工具更轻量:
bash复制mosquitto_sub -h 192.168.0.10 -t 'factory/#' -v
这一步能快速区分问题出在PLC侧还是Broker侧,是非常有效的定位手段。
4. 数据上送工程:变量映射、Topic规划与消息体设计
4.1 规划上送的数据与采集频率
MQTT网关不只是把PLC连接上就完事了,最花心思的是确定哪些数据要上送、以什么频率上送。
我在产线项目里通常把数据分三类:
第一类是状态型数据,比如设备运行/停止/故障、当前加工模式、急停状态。这类数据变化频率低,但需要实时感知,建议事件触发或每秒周期上送。
第二类是计数型数据,比如总产量、合格品数、设备运行时长。这类数据变化频率取决于节拍,一般每1到5秒上送一次即可,不用太频繁。
第三类是报警型数据,比如故障代码、报警时间戳。这类数据必须立即上报,而且最好带恢复状态,方便MES系统做报警闭环。
把这些数据整理成一张变量映射表,标注PLC变量名、数据类型、转换系数、采集周期,是做网关工程的第一步。不要一上来就写程序,先想清楚要上送什么,否则后面变量一多,程序会乱成一团。
4.2 Topic设计原则与命名规范
MQTT的Topic本质是字符串层级结构,设计得好,下游订阅方就能用通配符灵活筛选数据。
我推荐按“工厂/产线/设备/数据类型”四级来设计:
text复制factory/line1/dev001/status
factory/line1/dev001/alarm
factory/line1/dev001/quality
factory/line1/dev002/status
这种设计有几个好处。MES系统订阅factory/line1/#就能获取整条产线所有数据;可视化系统只要订阅factory/line1/dev001/status,就能聚焦单台设备;以后扩容新设备,直接新增Topic层级,不影响已有订阅。
层级命名建议全部用小写英文字母、数字和连字符,避免中文、空格和特殊字符。虽然MQTT协议本身不限制,但下游处理程序(尤其是用正则表达式解析Topic的系统)会非常痛苦。
4.3 在PLC里拼JSON消息体
MQTT消息体用什么格式没有硬性规定,但JSON是当前事实标准,解析方生态最好。PLC里拼JSON有两种办法。
最简单的是直接字符串拼接:
iecst复制sPayload := '{"ts":"' + sTimestamp + '","state":' + USINT_TO_STRING(nState) + ',"count":' + UDINT_TO_STRING(nTotalCount) + ',"alarm":"' + sAlarmCode + '"}';
这种方案适合数据字段少的场景,简单直观,不需要额外库。但有个大坑:如果字符串值里包含引号、反斜杠、换行符这些特殊字符,会导致JSON格式非法。所以字段值要么严格控制格式,要么做转义处理。
数据字段多了,推荐用TwinCAT的JSON序列化库,比如Tc3_JsonXml或者JsonDataUtilities。这套库可以根据数据结构生成JSON字符串,自动处理转义和类型转换,代码更规范。缺点是要多写一点数据结构定义。
我在项目中两种都用过。调试阶段为了快速验证链路,直接拼字符串最顺手。消息格式稳定后,再切换到JSON库,保证正式环境的规范性。
4.4 周期发布与事件触发相结合
发布频率的设计直接决定MQTT Broker的负载和PLC程序的执行效率。
我在项目里用两个定时机制配合。第一个是周期任务,在TwinCAT里新建一个Task,执行周期设为1000ms,每秒钟把状态型数据打包上送一次。第二个是事件触发,当报警变量上升沿到来时,立刻调用一次Publish方法,保证报警不迟报。
事件触发的判断逻辑很简单:
iecst复制IF bAlarmRising THEN
sPayload := BuildAlarmPayload();
bPublish := TRUE;
bAlarmRising := FALSE;
END_IF
如果每秒钟上送所有数据,包括报警数据,也不是不行,但消息量会大很多,而且报警恢复的时序就不好判断了。事件触发加周期上送,是效率和实时性的平衡方案。
4.5 多设备扩展的工程组织方式
一条产线通常不止一台倍福控制器。如果每台设备都写一套独立的MQTT发布逻辑,程序维护量会很大。
我是这样组织的:把MQTT发布功能写成一个功能块(FB_MQTTGateway),输入是设备ID和要上送的数据结构实例。每个设备实例化一个功能块,然后在任务里逐个调用。
iecst复制VAR
fbGatewayDev001 : FB_MQTTGateway;
fbGatewayDev002 : FB_MQTTGateway;
END_VAR
这样改Topic只需要改设备的实例名称,逻辑全部复用。代码量减少,排错也更方便。
5. 上线必踩的坑:中断、乱码与性能问题排查实录
5.1 连接频繁断开的根因排查
第一个让我头疼的问题是PLC和Broker之间的连接隔一段时间就断开,然后自动重连,断断续续。开始时怀疑是网络问题,但ping Broker完全不通的迹象都没有,TCP连接明明存在。
后来用Wireshark抓包才发现,是KeepAlive机制触发的。客户端每隔60秒发一次PINGREQ,Broker如果在规定时间内没收到,就认为客户端离线,主动断开。问题在于TwinCAT那边功能块的KeepAliveTime参数我设了60,但Broker端配置的KeepAlive超时阈值也是60,加上网络抖动,很容易出现刚好超过阈值的情况。
解决办法很简单:把PLC端的KeepAliveTime改小,比如30秒,给网络抖动留出余量。改完之后连接稳定多了,一天都没有掉线。
5.2 中文和特殊字符乱码
第二个坑是消息体里的中文乱码。PLC变量里存的报警描述是中文,拼到JSON里发出来,下游看就是一堆乱码,根本没法用。
根因是TwinCAT的STRING类型按单字节处理,编码规则跟随Windows系统的代码页,通常是GBK。而MQTT协议规定payload是UTF-8编码的字节流。GBK和UTF-8之间的转换没做,自然乱码。
解决方案有两种。第一种是PLC里全部用UTF-8字符串,TwinCAT从某个版本起支持UTF-8字符串操作,在程序里做一次转换再发出。第二种更稳妥,报警描述字段在PLC里只传代码,比如ERR_OVER_TEMP,具体的中文描述由下游系统根据代码映射。生产环境我推荐第二种,一方面避开了编码转换的坑,另一方面也减轻了PLC程序的字符串处理负担。
5.3 PLC扫描周期与MQTT发布频率打架
第三个问题比较隐蔽。我在快任务里写了发布逻辑,结果导致PLC扫描超时,CPU负载飙高。原因是Publish方法内部要做TCP非阻塞发送和MQTT协议栈处理,这些操作比较耗时,放在1ms或10ms周期的快任务里,会导致任务执行时间超过周期设定,触发Watchdog报警。
正确做法是把MQTT功能块的调用放到慢任务里,比如100ms或1000ms周期的任务。MQTT这种异步消息协议本身就不需要毫秒级实时性,放到慢任务里既不卡控制逻辑,又不丢数据。改完任务分配之后,CPU负载明显下降。
5.4 软复位后功能块不自动重连
还有一个让我措手不及的问题:PLC程序在线修改后执行Reload,或者软复位(Warm Reset),MQTT连接没有自动恢复,必须手动触发一次Connect才重新上线。
原因是Reload之后,功能块实例的内部状态清空了,但网络连接可能还残留在系统底层,导致Connect时收到“连接已存在”之类的错误。解决方法是PLC启动初始化时,先调用一次Disconnect,再调用Connect,确保连接状态干净。
iecst复制// 启动初始化
IF bFirstScan THEN
fbMqttClient.Disconnect(bExecute := TRUE);
bInitDone := TRUE;
END_IF
// 延迟后再连接
IF bInitDone AND bDelayDone THEN
bConnect := TRUE;
bInitDone := FALSE;
END_IF
实测这个方案在重复在线修改时非常可靠,没有再出现连不上的情况。
5.5 排查利器:Wireshark与Broker日志
如果问题比较诡异,比如消息丢了、重复了、顺序乱了,我建议直接上Wireshark抓包分析。Wireshark配置好TLS解密或直接抓明文1883端口的包,能看到完整的MQTT报文交互过程,连接是否正常、消息走向、QoS确认等一目了然。
另外,EMQX的日志非常详细,连接、断开、订阅、发布都有记录。出了问题先翻Broker日志,往往能比在PLC侧瞎猜更快定位方向。
6. 让网关更稳的进阶思路与运维建议
6.1 断线重连与看门狗机制
MQTT网关如果长时间运行,网络抖动、Broker重启、防火墙规则变更都可能造成连接中断。虽然TF6701内部有一定自动重连能力,但更稳妥的做法是在PLC侧加一个看门狗逻辑。
看门狗思路很简单:每10秒检测一次bConnected状态,如果未连接且未在重连中,就触发一次Connect。如果连接成功但长时间(比如5分钟)没有任何消息发布或接收,也可以主动断开重连。
我在程序里加了一个计数器,连续掉线超过5次就给出一个系统报警,方便运维人员介入。
6.2 用户名密码与TLS加密
生产环境的MQTT Broker不能裸奔,至少打开用户名密码认证。EMQX和Mosquitto都支持配置文件方式,把用户名、密码哈希写到认证文件里。PLC端的FB_MQTTClient提供了sUserName和sPassword参数,填上就行。
如果数据要经过公网传输,建议打开TLS,把1883端口换成8883,客户端配置证书。TF6701支持TLS模式,但证书管理稍微复杂,要在PLC端维护CA证书、客户端证书和私钥。边缘网关如果只在生产内网跑,TLS不是必须的,但用户名密码认证一定不能省。
6.3 遗嘱消息:让订阅端知道设备掉线了
MQTT的遗嘱消息(Last Will)在设备意外断线时非常有用。PLC侧可以在连接时设置遗嘱Topic和遗嘱消息,比如factory/line1/dev001/status,内容是{"state":0,"reason":"disconnect"}。这样如果PLC宕机、断电、网线断开,Broker会替设备发布这条遗嘱消息,订阅端就能立刻感知设备离线。
我在项目里用这个机制做设备离线监控,MES系统收到遗嘱消息后自动标记设备状态为离线,并触发告警。比传统的轮询心跳要实时得多。
6.4 批量上送:减少消息量
如果PLC要上送的数据点非常多,比如几十个变量,每秒钟逐条发布,消息量会很大,Broker和网络压力都不小。优化方案是把多个变量合并到一条JSON消息里,比如把所有模拟量数据放在一个数组字段,一条消息搞定。
json复制{"ts":"2025-01-01T12:00:00","signals":{"temp1":23.5,"temp2":24.1,"press":0.6,"speed":1430}}
这样消息量从几十条变成一条,Broker负载显著降低。缺点是下游解析时要多做一层数据分解,但换来的是整体架构更高的扩展性。
6.5 工程备份与版本管理
最后说个运维层面的问题。TwinCAT工程文件是二进制格式,不像文本代码那样方便做版本对比。我吃过亏:有一次改版工程没备份,结果上线后出现一个诡异的数据异常,翻遍代码找不到原因,最终靠旧版本对比才发现是某个变量被误改。
建议每次发布前,把TwinCAT工程整个归档成压缩包,连同PLC程序、库文件版本、Broker配置一起打一个快照。有条件的话,把工程放到Git里管理,配合TwinCAT的文本化导出格式,也能做到版本追踪。
部署MQTT网关这件事,技术上不难,难的是把各种细节想周全。从选型到上线,最深的体会是:数据的最终价值取决于它的质量和可达性。MQTT网关把PLC的数据从封闭的控制柜里释放出来,让更多人能用上这些数据,但这只是开始。后续如果再接入边缘计算节点做数据清洗,或者接实时数据库做历史存储,整个数采架构会越来越完整。我刚把这条路走通,后面还有很多可以优化的空间。
