简介:在工业自动化和上位机开发领域,串口通讯一直是连接PC与PLC设备的经典方案。RS485、RS232等物理接口以其接线简单、抗干扰能力强、传输距离远等优势,在产线数据采集和设备控制场景中广泛使用。而C#凭借高效的WinForm/WPF界面开发能力、完善的System.IO.Ports类库支持,成为上位机通讯编程的主流选择。要实现上位机与三菱PLC的稳定通讯,关键在于理解串口报文结构、软元件地址映射规则、校验和算法,以及正确的帧解析逻辑。通过掌握这些底层原理,开发者可以灵活应对FX系列、Q系列等不同型号的通讯需求,完成寄存器读写、状态监控、远程控制等典型操作。本文以三菱PLC编程口协议为例,详细拆解协议细节、C#封装方法和常见工程问题,并结合实际项目经验给出调试与排查技巧,帮助工程师快速构建可靠的上位机通讯系统。 做上位机开发的兄弟,尤其是刚接触工控这块的,应该都经历过这种场面:设备厂家扔过来一台三菱PLC,让你用C#写个串口通讯程序去读数据、写参数,你打开Visual Studio,拖了个SerialPort控件进来,然后盯着属性面板发呆——波特率设多少?校验位怎么选?发什么命令PLC才会理我?返回的数据怎么解析成十进制?我当年在这个环节上卡了整整两天,后来拿着串口调试助手一帧一帧抓报文,才算把整个链路彻底跑通。
这篇博客就把C#和三菱PLC串口通讯的完整方案摊开讲,包括协议报文格式、地址映射规则、校验和计算、C#源码封装、硬件接线,以及一堆我在现场踩过的坑。项目本身不复杂,但细节多,任何一个参数不对,结果就是通讯超时或者数据完全错乱。适合刚入门C#上位机开发的工程师,也适合需要在产线上快速实现PLC数据采集的老手做参考。
1. 项目概述与通讯方案选型
1.1 这个项目到底在做什么
先用一句话说清楚:这个项目是用C#写一个上位机程序,通过串口(RS232/RS422/RS485)和一台三菱PLC建立通讯,实现数据的读取和写入。读取的数据比如设备运行状态、产量计数、温度压力这些模拟量;写入的比如启动停止指令、变频器频率设定值、报警复位信号。
典型的应用场景有两类。第一类是数据采集,把PLC里的D寄存器数据定时读上来,存到数据库或者显示在WinForm界面,做产线看板、MES系统对接。第二类是远程控制,上位机下发指令,让PLC执行动作,比如码垛机的启停、步进电机的运行参数切换。很多项目是两者混合,既采集又控制。
这个场景下,C#的优势很明显,WinForm/WPF写界面快,SerialPort类开箱即用,LINQ处理批量数据方便,后续对接数据库、WebService也顺手。这也是工控上位机圈子里C#占了大半江山的原因。
1.2 为什么首选串口通讯
有人会问,现在三菱PLC基本都带网口,走TCP/IP不是更香吗?实际上串口在工控现场依然是刚需。
首先,老设备存量很大。FX系列很多老型号,比如FX2N、FX3U,标配只有编程口和扩展通讯口,没有网口。你不想额外花几千块换模块,就得老老实实走串口。
其次,串口接线简单、成本低。一根RS485屏蔽双绞线,两个终端电阻,最远的通讯距离能到1200米,普通车间环境够用。网口虽然快,但铺设网线、配置IP、处理交换机故障,在恶劣电磁环境下并不省心。
第三,也是很多人忽略的一点:串口通讯协议简单透明。用串口调试助手就能看到每一帧的原始字节,排查问题非常直观。而TCP通讯虽然底层可靠,但你要面对连接管理、重连机制、报文粘包拆包,出问题的时候反而更头疼。
所以这个项目选择串口通讯,不是为了炫技,就是冲着稳定、简单、通用这六个字去的。
1.3 C#上位机通讯的几种常见路子
确定了串口方案之后,还需要确定协议层面怎么走。我整理了三菱PLC串口通讯的几种常见做法,按照实现难度从低到高排一下。
第一种,走MODBUS-RTU协议。新型号FX3U、FX5U通过RS485扩展模块,可以在PLC程序里用MODBUS指令,或者直接把PLC侧设定成MODBUS从站。上位机这边用现成的C#库,比如NModbus,几行代码就能读写。省事,但前提是PLC侧支持且需要预先配置。
第二种,使用三菱的MC协议(4C帧/4E帧)。这是三菱官方定义的通讯协议,可以走串口也可以走以太网。格式规范,地址直接写成“D100”“M50”这种字符串,可读性好。但报文封装比较复杂,帧头、子帧头、网络编号、站号一大堆字段,实现起来代码量不小。
第三种,使用FX系列编程口协议。也就是PLC编程口上运行的那套协议,很多原厂编程线就是基于它。报文结构相对简单,命令只有一个字节,地址用十六进制字符串,校验和也是简单累加。我最推荐自己动手写这套协议,因为逻辑清晰、逻辑链路短,特别适合学习串口通讯的底层原理。
本文的源码就是基于第三种方式实现的。它不需要PLC侧做任何特殊配置,只要编程口能连上编程软件,就能用同样的通讯参数和上位机交互。一台老FX系列PLC,一根USB转串口线,马上就能跑起来。
| 方案 | 协议复杂度 | PLC侧要求 | 适合场景 |
|---|---|---|---|
| 编程口协议 | 低 | 无特殊要求 | 快速实现、学习调试 |
| MC协议 | 中高 | 部分型号支持 | 官方标准化、跨型号 |
| MODBUS-RTU | 低 | 需协议转换或专用指令 | 已有MODBUS设备联调 |
2. 三菱PLC串口通讯协议核心细节
2.1 通讯帧基本结构
明白了为什么选串口、选编程口协议之后,接下来必须啃协议本身。这部分是整套源码的核心,不理解帧结构,代码写出来也是云里雾里。
FX系列编程口协议的一帧请求数据,格式是这样的:
| 字段 | 长度 | 说明 |
|---|---|---|
| STX | 1字节 | 帧起始符,固定0x02 |
| CMD | 2字节ASCII | 命令字,读为“30”,写为“31” |
| 起始地址 | 4字节ASCII | 软元件地址的十六进制表示 |
| 单元数 | 2字节ASCII | 读取/写入的软元件个数 |
| ETX | 1字节 | 帧结束符,固定0x03 |
| 校验和 | 2字节ASCII | 累加和,取低8位转十六进制 |
注意,命令字、地址、个数、校验和在串口上实际发送的都是ASCII字符,不是二进制数值。比如要读D100这一个字,起始地址是十六进制数0064,发送时不是发一个0x00、0x64,而是发两个ASCII字符‘0’、‘0’、‘6’、‘4’,也就是字节序列0x30 0x30 0x36 0x34。
这个细节是很多初学者的第一个坑。用SerialPort.Write发送字符串倒是没问题,但如果你试图用byte数组去构建,又拿字符串拼接的方式去算校验和,很容易搞混。
2.2 软元件地址与命令格式
三菱PLC的软元件有很多种,D是数据寄存器(16位字),M是内部继电器(位),X是输入继电器(位),Y是输出继电器(位),还有S状态继电器、R文件寄存器等。编程口协议里,不同软元件的地址换算规则不同。
D区最简单,编号直接转十六进制。D100就换算成0064,D1000换算成03E8。读取一个D区的字,返回的数据是4个ASCII字符,代表一个16位无符号数值,比如返回“1388”就是十六进制0x1388,换算成十进制是5000。
M区和X/Y区是位元件,处理的时候要格外小心。以FX3U为例,M区的地址映射并非直接使用编号,需要按手册给出的偏移量计算。比如M0到M255对应地址空间0x0000到0x0100的位,M256到M511又在另一个区间。很多现成代码里不管位区,只读写D区,这是最省事也最安全的用法。如果项目必须操作M区,我的建议是先下载对应型号的通讯手册,把地址映射表啃明白,再对照串口抓帧验证,别凭感觉写。
对于写入操作,CMD变成“31”,数据部分直接放在ETX之前。写入D100一个值为100,报文就是STX + 31 + 0064 + 01 + 00000064 + ETX + 校验和。这里写入的数据同样以ASCII十六进制表示,每个字需要4个ASCII字符。
2.3 校验和的计算原理
校验和(Check Sum)是通讯协议里最容易被忽略又最容易出错的地方。FX编程口协议要求的累加和,是把“从CMD到ETX为止的所有ASCII字符的ASCII码值”逐个累加,取累加结果低8位,再把这个8位数值转换成两位大写十六进制ASCII字符,追加在帧尾。
举个例子。假设请求是读D100,命令字“30”,起始地址“0064”,单元数“01”。
累加范围:'3'(0x33) + '0'(0x30) + '0'(0x30) + '0'(0x30) + '6'(0x36) + '4'(0x34) + '0'(0x30) + '1'(0x31) + ETX(0x03) = 0x1B1,取低8位是0xB1。
0xB1转换成十六进制字符串就是"B1",发送时发送字符'B'和'1',也就是0x42和0x31。
这里有个特别容易搞混的地方:累加的是ASCII码值,不是字符所代表的十六进制数本身。如果你用int.Parse去把每个字符转成0x30这种数字再去加,结果就差之千里了。C#里直接对byte数组做加法就行,因为字符在byte数组里存的就是ASCII码值。
2.4 返回帧的解析规则
PLC收到命令后,会返回两种不同类型的帧。正常处理完成的响应帧以ACK(0x06)开头,后面跟具体数据,以ETX(0x03)结束,再跟2字节校验和。请求有误或者通讯异常时,以NAK(0x15)开头,后面跟2字节错误码。
以读取D100为例,如果D100里存的是十进制5000,也就是十六进制0x1388,返回帧就是:ACK 31 33 38 38 ETX 校验和。
注意数据段是“31333838”这8个ASCII字符,也就是“1388”,需要把它解析成两个字节0x13、0x88,再组成ushort。字节序方面,三菱PLC字软元件高位在前,所以0x13是高字节,0x88是低字节,拼起来就是0x1388。
解析返回帧时,强烈建议做三件事:判断第一个字节是ACK还是NAK,校验ETX位置,重新计算一遍校验和。我在实际项目中就遇到过因为线被干扰导致数据帧出错的场景,如果不做帧校验,界面上的数值会瞬间跳成奇怪的大数,还会往下游系统传脏数据。
3. C#串口通讯源码实现
3.1 串口参数配置与初始化
协议手写方案确定之后,就到了C#代码层面。用System.IO.Ports.SerialPort类,配置其实很简单,但参数必须和PLC侧完全一致。
三菱FX系列编程口的默认通讯参数是:波特率9600,数据位7,停止位1,偶校验。这个组合很多程序员会觉得陌生,因为平时用串口调试传感器基本都是8N1,怎么到三菱就变7E1了?这跟在电脑上用USB转串口线连PLC编程口时的历史习惯有关,三菱编程口默认就是7位ASCII通讯,所以必须按这个配置来。
C#初始化的代码大致是这个样子:
using System.IO.Ports; var serialPort = new SerialPort { PortName = "COM3", BaudRate = 9600, DataBits = 7, Parity = Parity.Even, StopBits = StopBits.One, ReadTimeout = 1000, WriteTimeout = 1000 }; serialPort.Open();注意,要是在PLC侧的D8120里改过通讯格式,比如把D8120配成了8位数据无校验,那上位机这边的DataBits、Parity也必须跟着改。D8120的设定和三菱的通讯格式字是绑定的,不匹配的话,PLC永远不给你回包。
初始化这块还有一个容易被忽略的点:打开串口前先检查COM口号是否存在,打开失败要捕获UnauthorizedAccessException和IOException。现场电脑USB口供电不稳,USB转串口线驱动异常,很容易出现端口打不开的情况。
3.2 通讯帧构建代码
理解了协议格式,代码实现就是机械工作了。我把构建请求帧写成一个独立方法,便于复用。
/// <summary> /// 构建读取请求帧 /// </summary> /// <param name="address">起始地址,如D100传100</param> /// <param name="count">读取单元数</param> /// <returns>待发送的字节数组</returns> public static byte[] BuildReadFrame(int address, int count) { string cmd = "30"; // 读命令 string addr = address.ToString("X4"); // 4位十六进制地址 string len = count.ToString("X2"); // 2位十六进制个数 string body = cmd + addr + len; byte[] frame = new byte[body.Length + 4]; frame[0] = 0x02; // STX for (int i = 0; i < body.Length; i++) frame[i + 1] = (byte)body[i]; frame[body.Length + 1] = 0x03; // ETX int sum = 0; for (int i = 1; i <= body.Length + 1; i++) sum += frame[i]; string checksum = (sum & 0xFF).ToString("X2"); frame[body.Length + 2] = (byte)checksum[0]; frame[body.Length + 3] = (byte)checksum[1]; return frame; }构建写入帧的方法同理,只多了个数据段。注意写入时数据个数和实际写入数据的长度要对得上,PLC校验到长度不匹配会直接回NAK。
/// <summary> /// 构建写入请求帧,value为写入的ushort值 /// </summary> public static byte[] BuildWriteFrame(int address, ushort value) { string cmd = "31"; // 写命令 string addr = address.ToString("X4"); string len = "01"; string data = value.ToString("X4"); // 写入数据,4位十六进制 string body = cmd + addr + len + data; byte[] frame = new byte[body.Length + 4]; frame[0] = 0x02; for (int i = 0; i < body.Length; i++) frame[i + 1] = (byte)body[i]; frame[body.Length + 1] = 0x03; int sum = 0; for (int i = 1; i <= body.Length + 1; i++) sum += frame[i]; string checksum = (sum & 0xFF).ToString("X2"); frame[body.Length + 2] = (byte)checksum[0]; frame[body.Length + 3] = (byte)checksum[1]; return frame; }3.3 数据读取与写入实现
帧构建好之后,读写核心就是发送、接收、校验、解析四步。这一步很多人会踩一个坑:发送完立刻读响应,结果读到的是上一次残留的数据,或者还没等PLC反应完就超时了。
我的做法是每次发送前先清空输入缓冲区,再发送,然后用ReadByte循环读取直到收到ETX或者超时。这样能有效避免脏数据干扰。
public ushort[] ReadD(int startAddress, int count) { byte[] request = BuildReadFrame(startAddress, count); _serialPort.DiscardInBuffer(); _serialPort.Write(request, 0, request.Length); // 读取返回帧 List<byte> response = new List<byte>(); int timeout = Environment.TickCount + 1000; while (Environment.TickCount < timeout) { try { int b = _serialPort.ReadByte(); if (b < 0) break; response.Add((byte)b); if (response.Count > 2 && response[response.Count - 1] == 0x03) break; } catch { break; } } if (response.Count < 5) throw new TimeoutException("PLC响应超时或响应帧不完整"); if (response[0] == 0x15) throw new Exception("PLC返回错误:" + BitConverter.ToString(response.ToArray())); // 解析数据区 int dataStart = 1; int dataLen = response.Count - 4; // 去掉ACK、ETX和2字节校验和 ushort[] result = new ushort[dataLen / 4]; for (int i = 0; i < result.Length; i++) { string hexStr = Encoding.ASCII.GetString(response.ToArray(), dataStart + i * 4, 4); result[i] = Convert.ToUInt16(hexStr, 16); } return result; }解析写入响应就简单多了。正常写入后PLC只返回ACK + 校验和,不需要解析数据区。判断第一个字节是不是0x06就行。
3.4 线程安全与轮询策略
串口通讯是半双工模式,一个请求对应一个响应,顺序不能乱。如果程序里有多个地方同时调用读写方法,比如界面刷新线程和后台采集线程同时发命令,串口上就会出现“请求A发出,还没收到响应,请求B又发出去”的情况。PLC收到的报文乱掉,返回的自然也是错误。
解决办法很简单,给读写操作加一把锁。最简单的做法是lock一个私有对象。
private readonly object _commLock = new object(); public ushort[] ReadDWithLock(int startAddress, int count) { lock (_commLock) { return ReadD(startAddress, count); } }在WinForm里定时刷新PLC数据,建议用System.Windows.Forms.Timer或者System.Threading.Timer,轮询周期一般设置在200到500毫秒。太短了PLC和串口都忙不过来,太长了数据实时性又不够。具体视PLC扫描周期和现场工艺要求来定。
我习惯在后台线程里做轮询,用CancellationToken控制退出,把数据刷到界面时再通过BeginInvoke或者Task回到UI线程。这样做的好处是界面不会卡顿,同时所有串口操作都被lock保护,不会并发冲突。
4. 实操过程与关键环节实现
4.1 硬件连接与PLC侧配置
软件代码有了,就必须把硬件链路打通。拿最常见的FX3U系列来说,有编程口和RS485扩展板两种接法。
用编程口接线相对最简单。FX3U上有一个圆形的编程口,走的是RS422电气标准,买一条USB转RS422的编程线(比如三菱SC09-FX或者国产兼容线),电脑上装好驱动,就能识别成一个COM口。接线就是一头插PLC编程口,一头插电脑USB口,注意这种线有些是需要外接电源的,买的时候问清楚。
用RS485通信板的接法更贴近工业现场。FX3U-485-BD扩展板上有四个接线端子:SDA、SDB、RDA、RDB。SDA和SDB是一对发送差分信号,RDA和RDB是一对接收差分信号,接电脑侧的USB转RS485时,需要把485转换器的A接到SDA和RDA,B接到SDB和RDB。这里没有统一的颜色标准,很多说明书也不写清楚,只能靠万用表量电阻或者看模块丝印判断。现场如果通讯没反应,十有八九是这两对线接反了。
PLC侧要确认两件事。一是编程口模式下不需要额外配置,但RS485模式下要检查D8120寄存器的通讯格式设置。D8120是十六位寄存器,每一位代表不同的协议参数,在没有特殊指令的情况下,D8120的默认值要匹配你实际使用的波特率和校验。二是PLC程序里有没有使用RS/RS2指令,如果占了485口,上位机再发消息就会冲突。遇到这种情况,要么改PLC程序,要么换用编程口通道。
4.2 用串口助手验证通讯
写C#代码之前,强烈建议先用串口调试助手把通讯链路验证一遍。这一步能帮你确认硬件接线、串口参数、协议报文是否都正确,也能在源码调试时提供对照。
打开串口助手,选择对应的COM口,波特率9600、数据位7、停止位1、偶校验,打开串口。然后手动发送读D100的请求帧,用十六进制格式发送:
02 30 30 30 36 34 30 31 03 42 31如果接线和参数都对,PLC会返回类似:
06 31 33 38 38 03 xx xx如果返回的是15开头,说明报文格式或者地址有问题,这时候把报文拿过来和手册逐字节对照。如果什么都不返回,优先排查线序和串口参数。
这个验证过程看着麻烦,实际上能省掉后面大量调试时间。我每次做新项目都会先花五分钟做这个动作,把链路验证完了,再写C#代码就非常踏实。
4.3 完成一次真实读写演示
用代码演示一次从读取到写入的完整动作,方便直接套用到自己的项目里。
假设现场有三菱FX3U,工艺数据存放在D100到D109这10个寄存器中,用定时器每秒刷新一次;控制命令放在D200,写入500表示“运行”,写入0表示“停止”。
// 打开串口 PlcSerialClient plc = new PlcSerialClient("COM3", 9600); plc.Open(); // 每隔500ms读取一次D100开始的10个字 var timer = new System.Threading.Timer(_ => { try { ushort[] values = plc.ReadD(100, 10); Console.WriteLine($"当前产量: {values[0]}, 温度: {values[1] / 10.0:F1}"); } catch (Exception ex) { Console.WriteLine($"采集异常: {ex.Message}"); } }, null, 0, 500); // 在某个按钮事件里写入D200 = 500,控制设备启动 plc.WriteD(200, 500);这段代码是典型的上位机采集控制骨架。实际项目中你还需要把采集到的数据塞进DataGridView或者曲线控件,写入操作改为通过按钮事件触发,并在界面关闭时释放串口资源。
写入的细节也需要再说一遍:WriteD传入的是ushort值,内部会转成十六进制字符串。如果你要写的是负数,比如D寄存器存的是有符号数,-100对应的16位二进制是0xFF9C,这时候Convert.ToUInt16("FF9C",16)是可以转换的,但C#的checked环境可能溢出,需要你自己处理一下符号转换逻辑。
5. 常见问题与排查技巧实录
5.1 通讯失败排查清单
串口通讯失败,先不要怀疑代码,从物理层开始一层层查。我在现场帮人排查的问题,大概80%都是硬件和参数引起的,真正代码本身出问题的反而少。
查线序是第一步。RS485的A/B反接,PLC根本收不到有效数据。这个问题最典型的特征是:串口能打开,发送有波形,但PLC没有任何响应。用万用表量一下485转换器输出端和PLC端子的对应关系就能确认。
查串口参数是第二步。很多同事习惯性用9600、8、N、1去连三菱,结果只有7E1能通。反过来也有,有人配了PLC的D8120改成8N1,但上位机还按7E1发,一样不通。通讯参数不一致时,PLC收到的是乱七八糟的电平组合,大概率不回包或者回错数据。
查端口占用是第三步。如果你开着GX Works正连着PLC,上位机再去打开同一个串口,Windows会提示COM口被占用,或者虽然打开成功但通讯完全无响应。因为三菱编程软件独占串口。我是习惯调试完PLC程序先把GX Works断开连接,再跑上位机。
5.2 数据错乱的坑
接上PLC了,返回值也有,但数值不对,这一块的问题往往出在解析上。
最常见的是十六进制和十进制转换错误。比如PLC返回“1388”,Convert.ToInt32("1388", 16)出来的才是5000,如果你直接Convert.ToInt32("1388"),得到的就是1388,差了个数量级。这个问题在新手代码里出现频率极高,看一眼解析代码就能发现。
第二个坑是字节序问题。读32位浮点数时,三菱PLC用两个字保存一个浮点数,低地址放的是浮点数的低位字,高地址放高位字。而C#的BitConverter.ToSingle默认是小端序,直接把4个字节丢进去会得到完全错误的数值。正确做法是先把两个ushort按PLC顺序组合成4字节数组,再调用BitConverter.ToSingle。
第三个坑是数据类型宽度。D寄存器是16位,最大只能表示65535。如果PLC程序里用的是32位计数器,你读一个字肯定不够,必须把连续的两个字拼起来。拼的时候注意高位字和低位字的顺序,搞反了数值会完全对不上。
| 现象 | 常见原因 | 处理方向 |
|---|---|---|
| 无任何响应 | 线序错误、参数不符 | 检查接线与串口参数 |
| 返回NAK | 报文错误、地址越界 | 对照手册检查报文 |
| 数值不对 | 进制转换、字节序错误 | 用调试助手对照原始帧 |
| 偶发超时 | 干扰、线程冲突 | 加校验和、加锁、优化轮询 |
5.3 线程与性能问题
串口通讯项目上了规模以后,最容易出问题的反而是线程调度。比如我做的一个设备数据采集项目,后台上位机有十几个窗口都要读PLC数据,大家都在调ReadD,串口上命令乱成一锅粥。
这时候光靠lock还不够,更合理的做法是做一个统一的数据采集服务。后台单独开一个线程,用队列管理读写请求,或者干脆把所有读操作合并成一批,一次读完所有D区地址,再在内存里分发。比如要在界面上显示D100、D200、D300,分三次读不如拼一次“读D100开始的201个字”,省下的时间非常可观。虽然会多读一些不用的D区,但串口通讯快很多。
轮询周期也要注意合理性。PLC的串口处理速度远没有网口快,一次往返基本在几十毫秒到上百毫秒。如果轮询周期设定为50毫秒,很多请求会积压,最终导致通讯越来越慢。我一般的做法是500毫秒起步,需要实时性高的场景压到200毫秒,再低就不推荐了。
5.4 排查技巧速查表
最后整理一份我在实际项目中沉淀下来的排查顺序,可以复制到自己的维护文档里。
第一,用串口调试助手做“桥梁”。不经过C#代码,直接发一帧读指令,确认PLC有没有响应。这一步能精准圈出问题在硬件层、协议层还是代码层。
第二,仔细读原始返回帧。不要只看界面上最终数值,要把串口收到的字节逐个打出来,对照ACK、数据区、ETX、校验和。我遇到过一个怪问题,返回帧中间莫名多了两个0x20空格字符,一看就是通讯干扰,后来排查发现是屏蔽层接地不良导致的。
第三,善用异常捕获。C#串口读写会抛很多奇怪的异常,IOException、TimeoutException、InvalidOperationException都要单独捕获,把异常信息记录到日志文件里。我在现场维护的软件,有一个专门的log文件夹,每个通讯异常都会记录时间、发送帧、接收帧、异常类型。这样出问题的时候,翻日志就好,而不是蹲在设备前面等复现。
第四,做通讯状态自检。PLC在运行中偶尔会进入“忙碌”状态,或者程序被修改重新下载后串口连接中断。上位机最好能自动重连。我的做法是连续三次读写超时后,主动关掉串口再重新打开,重试三次,如果还不行就在界面上弹出红色告警。
第五,注意USB转串口的兼容性。工控机上用的USB转串口线质量参差不齐,一些芯片在7位数据、偶校验模式下会不稳定。我做过一个项目,用某国产芯片的转接线怎么都通讯不稳定,换了一条FTDI芯片的线,问题立刻消失。做项目时别在这上面省钱,现场出问题排查的时间成本远超一根线的差价。
第六,软件退出时释放串口。C#的SerialPort在程序异常退出时可能不会立刻释放端口,重新打开时就会报“端口被占用”。稳妥的做法是重写OnFormClosing事件,或者用try-finally保证Close方法一定执行。我习惯把串口生命周期管理封装在IDisposable的类里,配合using块使用,逻辑干净又可靠。
最后说一点我在PLC通讯项目里最深的体会:串口通讯看似是个很“老”的技术,但它稳定、直接、可控。只要你把协议细节吃透,把通讯链路每一层验证过,用C#做上位机和三菱PLC通讯,真的可以做到开箱即用。希望这篇源码拆解能帮你少走一些弯路,至少在这个环节上,不需要再像我当年那样,拿着串口调试助手一帧一帧对着手册熬到半夜。
本文还有配套的精品资源,点击获取