
简介本资源是一套面向工业自动化开发者的C#与三菱FX5U系列PLC以太网通信实战DEMO源代码适用于具备基础.NET编程能力及PLC概念的工程师、自动化专业学生和设备集成人员解决上位机与FX5U控制器高效稳定通信的核心开发问题。压缩包共38个文件包含6个核心C#源码文件如Form1.cs、Program.cs、4个关键DLL库支撑协议解析与连接管理、3个可执行EXE程序含调试运行入口、以及配置文件app.config、资源文件.resx、解决方案工程.sln/.csproj等整体仅310KB轻量易部署。已有2149人学习下载代码结构清晰涵盖连接初始化、寄存器读写封装、异常处理机制与定时轮询逻辑特别适合作为二次开发起点或教学参考——读者可直接复用通信模块、理解FX5U内存映射规则并基于现有框架快速扩展HMI监控、数据采集或远程控制功能。1. 项目缘起为什么我们需要一个FX5U的C#通信DEMO如果你正在用C#开发上位机软件并且需要和三菱FX5U系列PLC打交道那么你大概率已经踩过或者即将踩进一个“坑”官方文档虽然详尽但直接上手写通信代码尤其是处理数据读写、错误重连这些细节时总感觉隔着一层纱。网上能找到的代码片段要么是基于老旧的MX Component要么是零散的Socket示例缺乏一个完整、健壮、能直接跑起来的项目骨架。这就是我当初的困境。我需要一个能稳定连接FX5U进行批量位、字数据读写并且能优雅处理网络异常和PLC端错误的C#程序。翻遍了三菱的MELSEC通信协议手册SLMP协议结合实际的调试经验我整理出了这套DEMO源代码。它不是一个简单的“Hello World”连接测试而是一个包含了连接管理、数据帧构造、响应解析、异常处理等核心模块的、可直接用于生产环境二次开发的通信库雏形。对于从零开始的开发者它能帮你跳过最痛苦的协议理解阶段对于有经验的工程师其中的一些设计思路和避坑点或许也能给你带来启发。2. 核心通信协议SLMP与MC协议的精髓与三菱PLC通信绕不开SLMP协议。你可以把它理解为三菱为自家设备定制的一套“语言规则”。FX5U作为新一代PLC全面支持基于TCP/IP的SLMP协议也称为MC协议这让我们用C#通过以太网进行通信变得非常直接。2.1 协议帧结构请求与响应的“对话模板”SLMP协议的核心在于其固定的帧结构。一次完整的“对话”由上位机我们的C#程序发送“请求帧”PLC返回“响应帧”。一个典型的读取软元件如D寄存器的请求帧结构如下字段名长度字节说明示例值十六进制副头部2固定为50 00表示SLMP50 00访问路径15网络号、PC号等通常默认为FF FF 03 00 11个00FF FF 03 00 00 00 00 00 00 00 00 00 00 00 00请求数据长度2后续请求数据的字节长度00 0C(表示后面有12个字节)定时器2通信超时时间单位250ms00 00表示使用PLC侧设置00 00命令2核心指明操作类型如04 00表示批量读04 00子命令2核心进一步细分如00 00表示软元件访问00 00起始软元件4核心要操作的软元件起始地址如D100A8 00 00 00(D100)软元件代码2核心软元件类型A8表示D寄存器A8 00软元件点数2核心要读取/写入的点数字数或位数00 02(2个字)注意这里的“软元件代码”和“起始软元件”的编码方式是关键坑点。对于D寄存器代码A8其地址100需要转换为100 * 16 1600再转为十六进制0x0640但在帧中需要以40 06 00 00小端序的形式存放。上表中的A8 00 00 00是经过转换后表示D100的最终形态。DEMO代码中的ConvertDeviceAddressToBytes函数就是专门处理这个转换的。响应帧的结构类似但包含一个“结束代码”字段。如果结束代码为00 00表示成功后面跟着请求的数据如果不是则表示发生了错误需要根据代码排查。2.2 TCP连接与保持不是连上就一劳永逸在C#中我们使用System.Net.Sockets.TcpClient来建立TCP连接。但工业现场的网络环境复杂PLC也可能重启因此简单的“连接-使用-断开”模式不可靠。DEMO中的连接管理策略心跳机制定期如每5秒向PLC发送一个轻量级的读取命令例如读取一个固定的系统位。如果连续多次失败则判定连接失效触发重连逻辑。自动重连在检测到连接断开后不是简单抛异常给上层而是进入一个后台重试循环尝试重新建立连接并在成功后恢复之前的通信状态。重连间隔应逐渐延长如1秒2秒4秒…避免频繁冲击网络和PLC。资源清理确保TcpClient和NetworkStream在使用完毕后被正确Dispose。我习惯使用using语句块或在类中实现IDisposable接口来管理。// 简化的连接与发送示例 public class MitsubishiPLCClient : IDisposable { private TcpClient _tcpClient; private NetworkStream _stream; private string _ipAddress; private int _port; private CancellationTokenSource _heartbeatCts; public async Task ConnectAsync(string ip, int port 5001) // FX5U默认端口通常是5001或5002 { _ipAddress ip; _port port; _tcpClient new TcpClient(); await _tcpClient.ConnectAsync(ip, port); _stream _tcpClient.GetStream(); StartHeartbeat(); } private void StartHeartbeat() { _heartbeatCts new CancellationTokenSource(); Task.Run(async () { while (!_heartbeatCts.Token.IsCancellationRequested) { await Task.Delay(5000, _heartbeatCts.Token); try { // 尝试读取一个无关紧要的位如SM0常ON await ReadBitAsync(SM0); } catch { // 心跳失败触发重连事件或标记连接状态 OnConnectionLost?.Invoke(this, EventArgs.Empty); } } }, _heartbeatCts.Token); } public void Dispose() { _heartbeatCts?.Cancel(); _stream?.Close(); _tcpClient?.Close(); } }3. DEMO核心模块详解从字节流到业务数据这个DEMO项目不是一个大而全的框架而是聚焦于通信最核心的几个功能连接、读、写。项目结构清晰主要分为以下几个部分3.1 地址转换器破解三菱的“地址密码”这是通信的第一道关卡。在C#里我们习惯用字符串如“D100”、“M50”来表示地址但协议帧里需要的是特定的二进制码。核心函数ConvertDeviceAddressToBytes逻辑解析字符串识别软元件类型D, M, Y, X等和十进制地址。类型映射将软元件字母映射到协议规定的代码如 D-0xA8, M-0x90。地址计算这是最容易出错的地方。对于字设备如D, W地址需要乘以16。D100-100 * 16 1600- 十六进制0x0640。对于位设备如M, X, Y计算方式相同但读写时命令不同。字节序处理将计算出的地址转换为字节数组并按照小端序低位在前排列。0x0640在帧中应为[0x40, 0x06, 0x00, 0x00]。// 地址转换核心代码片段 public static byte[] ConvertDeviceAddressToBytes(string deviceAddress, out byte deviceCode) { // 示例解析 “D100” char deviceType deviceAddress[0]; // ‘D’ int addressNumber int.Parse(deviceAddress.Substring(1)); // 100 switch (deviceType) { case D: case d: deviceCode 0xA8; // D寄存器代码 addressNumber * 16; // 关键步骤乘以16 break; case M: case m: deviceCode 0x90; // M继电器代码 addressNumber * 16; break; // ... 其他软元件类型 default: throw new ArgumentException($Unsupported device type: {deviceType}); } byte[] addressBytes BitConverter.GetBytes((uint)addressNumber); // 确保小端序如果系统不是小端序则需要反转数组 if (!BitConverter.IsLittleEndian) Array.Reverse(addressBytes); return addressBytes; // 返回4字节地址数组 }3.2 帧构造器组装完整的请求命令有了地址字节和操作类型读/写我们需要组装成一个完整的、符合SLMP协议的请求帧数组。BuildReadWordCommand函数流程创建固定长度的字节数组如26字节的头部 数据部分。按顺序填入副头部、访问路径等固定值。填入由ConvertDeviceAddressToBytes得到的软元件代码和地址字节。填入要读取的点数字数。计算“请求数据长度”字段并填入。注意这个长度是指命令码之后的数据长度不包括副头部、访问路径等。public byte[] BuildReadWordCommand(string startDevice, ushort pointCount) { byte deviceCode; byte[] addressBytes ConvertDeviceAddressToBytes(startDevice, out deviceCode); // 假设固定头部长度为 21 字节副头2路径15数据长2定时器2 int frameLength 21 10; // 10字节是命令码(2)子命令(2)地址(4)软元件码(2) byte[] frame new byte[frameLength]; int index 0; // 填充副头部 50 00 frame[index] 0x50; frame[index] 0x00; // 填充15字节访问路径 (默认) byte[] defaultPath new byte[] { 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; Array.Copy(defaultPath, 0, frame, index, defaultPath.Length); index defaultPath.Length; // **请求数据长度**从命令码开始到帧尾的字节数。这里是 10 字节。 ushort requestDataLength 10; byte[] lengthBytes BitConverter.GetBytes(requestDataLength); if (!BitConverter.IsLittleEndian) Array.Reverse(lengthBytes); frame[index] lengthBytes[0]; frame[index] lengthBytes[1]; // 定时器 (默认00 00) frame[index] 0x00; frame[index] 0x00; // 命令码0400 批量读 frame[index] 0x04; frame[index] 0x00; // 子命令0000 软元件访问 frame[index] 0x00; frame[index] 0x00; // 填入起始地址 (4字节小端序) Array.Copy(addressBytes, 0, frame, index, 4); index 4; // 填入软元件代码 (2字节) frame[index] deviceCode; frame[index] 0x00; // 填入点数 (2字节小端序) byte[] pointBytes BitConverter.GetBytes(pointCount); if (!BitConverter.IsLittleEndian) Array.Reverse(pointBytes); frame[index] pointBytes[0]; frame[index] pointBytes[1]; return frame; }3.3 通信执行与响应解析处理粘包与错误通过NetworkStream发送请求帧后我们需要读取响应。这里有两个关键点确定响应长度和处理粘包。响应解析步骤读取固定头部先读取响应帧的前11个字节副头部访问路径。从这个头部里可以解析出“响应数据长度”。计算总帧长总帧长 11字节头部 响应数据长度。根据这个长度继续读取剩余的字节。检查结束代码在响应数据的起始位置就是2字节的“结束代码”。00 00表示成功。提取数据如果成功结束代码后面就是实际读取到的数据。每个字16位占2个字节同样是小端序需要转换。public async Taskshort[] ReadWordsAsync(string startDevice, ushort pointCount) { byte[] requestFrame BuildReadWordCommand(startDevice, pointCount); await _stream.WriteAsync(requestFrame, 0, requestFrame.Length); // 1. 先读取11字节的响应头部 byte[] headerBuffer new byte[11]; int bytesRead await _stream.ReadAsync(headerBuffer, 0, 11); if (bytesRead ! 11) throw new InvalidDataException(Failed to read response header.); // 2. 从头部解析出响应数据长度 (位于第9-10字节小端序) ushort responseDataLength BitConverter.ToUInt16(headerBuffer, 9); if (!BitConverter.IsLittleEndian) responseDataLength (ushort)((responseDataLength 8) | (responseDataLength 8)); // 3. 读取剩余响应数据 byte[] dataBuffer new byte[responseDataLength]; bytesRead await _stream.ReadAsync(dataBuffer, 0, responseDataLength); if (bytesRead ! responseDataLength) throw new InvalidDataException(Incomplete response data.); // 4. 检查结束代码 (数据区前2字节) ushort endCode BitConverter.ToUInt16(dataBuffer, 0); if (endCode ! 0x0000) { throw new PlcCommunicationException($PLC returned error code: 0x{endCode:X4}); } // 5. 提取数据 (结束代码后开始) int dataStartIndex 2; short[] result new short[pointCount]; for (int i 0; i pointCount; i) { int dataOffset dataStartIndex i * 2; result[i] BitConverter.ToInt16(dataBuffer, dataOffset); // 注意如果PLC字数据是高位在前可能需要在这里做反转 // if (!BitConverter.IsLittleEndian) Array.Reverse(dataBuffer, dataOffset, 2); } return result; }实操心得粘包处理在高速通信时PLC可能将多个响应一次性返回或者网络底层合并了数据包。上面的代码通过“先读固定头部再根据头部信息读剩余部分”的方式完美解决了粘包问题。这是一种非常可靠的模式。4. 从DEMO到实用必须考虑的进阶问题与优化把DEMO跑通只是第一步。要用于实际项目以下几个方面的考量至关重要。4.1 错误处理与重试策略让程序更健壮工业通信必须稳定。不能因为一次网络抖动或PLC忙就导致整个程序卡死或崩溃。超时设置TcpClient和NetworkStream的ReadTimeout、WriteTimeout属性必须设置。通常设为3000-5000毫秒。异常分类处理SocketException网络层错误如连接拒绝、主机不可达。应触发重连逻辑。IOException流错误可能是超时或连接中断。也应触发重连。PlcCommunicationException自定义PLC返回非零结束代码表示逻辑错误如地址非法。这类错误通常不需要重连但需要记录并通知用户。重试策略对于可重试的异常如网络超时实现指数退避重试。例如第一次重试等待1秒第二次2秒第三次4秒最多重试3次。public async Taskshort[] ReadWordsWithRetryAsync(string device, ushort points, int maxRetries 3) { int retryCount 0; while (true) { try { return await ReadWordsAsync(device, points); } catch (SocketException ex) { retryCount; if (retryCount maxRetries) throw; Logger.Warn($Socket error reading {device}, retry {retryCount}/{maxRetries}. Error: {ex.Message}); await Task.Delay(1000 * (int)Math.Pow(2, retryCount - 1)); // 指数退避 await ReconnectAsync(); // 尝试重新连接 } catch (IOException ex) when (ex.InnerException is SocketException) { // 处理因Socket引起的IO异常 retryCount; if (retryCount maxRetries) throw; Logger.Warn($IO error reading {device}, retry {retryCount}/{maxRetries}. Error: {ex.Message}); await Task.Delay(1000 * (int)Math.Pow(2, retryCount - 1)); await ReconnectAsync(); } catch (PlcCommunicationException) { // PLC逻辑错误直接抛出不重试 throw; } } }4.2 性能优化批量操作与异步并发频繁地读写单个字效率极低。SLMP协议支持批量读写一定要充分利用。批量读写一次命令读写多个连续地址。DEMO中的pointCount参数就是用于此。尽量将需要同步更新的数据安排在连续的地址内一次操作完成。异步编程使用async/await避免UI线程或主线程阻塞。所有的ReadAsync、WriteAsync、ConnectAsync都应使用异步版本。连接池与资源复用对于需要与多台PLC通信的大型系统可以考虑管理一个TcpClient连接池避免频繁创建和销毁连接的开销。但FX5U通常一对一通信单例模式管理一个长连接即可。4.3 FX5U特定配置让通信真正连通代码写得再好PLC侧配置不对也是白搭。确保以下几点PLC IP设置为FX5U设置固定的IP地址、子网掩码和默认网关确保与上位机在同一网段。内置以太网端口参数在GX Works3中需要配置“模块参数”。打开“内置以太网端口”设置。在“打开设置”中新建一个协议选择“MC协议”。设置端口号默认为5001或5002需与代码中一致。设置通信目标通常允许“所有连接对象”或指定上位机的IP。务必设置“在线操作”-“当前连接设置”将你创建的MC协议配置进去并设置好IP地址和端口。这是最容易遗漏的一步防火墙关闭PLC和上位机Windows的防火墙或为相应端口添加入站规则。PLC运行状态确保PLC处于RUN模式STOP模式下某些通信可能被禁止。4.4 数据类型转换处理浮点数与长整数PLC中的D寄存器是16位字。但实际数据可能是32位整数DINT、32位浮点数REAL或64位长整数LINT。32位数据如D100-D101组成的浮点数读取连续的2个字D100, D101将其转换为4字节的byte[]再用BitConverter.ToSingle()转换为float。特别注意字节顺序三菱FX5U的浮点数格式通常是IEEE 754标准但字序可能是“高字在前低字在后”即D100是高16位D101是低16位也可能相反。这需要在代码中根据实际情况调整或提供配置选项。位操作读写单个线圈如M0使用不同的命令码位读/位写。读取多个连续位时响应数据中每个位占用一个字节0x00或0x01。DEMO源代码中提供了ReadFloat和WriteFloat的示例方法展示了如何组合两个字并进行字节序处理。5. 调试与排错实战当通信失败时该怎么办即使按照上述步骤第一次调试也难免失败。这里有一套系统的排查流程。第1步检查物理连接与基础配置Ping测试在电脑命令行ping PLC的IP地址。如果不通检查网线、交换机、IP设置。确认端口使用telnet PLC_IP 5001测试端口是否开放。如果连接被拒绝检查PLC的MC协议配置是否启用且端口正确。核对GX Works3配置反复确认“当前连接设置”中已添加了MC协议并且IP/端口与代码一致。第2步抓包分析——终极武器当逻辑层面找不到问题时网络抓包是唯一真相。使用Wireshark。在运行上位机程序前在Wireshark中选择正确的网卡开始抓包。执行一次通信操作如点击“读取”按钮。停止抓包使用过滤器tcp.port 5001筛选出与PLC的通信数据包。分析请求帧找到上位机发出的TCP包展开“Melsec”协议如果Wireshark识别正确。逐字节对比你的代码生成的帧与Wireshark解析出来的帧是否完全一致。重点关注命令码、地址、软元件代码、点数这几个字段。分析响应帧查看PLC返回的包。如果结束代码非零Wireshark通常会直接显示错误信息如“地址超出范围”。这能直接定位问题。第3步代码层逐段调试打印字节数组在发送前和接收后将byte[]以十六进制格式打印到日志中。与手册中的示例帧或Wireshark抓到的包进行比对。检查地址转换单独测试ConvertDeviceAddressToBytes函数输入“D100”看输出的4字节地址是否是[0x40, 0x06, 0x00, 0x00]。简化测试先尝试最简单的功能比如读取一个位的状态如X0因为位操作帧更短便于分析。常见错误代码与原因0xC050访问目标不正确IP/端口错或PLC未配置MC协议。0xC054请求数据长度不正确你构造的帧长度字段算错了。0xC059指令错误命令码或子命令码不正确。0xC05B软元件地址超出范围地址转换错误或点数设置过大。我个人的经验是90%的通信问题都出在地址转换和帧长度计算这两个环节。务必把这两个部分的代码逻辑和手册反复核对。6. 超越DEMO构建生产级通信库的思考这个DEMO提供了一个坚实的起点。在此基础上你可以根据项目需求进行扩展抽象与接口定义一个IPlcCommunicator接口包含Connect,Disconnect,ReadWord,WriteWord等方法。这样可以将三菱FX5U的具体实现与业务逻辑解耦未来如果需要支持西门子、欧姆龙等其他品牌的PLC只需实现新的接口即可。配置化将PLC的IP、端口、站号、超时时间、重试策略等提取到配置文件如appsettings.json中。日志与监控集成像Serilog或NLog这样的日志框架详细记录每一次通信请求、响应、耗时和异常。这对于线上问题排查至关重要。数据映射与缓存对于需要频繁读取的工艺参数可以实现一个缓存层定时从PLC读取并更新到内存中业务逻辑直接访问缓存减少实时通信压力。支持更多软元件和功能扩展支持T、C定时器、计数器的当前值读取支持Z、V变址寄存器甚至扩展文件寄存器R的访问。最后这个DEMO的源代码我会整理后分享出来。它可能不是功能最全的但其中的连接管理、帧构造、响应解析和错误处理骨架是经过实际项目验证的、稳定可靠的核心。希望它能帮你更快地打通C#与FX5U之间的通信桥梁把精力更多地投入到上层业务逻辑的开发中。记住工业通信稳定性和可靠性永远排在第一位优雅的代码和健壮的错误处理是达成这一目标的基石。本文还有配套的精品资源点击获取