ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#串口上位机对接松下PLC:Mewtocol协议帧与CRC校验详解

C#串口上位机对接松下PLC:Mewtocol协议帧与CRC校验详解 简介C#上位机与Panasonic PLC通讯资源包是一份亲测有效的通讯实现工程面向C#上位机开发人员和工业自动化领域初学者解决PC端与Panasonic PLC之间的实时数据交互问题。压缩包共包含27个文件整体体积约82KB主要文件类型为cs源代码、exe可执行程序、pdb调试符号、resx及resources资源文件、sln解决方案等目录结构完整清晰便于直接查看代码结构和编译运行。功能方面覆盖了对PLC外部输出的单个及多个写操作、内部继电器的控制、数据寄存器的读写、多个外部输出与内部寄存器的读取以及PLC运行状态的获取基本满足常见PLC通讯开发中的核心需求。通过该资源读者可直观学习到C#上位机与PLC通讯的串口实现思路、代码组织方式和排错要点并可将其中读写逻辑提取为供自身项目复用的基础模块。目前该资源已有1226人学习适合新手快速入门也适合有一定经验的开发人员参考借鉴。1. C#上位机对接Panasonic PLC为什么卡在串口这一层松下FP系列PLC自带的COM口不需要额外买通讯模块直接用RS-232C或RS-485线就能跑Mewtocol协议。很多C#上位机项目卡住不是界面写不出来而是对着手册拼命令帧时站号、命令码、地址、CRC累加和、CR结束符这五段的关系没理清楚导致命令发出去PLC要么不响应要么回一个错误码。这篇把一套完整的C#串口上位机源码拆开讲SerialCommunication.cs负责串口帧收发、CRC校验和同步等待Form1.cs把按钮事件映射成具体的PLC读写命令覆盖单个/批量触点读写、数据寄存器读写和运行状态获取。新手可以直接把串口通讯层移植到自己的项目里熟手可以重点看多寄存器合并读和UI轮询防卡顿的写法。2. Mewtocol协议帧结构与串口参数确认2.1 先确认PLC侧的串口参数松下PLC的COM口默认参数并不统一FP-X、FP-XH这类机身自带串口多数是9600波特率、8数据位、无校验、1停止位但通过通信插件扩展出来的串口可能被配置成38400甚至更高。连不上之前先到PLC编程软件FPWIN GR或Control FPWIN Pro的系统寄存器里确认COM口通信设置这是排查的第一步也是最常被跳过的第一步。参数常见取值说明波特率9600 / 19200 / 38400与PLC侧一致RS-485多站时不低于9600数据位8Mewtocol整帧都是ASCII字符固定8位校验位无 / 偶校验大多数默认无校验有校验时CRC算法不变停止位1常见默认少数老机型可能为2流控无日常串口调试基本不开启硬件流控提示修改PLC侧串口参数后必须断电重启PLC才生效。热修改后上位机还在用旧参数握手是时而通时而不通的常见来源。C#侧用System.IO.Ports.SerialPort类初始化构造参数的顺序容易写错按端口名、波特率、校验位、数据位、停止位排列_port new SerialPort(COM3, 9600, Parity.None, 8, StopBits.One);参数说明Parity.None对应无校验8是数据位StopBits.One是停止位。如果PLC侧是偶校验这里改成Parity.Even帧结构不用动。USB转串口线在Windows 10以上常被识别成COM3以上编号PLC侧没反应时先去设备管理器确认端口号没被蓝牙或其它虚拟串口占用。2.2 Mewtocol命令帧命令码与地址编码Mewtocol的本质是ASCII文本行协议一条完整命令帧的格式固定为% 站号(2位) 命令码(2位) 地址与数据区 累加校验(2位) CR站号是PLC单元号十六进制ASCII表示默认01命令码两位读/写加对象类型组合而成帧尾必须是回车符CR也就是十六进制的0x0D。本资源涉及的功能对应命令码功能命令码地址与数据区格式读触点RCS类型四位地址两位数量写触点WCS类型四位地址数量按位打包数据读数据寄存器RCP类型(DT/WR)四位地址两位数量写数据寄存器WCP类型(DT/WR)四位地址数量每个寄存器四位的值获取运行状态SC无地址区地址一律是四位十六进制ASCII字符串。比如读Y0到Y9这10个外部输出触点命令码RCS、类型Y、起始地址0000、数量0A拼起来是RCSY00000A加上站号后为%01RCSY00000A再追加两位CRC和一个CR就是完整发送帧。注意数量字段是两位十六进制10个点写成0A而不是10。协议里把十进制数字直接拼进帧是格式错误最典型的诱因。2.3 CRC累加校验从%开始逐字符求和Mewtocol的校验不是CRC16多项式而是累加和从%字符开始到数据区最后一个字符为止把所有字符的ASCII码相加取总和的低8位再转成两位大写十六进制追加到帧尾。例如对%01RCSY00000A逐字符求和并截取低字节得到类似B4的结果完整发送帧就是%01RCSY00000AB4\r。提示%参与累加CR不参与。很多移植失败的案例都是边界没卡准把%前面的内容或CR之后的换行符也加进校验值导致PLC一直回21号错误码。响应帧的结构与命令帧对称% 站号 命令码 数据区 CRC CR。写操作正常响应时没有数据区读操作正常响应时数据区是该操作读回的具体值异常时数据区是两位错误码例如21表示累加校验错误、22表示帧格式错误。3. SerialCommunication.cs封装CRC校验与响应等待3.1 串口初始化与接收缓冲设计SerialCommunication.cs这个类的职责很纯粹管好SerialPort、拼帧、等响应、把错误码抛给上层。构造时传入端口名和站号站号默认填01换PLC单元号时不用改业务代码public class SerialCommunication : IDisposable { private SerialPort _port; private readonly string _stationNo; private readonly object _sync new object(); private readonly ManualResetEventSlim _signal new ManualResetEventSlim(false); private StringBuilder _buffer new StringBuilder(); private string _response; public SerialCommunication(string portName, string stationNo 01) { _stationNo stationNo; _port new SerialPort(portName, 9600, Parity.None, 8, StopBits.One) { ReadTimeout 500 }; _port.DataReceived OnDataReceived; } }DataReceived事件触发时机在.NET Framework和.NET Core下表现有细微差异但原则一致不要在事件线程里直接操作UI控件先把字节收进StringBuilder等缓冲末尾出现CR再认为一帧完整。一个中断可能只收到半帧也可能一次收到两帧只有CR才是可靠的帧边界。3.2 拼帧与CRC校验实现private string CalculateChecksum(string frameWithoutCr) { int sum 0; byte[] bytes Encoding.ASCII.GetBytes(frameWithoutCr); foreach (byte b in bytes) { sum b; // 逐字符累加ASCII码 } return (sum 0xFF).ToString(X2); // 取低8位转大写十六进制 } private string BuildFrame(string body) { string frame % _stationNo body; string crc CalculateChecksum(frame); return frame crc \r; }BuildFrame的入参是命令码开始的正文比如RCSY00000A。站号外置成字段而不是写死在每条命令里后续调整单元号只改构造函数一处。ToString(X2)保证校验值输出为大写字母松下PLC对十六进制大小写敏感这一行不能省成x2。3.3 收发互斥与同步等待上位机串口通讯是典型的一问一答模型界面按钮点击、定时器轮询都会触发串口读写。如果多个线程同时写PLC端收到的帧会交错响应也对不上号所以Transact方法必须用锁串行化public string Transact(string body) { lock (_sync) { _signal.Reset(); _buffer.Clear(); _port.Write(BuildFrame(body)); if (_signal.Wait(2000)) { return ParseResponse(_response); } throw new TimeoutException(PLC响应超时检查串口参数和站号); } }ManualResetEventSlim在这里的任务是连接异步接收和同步等待DataReceived事件把完整响应帧填进_response后SetTransact在锁内等待信号超时定2秒。9600波特率下一帧几十字节最多十几毫秒传完2秒足够超过2秒基本可以断定参数不对或线没通。等锁期间其它线程的Transact请求排队不会把同一帧响应分给两条命令。接收侧代码private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { while (_port.BytesToRead 0) { _buffer.Append((char)_port.ReadByte()); } if (_buffer.Length 3 _buffer[_buffer.Length - 1] \r) { _response _buffer.ToString(); _signal.Set(); } }这个实现默认每次只等一帧收到CR就Set交给上层处理。若PLC固件开启了主动上报功能上位机会在非请求状态下收到数据帧那时需要在事件里按CR拆帧把完整帧逐条入队处理不能直接用覆盖式_response单帧变量。3.4 响应拆帧与错误码识别private string ParseResponse(string response) { if (!response.StartsWith(%)) { throw new InvalidDataException(响应不以%开头帧头异常); } string data response.Substring(5, response.Length - 8); if (data.Length 2 int.TryParse(data, out int errorCode)) { throw new PlcException(PLC错误码: errorCode); } return data; }数据区起点的5是这样来的%占1位、站号占2位、命令码占2位末尾CRC占2位、CR占1位所以总长度减8就是数据区字符数。读响应数据区长度为0时Substring(5, 0)会返回空字符串逻辑成立。错误码的判断规则是数据区恰好两位纯数字这个启发式在绝大多数工况下可靠因为正常的触点/寄存器读回值都是十六进制字母开头。常见错误码对照错误码含义排查方向21累加校验错误检查%是否参与计算、CR是否被计入22帧格式错误检查地址位数、数量十六进制、命令码大小写23长度错误核对批量读写的数量和写入值个数24数据编号错误地址超软元件范围对照机型寄存器表接收缓冲里的字符串用Encoding.ASCII处理不用UTF-8。Mewtocol全帧都在ASCII可见字符与控制符范围内UTF-8解码虽然不会出错但ASCII解码能让人更直观地看到帧在哪个字符位置断掉。4. Form1.cs业务层触点读写与运行状态获取4.1 软元件类型与地址映射松下PLC的软元件在Mewtocol协议里用单个字母或双字母标识上位机侧的地址框填什么取决于工艺点位表怎么定义。常用的类型对应关系助记符含义Mewtocol地址写法位/字X外部输入X0000位Y外部输出Y0000位R内部继电器R0000位DT数据寄存器DT0000字16位WR工作寄存器WR0000字16位地址虽然在协议里固定四位但实际PLC软元件个数有限比如FP-X的Y输出一般只到Y2F附近。上位机把地址写超了不会报错而是读到0或写不进去这种假正常比显式错误码更难排查所以地址范围校验要在界面层提前做一道。4.2 写单个触点外部输出与内部继电器写触点命令码是WCS数据区只需要1位十六进制字符1接通0断开。以下方法同时覆盖外部输出Y和内部继电器R的写入public void WriteSingleContact(string contactType, string startAddress, bool value) { if (contactType ! Y contactType ! R) { throw new ArgumentException(写操作一般只针对Y外部输出或R内部继电器); } string body $WCS{contactType}{startAddress}{(value ? 1 : 0)}; _comm.Transact(body); statusLabel.Text ${DateTime.Now:HH:mm:ss} 写{contactType}{startAddress} - {(value ? ON : OFF)}; }触点地址必须固定四位用户输入Y0时上层要统一补充成Y0000再拼帧PLC侧不负责容错。value用三元表达式输出1或0不要用bool.ToString()因为C#输出的True/False直接拼进帧会让CRC位置整体错位。这样写Y000F输出通断实际发送帧是%01WCSY000F1加CRC加CR。批量写多个外部输出时数据区按位打包起始触点对应二进制最低位4个触点合成一个十六进制字符。例如要置位Y0和Y2Y3Y2Y1Y0在二进制里是0101数据区就是5命令主体拼出来为WCSY0000045。批量写把逐点循环从N次串口往返降成一次适合换型时同步复位一组气缸或夹具的输出。4.3 读多触点十六进制到bit数组的转换public int ReadContacts(string contactType, string startAddress, int count) { string countHex count.ToString(X2); // 转两位十六进制 string body $RCS{contactType}{startAddress}{countHex}; string data _comm.Transact(body); int raw Convert.ToInt32(data, 16); return raw ((1 count) - 1); // 低位掩码截掉高bit }返回值里bit0对应起始地址的触点bit1对应地址加1的触点依次递增。比如读Y0到Y9共10个点返回0x035F时转成二进制从最低位往上读就能还原Y0到Y9的通断状态。(1 count) - 1做低位掩码防止相邻高位的数据混进来。一次读触点数量控制在16以内超过16个点的状态建议拆成多组协议里数量字段只有两位十六进制极限是255但返回值落在int类型里超过16位必然截断实用性也随触点增多而下降。4.4 数据寄存器读写与运行状态获取public string ReadRegisters(string regType, string startAddress, int count) { string body $RCP{regType}{startAddress}{count.ToString(X2)}; return _comm.Transact(body); } public void WriteRegisters(string regType, string startAddress, params ushort[] values) { string data string.Concat(values.Select(v v.ToString(X4))); string body $WCP{regType}{startAddress}{values.Length.ToString(X2)}{data}; _comm.Transact(body); }ReadRegisters返回的是每个寄存器4位十六进制字符串的拼接读两个DT值会得到类似006400C8的结果按每4位切一段再Convert.ToUInt16(hex, 16)还原成整数。PLC侧寄存器是无符号存储-1存成FFFF转成ushort后按业务需要强转short电压、温度这类可能为负的模拟量才不会被解析丢符号。WriteRegisters用params可变参数接收要写入的值数组界面层从文本框解析出ushort数组直接传进来比在业务代码里手工拼十六进制字符串直观很多。运行状态获取用SC命令public string ReadRunStatus() { string data _comm.Transact(SC); // 常见解析数据第一位表示模式0STOP1RUN return data; }不同松下PLC机型对SC返回状态字的位定义不完全一致界面显示上我一般只区分为RUN、STOP、异常三态解析时取数据第一位做粗判。要精确定位到PLC内部错误信息需要对照该机型编程手册的SC响应表逐位核对上位机侧不应该假设所有机型的状态字结构完全一样。5. 批量轮询与UI刷新的双缓冲策略5.1 轮询还是定时器UI卡顿的根因把读操作直接写进System.Windows.Forms.Timer的Tick事件PLC响应稍慢Tick就会排队累积界面只剩假死。常见做法是Timer只负责发起采集信号真正读取放Task.Run后台线程结果通过BeginInvoke切回UI线程private void PollTimer_Tick(object sender, EventArgs e) { if (_busy) return; // 上一轮未完成跳过本轮 _busy true; Task.Run(() { try { string yData _comm.Transact(RCSY00004); // 读Y0-Y3共4点 string dtData _comm.Transact(RCPDT00000A); // 读DT0-DT9共10字 BeginInvoke((Action)(() UpdateUI(yData, dtData))); } catch (Exception ex) { BeginInvoke((Action)(() statusLabel.Text ex.Message)); } finally { _busy false; } }); }_busy标志相当于软件锁存在UI线程和Task线程间共享声明时加volatile更稳。轮询周期放到100ms以上比较合理9600波特率下读10个寄存器和几个触点大约几十毫秒周期小于50ms只会把CPU和串口带宽耗在重复请求上读回来的数据UI也来不及消化。5.2 合并读把多点状态压进同一帧UI刷新慢很多时候是上位机自己造成的例如一次只问一个点。读20个Y触点逐点访问需要20个往返合并读只发一帧RCSY00000014这类命令返回的4位十六进制数据按bit位拆开再映射到界面状态灯上轮询耗时从N次串口往返降为一次往返加位运算。采集20路数字输入加10路模拟量寄存器时可以拆成两帧合并读一帧RCS拿触点状态一帧RCP拿寄存器数值整体耗时与单次命令在一个量级。5.3 断线重连与通讯日志产线调试中串口线松动、PLC断电、串口号被其它软件占用最终都表现为Transact抛超时异常。界面捕获异常后每轮都弹窗会弹到人崩溃简单做法是连续超时3次自动关闭串口状态栏提示通讯中断等用户点击重新连接按钮再重开端口。调试日志方面在Transact成功返回时把发送帧和响应帧原样追加到文本文件源串保留ASCIIHex列记录每个字节的十六进制值。Mewtocol帧里肉眼不可见的CR在Hex列一眼就能看出是否被截断排查干扰帧、粘包、半包问题时这个日志比任何断点调试都来得快。本文还有配套的精品资源点击获取
返回列表