三菱FX5U以太网通信上位机开发:MC协议报文解析与WinForm实战
简介本资源面向工业自动化与上位机开发人员提供一套基于C#与WinForm实现三菱FX5U PLC以太网通信的完整工程源码重点解决3E帧报文构造、连接建立、数据收发及错误重试等核心问题可应用于自动化立体仓库的库存管理、货位查询与物料搬运等场景。压缩包共35个文件约79KB以cs源码文件为主辅以config配置、resx与resources资源、csproj与sln工程文件及exe可执行程序结构完整便于直接编译运行与二次开发。目前已有465人学习下载。读者可从中获取3E帧报文封装、RawSocket与TcpClient通信选型、PLC指令集解析、超时重发机制以及WinForm界面交互等具体实现思路适合具备一定C#与网络编程基础、希望快速搭建PLC上位机通信框架的开发者参考借鉴。1. 三菱FX5U以太网通信上位机从报文黑匣子到WinForm落地产线调试最怕什么PLC就在对面网线插好了可上位机就是读不到D寄存器里的产量数据。三菱FX5U自带以太网口支持SLMPSeamless Message Protocol协议理论上用C#写个WinForm就能直接读写软元件不需要额外买通信模块。但真到动手时MC协议报文怎么拼、二进制和ASCII格式怎么选、批量读取时字地址和位地址怎么换算每一步都能卡住人。这份资源围绕FX5U以太网通信的上位机程序展开核心是C# WinForm通过MC协议与FX5U直连覆盖连接建立、报文构造、数据解析和自动化立体仓库场景下的实际应用。适合做非标自动化上位机、仓储物流监控系统的C#开发者尤其是需要绕开OPC、直接走原生协议的那批人。2. MC协议报文拆解二进制格式与ASCII格式怎么选2.1 先搞清楚FX5U的通信端口和协议栈FX5U本体自带一个以太网口支持同时作为服务器和客户端。上位机主动连它走的是TCP端口默认5021用于SLMP/MC协议。这里有个容易混淆的点三菱的MC协议分两种帧格式——3E帧和4E帧。3E帧是早期Q系列、L系列的通用格式4E帧是iQ-R和FX5U原生支持的多了序列号和更长的站号字段。FX5U两种都认但如果你用4E帧报文头会多出2字节的序列号解析时偏移量要跟着变。我一般建议新项目直接上4E帧二进制格式。原因有三个二进制比ASCII省一半带宽批量读100个字节能少发几百字节4E帧有序列号可以并发发多条请求靠序列号匹配响应不用傻等二进制解析用BitConverter直接转不用像ASCII那样先转字符串再解析十六进制少一层出错可能。但二进制有个坑所有地址和长度都是小端序。比如你要读D100开始的10个字地址字段写的是0x0064但实际字节流里是64 00。第一次写的时候我在这上面翻了车发出去的报文PLC直接返回错误码查了半天才发现是字节序问题。2.2 请求报文逐字节构造以4E帧二进制批量读D寄存器为例请求报文结构如下字段长度(字节)值示例说明副头部20x54004E帧二进制固定网络号10x00通常0PLC号10xFF站内PLC固定FF请求目标模块IO20x03FF固定请求目标模块站号10x00固定请求数据长度20x0C00后续字节数监视定时器20x10004秒超时指令20x0104批量读子指令20x0000字单位首地址30x64 00 00D100小端软元件代码10xA8D寄存器点数20x0A00读10个拼成C#代码大概是这样byte[] BuildReadRequest(ushort startAddr, ushort count) { var list new Listbyte(); list.AddRange(new byte[] { 0x54, 0x00 }); // 副头部 4E二进制 list.Add(0x00); // 网络号 list.Add(0xFF); // PLC号 list.AddRange(new byte[] { 0xFF, 0x03 }); // 目标模块IO list.Add(0x00); // 目标模块站号 // 请求数据长度 后续所有字节数先占位 int lenIndex list.Count; list.AddRange(new byte[] { 0x00, 0x00 }); list.AddRange(new byte[] { 0x10, 0x00 }); // 监视定时器 4秒 list.AddRange(new byte[] { 0x01, 0x04 }); // 指令 批量读 list.AddRange(new byte[] { 0x00, 0x00 }); // 子指令 字单位 // 首地址 3字节小端 list.Add((byte)(startAddr 0xFF)); list.Add((byte)((startAddr 8) 0xFF)); list.Add(0x00); list.Add(0xA8); // D寄存器代码 list.Add((byte)(count 0xFF)); // 点数小端 list.Add((byte)((count 8) 0xFF)); // 回填数据长度 ushort dataLen (ushort)(list.Count - lenIndex - 2); list[lenIndex] (byte)(dataLen 0xFF); list[lenIndex 1] (byte)((dataLen 8) 0xFF); return list.ToArray(); }逻辑说明请求数据长度字段是从监视定时器开始到报文末尾的字节数不是整个报文长度这个点很容易算错。首地址是3字节虽然D寄存器地址不会超过0xFFFF但协议规定就是3字节第三字节补0。软元件代码D是0xA8M是0x90X是0x9CY是0x9D这些代码在手册里能查到但手册是日式英语翻译过来的第一次看容易懵。参数说明监视定时器0x0010代表4秒单位是250ms所以0x0010乘以250ms等于4000ms。如果网络延迟大可以改成0x0020即8秒。点数最大可以一次读960个字但实际项目中我一般不超过100个一是响应报文太长解析慢二是万一中间某个地址越界整条请求都失败分批读能缩小故障范围。2.3 响应报文解析与错误码处理响应报文的前11个字节和请求报文结构一致从第12字节开始是响应数据。批量读的响应里第12-13字节是结束代码0x0000表示成功其他值就是错误码。结束代码之后才是实际数据每个字2字节小端。ushort[] ParseReadResponse(byte[] resp, int count) { // 结束代码在偏移11处 ushort endCode BitConverter.ToUInt16(resp, 11); if (endCode ! 0) throw new Exception($PLC返回错误码: 0x{endCode:X4}); var values new ushort[count]; int offset 13; // 跳过结束代码 for (int i 0; i count; i) { values[i] BitConverter.ToUInt16(resp, offset); offset 2; } return values; }常见错误码0xC059表示指令/子指令错误通常是软元件代码写错了0xC051表示点数超限0xC056表示地址越界。我遇到最多的是0xC059原因是用3E帧的软元件代码去发4E帧比如把D的0xA8写成了0x44PLC直接拒绝。排查方法很简单用Wireshark抓包对比发出的字节和手册里的示例报文逐字节对一般十分钟内能找到问题。3. WinForm上位机实战连接管理、数据绑定与立体仓库场景3.1 TCP连接池与断线重连FX5U的以太网口同时支持的连接数有限官方手册写的是最多16个但实际项目中超过4个并发连接就容易出现响应延迟。WinForm上位机一般只需要一个长连接但产线环境网络抖动是常态断线重连必须做。我一般用TcpClient加一个后台线程做心跳每3秒发一次4E帧的CPU型号读取指令指令0x0101如果连续3次没响应就判定断线触发重连。重连不是简单Close再Connect要先Dispose旧的TcpClient否则端口会处于TIME_WAIT状态短时间内连不上。async Taskbool HeartbeatAsync() { try { var req BuildCpuTypeRequest(); // 指令0x0101 var resp await SendAndReceiveAsync(req, 2000); return resp ! null resp.Length 11; } catch { return false; } }参数说明SendAndReceiveAsync的超时设2000ms比监视定时器的4秒短这样即使PLC没回上位机也能快速感知。心跳指令用0x0101读CPU型号返回数据固定解析简单不会因为软元件地址变化而误判。3.2 自动化立体仓库的报文映射立体仓库场景下上位机通常要读三类数据货位状态M寄存器位单位、堆垛机坐标D寄存器字单位、报警信息D寄存器32位整数。位单位和字单位的读取指令不同位单位批量读的子指令是0x0001返回数据是每个位一个字节0x00或0x01。货位状态一般有几百个点比如16排×20列×4层1280个货位对应1280个M位。一次读1280个位响应报文长度是1280131293字节TCP一次传输没问题但解析时要注意位数据的每个字节只用了最低位高7位是0不要当成有效数据。bool[] ParseBitResponse(byte[] resp, int count) { ushort endCode BitConverter.ToUInt16(resp, 11); if (endCode ! 0) throw new Exception($错误码: 0x{endCode:X4}); var bits new bool[count]; for (int i 0; i count; i) bits[i] resp[13 i] 0x01; return bits; }堆垛机坐标是32位整数占两个连续D寄存器。FX5U的D寄存器是16位的32位数据要读两个再拼。注意字节序MC协议返回的是小端两个D寄存器拼成32位时低16位在前高16位在后。int ParseInt32(ushort low, ushort high) { return (int)((uint)(high 16) | low); }这里有个血泪经验三菱PLC内部处理32位数据时低字在前还是高字在前取决于具体指令。用DMOV指令时低16位在低地址高16位在高地址和MC协议返回的顺序一致。但如果你用浮点数FX5U的浮点数是32位IEEE754同样低字在前解析时要用BitConverter.ToSingle不能自己拼。3.3 数据绑定与UI刷新策略WinForm的DataGridView直接绑定List货位状态时如果每秒刷新一次1280行数据会导致UI卡顿。我一般用BindingList加虚拟模式或者干脆用Timer每500ms读一次但只更新变化的行。更稳的做法是后台线程负责通信和解析解析结果放到ConcurrentQueueUI线程用Timer每200ms从队列取最新数据只刷新DataGridView的可见区域。这样即使通信线程偶尔阻塞UI也不会卡死。// 后台线程 while (running) { var data ReadWarehouseStatus(); _queue.Enqueue(data); Thread.Sleep(500); } // UI线程 private void UiTimer_Tick(object sender, EventArgs e) { if (_queue.TryDequeue(out var latest)) { // 只更新可见行 foreach (DataGridViewRow row in dataGridView1.Rows) { if (row.Visible) UpdateRow(row, latest); } } }参数说明Thread.Sleep(500)是通信周期实际项目中要根据PLC扫描周期调整。FX5U的扫描周期一般在1-10ms500ms读一次完全够用。如果产线节拍快可以降到200ms但要注意TCP缓冲区别让请求堆积。4. 避坑与排查FX5U以太网通信的五个翻车现场4.1 现象连接成功但读不到数据PLC返回0xC059原因软元件代码错误。最常见的是把D寄存器的0xA8写成了0x44那是3E帧的代码或者把M寄存器的0x90写成了0x91。FX5U的4E帧二进制格式和3E帧的软元件代码表不完全一样不能混用。解决查FX5U用户手册SH-081219的MC协议章节里面有完整的软元件代码表。或者用GX Works3的通信测试工具先确认PLC能正常响应再对比自己发的报文。4.2 现象批量读100个字返回数据只有前50个原因TCP粘包。FX5U的响应报文可能分多个TCP包发送如果Receive只调一次拿到的可能是不完整的报文。特别是数据量大时第一个包可能只包含报文头和前几十字节。解决根据报文头里的请求数据长度字段循环Receive直到收满。或者用NetworkStream的DataAvailable属性判断。我一般封装一个ReadExact方法确保读满指定字节数。byte[] ReadExact(NetworkStream stream, int length) { var buffer new byte[length]; int offset 0; while (offset length) { int read stream.Read(buffer, offset, length - offset); if (read 0) throw new IOException(连接已关闭); offset read; } return buffer; }4.3 现象心跳正常但读D寄存器偶尔超时原因FX5U的以太网口在处理大量通信请求时内部缓冲区会满。如果上位机同时开了多个连接或者心跳和业务请求并发PLC可能来不及响应。解决串行化所有请求用一个SemaphoreSlim保证同一时间只有一个请求在途。心跳和业务请求共用同一个连接不要开多个TcpClient。4.4 现象32位数据解析出来是负数或乱码原因字节序搞反了。MC协议返回的是小端但C#的BitConverter默认按系统字节序x86是小端所以直接转没问题。但如果手动拼字节容易把高低字弄反。解决统一用BitConverter.ToUInt16和BitConverter.ToInt32不要自己移位。如果必须手动拼记住低地址在前。4.5 现象WinForm关闭时程序卡死原因后台线程还在阻塞在NetworkStream.Read上FormClosing事件里没有正确关闭连接。解决在FormClosing里先设置runningfalse然后Close TcpClient最后Join后台线程。注意Close TcpClient会触发Read抛出异常要在catch里忽略。5. 进阶技巧用报文模拟器验证与性能调优5.1 搭一个FX5U报文模拟器没有PLC的时候怎么调试我一般写一个简单的TCP服务器监听5021端口收到请求后按MC协议格式返回模拟数据。这样上位机代码可以脱离硬件开发单元测试也能跑。// 模拟器核心逻辑 var listener new TcpListener(IPAddress.Any, 5021); listener.Start(); while (true) { var client await listener.AcceptTcpClientAsync(); _ Task.Run(() HandleClient(client)); } async Task HandleClient(TcpClient client) { var stream client.GetStream(); var buffer new byte[1024]; while (true) { int read await stream.ReadAsync(buffer, 0, buffer.Length); if (read 0) break; // 解析请求构造响应 var resp BuildMockResponse(buffer, read); await stream.WriteAsync(resp, 0, resp.Length); } }模拟器的关键是响应报文的结束代码要返回0x0000数据部分可以填随机值或固定值。这样上位机的解析逻辑、UI刷新、异常处理都能提前验证等真PLC到了再联调效率高很多。5.2 批量读取的点数优化一次读多少点最合适我做过测试读10个点往返延迟约2ms读100个点约5ms读500个点约15ms读960个点最大值约30ms。看起来读得越多越快但实际项目中要考虑如果中间某个地址越界整条请求失败重试成本高。所以我的策略是按功能块分批货位状态一批、坐标数据一批、报警信息一批每批不超过200个点。这样单批失败不影响其他数据。5.3 用Wireshark抓包定位协议问题Wireshark是排查MC协议问题的后悔药。过滤条件用tcp.port5021然后看请求和响应的十六进制。重点看三个地方副头部是不是54 00软元件代码是不是A8结束代码是不是00 00。如果响应报文里结束代码非零直接查手册的错误码表比猜快得多。从那以后我每次新项目联调都强制先跑一遍模拟器再抓一次真实报文对比确认字节序和软元件代码无误后才写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取