三菱FX系列PLC与PC通讯实战:C#上位机串口读写与协议解析
2026/9/1 1:18:23 网站建设 项目流程

简介:本资源是面向工业自动化领域开发者与电气工程师的三菱FX系列PLC与PC通信实战源码包,解决上位机与PLC之间数据读写、状态监控及远程控制等核心需求,适用于设备调试、产线数据采集及HMI开发等典型场景。压缩包共47个文件,含7个C++源文件(cpp)、9个头文件(h)用于协议封装与串口通信逻辑,另有vcxproj工程配置、rc资源文件、png图标及ReadMe说明文档等,完整构建了基于Windows平台的可视化通讯应用框架,包体大小为19.12MB。已有195人学习下载,源码结构清晰,包含SerialComn.h/.cpp串口通信模块、PC_FXPLCDlg_RW.cpp寄存器读写功能、Test与Others功能扩展模块,覆盖MODBUS/FX专用协议解析、寄存器地址映射、超时重试机制及错误码处理等关键实现细节,可直接编译调试并快速集成到实际项目中。 做设备上位机开发这些年,被问得最多的一句话就是:“三菱FX的PLC,PC到底怎么连上去?”其实梯形图写得好不好是一回事,通讯能不能打通才是真正容易卡人的地方。尤其是FX系列这种服役多年的老平台,型号多、接口杂、协议版本还不太统一,网上搜到的例程要么是残缺的复制粘贴,要么只给了一段代码却不说清楚原理,照着用经常是“连上了但读不到数据,读到了又不知道对不对”。

这篇文章我打算把三菱FX系列PLC与PC通讯这件事从头到尾拆开讲透,重点落在一套可以直接参考的C#例程源代码上。核心思路是:先把通讯链路和协议类型搞清楚,再动手写代码。换句话说,不光给你鱼,还把钓鱼的整套家伙事摆出来讲明白。不管你是刚接触PLC通讯的自学者,还是要给现场设备写上位机的工程师,按这个思路走,能少走不少弯路。

1. 动手前先分清:FX系列PC通讯的三种主流链路

很多人在第一步就栽了,原因很常见:拿到一台FX3U,看到背面有个mini-DIN圆口,以为插上编程线就能和自写的上位机通讯。实际上“编程口”和“通讯口”不是一回事,三菱FX系列和PC通讯的路径至少有三条,选错了路,后面全白搭。

1.1 自带编程口:好用但别急着用

FX3U、FX3GC这类型号自带一个mini-DIN 8针编程口,GX Works软件就是通过这个口下载程序的。物理层是RS422,不是电脑串口常见的RS232。如果你手里没有三菱原装的USB-SC09-FX编程线,或者一条USB转RS422的转换线,那这个口对自写上位机来说基本是摆设。

即便有合适的电缆,这个编程口默认工作在“编程协议”下,也就是GX Works和PLC之间对话的那套私有帧。它当然可以被第三方程序解析,网上也有不少基于编程口协议的库,但帧格式繁琐,还要做ENQ握手,通讯顺序固定,踩坑成本高。我的建议是:如果是自己写例程玩,别在这个口上死磕;如果项目里只有编程口可用,那也尽量买三菱官方通讯线,用自带驱动库的方式来做。最省心的方案,是加一块通讯扩展板。

1.2 232-BD与485-BD:写例程最该选哪条路

FX系列最大的优点是扩展灵活。以FX3U为例,可以插FX3U-232-BD或者FX3U-485-BD这类内置通讯板,一个巴掌大的小板子插在PLC正面,就能把RS232或RS485接口引出来。

对于PC上位机直接连PLC的场景,我优先推荐RS232的232-BD,原因很朴素:PC端串口天然就是RS232电平,一根交叉线就能连,不用考虑电平转换。RS485当然也行,但PC一般没有485口,还得挂一个USB转485的转换器,多一个设备就多一个故障点。

232-BD接出来的是一个9针D型公头,和PC串口的连线大概是:PLC侧RXD接PC侧TXD,PLC侧TXD接PC侧RXD,GND接GND。别小看这三根线,很多人通讯不通,最后查出来就是2、3脚没交叉,拿着直通线当交叉线用。

