C#原生SerialPort实现三菱PLC串口通信(MC协议)

发布时间:2026/10/15 21:09:27
C#原生SerialPort实现三菱PLC串口通信(MC协议) 简介本资源是一套基于C#实现串口与三菱PLC通信的完整工程实践方案面向工业自动化领域的初中级开发者、自控工程师及高校机电/自动化专业学生解决PLC底层数据交互中单点/批量布尔量、Word/Dword数值读写、心跳监测与多线程稳定通信等典型问题。压缩包共49个文件含12个核心C#源码文件如MitsubshiPLC.cs、SerialPortClass.cs、frmMain.cs等、3个可执行程序exe、3个配置文件config及配套资源文件resx、ico、sln等结构清晰模块职责分明便于理解通信逻辑分层与UI交互设计整体体积仅184KB轻量易部署。已有2155人学习下载资源附带完整VS解决方案DebugClass.sln与调试类库DebugClass20170920涵盖串口参数配置、二进制数据解析、异常重连机制及线程安全读写封装可直接编译运行并快速适配实际产线PLC设备。1. C#串口读写三菱PLC为什么用标准串口协议比“万能DLL”更稳、更可控、更适合产线长期运行你手头有一台FX3U或Q系列三菱PLC现场只有RS-485物理接口没有以太网模块上位机是工控机或嵌入式Windows设备没装专用驱动但必须实时读取20个输入点X0~X19、写入16个输出点Y0~Y15、监控D0~D9共10个字寄存器、每500ms发一次心跳信号防止通信超时——这种场景下别急着找“三菱PLC通讯DLL封装包”或“一键生成上位机工具”它们往往黑匣子深、日志缺、异常难捕、版本一升级就翻车。真正一线产线工程师的共识是用C#原生SerialPort 自定义MC协议帧解析才是对通信链路有完全掌控力的落地路径。它不依赖第三方二进制库可精准控制超时、重试、校验、缓冲区清空时机能直接对接PLC手册里的MC协议格式如QnA兼容3E帧支持单bit、连续bit块、Word/DWord/Float等全类型读写更重要的是心跳机制、断线自动重连、寄存器地址越界防护这些关键逻辑全在你代码里不是藏在DLL某次回调里。本文就带你从零写出一个可稳定跑7×24小时的C#串口PLC通信模块覆盖FX/Q系列主流型号重点讲清MC协议帧构造、地址映射规则、超时陷阱和心跳保活的实操细节。2. 搭建基础通信框架用SerialPort初始化串口并发送标准MC协议读取指令2.1 确认PLC通信参数与MC协议版本选择三菱PLC串口通信需严格匹配硬件设置。FX系列FX1S/3U/5U默认使用MC协议MELSEC Communication Protocol的3E帧格式Q/L系列则多用QnA兼容3E帧。二者核心差异在于首字节命令码和地址编码方式但帧结构一致STX PC号 CPU监视目标模块命令子命令数据长度数据ETXSUM。提示务必在PLC编程软件GX Works2/GX Works3中确认以下三项① PLC串口设置为“MC协议”模式非“Modbus RTU”② 波特率、数据位、停止位、校验位常见为9600,8,N,1③ PC号通常设为0xFF即255与PLC站号默认0x00。若PLC站号非0后续所有指令中的“目标模块”字段需对应修改。2.2 初始化SerialPort并配置关键参数private SerialPort _serialPort; private void InitializeSerialPort(string portName, int baudRate 9600) { _serialPort new SerialPort { PortName portName, BaudRate baudRate, DataBits 8, StopBits StopBits.One, Parity Parity.None, ReadTimeout 1000, // 关键读超时设为1秒避免阻塞主线程 WriteTimeout 500, // 写超时500ms防串口卡死 ReceivedBytesThreshold 1 // 每收到1字节即触发DataReceived事件用于流式解析 }; // 必须启用DTR/RTS否则部分FX PLC无法响应 _serialPort.DtrEnable true; _serialPort.RtsEnable true; _serialPort.DataReceived OnDataReceived; _serialPort.Open(); }ReadTimeout1000是血泪经验PLC响应慢于1秒时Read()会挂起导致UI冻结或定时器失效设为1秒后配合重试逻辑更健壮。DtrEnable/RtsEnabletrue是FX系列PLC的硬性要求缺一不可否则指令发出去PLC根本不回。ReceivedBytesThreshold1避免因PLC分帧返回如先回STX再回数据导致ReadExisting()读不到完整帧。2.3 构造MC协议读取指令以读取X0~X78个输入点为例MC协议中位元件X/Y/M/S读取使用命令码0000H子命令0000H地址格式为0000H 元件类型码 起始地址。X输入点类型码为00H起始地址X0对应0000Hprivate byte[] BuildReadXCommand(int startAddress, int count) { // 地址计算X0 0x0000, X1 0x0001 ... X7 0x0007 ushort address (ushort)startAddress; byte[] addrBytes BitConverter.GetBytes(address); Array.Reverse(addrBytes); // MC协议用大端序 // 帧结构STX(0x05) PC号(0xFF) CPU监视(0x00) 目标模块(0x00) // 命令(0x00,0x00) 子命令(0x00,0x00) 数据长度(0x00,0x02) // 地址类型(0x00) 地址高位 地址低位 ETX(0x03) var frame new Listbyte { 0x05, // STX 0xFF, 0x00, 0x00, // PC号、CPU监视、目标模块 0x00, 0x00, // 命令码 0x00, 0x00, // 子命令 0x00, 0x02, // 数据长度2字节类型码地址 0x00, // X元件类型码 addrBytes[0], addrBytes[1], // 地址高位、低位大端 0x03 // ETX }; // 计算SUM校验STX到ETX所有字节异或 byte sum 0; for (int i 0; i frame.Count - 1; i) // 排除ETX后的SUM位置 sum ^ frame[i]; frame.Add(sum); return frame.ToArray(); } // 调用示例读取X0~X78个点 byte[] cmd BuildReadXCommand(0, 8); _serialPort.Write(cmd, 0, cmd.Length);地址计算逻辑X0地址为0X10为10直接传入整数即可无需手动转BCD或十六进制字符串。数据长度字段此处为0x0002因只传1字节类型码2字节地址若读WordD寄存器长度为0x0003类型码3字节地址。SUM校验必须包含STX到ETX之间所有字节不包括ETX本身这是PLC校验失败的最常见原因。3. 解析PLC响应帧从原始字节数组提取Bool、Word、DWord值3.1 识别完整MC响应帧的边界与校验PLC响应帧以STX(0x05)开头ETX(0x03)结尾末尾紧跟1字节SUM校验。由于串口可能丢包或粘包需在DataReceived事件中做流式缓冲与帧切分private readonly Queuebyte _receiveBuffer new Queuebyte(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { try { int bytesToRead _serialPort.BytesToRead; byte[] buffer new byte[bytesToRead]; _serialPort.Read(buffer, 0, bytesToRead); foreach (byte b in buffer) _receiveBuffer.Enqueue(b); // 尝试从缓冲区提取完整帧 while (_receiveBuffer.Count 4) // 最小帧长STXPCETXSUM 4字节 { if (_receiveBuffer.Peek() 0x05) // 找到STX { var frame ExtractFrameFromBuffer(); if (frame ! null VerifySum(frame)) ProcessResponseFrame(frame); } else { _receiveBuffer.Dequeue(); // 丢弃非STX开头的脏字节 } } } catch (Exception ex) { Console.WriteLine($接收异常: {ex.Message}); } } private byte[] ExtractFrameFromBuffer() { var list _receiveBuffer.ToList(); int stxIndex list.FindIndex(b b 0x05); if (stxIndex -1) return null; int etxIndex list.FindIndex(stxIndex, b b 0x03); if (etxIndex -1 || etxIndex stxIndex) return null; // 帧长度 ETX位置 - STX位置 2含ETX和SUM int frameLength etxIndex - stxIndex 2; if (list.Count stxIndex frameLength) return null; byte[] frame list.Skip(stxIndex).Take(frameLength).ToArray(); // 从缓冲区移除已处理帧 for (int i 0; i frameLength; i) _receiveBuffer.Dequeue(); return frame; }缓冲区设计用Queuebyte而非StringBuilder避免字节与字符编码混淆串口是纯字节流。粘包处理ExtractFrameFromBuffer()确保每次只取一个完整帧即使PLC连续发多个响应也逐帧处理。脏字节丢弃若缓冲区头部不是STX直接Dequeue()丢弃防止因线路干扰导致解析错位。3.2 解析Bool型响应从字节流还原X/Y/M/S状态MC协议返回的位元件数据按字节打包每字节8位低位在前LSB first。例如读X0~X7PLC返回1字节bit0X0, bit1X1...bit7X7public bool[] ParseBoolResponse(byte[] frame, int expectedCount) { // 响应帧结构STX PC 响应码 数据长度 数据... ETX SUM // 数据起始位置索引4STX,PC,响应码,数据长度后 int dataStartIndex 4; int dataLength (frame[2] 8) | frame[3]; // 响应码后2字节为数据长度 int actualDataBytes Math.Min(expectedCount, (dataLength 7) / 8); // 向上取整字节数 bool[] result new bool[expectedCount]; for (int i 0; i expectedCount; i) { int byteIndex i / 8; int bitIndex i % 8; if (byteIndex actualDataBytes dataStartIndex byteIndex frame.Length - 2) // -2排除ETXSUM { byte dataByte frame[dataStartIndex byteIndex]; result[i] (dataByte (1 bitIndex)) ! 0; // 注意bitIndex0对应最低位 } else { result[i] false; // 超出范围默认false } } return result; } // 使用示例读X0~X7后解析 bool[] xStates ParseBoolResponse(receivedFrame, 8); Console.WriteLine($X0{xStates[0]}, X1{xStates[1]});位序陷阱PLC返回的bit0是X0对应字节的bit0最低位不是bit7。1 bitIndex正确提取。长度安全expectedCount由调用方传入ParseBoolResponse内部做越界保护避免数组越界异常。3.3 解析Word/DWord响应处理大端序与字节对齐读D寄存器Word时PLC返回2字节高位在前大端读DWord双字返回4字节同样大端。需用BitConverter并反转字节序public ushort ParseWordResponse(byte[] frame, int wordIndex 0) { int dataStartIndex 4; // Word数据从索引dataStartIndex开始每2字节一个Word int offset dataStartIndex wordIndex * 2; if (offset 2 frame.Length - 2) throw new InvalidOperationException(响应数据不足); byte[] wordBytes { frame[offset 1], frame[offset] }; // 反转为小端供BitConverter return BitConverter.ToUInt16(wordBytes, 0); } public uint ParseDWordResponse(byte[] frame, int dwordIndex 0) { int dataStartIndex 4; int offset dataStartIndex dwordIndex * 4; if (offset 4 frame.Length - 2) throw new InvalidOperationException(响应数据不足); byte[] dwordBytes { frame[offset 3], frame[offset 2], frame[offset 1], frame[offset] }; return BitConverter.ToUInt32(dwordBytes, 0); } // 示例读D0第一个Word ushort d0Value ParseWordResponse(responseFrame, 0); Console.WriteLine($D0 {d0Value});字节序转换PLC发大端C#BitConverter默认小端故需手动反转字节顺序。索引安全wordIndex/dwordIndex支持批量读取时定位第N个值避免硬编码偏移。4. 实现批量读写与心跳保活解决长连接下的超时、断线、地址越界问题4.1 批量读取Bool一次指令读取最多64个X/Y/M/S点MC协议单次读取位元件上限为64点8字节。超过需分帧但批量读能显著降低通信开销public bool[] ReadMultipleBool(string elementType, int startAddress, int count) { // elementType: X, Y, M, S → 对应类型码 0x00,0x01,0x02,0x03 byte typeCode elementType switch { X 0x00, Y 0x01, M 0x02, S 0x03, _ throw new ArgumentException(不支持的元件类型) }; // 构造批量读指令同单点仅地址范围不同 ushort address (ushort)startAddress; byte[] addrBytes BitConverter.GetBytes(address); Array.Reverse(addrBytes); var frame new Listbyte { 0x05, 0xFF, 0x00, 0x00, // STX, PC, CPU监视, 目标模块 0x00, 0x00, // 命令 0x00, 0x00, // 子命令 0x00, 0x02, // 数据长度 typeCode, addrBytes[0], addrBytes[1], 0x03 // ETX }; byte sum frame.Skip(0).Take(frame.Count - 1).Aggregate((s, b) (byte)(s ^ b)); frame.Add(sum); // 发送并等待响应带重试 return SendCommandAndGetBoolResponse(frame.ToArray(), count); } private bool[] SendCommandAndGetBoolResponse(byte[] cmd, int expectedCount) { const int maxRetries 3; for (int i 0; i maxRetries; i) { try { _serialPort.DiscardInBuffer(); // 清空旧数据 _serialPort.Write(cmd, 0, cmd.Length); // 等待响应超时则重试 var response WaitForResponse(1500); if (response ! null) return ParseBoolResponse(response, expectedCount); } catch { /* 忽略单次异常 */ } Thread.Sleep(100); } throw new TimeoutException($批量读取{expectedCount}个{elementType}点超时); }批量优势读64点与读1点指令长度相同通信效率提升64倍。重试机制maxRetries3Thread.Sleep(100)防止高频重试冲击PLC。4.2 心跳信号实现每500ms发送空指令维持连接PLC在MC协议下若1秒内无指令可能关闭连接或进入休眠。心跳必须轻量、无副作用private Timer _heartbeatTimer; private void StartHeartbeat() { _heartbeatTimer new Timer(_ { try { // 发送“读取PC号”指令0x0101命令PLC必响应且不操作任何寄存器 byte[] heartbeatCmd { 0x05, 0xFF, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x03 }; byte sum heartbeatCmd.Take(heartbeatCmd.Length - 1).Aggregate((s, b) (byte)(s ^ b)); heartbeatCmd[heartbeatCmd.Length - 1] sum; _serialPort.Write(heartbeatCmd, 0, heartbeatCmd.Length); } catch (Exception ex) { Console.WriteLine($心跳发送失败: {ex.Message}); // 此处可触发重连逻辑 } }, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(500)); }心跳指令选择0x0101读PC号是PLC固件级支持指令响应快、无副作用比读空地址更可靠。定时精度Timer比Task.Delay更适合后台心跳不阻塞线程。4.3 写入Bool与Word构造带数据的MC写指令写指令需在帧中追加待写入数据。写X/Y/M/S使用命令码0101H写D寄存器用0102Hpublic void WriteBool(string elementType, int address, bool value) { byte typeCode elementType switch { Y 0x01, // 仅Y输出点可写X输入点只读 M 0x02, S 0x03, _ throw new ArgumentException(仅Y/M/S支持写入) }; ushort addr (ushort)address; byte[] addrBytes BitConverter.GetBytes(addr); Array.Reverse(addrBytes); // 写单点数据长度3类型码地址1字节数据 var frame new Listbyte { 0x05, 0xFF, 0x00, 0x00, 0x01, 0x01, // 命令码 0x00, 0x03, // 数据长度3 typeCode, addrBytes[0], addrBytes[1], (byte)(value ? 0x01 : 0x00), // 数据字节 0x03 }; byte sum frame.Take(frame.Count - 1).Aggregate((s, b) (byte)(s ^ b)); frame.Add(sum); _serialPort.Write(frame.ToArray(), 0, frame.Count); } public void WriteWord(int dAddress, ushort value) { ushort addr (ushort)dAddress; byte[] addrBytes BitConverter.GetBytes(addr); Array.Reverse(addrBytes); byte[] valueBytes BitConverter.GetBytes(value); Array.Reverse(valueBytes); // 转为大端 var frame new Listbyte { 0x05, 0xFF, 0x00, 0x00, 0x01, 0x02, // 写Word命令 0x00, 0x05, // 数据长度5类型码3字节地址2字节值 0x00, // D寄存器类型码 addrBytes[0], addrBytes[1], addrBytes[2], // D地址3字节高位在前 valueBytes[0], valueBytes[1], // 值2字节高位在前 0x03 }; byte sum frame.Take(frame.Count - 1).Aggregate((s, b) (byte)(s ^ b)); frame.Add(sum); _serialPort.Write(frame.ToArray(), 0, frame.Count); }写入限制X输入点为只读强行写会报错Y/M/S可写但需确认PLC程序未锁定。地址字节数D寄存器地址用3字节如D100 0x000064符合MC协议规范。5. 避坑指南生产环境踩过的5个真实雷区与解法5.1 现象串口打开后PLC无响应Write()成功但Read()永远超时原因FX系列PLC必须同时启用DTR和RTS信号缺一不可部分USB转485适配器需额外短接DTR/RTS引脚。解决代码中强制设置_serialPort.DtrEnable true; _serialPort.RtsEnable true;并用万用表确认适配器DTR/RTS引脚电压为3V以上。5.2 现象读取D寄存器时数值忽大忽小或解析出负数原因PLC返回大端序而BitConverter.ToUInt16()默认小端未反转字节序导致高位低位颠倒。解决读Word/DWord时先将PLC返回的字节数组反转Array.Reverse()再传给BitConverter。5.3 现象批量读取64个点时第63、64位状态总是错误原因MC协议规定位元件读取最大64点但实际传输时若count非8的倍数PLC仍返回整字节数如读60点返回8字节解析时未按count截断导致最后4位取了下一字节的高位。解决ParseBoolResponse()中严格按expectedCount循环超出count的bit位不参与解析用Math.Min(expectedCount, (dataLength 7) / 8 * 8)计算有效bit数。5.4 现象程序运行几小时后通信卡死ReadTimeout频繁触发原因SerialPort内部缓冲区溢出未清空或PLC因干扰发送乱码导致帧解析失败缓冲区持续堆积脏字节。解决每次发送指令前调用_serialPort.DiscardInBuffer()在OnDataReceived中增加脏字节丢弃逻辑见3.1节并监控_receiveBuffer.Count超1024字节时强制清空并记录告警。5.5 现象心跳信号发送后PLC响应异常帧如返回0x05 0xFF 0x00 0x00 0x01 0x01 0x00 0x00 0x03 0xXX但后续读指令全部失败原因心跳指令0x0101的响应帧中数据长度字段为0x0000但部分旧版PLC固件会错误地在数据区返回1字节PC号导致SUM校验失败VerifySum()返回false该帧被丢弃但缓冲区残留未处理字节污染后续帧解析。解决对心跳响应帧单独处理——不校验SUM仅检查STX/ETX完整性或改用0x0100读PLC型号指令其响应固定为12字节SUM校验稳定。6. 进阶技巧构建可热更新的寄存器映射表与断线自动恢复策略6.1 用JSON配置文件管理寄存器地址与类型避免硬编码将PLC地址映射关系抽离为plc_mapping.json支持运行时热重载无需重启程序{ inputs: [ { name: EmergencyStop, address: 0, type: X, count: 1 }, { name: DoorOpen, address: 1, type: X, count: 1 } ], outputs: [ { name: MotorRun, address: 0, type: Y, count: 1 }, { name: AlarmLight, address: 1, type: Y, count: 1 } ], registers: [ { name: Temperature, address: 0, type: D, count: 1, dataType: Int16 }, { name: ProductionCount, address: 10, type: D, count: 1, dataType: UInt32 } ] }C#加载逻辑public class PlcMapping { public ListRegisterGroup Inputs { get; set; } public ListRegisterGroup Outputs { get; set; } public ListRegisterGroup Registers { get; set; } } public class RegisterGroup { public string Name { get; set; } public int Address { get; set; } public string Type { get; set; } // X,Y,D public int Count { get; set; } public string DataType { get; set; } // Bool,Int16,UInt32 } private PlcMapping _mapping; private void LoadMapping(string jsonPath) { string json File.ReadAllText(jsonPath); _mapping JsonSerializer.DeserializePlcMapping(json); // 启动定时器定期检查文件修改时间变化则重载 var watcher new FileSystemWatcher(Path.GetDirectoryName(jsonPath)); watcher.Filter Path.GetFileName(jsonPath); watcher.Changed (_, __) ReloadMapping(); watcher.EnableRaisingEvents true; }热更新价值产线调试时修改地址只需改JSON不用编译部署支持多PLC型号共用同一套上位机程序。6.2 断线自动恢复检测通信中断并执行三步重连通信中断常因USB插拔、电源波动导致。不能只靠SerialPort.IsOpen需结合心跳超时判断private DateTime _lastValidResponse DateTime.Now; private Timer _monitorTimer; private void StartMonitor() { _monitorTimer new Timer(_ { if (DateTime.Now - _lastValidResponse TimeSpan.FromSeconds(2)) { Console.WriteLine(检测到通信中断启动重连...); TryReconnect(); } }, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); } private void TryReconnect() { const int maxAttempts 5; for (int i 0; i maxAttempts; i) { try { if (_serialPort?.IsOpen true) _serialPort.Close(); Thread.Sleep(200); InitializeSerialPort(_currentPortName); // 重新初始化 StartHeartbeat(); // 发送测试指令验证 var testResp SendCommandAndGetBoolResponse( BuildReadXCommand(0, 1), 1); if (testResp.Length 0) { Console.WriteLine(重连成功); return; } } catch (Exception ex) { Console.WriteLine($重连尝试{i1}失败: {ex.Message}); } Thread.Sleep(1000); } Console.WriteLine(重连失败请检查硬件连接); }双重检测_lastValidResponse记录最后一次成功解析响应的时间比单纯IsOpen更准确反映通信质量。渐进式重试首次失败后立即重试后续间隔拉长避免高频重连冲击PLC。6.3 生产环境必备添加通信日志与性能统计在关键路径埋点输出可读日志供运维排查private void LogCommunication(string direction, string command, byte[] frame, TimeSpan duration) { string hexFrame BitConverter.ToString(frame).Replace(-, ); string log $[{DateTime.Now:HH:mm:ss.fff}] {direction} | {command} | Len:{frame.Length} | Time:{duration.TotalMilliseconds:F1}ms | {hexFrame}; // 写入文件按日分割 string logPath $logs/plc_comm_{DateTime.Today:yyyyMMdd}.log; File.AppendAllText(logPath, log Environment.NewLine); // 若耗时500ms标记为WARN if (duration.TotalMilliseconds 500) Console.ForegroundColor ConsoleColor.Yellow; Console.WriteLine(log); Console.ResetColor(); } // 在SendCommandAndGetBoolResponse中调用 var sw Stopwatch.StartNew(); _serialPort.Write(cmd, 0, cmd.Length); var response WaitForResponse(1500); sw.Stop(); LogCommunication(TX, $Read{elementType}[{startAddress}-{startAddresscount-1}], cmd, sw.Elapsed);日志价值当产线报警时运维可直接查当日log定位是PLC响应慢TX日志中Time列高、还是上位机处理慢RX日志中Time高。性能基线正常FX3U串口读取应在20~80ms内超200ms需检查波特率或线路干扰。我干这行八年经手过二十多条产线的PLC上位机开发最深的体会是串口通信的稳定性不取决于用了多炫的框架而取决于你是否亲手拆解过每一帧字节、是否在凌晨三点盯着串口分析仪波形、是否把“超时”“重试”“校验”这三个词刻进了肌肉记忆。这套C#原生方案没有魔法但它让你在PLC突然掉线时能立刻说出是DTR没拉高、还是SUM校验错了一位、或是心跳间隔设成了600ms——这种确定性就是产线工程师最硬的底气。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询