☰
C#实现三菱Q系列PLC MC协议通讯实战
2026/10/27 7:07:59 网站建设 项目流程

简介:本资源是一套面向工业自动化开发者的C#与三菱Q系列PLC串口通信实战源码包,聚焦MODBUS RTU协议下的寄存器读写、连接管理与异常处理,适用于具备基础C#和PLC概念的中级开发者快速构建稳定通讯程序。压缩包共66个文件,含12个核心.cs源码文件(如FrmMain.cs、Program.cs)、6个可执行exe及配套dll库、4个PNG界面截图(含PLC设置与断开配置图)、2个Visual Studio解决方案文件(.sln/.csproj)及调试所需pdb、resx等工程文件,整体4.14MB,结构完整,开箱即用。已有1895人学习下载,资源直接呈现典型工业场景下的通讯流程:从串口参数配置、MODBUS帧构造、X/Y/M/D寄存器批量读取,到线程安全异步操作与超时重试机制实现,代码注释清晰,配合预览中的PLC设置图与主窗体设计,便于理解硬件对接逻辑与软件分层结构。

1. C#与三菱Q系列PLC通讯:不是“连上就行”,而是让上位机真正听懂Q系列的“工业语法”

你写完C#代码,TcpClient.Connect()返回true,心跳包也发出去了——但读不到D100的值,写入M200后PLC逻辑没响应,监控软件里寄存器状态灰着不动。这不是网络通了就万事大吉,而是你还没摸清Q系列PLC的通讯协议底层逻辑:它不认HTTP、不讲JSON、不走通用Socket裸传,它只认MC协议(即MELSEC Communication Protocol),一种带固定帧头、校验码、目标站号、网络号、节点号、请求类型、数据长度、数据体的二进制报文结构。很多C#开发者卡在第一步——以为用Socket发个字节数组就能读D区,结果PLC静默丢包;或误用FX系列的ADRC指令去连Q,直接触发QCPU的“非法协议”错误中断。本篇不讲抽象概念,只拆解真实产线中跑通Q系列通讯的最小可行路径:从协议选型(为什么不用OPC UA?)、MC协议帧构造(含QnA兼容模式)、C#二进制序列化实操、超时与重试机制设计,到现场最常踩的3个“PLC不回包”黑盒陷阱。适合正在做Q系列上位机、SCADA对接、MES数据采集的C#工程师,尤其当你手头只有Q03UDVCPU、Q06H、Q13UDHCPU这类主流型号,且PLC侧已配置好以太网模块(如QJ71E71-100)和GX Works2网络参数。


2. 为什么必须用MC协议:Q系列通讯的协议选型真相与OPC UA的现实边界

2.1 Q系列原生通讯协议栈:MC协议才是“亲儿子”,OPC UA是“编外员工”

三菱Q系列PLC的以太网通讯能力分三层:

  • 底层硬件层:QJ71E71-100等以太网模块提供TCP/IP物理通道,但它本身不解析应用层协议;
  • 中间协议层:MC协议(MELSEC Communication Protocol)由QCPU固件原生支持,无需额外授权、无需PLC侧编程,只要设置好站号/网络号/节点号,即可通过TCP 5001端口(默认)收发二进制指令;
  • 上层扩展层:OPC UA需额外安装Q系列专用OPC UA Server(如MX OPC UA Server for Q系列),且仅支持Q13UDHCPU及以上高端型号,Q03UDVCPU等主流型号不支持,强行启用会触发CPU报警(Error Code: 0x004F)。

提示:热词“process simulate-通过opcua与西门子plc进行通讯”在Q系列场景下是误导性参考——西门子S7-1200/1500原生支持OPC UA,而Q系列需额外软硬件授权,成本高、兼容性差。产线实际选型中,90%以上的Q系列上位机项目仍采用MC协议。

2.2 MC协议两大模式:QnA兼容模式 vs. 3E帧格式,选错直接通讯失败

