简介这份资源是一套基于C# 2010、通过TcpClient实现三菱FX3U PLC读写通信的完整工程源码面向从事工业自动化、设备控制与上位机开发的初中级程序员解决单台乃至多台PLC批量联网读写的问题。压缩包共41个文件约120KB以cs源码、resx资源、settings配置、csproj与sln工程文件为主另含exe、dll、pdb等编译产物可直接在Visual Studio中打开调试。代码围绕System.Net.Sockets下的TcpClient类建立TCP连接涉及寄存器地址映射、字节数组收发、多线程或异步并发处理多个PLC等关键环节对理解MODBUS TCP类通信协议与批量读写思路有实际参考意义。目前已有251人学习下载适合需要快速搭建三菱PLC通信原型、借鉴工程组织方式的开发者研读。1. 用 TcpClient 批量读写三菱 FX3U一条被低估的产线数据通道车间里最容易被忽略的活往往不是写逻辑而是把十几台甚至几十台三菱 FX3U 的数据同时收上来。串口轮询慢、专用模块贵、OPC 授权按点收费很多做 C# 上位机的工程师最后都会绕回一个朴素方案用 TcpClient 走以太网直接读写三菱 PLC 的软元件。这个标题里的 TcpClient、C#2010、三菱 FX3U、批量三菱 PLC说的就是这件事——在 .NET 环境下用 TCP 客户端把多台 FX3U 的 D 寄存器、M 继电器、X/Y 点位批量抓下来或写下去。它适合做产线看板、设备联网改造、MES 数据采集的从业者也适合手里已经有一批 FX3U、又不想动 PLC 程序的人。核心难点不在 TcpClient 本身而在三菱的通信协议格式、批量请求的拼包方式以及多台设备并发时的连接管理。2. 三菱 FX3U 以太网通信的协议底座与选型理由2.1 为什么是 MC 协议而不是 Modbus三菱 FX3U 本体没有网口常见做法是加装 FX3U-ENET-ADP 或 FX3U-ENET-L 以太网模块也有用串口转以太网网关的。无论哪种只要模块支持最稳的通道就是三菱自己的 MC 协议MELSEC Communication Protocol。它分两种帧格式ASCII 帧和二进制帧。ASCII 帧可读性好调试时能直接看懂报文二进制帧体积小、速度快批量读写时优势明显。很多人第一反应是用 Modbus TCP因为库多、资料全。但 FX3U 走 Modbus 通常需要额外配置或网关转换而且 Modbus 的寄存器地址和 FX3U 的软元件不是一一对应D、M、X、Y 要自己做映射表。MC 协议则是三菱原生软元件代号直接写在报文里D 就是 DM 就是 M少一层翻译就少一层出错的可能。我一般会优先选 MC 二进制帧只有在现场调试、需要抓包肉眼核对时才临时切 ASCII。2.2 MC 协议二进制帧的请求结构一次批量读 D 寄存器的二进制请求结构是固定的。以读取 D100 开始的 10 个字为例请求帧大致如下50 00 // 副头部固定 00 FF // 网络号、PLC 号 FF 03 // 请求目标模块 IO 号、站号 0C 00 // 请求数据长度 01 04 // 指令批量读 00 00 // 子指令按字读 64 00 00 // 起始软元件地址 D100 A8 // 软元件代码D 寄存器 0A 00 // 读取点数 10响应帧会在副头部后跟结束代码然后是数据区。结束代码00 00表示成功非零就是出错比如地址越界、点数超限。这里有个容易翻车的点软元件地址在二进制帧里是 3 字节小端D100 要写成64 00 00不是直接写 100 的十进制。软元件代码也要查表D 是A8M 是90X 是9CY 是9D不同系列可能略有差异FX3U 用这套。2.3 批量读写的边界与点数上限MC 协议单次请求有长度限制。二进制帧下一次批量读的字数一般不超过 960 个字位软元件不超过 7168 点。实际项目里我不会贴着上限走通常按 200 到 500 个字一批原因是单帧太大一旦丢包重传代价高而且很多以太网模块的内部缓冲有限超长帧可能被截断。批量写的限制类似写 D 寄存器一次别超过 960 字。选型上还有一点FX3U-ENET-ADP 和 ENET-L 支持的连接数不同。ADP 通常支持 8 个以内的 TCP 连接ENET-L 多一些。如果你要批量管理 20 台 PLC不是每台都开长连接而是用连接池轮询或者分组并发。这个在后面并发章节会展开。3. 用 C#2010 写一个能跑通的 TcpClient 读写类3.1 建立连接与超时控制C#2010 对应 .NET Framework 4.0TcpClient 的异步模型和后来略有不同但同步阻塞方式足够用。关键是设置好收发超时否则一台 PLC 掉线会把整个采集线程卡死。using System; using System.Net.Sockets; public class MelsecTcpClient { private TcpClient _client; private NetworkStream _stream; // 连接 PLC超时单位毫秒 public bool Connect(string ip, int port, int timeoutMs) { try { _client new TcpClient(); // 用 BeginConnect 配合 WaitOne 实现可控超时 IAsyncResult ar _client.BeginConnect(ip, port, null, null); if (!ar.AsyncWaitHandle.WaitOne(timeoutMs, false)) { _client.Close(); return false; } _client.EndConnect(ar); _client.ReceiveTimeout timeoutMs; _client.SendTimeout timeoutMs; _stream _client.GetStream(); return true; } catch { return false; } } }逻辑说明TcpClient 直接 Connect 在目标不可达时可能等很久用 BeginConnect 加 WaitOne 把连接超时压到可控范围。ReceiveTimeout 和 SendTimeout 是后续读写不卡死的关键。参数上timeoutMs 我一般设 1000 到 3000产线网络抖动大就设 3000本地调试 1000 足够。注意 C#2010 里 TcpClient 没有 ConnectAsync只能用这种异步等待的写法。3.2 拼一个批量读 D 寄存器的请求下面这个方法把起始地址和点数拼成 MC 二进制帧然后发送并解析响应。public byte[] BuildReadRequest(ushort startAddr, byte deviceCode, ushort count) { byte[] frame new byte[21]; frame[0] 0x50; frame[1] 0x00; // 副头部 frame[2] 0x00; // 网络号 frame[3] 0xFF; // PLC 号 frame[4] 0xFF; frame[5] 0x03; // 目标模块 IO、站号 frame[6] 0x0C; frame[7] 0x00; // 请求数据长度 frame[8] 0x01; frame[9] 0x04; // 批量读指令 frame[10] 0x00; frame[11] 0x00; // 按字 frame[12] (byte)(startAddr 0xFF); frame[13] (byte)((startAddr 8) 0xFF); frame[14] 0x00; // 地址高字节 frame[15] deviceCode; // 软元件代码 frame[16] (byte)(count 0xFF); frame[17] (byte)((count 8) 0xFF); return frame; }逻辑说明地址三字节小端低字节在前。deviceCode 传 D 寄存器就是 0xA8。count 是读取点数。参数上startAddr 是软元件编号比如 D100 就传 100不是 0x100。这里最容易错的是把地址当成字节偏移结果读出来全是错位数据。3.3 发送、收响应与解析public ushort[] ReadWords(ushort startAddr, byte deviceCode, ushort count) { byte[] req BuildReadRequest(startAddr, deviceCode, count); _stream.Write(req, 0, req.Length); // 响应长度 9 字节头 数据长度 byte[] resp new byte[9 count * 2]; int read 0; while (read resp.Length) { int n _stream.Read(resp, read, resp.Length - read); if (n 0) throw new Exception(连接中断); read n; } // 结束代码在 resp[8] resp[9]非零即错误 if (resp[8] ! 0 || resp[9] ! 0) throw new Exception(PLC 返回错误码: resp[8] - resp[9]); ushort[] result new ushort[count]; for (int i 0; i count; i) { // 数据从 resp[11] 开始小端 result[i] (ushort)(resp[11 i * 2] | (resp[12 i * 2] 8)); } return result; }逻辑说明响应头 9 字节其中第 8、9 字节是结束代码。数据区从第 11 字节开始每个字小端。参数上count 要和请求一致否则解析长度对不上。这里用循环 Read 是因为 TCP 是流一次 Read 不一定拿到完整帧必须读到预期长度为止。这是新手最容易踩的坑之一直接 Read 一次就解析网络稍慢就出错。3.4 批量写 D 寄存器的实现写和读结构类似指令换成 0x01 0x14子指令 0x00 0x00后面跟起始地址、软元件代码、点数再跟数据。public void WriteWords(ushort startAddr, byte deviceCode, ushort[] values) { int count values.Length; byte[] frame new byte[21 count * 2]; frame[0] 0x50; frame[1] 0x00; frame[2] 0x00; frame[3] 0xFF; frame[4] 0xFF; frame[5] 0x03; // 数据长度 12 点数*2 int dataLen 12 count * 2; frame[6] (byte)(dataLen 0xFF); frame[7] (byte)((dataLen 8) 0xFF); frame[8] 0x01; frame[9] 0x14; // 批量写 frame[10] 0x00; frame[11] 0x00; frame[12] (byte)(startAddr 0xFF); frame[13] (byte)((startAddr 8) 0xFF); frame[14] 0x00; frame[15] deviceCode; frame[16] (byte)(count 0xFF); frame[17] (byte)((count 8) 0xFF); for (int i 0; i count; i) { frame[18 i * 2] (byte)(values[i] 0xFF); frame[19 i * 2] (byte)((values[i] 8) 0xFF); } _stream.Write(frame, 0, frame.Length); byte[] resp new byte[9]; int read 0; while (read 9) { int n _stream.Read(resp, read, 9 - read); if (n 0) throw new Exception(连接中断); read n; } if (resp[8] ! 0 || resp[9] ! 0) throw new Exception(写入错误码: resp[8] - resp[9]); }逻辑说明写请求的数据长度字段要算对是 12 加数据字节数。写响应只有 9 字节头没有数据区。参数上values 数组长度就是点数别超过 960。写入前最好确认目标地址没有被 PLC 程序高频改写否则会出现你写进去、PLC 立刻覆盖的情况看起来像写失败。4. 批量管理多台三菱 PLC 的连接与轮询策略4.1 连接池与分组并发20 台 FX3U如果每台开一个长连接、每 100ms 轮询一次网络和 PLC 模块都吃不消。我的做法是分组把 20 台分成 4 组每组 5 台用一个采集线程负责一组组内串行轮询组间并行。这样任意时刻最多 4 个并发连接对交换机和 PLC 都友好。连接不要每次读写都新建维护一个连接池。每台 PLC 对应一个 MelsecTcpClient 实例采集前检查连接是否存活断了就重连。重连要有退避连续失败 3 次就标记该台离线隔 30 秒再试避免死循环刷日志。4.2 轮询周期与数据变化检测不是所有数据都需要高频采集。D 寄存器里的产量计数可以 500ms 一次M 继电器的状态可以 200ms 一次。按数据重要程度分优先级别一刀切。另外可以在上位机做变化检测只有值变了才写数据库或推看板减少下游压力。// 简化的分组轮询骨架 foreach (var plc in group) { try { if (!plc.IsConnected) plc.Reconnect(); ushort[] data plc.ReadWords(100, 0xA8, 50); // 变化检测后再入库 plc.LastValues data; } catch (Exception ex) { plc.FailCount; if (plc.FailCount 3) plc.MarkOffline(); } }逻辑说明每台独立 try-catch一台出错不影响整组。FailCount 用于离线判定。参数上ReadWords 的点数按实际需要设别为了省事一次读一大片用不到的地址。4.3 线程安全与资源释放C#2010 里多线程访问同一个 TcpClient 必须加锁或者保证一台 PLC 只被一个线程访问。我倾向后者分组后每组一个线程天然避免竞争。程序退出时要显式关闭所有连接NetworkStream 和 TcpClient 都要 Dispose否则端口会处于 TIME_WAIT重启程序时可能连不上。5. 批量读写三菱 FX3U 的避坑与排查清单5.1 现象连接成功但读出来全是 0 或乱码原因地址或软元件代码写错。D 寄存器代码是 0xA8如果误用了 M 的 0x90PLC 会按位软元件解析返回的数据长度和内容都不对。另外地址三字节小端容易写成大端。解决先用 ASCII 帧发一条读 D100 的请求抓包看返回确认地址和代码无误后再切二进制。软元件代码表要对着 FX3U 手册核一遍。5.2 现象偶尔读到半截数据解析报错原因TCP 流式传输一次 Read 不保证拿到完整帧。代码里如果只 Read 一次就解析网络稍有延迟就出错。解决像 3.3 那样循环 Read 到预期长度。预期长度要算准读响应是 9 加点数乘 2写响应固定 9。5.3 现象写入成功但 PLC 里值没变原因目标地址被 PLC 程序周期性覆盖或者写的是 X、Y 这类由硬件刷新的点位。X 是输入继电器外部信号决定写进去立刻被覆盖。解决写之前确认地址是 D 或 M 这类可保持的软元件。如果是 M检查 PLC 程序里有没有同地址的线圈输出。5.4 现象多台 PLC 时部分设备频繁掉线原因连接数超过以太网模块上限或者轮询太密导致模块响应不过来。FX3U-ENET-ADP 连接数有限超了就会拒绝新连接。解决减少并发连接数拉长轮询周期或者换 ENET-L 模块。分组轮询是性价比最高的办法。5.5 现象程序运行几小时后卡死原因异常没捕获某个连接断了之后 Read 一直阻塞或者连接没释放导致句柄泄漏。解决所有网络操作包 try-catch设置 ReceiveTimeout定期检查连接状态并重连。退出时统一 Dispose。6. 把批量采集做成可验证的稳定通道几个进阶习惯写到能跑通只是第一步真正上线还要能验证。我一般会加一个自检机制程序启动时对每台 PLC 读一个已知的固定值地址比如 D8000 附近的系统区确认通信正常再进入正式采集。采集过程中每隔一段时间记录一次成功率和平均耗时低于阈值就告警。另一个习惯是保留原始报文日志。出问题时把请求和响应帧的十六进制打出来对照协议手册逐字节看比猜快得多。日志按天切分别写太大。还有一个技巧批量读的时候把相邻地址合并成一次请求。比如要读 D100 到 D120 和 D130 到 D150中间 D121 到 D129 用不到也可以一次读 D100 到 D150多读几个字换一次通信往返通常更划算。但要注意别跨软元件类型合并D 和 M 不能混在一个请求里。验证方法上可以用一个简单的回环测试往一个不用的 D 地址写一个随机数再读回来比对连续跑 1000 次统计错误率。这个测试能暴露大部分拼包和解析问题。最后说个血泪经验现场调试时先拿一台 PLC 把读写跑通再复制到多台。我见过太多人一上来就 20 台并发结果协议还没调对排查时根本分不清是协议问题还是并发问题。单台跑通、抓包确认、再扩到多台这个顺序能省下大量时间。希望帮到你。本文还有配套的精品资源点击获取
