☰
LabVIEW自研三菱PLC MC 3E以太网通讯库:帧解析与实战踩坑
2026/10/24 7:44:37 网站建设 项目流程

1. 先说清楚:为什么放着现成方案不用,非要自己写通讯库

上个月有个项目又要连三菱PLC,客户点名要用LabVIEW做上位机,通讯点表拉出来三百多个,D区、M区、X/Y全都有。我一开始也想走老路——装MX Component调DLL,后来想想还是算了,直接用TCP Socket按MC 3E帧协议自己搓一个LabVIEW通讯库出来。这篇文章就是这次改造的完整记录:帧协议怎么拆、通讯库怎么分层、核心VI怎么写、现场到底踩了哪些坑。

如果你正准备在LabVIEW里跟三菱PLC做以太网通讯,或者已经在用现成DLL但被各种环境问题折磨,这篇文章应该能帮你省掉不少弯路。

1.1 现成方案的实际痛点

MX Component这条路本身是通的,三菱官方提供,功能也全,但用几年下来,它在现场给我的折腾远超预期。先说第一个问题:环境依赖太重。MX Component的运行时有几个版本,现场工控机上如果没有提前装好对应版本,软件部署到一半才发现通讯起不来,那场面相当尴尬。第二个问题是版本匹配,MX Component版本、PLC固件版本、电脑位数、LabVIEW位数,四者要对上。32位和64位混用的工控机上,经常出现"明明装了DLL就是加载不出来"的情况。

第三个问题最要命:不透明。DLL内部做了什么你没法看到,哪天通讯不上,只能对着事件日志猜,或者自己上抓包工具去核对帧内容,等于绕了个大远路。而且在LabVIEW里调ActiveX节点、配置属性方法,封装完以后代码可读性很差,换个人接手基本看不懂。

1.2 自研通讯库的目标边界

我给自己定的目标很清晰:纯LabVIEW实现,不依赖任何外部DLL,一个文件夹拷走就能部署;只覆盖工作中最常用的功能,也就是D区、M区、X/Y软元件的批量读取和批量写入;帧格式、超时时间、重连逻辑全部自己可控;另外必须留一个原始收发帧的日志开关,方便现场排查。

风险我当然评估过。跟PLC通讯协议打交道,出错轻则通讯失败,重则可能把数据写进错误的地址,所以安全措施必须做足。后文我会专门讲地址校验和结束码检查是怎么做的,这些不是可选项,是必须项。

2. MC 3E帧协议核心拆解:从请求到响应的完整帧结构

2.1 3E帧的两种形态:二进制帧与ASCII帧

MC协议里,3E帧是面向以太网的帧格式,也是现在三菱PLC以太网通讯的主流。它分二进制和ASCII两种形态,结构逻辑完全一样,区别只在于数值是用原始字节表示还是用ASCII字符表示。二进制帧的子帧头是0x5000两个字节,ASCII帧则是50 30 30 30这四个ASCII字符。

我直接选了二进制,理由很实际:数据量小、解析快、不用来回做进制转换。前提是把PLC侧通讯设置的"通讯数据代码"选成二进制,绝大多数三菱PLC默认就是二进制,基本不用改。后面讲帧结构也都是按二进制模式来讲。

2.2 请求帧逐字段解析

一个标准的MC 3E二进制请求帧长这样(以批量读取D0开始的10个字为例):

50 00 00 FF FF 03 10 00 0B 00 04 01 00 00 A8 00 00 00 00 0A 00

每个字段的含义可以对着下表看:

字段长度本例取值说明
子帧头2字节50 00固定0x5000,标识3E二进制帧
访问路径2字节00 FF网络号+PC号,常规填0x00和0xFF
I/O编号2字节FF 03固定0x03FF,小端序存储
监控定时器2字节10 00单位250ms,0x0010即4秒
请求数据长度2字节0B 00从"命令"字段到帧尾的字节数
命令2字节04 010x0401批量读取
子命令2字节00 000x0000表示读
软元件代码2字节A8 000x00A8对应D寄存器
起始软元件编号3字节00 00 00小端序,这里是D0
软元件点数2字节0A 00读取10个字

这里有个地方特别容易错:请求数据长度是从命令字段开始算的,不包含前面的子帧头、访问路径、I/O编号和监控定时器。批量读取时这个值固定是11,也就是命令2字节+子命令2字节+软元件代码2字节+起始编号3字节+点数2字节。

提示:我见过不少人把长度字段从头开始算,结果PLC一直回结束码0x1022"请求数据长度错误"。判断标准很简单——你数到多少个字节参与计算,这个字段就等于多少。

2.3 响应帧结构与结束码

批量读取的响应帧长这样(假设D0到D9的值分别是1到10):

50 00 00 FF FF 03 10 00 1C 00 04 01 00 00 16 00 00 00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 00 09 00 0A 00

