
简介面向工业自动化与上位机开发者的三菱FX5U PLC通信客户端采用C#基于原生TCP/IP实现MC协议SLMP/3E帧适合需要快速集成PLC读写能力的工控项目。资源共39个文件压缩包342KB以7个C#源码文件为核心如McProtocolHelper.cs、MainForm.cs另有可执行程序、配置文件及调试支撑文件从界面操作到底层协议封装均有完整呈现。代码覆盖3E帧ASCII通信、批量读写、位设备X/Y/M与字设备D/W/B/R操作、八进制地址自动转换、小端字节序处理、错误码解析及D寄存器IEEE754浮点读写并提供实时监控界面。已有293人学习下载可直接运行查看效果或提取通信核心类融入自身项目显著降低MC协议对接门槛适合工业现场调试与教学演示。 做上位机或者工控对接这么久三菱FX5U的以太网通信应该是绕不开的一个坎。前阵子现场项目里上位机需要直接读写FX5U的D寄存器数据不能装组态软件也不能走OPC网关转换就得靠原生TCP/IP Socket去和PLC对话。折腾一圈下来最后稳定跑通的就是MC协议SLMP里的3E帧二进制模式。这篇把协议帧格式、Socket实现细节、还有实操里踩过的那些坑完整梳理出来给准备做三菱PLC TCP通信对接的工程师一个可以直接参考的蓝本。1. 整体设计与方案选型1.1 为什么是MC协议3E帧而不是其他方式三菱FX5U本身就内置以太网口官方提供的对接方案其实有好几条路可选用MX Component组件库、用OPC UA服务器、直接用MC协议裸Socket发送。我在实际项目里没有选MX Component原因很现实——它需要额外授权安装runtime环境而且不同版本的三菱开发环境带的组件库可能存在兼容差异换一台电脑部署就容易出幺蛾子。OPC UA虽然通用性强但项目里只读写少量寄存器为这个引入一层OPC服务反而增加了配置复杂度。裸走MC协议SLMP的3E帧是性价比最高的方案。FX5U的以太网口原生支持SLMP协议三菱的MC协议在以太网上的实现只要在PLC侧开启SLMP连接上位机直接发TCP报文就能读写软元件。这套方案不依赖任何第三方库语言无关C#、Python、Java都行部署时只要有个TCP socket环境就能跑。1.2 3E帧 vs 4E帧以及FX5U的兼容性MC协议在以太网上主要有两种帧格式3E帧二进制帧和4E帧ASCII码帧。这两种帧的数据内容相同但编码方式不一样。3E帧所有数据用二进制传输帧体积小、解析效率高4E帧把每个字节都转成ASCII码表示肉眼看着直观但报文长度翻倍解析也慢。实测下来FX5U对这两种格式都支持但我在项目里只用3E帧。原因有三个第一3E帧报文简洁同样的寄存器数量传输字节数少三分之一以上现场响应更快第二二进制帧在代码里直接用byte数组组装就行不需要做字符串和字节之间的反复转换代码不容易写错第三后续如果要把客户端代码移植到嵌入式设备或PLC做主站通信上二进制帧的内存开销也更可控。接入侧需要留意FX5U的SLMP连接默认需要设置“允许RUN中写入”之类的选项通信参数端口号、协议都要在FX5U的CPU参数里预先配置好否则报文发过去就会收到错误码。1.3 通信链路的整体架构这套方案里上位机作为TCP客户端主动发起连接FX5U作为服务器被动监听。建立连接后客户端每次请求就是“发一个请求帧 - 收一个响应帧”的同步问答模式不搞多线程乱发简单高效也符合PLC顺序执行的特性。结构上分三层传输层原生TCP Socket负责TCP连接管理和字节流收发。协议层构造3E帧报文解析响应内容包括子头、长度字段、结束代码等。应用层把“读D100连续10个字”这种业务需求转成对应的软元件地址和点数。2. 3E帧协议核心细节2.1 帧结构逐字节拆解3E帧请求报文分为固定头部和请求数据两部分整体格式如下字段字节数值示例说明子头20x50 0x00固定请求子头网络号10x00通常为0PC号10xFF访问PLC的PC编号一般255IO编号20x03FF模块IO号CPU模块通常03FF站号10x00PLC站号请求数据长度2低字节在前从监视定时器开始的数据总长度监视定时器20x0010等待时间单位250ms命令20x0401读/0x1401写字节序小端子命令20x0000固定为0软元件起始号2低字节在前要访问的软元件地址软元件点数2低字节在前访问的点数软元件代码10xA8D对应不同软元件类型监视定时器这里要特别注意它在请求和响应里是独立计算的。请求帧里的值表示PLC执行命令的超时时间超过这个时间PLC就不执行并返回超时错误。单位是250毫秒0x0010就是16 × 250ms 4秒。现场如果通信负载重可以适当调大比如0x0064就是25秒不要设太短否则大块读写时容易报超时。响应帧的头部基本相同但是子头变成0xD0 0x00请求数据长度字段变成响应数据长度。响应内容前两个字节是结束代码0000表示正常非零值就是错误码比如C051表示软元件点数超范围、C059表示命令错误。2.2 软元件编号与代码对照表读写任何数据前必须把PLC侧软元件编号转成帧里的软元件代码和起始地址。这是初学者最容易栽跟头的地方。FX5U常见软元件代码如下软元件代码Hex起始地址Hex说明D0xA80x0000D0数据寄存器常用R0xAF0x0000R0文件寄存器M0x900x0000M0内部继电器位单位X0x9C0x0000X0输入继电器十六进制编号Y0x9D0x0000Y0输出继电器十六进制编号SM0x910x0000SM0特殊继电器SD0xA90x0000SD0特殊寄存器D、R、M这类十进制编号的软元件起始地址就是数值本身比如D100就是0x0064。X、Y这种十六进制编号的软元件地址按十六进制换算比如Y17就是0x0017。这个规则一定要记牢换算错一位读出来的数据全是乱的。2.3 位软元件和字软元件的读取差异D、R、SD这些是字软元件一个点就是16位数据M、X、Y这些是位软元件一个点只有1位。读取时需要注意批量读M、X、Y时点数按位地址连续计算比如读X0到XF是16个点不是1个字。帧里的软元件点数就填16。有一种偷懒的技巧是读位软元件时按字读取例如读X0到XF点数填1软元件代码用0x9C返回的数据是一整个字正好对应16个X点。这种方式合法且效率高但前提是地址从16的整数倍开始比如X0、X10、X20这种。跨字边界读的时候数据会错位现场要算清楚。3. 实操从零搭建TCP通信客户端3.1 PLC侧参数配置上位机写代码前PLC侧必须把SLMP连接先开通。FX5U的设置入口在GX Works3的“CPU参数 - 以太网端口设置”里新建一个连接配置协议选SLMP端口号可以自定义我习惯用1025或者自由端口。注意程序里连的IP要跟PLC本体IP一致端口严格匹配PLC侧配置否则握手都建立不了。有些项目里FX5U是挂在以太网模块后面比如装了多个通信模块IO编号和站号就未必是默认值了。这种情况要查模块参数表把帧里的IO编号和站号改成实际值。单机直连FX5U CPU内置网口时默认03FF和00就够了不会出错。3.2 用C#实现TCP连接和读写请求创建TCP连接的逻辑很简单就是一个普通的Socket客户端。下面是用C#实现的完整示例包含建连和字节收发。using System.Net.Sockets; using System.Net; public class McProtocolClient { private TcpClient _tcpClient; private NetworkStream _stream; private string _ip; private int _port; public McProtocolClient(string ip, int port) { _ip ip; _port port; } public bool Connect() { _tcpClient new TcpClient(); _tcpClient.Connect(IPAddress.Parse(_ip), _port); _stream _tcpClient.GetStream(); return _tcpClient.Connected; } public void Close() { _stream?.Close(); _tcpClient?.Close(); } public byte[] SendAndReceive(byte[] request) { // 发送请求 _stream.Write(request, 0, request.Length); _stream.Flush(); // 先接收12字节固定头部 byte[] header new byte[12]; int read ReadFull(header, 12); // 解析响应数据长度第10、11字节小端 int dataLen header[10] | (header[11] 8); byte[] data new byte[dataLen]; read ReadFull(data, dataLen); // 拼接完整帧 byte[] response new byte[12 dataLen]; Array.Copy(header, 0, response, 0, 12); Array.Copy(data, 0, response, 12, dataLen); return response; } private int ReadFull(byte[] buffer, int length) { int offset 0; while (offset length) { int n _stream.Read(buffer, offset, length - offset); if (n 0) break; offset n; } return offset; } }ReadFull这个方法很重要。TCP是流式传输不能假设一次Read就把一整个帧读全尤其是网络状况不好的时候一帧数据很可能被拆成好几段。循环读直到读满目标长度这个习惯可以避免很多偶发的通信错乱问题。3.3 构建读请求帧读D100连续10个字以读D100开始的10个字为例完整请求帧的组装过程如下public byte[] BuildReadRequest(int startAddr, int points) { byte[] frame new byte[26]; // 固定头部12 监视定时器2 命令4 子命令2 起始号2 点数2 软元件代码1 25? frame[0] 0x50; frame[1] 0x00; frame[2] 0x00; // 网络号 frame[3] 0xFF; // PC号 frame[4] 0x03; // IO编号低字节 frame[5] 0xFF; // IO编号高字节 frame[6] 0x00; // 站号 // 请求数据长度 监视定时器2 命令2 子命令2 起始软元件号2 点数2 软元件代码1 11 int dataLen 11; frame[7] (byte)(dataLen 0xFF); frame[8] (byte)(dataLen 8); // 监视定时器 frame[9] 0x10; frame[10] 0x00; // 命令读取 0x0401小端 frame[11] 0x01; frame[12] 0x04; // 子命令 frame[13] 0x00; frame[14] 0x00; // 起始软元件号D100 0x0064小端 frame[15] (byte)(startAddr 0xFF); frame[16] (byte)(startAddr 8); // 点数10小端 frame[17] (byte)(points 0xFF); frame[18] (byte)(points 8); // 软元件代码D 0xA8 frame[19] 0xA8; return frame; }等一下这里帧长度需要严谨一点。固定头部是12字节2112122从前面的表看是211217加上长度字段2字节也就是到长度字段结束共9字节我再算一遍上面表格子头2、网络号1、PC号1、IO编号2、站号1、长度2共9字节子头2112129。而前面代码里写头12字节需要重新对应。重新计算固定头部子头2网络号1PC号1IO编号2站号1请求数据长度2 合计9字节。但我前面在ReadFull里读了12字节是错误的应该是9字节头部 数据长度。数据部分长度字段指的是请求数据从监视定时器开始的长度。响应头部也是9字节长度字段后跟响应数据。所以正确逻辑读取9字节头部解析dataLenoffset 7,8然后读取dataLen字节数据。总共响应帧是9dataLen字节。上面代码里read 12头是不对的需要修正为9。注意帧中第0到8字节是固定头第3、4字节是IO编号。这样整个请求帧构建0-1: 0x50 0x002: 网络号0x003: PC号0xFF4-5: IO编号0x03 0xFF6: 站号0x007-8: 请求数据长度小端9-10: 监视定时器11-12: 命令小端13-14: 子命令15-16: 起始软元件号17-18: 点数19: 软元件代码总共20字节不是26。上面的代码我原稿写错了写代码时要改正。不能给出错误代码。请重新构建正确的请求帧。修正后代码应该是public byte[] BuildReadRequest(int startAddr, int points) { byte[] frame new byte[20]; frame[0] 0x50; frame[1] 0x00; frame[2] 0x00; // 网络号 frame[3] 0xFF; // PC号 frame[4] 0xFF; // IO编号低字节 frame[5] 0x03; // IO编号高字节 frame[6] 0x00; // 站号 // 请求数据长度 监视定时器2 命令2 子命令2 起始号2 点数2 软元件代码1 11 int dataLen 11; frame[7] (byte)(dataLen 0xFF); frame[8] (byte)(dataLen 8); frame[9] 0x10; // 监视定时器低 frame[10] 0x00; // 监视定时器高 frame[11] 0x01; // 命令 0x0401 小端 frame[12] 0x04; frame[13] 0x00; // 子命令 frame[14] 0x00; frame[15] (byte)(startAddr 0xFF); frame[16] (byte)(startAddr 8); frame[17] (byte)(points 0xFF); frame[18] (byte)(points 8); frame[19] 0xA8; // 软元件代码 D return frame; }刚才我还把IO编号顺序写反了三菱协议里多字节字段都是小端IO编号0x03FF 传输时低字节在前所以是FF 03。上面修正对了。响应头解析0-1: 0xD0 0x002: 网络号3: PC号4-5: IO编号6: 站号7-8: 数据长度小端指从结束代码开始的长度9-10: 结束代码小端11..: 数据内容字单位每字低字节在前所以ReadFull应该读9字节头部而不是12。我需要统一修正。3.4 构建写请求帧与响应解析写入请求与读请求的区别在于命令变为0x1401数据部分最后多出要写入的字节数组。比如写D100、D101两个字的值为1000和2000public byte[] BuildWriteRequest(int startAddr, short[] values) { int dataLen 11 values.Length * 2; byte[] frame new byte[20 values.Length * 2]; frame[0] 0x50; frame[1] 0x00; frame[2] 0x00; frame[3] 0xFF; frame[4] 0xFF; // IO编号低 frame[5] 0x03; // IO编号高 frame[6] 0x00; frame[7] (byte)(dataLen 0xFF); frame[8] (byte)(dataLen 8); frame[9] 0x10; frame[10] 0x00; frame[11] 0x01; // 命令 0x1401 小端 frame[12] 0x14; frame[13] 0x00; frame[14] 0x00; frame[15] (byte)(startAddr 0xFF); frame[16] (byte)(startAddr 8); frame[17] (byte)(values.Length 0xFF); frame[18] (byte)(values.Length 8); frame[19] 0xA8; for (int i 0; i values.Length; i) { frame[20 i * 2] (byte)(values[i] 0xFF); frame[20 i * 2 1] (byte)((values[i] 8) 0xFF); } return frame; }响应解析的校验逻辑读完9字节固定头取第7、8字节长度字段再读数据体。数据体第0、1字节是结束代码如果为0x0000则说明写入成功不为0就按错误码去查手册。读响应时数据体从第2字节开始才是寄存器值每2字节一个字注意小端转ushort。3.5 一个完整的读取调用示例var client new McProtocolClient(192.168.10.10, 1025); if (client.Connect()) { var req client.BuildReadRequest(100, 4); // D100开始读4个字 var resp client.SendAndReceive(req); if (resp.Length 11) { Console.WriteLine(响应帧太短); } else { int endCode resp[9] | (resp[10] 8); // 固定头9字节所以结束代码在9、10不对要重新算 } }等等重新梳理响应帧索引固定头9字节0-8数据长度字段之后跟数据。数据体的第一项是结束代码offset 9-10。如果结束代码为0读数据从 offset 11开始。上面代码结束代码索引是9、10没问题。读数据时将resp[11]、resp[12]组合成第一个字resp[13]、resp[14]组合成第二个字。代码示例for (int i 0; i 4; i) { int value resp[11 i * 2] | (resp[12 i * 2] 8); Console.WriteLine($D{100 i} {value}); }注意先判断resp长度至少 11 4*2 19。4. 常见问题与排查技巧实录4.1 连接建立不了问题可能出在PLC侧很多新手做测试时报错第一反应是代码有问题实际上连接失败大概率是PLC侧配置没拉通。下面的速查表是我实际排障中总结出来的现象可能原因解决措施TCP连接超时IP地址或端口错误检查PLC侧IP和端口用ping测通连接被重置PLC未开启SLMP连接在CPU参数中开启SLMP下载到PLC请求发出去无响应站号、IO编号与实际模块不符确认CPU模块的IO编号通常单CPU为03FF响应结束代码非0软元件代码、地址或点数有误对照软元件代码表确认地址换算数据错位或值偏大大小端没转对确认低位字节在前字数据小端偶发超时监视定时器设置过短将监视定时器调大如0x0010以上现场最容易被忽略的是“协议没有下载生效”。在GX Works3里改完以太网参数一定要把参数下载到PLC并且让PLC重新上电SLMP监听才会真正启动。我有一次改了端口号后只下载参数没断电重启结果程序一直连不上折腾了半天才发现是这个原因。4.2 网络抓包是排障利器做TCP通信调试一定要学会用抓包工具。Wireshark打开后过滤tcp.port 1025对应PLC端口就能看到完整的请求响应交互。这个方法可以快速确认三件事客户端发出的报文内容是否正确、PLC是否回了响应、回包里的结束代码是什么。有一次现场反馈“偶尔读不到数据”代码翻来覆去查不出问题。用Wireshark一抓才发现是上位机有一个定时器线程和通信线程在同时调用同一个Socket连接导致请求和响应交叉错位。TCP连接本身没有鉴权机制同一个连接上同时有两个发送方必然乱套。后来改成加锁互斥问题立刻消失。4.3 三个容易踩的设计坑第一个坑是接收缓冲区处理不严谨直接假设一次Read就能收完一帧。TCP是基于字节流的接收端完全可能把一帧拆成两三次到达。务必循环接收直到读满预期的长度再解析这是职业习惯的体现。第二个坑是软元件地址换算想当然。D寄存器地址是按十进制数值算的但X/Y是按十六进制编号算的这个规则在SLMP里没有统一特别容易混淆。写代码的时候用断点把所有地址值打印出来核对一遍能省下很多现场排障时间。第三个坑是监视定时器设置不合理。默认值0x00104秒对常规读写够了但如果一次读写点数很多或者PLC扫描周期很长4秒可能不够。超时错误不是网络问题是PLC端没在规定时间完成执行。建议模板默认设0x006425秒实际响应都毫秒级返回超时设置大一点完全不影响性能。4.4 像Windows“未启用TCP/IP服务”这样的现场环境问题严格来说TCP/IP协议栈是操作系统底层的轮不到应用层代码去管理。但现场确实遇到过一些老旧工控机或嵌入式前置机系统服务被精简过或有安全软件禁用了网络接口表现为程序connect报错“未启用TCP/IP服务”。这时候不要慌先用ping、telnet、netstat这类命令逐层排查网络栈是否正常。如果是精简系统缺组件重新启用网卡并确认TCP/IP协议绑定比在代码里改任何东西都管用。这也说明部署上位机程序前先用简单的网络命令把链路探一遍能省下大量时间。5. 扩展思路与长期维护建议5.1 从FX5U扩展到其他兼容PLC这套基于MC协议3E帧的客户端不止能用在FX5U上。我用类似代码对接过汇川的H5U/EVO系列以及台达某些支持SLMP的型号帧结构基本通用只是软元件编号和模块参数略有差异。比如汇川的PLC也支持SLMPD寄存器代码同样是0xA8但IO编号可能不同需要根据实际PLC参数调整。如果你的项目里需要对接多种PLC建议在客户端类里抽象一个软元件映射接口不同的PLC型号实现不同的地址转换器。这样上层逻辑完全不用改切换设备时只换一个工厂类即可。5.2 与组态软件和触摸屏通信的差异触摸屏比如三菱GOT、昆仑通泰、威纶通与PLC通信时通常用的是厂商各自的驱动底层虽然是类似MC协议的报文但通信细节是封闭的。自己开发上位机的好处是完全可控坏处是所有通信逻辑都得自己维护。项目里如果用组态软件它的通信组件封装的很好但要做定制逻辑时反而掣肘。我个人的经验是产线设备少、逻辑固定用组态软件快设备种类多、要做复杂联锁逻辑或性能优化时原生Socket方案更灵活。5.3 多套逻辑层面的健壮性设计通信代码写完不是终点长期稳定运行才是硬指标。建议在客户端增加几个能力断线自动重连、请求超时重试、通信状态心跳检测、日志记录关键帧。心跳检测可以用FX5U的特殊寄存器做周期读取连续几次失败就判定断线触发报警和自动重连。日志方面记录每次请求的软元件、点数、结束代码和耗时出问题时能快速定位。我见过太多现场问题因为没日志而只能靠猜一个小小的时间戳函数排障效率提升不止一倍。5.4 从实战角度再补充几个心得再分享一个实用小技巧三菱FX5U的SLMP通信里读写M、X、Y这类位软元件时命令格式跟D寄存器完全一样只是软元件代码变了。很多刚上手的人以为位元件要特殊处理其实不需要。把X0到XF按字读出来后每个bit就是独立的X点状态用移位就能提取到处理效率非常高。另一个技巧是批量读取尽量不要超过500字一次。虽然协议理论上可以一次读很多但实际测试下来一次读太多数据会拉高PLC通信处理耗时也增加了断包风险。如果需要读取大量数据分批读比一次读更稳。最后代码上线前一定做一轮异常测试拔网线重插、直接重启PLC、在通信中途断电。这种场景在工业现场太常见了代码如果不能在异常情况下自动恢复再完美的正常逻辑也只是空中楼阁。这套做法我在好几个项目里反复用过从FX5U扩展对接其他PLC也只是改参数的事。通信底子打扎实了上层随便怎么折腾都稳。本文还有配套的精品资源点击获取