☰
C#三菱FX系列PLC串口通讯实战:专用协议报文解析与源码
2026/10/13 0:45:06 网站建设 项目流程

简介:本资源是一套面向工控自动化开发者的C#与三菱FX系列PLC串口通讯实战源码,专为初学者及具备基础C#和PLC知识的工程师设计,解决上位机与PLC编程口通信这一典型工业场景中的数据读取难题。压缩包共50个文件,包含10个核心C#源文件(如Form1.cs、PortControlHelper.cs)、3个可执行程序(exe)、1个协议参考文档(FX编程口协议中文.pdf)以及项目配置(.sln、.csproj)、资源文件(.resx、.ico)和调试支持文件(.pdb、.cache),整体体积仅3.63MB,结构清晰、开箱即用。已有1450人学习下载,源码已在FX-3GA-40MT机型实测通过,覆盖M、D、X、Y等常用软元件读取逻辑,注释完整、模块解耦良好,便于二次扩展至其他软元件或适配不同FX型号。读者可直接编译运行,快速掌握串口帧构造、校验计算、响应解析等关键通信细节,并基于现有框架开展定制化开发。 搞工控上位机的朋友应该都经历过这个阶段:项目还没开始切需求,甲方已经甩过来一句“用C#写个串口通讯,把三菱PLC里的数据读出来”。听起来不复杂,真动手才发现,光是搞清楚三菱PLC那条串口线里跑的到底是什么协议、什么报文格式,就能耗掉一整天。这篇文章我把自己落地的C#与三菱PLC串口通讯源码、报文拆解和实测过程中踩过的坑一次性整理出来,给准备拿C#写三菱PLC通讯、尤其是接手FX系列串口项目的朋友做个参考。

我自己是从组态软件转过来的,以前读写PLC靠的是组态软件的现成驱动,参数填完就能用。第一次裸写C#串口和三菱FX3U通讯的时候,连“通讯协议”这个概念都是模糊的,以为串口参数设对、发几个字节就有数据回来。结果不是PLC没反应,就是返回Garbage乱码,要么干脆串口被占用打不开。后来把协议、帧格式、校验和、返回帧结构一条条理清楚,才发现这东西本质上一点不难。难的是没人告诉你哪些细节是决定成败的。

1. 先别急着写代码:三菱PLC串口通讯到底哪套协议

1.1 三菱FX系列串口通讯的两条路线

三菱FX系列PLC(FX3U、FX3G、FX5U这些)串口通讯常见的有两条路线。第一条是编程口协议,也就是笔记本电脑用编程线连接PLC那个圆形mini-DIN 8针口,或者FX5U的USB口,GX Works2/3通过它读写PLC时用的就是这套协议。第二条是专用协议(也叫Computer Link协议、MC协议串口版),PLC上需要加装485扩展板或者通讯适配器,比如FX3U-485-BD、FX3U-485ADP,组态软件、触摸屏、上位机通过RS485总线访问PLC时用的更多是这套。

做C#上位机通讯,我个人优先建议走专用协议。原因是编程口的通讯线缆、握手时序、和上位机联调的限制相对多一些,而且如果PLC程序在调试,你上位机还连着编程口,经常出现“通讯被占用”“下载程序时上位机掉线”这类麻烦事。专用协议走485总线,调试时各干各的,互不干扰。

专用协议内部又分ASCII格式和二进制格式,实际工程里用ASCII格式的最多,帧里每个字节都是可见字符,抓包调试时一眼能看明白,即使没有专业抓包工具,用串口调试助手也能把报文读出来。本文的源码就是基于这套协议写完并跑通的。

1.2 为什么我推荐用三菱专用协议而不是Modbus

我知道很多人会问:现在Modbus-RTU这么通用,为什么不用Modbus?三菱FX3U从某个固件版本开始也确实支持Modbus-RTU从站功能,写上位机时用现成的Modbus库甚至不用自己拼帧,听起来好像更省事。