响应帧的头部结构跟请求帧一致,但多了一个"数据长度"字段,位于子命令之后。这个字段表示从"结束码"开始到数据区末尾的总字节数。对上述例子来说,结束码2字节加20字节数据,所以数据长度是0x0016也就是22。响应数据长度字段则是从命令字段到帧尾的字节数,0x001C等于28。

结束码是判断通讯是否正常的核心,0x0000表示正常,其它值都代表PLC侧明确返回了异常。常见结束码我整理了一下:

结束码含义常见原因
0x0000正常无
0x1001命令未定义不支持该命令或软元件代码
0x1010软元件范围错误起始地址越界
0x1011软元件数据错误起始地址加点数超出范围
0x1020帧格式错误帧长度或字段排列不对
0x1022请求数据长度错误长度字段算错
0x1023监控定时器错误定时器值不受支持

对于写入操作,响应帧没有数据区,结束码之后直接结束。看到结束码非零的第一反应应该是查通讯库代码和你的参数,而不是怀疑PLC坏了——绝大多数情况都是上位机这边的问题。

3. 通讯库的层次设计与连接管理策略

3.1 三层架构:连接层、帧处理层、业务层

通讯库最怕的就是做成一个大杂烩VI,所有逻辑堆在一起,后面想扩展一种软元件或者换帧格式都没法下手。我最终把整个库分成三层。

连接层负责TCP连接的打开、关闭、发送和接收原始字节,同时处理超时和半包读取。这一层不关心帧内容,只保证"字节流能可靠地发出去、按数量读回来"。帧处理层负责把业务参数组装成标准帧,解析响应帧,检查结束码。以后想扩展ASCII模式或者4E帧,只改这一层就行。业务层是面向使用者的高层VI,比如"读取D区连续50个字"、"把M10置ON",使用者不用碰任何字节。

对应的LabVIEW工程结构大致是这样:

MC3E_Library.lvlib ├─ Connection Layer │ ├─ MC3E Open.vi │ ├─ MC3E Close.vi │ └─ MC3E Send Receive.vi ├─ Frame Layer │ ├─ MC3E Build Read Request.vi │ ├─ MC3E Build Write Request.vi │ ├─ MC3E Parse Response.vi │ └─ MC3E Check End Code.vi └─ Business Layer ├─ MC3E Read D.vi ├─ MC3E Write D.vi ├─ MC3E Read M.vi └─ MC3E Write M.vi

业务层VI只需要露出PLC的IP、端口、起始地址、点数、数据数组和错误簇这几个接线端,内部调用帧处理层和连接层。这样写出来的代码,哪怕过半年再看,也能一眼知道每个VI是干什么的。

3.2 连接管理与超时策略

TCP连接我用长连接,打开一次之后反复复用,不要在每次读写时都重新开关连接。PLC侧对于频繁建连并不友好,长连接的响应速度也稳定得多。打开连接的超时建议设在2到3秒,如果连不上要能明确区分"连接失败"和"响应超时"这两种情况——前者是网络不通或IP端口错了,后者是PLC忙或者通讯参数不对。

超时参数我建议这样设计:通讯库内部的读超时设置为监控定时器的两倍。比如帧里填的监控定时器是4秒,那读取响应的超时就给到8秒。为什么?监控定时器是PLC侧的处理上限,但TCP栈、操作系统调度、网络延迟都会额外消耗时间,只给4秒很容易在PLC正常但系统繁忙时误报超时。

重连逻辑也要做成可配置的。产线场景里PLC偶尔会断电重启或程序重新下载,此时TCP连接会断掉。我的做法是业务层捕获到连接错误后,自动执行"关闭-等待-重开"的流程,重开成功后再把当前读写请求重发一次。这个逻辑在无人值守的上位机里非常关键,不然半夜某台PLC重启一下,第二天早上看板上全是通讯报警。

4. 核心VIs的实现细节与核心逻辑

4.1 批量读取帧的构造

在LabVIEW里构造请求帧,思路就是建一个U8数组,按字段顺序往里拼。不要用Flatten to String把数值直接转成字符串发出去,因为Flatten默认按大端序输出,而三菱协议要的是小端序。我在Block Diagram里习惯用Build Array配合Join Numbers的拆分输出,把U16拆成低字节和高字节,手动拼出小端序。

读取D区请求帧的伪代码逻辑:

-- 伪代码:构造D区批量读取帧 frame = [0x50, 0x00] -- 子帧头 frame += [0x00, 0xFF] -- 访问路径 frame += [0xFF, 0x03] -- I/O编号(0x03FF小端) frame += [0x10, 0x00] -- 监控定时器(4秒) frame += [0x0B, 0x00] -- 请求数据长度(11) frame += [0x04, 0x01] -- 命令:批量读取 frame += [0x00, 0x00] -- 子命令:读 frame += [0xA8, 0x00] -- 软元件代码:D frame += [startAddr低位] -- 起始编号3字节 frame += [startAddr中位] frame += [startAddr高位] frame += [点数低位, 点数高位] -- 软元件点数

