简介:C#与三菱FX5U PLC的TCP通讯交互源码包,面向工控软件开发人员、PLC与PC通信初学者及有项目集成经验的工程师。资源同时提供VB.NET与C#两个版本工程,支持整数、双整数、浮点数等多种数据类型,并兼容ASCII和二进制两种报文格式,无需额外安装,复制到项目即可使用。包内含95个文件,以.cs、.vb源文件、exe可执行程序、dll动态库为主,另附配置文件、项目工程文件、接口说明txt及可编程控制器PC通讯组件使用说明PDF,压缩包仅681KB,结构紧凑便于查阅。已有1544人学习下载。读者可获得可直接运行的通讯示例、接口调用说明及完整工程结构,适合快速验证FX5U通讯逻辑并在此基础上进行二次开发,尤其适合需要短时间落地TCP通讯功能的新手和中级开发者。
1. 把C#/VB上位机接到三菱FX5U,先想清楚这三件事
一台三菱FX5U摆在面前,C#或VB.NET要读写它的D寄存器,网上大多数示例只会甩给你一段调用三菱MX Component的代码。可真正在产线上跑过的工程师都清楚,通讯交互卡住的地方通常不是写代码,而是三件小事:PLC侧的以太网端口和协议没开、软元件地址与帧格式对不上、循环采集时UI线程被通讯超时堵死。下面按“PLC侧配置 → 最小通讯实现 → 批量采集与UI刷新 → 抓包验证”这条路走一遍,把C#和VB.NET两套方案讲透,所有代码按可直接复现的粒度给出。适合正在做上位机、设备数据采集,或者准备接手FX5U项目的工程师。
2. FX5U通讯前必须做对的PLC侧配置与协议选择
2.1 MC协议、SLMP和Modbus-TCP:FX5U上怎么选
FX5U的内置以太网口同时支持多种通讯协议,最常用的是三菱的MC协议(3E帧)、SLMP和Modbus-TCP。这里有个容易混淆的点:SLMP看起来是个新名词,实际上报文结构和MC协议的3E帧基本一致,可以理解为三菱把MC协议开放给第三方设备时的公开名称。对C#上位机来说,3E帧二进制是首选:帧结构紧凑、批量读写能力完整、一个请求最多可以处理接近500个字的数据。Modbus-TCP的出场机会在于现场已经有第三方HMI、SCADA或者网关,统一成Modbus可以少做一层协议转换。
| 协议 | 默认端口 | 报文特点 | 批量读写 | 典型场景 |
|---|---|---|---|---|
| MC 3E帧二进制 | 6000 | 帧头D0 00开头,紧凑 | 按字/位批量,效率最高 | C#/VB上位机直连,首选 |
| MC 3E帧ASCII | 6000 | 每个字节转成两个ASCII字符 | 同样支持批量,带宽翻倍 | 调试抓包、日志记录 |
| SLMP | 6000 | 与3E帧基本一致 | 支持 | 接入第三方设备 |
| Modbus-TCP | 502 | 标准Modbus功能码 | 受映射表限制 | 跨品牌HMI、网关 |
选型的判断标准很直接:如果你只需要读写FX5U的D、M、X、Y,且上位机用C#或VB编写,直接走MC协议,不用犹豫。如果后续设备可能换成西门子或汇川,Modbus-TCP反而能让通讯层改动最小,代价是FX5U侧的Modbus从站映射先要配置好,多一层搬移。
2.2 GX Works3里必须打开的三个开关
先到GX Works3里把PLC侧准备做好,不然上位机代码再对,PLC不回应也白搭。操作路径是「导航 → 参数 → FX5UCPU → 模块参数 → 以太网端口」,不同版本菜单名略有差异,但关键项就三个。
第一是IP地址配置。给PLC一个固定IP,例如192.168.1.10,子网掩码255.255.255.0,上位机网卡配到同一网段,比如192.168.1.20。不要用DHCP,产线环境里PLC掉电后拿到个新IP,上位机所有连接都会断。配置完把设置写入PLC并复位,IP才生效。
第二是通讯协议支持。在以太网端口参数里有一个“通信协议支持”的勾选项,必须把MC协议(有的版本里叫SLMP)勾上,并确认TCP端口号是6000。这个开关最容易漏,漏掉之后的上位机症状很典型:ping得通,telnet 192.168.1.10 6000不通。
第三是运行中写入许可。位置在「CPU参数 → 运行中写入许可」,勾选允许。如果不勾,PLC处于RUN状态时,上位机发写请求会收到异常代码0xC061,提示远程操作被禁止。调试阶段直接勾上,等上了线再按安全要求收严。
配完这三个地方,在PC的命令行窗口执行一下telnet 192.168.1.10 6000,能进入黑窗口不报错,说明PLC的MC协议服务已经在监听了。如果你机器的telnet客户端没装,用PowerShell执行Test-NetConnection 192.168.1.10 -Port 6000,输出里的TcpTestSucceeded为True即代表端口已打开。确认完立刻退出,别留着一个半连接状态占着连接数。
提示:FX5U的以太网连接数有限,GX Works3的在线监视也占一个连接。调试时不要同时开五六个上位机实例连同一台PLC。
2.3 3E帧的软元件地址映射与报文字段拆解
用库封装之后很少需要手拼报文,但你要能看懂抓包和报错。3E帧二进制请求报文的核心字段是请求数据长度、命令、子命令、起始软元件编号、点数。起始软元件编号不是把“D100”的字符填进去,而是换算成帧内的软元件代码和偏移量。三菱的换算规则是:1号软元件的偏移为0,D寄存器每个编号占2个偏移量,M、X、Y等位软元件每个编号占1个偏移量。
以D100为例,帧内偏移就是 (100-1)×2 = 198 = 0x00C6,放在报文里按小端字节序排列就是C6 00。M100的偏移是99 = 0x0063。帧内的软元件代码是固定值:D为0xA8,M为0x90,X为0x9C,Y为0x9D。
| 软元件 | 帧内代码 | 偏移量公式 | 说明 |
|---|---|---|---|
| D | 0xA8 | (编号-1)×2 | D100 → 0x00C6 |
| M | 0x90 | 编号-1 | M100 → 0x0063 |
| X | 0x9C | 按八进制编号换算 | X0、X17直接传字符串给库 |
| Y | 0x9D | 按八进制编号换算 | 同上 |
这里还要注意一个FX5U的惯例:X和Y的编号是八进制,X8在物理上不存在,X0到X7一共8个点,下一个输入点是X10。用库时直接写“X10”表示第九个输入点,库会替你处理编号转换。当HslCommunication返回报错0xC051(起始软元件编号非法)时,第一个要去核对的就是地址映射有没有算错。
3. C#及VB.NET调用HslCommunication读写FX5U的最小源码
3.1 为什么不用MX Component直接上手
三菱官方MX Component通过逻辑站号访问PLC,功能完备,这是它长期被教程推荐的原因。但它的问题也很现实:要安装三菱运行库、要配逻辑站号、部署时得带上不少DLL,而且COM组件在.NET环境下的异常处理总是不够直接。对于产线内部一台PC管一台PLC的场景,用开源库HslCommunication更轻盈。NuGet搜HslCommunication,引用后直接new一个MelsecMcNet去连FX5U,唯一要保证的是.NET Framework 4.5以上或.NET Core/.NET 5以上,WinForm和WPF都能用。
MX Component也不是不好,适合公司强制统一三菱工具链、或者需要直接映射批量地址的存量项目。如果走那条路线,逻辑站号在MX Sheet里配好,C#里new一个ActUtlType就可以读写,部署时要带齐三菱运行库。下面先讲开源库路线。
3.2 C#用MelsecMcNet实现最小读写代码
新建一个WinForm项目,NuGet安装HslCommunication,然后写一个通讯类:
using HslCommunication; using HslCommunication.Profinet.Melsec; public class Fx5uTcpClient { private MelsecMcNet _plc; public bool Connect(string ip, int port = 6000) { _plc = new MelsecMcNet(ip, port); _plc.SetPersistConn(true); // 使用长连接,避免每次读写握手 _plc.ConnectTimeout = 3000; // 连接超时3秒,单位为毫秒 OperateResult result = _plc.ConnectServer(); if (!result.IsSuccess) { Console.WriteLine($"连接失败: {result.Message}"); return false; } return true; } /// <summary>读取D寄存器,返回16位无符号整数数组</summary> public OperateResult<ushort[]> ReadWords(int startAddress, int count) { return _plc.ReadUInt16("D" + startAddress, (ushort)count); } /// <summary>写单个D寄存器,short范围 -32768~32767</summary> public OperateResult WriteWord(int address, short value) { return _plc.Write("D" + address, value); } /// <summary>读位软元件M,常用于读取设备状态</summary> public OperateResult<bool[]> ReadBits(int startAddress, int count) { return _plc.ReadBool("M" + startAddress, (ushort)count); } }代码里两个关键参数:SetPersistConn(true)让TCP连接保持复用,通讯频率上来之后性能差距很大,如果保持默认的短连接,每次读写都要挥一次三次握手,循环采集时吞吐量直接掉一个数量级。ConnectTimeout是连接超时,别设太短,Windows网卡在目标IP不可达时本身要等约1.2秒的ARP超时,设成1000毫秒以内容易产生假失败。
读D寄存器注意数据宽度:FX5U的D寄存器是16位,ReadUInt16读出来的范围是0到65535;如果PLC里存的是带符号数,比如负的温度值,要用ReadInt16读short。32位数据则要跨两个D,ReadInt32("D100", 2)会拿两个相邻字拼接,HslCommunication会自动按三菱的低位在前规则处理。
3.3 VB.NET版本与C#的三处类型差异
VB.NET版本结构完全一样,换成VB语法即可,主要是三处类型和写法差异容易踩坑。第一,VB的Short就是C#的short,对应数据范围-32768到32767;无符号版本用UShort,对应C#的ushort,返回值里不能直接用Integer接收,否则会溢出。第二,VB里没有C#那种async匿名委托连写,但Task.Run一样能用,写法是Task.Run(Function() _plc.ReadUInt16(...))。第三,字符串拼接用的是&,不是C#的+,拼接软元件地址时最容易写错成"D" + address然后编译报错。
Imports HslCommunication Imports HslCommunication.Profinet.Melsec Public Class Fx5uTcpClient Private _plc As MelsecMcNet Public Function Connect(ip As String, Optional port As Integer = 6000) As Boolean _plc = New MelsecMcNet(ip, port) _plc.SetPersistConn(True) _plc.ConnectTimeout = 3000 Dim result As OperateResult = _plc.ConnectServer() If Not result.IsSuccess Then Console.WriteLine($"连接失败: {result.Message}") Return False End If Return True End Function Public Function ReadWords(startAddress As Integer, count As Integer) As OperateResult(Of UShort()) Return _plc.ReadUInt16("D" & startAddress, CUShort(count)) End Function Public Function WriteWord(address As Integer, value As Short) As OperateResult Return _plc.Write("D" & address, value) End Function End Class有一个很实际的坑顺带提一句:如果你把别人的VB项目拿到VS2010或者老版本VS里打开,双击一个.vb窗体文件时被系统提示“确保已安装文件类型(.vb)的应用程序”,这不是代码问题,是Windows的文件关联没关联到那个版本的devenv.exe。右键选打开方式,指向VS安装目录里的devenv.exe就行,或者直接把.vb文件拖进已打开的解决方案资源管理器。
另外,VB里的CUShort(count)在这里是必要的。HslCommunication的重载签名里长度是ushort,VB开启Option Strict后不会自动把Integer转成UShort,显示转换最省事。
3.4 需要Modbus-TCP时的NModbus4写法与地址映射
有些项目硬性要求统一走Modbus-TCP,比如要把FX5U接到第三方网关或者现有的OPC UA采集框架。FX5U本身支持Modbus-TCP从站,端口502,C#侧用NModbus4这类标准库就能实现,不必再依赖三菱私有帧。
using Modbus.Device; using System.Net.Sockets; using (var tcp = new TcpClient("192.168.1.10", 502)) { ModbusIpMaster master = ModbusIpMaster.CreateIp(tcp); master.Transport.ReadTimeout = 3000; // 读取保持寄存器,对应FX5U的D区 ushort[] values = master.ReadHoldingRegisters(0, 10); // 写单个保持寄存器 master.WriteSingleRegister(0, 1234); }NModbus4的地址端口是0到65535的寄存器序号,不是三菱的D编号。FX5U那侧需要在GX Works3的Modbus TCP从站设置里把保持寄存器起点映射到D0,映射之后,Modbus地址0对应的就是D0,地址1对应D1,以此类推。这里的偏移量完全取决于PLC侧映射表配置,别想当然地认为地址0就是D0,先读一遍PLC配置再写代码。
Modbus方式的劣势也在这里暴露:读M线圈要用功能码01,但地址映射得对M区再建一张表,位和字混排时容易错乱。所以我的建议还是开头那句:纯FX5U上位机就用MC协议,Modbus留给多品牌混用的现场。
4. C#循环数据采集与UI刷新卡顿的根治:批量读与异步刷新
4.1 UI卡顿根因:把UI线程当通讯线程
上位机开发里“c# 循环数据采集和ui刷新卡顿”这个问题,十有八九是这么写出来的:
// 错误示范:在UI线程里同步读PLC while (true) { textBox1.Text = _plc.ReadUInt16("D0", 1).Content[0].ToString(); Application.DoEvents(); Thread.Sleep(200); }这段代码有两个致命问题。第一个问题是UI线程被通讯阻塞,碰到PLC响应超时时,窗口完全冻住,拖动都会变成白框;DoEvents只让消息队列暂时喘口气,本质上还在轮询。第二个问题是每次只读1个字,MC协议一个请求的开销远大于数据本身,读取100个点就要发100次请求,通讯口被无谓占用,GX Works3在线监视都会变慢。
正确的方向是把通讯从UI线程挪走,同时把多次小请求合并成一次批量读。批量读的收益比异步更明显,读200个字和读1个字的时间差往往只有1到2毫秒,而发200次请求至少要多花几百毫秒。
4.2 用Task.Run和BeginInvoke做异步采集刷新
下面这套是我在设备数据采集里常用的写法,WinForm和WPF都适用,WPF把BeginInvoke换成Dispatcher即可:
private CancellationTokenSource _cts; private async void btnStart_Click(object sender, EventArgs e) { _cts = new CancellationTokenSource(); var token = _cts.Token; while (!token.IsCancellationRequested) { // 一次读取200个D寄存器,而不是循环读200次 OperateResult<ushort[]> result = await Task.Run(() => _plc.ReadUInt16("D0", 200), token); if (result.IsSuccess) { ushort[] snap = result.Content; txtSummary.BeginInvoke(new Action(() => { txtSummary.Text = $"首值={snap[0]}, 末值={snap[199]}"; })); } else { txtStatus.BeginInvoke(new Action(() => txtStatus.Text = $"采集失败: {result.Message}")); } // 用Task.Delay代替Thread.Sleep,支持CancellationToken取消 await Task.Delay(200, token); } } private void btnStop_Click(object sender, EventArgs e) { _cts?.Cancel(); }这里每个参数都有讲究。采集周期200毫秒适合绝大多数设备监控场景,PLC扫描周期加通讯往返时间通常不超过几十毫秒,留足余量;如果要做波形分析,压到50毫秒也行,但前提是FX5U那边批量读的响应时间稳定。批量长度200个字在MC协议单帧上限之内,一个请求约400字节报文,以太网毫无压力。
BeginInvoke的作用是异步投递UI更新,主线程不需要等待UI逻辑执行完就继续跑采集循环,UI不会卡。真要说极限性能,可以用Channel或环形缓冲把采集和UI彻底解耦,但绝大多数项目用不着,这套写法已经能解决卡顿问题。
4.3 BlockingCollection生产者消费者模型处理大批量数据
如果采集的数据既要刷新UI,又要写数据库或文件,把两件事放在同一个循环里会互相拖累。一个标准做法是:生产者线程循环读PLC,数据压进BlockingCollection;消费者线程从队列取数据做落库。这样读PLC的速度不会被SQL写入卡住。
var queue = new BlockingCollection<ushort[]>(1000); // 生产者:只负责读PLC Task.Run(() => { while (!_cts.IsCancellationRequested) { var r = _plc.ReadUInt16("D0", 200); if (r.IsSuccess) queue.Add(r.Content, _cts.Token); Thread.Sleep(100); } }); // 消费者:只负责处理 Task.Run(() => { foreach (var data in queue.GetConsumingEnumerable()) { // 写数据库、存文件都可以,慢不影响采集 SaveSnapshot(data); } });BlockingCollection的容量设成1000,相当于1000个快照的缓冲,按100毫秒一个快照算大约100秒的数据量。队列满时queue.Add会阻塞生产者,自然形成背压,PLC读线程不会无限堆积内存。这个模型适合对数据完整性有要求的场景,比如产品追溯需要连续记录。
要注意异常保护:生产者循环里通讯失败不能一直retry打日志,连续失败几次就应该进入重连流程,重连逻辑在第5章讲。
4.4 写操作要避开的三个坑
读数据没问题之后,写操作有三个高频翻车点。第一个是运行中写入许可,前面2.2讲过,没勾选的话Write调用会返回0xC061错误,PLC固件直接拒绝。第二个是位软元件的瞬时触发,设备上电、气缸动作这类M线圈信号,上位机常用“写True再写False”模拟一个脉冲,但两次TCP写之间没有时间间隔保证,可能被PLC当成同一个扫描周期里的连续变化,最好在两次写之间加20毫秒以上的延时,或者改用PLC侧的SET/RST指令语义。第三个是32位数据的字节序,FX5U的32位数据低字在前,读D100和D101拼Int32时D100是低16位,用HslCommunication的ReadInt32由库处理,自己拼的话按高位在前就会完全颠倒。
写D寄存器之前,也建议先读一次当前值,按位或运算置位、按位与运算复位,避免把同一组D寄存器里的多个压缩状态误清零。PLC没有提供按位写接口时,这是最稳妥的做法。
5. 验证FX5U通讯的三个反直觉技巧:抓包、重连与扫描周期
5.1 Wireshark抓6000端口:MC协议异常码其实很好读
通讯不通的时候先用Wireshark确认PLC到底有没有回包。选择跟PLC在同一网卡的抓包,过滤器写tcp.port == 6000,然后点上位机的读按钮。正常会看到一条请求和一条响应;只有请求没有响应,说明PLC侧通讯服务没起来或者网络隔离;有响应但上位机报错,右键选中响应包,在协议树里展开三菱MC协议层,错误码字段会直接显示类似C051的十六进制值。
| 异常代码 | 含义 | 排查方向 |
|---|---|---|
| 0xC051 | 起始软元件编号非法 | 地址映射算错、D区超范围 |
| 0xC056 | 请求点数非法 | 批量长度超过上限 |
| 0xC059 | 请求数据长度非法 | 报文长度字段拼错,多见于手拼帧 |
| 0xC061 | 远程操作被禁止 | 未开启运行中写入许可,见2.2 |
看到0xC051先查地址映射,别怀疑网线。0xC061也是很多人卡半天的问题,实际就是GX Works3里一个勾选项。
5.2 断线重连:别只判断一次IsSuccess
PLC断电重启、网线被踢掉、上位机休眠唤醒,都会让TCP连接处于假死状态。把重连做进采集循环里,但不能用延迟过大的Sleep去轮询。
int failCount = 0; while (!_cts.IsCancellationRequested) { var r = await Task.Run(() => _plc.ReadUInt16("D0", 100), token); if (r.IsSuccess) { failCount = 0; // 正常处理 } else { failCount++; if (failCount >= 3) { _plc.ConnectClose(); await Task.Run(() => _plc.ConnectServer(), token); failCount = 0; Thread.Sleep(1000); // 重连后等PLC把连接状态稳定 } await Task.Delay(500, token); } }重连前一定要ConnectClose()释放旧Socket,否则HslCommunication复用旧句柄时可能一直连不上。重连成功后清空累积的缓存数据,避免UI上闪现PLC断电前最后一个值。
5.3 通讯响应时间还受PLC扫描周期影响
很多上位机工程师会忽视一个事实:FX5U的以太网通讯处理是与PLC扫描周期同步的,响应时间不仅取决于网络往返,还取决于PLC程序一个扫描周期跑多久。如果PLC程序里有长任务,比如大段浮点运算或者循环指令,扫描周期被拉到10毫秒以上,上位机每个读写请求的响应也会跟着变慢,表现为通讯时快时慢。用GX Works3的监视功能可以直观看到当前扫描时间。
缓解办法很简单:在PLC程序的最前面加一段“数据快照”,把上位机经常要读的D/M值集中复制到一段专用D缓存区,上位机只读缓存区,既避免读到业务逻辑中途的不一致数据,也把PLC侧的通讯处理尽量固定在一个稳定的执行位置。扫描时间如果持续超过5毫秒,回去检查程序里的循环体,FX5U处理小数运算尤其慢,把可以预计算的量挪出周期任务能省下不少扫描时间。
本文还有配套的精品资源,点击获取