ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C#上位机与松下PLC通信:Mewtocol协议解析与串口开发实战

C#上位机与松下PLC通信:Mewtocol协议解析与串口开发实战

简介:面向C#上位机与松下面板控制器通信的工程样例,核心场景为手机屏幕异物检测,适合有C#基础、从事工业自动化上位机开发的工程师参考。压缩包共113个文件、约5.64MB,以44个cs源代码为主体、20套resx/resources界面资源配合,包含sln解决方案、编译生成的exe程序,以及accdb数据库、xls配置表,工程结构清晰。已有1900人学习下载。代码覆盖串口/TcpClient连接、松下PLC寄存器读写指令封装、数据解析与界面联动,并考虑了超时重试、异常捕获等容错处理;SetParaY.cs、SetJiemo.cs等窗体文件保留完整布局,便于对照实际应用二次开发和运行调试。整套代码可直接作为松下PLC通信的入门到进阶模板使用,能显著缩短上位机联调周期,对理解制造现场的检测流程和设备通信机制很有帮助。

1. 上位机与松下PLC通信:先解决“连不上”再说组态

做C#上位机开发,最让人头疼的往往不是界面好不好看,而是设备通信能不能稳定跑起来。一个很典型的场景:车间里松下PLC控制着产线,上位机需要每隔100毫秒读取一次产量数据和设备状态,同时还要支持参数下发。不少同学拿到需求后第一反应是先画界面、拉控件,等到联调时才发现连串口都打不开,或者数据读出来全是乱码。这套方案要解决的核心问题,就是用C#通过串口或以太网,按松下Mewtocol协议和PLC稳定交换数据,并且把通信层做成可复用的模块,后续换设备型号、换传输方式都不用推倒重来。适合正在做C#上位机课设、毕设,或者刚进工控行业需要快速上手松下PLC通信的开发者。

2. 通信方案设计:三层模块划分与Mewtocol协议帧怎么定

2.1 为什么选Mewtocol协议,而不是Modbus

松下PLC自带串口通信默认采用Mewtocol协议,这不是程序员能随便改成Modbus的,除非PLC端重新配置特殊通信模式。所以方案的第一步就是接受Mewtocol,把它当成和PLC对话的唯一语言。Mewtocol属于主从协议,上位机永远是主机,PLC作为从机,每次通信都由上位机发起请求,PLC返回响应。帧结构非常紧凑,但也是初学时最容易翻车的地方。

典型的请求帧格式是这样的:起始符%、两位十六进制站号、命令字符、地址及数据段、校验字节、回车符CR。响应帧则区分正常响应和错误响应,错误响应里会带错误码。比如读取数据寄存器,命令码常见的是RCP,写入则用WCP,读触点是RCS,写触点是WCS。具体哪些命令码可用,取决于PLC型号和端口设置,建议开发前先从对应型号的通信手册里把命令码表格摘出来,做成一张常量表。

2.2 通信层、协议层、业务层各自干什么

我一般会把通信代码拆成三层,避免把所有逻辑堆在窗体的事件里。通信层只负责串口或TCP连接的打开、关闭、字节收发;协议层负责把业务数据包成Mewtocol帧,以及把PLC响应帧解析成结构体;业务层则专注于“读回来之后怎么用”,比如换算成温度、次数,或者写入数据库。

这样做的好处是,如果以后从串口换成以太网,只需要替换通信层的实现,协议层和业务层一行都不用改。协议层里最关键的是帧构建和校验算法。Mewtocol的校验是BCC:对帧内起始符之后、校验字节之前的所有字符做异或,结果再转成两位十六进制ASCII字符串。这个校验必须自己算,不能依赖串口底层,很多通信偶发失败都是这里写错了。

2.3 地址体系与数据映射,先列一张表

松下PLC的寄存器区有D(数据寄存器)、R(文件寄存器)、W(字触点区)等。Mewtocol帧里的地址不是直接用D100这种文本,而是经过编码的。不同型号的编码规则不同,比如有些型号要求地址长度固定为4位十六进制,有些要求5位。方案里可以先定义一个数据点配置类,把“用户看到的PLC地址”和“协议帧中的地址段”做成映射表,这样改地址时不需要动代码,只改配置文件。

我的建议是,项目一开始就做一张Excel格式的点表,里面至少包含:点位名称、数据类型、读写权限、PLC地址、协议地址编码、缩放比例、单位。上位机读取这个点表自动生成数据结构。这样即使是毕设,答辩时也能让导师觉得你已经有了工程化思维,而不是纯粹在写demo。