起始编号这3字节是从一个U32数值里拆分出来的,LabVIEW里可以用Split Number或者移位运算取8位一组。有一个细节:起始编号和点数用U16和U32都行,但拼接前一定要确认数值范围,起始编号加上点数不能超过软元件的最大地址,这个校验放在业务层做,能避免因为地址写错而操作到非预期区域。

4.2 响应解析与数据还原

响应解析的可靠性直接决定通讯库稳不稳。我的策略是分两步读取:先读固定长度的头部,再从头部里解析出数据长度,继续读完剩余字节。头部固定16字节,即子帧头2字节、访问路径2字节、I/O编号2字节、监控定时器2字节、响应数据长度2字节、命令2字节、子命令2字节、数据长度2字节。之后还需要读的字节数就等于数据长度字段的值。

读完以后先检查结束码。如果非零,直接把这个结束码映射成中文错误信息放进错误簇返回,业务层不需要自己猜。如果正常且是读取操作,数据区就是每个软元件2字节、小端序排列。D区读出来的原始值是U16,上位机展示时再把U16转换成业务需要的数值类型。

提示:PLC回传的数据到底按无符号还是有符号解释,取决于PLC程序里存的什么。通讯库层面我统一按U16返回,业务层再决定怎么转。如果通讯库层面就擅自按I16解释,遇到温度传感器传负值时,你会得到一堆65535级别的异常数字,排查起来非常绕。

4.3 批量写入帧的构造

写入帧跟读取帧的区别主要在三处:命令换成0x1401,子命令换成0x0001,数据区多出要写入的二进制内容。请求数据长度也变成了11加上点数乘以2。

写D0到D9共10个字的请求帧长这样:

50 00 00 FF FF 03 10 00 1F 00 14 01 00 01 A8 00 00 00 00 0A 00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 00 09 00 0A 00

注意请求数据长度是0x001F,也就是31,正好等于11加20字节数据。写入操作没有数据区回传,所以响应非常短,固定18字节,结束码位于最后两个字节。收到响应后同样先检查结束码,再判定写入成功。

4.4 位软元件读写与32位数据处理

M、X、Y这类位软元件跟D区最大的不同在于:它们按"位"寻址,但传输时按"字"打包。读取M0到M15共16个点,响应数据区只有2个字节,bit0对应M0,bit1对应M1,以此类推。如果你要读的位数不是16的整数倍,比如读17个M点,协议会按2个字、4字节返回。所以在实用层面,建议位软元件总是按16的倍数来读,这样解析逻辑最简单,也不会出现多余的填充位。

写入位软元件时,要把每个bit状态拼成16位字再放进数据区。比如要把M0和M3置ON、其它M点置OFF,那这个字的二进制就是第0位和第3位为1,即0x0009,按小端序发送09 00。

32位数据处理是D区最常见的需求。三菱PLC的32位寄存器由两个连续的16位D组成,低字在前。比如32位数据存在D0D1里,D0是低16位,D1是高16位。我在通讯库的业务层专门写了一个"读取32位字"的VI,先按两个字读出,再用LabVIEW的Join Number把D0的值作为低16位、D1的值作为高16位组合成U32,最后按业务需要转换成I32或者浮点。千万不要把顺序弄反,反过来读出来的值完全是错的,而且这种错误在界面上很难一眼发现。

5. 实测踩坑全记录:从Demo跑通到稳定上线之间的事

5.1 监控定时器的单位和误填问题

监控定时器字段是2字节,单位是250毫秒,0x0010代表16乘250毫秒等于4秒。这个字段在实际使用中最容易犯的错有两个:一是把0x0010直接理解成10秒或者10毫秒,二是某些示例代码里直接写0x10,结果实际超时时间只有250毫秒乘16,也就是4秒,虽然碰巧一样,但如果你本想设的是10秒,那就差远了。

还有一个更隐蔽的坑:监控定时器太小会让PLC在忙的时候直接放弃处理。我刚开始测试时图省事,把定时器改成0x0002,也就是500毫秒,想着"反正PLC响应快"。结果PLC程序里有一段比较长的气缸动作逻辑,扫描周期偶尔飙到600毫秒以上,这时我的请求就频繁超时。后来把监控定时器统一改成0x0010配合客户端8秒读超时,就再没出现过这种"伪超时"。

5.2 小端字节序与LabVIEW的类型转换陷阱

