ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#上位机开发:三菱PLC MC协议3E帧调试实战

C#上位机开发:三菱PLC MC协议3E帧调试实战 简介SanLingMC是一套基于C#开发的三菱PLC调试示例程序专注于MC协议单地址读取与写入适合需要入门PLC通信、理解上位机与三菱设备之间报文交互的开发者。程序以Visual Studio工程形式组织包含窗体界面、资源文件、程序集信息与可执行程序读者可直接打开工程逐行查看代码掌握Socket连接建立、请求命令帧构建、响应数据解析及异常处理等关键环节。PLC调试过程中常见的寄存器地址映射、位元件与字元件读写规则也能在这个最小可运行示例中对照验证。资源压缩包共40个文件涵盖cs源码、resx资源、exe可执行文件、pdb调试符号、settings配置、licenses许可文件等类型整体仅113KB结构紧凑且易读。目前已有1112人学习下载附带Release与Debug编译产物可边运行边观察输出结果。通过这份示例开发者既能看到C#网络编程在工业通信中的落地方式又能以MC协议为基础向批量读取、异步轮询、异常重试等真实项目能力延伸。1. 用C#写三菱PLC调试程序MC协议绕不开的3个事实做C#上位机绕不开一个场景对面设备是三菱PLCFX5U、Q系列、L系列网口都支持MC协议。很多工程师第一次写调试程序时会被三菱MC协议的报文格式卡住尤其是3E帧的二进制模式。网上能搜到SanLingMC这类C#项目命名通常是“三菱MC”但代码质量参差直接抄过来往往在地址转换、响应解析和超时处理上踩坑。这也解释了为什么搜三菱PLC编程教学的人很多能一次跑通读写的人却不多。三菱MC协议和Modbus TCP有个本质差别Modbus有现成的NModbus4这类库寄存器模型单一MC协议则是三菱各系列PLC共用一套帧格式命令码和软元件代码非常多。掌握3E帧的头部、命令、地址封装调试程序就能脱离具体PLC型号通用。下文不依赖某一份现成源码从MC协议帧结构开始用C#把读写三菱PLC的调试程序完整实现。适合已经会用TcpClient但没碰过三菱协议的工程师也适合想把手里的Modbus上位机迁移到MC协议的开发者。2. MC协议3E帧结构拆解与C#报文封装实现2.1 二进制帧与ASCII帧的选型头部字段逐字节说明三菱MC协议在以太网上最常见的形态是QnA兼容3E帧。3E帧有二进制和ASCII两种编码上位机里优先选二进制原因很直接同样一帧读请求二进制比ASCII少一半字节解析响应时不用做十六进制字符到数值的转换CPU开销小。FX5U、Q系列、L系列都支持PLC侧在以太网参数里把帧类型选成“3E帧二进制”即可默认端口按PLC型号不同常见在2000到5000之间具体在GX Works的模块参数里能看到。3E帧二进制头部固定占用9个字节后面跟着监视定时器和命令数据。头部有一个著名的字节序陷阱子头D0 00和IO号03 FF在报文里保持原样不做大小端交换但请求数据长度、监视定时器、软元件地址、点数全是小端序低字节在前。很多第一版调试程序连不上PLC问题就出在把D0 00写成了00 D0。字段字节数典型值说明子头2D0 00二进制3E帧固定不交换字节序网络号100与PLC网络号对应单机写00PC号1FF上位机编号默认FFIO号203 FFCPU模块IO编号固定03FF站号100多CPU系统时区分监视定时器字段的用途是防止连接长期不活动单位是250ms0x0010等于4秒。上位机请求发出后PLC会在定时器之内返回如果超过定时器没收到新请求PLC侧会主动断开。调试程序里给0x0010比较稳妥批量写文件或写大数组时不会因为指令间隔太长被断开也避免了异常连接长时间占用PLC资源。2.2 用C#构建批量读取请求帧有了头部和字段分工C#里构建批量读取D寄存器的报文就是纯字节操作。下面方法从D100开始连续读取16个字public static byte[] BuildReadD(int start, int count) { var buf new Listbyte(21); buf.AddRange(new byte[] { 0xD0, 0x00, 0x00, 0xFF, 0x03, 0xFF, 0x00 }); int lenPos 7; // 请求数据长度字段的起始下标 buf.AddRange(new byte[] { 0x00, 0x00 }); // 长度占位稍后回填 buf.AddRange(new byte[] { 0x10, 0x00 }); // 监视定时器 4秒 buf.AddRange(new byte[] { 0x01, 0x04 }); // 0x0401 批量读取 buf.AddRange(new byte[] { 0x00, 0x00 }); // 子命令 0x0000 字单元 buf.Add((byte)(start 0xFF)); // 起始地址低字节 buf.Add((byte)((start 8) 0xFF)); // 起始地址高字节 buf.Add(0x00); // 位号字单元固定0 buf.Add(0xA8); // 软元件代码D buf.Add((byte)(count 0xFF)); // 点数低字节 buf.Add((byte)((count 8) 0xFF)); // 点数高字节 int len buf.Count - lenPos - 2; // 长度监视定时器到数据末尾 buf[lenPos] (byte)(len 0xFF); buf[lenPos 1] (byte)((len 8) 0xFF); return buf.ToArray(); }逻辑说明前7个字节是固定头部第7、8字节是请求数据长度长度值从监视定时器开始算到命令数据结束为止所以用总字节数减去头部7字节再减去长度字段2字节。命令0x0401是批量读取子命令0x0000表示按字单元读取。三菱D寄存器编号在报文里按十六进制放D100就是0x0064小端序写入64 00。参数说明start是十进制软元件号方法内部会自动截成低字节和高字节count是读取点数D区连续读256个字没问题批量读取上限通常是960字超过会返回结束码错误。软元件代码0xA8对应数据寄存器D如果读取R文件寄存器把0xA8换成0xAF起始地址的位号字节保持0x00。2.2.1 读取位软元件时的差异读M、X、Y这类位软元件时命令还是0x0401但子命令要改成0x0001起始地址第三个字节变成位号。M100的报文地址按64 00 00发送前两字节是十六进制软元件号第三字节是位号0X和Y的软元件号是八进制X7实际编号是7X10实际编号是8转换时先做八进制到十进制的换算再按十六进制放入报文。2.3 软元件代码与常用命令速查写调试程序最常用的软元件可以收敛到下面这张表表格里的代码是二进制帧里的单字节值软元件代码编号进制典型用途DA810进制数据寄存器整数、浮点、字符串都靠它RAF10进制文件寄存器ZRB010进制扩展文件寄存器容量更大M9010进制内部继电器跑流程状态X9C8进制输入继电器接传感器信号Y9D8进制输出继电器控制执行机构SM9110进制特殊继电器PLC状态位SDA910进制特殊数据寄存器系统诊断值WB410进制链接寄存器CC-Link用命令码方面批量读写是调试程序的主干读取命令0x0401写入命令0x1401随机读取0x0403。随机读取可以一次读多个不连续地址适合把散落在不同段的温度、压力、设备状态拉回来请求体里先放点数再放一组“地址3字节软元件代码1字节”的结构。批量写入则把命令0x1401和写入数据拼接在点数后面每个字占2字节小端序。我平时做上位机时喜欢用一个字典把软元件字符串D、M、X映射到代码字节再用一个方法把“Y10”这种带编号的字符串拆成八进制编号和位号。这样调试界面上输入Y10就能直接读不用每次查表对刚接触三菱PLC编程教学的同事更友好。3. SanLingMC式C#上位机三菱PLC读写通道与参数配置3.1 C#上位机的连接通道TcpClient的封装与断线重连项目叫SanLingMC还是plcmc并不重要核心都是三件事连接管理、帧收发、数据解析。连接管理用System.Net.Sockets.TcpClient就够PLC侧没有Modbus TCP那种并发连接数限制同一个端口可以挂多个TCP客户端但调试程序应该只维护一个连接轮询逻辑挂在同一个Socket上避免多个连接并发读写同一个软元件区造成数据竞争。public class McTcpClient { private TcpClient? _client; private NetworkStream? _stream; private readonly object _lock new(); public async Task ConnectAsync(string ip, int port) { _client new TcpClient(); _client.ReceiveTimeout 3000; _client.SendTimeout 3000; await _client.ConnectAsync(ip, port); _stream _client.GetStream(); } public byte[] Transceive(byte[] req) { lock (_lock) { _stream!.Write(req, 0, req.Length); _stream.Flush(); var resp ReadFrame(_stream); return resp; } } }连接方法里把接收和发送超时都设成3秒这个值对本地局域网或单机调试足够如果PLC在远端又经过工业交换机可以放大到5秒。Transceive用lock串行化收发轮询周期短的时候不会出现前一帧响应还没读完、后一帧请求已经发出去的情况。三菱PLC侧对同时到达的多帧请求处理能力有限串行IO是最稳妥的写法。Response帧长度3E帧响应没有在TCP流里自带帧结束标志要靠响应数据长度字段判断读多少字节。读头部9个字节后字段布局和请求一致取出第7、8字节的小端长度值再继续读对应长度拼成完整响应帧。这个拆解逻辑是调试程序里最容易出错的地方不能只按固定字节数去读因为错误码响应比正常响应短。3.2 C#上位机批量读写三菱PLC的通用方法把帧构建和连接管理合起来封装通用读写方法。下面给出读取Word数组和写入Int16数组的完整样例public async Taskushort[] ReadWordsAsync(string device, int start, int count) { byte devCode DeviceCodes[device]; // 从字典取软元件代码 byte[] req BuildReadRequest(device, start, count, devCode); byte[] resp Transceive(req); // 结束码响应第9、10字节大端序0xC05D 这种格式 ushort end (ushort)((resp[9] 8) | resp[10]); if (end ! 0) throw new McProtocolException($读失败 结束码{end:X4}); var result new ushort[count]; for (int i 0; i count; i) result[i] BitConverter.ToUInt16(resp, 11 i * 2); return result; }逻辑说明BuildReadRequest根据软元件类型选择子命令是0x0000还是0x0001DeviceCodes是软元件字符串到单字节代码的映射。响应里从第11字节开始是数据区每个字占2字节小端序。结束码不为0时直接抛异常异常信息里带十六进制结束码后面排查错误码时非常有用。写入方法的帧结构只是把命令换成0x1401并在末尾追加数据区。写32位整数时要注意三菱的数据格式是低位字在前比如要写int值123456到D100需要先写D100低16位再写D101高16位。C#里可以用BitConverter.GetBytes把int拆成4个字节按低字、高字的顺序拼进数据区读出来时反向组合。float类型也是同样的规矩把4字节按低字在前放入两个D寄存器解析时用BitConverter.Int32BitsToSingle恢复。3.3 监视定时器、超时与重试的参数怎么调调试程序跑不稳九成是三个参数没调对重试次数、超时时间和监视定时器。监视定时器在请求帧里上位机设置0x00104秒基本够用超时时间放在TcpClient上设置为监视定时器的1.5到2倍比较合理万一PLC侧没来得及返回上位机不会先放弃。轮询型上位机最常见的错误是让所有读请求共享一个超时时间读写频繁时会误伤写操作建议读、写各用一组超时参数。参数推荐值说明ReceiveTimeout3000 ms局域网调试跨网段放大到5000SendTimeout3000 ms与接收一致避免写卡住Retries3线性退避间隔200ms起监视定时器0x00104秒写入大块数据时保持public async Taskbyte[] TransceiveWithRetryAsync(byte[] req, int retries) { for (int i 0; i retries; i) { try { return Transceive(req); } catch (IOException ex) { if (i retries - 1) throw; await Task.Delay(200 * (i 1)); await ReconnectAsync(); } } return null!; // 不会执行到这里 }重试间隔用200毫秒乘重试次数做线性退避第一次等200ms第二次等400ms。重连前必须关闭旧Socket否则Windows上端口会进入TIME_WAIT状态频繁重连很快会把可用端口耗光。重试次数默认给3次再往上容易在PLC真实停机时让上位机界面卡死不如直接弹报警让操作员介入。4. 三菱PLC调试实战响应解析、错误码与轮询性能调优4.1 响应帧解析把字节还原成int、float和字符串读回来的ushort数组只是原始数据调试程序要显示的是具体物理量。三菱PLC里D寄存器存整数、浮点、字符串的方法是固定的Int16直接一个D字Int32用连续两个D字低位字在前Float也用两个D字按IEEE 754单精度排列字符串按每2字节一个字符存放规则在PLC侧程序里约定上位机按同样规则解析。public static float ReadFloat(ushort[] words, int index) { int raw (words[index 1] 16) | words[index]; // 高字在前 return BitConverter.Int32BitsToSingle(raw); } public static string ReadString(ushort[] words, int index, int charCount) { var bytes new byte[charCount * 2]; for (int i 0; i charCount; i) { bytes[i * 2] (byte)(words[index i] 0xFF); bytes[i * 2 1] (byte)(words[index i] 8); } return Encoding.ASCII.GetString(bytes).Trim(\0); }逻辑说明ReadFloat把两个D字组装成32位整数再按单精度浮点解释。注意组合顺序三菱的存储方式是D100存低16位D101存高16位所以左移的是words[index1]。ReadString按照字内低字节在前的规则展开字节序列再交给Encoding解析实际项目里字符串可能含日文或中文根据PLC侧设定改用Encoding.Unicode。解析时最容易出现的问题是数值漂移比如用ReadWords读回模拟量通道直接把ushort当成十进制显示但PLC侧的模拟量模块可能输出的是带符号的Int16或者做了量程缩放。调试程序里最好保留原始值同时提供换算表达式界面显示工程值时再套系数和偏移量不要污染底层原始数组。4.2 结束码排查遇到C0 5B、C0 5D别急着改代码MC协议响应帧的第9、10字节是结束码结束码为0x0000表示成功。排查顺序先看结束码再检查报文长度最后检查网络。实际调试中C0 5D地址越界和C0 5C软元件不存在配合出现的频率最高原因是软元件号转换出错尤其是X和Y的八进制编号被当成十进制处理。很多工程师对着三菱PLC编程教学视频写了一晚上代码最后发现是X20的八进制转成了十进制32报文里却按十进制20去编址。结束码含义常见原因与处理0x0000正常无需处理0xC051点数超限count超过上限分块读取0xC058请求长度错误构建帧时长度回填算错检查lenPos0xC05B命令/子命令不支持确认PLC型号和帧类型核对命令码0xC05C软元件不存在设备代码错误核对软元件代码表0xC05D地址越界startcount超过范围或八进制未转换0xC060数据长度不一致响应解析偏移量算错结束码还有一个实用技巧把结束码记进日志连续出现3次以上的同一个结束码就把该软元件区标记为故障在调试界面用红色高亮。这样现场排查时不用逐个寄存器点查报警信息已经能定位到具体地址段。错误码日志用十六进制大写记录方便和PLC手册对照。4.3 轮询周期、异步收发与性能调优一套典型的调试程序会同时轮询D区参数、M区状态、SD区诊断三个块如果每块一帧请求1秒周期下CPU压力很小但要想把周期压到100ms以内就要减少帧数。常见做法是把相邻软元件合并成一片读取D区连续的话一次读256个字再在内存里按地址偏移切片比发几十帧小请求快得多。M区也类似连续32位的布尔量可以装进一个字用按位与、按位或去提取状态。TcpClient的收发用同步方法已经能满足500ms周期的调试场景如果做的是数据采集站建议用async/await改写ReadFrame避免PLC响应慢时阻塞UI线程。WinForms或WPF界面里不要直接在事件处理器里做网络同步调用把轮询逻辑放到BackgroundService或ThreadPool里再用Control.BeginInvoke或事件把结果送回到界面线程。这里要提一个容易忽略的细节三菱PLC对连续快速访问有限制扫描周期短的PLC可能还没来得及刷新软元件上位机就会读到旧值。轮询周期不要低于PLC扫描周期的10倍量级FX5U扫描周期一般在1ms到10ms上位机50ms轮询已经很快了。想获得更平滑的曲线宁可在上位机侧做时间戳和插值也不要盲目提高请求频率。5. 用GX Works仿真和帧回环验证C#调试程序5.1 GX Works仿真器跑通全链路调试程序写完不要直接怼着PLC调先开GX Works2或GX Works3的仿真模式。方法是新建工程后点“模拟开始”PLC程序跑在PC里然后给仿真器配置一个虚拟以太网端口通过本机回环地址访问。仿真器支持的MC协议帧和真实PLC基本一致能覆盖大部分协议调试需求。需要注意仿真器的默认端口可能和真实PLC不同需要在仿真设置里找到通信参数把TCP端口固定下来。调试程序把IP和端口做成配置项连仿真器填127.0.0.1和对应端口连真实PLC就改成实际IP不用改代码。这样在家用笔记本上也能做C#上位机的功能开发和界面预览。5.2 帧回环自检代码没有PLC也没有仿真器时可以用一个最简单的回环方法验证帧构建是否正确public static void LoopbackCheck() { byte[] req BuildReadD(100, 16); int len BitConverter.ToUInt16(req, 7); // 请求数据长度应该等于总长度减头部7字节和长度字段2字节 Debug.Assert(len req.Length - 9); // 命令码位置固定在第9、10字节 Debug.Assert(req[9] 0x01 req[10] 0x04); }逻辑说明BuildReadD返回的报文第7、8字节存着请求数据长度校验它等于总长度减9能抓住长度回填的错误。命令码位置固定在第9、10字节0x0104是正确的批量读取命令。这个自检可以挂进单元测试每次改协议层代码后跑一遍避免把报文基础结构改坏。进一步可以做端到端自检写一个简易的TCP Server监听本地端口收到请求帧后按同样的帧格式拼一组响应数据回给客户端。客户端把接收到的响应按第4章的解析方法还原成ushort数组比对预期值。这样在没有真实PLC的环境里也能把协议栈的收发、解析两个方向都验证到。5.3 一个收尾技巧把原始帧写进日志给Transceive方法加一个可选的原始帧日志输出请求和响应的十六进制字节文件按小时滚动。现场排查“上位机显示正常但值不对”的诡异问题时原始帧日志比任何断点都好用因为协议层之外的编码问题全都会在字节序列里留下痕迹。日志里同时打印帧方向、时间戳和结束码不需要等到复现翻日志就能定位是哪一帧命令在什么时间点返回了C0 5D。帧日志不需要把每个轮询周期的帧都记录下来只记录失败帧或结束码非0的响应磁盘占用小排查效率更高。给日志类加一个开关默认开、轮询时静默遇到异常再输出完整帧这套验证方式在项目里帮我省掉过不止一次现场出差。本文还有配套的精品资源点击获取
返回列表