
简介针对C#与三菱Q系列PLC通过MC协议通信的需求这份示例工程面向工业自动化上位机开发和PLC调试人员演示了如何借助以太网读取和写入PLC寄存器数据。压缩包为zip格式共31个文件主要包含cs源码、exe可执行程序、pdb调试符号、resources资源文件、txt文本说明等类型包体仅64KB结构紧凑便于快速定位核心代码和运行效果。工程基于Visual Studio解决方案组织包含窗体界面、程序主入口、属性配置及资源文件编译后生成的exe可直接验证通信效果源码中可看到MC协议通信建立、寄存器数据读取与写入的完整流程并配有基本说明文本方便移植到实际项目中。当前已有6098人浏览学习对初次接触三菱Q系列PLC通信开发的读者具有较强的参考价值无论用于设备数据采集、产线监控还是PLC程序联调都能提供可直接套用的通信样例。 做了几年工控上位机要说最常碰到的需求跑不了“把现场Q系列PLC的数据弄上来”这件事。C#配合三菱Q系列PLC走MC协议通信基本是标配方案。这篇文章就围绕这个主题把报文结构、C#实现、踩坑点都过一遍适合刚接触上位机开发的同行也适合被通信问题卡住的老哥们参考。MC协议MELSEC Communication Protocol是三菱PLC自带的通信协议Q系列以太网模块和CPU内置以太网口都支持。相比OpcServer、MX Component这些方案直接走MC协议的好处是不需要额外装运行时不依赖商业组件一个C#程序集就能搞定部署的时候拷个exe过去就能跑。1. 方案选型与整体思路1.1 为什么直接用MC协议接触过三菱PLC通信的应该知道能选的路不少MX Component、MX Sheet、OPC UA/DA、SLMP即MC协议的新叫法、HTTP网关等。我的实际经验是如果只是做本地局域网的数据采集不搞复杂的历史库路由MC协议是最省事的一条路。首先Q系列PLC自带MC协议支持无需在PLC侧增加任何授权费用其次MC协议走标准TCP/UDPC#的Socket原生就能处理不像OPC还需要纠结DCOM配置、防火墙规则、版本兼容再者MC协议的请求响应结构非常固定出错时通过结束代码基本能直接定位问题调试成本低。当然如果项目要求多客户端并发、跨平台、或需要统一建模OPC UA会更合适但那是另一个topic。单就“C#读写Q系列数据”这个诉求MC协议够了。1.2 通信方式选型TCP还是串口Q系列MC协议分以太网和串口两大类。以太网帧常见的是QnA兼容3E帧串口则是4C帧、2C帧这类。我几乎一律推荐TCP走3E帧。原因很现实串口速率瓶颈明显115200bps下刷100个D寄存器就觉得卡而以太网跑千兆实际工业环境百兆也足够一次读几百点数据毫秒级返回。另外C#里TCP的处理比串口简单没有串口打开失败、USB转串口驱动异常这些幺蛾子。3E帧还分二进制模式和ASCII模式。文本协议适合人看但报文膨胀一倍上位机开发我默认选二进制模式效率高、解析也简单。1.3 通信模型设计MC协议是典型的“请求-响应”模型。上位机发一条请求报文PLC返回一条响应报文。由于TCP是流式传输你必须处理粘包和半包问题。我常用的方案是一个TcpClient维护长连接发送时用MemoryStream构包接收时根据报文头里的“数据长度”字段定长读取。这样逻辑最清晰也不需要引入第三方库。2. MC协议核心概念拆解2.1 帧结构组成QnA兼容3E帧的格式是固定的搞清楚这一段代码就是体力活。字段字节数说明副头部2固定D0 00标识QnA兼容3E帧网络号1通常00PC号1通常FF表示目标PLCIO编号2通常03 FF即CPU模块站号1通常00请求数据长度2从“监视定时器”到请求结束的字节数低字节在前监视定时器2超时时间单位250ms如0x0010表示4秒命令/子命令4例如0401 0000表示批量读取软元件信息若干设备代码、首地址、点数注意“请求数据长度”是低字节在前新手经常栽在这上面。2.2 命令与子命令最常用的两个命令批量读取命令0x0401子命令0x0000批量写入命令0x1401子命令0x0000PLC返回的响应帧中命令和子命令原样返回随后是2字节结束代码0x0000表示正常非零对应具体错误。比如C051表示软元件不存在C055表示起始地址超范围。2.3 设备代码与地址换算Q系列内部软元件不是一个统一地址空间D寄存器、M继电器、X输入、Y输出都有各自的设备代码报文中通过设备代码区分。软元件设备代码地址进制说明DA8十进制最常用32位时两个连续地址WB4十进制链路软元件M90十进制内部继电器L92十进制锁存继电器X9C十六进制输入信号Y94十六进制输出信号BA0十六进制链路继电器F93十进制报警器关键点D、M这类是十进制地址报文里直接填编号的二进制值X、Y是十六进制地址比如X10实际是十进制的16报文里要填0x10。地址字段固定3字节低字节在前。另外点位数量是2字节批量读取字软元件时一次最多别超过约480字960字节数据留点余量一次读太多不仅容易触发PLC限制TCP分包也更难处理。3. C#实现MC通信的完整流程3.1 准备工作与参数确认动手写代码前先在PLC侧确认几个参数否则后面全是坑PLC的IP地址和端口号端口在以太网模块参数里设置常见2000、5000也可自定义通信协议选择TCP、打开方式选择MC协议通信数据代码二进制还是ASCII代码里必须对应如果是写入操作CPU参数里务必勾选“RUN中允许写入”否则运行状态下写会报错3.2 核心通信类实现通信类我用一个简单的结构连接、断开、发送命令、接收响应、解析数据。构建请求报文的代码类似这样public byte[] BuildReadCommand(int startAddress, int pointCount) { using MemoryStream ms new MemoryStream(); ms.WriteByte(0xD0); ms.WriteByte(0x00); // 副头部 ms.WriteByte(0x00); // 网络号 ms.WriteByte(0xFF); // PC号 ms.WriteByte(0x03); ms.WriteByte(0xFF); // IO编号 ms.WriteByte(0x00); // 站号 ms.WriteByte(0x0C); ms.WriteByte(0x00); // 请求数据长度 12 ms.WriteByte(0x10); ms.WriteByte(0x00); // 监视定时器4秒 ms.WriteByte(0x01); ms.WriteByte(0x04); // 命令0401 低字节在前 ms.WriteByte(0x00); ms.WriteByte(0x00); // 子命令0000 ms.WriteByte(0xA8); // 设备代码D寄存器 ms.WriteByte((byte)(startAddress 0xFF)); // 起始地址低字节 ms.WriteByte((byte)((startAddress 8) 0xFF)); ms.WriteByte((byte)((startAddress 16) 0xFF)); ms.WriteByte((byte)(pointCount 0xFF)); // 点数低字节 ms.WriteByte((byte)(pointCount 8)); return ms.ToArray(); }如果读M或X/Y替换设备代码和地址进制即可其他字段不变。发送后用异步ReceiveAsync按“数据长度”字段取完整响应再跳过头部从固定偏移解析数据。3.3 批量读取与写入示例读取D100开始的20个寄存器响应解析代码public float[] ReadD(int start, int count) { byte[] resp SendReceive(BuildReadCommand(start, count)); int offset 9; // 9 副头部2 网络号1 PC号1 IO编号2 站号1 数据长度2 ushort endCode (ushort)(resp[offset] | resp[offset 1] 8); if (endCode ! 0) throw new Exception($PLC错误 0x{endCode:X4}); // 数据区从 offset4 开始命令2 子命令2 结束代码2 int dataStart offset 6; byte[] data new byte[count * 2]; Array.Copy(resp, dataStart, data, 0, data.Length); // 每个字低字节在前转ushort }写操作只是把命令换成0x1401字段后追加待写入的数据每个字2字节低字节在前。注意写入和读取的报文长度不同请求数据长度要重算不能照抄12。4. 循环采集与UI刷新的实战处理4.1 定时采集方案上位机最常见的问题是界面卡顿这锅不全是PLC的多半是采集线程没处理好。Socket的SendReceive操作本身是阻塞的如果你在UI线程里直接同步调用等待PLC响应时界面就死了。正确做法是单独开一个采集循环用async/await CancellationToken做定时读取private async Task PollLoop(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { var data await Task.Run(() plc.ReadD(0, 100)); UpdateUI(data); } catch (Exception ex) { Log(ex); } await Task.Delay(100, ct); } }这个循环在后台Task里跑UI只负责显示。UpdateUI里用BeginInvoke或控件.BeginInvoke把数据切回UI线程避免跨线程访问控件。4.2 UI刷新不要实时怼还有个经常被忽略的点数据更新频率不用和采集频率一样。采集可以100ms一次但UI没必要每100ms刷一次否则图表控件列表控件很容易成为瓶颈。我的习惯是数据采集后放入一个ConcurrentQueue或直接用Volatile变量存最新快照UI用一个300~500ms的Timer定时从快照取数刷新。这样即使采集掉了几帧界面依然流畅视觉上也不会有明显延迟。对于历史曲线这类控件更要注意批量添加数据避免OneByOne地AddPoint实测下来同样数据量性能差好几倍。5. 常见问题与排查经验5.1 连接模式和重连机制TCP长连接在工控环境里必须做断线重连。PLC重启、交换机断电、网线松动都会让Socket变成半开状态。不要指望Socket自己发现断线必须要主动检测。我有两个土办法一是接收数据时设置ReceiveTimeout超时说明链路可能挂了二是定期发送一个监视定时器比较短的小批量读取命令当心跳连续几次没响应就重连。重连时不要频繁尝试建议退避式重试比如间隔1秒、2秒、5秒递增直到恢复。5.2 报文格式与字节序检查清单很多C051、C055这类错误其实不是PLC真出错而是报文构造不对。我最常查的几个位置排查点典型错误数据长度字段忘了低字节在前或没包含监视定时器设备代码D写成0x00A8却在地址区填了十六进制X/Y地址应该按十六进制填D/M按十进制填IO编号使用了PLC侧连接编号而非03FF缓冲区读取第9个字节就结束代码解析偏移不对5.3 运行效率与稳定性经验分享几个项目里沉淀的细节TCPClient的NoDelay务必设为true否则小报文会被延迟打包产生毫秒级延迟对高频读写有影响一次多读点比循环单点读效率高得多。需要同时读D100、D200、D300时不要发三条命令要么把地址合并要么用“多个点数连续读”的方式缩小请求次数。Q系列对请求间隔有处理时间少一次请求就少一段等待写入时注意数据类型32位浮点/整数的字节序是两个连续16位字的低字在前读出来按uint拼接再转float请求报文的监视定时器不要设太长我一般用0x00104秒。设长了一旦PLC卡死PLC侧不会响应上位机要等到超时才报错PLC端程序扫描周期很短时连续读请求间隙过小偶尔会出现C1050这类暂时性错误适当加个5ms间隔就能缓解按这套思路做下来C#和三菱Q系列PLC的MC通信基本是稳的。我这些方法都是基于最常见的QnA兼容3E帧TCP方案如果你的项目里用的是L系列或者iQ-R帧结构会有些差异但排查思路和代码骨架是通用的。遇到通信问题不要急着改代码拿个TCP调试工具把报文拉出来对着手册逐字节检查往往比瞎改更快定位问题。本文还有配套的精品资源点击获取