三菱MC协议全线走小端字节序,而LabVIEW里最容易埋雷的是数据类型转换。Flatten to String默认按大端网络序输出,如果你图省事把数值Flatten成字符串直接发出去,帧里所有多字节字段的字节序都是反的,轻则地址读错,重则通讯直接被拒。正确的做法是像我前面说的那样,用Join Numbers的拆分输出手动组字节。

读取D区数据时,响应帧里的每两个字节也要手动还原成U16,公式是"低位字节加高位字节乘以256"。用Type Cast把两个字节直接转成U16也可以,但必须确认当前平台确实是小端序。为了保证库的可移植性,我全部用移位拼接,不做任何平台假设。

多个D寄存器组成32位数据时,还要注意PLC侧的排列顺序。之前我接过一个项目,PLC程序里把温度值写在D20D21,上位机按D21D20读出来,界面上的温度数字直接翻了几百倍。查了半天才发现是高低字顺序错了,跟PLC程序一核对,规范里写得很清楚:低字在前。这个顺序谁都不能改,只能上位机适配。

5.3 X/Y软元件的八进制地址陷阱

X和Y这类软元件在三菱PLC里沿用了继电器时代的八进制编号习惯:X0到X7,接着是X10到X17,然后是X20到X27,逢八进一。但是在MC 3E帧协议里,起始软元件编号字段填的是十六进制数值,它表示的是这个软元件在整个地址空间里的序号。

举个例子,X20在八进制编号里等于十进制的16,换成十六进制也就是0x0010,帧里要填10 00 00这3个字节。如果你看到网上示例直接把X20填成0x0020,那寻址就偏到X40去了,读回来的数据自然全是错的。这类问题特别难排查,因为PLC不会报地址错误——X40确实存在,通讯正常,但数据就是不对。

我的解决办法是在业务层放一个小工具VI,输入字符串形式的软元件名,比如"X20",自动做八进制到十六进制的转换,然后才组帧。这样使用者不用记这层规则,也从根本上杜绝了手算地址的失误。

5.4 TCP粘包与读半包的接收策略

TCP是字节流,没有消息边界。理论上一次发送对应一次接收,但工业现场网络状态复杂,一帧响应被拆成两次到达、或者连续两次请求的响应合并成一次到达,都是常态。如果代码里只调用一次TCP Read,读到的数据可能不完整,也可能多出来一部分,解析必然错乱。

我的策略是:每次读写请求严格保持一问一答的串行机制,一个请求发出后,必须把整个响应帧按前面说的分两步读完整,才允许发下一个请求。读取时用循环累加收到的字节数,直到凑齐目标长度为止,TCP Read的超时设置得比正常响应时间宽裕很多。这样既不丢数据,也不会把两次响应的内容混在一起。实测下来,只要守住"串行读写加循环读满"这两条,粘包问题基本不会出现。

6. 性能实测与现场落地的效果

6.1 读写性能数据

在普通工控机上,用千兆网直连某型号Q系列PLC,连续批量读取D区1000个字,单次事务耗时大概在2到4毫秒。这个速度对于SCADA刷新、数据看板、配方下发这类场景完全够用。如果按每次读500个字、每秒刷新一次的频率跑,CPU占用几乎可以忽略。

速度瓶颈主要不在上位机,而在PLC侧。PLC程序里如果启用了通讯中断处理,响应会明显更快;否则请求可能要等到当前扫描周期结束才被处理。所以现场优化时除了调上位机,还应该检查PLC程序的通讯中断设置,这一步经常被人忽略。

6.2 批量读写的本质差异

通讯开销的大头是帧头和往返延迟,而不是数据本身。把要读的数据尽量合并进一个批次,性能差异是数量级的。我见过一个改造案例:原程序一次读1个字,循环500次,总耗时接近15秒;改成一次读500个字之后,总耗时不到半秒。两者差出的30倍性能,不是靠换硬件得来的,纯粹是把500次往返压缩成了1次。

这个原则在写入场景同样适用。配方下发时,把几百个参数分成几条批量写入帧,比一条条写快得多。代价是需要把数据按软元件类型分组、拼接成连续地址块。我在业务层提供了一个"按起始地址自动合并连续段"的工具VI,批量下发前先对点表做一次合并,能合并的合并,不能合并的分段写入。

6.3 实测中的个人经验建议

这个通讯库在产线上已经跑了大半年,三台PLC轮询刷新,稳定得让我一度忘了它的存在。如果让我再选一次,我大概率还是走自研这条路——不是否定现成组件,而是通讯这个环节,透明度比省事重要得多。

最后再分享一个让我受益最多的小建议:务必在通讯库里留一个记录原始收发帧的开关,把每次请求的完整字节和响应字节存成文本日志。现场出问题的时候,打开这个开关跑一轮,对着协议文档一看就知道是地址错、长度错还是字节序错。这个开关帮我解决过的疑难杂症,比任何调试器和抓包工具加起来都多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询