MC协议在Q系列中有两种帧格式,必须严格匹配PLC侧设置:

  • QnA兼容模式(旧但稳):帧结构为0x50 0x00 + 网络号 + 节点号 + 站号 + 请求类型 + 数据长度 + 数据体 + 校验,适用于所有Q系列CPU(包括Q02H/Q06H),PLC侧需在GX Works2中设置“通信方式:QnA兼容”。这是C#开发首选,因协议简单、文档公开、容错率高;
  • 3E帧格式(新但坑多):帧头为0x50 0x00 0x00 0x00,支持更大数据块读写,但要求PLC固件版本≥1.28,且Q03UDVCPU等低端型号不支持。若PLC侧设为3E模式而C#代码按QnA发包,PLC直接丢弃,无任何错误反馈。

验证方法:用GX Works2的“在线监视→以太网通信监视”功能,抓取PLC收到的原始报文,看前4字节是否为0x50 0x00 + 网络号 + 节点号(QnA)或0x50 0x00 0x00 0x00(3E)。

2.3 为什么不用Modbus TCP?Q系列Modbus支持是“阉割版”

部分开发者试图用Modbus TCP连接Q系列,理由是“通用、C#库多”。但Q系列Modbus TCP仅支持只读型寄存器映射(如D区映射到Modbus Holding Register),且需在PLC侧手动配置映射表(GX Works2 → 参数 → Modbus TCP设置),一旦PLC程序修改D区地址,映射表必须同步更新,否则读到脏数据。更致命的是:Modbus无法访问Q系列特有资源,如锁存继电器(L)、链接寄存器(W)、智能功能模块缓冲存储器(BFM),而这些正是运动控制、温度采集等场景的核心数据源。MC协议则原生支持所有Q系列地址空间,无需映射。


3. C#实现MC协议通讯:从Socket封装到二进制帧构造的完整链路

3.1 基础Socket连接与超时控制:别让PLC等你30秒

Q系列MC协议要求TCP连接建立后立即发送有效帧,若空闲超时(默认10秒),PLC会主动断开连接。C#原生TcpClient未提供细粒度超时控制,必须用Socket底层API:

public class QPlcConnection { private Socket _socket; private const int ConnectionTimeoutMs = 3000; // 连接超时3秒,避免产线等待 private const int SendTimeoutMs = 1000; // 发送超时1秒 private const int ReceiveTimeoutMs = 2000; // 接收超时2秒(Q系列响应通常<500ms) public bool Connect(string ip, int port = 5001) { try { _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, SendTimeoutMs); _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, ReceiveTimeoutMs); var endPoint = new IPEndPoint(IPAddress.Parse(ip), port); var asyncResult = _socket.BeginConnect(endPoint, null, null); if (!asyncResult.AsyncWaitHandle.WaitOne(ConnectionTimeoutMs)) { _socket.Close(); return false; } _socket.EndConnect(asyncResult); return true; } catch { return false; } } }

逻辑说明:BeginConnect配合WaitOne实现非阻塞连接超时,避免主线程卡死;SendTimeout和ReceiveTimeout设为毫秒级,因Q系列PLC响应极快(典型读D区耗时8~15ms),过长超时会导致上位机假死。

3.2 QnA兼容模式帧构造:C#二进制序列化的硬核写法

MC协议QnA帧结构(以读D100开始的2个字为例):

字段长度值(十六进制)说明
Header2字节0x50 0x00固定帧头
Network Number1字节0x00网络号(通常0)
Node Number1字节0xFF节点号(广播用0xFF,单播填PLC节点号)
Station Number1字节0x00站号(通常0)
Request Type2字节0x00 0x00读请求(0x0000)
Data Length2字节0x00 0x0C后续数据长度(12字节)
Device Code2字节0x01 0x00D区代码(0x0100)
Device Address4字节0x00 0x00 0x00 0x64D100地址(小端序:0x64=100)
Data Count2字节0x00 0x02读2个字(4字节)
Reserved2字节0x00 0x00保留字段

C#构造代码(使用Span<byte>避免GC压力):

public byte[] BuildReadFrame(string deviceCode, uint address, ushort count) { // 设备代码映射:D区=0x0100, M区=0x00A8, X区=0x009C, Y区=0x009D var codeBytes = BitConverter.GetBytes((ushort)GetDeviceCode(deviceCode)); if (BitConverter.IsLittleEndian) Array.Reverse(codeBytes); // 转大端序 var addrBytes = BitConverter.GetBytes(address); if (BitConverter.IsLittleEndian) Array.Reverse(addrBytes); // 地址需大端序 var countBytes = BitConverter.GetBytes(count); if (BitConverter.IsLittleEndian) Array.Reverse(countBytes); var frame = new byte[22]; // QnA读帧固定22字节 frame[0] = 0x50; frame[1] = 0x00; // Header frame[2] = 0x00; // Network Number frame[3] = 0xFF; // Node Number (单播时改为PLC节点号) frame[4] = 0x00; // Station Number frame[5] = 0x00; frame[6] = 0x00; // Request Type: Read frame[7] = 0x00; frame[8] = 0x0C; // Data Length: 12 frame[9] = codeBytes[0]; frame[10] = codeBytes[1]; // Device Code frame[11] = addrBytes[0]; frame[12] = addrBytes[1]; frame[13] = addrBytes[2]; frame[14] = addrBytes[3]; // Device Address frame[15] = countBytes[0]; frame[16] = countBytes[1]; // Data Count frame[17] = 0x00; frame[18] = 0x00; // Reserved frame[19] = 0x00; frame[20] = 0x00; // Reserved frame[21] = CalculateChecksum(frame, 0, 21); // 校验码(异或校验) return frame; } private byte CalculateChecksum(byte[] data, int start, int length) { byte sum = 0; for (int i = start; i < start + length; i++) sum ^= data[i]; return sum; }

参数说明:deviceCode传"D"、"M"、"X"等字符串,address为十进制地址(D100传100),count为字数(非字节数);校验码为从帧头到倒数第二字节的异或值,Q系列PLC严格校验此字节,错则丢包。

3.3 解析PLC响应帧:从原始字节流到C#强类型数据

Q系列响应帧结构(读D100成功):

字段长度值说明
Header2字节0x50 0x00同请求帧
Network Number1字节0x00同请求
Node Number1字节0xFF同请求
Station Number1字节0x00同请求
Response Type2字节0x00 0x00成功响应(0x0000)
Data Length2字节0x00 0x08后续数据长度(8字节:2字×4字节)
Error Code2字节0x00 0x00错误码(0x0000=成功)
Data Body可变0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00实际读取的2个字(小端序)

解析代码:

public ushort[] ParseReadResponse(byte[] response, ushort expectedCount) { if (response.Length < 14) throw new InvalidOperationException("响应帧过短"); // 检查错误码(偏移10~11字节) var errorCode = BitConverter.ToUInt16(response, 10); if (errorCode != 0) throw new PlcException($"PLC错误码: 0x{errorCode:X4}", errorCode); // 数据体起始位置:Header(2)+Net(1)+Node(1)+Station(1)+RespType(2)+DataLen(2)+ErrorCode(2) = 11字节 var dataStart = 11; var dataLength = response.Length - dataStart; if (dataLength < expectedCount * 2) throw new InvalidOperationException("响应数据长度不足"); var values = new ushort[expectedCount]; for (int i = 0; i < expectedCount; i++) { // Q系列返回字为小端序,直接读取 values[i] = BitConverter.ToUInt16(response, dataStart + i * 2); } return values; }

注意:Q系列MC协议返回的字(Word)为小端序,与C#BitConverter默认一致,无需翻转;但双字(DWord)需注意字节序,读D区时用uint解析需BitConverter.ToUInt32(response, offset)并确认PLC侧D区数据格式。


4. Q系列PLC之间通讯:用MC协议实现QCPU到QCPU的跨站数据交换

4.1 QCPU间通讯的本质:不是“PLC对PLC”,而是“主站PLC作为客户端访问从站PLC”

Q系列PLC之间通讯并非直接互联,而是通过以太网模块+MC协议实现:一台QCPU(主站)运行用户程序,用MC协议向另一台QCPU(从站)的以太网模块(如QJ71E71-100)发起读写请求。这要求从站PLC必须启用“远程RUN”功能,并在GX Works2中设置“以太网通信设定→允许远程操作”。

关键配置步骤(从站PLC侧):

  1. 在GX Works2中打开“参数→PLC参数→以太网通信设定”;
  2. 勾选“允许远程RUN/STOP”和“允许远程复位”;
  3. 设置“允许访问的IP地址范围”(如192.168.1.0/24);
  4. 确保“网络号”、“节点号”、“站号”与主站C#代码中填写的值一致。

提示:“三菱q系列PLC之间通讯”常被误解为“两台PLC自动同步”,实际是主站PLC周期性轮询从站PLC的D区,类似上位机逻辑。若需实时性,建议用QJ72LP25-25光纤环网模块构建专用高速环网。

4.2 主站PLC调用MC协议:用GX Works2梯形图实现“PLC版C#”

当C#上位机不可用时(如嵌入式场景),主站QCPU可通过梯形图调用MC协议指令:

  • 使用MC指令(MELSEC Communication):指定目标站号、网络号、节点号、设备代码、地址、数据长度;
  • 示例:MC D100 K100 K2 D200表示从站D100读2个字存入本地D200;
  • 注意:MC指令需配合M8038(通讯等待时间)和M8039(通讯错误标志)做状态监控。

C#代码与PLC梯形图调用的区别:

维度C#上位机主站QCPU梯形图
开发效率高(C#调试快)低(需下载PLC程序)
实时性~15ms(TCP延迟)~5ms(内部总线)
灵活性可动态改地址/数量地址/数量需编译时固定
故障定位C#日志可记录详细错误码依赖M8039和PLC诊断缓冲区

4.3 跨站通讯的地址映射陷阱:为什么读到的D区总是0?

现象:主站PLC用MC指令读从站D100,返回值全0。
原因:从站PLC的D区地址未在“参数→PLC参数→以太网通信设定→允许访问的软元件范围”中启用。默认Q系列只开放D0-D1023供远程访问,超出范围的地址(如D10000)需手动添加。
解决:在GX Works2中,进入“参数→PLC参数→以太网通信设定→允许访问的软元件范围”,添加D10000到D19999区间,并下载参数。


5. 避坑指南:Q系列MC协议通讯的3个血泪经验与排查清单

5.1 现象:Socket.Connect()成功,但Send()后Receive()永远阻塞,超时返回0字节

原因:PLC侧以太网模块IP地址与C#代码IP不匹配,或PLC防火墙拦截(QJ71E71-100的“安全设置”未关闭)。
排查:

  • 用ping确认网络连通;
  • 用telnet 192.168.1.10 5001测试端口是否开放(若不通,检查PLC以太网模块LED状态:LINK灯亮但COLL灯闪表示冲突,需换网线);
  • 在GX Works2中打开“在线→以太网通信监视”,看PLC是否收到报文(若无记录,说明报文未到达PLC);
    解决:关闭QJ71E71-100的“安全设置”(参数→以太网通信设定→安全设置→设为“无效”)。

5.2 现象:Receive()返回数据,但解析出错码0x004F(非法协议)

原因:C#代码按QnA模式发包,但PLC侧设为3E帧格式,或反之。
排查:

  • 抓包工具(Wireshark)过滤tcp.port==5001,看C#发出的帧头是50 00还是50 00 00 00;
  • 对照GX Works2中“参数→以太网通信设定→通信方式”确认PLC当前模式;
    解决:统一模式——产线推荐全部设为“QnA兼容”,代码用3.2节帧构造。

5.3 现象:读D区正常,但写M200后PLC逻辑无反应

原因:Q系列M区分为“普通M”和“锁存M”,C#写入的是普通M(断电清零),而PLC程序中用的是锁存M(M1000以上),或PLC程序中M200被其他逻辑强制复位。
排查:

  • 在GX Works2中“在线监视→软元件测试”,手动写M200看PLC响应;
  • 检查PLC程序中M200的触点是否被RST指令复位;
    解决:写入前先用MC指令或C#读取M200当前值,确认地址有效性;若需锁存功能,改用L区(锁存继电器)。

6. 工业级健壮性增强:重试机制、连接池与PLC状态自检的落地技巧

6.1 智能重试策略:不是简单for循环,而是基于错误码的分级重试

Q系列MC协议错误码蕴含丰富信息,应差异化处理:

错误码(Hex)含义重试策略说明
0x0000成功—无需重试
0x0001目标设备不存在立即终止检查PLC站号/节点号配置
0x0002设备忙休眠100ms后重试(最多3次)PLC正在执行其他通讯
0x0003地址范围错误终止并告警D区地址超出PLC内存容量
0x004F非法协议终止并切换协议模式触发QnA/3E模式自动检测

C#实现:

public async Task<ushort[]> ReadDRegisterAsync(string ip, uint address, ushort count, int maxRetry = 3) { for (int i = 0; i <= maxRetry; i++) { try { if (!_connection.IsConnected && !await _connection.ReconnectAsync(ip)) throw new PlcConnectionException("重连失败"); var frame = BuildReadFrame("D", address, count); await _connection.SendAsync(frame); var response = await _connection.ReceiveAsync(); var result = ParseReadResponse(response, count); var errorCode = BitConverter.ToUInt16(response, 10); switch (errorCode) { case 0x0000: return result; // 成功 case 0x0002: // 设备忙 if (i < maxRetry) await Task.Delay(100); else throw new PlcBusyException(); break; case 0x0001: case 0x004F: throw new PlcConfigurationException($"PLC配置错误: 0x{errorCode:X4}"); default: throw new PlcException($"未知错误: 0x{errorCode:X4}"); } } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionReset) { // PLC主动断开,需重连 await _connection.DisconnectAsync(); if (i == maxRetry) throw; } } throw new InvalidOperationException("重试耗尽"); }

6.2 PLC连接池:避免频繁建连导致PLC资源耗尽

Q系列以太网模块支持的并发连接数有限(QJ71E71-100为8个),C#每读一次都new Socket()会快速占满连接数。应实现连接池:

public class QPlcConnectionPool { private readonly ConcurrentBag<QPlcConnection> _pool = new(); private readonly SemaphoreSlim _semaphore = new(8, 8); // 最大8连接 private readonly string _ip; public QPlcConnectionPool(string ip) => _ip = ip; public async Task<QPlcConnection> GetConnectionAsync() { await _semaphore.WaitAsync(); var conn = _pool.TryTake(out var existing) ? existing : new QPlcConnection(); if (!conn.IsConnected && !conn.Connect(_ip)) throw new PlcConnectionException("获取连接失败"); return conn; } public void ReturnConnection(QPlcConnection conn) { if (conn.IsConnected) _pool.Add(conn); _semaphore.Release(); } }

关键点:SemaphoreSlim限制最大连接数,ConcurrentBag无锁存连接对象,避免Socket频繁创建销毁的GC压力。

6.3 PLC状态自检:用MC协议读取CPU运行状态,替代ping的“假阳性”

ping只能确认网络层通,无法判断PLC是否RUN。Q系列提供特殊地址读取CPU状态:

  • 读D8000:返回0x0000=STOP,0x0001=RUN,0x0002=PAUSE;
  • 读D8001:返回错误码(0x0000=无错);
  • 读D8002:返回扫描周期(ms);

C#封装:

public async Task<PlcStatus> GetPlcStatusAsync() { var status = await ReadDRegisterAsync("D8000", 1); var error = await ReadDRegisterAsync("D8001", 1); var cycle = await ReadDRegisterAsync("D8002", 1); return new PlcStatus { IsRunning = status[0] == 1, ErrorCode = error[0], ScanCycleMs = cycle[0] }; } public struct PlcStatus { public bool IsRunning; public ushort ErrorCode; public ushort ScanCycleMs; }

我的习惯:在上位机启动时必调用GetPlcStatusAsync(),若IsRunning==false,立即弹窗提示“PLC未RUN,请检查”,而不是让用户等到数据刷新失败才察觉。这比等30秒超时再报错,体验好太多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询