ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#三菱PLC FX5U通信DEMO:Socket与MC协议3E帧全解析

C#三菱PLC FX5U通信DEMO:Socket与MC协议3E帧全解析 简介一份面向C#开发工程师与工业自动化工程技术人员的三菱FX5U PLC通信示例工程完整展示了如何在.NET框架下通过以太网与三菱FX5U系列PLC建立连接、读写内部寄存器并处理各类通信异常解决上位机与PLC数据交互时的常见难题。资源包共38个文件除完整C#项目源码外还包含依赖DLL动态库、可执行程序、Config配置、窗体资源及工程设置文件整体压缩包约310KB采用标准WinForms窗体结构便于快速定位程序入口、界面设计及配置参数。目前已有2153人学习下载实用性受到较多关注。通过学习这段DEMO能够系统理清PLC通信中连接管理、数据模型、读写操作、错误处理、定时任务与日志记录等关键模块的代码组织方式掌握FX5U内存映射、寄存器寻址以及通信协议的调用细节。适合有一定C#基础、希望深入工业通信开发的读者可作为后续工业数据采集、设备监控或控制程序的可复用工程参考。1. C# 三菱PLC FX5U 通信DEMO源代码从Socket到报文的全链路拆解车间里最常见的需求之一就是上位机要读三菱FX5U的数据——产量、温度、报警、设备状态全得从PLC里掏出来。我拆过不少号称「拿来就能跑」的通信DEMO大部分要么只贴了核心代码不解释报文格式要么压根没处理异常。这份C#三菱PLC FX5U通信DEMO源代码属于能直接落地的类型它走以太网MC协议覆盖了Socket连接、3E帧构造、报文解析、批量读写和常见坑位处理。适合刚接触PLC通信的C#上位机开发者也适合那些已经写过ModbusTCP、想切换到三菱MC协议的人。整个拆解过程中我最有感触的一点是三菱的MC协议远看像黑匣子近看就是一套固定模板的字节拼装游戏。难点不在协议本身而在工程里的细节——站号怎么填、批量读上限是多少、浮点数高低字怎么颠倒。这些在这份DEMO里几乎都有对应处理下面按我的拆解顺序一条条说。2. MC协议与FX5U以太网通信3E帧结构拆解与选型逻辑2.1 为什么FX5U首选以太网MC协议而非串口三菱FX5U自带以太网口用MC协议Melsec Communication Protocol走TCP/IP是最省事的路。相比串口通信以太网的优势非常明显不需要额外买通信板卡、传输速度快、支持多个上位机同时连接FX5U默认允许16个连接。我见过不少老工程师还在用FX3U时代的思路给FX5U配串口结果绕了一大圈——FX5U的串口模块也要额外订购速率上限锁在115200bps而以太网默认就跑到100Mbps。MC协议有多个帧类型FX5U的以太网通信主要用3E帧。3E帧又分二进制和ASCII两种编码方式。这份DEMO用的是二进制帧因为同样的数据量二进制帧更短解析时不用做字符到字节的转换代码量少出错概率低。ASCII帧的好处是抓包时人能直接看但上位机开发时多一层转换开销非必要不建议。选择3E帧还有一个隐蔽的理由FX5U以太网可以同时承载MC协议和SLMPSeamless Message Protocol即MC协议以太网版的正式名称两者本质是同一套帧格式。所以网上搜SLMP的资料完全适用于这份DEMO的报文构造学习资料比想象中多。2.2 3E帧结构逐字节拆解与报文模板3E帧二进制格式的核心结构分两部分请求帧和响应帧。请求帧由副头部、网络号、PLC站号、请求数据长度、监视定时器和命令数据组成。副头部固定为0xD0002字节标识3E帧网络号默认0x00PLC站号默认0xFF表示允许任意站号应答。命令数据里最核心的是命令字段和子命令字段。读命令为0x01 0x04字软元件读取写命令为0x01 0x14字软元件写入。后面跟软元件代码如D寄存器是0x00A8M线圈是0x0090、起始地址、读取点数。地址计算方式是一般人最容易卡住的地方。三菱的软元件地址是十六进制表示的比如D100的地址就是100十六进制0x64但部分软元件需要加上偏移量。FX5U的D寄存器地址直接使用十进制转十六进制即可M继电器则需要区分编号范围M0-M7679对应地址0x0000-0x1DFFM7680-M8191需要额外处理。DEMO里封装了地址转换函数核心思路是「按软元件类型查表决定直接映射还是加偏移」。2.3 帧格式参数对照表字段字节数值/说明副头部20xD000标识3E帧网络号10x00通常固定PLC站号10xFF允许任意站应答请求数据长度2从命令字段到数据末尾的总字节数监视定时器20x0010单位250ms即4秒超时命令20x0104读0x0114写子命令20x0000固定软元件代码20x00A8D寄存器0x0090M线圈起始地址3软元件的十六进制地址读取点数21-960字单位数据写时N写入值每个字2字节2.4 响应帧结构与异常码识别响应帧比请求帧简单副头部、网络号、PLC站号、响应数据长度、命令字段、子命令字段、结束代码、数据区。结束代码是关键——0x0000表示正常非0值代表具体异常。常见异常码有0xC051请求长度错误、0xC052软元件地址超范围、0xC054请求点数超限、0xC056软元件代码指定错误。这些不是靠猜的三菱手册里列得清清楚楚。我第一次联调时看到0xC052直接懵了后来查手册才知道是地址算错了——PLC端站号设置成了0而帧里填了0xFF看似无关的两个字段组合起来就触发了地址校验失败。提示帧里的站号字段建议直接填0xFF。FX5U默认允许任意站号请求填具体站号反而可能因为PLC侧设置不一致导致请求被丢弃。2.5 这份DEMO的代码组织方式DEMO的代码结构属于「单文件能跑、分文件能扩」的类型。核心是一个McProtocolClient类里面包含Connect、Disconnect、ReadDevices、WriteDevices四个主要方法外加地址转换和响应解析的私有方法。没有用复杂的依赖注入或MVVM框架这一点我认为是对的——通信DEMO的首要任务是让人看懂协议交互框架越薄越好。更难得的是DEMO里附带了报文打印功能每次收发都会在控制台输出十六进制报文。调试时这个功能就是后悔药——一旦通信异常把报文贴出来和手册对比基本能五分钟定位问题。我将要在下一章展开它的核心实现细节。3. C# Socket通信实现帧构造、报文解析与参数说明3.1 Socket连接初始化与超时处理C#操作TCP连接最直接的方案是System.Net.Sockets.TcpClient。DEMO里的连接部分没有用异步方式而是同步带超时控制——通过IAsyncResult轮询判断连接是否成功避免PLC掉线时UI线程卡死。public bool Connect(string ip, int port, int timeoutMs 3000) { _tcpClient new TcpClient(); IAsyncResult result _tcpClient.BeginConnect(ip, port, null, null); bool connected result.AsyncWaitHandle.WaitOne(timeoutMs); if (!connected) { _tcpClient.Close(); throw new TimeoutException($PLC连接超时{timeoutMs}ms请检查IP和端口); } _tcpClient.EndConnect(result); _tcpClient.ReceiveTimeout 3000; _tcpClient.SendTimeout 3000; Console.WriteLine($[INFO] 已连接 {ip}:{port}); return true; }这里有几个参数值得展开FX5U的默认MC协议端口是5000UDP是5001但MC二进制3E帧走TCP时用5000不是某些资料里写的2000。WaitOne(timeoutMs)的作用是阻塞等待连接结果3秒不够就抛异常这个值在工业现场建议设到5秒——有些PLC在断电重启后网络栈起来需要2到4秒时间3秒太激进。连接建立后ReceiveTimeout和SendTimeout也要显式设置。TcpClient默认的读写超时是0无限等待一旦PLC断线但TCP连接还半开着你的读请求会卡死在那里。设置3秒超时后至少能快速抛异常方便上层做重连。3.2 请求帧构造封装帧构造是DEMO里最值得参考的部分。它不是简单地把字节拼起来而是提供了一个带参数校验的封装方法调用方只需要传软元件类型、起始地址和点数内部自动完成地址转换和长度计算。public byte[] BuildReadFrame(string deviceType, int startAddress, int points) { var command new byte[2]; var subCommand new byte[2]; var deviceCode GetDeviceCode(deviceType); var addressBytes GetAddressBytes(startAddress); var pointBytes BitConverter.GetBytes((ushort)points); var dataLength 12 points; // 命令子命令设备码地址点数 using (var ms new MemoryStream()) { ms.Write(new byte[] { 0xD0, 0x00 }, 0, 2); // 副头部 ms.WriteByte(0x00); // 网络号 ms.WriteByte(0xFF); // 站号 ms.Write(BitConverter.GetBytes((ushort)dataLength), 0, 2); ms.Write(new byte[] { 0x00, 0x10 }, 0, 2); // 监视定时器 ms.Write(command, 0, 2); ms.Write(subCommand, 0, 2); ms.Write(deviceCode, 0, 2); ms.Write(addressBytes, 0, 3); ms.Write(pointBytes, 0, 2); return ms.ToArray(); } }核心逻辑都在最后的数据长度计算上12 points这12个字节是指命令2字节、子命令2字节、软元件代码2字节、起始地址3字节、点数2字节加监视定时器2字节的总和不包括前三段副头部、网络号、站号。这个长度计算是三菱协议最玄学的地方之一——多算或少算一个字节PLC直接返回错误码C051。地址转换的细节也藏在代码里。D寄存器这类字软元件起始地址的3字节结构是低2字节是实际地址的十六进制值第3字节是扩展段通常为0。但有部分软元件需要加偏移比如FX5U的M软元件超过7679时地址空间要跳到0xE000之后。DEMO里GetAddressBytes内部处理了这个逻辑不熟悉的开发者直接拿原始编号去拼帧大概率会触发地址超范围错误。3.3 响应解析与字节序处理响应解析比请求帧构造更容易出错因为三菱的字节序是低字节在前和C#的BitConverter默认行为一致但和人的阅读习惯正好相反。public bool ParseReadResponse(byte[] response, out ushort[] values) { values null; if (response.Length 9) return false; ushort endCode BitConverter.ToUInt16(response, 9); if (endCode ! 0) { Console.WriteLine($[错误] 响应异常码: 0x{endCode:X4}); return false; } int dataStartIndex 11; int dataCount (response.Length - dataStartIndex) / 2; values new ushort[dataCount]; for (int i 0; i dataCount; i) { values[i] BitConverter.ToUInt16(response, dataStartIndex i * 2); } return true; }结束代码的位置固定在偏移量9处也就是响应帧的第10和第11个字节。如果正常数据区从偏移量11开始每2字节对应一个字软元件的值。一个关键点响应的结束代码也可能是1字节的这取决于PLC的响应格式设置。DEMO按2字节解析对应FX5U默认设置。如果你的PLC改过配置需要同步调整解析逻辑否则会得到一坨乱码。判断方法很简单——正常响应时第10字节是0x00、第11字节是0x00如果看到第10字节是0x00而第11字节是奇异值赶紧查PLC侧通信格式设置。3.4 读写操作的统一入口DEMO把读写操作封装成统一入口不需要调用方关心帧构造和解析的细节public ushort[] ReadD(int startAddress, int points) { byte[] frame BuildReadFrame(D, startAddress, points); byte[] response SendAndReceive(frame); if (ParseReadResponse(response, out ushort[] values)) return values; return new ushort[points]; } public bool WriteD(int startAddress, ushort[] values) { byte[] frame BuildWriteFrame(D, startAddress, values); byte[] response SendAndReceive(frame); return ParseWriteResponse(response); // 写响应只需判断结束代码 }SendAndReceive内部做了锁同步lock关键字防止多线程同时收发导致响应错乱。这个细节在DEMO里可能不起眼但在真实项目里相当重要——如果上位机有多个定时器同时读不同地址不加锁的话TCP流里的数据会串包A请求的响应被B请求收到解析出来全是乱码。关于读点数上限FX5U的3E帧单次读取字软元件最多960点位软元件是2560点。DEMO里的BuildReadFrame没有硬限制但我在实际项目里会加一层保护超过960自动拆包避免响应帧长度超限导致解析异常。这个限制在手册里有但大多数人不会去翻拆这份DEMO时我特意验证过。4. DEMO的读写扩展定时轮询、批量操作与工程量换算4.1 基于System.Timers.Timer的轮询架构DEMO只提供了通信方法但实际工程里还要解决「什么时候读、多久读一次」的问题。我一般会用System.Timers.Timer做定时轮询而不是System.Windows.Forms.Timer——后者依赖UI线程消息循环窗体最小化或拖拽时会有延迟前者跑在线程池上定时精度好得多。private Timer _pollTimer; private readonly McProtocolClient _plc new McProtocolClient(); void StartPolling(int intervalMs 500) { _pollTimer new Timer(intervalMs); _pollTimer.Elapsed (s, e) { try { ushort[] temps _plc.ReadD(100, 4); // 读D100-D103 ushort[] counts _plc.ReadD(200, 2); // 读D200-D201 UpdateUI(temps, counts); // 更新界面 } catch (Exception ex) { Console.WriteLine($[轮询异常] {ex.Message}); _plc.Reconnect(); // 断线重连 } }; _pollTimer.AutoReset true; _pollTimer.Start(); }轮询间隔的设定要结合PLC扫描周期。FX5U默认扫描周期在1-10ms之间但通信处理本身有延迟。500ms的间隔是相对稳妥的起点——太快低于100ms会导致PLC通信负载过高影响扫描周期稳定性太慢则数据刷新不及时。还有一点Elapsed事件里不要直接操作UI控件。跨线程更新UI在WinForm里会抛异常正确做法是通过Invoke或BeginInvoke封送到UI线程。DEMO里没体现这部分但实际写上位机时这是必修课。4.2 批量写操作的字节组装写操作比读操作多一个环节要把写入值转换成字节数组并按三菱的字节序排列。DEMO里BuildWriteFrame的写法很清晰public byte[] BuildWriteFrame(string deviceType, int startAddress, ushort[] values) { int dataLength 12 values.Length * 2; byte[] frame new byte[dataLength]; int offset 0; // 到这里为止帧头和读请求构造相同 frame[offset] 0x01; frame[offset] 0x14; // 写命令 frame[offset] 0x00; frame[offset] 0x00; // 子命令 // ...软元件代码、起始地址、点数... foreach (ushort value in values) { byte[] bytes BitConverter.GetBytes(value); frame[offset] bytes[0]; // 低字节在前 frame[offset] bytes[1]; } return frame; }BitConverter.GetBytes返回的字节数组天然是低字节在前符合三菱的协议要求。写多个连续寄存器时顺序就是地址从低到高依次排列。这里要特别提醒float类型的写入需要先做转换。C#的浮点数在内存里的布局和PLC里的D寄存器双字组合方式不同——三菱是高位字在后大端字序、小端字节序的组合直接用BitConverter.GetBytes(float)会得到反序的四个字节需要手动反转第二和第三个字的位置。4.3 批量读写D寄存器与M线圈的差异D寄存器和M线圈的读写差异不只是软元件代码不同底层语义也完全不同。D寄存器是16位字软元件读写以字为单位M线圈是位软元件一个字节包含8个位状态。读写M线圈时起始地址是0.0格式的位地址读取点数也是位点数。// 读M100-M1078个位的DEMO用法 ushort[] bits _plc.ReadM(100, 8); // 返回值每个ushort只有最低位有效0或1 // 批量写M线圈 _plc.WriteM(100, new ushort[] { 1, 0, 1, 0, 1, 1, 0, 1 });但实际工程里我更推荐把位状态组装成字节FX5U的M软元件地址连续时8个位正好对应一个字节的8个bit。这样上位机读一次ReadM(100, 8)就能拼出一个完整字节对应设备的8个传感器状态。这个技巧在DEMO里没有明说但在注释里提了一嘴M线圈读取的返回数组最低位就是当前位的值。4.4 从原始值到工程量数据转换的两种典型场景DEMO读出来的是ushort原始值但现场数据往往表现为温度、压力、转速等工程量。以温度为例常见传感器用0-10V模拟量输入FX5U的模拟量模块转换后得到0-4000的数字量实际温度0-100度线性关系就是温度 原始值 / 4000 * 100。ushort raw _plc.ReadD(300, 1)[0]; double temperature raw / 4000.0 * 100.0; Console.WriteLine($当前温度: {temperature:F1} ℃);如果是转距、扭矩这类带符号的值则要注意ushort和short的转换。FX5U的D寄存器以二进制补码存储负数C#里直接(short)raw就能还原带符号值。DEMO有一个示例读扭矩的代码核心就是这一行转换很多新人在这里踩坑——把0xFF9C-100当成65436读出来还奇怪PLC数据为什么不对。提示PLC端如果启用了整数格式的BCD码存储上位机解析时还要额外做BCD到二进制的转换。DEMO默认处理的是二进制格式如果你的PLC程序里用了BCD指令记得改解析逻辑。5. 通信排障与避坑超时、字节序、批量上限四类翻车现场5.1 现象连接正常但读取超时无响应第一次联调这份DEMO时IP能ping通端口也能连上但发送读请求后PLC就是不回。抓包发现请求已经发出去了PLC就是不应答。折腾了半小时最后发现是PLC侧参数设置的问题——FX5U的以太网配置里MC协议被设置成了「UDP」模式而DEMO用的是TCP连接。PLC收到TCP包直接丢弃自然不会回响应。解决办法在GX Works3中打开FX5U的以太网端口设置确认「SLMP连接」的协议类型是TCP且端口号设为5000。改完设置需要重启PLC或刷新参数才生效这个坑几乎每个第一次接FX5U的人都会踩。5.2 现象读取D100返回的数据完全不对甚至报地址错误0xC052排查半天发现是地址转换问题。三菱PLC的地址大多是八进制或十进制表示的但帧里要的是十六进制。比如D100十进制100转十六进制是0x64填进帧里没问题但M100在三菱的表示里是十进制的100转换成十六进制0x64后还要加上M软元件的基础地址偏移。FX5U的M软元件基础地址从0x0000开始M100实际地址就是0x0064看似没错——但如果你写的是M8000特殊继电器地址就要查表了M8000的地址不是0x1F40而是0x00C8开头的特殊区段。最稳的做法是参考三菱的「软元件编号-地址对照表」手册里有附录或者直接先用GX Works3的软元件批量监视窗口把目标地址列出来对比。DEMO里的地址转换函数对D、M、X、Y做了覆盖但F功能继电器、L闩锁继电器等冷门软元件可能没做——用之前先确认自己的软元件类型在支持列表里。5.3 现象浮点数读写数值颠倒、符号不正确写入一个float值28.5PLC里看到的却是完全离谱的数字或者读出来是负数。原因就是字节序三菱PLC的单精度浮点数在D寄存器中的存储方式是低字在前、高字在后但每个字内部的字节又是小端序。C#的BitConverter.GetBytes(float)出来是4个字节的低端序排列直接塞进帧里正好反了。DEMO里对浮点数的处理是自己写了转换函数static byte[] FloatToPlcBytes(float value) { byte[] bytes BitConverter.GetBytes(value); // 交换前两个字节和后两个字节的位置字序反转 return new byte[] { bytes[2], bytes[3], bytes[0], bytes[1] }; } static float PlcBytesToFloat(byte[] bytes) { // 恢复字序再转换 byte[] corrected new byte[] { bytes[2], bytes[3], bytes[0], bytes[1] }; return BitConverter.ToSingle(corrected, 0); }在FX5U里做32位数据通信时这个步骤是绕不开的。定时器读浮点数据时如果不做字序交换数据几乎必然错乱。DEMO源码里这个函数封装好了但如果你从别的项目里移植代码要确认是否处理了这一步。5.4 现象批量读960点以上时报错C054三菱手册明确规定3E帧读字软元件一次最多960点位软元件一次最多2560点。很多人第一次写批量读取时习惯性一次性读1024或2000个点结果直接收到错误码C054。DEMO的做法是封装了一个自动拆包的方法。我在实际工程里也建议这样干设定一个最大点数常量比如500超过就拆成多次读取每次读500点最后拼接结果。这样既不会触发PLC限制也不会让单次报文过长导致网络延迟。5.5 现象读响应偶发解析失败数据错位这类问题大概率是TCP粘包/拆包导致的。MC协议没有帧头结束符靠的是「请求-响应一一对应」来分割数据。如果上次响应还没读完就发了新请求或者PLC的响应被TCP拆成了多个包解析端就会错位。解决办法SendAndReceive必须严格执行「发一帧、收一帧」的同步逻辑。DEMO里用了lock保证同一时刻只有一个请求在途但还要注意接收端要做循环读取——直到读满响应帧头里声明的长度为止。NetworkStream.Read一次不一定能读完整包下面这段是标准做法byte[] ReadFullResponse(int expectedLength) { using (var ms new MemoryStream()) { byte[] buffer new byte[1024]; while (ms.Length expectedLength) { int read _stream.Read(buffer, 0, buffer.Length); if (read 0) throw new IOException(PLC连接已断开); ms.Write(buffer, 0, read); } return ms.ToArray(); } }expectedLength从响应帧偏移量7-8字节读取。有了这个循环哪怕PLC的响应被拆成五六个TCP包到达也能完整拼出来。6. 从DEMO到工程化心跳检测、重连机制与多PLC的参数配置习惯DEMO解决的是「能用」的问题但现场要的是「一直能用」。我接手过的项目里十有八九的PLC通信问题不是协议不会写而是断线重连没做好。上位机连着PLCPLC重启一下固件上位机还在傻等然后所有人以为PLC坏了。最常见的一套工程化组合是心跳检测加断线重连。心跳的目的不是读数据而是确认链路是通的——我习惯每隔2秒发一次读请求读D0这个几乎不用的寄存器只要返回正常就说明连接健康。如果连续3次心跳失败进入重连流程先主动关闭连接再按指数退避策略重连1秒、2秒、4秒、8秒最多30秒封顶直到重连成功。void HeartbeatCheck() { if (_failCount 3) { _plc.Disconnect(); _reconnectAttempts 0; while (!_plc.Connect(_plcIp, _plcPort, 2000)) { int delay Math.Min(30000, 1000 * (int)Math.Pow(2, _reconnectAttempts)); Thread.Sleep(delay); // 指数退避避免频繁重连 } _failCount 0; } }重连成功后的第一件事是重新初始化PLC端的监视定时器不然之前的请求可能还挂在PLC的队列里导致后续响应错位。这个细节是我被坑过一次之后才记住的——有一次重连后数据一直不对排查半天发现是PLC端还留着旧请求的响应没发完。多PLC管理也是工程化的重头戏。一份DEMO拿来做单台PLC的通信说明绰绰有余但产线上经常是三台五台一起连。我会给每台PLC创建一个独立的McProtocolClient实例而不是共用一个Socket——共享连接一旦某台PLC重启所有通信都受影响。Dictionarystring, McProtocolClient plcClients new Dictionarystring, McProtocolClient { { 加热炉, new McProtocolClient(192.168.1.10, 5000) }, { 清洗机, new McProtocolClient(192.168.1.11, 5000) }, { 装配线, new McProtocolClient(192.168.1.12, 5000) } };配置参数我习惯统一放在App.config里IP、端口、超时时间、轮询间隔全做成可配置项杜绝硬编码。每次换现场环境开发人员不用改代码重新编译改配置文件就行——这个习惯在多个项目里帮我省了至少半天的人天。回看这份DEMO它的价值在于把三菱MC协议的骨架搭好了但真正让它成为生产力工具的是你往里填工程细节的过程。从那以后我每接一个新的PLC通信项目都会强制走一遍这四步先用DEMO跑通读写再改造成可配置的轮询架构然后加上心跳重连最后才是业务逻辑的接入。希望帮到你。提示文中提到的GX Works3版本号、FX5U固件版本均为通用性描述实际使用时以你手头设备和软件的具体版为准。报文中的监视定时器数值0x0010在不同的三菱手册版本里有轻微差异如果通信不稳定优先把它调大到0x0020对应5秒超时再排查其他因素。本文还有配套的精品资源点击获取
返回列表