但实际用下来,专用协议有三个优势是Modbus替代不了的。第一个优势是地址直观。专用协议里读D100就是发“地址100”,读M50就是“地址50”,和PLC程序里的软元件编号一致,不用像Modbus那样去查映射表。第二个优势是批量读写方便,一次读连续的D寄存器、M继电器都很直接。第三个优势是把PLC作为主站连接变频器、仪表之类的第三方设备时,三菱PLC程序里仍然是通过专用协议指令去读写的,你在上位机侧用同样的协议思维去理解整个系统,思路是连贯的。

当然,Modbus-RTU也有它的用武之地,比如一个项目里既有三菱PLC又有第三方仪表、电力监控模块,总线要统一走Modbus,那该用还得用。这个后面第6章我详细说。

1.3 硬件连接与PLC侧参数的一次性设置

硬件层面,FX3U-485-BD是一块插在PLC左侧或右侧的扩展板,端子直接标了A和B,对应RS485的A+/B-。FX3U-485ADP则是独立的小模块,供电和接线方式稍有不同,但上位机视角看都是一条RS485总线。

具体接线有几条铁律:A接A、B接B,这俩接反了PLC大概率完全没响应;通讯线用屏蔽双绞线,屏蔽层单端接地;485总线的终端电阻,短线几十米内不加问题不大,超过一百米或者总线节点多,要按手册要求并联终端电阻。

PLC侧要确认通讯参数。FX系列有个特殊数据寄存器D8120,专门用来设置串行通讯的格式,包括波特率、数据位、校验位、停止位、协议类型、是否启用和校验等。这个寄存器可以在GX Works的“PLC参数-串口设置”窗口里配置,也可以在PLC程序里用MOV指令赋值。这里有个工程上特别容易踩的坑:如果你在程序里写了向D8120赋值的指令,那编程软件里的串口设置可能会被程序运行时的值覆盖,导致你在软件里看到的参数和实际跑的参数对不上。排查通讯问题时,两个地方都要看。

现场最常用的组合一般是19200bps、8数据位、偶校验、1停止位、和校验。上位机串口必须和PLC侧完全一致,差一个校验位都不能通讯。我之前就干过串口参数里把偶校验选成无校验,结果PLC面前就是不回应,浪费了半小时。

2. 报文拆解:读一个D寄存器,线上到底跑了什么

2.1 读命令帧的组成与校验和计算

三菱FX系列专用协议的读命令帧长这样:

ENQ + 命令代码 + 起始地址 + 点数 + ETX + 和校验

其中ENQ是0x05,表示请求;命令代码“01”代表批量读取(ASCII字符0x30 0x31);起始地址是4位十进制ASCII码,D100就是“0100”;点数是2位十进制ASCII码,读1个字就是“01”;ETX是0x03,表示帧结束;末尾再加2位十六进制ASCII码的和校验。

举个例子,读取D100开始的1个字,报文是:

05 30 31 30 31 30 30 30 31 03 32 35

拆开看:0x05是ENQ,“01”是读命令,“0100”是D100的十进制地址,“01”是点数,0x03是ETX,“25”是根据前面内容算出来的校验和。注意,这里地址是十进制的100,不是十六进制的0x64,这个细节坑过不少刚上手的人。D1000就是“1000”,D258就是“0258”,前面补零到4位。

校验和的计算方法是:从命令字符的ASCII码开始加,一直加到ETX(就是0x03)为止,把累加和的低8位转换成2位大写十六进制ASCII字符。比如上面这个例子,命令“01”的ASCII码0x30+0x31=0x61,地址“0100”四个字符都是0x30,加起来0xC0,点数“01”又是0x30+0x31=0x61,再加上ETX的0x03,总和是0x61+0xC0+0x61+0x03=0x125,取低8位0x25,转成ASCII就是“25”。

很多网上的例子发完帧不会算校验和,或者把CRC16那套用在专用协议上,那肯定是通讯不上的。专用协议用和校验,不是CRC。

2.2 写命令和批量读写

写命令的帧格式和读命令几乎一样,只是命令代码改成“02”,正文里增加了要写入的数据内容,每1个字用4位十六进制ASCII码表示,高位在前。比如向D100写入0x1234这个值,报文是:

05 30 32 30 31 30 30 30 31 31 32 33 34 03 校验和