485-BD的优势在于距离和多机通讯。485总线能拖几十台设备,距离能到几百米,适合PLC和变频器、仪表组网的场景。比如后面要说的PC→PLC→变频器三层结构,PLC和变频器之间走485,就很合适。

1.3 PLC侧参数:D8120与D8121的角色

通讯板装好了,线也接对了,接着要面对的是PLC侧的特殊寄存器:D8120和D8121。这两个东西是串口通讯的“大脑”,不设置好,PC端再努力也白搭。

D8120负责通讯格式,包括数据位、校验位、停止位、波特率,一个16位寄存器用不同位的组合把整个通讯参数定下来。D8121负责站号,多台PLC挂在同一条总线上的时候,通过站号区分彼此。

实操中,我一般用GX Works的“PLC参数”界面图形化设置,或者在程序里直接向D8120写一个16位值。这里要特别提醒:D8120的设置在PLC重启之后才生效。我早期干过一件蠢事,参数写进去了,程序也下载了,结果通讯还是不通,折腾了半小时才想起来PLC没断电重启。后来习惯改完D8120顺手断电一次,这个动作比什么调试技巧都管用。

至于具体填什么值,以“8数据位、偶校验、1停止位、波特率9600”为例,不同FX型号的D8120位定义略有差异,建议直接查对应型号的手册末尾位定义表。原则很简单:PLC侧和PC侧串口参数必须一致。PC端用8E1,PLC侧就配8E1,两边对不上,数据收到的一定是乱码。

2. 例程核心:计算机链接协议的帧格式与C#封装

通讯链路物理层打通后,就得面对协议了。三菱FX系列在232-BD/485-BD上最常用的开放协议叫“计算机链接协议”,也叫Computer Link Protocol。这个协议结构清晰、文档公开、适合自写上位机,我后面的C#例程就基于它展开。

2.1 一帧请求的组成与校验和算法

计算机链接协议里,PC发给PLC的一帧请求长这样:

  • 控制码ENQ(0x05),表示请求发送
  • 站号,2位ASCII字符,比如“00”
  • 命令码,4位ASCII字符
  • 软元件地址
  • 点数或数据
  • 结束码ETX(0x03)
  • 校验和,2位ASCII十六进制字符

以读取D100开始的3个字为例,完整的请求帧是:

ENQ 00 WR 0100 03 ETX 校验和

这里的“WR”是字软元件批量读取命令,地址“0100”是D100的4位十进制ASCII表示,点数“03”是3个点。如果是写操作,命令码换成“WM”,后面跟上要写入的数据。

校验和算法是整个协议里最容易被写错的地方。规则是:从站号的第一个字符开始,到ETX(包含ETX)为止,把所有字符的ASCII码累加,取累加和的低8位,再转成两个大写十六进制字符。举个例子,帧“00WR010003”加上ETX,ASCII码依次相加,得到一个和,低字节是某个值,就转成两位十六进制拼在帧尾。

这个算法看起来简单,实际写代码时容易漏掉ETX,或者把ENQ也算进去。记住一句话:校验范围从站号开始,到ETX结束,不含ENQ。

2.2 响应帧里藏着哪些信息

PC发出请求后,PLC会回一帧。正常读取响应以STX(0x02)开头,后面跟上站号、命令码、数据、ETX、校验和。D寄存器每个点对应4个ASCII十六进制字符,比如D100里存的是0x1234,响应数据段就是“1234”,高位在前。

如果是写操作,PLC回的是ACK(0x06),表示写入成功。如果通讯帧有错误,比如校验和不对、地址超范围,PLC回的是NAK(0x15),后面带上站号和错误码。常见的错误码有02(校验和错误)、03(协议格式错误)、18(软元件地址超限)等。

解析响应时,我的习惯是先读一个字节判断控制码:STX走正常解析,ACK认为写入成功,NAK则把后面两位错误码读出来,直接抛给上层,方便排查。

2.3 为什么我不用编程口协议做上位机

前面提到,编程口协议是GX Works和PLC之间通讯用的私有协议,虽然也可以被第三方程序解析,但它有几个明显的劣势。

