
做机器视觉项目这些年我最大的体会是真正让项目“卡壳”的往往不是相机选型也不是算法调参而是那个看起来最简单的环节——上位机和PLC之间的通讯。你想象一下这个场景玻璃划痕检测的视觉算法终于跑通了检测结果明明已经在工控机的内存里了但PLC那边就是收不到气缸不动作良品和不良品全混在流水线上出去了。后来一查问题就出在C#上位机通过串口和Panasonic松下PLC通讯这一环上。这篇博文我就结合实际项目把C#机器视觉上位机和松下PLC串口通讯这件事从头到尾捋一遍。从通讯协议、C#代码实现到视觉联动场景、常见坑位排查尽量讲透。适合刚入门的上位机开发工程师也适合做视觉集成、正在被PLC通讯折磨的朋友参考。松下FP系列PLC的串口通讯走的是松下自己的MEWTOCOL协议。别看这个名字有点陌生它本质上就是一套“你问我答”的报文规则。只要把帧格式组对、把校验算对、把地址写对通讯就成功了一大半。接下来我把每一块都拆开讲。1. 机器视觉项目里上位机到底在忙什么1.1 一次完整的视觉检测流程先别急着写代码我们得先搞清楚上位机在整个视觉项目里的位置。拿我做过的一个玻璃划痕检测项目举例完整的检测流程是这样的PLC控制传送带把玻璃送到拍照工位后输出一个“到位信号”。上位机收到这个信号触发工业相机拍照拿到图像后跑视觉算法。算法这边通常需要对图像做预处理比如划痕检测时我会先用开运算把玻璃表面的孤立噪点去掉再用闭运算把断裂的细微划痕连起来开闭运算的结构元素大小和迭代次数要结合现场打光情况调这个后面说。处理完以后上位机得到“OK”或者“NG”的结果再通过串口把这一个结果写入PLC的指定寄存器。PLC读到这个值控制气缸把不良品推出去。整个链路里上位机干的事情本质上是三件一是接收PLC的信号二是跑视觉算法三是把结果回传给PLC。其中第一步和第三步都绕不开“通讯”这两个字。视觉算法只是产生一个结果PLC只认寄存器和触点的状态中间这个翻译官就是C#上位机程序。1.2 通讯方案选型为什么先选串口在中小型机器视觉项目里串口通讯是目前最稳妥、也最常见的选择原因有三个成本低几乎所有工控机都自带串口没有串口也可以用USB转串口芯片选CH340或者FTDI的线缆都行驱动装好就能用。调试直观串口调试助手一发一收看得清清楚楚不像以太网通讯要考虑IP、端口、防火墙一堆东西。响应速度快对视觉检测这种毫秒级的产线节拍来说9600波特率下写一条指令也就几毫秒完全够用。当然如果项目里有多台PLC、数据量又大可以考虑换Modbus TCP或者松下专有的以太网协议。但如果是单台PLC一台工控机的典型组合串口就是性价比最高的答案。顺带说一句如果你之前用过威纶通触摸屏会发现触摸屏通过网线和上位机板卡做Modbus TCP通讯时新建工程要选对设备类型。串口通讯也一样第一件事就是把协议对象选对——松下FP系列上位机链接协议用的就是MEWTOCOL别选成Modbus RTU否则报文格式对不上通讯必然失败。2. 松下PLC串口通讯核心先说协议再说代码2.1 硬件连接与串口参数通讯的第一步是物理连接。松下FP系列PLC一般自带编程口形状是圆头八针需要用松下专用的编程线转成RS232或者USB口接到电脑。另外部分FP-X、FP0R型号自带或可以扩展RS485串口这种场景下走RS485可以拉更远的距离传输也更稳。连接好硬件后串口参数必须和PLC侧保持一致。我常用的配置是波特率9600数据位8校验位None停止位1PLC侧的参数可以在松下的编程软件里设置不同系列菜单位置不一样一般在“系统寄存器”或者“通讯设置”里。这里有个关键点PLC侧的站号也要记下来默认是1对应报文里的“01”。如果你改了站号上位机报文里的站号也要跟着改。注意串口参数错误是通讯故障的第一大原因。很多现场问题不是代码写得不对而是PLC侧波特率设成了19200上位机还在用9600。排查的时候先统一参数再谈报文。2.2 MEWTOCOL协议的基本帧结构松下FP系列PLC的上位机通讯协议叫MEWTOCOL有ASCII模式和二进制模式我们一般用的是二进制模式也就是MEWTOCOL-COM。它的帧结构很固定一帧报文分成六个部分部分说明示例起始码固定为“%”%站号2位十六进制ASCII字符01命令代码4位ASCII字符RCP/WCP等RCP文本地址、点数、数据等可变内容0064000ABCC校验2位十六进制ASCII字符5E结束码回车符CR\rBCC校验是把起始码“%”之后、结束码“\r”之前的所有字符按ASCII码逐字节做异或结果转成2位十六进制大写字符串。这个校验算法很重要很多人写的程序收不到正常响应问题就出在校验算错。举个例子发送读取DT100开始的10个字的指令报文长这样%01RCP0064000A5E\r其中“01”是站号“RCP”是读寄存器命令“0064”是DT100的十六进制地址“000A”是10的十六进制点数“5E”是BCC校验值。响应帧也同样以“%”开头站号、命令、文本原样返回文本后面跟的是数据区。比如上面这条读指令的响应会返回10个字每个字用4位十六进制表示也就是40个字符的数据区然后是校验和回车。这里也顺带提一下常用命令读寄存器用RCP写寄存器用WCP读触点状态用RCS强制写触点用WCS。视觉得出结果回传PLC最常用的就是WCP直接把结果写入数据寄存器。2.3 地址与器件映射最容易踩坑的地方MEWTOCOL协议的地址不是直接填“DT100”这种字符串而是要把器件地址转成协议规定的映射编码。这一点是初学者最容易懵的地方。以我用的FP-X系列为例数据寄存器DT的地址映射规则是把DT编号转成十六进制比如DT0对应0000DT10对应0001DT100对应0064DT1000对应03E8。内部继电器R的映射要复杂一些R0到R9是一段R10到R9FF又是另一段中间涉及十进制和十六进制的切换X、Y这类IO触点也有自己的编码段。我的建议是动手写代码前先把松下PLC通讯手册里的“存储器地址分配表”找出来对照着查一遍。不同系列FP0R、FP-X、FPΣ的地址映射规则有差异不要凭经验觉得“DT100的十六进制是64那就直接填0064”就完事凡是涉及到R、X、Y的一定以手册为准。踩过一次坑以后我才明白地址映射表这种东西不是用来背的是拿来“按图索骥”的。放在手边查一次调通一次记到自己的笔记里比什么都管用。3. C#串口通讯代码从能跑到跑稳3.1 SerialPort基础配置与打开C#里串口通讯最基础的就是System.IO.Ports.SerialPort这个类。打开串口这一步本身不复杂但实际项目中有一个隐藏问题设备管理器里的COM口号会变。今天插在COM3明天换个USB口可能就变成COM7了。所以上位机最好做成可配置的我一般把串口号、波特率、站号这些参数放到配置文件里界面上留一个下拉框动态刷新可用串口。using System.IO.Ports; SerialPort port new SerialPort(); port.PortName COM3; port.BaudRate 9600; port.DataBits 8; port.Parity Parity.None; port.StopBits StopBits.One; try { port.Open(); if (port.IsOpen) { // 打开成功这里可以更新UI状态 } } catch (Exception ex) { // 串口被占用、端口不存在、驱动异常都会走到这里 MessageBox.Show(打开串口失败 ex.Message); }打开失败的原因有很多最常见的是USB转串口驱动没装好。设备管理器里如果看到带黄色感叹号的设备先装驱动。CH340、FTDI、2303这几类芯片的驱动我都装过CH340的驱动在官网下载最新版一般就能解决问题FTDI的要留意是VCP还是D2XX模式模式不对端口不会显示。3.2 组帧和BCC校验实现接下来是整个通讯最关键的部分组帧和BCC校验。我封装了一个CommHelper类里面核心就两个方法一个是计算BCC一个是生成读指令、写指令的帧。public class PanasonicMewtocolHelper { private string stationNo 01; // PLC站号 /// summary /// 计算BCC校验对%和\r之间的所有字符做异或结果转大写十六进制 /// /summary private string CalcBcc(string content) { byte xor 0x00; foreach (char c in content) { xor ^ (byte)c; } return xor.ToString(X2); } /// summary /// 读取数据寄存器RCP命令 /// /summary /// param namestartAddr起始地址如DT100传入100/param /// param namecount读取字数/param public string BuildReadWordCommand(int startAddr, int count) { string text startAddr.ToString(X4) count.ToString(X4); string cmd stationNo RCP text; string bcc CalcBcc(cmd); return % cmd bcc \r; } /// summary /// 写入单个字WCP命令 /// /summary /// param nameaddr目标地址如DT0传入0/param /// param namevalue要写入的数值/param public string BuildWriteWordCommand(int addr, int value) { string text addr.ToString(X4) 0001 value.ToString(X4); string cmd stationNo WCP text; string bcc CalcBcc(cmd); return % cmd bcc \r; } }注意CalcBcc方法里参与校验的内容只包含站号、命令、文本不包括开头的“%”和结尾的“\r”。这一点要跟手册核对清楚因为不同协议的BCC计算范围不一样算错了就是所有报文都被PLC拒收表现是“发出去没有任何响应”。写指令的文本结构是地址4位 点数4位 数据。上面代码写入的是1个字所以点数固定是“0001”数据用4位十六进制表示。如果你要一次写入多个字把每个字的数据挨个拼接在后面就行。3.3 接收线程与帧解析串口的接收不能放在UI线程里做否则界面一卡通讯也跟着乱。我一般用SerialPort自带的DataReceived事件这个事件在后台线程触发收到数据就往队列里放再由解析线程或者定时器处理。但这里有一个容易踩的坑DataReceived事件触发时缓冲区里的数据不一定是一整帧。可能是半帧也可能是好几帧粘在一起。所以不能直接有一帧读一帧必须自己做“缓存判断完整帧”的逻辑。private Queuebyte recvBuffer new Queuebyte(); private void port_DataReceived(object sender, SerialDataReceivedEventArgs e) { int byteCount port.BytesToRead; byte[] buffer new byte[byteCount]; port.Read(buffer, 0, byteCount); lock (recvBuffer) { foreach (byte b in buffer) { recvBuffer.Enqueue(b); } } }等到要取完整响应帧的时候扫描队列里的数据找到起始码“%”然后一路往下读直到遇到结束码“\r”取出来这一个完整帧去解析。如果缓冲区里没有完整帧说明数据还没收全继续等下一轮。很多新手直接用红线代码ReadLine去读串口量产项目里真的不建议这么干。ReadLine依赖结束符一旦通讯干扰导致中间丢了一个字符整帧就废了而且还会影响后续所有帧的解析。用队列状态机的方式看起来麻烦实际上最稳能扛住现场各种乱七八糟的干扰。3.4 不卡UI的高频刷新方案热搜词里经常看到“C#循环数据采集和UI刷新卡顿”这个问题在做PLC数据监控的时候特别典型。很多人一开始把串口读取和文本框赋值都写在一个方法里循环去跑结果UI越跑越卡最后直接假死。原因很简单UI刷新是在UI线程执行的而串口读取是高频的。如果每收一帧数据就直接去更新文本框UI线程会被大量刷新任务淹没。尤其是从串口后台线程调用Form控件的时候必须跨界更新UI如果没有正确用Invoke程序甚至直接抛异常。我现在的做法是分层处理串口线程只负责收数据放进队列。一个独立的工作线程做协议解析把解析结果放到一个公共的缓存对象里。UI层用一个System.Windows.Forms.Timer比如500毫秒刷新一次把缓存里的最新值显示出来。// 定时器里统一刷新避免高频UI更新 private void timer_Refresh_Tick(object sender, EventArgs e) { if (latestValue null) return; if (this.textBoxValue.InvokeRequired) { this.textBoxValue.BeginInvoke(new Action(() { this.textBoxValue.Text latestValue.Value.ToString(); })); } else { this.textBoxValue.Text latestValue.Value.ToString(); } }这里有个细节跨线程刷新UI的时候用BeginInvoke而不是Invoke。Invoke是同步等待UI线程序列执行完再返回如果UI线程卡了后台线程也会被堵住。BeginInvoke是异步的丢进去就继续干活不会互相拖累。在实际项目里这个区别在UI操作稍微多一点的时候特别明显。还有一个技巧高频场景下UI不一定要每帧都刷新如果监控的是PLC里连续变化的数值可以只取最新值刷新中间的值可以直接丢掉。毕竟人眼每秒能感知的变化就那么几次刷新太频繁反而让界面文字闪个不停。机器视觉检测产线里对DT寄存器里的计数数据做监控时我都是这样降频处理的。4. 视觉联动场景实操触发拍照与结果回传4.1 场景APLC触发相机拍照先讲典型的视觉检测触发场景。产线上产品到位后PLC输出一个触发信号。上位机怎么知道这个信号来了两种做法第一种是读PLC的触点状态也就是用RCS命令去轮询某个内部继电器R0或者输入触点X0的状态。模式切换为“收到PLC触发请求”时该触点变ON上位机读到ON就去触发相机拍照。第二种是PLC主动向上位机发一条触发命令。松下PLC可以用“发送指令”主动给上位机推数据上位机收到特定帧后触发拍照。这种方式实时性更好但PLC侧的梯形图也要额外配置。我实际项目里用得比较多的是第一种轮询方式。虽然轮询有点“笨”但它简单可靠逻辑链路清晰。轮询间隔我通常设在20到50毫秒对产线节拍来说足够了。下面这段代码是轮询读取触点状态的简化版// 轮询读取R0触点的ON/OFF状态模拟“是否请求拍照” string cmd helper.BuildReadContactCommand(0, 1); // RCS命令 port.Write(cmd); Thread.Sleep(30); string response GetResponse(); // 从队列里解析出完整响应帧 // 判断响应帧的数据区最后一位是1还是0 if (response.Contains(01) || response.Contains(1)) // 具体判断逻辑看帧格式 { // 触发相机拍照 TriggerCamera(); }4.2 场景B检测结果写回PLC视觉算法跑完以后会得到一个检测结果。玻璃划痕检测这种场景结果就是一个枚举值OK或者NG。上位机要做的事情相当简单把结果写入PLC的数据寄存器例如DT0约定1代表OK2代表NG。int resultCode resultIsOk ? 1 : 2; string command helper.BuildWriteWordCommand(0, resultCode); port.Write(command);PLC侧梯形图只需要用比较指令判断DT0的值等于1继续放行等于2触发剔除气缸动作。这一条看似简单的指令其实承载了视觉系统与产线执行机构的全部逻辑联系通讯稳定性的重要性在这里就体现出来了——如果这条指令偶发发送失败后果可能就是不良品直接流出产线。所以我在写结果回传这段代码的时候加了重试机制发送完成后校验响应帧如果没收到或者收到错误码自动重发最多重发三次。只有三次都失败才判定通讯异常并报警让操作员来人工处理。4.3 联调时建议的日志设计这里说说日志因为联调阶段没有日志基本寸步难行。现场联调通讯问题的时候我除了在界面上显示收发状态还一定会把每一帧的原始收发数据都写到日志文件里。格式很简单带时间戳发送和接收分开标[2025-06-12 14:23:45.123] TX: %01WCP0000000100014E2B\r [2025-06-12 14:23:45.158] RX: %01WCP0000000100014E2B\r收到一帧发出去一帧时间戳精确到毫秒。这样做有两个好处一是出问题的时候可以精确复现整个通讯过程二是可以通过时间戳看出指令周期是否稳定。我见过很多项目前期联调没问题量产以后偶发卡顿最后就是靠这种日志比对才发现是某条指令因为数据量大耗时比其他指令长了好几倍。日志放本地文件就行注意按天分割别让单个文件无限增长。我习惯把日志目录放到程序exe同级的Log文件夹下文件名带日期方便现场同事远程取日志回来分析。5. 常见问题与排查技巧实录5.1 常见问题速查表我把这些年做C#上位机串口通讯遇到的典型问题整理成了一张表联调的时候照着这张表排查能省下大量时间现象可能原因排查方向串口打开失败端口被占用、驱动异常、端口不存在检查设备管理器、换COM口、重新装驱动CH340/FTDI/2303发送指令PLC无响应串口参数不一致、线序不对、站号错误、帧格式错误先用串口调试助手发同样的帧测试确认PLC侧有响应收上来的数据是乱码波特率或校验位不一致两边统一参数再次确认PLC侧系统寄存器设置偶发超时或丢帧通讯线过长、干扰大、地电位差换屏蔽双绞线、加磁环、降低波特率到9600、代码加超时重试UI卡顿高频刷新UI、跨线程阻塞分层刷新、降频刷新、用BeginInvoke程序偶发崩溃SerialPort被并发访问所有串口读写统一加锁或者用独立收发队列5.2 串口调试助手先于代码这几个排查手段里我最想强调的就是先拿串口调试助手验证再调代码。曾经有一个项目上位机程序写好了一运行就发现PLC没有响应。我在代码里排查了半天组帧、校验、串口参数都检查了一遍没找到问题。后来拿串口调试助手手动发了一条RCP指令发现PLC正常返回。这就说明问题出在上位机程序而不是协议理解。再回头查发现是我在组帧的时候忘了在帧尾加“\r”串口调试助手手动输入的时候回车键附带了这个字符所以能通代码却漏掉了。就这一个字符查了一下午。所以我的流程永远是先拿串口调试助手手动发一条指令确认硬件和协议都通再写代码。代码写完后用串口调试助手模拟PLC端回帧验证上位机解析逻辑。两边都验证通过才上现场联调。5.3 判断成功响应帧的细节还有一个细节容易判断错松下PLC的正常响应和异常响应的帧结构不一样。正常响应帧里命令代码会原样回显比如你发“RCP”响应帧里也带“RCP”。但如果PLC收到非法指令或地址错误返回帧里的命令代码会变成“!”后面跟着两位错误码。比如“%01!0100...”这种格式。我在代码里判断响应时会先检查响应帧里是不是以“!”开头如果是说明PLC返回了错误码此时解析出来的地址或命令肯定有问题。这个细节太容易被忽略了很多新手只判断“有没有收到响应”不判断“收到的是不是成功响应”结果程序提示通讯正常实际上PLC一直在报错拒绝执行。5.4 抗干扰的几个土办法现场环境对串口通讯的影响做项目前很难想象。电柜里变频器一启动通讯就开始偶发超时这种情况我遇到过不止一次。几个实测有效的土办法通讯线用屏蔽双绞线屏蔽层单端接地一般接PLC侧。线缆远离变频器动力线实在绕不开就交叉走线不要平行走线。波特率能低就低9600比19200抗干扰能力强很多视觉检测项目串口数据量不大9600完全够用。上位机程序里所有读写指令都带超时重试重试次数不要太多三次左右够了。这些办法看着简单但每一条都能解决一类真实的现场问题。做上位机通讯稳定比快重要重试比抱怨重要。写在最后做C#机器视觉上位机通讯这么多年我最深的感受是协议本身并不复杂复杂的永远是现场。串口通讯看似老土但在机器视觉项目里它依然是最可靠的通讯方式之一你的算法再牛结果传不出去一切都是零。如果你正在做一个松下PLC的串口通讯项目我的建议很简单第一先拿串口调试助手把协议调通再写代码第二地址映射表一定要查手册不要凭经验猜第三日志一定要留全这是你排查现场问题的最后一根救命稻草。希望这篇分享能帮你少踩几个坑项目顺利上线。