拆开看:0x05是ENQ,“02”是写命令,“0100”是D100,“01”是1个字,“1234”就是要写入的数据,0x03是ETX,后面两位是校验和。如果是批量写3个字,数据部分就是连续12个十六进制字符,每个字4位,按顺序排列。

批量读取也是一样,读D100到D102共3个字,点数字段就是“03”,返回的数据是连续的12个ASCII十六进制字符。不过实际使用中,我建议单次读取点数不要超过64个字。一是三菱专用协议不同型号对单次读取的点数有上限要求,二是上位机侧一次处理太多数据,出现半包粘包时反而不好恢复。需要读大批量数据,比如几百个D寄存器,写成循环,一次读64个或者32个,分几次读完。

2.3 PLC的返回帧长什么样

读命令的返回帧很多人容易搞错。PLC正常响应时,会先返回一个ACK(0x06)表示命令接收成功,然后再返回一个数据帧,数据帧格式是:

STX + 数据内容 + ETX + 校验和

STX是0x02,数据内容是连续的4位ASCII十六进制字符,每个字对应4个字符,从低地址到高地址排列,但每个字内部是高位字节在前。比如读D100和D101两个字的返回值,可能是“1234ABCD”,那D100=0x1234,D101=0xABCD。

这里有个常见坑:串口数据是异步到达的,ACK和数据帧可能分两次到达,也可能粘在一起一次性到达。很多新手在读完一个字节发现是0x06就以为结束了,或者只读到一个STX帧就把前面ACK当垃圾丢掉。正确做法是先把ACK读掉,再等STX数据帧;或者干脆做一个接收缓存,把完整的一帧按帧头帧尾拼出来再解析。

如果是写命令,正常返回就是一个ACK(0x06),非常简单。如果返回NAK(0x15),说明PLC拒绝了这条命令,后面通常还跟两位错误代码ASCII字符。比较常见的错误代码包括:02控制帧格式错误、03校验错误、06地址越界、07数据超范围等。看到这些错误码,基本就能定位到是地址写错还是帧格式不对了。

2.4 浮点数的存储顺序

做上位机控制变频器频率、读取温度、压力这类浮点数据时,会碰到三菱PLC浮点数存储顺序的坑。

三菱PLC里32位浮点数占用连续的2个D寄存器。按照FX系列常见的数据存储习惯,16位的字组合成32位数据时,通常是低地址存放低16位,高地址存放高16位。比如D0和D1组合成一个浮点数,D0存的是这个浮点数的低16位,D1存的是高16位。

举个例子,PLC里想表示7.0,32位浮点数在IEEE 754编码下十六进制是0x40E00000,那D0=0x0000、D1=0x40E0。C#这边要把两个16位无符号整数拼成float,正确的做法是:

uint raw = ((uint)high << 16) | low; float value = BitConverter.ToSingle(BitConverter.GetBytes(raw), 0);

这里BitConverter.GetBytes(raw)在PC上得到的是小端字节序,也就是低字节在前的4个字节,刚好和PLC那边“低地址存低16位、高地址存高16位”的顺序对应上,转换出来就是正确的浮点数。

不过每个项目的PLC程序风格不一样,有些老工程师习惯在PLC里把32位数据的高低字交换存储,这种情况下上位机读出来的float数值就明显不对。排查方法很简单:连续读两个D寄存器,把高低16位换一下位置再转float,对比哪个数值是“看起来合理的”,就知道这个PLC的存储顺序了。我一般会在调试界面加一个“交换高低字”的勾选框,实测非常省心。

3. C#串口通讯源码:可以直接复制进项目里的封装类

3.1 环境准备与SerialPort配置

我使用的是Visual Studio 2022,项目类型是Windows窗体应用,目标框架看现场电脑情况选,如果是给客户部署的老Windows系统,用.NET Framework 4.7.2最稳;如果现场电脑是Win10/Win11且环境统一,用.NET 6/8也没问题。注意.NET Core/.NET 5+的System.IO.Ports不是在框架里默认带的,需要通过NuGet安装System.IO.Ports包。

串口关键参数就四个:串口号、波特率、数据位、停止位、校验位。三菱FX系列专用协议最常见的组合是19200、8、偶校验、1停止位,但最终以PLC侧D8120实际设置和编程软件参数里的配置为准。如果对不上,表现就是PLC完全没反应或者返回乱码,串口调试助手发命令都试不出东西来。