一是帧格式和计算机链接协议完全不同,编程口协议要先发ENQ等待ACK,再发命令帧,流程上多了一个握手环节,时序要求更严格。二是软元件地址编码复杂,D寄存器要拆成软元件类型和十六进制地址两段表示,解析时容易搞混。三是协议的公开资料参差不齐,网上流传的版本经常有细微差异,照着抄很容易踩坑。

计算机链接协议就清爽得多,地址直接用十进制ASCII表达,命令码只有BR、WR、BM、WM等几个,调试时拿串口助手都能肉眼读帧。所以但凡PLC侧有条件走计算机链接协议,我就不会去碰编程口协议。

3. C#源码逐段拆解:从串口到D寄存器读写

下面进入正题,给一套可以直接参考的C#例程。这套代码我在多个项目里改过,核心结构没变过,只根据现场情况调整串口号、站号和地址。先说明一下编程语言选择:C#做工业上位机目前还是主流,SerialPort库成熟,和WinForm/WPF界面结合方便,适合快速出活。

3.1 串口打开与基础设置

串口参数这块,要在代码里和PLC侧D8120的配置保持严格一致。下面这段是打开串口的函数,默认按9600、偶校验、7数据位、1停止位设置,这是三菱FX计算机链接协议很常见的出厂配置。

using System; using System.IO.Ports; using System.Text; class FxComputerLink { private SerialPort _port; public bool Open(string com, int baud = 9600, Parity parity = Parity.Even, int dataBits = 7, StopBits stopBits = StopBits.One) { _port = new SerialPort(com, baud, parity, dataBits, stopBits) { ReadTimeout = 1000, WriteTimeout = 1000 }; try { _port.Open(); return _port.IsOpen; } catch (Exception ex) { Console.WriteLine("串口打开失败: " + ex.Message); return false; } } }

几个细节说一下。ReadTimeout和WriteTimeout必须设,否则PLC没响应时程序会一直卡在串口读上,现场调试会以为死机了。数据位用7还是8取决于PLC侧配置,这点很容易被忽略,PC串口默认是8位,而三菱计算机链接默认是7位,配错的结果就是收发内容对不上。

3.2 读取D寄存器的完整流程

读取操作是整个上位机通讯里最常用的,我把构建帧、发送、解析封装成了完整函数。这部分代码可以直接跑,调用方只需要传入站号和D寄存器地址。

// 构建读取D寄存器的请求帧 private byte[] BuildReadFrame(byte station, string deviceAddr, int count) { // deviceAddr 是4位十进制ASCII地址,例如D100对应 "0100" string dev = deviceAddr.PadLeft(4, '0'); string command = $"{(char)0x05}{station:D2}WR{dev}{count:X2}{(char)0x03}"; int sum = 0; for (int i = 1; i < command.Length; i++) // 从站号开始累加,跳过ENQ { sum += command[i]; } command += (sum & 0xFF).ToString("X2"); return Encoding.ASCII.GetBytes(command); } // 读取一串D寄存器,返回ushort数组 public ushort[] ReadDWords(byte station, string deviceAddr, int count) { byte[] frame = BuildReadFrame(station, deviceAddr, count); _port.DiscardInBuffer(); _port.Write(frame, 0, frame.Length); int ctrl = _port.ReadByte(); if (ctrl == 0x15) // NAK { byte[] err = new byte[3]; _port.Read(err, 0, 3); throw new Exception("PLC返回错误,错误码: " + Encoding.ASCII.GetString(err, 1, 2)); } if (ctrl != 0x02) // 不是STX { throw new Exception("未知响应控制码: 0x" + ctrl.ToString("X2")); } // STX之后依次是:站号2 + 命令码2 + 数据(4*count) + ETX1 + 校验和2 int dataLen = 4 * count; byte[] body = new byte[2 + 2 + dataLen + 1 + 2]; _port.Read(body, 0, body.Length); string dataText = Encoding.ASCII.GetString(body, 4, dataLen); ushort[] result = new ushort[count]; for (int i = 0; i < count; i++) { string hex = dataText.Substring(i * 4, 4); result[i] = Convert.ToUInt16(hex, 16); } return result; }

这段代码里有一个很容易忽略的点:BuildReadFrame里用了count:X2,也就是读取点数转成两位十六进制ASCII。比如读3个点,拼出来是“03”而不是“3”。计算机链接协议对点数的格式要求就是这样,少了前导零,PLC会直接回NAK。

