news 2026/9/23 15:47:24

C#2010用TcpClient批量读写三菱FX3U:MC协议与多台PLC并发采集实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#2010用TcpClient批量读写三菱FX3U:MC协议与多台PLC并发采集实战

简介:这份资源是一套基于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 就是 D,M 就是 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 是A8,M 是90,X 是9C,Y 是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.0,TcpClient 的异步模型和后来略有不同,但同步阻塞方式足够用。关键是设置好收发超时,否则一台 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 的 0x90,PLC 会按位软元件解析,返回的数据长度和内容都不对。另外地址三字节小端容易写成大端。

解决:先用 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 台并发,结果协议还没调对,排查时根本分不清是协议问题还是并发问题。单台跑通、抓包确认、再扩到多台,这个顺序能省下大量时间。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 15:46:33

ArcGIS Python脚本中Exists函数的深度解析与应用

1. ArcGIS Python脚本开发&#xff1a;Exists函数深度解析与应用实战作为一名GIS开发工程师&#xff0c;我经常需要处理各种地理数据的检查和管理工作。arcpy.Exists()函数是我日常脚本中最常用的工具之一&#xff0c;它看似简单&#xff0c;但在实际项目中能帮我们避免很多潜在…

作者头像 李华
网站建设 2026/9/23 15:46:17

线上事故发生时的大模型排障引导交互设计

线上事故发生时的大模型排障引导交互设计当生产环境突然爆发出大面积 5xx 错误、电话告警响个不停时&#xff0c;值班工程师&#xff08;On-call&#xff09;面临的最大敌人往往不是技术复杂度本身&#xff0c;而是严重的信息过载与极度紧张下的决策混乱。 传统的故障辅助工具要…

作者头像 李华
网站建设 2026/9/23 15:45:29

嵌入式按键子函数封装:从裸机轮询到状态机事件驱动

1. 从裸机轮询到上层逻辑&#xff1a;按键子函数封装的核心思路1.1 为什么“能跑”和“好维护”是两码事刚接触单片机或者嵌入式开发的朋友&#xff0c;大概率都写过这样的代码&#xff1a;在主循环里塞一个if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0)&#xff0c;然后跟…

作者头像 李华
网站建设 2026/9/23 15:44:08

学术写作AI:破解黑话,提升论文可读性与影响力

1. 项目概述&#xff1a;当学术写作遇上"人话革命"去年审阅某核心期刊投稿时&#xff0c;我遇到一篇让我哭笑不得的论文——作者用"基于多维度认知框架的跨模态表征重构"来描述"用不同方法分析数据"&#xff0c;通篇充斥着"后现代性话语解构…

作者头像 李华
网站建设 2026/9/23 15:44:00

药品小样本目标检测实战:板蓝根颗粒数据集VOC转YOLO与训练全解析

简介&#xff1a;针对板蓝根颗粒袋装药品检测任务&#xff0c;这份数据集面向计算机视觉初学者和工业质检应用开发者&#xff0c;提供111张真实拍摄的jpg原图以及完全对应的VOC格式xml与YOLO格式txt标注文件&#xff0c;覆盖“999ganmaoling”和“banlangen”两个类别&#xff…

作者头像 李华