还有两个小参数容易被忽略。一个是ReadTimeout和WriteTimeout必须设置,否则PLC没响应时程序会卡死在读串口那一步,整个界面假死。另一个是ReceivedBytesThreshold,如果走DataReceived事件,默认1个字节触发一次,但对工控轮询场景,我后面会说其实不推荐用这个事件。我先给出一段串口打开和参数配置的代码:

using System.IO.Ports; SerialPort port = new SerialPort(); port.PortName = "COM3"; port.BaudRate = 19200; port.DataBits = 8; port.Parity = Parity.Even; port.StopBits = StopBits.One; port.ReadTimeout = 1000; port.WriteTimeout = 1000; port.Open();

3.2 FxSerialProtocol类的核心实现

我封装了一个串口通讯类,命名为FxSerialProtocol,所有专用协议相关的操作都收敛在这个类里。这样窗体只关心业务逻辑,不关心协议细节。类的基本成员和打开关闭方法如下:

public class FxSerialProtocol { private SerialPort _port; private readonly object _lockObj = new object(); public bool IsOpen { get { return _port != null && _port.IsOpen; } } public bool Open(string portName, int baudRate = 19200, Parity parity = Parity.Even, int readTimeout = 1000) { Close(); _port = new SerialPort(portName, baudRate, parity, 8, StopBits.One); _port.ReadTimeout = readTimeout; _port.WriteTimeout = readTimeout; _port.Open(); return _port.IsOpen; } public void Close() { if (_port != null) { if (_port.IsOpen) _port.Close(); _port.Dispose(); _port = null; } } }

注意所有读写操作都放在_lockObj锁里,这是为了多线程环境下的安全。工控程序里Timer轮询、按钮写值、日志线程有可能同时访问串口对象,不加锁会出现同一个串口被两个线程同时写入导致帧错乱的问题。

接着是核心的发送和接收逻辑。BuildFrame方法负责把命令正文转换成一帧完整的报文,包括ENQ、正文、ETX、校验和:

private byte[] BuildFrame(string commandBody) { byte[] body = Encoding.ASCII.GetBytes(commandBody); int sum = 0x03; // 先把ETX算进去 foreach (byte b in body) { sum += b; } string sumText = (sum & 0xFF).ToString("X2"); byte[] sumBytes = Encoding.ASCII.GetBytes(sumText); byte[] frame = new byte[body.Length + 2 + sumBytes.Length]; frame[0] = 0x05; // ENQ Buffer.BlockCopy(body, 0, frame, 1, body.Length); frame[body.Length + 1] = 0x03; // ETX Buffer.BlockCopy(sumBytes, 0, frame, body.Length + 2, sumBytes.Length); return frame; }

接收响应时,要区分两种情况:写命令返回ACK,读命令返回ACK+STX数据帧。所以我写了一个ReadResponse方法,先探测第一个字节是不是ACK,再继续读,直到ETX结束:

private byte[] ReadResponse() { List<byte> bytes = new List<byte>(); int first = _port.ReadByte(); bytes.Add((byte)first); if (first == 0x06) // ACK,继续读数据帧(读命令) { int second = _port.ReadByte(); bytes.Add((byte)second); } // 逐字节读,直到ETX while (true) { int b = _port.ReadByte(); bytes.Add((byte)b); if (b == 0x03) { break; } } // 把ETX后面的2个和校验字节也读掉,保持接收缓冲干净 for (int i = 0; i < 2; i++) { bytes.Add((byte)_port.ReadByte()); } return bytes.ToArray(); }

这个是简化版,实际项目里我会在循环里加一个最大长度限制,防止PLC异常时无限等待。对工业场景来说,宁可报超时错误重试,也不要让程序卡死。

批量读取字软元件的方法:

public ushort[] ReadWords(int startAddr, int count) { lock (_lockObj) { string body = $"01{startAddr:D4}{count:D2}"; byte[] frame = BuildFrame(body); _port.Write(frame, 0, frame.Length); byte[] response = ReadResponse(); return ParseWordsResponse(response, count); } } private ushort[] ParseWordsResponse(byte[] response, int count) { int stxIndex = Array.IndexOf(response, (byte)0x02); int etxIndex = Array.IndexOf(response, (byte)0x03); if (stxIndex < 0 || etxIndex < 0 || etxIndex <= stxIndex) { throw new InvalidOperationException("返回帧格式错误"); } string data = Encoding.ASCII.GetString(response, stxIndex + 1, etxIndex - stxIndex - 1); if (data.Length < count * 4) { throw new InvalidOperationException("返回数据长度不足"); } ushort[] result = new ushort[count]; for (int i = 0; i < count; i++) { string wordText = data.Substring(i * 4, 4); result[i] = Convert.ToUInt16(wordText, 16); } return result; }

批量写多个字的方法:

public bool WriteWords(int startAddr, ushort[] values) { lock (_lockObj) { StringBuilder dataBuilder = new StringBuilder(); foreach (ushort value in values) { dataBuilder.Append(value.ToString("X4")); } string body = $"02{startAddr:D4}{values.Length:D2}{dataBuilder}"; byte[] frame = BuildFrame(body); _port.Write(frame, 0, frame.Length); byte[] response = ReadResponse(); return response.Length > 0 && response[0] == 0x06; } }

这几个方法看起来不多,但已经覆盖了最常见的读D寄存器、写D寄存器场景。现场只要保持串口参数一致,这套代码连上真实PLC就能用。

3.3 把浮点数读出来的C#代码

既然PLC里32位浮点数是占用两个D寄存器,那上位机读浮点的方法就是连续读2个字,然后组合。组合方式我在第2.4节已经说了,下面给出完整方法:

public float ReadFloat(int startAddr) { ushort[] words = ReadWords(startAddr, 2); ushort low = words[0]; ushort high = words[1]; uint raw = ((uint)high << 16) | low; return BitConverter.ToSingle(BitConverter.GetBytes(raw), 0); }

如果你排查后确定现场PLC存储顺序是反的,也就是高地址存放低16位,那把组合语句换成:

uint raw = ((uint)low << 16) | high;

就行。为了调试方便,建议在窗体上做一个开关,手动选择是高字在前还是低字在前,第一次联调时两种都试一下,总有一种读数是对的。

4. 实测最容易翻车的几个点

4.1 串口被GX Works占用的经典冲突

这个是我接手现场项目时遇到最高频的问题。C#程序打开串口时报“串口被占用”或者打开成功但读写全部没反应,排除了半天,发现电脑上还开着GX Works2或GX Works3,并且连接的就是同一个PLC。只要GX Works保持在线监控状态,串口或者编程口就一直被它占用,上位机程序根本没机会读写。

解决办法很直接:联调时只要跑上位机程序,就把GX Works的在线监控断开;反过来,如果要去PLC里改程序,先把上位机程序退出。还有一点,如果你用的是485通信,而电脑上同时插着USB转编程线连接到PLC的编程口,有些型号PLC会在两个通讯口同时启用时出现访问阻塞,表现为485总线上的响应特别慢。所以现场调试,尽量只保留一条物理连接路径。

4.2 485接线和USB转串口驱动的坑

RS485接线接反是最经典的“没反应”故障。上位机发送读命令时,用串口调试助手能看到发送的帧,但永远等不到PLC的任何返回。这时候第一反应不要怀疑代码,先去看接线,A对A、B对B,反了立刻换过来。好的USB转485模块上会有TX/RX指示灯,如果发命令时模块的TX灯亮但RX灯不亮,八成是PLC那边没响应;如果RX灯亮但收到的数据是乱码,检查波特率和校验位。

USB转串口适配器这个环节也容易出问题。市面上的PL2303芯片在Win10/Win11上驱动兼容性差,经常出现“设备管理器里显示正常,但打开串口就是没数据”的情况,本质上是因为新版驱动和旧版芯片不匹配,导到通讯失败。这正好对应很多人在网上搜到的“PL2303HX停产装旧驱动无法通讯”的经典问题。我的经验是,工控现场尽量用FTDI FT232或者CH340这类芯片的模块,尤其是FT232,兼容性在win10/win11下很稳定。如果只能用PL2303,而且系统是新版本,就在设备管理器里把驱动回滚到旧版,或者手动指定驱动目录安装兼容驱动。

4.3 半包、粘包和数据错位

串口通讯的本质是字节流,没有“一条完整报文”的概念。PLC返回的ACK和STX数据帧可能分两次到达,也可能粘在一起。如果你在DataReceived事件里每次都简单ReadExisting然后直接解析,很容易出现第一次只读到ACK、第二次才读到STX帧,或者一次读多帧数据的情况。

我的处理方式是:读写全部走同步方式,发完命令后阻塞等待完整响应,用ReadByte逐字节读,直到ETX为止。这样天然规避了半包粘包问题,因为每次读响应都是针对当前这条命令的,一读一答,不会出现多帧数据堆积。程序里只要注意ReadTimeout别太短,给PLC留够响应时间,一般500毫秒到1秒是合理的。

如果项目确实要用事件驱动模式,比如PLC主动上报数据,那接收逻辑就得改成一个缓存队列。把每次收到的字节追加到字节列表里,然后循环检查列表里有没有完整的“STX...ETX...校验”帧,解析完一帧就移除一帧。这个思路和TCP粘包处理一样,工控程序里非常实用。

4.4 DataReceived事件里的UI跨线程问题

很多新手会直接在SerialPort.DataReceived事件里写代码更新窗体的Label和TextBox,运行起来发现程序直接异常崩溃,或者界面偶尔卡死。原因是DataReceived事件在后台线程触发,而WinForms控件的UI属性只能在创建该控件的线程(主线程)上修改,跨线程访问就会抛异常。

解决办法有两个。第一个是使用Invoke强制切换到UI线程,代码长且啰嗦;第二个更推荐——不要在DataReceived事件里做业务处理,改成Timer轮询。上位机读PLC的场景本来就是周期性刷新,比如200毫秒读一次D寄存器刷新显示,Timer里直接同步调用ReadWords方法就行,简单直观,完全绕开跨线程问题。串口事件留给那些“PLC主动上报”的场景用足够了,平时轮询为主的项目根本不需要它。

5. 从“能读能写”到可以交付:轮询、点位表和日志

5.1 用Timer做轮询刷新最简单

功能Demo跑通之后,接下来就是把它变成一个可以交付给操作工使用的上位机界面。多数工控上位机的核心逻辑就是:定时器周期读取PLC数据、更新显示、用户点击按钮写值。我强烈建议用System.Windows.Forms.Timer做轮询,而不是把读写逻辑塞在按钮事件里。

窗体上放一个Timer,Interval设200毫秒,Tick事件里调用封装的FxSerialProtocol读取需要监控的D寄存器,再更新到界面。200毫秒的刷新周期对大多数工业监控场景都够了,肉眼看起来实时性很好,而且给PLC留了充足的响应时间。如果刷新周期低于100毫秒,就要注意了,PLC的串口处理能力有限,请求太频繁会造成响应延迟,甚至触发PLC侧通讯超时。

private void timerRefresh_Tick(object sender, EventArgs e) { if (!plc.IsOpen) return; try { ushort[] values = plc.ReadWords(100, 4); textBoxStatus.Text = string.Join(",", values); } catch (TimeoutException ex) { // 超时先重试,连续几次失败再弹错误提示 retryCount++; if (retryCount > 3) { Log("通讯超时:" + ex.Message); retryCount = 0; } } }

5.2 点位表驱动:几十台设备不至于写死

如果项目里要监控的设备点位很少,比如就三五个温度、一个启停状态,直接在代码里硬编码寄存器地址也能凑合。但如果要监控几十上百个点位,或者有多台PLC,硬编码就是灾难。

我习惯把点位做成配置表,用JSON文件或者XML文件维护。每行记录包括点位名称、PLC站号、软元件类型、寄存器地址、数据类型、量程、单位、是否可写、报警上下限等信息。程序启动时加载这张表,动态生成读写任务列表,根据点位表循环刷新。这样新增加一台设备、一个监控点位,只需要改配置文件重新启动程序,不用重新编译代码。

用点位表后有两点要注意。第一,点位表的地址和数据类型必须和PLC程序约定一致,尤其是浮点数、双字、位抽出来的时候,别把两个相邻点位搞混。第二,如果不同点位类型不同,每种读取方式要独立封装好,读浮点位和读整数点位不能串。

5.3 写值前后的校验与日志

上位机写PLC数据是对生产有实际影响的操作,不能像Demo那样写个按钮就直接WriteWords。我的习惯是写值之前先做三查:查写入范围、查当前值、查权限。比如要写变频器频率,先检查目标频率是否在0到50Hz之间,如果超出范围直接拒绝并提示。

写值完成后还要读回验证。写一个值,立刻读回同一个地址,对比写入值和读回值是否一致,不一致说明通讯过程中发生了数据错误,需要记录日志并报警。这个流程对现场故障排查特别有价值,可以快速判断是上位机没写进去,还是PLC程序把值做了处理,还是通讯线路丢包。

日志记录建议把报文级别的信息也留下来,包括发送的原始帧、接收的原始帧、解析后的业务值。这样一旦现场出问题,直接看日志就能定位是协议层还是业务层的问题,不用抱着笔记本去车间现场复现。

6. 后续扩展:从串口到以太网MC协议/MODBUS-TCP

6.1 为什么Q系列设备普遍走以太网

串口通讯靠谱归靠谱,但有一个天然瓶颈:单条485总线的速率和节点数有限。如果项目上到了Q系列PLC、L系列PLC,或者一个车间几十台设备要组网,通常就不会再考虑串口了,而是走以太网。Q系列PLC的以太网模块上跑的MC协议,和三菱FX系列串口专用协议是同一个思路,帧结构相似,只是传输载体从串口换成了TCP/IP,并且增加了网络号、PC号、站号之类的路由信息。

从C#上位机的角度看,和网络MC协议通讯本质就是把之前往SerialPort里写字节改成往Socket里写字节,报文的拼装逻辑和校验和逻辑很多可以复用。这也是为什么我建议哪怕只做串口项目,也一定要把帧的拼装和解析单独封装,不要和SerialPort绑死。

6.2 三菱以太网MC协议和串口协议的关系

以太网MC协议常见的帧格式有ASCII方式和二进制方式。如果是ASCII方式,你会发现里面有很多熟悉的元素:命令“01”还是读、“02”还是写,数据排列还是4位ASCII十六进制字符,校验方式也类似。区别在于报文开头多了以太网头,包括子帧头、网络号、PC号、请求目标模块IO号、请求目标模块站号等固定字节,结尾的校验和处理可能和串口不同。

做迁移时不要把所有代码推倒重来,把帧拼装的公共逻辑抽出来。比如构造一个抽象方法GetFrameHeader和GetFrameTail,串口协议实现填ENQ/ETX/SUM,以太网协议实现填网络头/尾帧。这样底层换传输介质,上层业务代码基本不动。我去年把一个FX3U串口项目升级到FX5U以太网,整个迁移过程只花了小半天,得益于当时做了这个抽象。

6.3 换个思路:识别协议需求,别一上来就写源码

最后分享一个经验。但凡是接手新项目,先不要急着打开Visual Studio写代码。先去车间看PLC型号、通讯模块型号、确认现场布线是串口还是以太网、搞清楚PLC程序里有没有读写D8120、确认通讯参数。最好用串口调试助手或者简单的测试程序先和PLC建立通讯,能正常读回数据再开始写完整的上位机代码。

我第一次做三菱PLC通讯项目时,就是打开代码直接写,写完发现PLC没反应,来回改了几十次都没用,最后发现是485线接反了。所以,协议搞不清楚,代码写得再漂亮也是白搭。把报文格式吃透,用最小的工程验证链路通不通,再往上叠业务逻辑,这个顺序尽量不要反过来。

在实际项目里,我把这套C#串口通讯源码复制到新项目之后,通常还会再做一个小调整:把类里所有和窗体相关的东西全部去掉,让它成为一个纯协议层组件,这样即使是后续要做成Windows服务、或者嵌入到其他系统里,这套通讯代码都可以直接复用。这也是我坚持在工控上位机开发里做分层的原因,协议层、业务层、界面层各管各的,不论项目大小,长期维护下来都轻松得多。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询