调用方式很简单:

var fx = new FxComputerLink(); if (fx.Open("COM3")) { ushort[] values = fx.ReadDWords(0, "0100", 5); // 读到了D100~D104共5个寄存器的值 }

注意这里的"0100"是D100的4位十进制地址。如果以后要读其他寄存器,比如D1234,就传"1234",D5就传"0005"。我习惯在代码里加一个辅助函数,把D寄存器编号转成地址字符串,避免手写前导零出错。

3.3 写入操作与M位元件扩展

写入单个D寄存器的思路和读取对称,只是命令码换成“WM”,数据区放要写入的值。代码实现如下:

private byte[] BuildWriteFrame(byte station, string deviceAddr, ushort value) { string dev = deviceAddr.PadLeft(4, '0'); string command = $"{(char)0x05}{station:D2}WM{dev}{value:X4}{(char)0x03}"; int sum = 0; for (int i = 1; i < command.Length; i++) { sum += command[i]; } command += (sum & 0xFF).ToString("X2"); return Encoding.ASCII.GetBytes(command); } public void WriteDWord(byte station, string deviceAddr, ushort value) { byte[] frame = BuildWriteFrame(station, deviceAddr, value); _port.DiscardInBuffer(); _port.Write(frame, 0, frame.Length); int ctrl = _port.ReadByte(); if (ctrl == 0x15) // NAK { byte[] err = new byte[3]; _port.Read(err, 0, 3); throw new Exception("写入失败,错误码: " + Encoding.ASCII.GetString(err, 1, 2)); } if (ctrl != 0x06) // 不是ACK { throw new Exception("未知响应控制码: 0x" + ctrl.ToString("X2")); } }

M位元件也就是中间继电器的读写,协议上对应命令码“BR”和“BM”。位元件每个点的响应数据只有1个ASCII字符,不是D寄存器的4个字符,解析逻辑稍有不同,但帧构建原理完全一样。实际项目里,如果只是控制设备启停,读M元件的频率比读D寄存器还高,代码改起来很快。

4. 实测通讯不通的排查链路与三个隐蔽坑

代码写完了,通讯不通,这是大多数人的下一个坎。而且越是感觉自己写得没问题的,越容易卡住。这里我把自己调试的排查顺序和几个隐蔽坑完整列出来,直接照着检查就行。

4.1 先按这个顺序把通讯链路走一遍

通讯调试最忌讳东敲一锤子西打一棒子。我现在的习惯是严格按下面的顺序排查:

  1. 物理链路层:用万用表量一下TXD/RXD/GND有没有断线,232接口的2、3脚是否交叉正确。很多人第一步就错在这里,用了一根直通线,以为不用交叉。
  2. 串口号确认:设备管理器里看COM口号,USB转串口线每次插拔都可能变号,代码里写死COM3,实际可能是COM7,打开串口失败时先看这里。
  3. 串口参数一致性:9600、7E1还是8E1,PC端和PLC端必须一致。这个最隐蔽,因为两边都看起来“正常”,但就是满屏乱码。
  4. 用串口助手验证:先把C#程序放一边,用串口调试助手手动发一帧,比如05 30 30 57 52 30 31 30 30 30 33 03 校验和,看PLC回什么。这能快速区分是协议问题还是程序问题。
  5. 检查站号和地址:站号对不对,地址是不是4位补零,点数是不是两位十六进制。这些细节错一个,CRC和格式都对,PLC照样回NAK。
  6. 捕获响应帧十六进制:在代码里把收到的原始字节打出来,Hex格式逐字节看,不要只盯着解析后的值。

如果做到第4步用串口助手能正常收到应答,那问题基本就锁定在程序侧;如果串口助手都收不到,一定是链路层或者PLC侧设置有问题,继续检查D8120和硬件接线。

4.2 三个我踩过的隐蔽坑

第一个坑是“能发不能收”的USB转串口线。市面上不少便宜的USB转串口线用的芯片方案不完整,只做了发送引脚,接收引脚是悬空的,表现为程序发送正常,PLC那边也有反应,但PC永远收不到数据。现场调试时备一根好点的转换线,比如FTDI芯片方案的,能省掉一大半这种玄学问题。

第二个坑是串口缓冲区残留数据。程序启动时串口里可能还有上次通讯的残留字节,如果不先DiscardInBuffer(),读取响应时会先读到一堆脏数据,控制码对不上STX,直接抛异常。我在ReadDWordsWriteDWord里都做了发送前清空缓冲区的操作,这个习惯值得保留。

第三个坑是读取点数和实际不符。计算机链接协议单次批量读取点数有限制,超过上限PLC会拒绝响应。我一开始没看手册,一口气读了D100到D200,共101个字,结果PLC一直不回帧。后来查手册才知道限制,改成每批最多60个点,分批循环读取,问题就解决了。如果你发现读取几十个点正常,上百个点就没响应,大概率是这个原因。

5. 把例程延伸出去:PC→FX→变频器的分层通讯

基础通讯打通之后,我经常遇到的一个需求是:PC不仅读PLC的寄存器,还想通过PLC去控制变频器的频率。这个场景在热搜词里出现频率极高,比如“三菱plc读取写入变频器频率程序”“fx2n plc读取和写入变频器频率”,说明很多人都在做类似的事。

5.1 为什么要把变频器挂在PLC下面

有些人的第一反应是PC直接连变频器,一台PC挂一条RS485总线把所有变频器都带上。这个方案在某些场景下可行,但实际产线上,我更倾向让PLC做中间层,PC只跟PLC通讯,变频器挂在PLC的485总线上。

好处有三个:一是PLC和变频器之间的协议由PLC程序处理,PC无需关心变频器是哪个品牌型号,换变频器时PC端代码可以不动;二是PLC本身有逻辑控制能力,可以在通讯故障时立即停车,比PC上位机响应可靠得多;三是产线设备往往已经有PLC和变频器的物理连接,PC只是搭便车,不用再拉长距离通讯线。

5.2 ADPRW指令把PLC变成Modbus主站

三菱FX3U系列内置了ADPRW指令,可以直接把PLC的485通讯口变成Modbus RTU主站,去读写支持Modbus协议的变频器。FX2N没有这个指令,就需要用RS无协议通讯指令自己组Modbus帧,自己算CRC16,稍微麻烦一点。

用ADPRW写梯形图的思路是:先配置好串口参数,再用ADPRW指令指定从站号、Modbus功能码和寄存器地址。比如要向变频器写入目标频率,就要查变频器通讯手册里的频率设定寄存器地址,填到ADPRW里,数据来源一般是PLC程序里的某个D寄存器。

这里有个容易搞混的点:ADPRW指令里的“Modbus软元件编号”不是三菱PLC的D编号,而是变频器侧Modbus地址。比如某款三菱变频器的频率设定地址是0x0001,你就要在指令里写H0001,而不是随便填个D100。变频器说明书里的“通讯协议”章节都会有寄存器列表,照着查就行。

5.3 代码里怎么处理分层通讯

PC上位机侧的逻辑就简单了:仍然用前面这套计算机链接协议读写PLC的D寄存器。PLC程序里把“目标频率”写到某个D寄存器,上位机往这个D写入数值,PLC检测到数值变化后,再通过ADPRW转发给变频器。反向也一样,变频器当前频率被PLC读回来后存入另一个D寄存器,上位机定时读这个D就能拿到实时频率。

整体时序大概是:

  • 上位机写D100,写入值比如500,代表5.00Hz
  • PLC程序检测到D100变化,调用ADPRW指令把500写入变频器频率寄存器
  • 变频器接收指令,频率开始变化
  • PLC周期性通过ADPRW读取变频器当前频率,存入D110
  • 上位机读D110,刷新界面显示

这套架构的好处是,PC上位机始终只需要和PLC打交道,串口帧、校验和、寄存器映射这些只需要维护一份,变频器侧的协议细节全部被PLC程序屏蔽掉了。产线上如果换了一台其他品牌的变频器,PC端一行代码都不用改,只要改PLC程序和变频器参数就行。

说到底,三菱FX系列和PC通讯这件事,真正难的从来不是写代码,而是把物理层、协议层、PLC参数层这三层串起来理解。你把这套链路吃透了,不管是换用Python写上位机,还是改用485总线挂多台设备,都只是换一层皮,基本原理完全一样。

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

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

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

立即咨询