3. 关键实现:C#串口通信类与读写指令的代码落地

3.1 串口通信基础类:从打开端口到接收响应

所有通信都收拢在同一个类里。先看核心实现,这里用System.IO.Ports.SerialPort,也是C#做串口通信最常用的库。下面的代码演示如何打开端口并执行一次同步读写:

public class MewtocolClient : IDisposable { private SerialPort _port; public bool Connect(string portName, int baudRate, int dataBits, StopBits stopBits, Parity parity) { _port = new SerialPort(portName, baudRate, parity, dataBits, stopBits); // 接收超时设为1000毫秒,避免等不到PLC响应时卡死 _port.ReadTimeout = 1000; _port.WriteTimeout = 1000; try { _port.Open(); return true; } catch (Exception ex) { // 常见原因:端口被占用、串口号不对、USB转串口驱动异常 Console.WriteLine($"[连接失败] {ex.Message}"); return false; } } public void Disconnect() { if (_port != null && _port.IsOpen) { _port.Close(); } } public void Dispose() { Disconnect(); _port?.Dispose(); } }

这里有几个参数要说明:波特率、数据位、停止位、校验位必须和PLC通信参数完全一致,任何一个不匹配都会导致乱码或者完全无响应。松下PLC出厂默认通常是9600波特率、8数据位、1停止位、无校验,但很多现场为了稳定性会改成19200。所以在连接前,我通常会先去PLC的编程软件里确认通信配置,而不是靠猜。

3.2 帧构建与BCC校验算法

协议层需要把发送内容按Mewtocol规范拼出来。下面这段代码负责构建一帧请求:

public static string BuildFrame(string stationNo, string command, string address, string data = "") { // 帧结构:% + 站号(2位) + # + 命令 + 地址 + 数据 + BCC + CR string header = "%" + stationNo + "#"; string prefix = header + command + address + data; byte bcc = 0; foreach (char c in prefix) { // BCC校验:对起始符之后的所有字符做异或,注意起始符'%'不参与 bcc ^= Convert.ToByte(c); } string bccStr = bcc.ToString("X2"); return prefix + bccStr + "\r"; }

使用的时候,比如读取PLC的D100这个数据寄存器,站号为1,地址段按当前型号编码成0100,那请求就是BuildFrame("01", "RCP", "0100")。如果没有数据部分,data传空字符串即可。写入时则把要写入的数据放在data段,注意数据内容是ASCII编码的十六进制或者十进制字符串,具体格式取决于你读到的寄存器类型是HEX还是BCD。

这段代码的关键点是BCC算法:一定从起始符之后开始异或,而且结尾的回车符不需要参与。很多同学会把起始符也异或进去,导致PLC一直不回帧。另外BCC结果是两位十六进制,必须是大写字母,转为ASCII后追加到帧尾。

3.3 发送请求并解析响应,处理应答延时

同步读写最容易遇到的问题就是PLC刚收到请求还没处理完,上位机已经等到超时。做法是给发送和接收都设置超时,并且在软件层面做重试。下面是一个完整的读周期示例:

public string ReadRegister(string stationNo, string address) { string frame = BuildFrame(stationNo, "RCP", address); byte[] sendBytes = Encoding.ASCII.GetBytes(frame); lock (_port) { // 清空接收缓冲,避免上次残留数据干扰本次解析 _port.DiscardInBuffer(); _port.Write(sendBytes, 0, sendBytes.Length); // 读取响应,正常情况下响应帧同样以%开头,以CR结束 string response = _port.ReadTo("\r"); if (string.IsNullOrEmpty(response) || !response.StartsWith("%")) { throw new TimeoutException("PLC响应超时或帧格式错误"); } // 此时response末尾带\r,最好再校验一下BCC if (!CheckBcc(response)) { throw new InvalidDataException("BCC校验失败,数据可能被干扰"); } // 根据命令类型截取数据段,RCP响应中数据通常在地址段之后 return response.Substring(7, 2); } }

这里ReadTo("\r")会自动阻塞到收到回车符,省去自己拼字节数组的麻烦。但要注意,如果PLC由于故障没返回回车符,线程就会一直卡住,所以ReadTimeout必须设成有限值。解析响应时,不同命令码返回的数据位置略有不同,我不能在这里把所有变体都列全,因为不同松下PLC系列的命令差异太大了。最稳妥的办法是把你实际使用的PLC型号的响应帧样例抓下来,对照着写解析索引。这也是我平时调试时的标准动作。

3.4 写入寄存器:先写单个点位,再批量写

写入和读取类似,只是命令从RCP换成WCP,数据段要带上要写入的值。返回值通常是一个短帧,表示写入是否成功。需要注意的是,PLC运行状态下并不是所有寄存器都允许写入,比如一些特殊寄存器是只读的,写入会返回错误码。实际项目里我一般会先写一个测试按钮,只写一个内部没有实际作用的D寄存器,验证通了再去接真实设备。

批量写时还有一个坑:数据长度必须和波特率匹配,如果一次写入超过一定字节数,可能中间被拆分,PLC会判定帧错。建议一次最多写8个字,如果超过就拆成多次写入并间隔20毫秒以上。这个间隔不是死参数,如果PLC扫描周期较长或者使用了通信中断,可能需要调大。

4. 常见问题排查:最容易翻车的4个通信坑

4.1 现象:串口助手能收到数据,但C#程序收不到

原因多半不是代码问题,而是程序同时打开了同一个串口。串口是独占资源,一个进程占用后,另一个进程打不开。很多开发者在调试时会开着串口助手没关,然后程序去连接,自然失败。解决方法是先关闭串口助手,或者把波特率参数改成和串口助手一样的配置。另外某些USB转串口设备有虚拟串口号漂移问题,拔插后COM号变了,程序里写死的COM3会失效,最好在界面上提供端口下拉框。

4.2 现象:发送请求后PLC一直没响应

先排查硬件接线,尤其是RS485的A/B线是否接反,RS232的TXD和RXD是否交叉。电气上没问题再检查站号,Mewtocol的站号是两位十六进制,比如站号1要写成01,不能写1。还有一点容易被忽视:PLC的串口通信可能没有使能,或者端口的协议被设成了其他协议。去PLC编程软件里把通信端口设置成“计算机链接(Computer Link)”模式,并核对波特率,再重启PLC让配置生效。

4.3 现象:通信一直正常,但每隔几分钟偶发一次超时

这种“玄学”问题通常是干扰或时序冲突。现场有大功率设备启停时,通信线容易被干扰,解决方法是使用屏蔽双绞线并把屏蔽层单端接地。如果通信距离短但依然偶发超时,则要检查代码里是否有多线程并发访问串口。我之前就遇到过两个定时器同时调用写函数,导致帧交叉,后来加了lock (_port)才解决。另一个常见原因是PLC扫描周期较长,500毫秒一次,上位机100毫秒就读一次,部分请求在PLC忙时被丢弃。遇到这种情况,要么降低读取频率,要么在协议层做失败重试,我习惯重试两次,间隔200毫秒。

4.4 现象:读回来的数据值很不合理,像是移位了

这盘多半是地址编码错误或者数据格式理解错误。松下寄存器的数据可能是BCD编码,也可能是二进制数,直接转成十进制就会得到奇怪结果。比如PLC里存了一个十进制数1234,在BCD模式下字节内容是0x12 0x34,如果按二进制解析就成了4660。处理方法是先确定PLC侧数据格式,再在协议层写一个转换函数。另一种情况是地址编码长度不对,导致PLC实际响应的是相邻寄存器。调试时可以先用松下官方通信调试工具看原始响应帧,再对着代码解析结果判断到底哪里错了。

5. 协议扩展与数据上抛:把PLC数据汇入业务系统

5.1 从串口到以太网:通信层替换不影响上层逻辑

很多方案做到串口通信就停了,但实际项目往往要求上位机能通过网络把PLC数据送到监控大屏或数据库。Mewtocol协议同时支持串口和以太网,只是帧封装在TCP流里,协议层的帧结构不用变。我可以把第3章里的串口客户端类抽象成接口IMewtocolTransport,串口实现和TCP实现分别写各自的SendAndReceive方法。业务层只依赖接口,这样换传输方式就是替换一行依赖注入。

TCP实现时要注意沾包和拆包问题。因为TCP是流式传输,可能一次写好几帧,或者一帧被拆成两次到达。需要按回车符\r作为帧边界,在接收循环中缓存字节,直到找到回车符再交给协议层解析。这个做法和串口ReadTo("\r")本质一样,但TCP场景下要处理多帧粘在一起的情况。

5.2 定时轮询与断线重连:通信稳定性依赖状态机

上位机不能永远等数据,需要一套状态机来管理连接情况。我习惯把通信状态定义成三态:未连接、正常、故障。连接成功进入正常状态,之后用System.Windows.Forms.Timer或后台线程定时轮询。每次读写失败时,错误计数加一,连续超过三次就切换到故障状态并尝试重连。重连不能太频繁,否则PLC端口可能被反复打开关闭;用一个退避策略,第一次等1秒,第二次等2秒,最多等10秒。

下面这段代码展示轮询逻辑的基本结构:

private async void PollTimer_Tick(object sender, EventArgs e) { // 关闭按钮不让重入,避免上一个请求还没返回又发下一帧 if (_isPolling) return; _isPolling = true; try { string value = _client.ReadRegister("01", "0100"); double number = ConvertValue(value); UpdateDisplay(number); // 更新界面 WriteToDatabase(number); // 上抛到数据库 _failCount = 0; } catch (Exception ex) { _failCount++; // 连续失败才切换状态,避免单次干扰就重连 if (_failCount >= 3) { TransitionToFault(ex.Message); TryReconnect(); } } finally { _isPolling = false; } }

这样设计的好处是,如果现场只是瞬时干扰,重试几次就能恢复,不需要把整个通信链路推倒重来。等毕设答辩或项目验收时,你可以直接演示拔掉通信线再插回去,系统能在几秒内自动恢复正常,这一项能加不少分。

5.3 数据上抛:定时写数据库和本地日志

读回来的数据最终要落到数据库里。我一般用SQLite做本地存储,原因很简单:免安装、单文件、方便调试。线上项目再考虑换SQL Server。建表时把时间戳、点位编码、原始值、换算值一起存,这样后续排查问题时能看到“故障前后数据到底跳变到什么值”。同时写一个日志文件,记录每次通信异常的时间、错误信息、当前重试次数。这个日志在答辩时非常有用,可以用来证明系统具备可追溯性。

需要注意的是,数据库写入和PLC通信不能放在同一个线程里串行执行,否则一次数据库卡顿会直接阻塞PLC通信。建议PLC轮询线程只把数据放入Channel或ConcurrentQueue,另一个后台线程专门负责批量写库。这样做即使数据库临时不可用,PLC通信也不会中断。

6. 验收与答辩:怎么证明这套通信方案能交付

6.1 用串口助手做帧级验证

验收的第一步不是看业务数据准不准,而是看帧对不对。先把C#程序发出的每一帧抓下来,和串口助手发出的同一帧做十六进制逐字节对比。对比通过后,再在PLC模拟器或真实PLC上做“命令-响应”匹配测试。我会准备一张测试表,列出测试项、预期响应、实际响应、是否通过。至少包含:正常读取、正常写入、写入只读寄存器返回错误码、站号错误无响应、波特率不匹配乱码这五个场景。

6.2 模拟故障,验证重连机制

答辩时最容易让导师认可的是现场演示断线重连。把通信线的中间串一个开关,程序运行中手动断开,观察上位机是否在几秒内跳到故障状态,再恢复连接,观察是否能自动回到正常状态。如果时间允许,还可以人为发送一帧伪造数据,验证BCC校验能否把它拦截下来。这些都属于“可演示的鲁棒性”,比单纯界面上显示数据更有说服力。

6.3 答辩时的讲解顺序

不要一上来就讲代码。先讲需求背景:为什么需要上位机,为什么选择Mewtocol。再讲总体设计:三层架构、点表配置、状态机。然后挑一两个关键代码片段讲帧结构和BCC校验。最后展示测试记录和线上运行截图。整个过程控制在10分钟内。导师如果问你“波特率为什么选9600”,你就回答先和PLC配置对齐,再根据通信距离和稳定性需求调整;如果问你“怎么保证不丢数据”,就回答轮询失败重试和应用层日志。

做这个项目之后,我养成了一个习惯:任何PLC通信项目,先花一小时把官方通信手册里的帧格式和地址编码做成表格,再开始写代码。那次因为没核对地址编码,导致整整一天都在查一个根本不存在的问题。从那以后,每次拿到新设备型号,我先写一个通信自检程序,把本机发送帧、响应帧、解析结果三段都打印出来,确认无误后再往上叠加业务逻辑。希望这套方案能帮你在C#与松下PLC通信上少踩几个坑,顺利把项目交付出去。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进