1. 从零搭建C#与松下PLC的通信环境
第一次接触松下FP系列PLC通信时,我踩过不少坑。记得有个物流分拣项目,设备已经进场了才发现通信协议没调通,现场十几号人等着调试,急得我后背直冒汗。今天我就把多年积累的实战经验整理出来,让你少走弯路。
硬件准备清单就像组装一台电脑:
- 松下FP-XH或FP0R系列PLC(我常用的是FP-XH C30T)
- 9针RS232串口线(注意必须是直连线,交叉线无法通信)
- USB转串口适配器(推荐用FTDI芯片的,CH340经常出幺蛾子)
- 24V直流电源(给PLC供电)
接线时要特别注意引脚定义,这里有个血泪教训:有次我把2脚和3脚接反了,排查了整整一天。正确的接法是:
PLC端(母头) 计算机端(公头) 2 ------------------- 3 (TXD) 3 ------------------- 2 (RXD) 5 ------------------- 5 (GND)软件环境配置分三步走:
- 安装松下编程软件FPWIN GR(版本7.0以上)
- Visual Studio我用2022社区版,记得勾选.NET Framework 4.8
- NuGet安装SerialPort库:
Install-Package System.IO.Ports
测试通信前,先用串口调试助手验证硬件是否正常。发送这条测试指令(16进制格式):
25 30 31 23 57 43 53 52 30 30 31 32 31 32 30 0D如果收到类似%01$WC14的回复,说明物理层通信正常。注意波特率默认是9600,校验位要设为偶校验。
2. 深入解析MEWTOCOL协议帧结构
这个协议就像我们打电话的固定话术,必须严格遵循格式。有次我漏写了个结束符CR,PLC直接拒绝响应,调试时这种低级错误最浪费时间。
命令帧的解剖图:
% 01 # RCS X 0000 1D CR │ │ │ │ │ │ │ │ │ │ │ │ │ └── 结束符(0x0D) │ │ │ │ │ └───── BCC校验码 │ │ │ │ └────────── 数据区 │ │ │ └──────────── 寄存器类型 │ │ └─────────────── 指令代码 │ └────────────────── 站号 └──────────────────── 起始符响应帧的两种面孔:
- 正确响应:
%01$RC120**(第4字符是$) - 错误响应:
%01!RC21(第4字符是!)
BCC校验码的计算是个易错点,这里分享我的调试技巧:先用这个在线工具验证计算逻辑是否正确。算法本质是异或累加,但要注意字符编码:
public string CalculateBCC(string input) { byte xorResult = 0; foreach (char c in input) { xorResult ^= (byte)c; } return xorResult.ToString("X2"); // 转成16进制大写 }实测案例:读取X0触点状态 发送:%01#RCSX000014应返回:%01$RC0101(最后两位01表示ON状态)
3. 核心通信指令的封装技巧
在贴标机项目里,我封装了一套通信类库,现在分享几个关键方法。先建立基础通信类:
public class PanasonicPLC { private SerialPort _port; public void Connect(string portName) { _port = new SerialPort(portName, 9600, Parity.Even, 7, StopBits.One); _port.NewLine = "\r"; // CR结束符 _port.Open(); } public string SendCommand(string cmd) { string fullCmd = $"{cmd}{CalculateBCC(cmd)}\r"; _port.WriteLine(fullCmd); return _port.ReadLine(); } }触点读写三剑客:
- 读取单个触点(RCS指令):
public bool ReadContact(string contactType, int address) { string cmd = $"%01#RCS{contactType}{address:D4}"; string resp = SendCommand(cmd); return resp.Substring(6,2) == "01"; } // 使用:bool isOn = ReadContact("X", 0);- 写入单个触点(WCS指令):
public void WriteContact(string contactType, int address, bool value) { string cmd = $"%01#WCS{contactType}{address:D4}{(value ? "1" : "0")}"; string resp = SendCommand(cmd); if(resp.Contains("!")) throw new Exception("写入失败"); }- 批量读取数据寄存器(RD指令):
public short[] ReadRegisters(int startAddr, int count) { string cmd = $"%01#RDD{startAddr:D4}{count:D2}"; string resp = SendCommand(cmd); List<short> values = new List<short>(); for(int i=0; i<count; i++) { string hexStr = resp.Substring(6+i*4, 4); values.Add(Convert.ToInt16(hexStr, 16)); } return values.ToArray(); }数据转换的坑:松下PLC采用低位在前存储方式。比如读取到"1F02"要转成0x021F(543)而不是0x1F02(7938)。我专门写了转换工具方法:
public static short ConvertPlcValue(string hex) { string lowByte = hex.Substring(2,2); string highByte = hex.Substring(0,2); return Convert.ToInt16(lowByte + highByte, 16); }4. 工业级应用的异常处理策略
在化工厂项目里,我遇到过电磁干扰导致通信中断的问题。后来总结出这套健壮性方案:
通信重试机制:
public string SafeSendCommand(string cmd, int retry=3) { while(retry-->0) { try { return SendCommand(cmd); } catch(TimeoutException) { Thread.Sleep(100); _port.DiscardInBuffer(); } } throw new Exception($"指令{cmd}发送失败"); }心跳检测方案:
private Timer _heartbeatTimer; public void StartHeartbeat() { _heartbeatTimer = new Timer(_ => { try { var resp = ReadContact("X", 0); LastActiveTime = DateTime.Now; } catch { OnDisconnected?.Invoke(); } }, null, 0, 5000); // 每5秒检测一次 }常见错误代码速查表:
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| !RC10 | 帧格式错误 | 检查BCC校验和CR结束符 |
| !RC20 | 非法指令代码 | 核对MEWTOCOL协议手册 |
| !RC21 | 数据区格式错误 | 检查地址和数值格式 |
| !RC50 | 通信超时 | 检查接线和波特率设置 |
日志记录建议用NLog,配置为每日滚动日志:
<nlog> <targets> <target name="file" xsi:type="File" fileName="${basedir}/logs/${shortdate}.log" layout="${longdate}|${level}|${message}" /> </targets> <rules> <logger name="*" minlevel="Debug" writeTo="file" /> </rules> </nlog>记得在产线环境测试时,通信线一定要用带屏蔽层的双绞线。有次省成本用了普通线,电机一启动就丢包,后来换了Belden的3106A线才解决问题。