拓冰建站拓冰建站
首页 / 资讯中心 / 正文

三菱FX系列PLC与PC通讯方案详解:Modbus RTU实战与排坑指南

简介本资源是面向工业自动化开发者与PLC初学者的三菱FX系列PLC与PC串口通讯实战例程解决上位机数据采集、远程监控及寄存器读写等典型工程需求。压缩包共47个文件含7个核心cpp源文件实现串口通信、寄存器读写、响应解析等逻辑、9个h头文件含SerialComn.h等通讯封装模块、1个Visual Studio解决方案.sln及配套项目配置文件.vcxproj、.filters另有资源图标、对话框界面代码及调试日志文件整体19.12MB结构完整、可直接编译运行。已有195人学习下载适用于熟悉C基础并希望快速掌握FX系列PLC底层通讯机制的工程师。读者可直接复用通信框架理解FX专用协议帧格式、寄存器地址映射规则、超时重试与错误码处理逻辑并基于PC_FXPLCDlg_RW.cpp等模块快速扩展多寄存器批量读写或状态轮询功能。 搞工控这么多年三菱FX系列PLC和PC通讯这事儿我前前后后做了不下二十个项目。不管你是做MES数据采集、设备运行监控还是给产线写上位机软件早晚都会碰到一个问题怎么稳定高效地把PLC里的数据读上来。今天这篇就把我自己一直在用的通讯方案、例程源代码思路、调试时踩过的坑一次性说清楚。只要你的PLC是FX系列FX3U、FX3G、FX2N都适用参考价值都很大。新入门的朋友可以照着搭一套能跑的通讯老手也能从排坑记录里找到点自己遇到过但没深究的细节。1. 先理清楚FX系列PLC和PC通讯有哪几条路可以走很多新手一开始就卡在选型上到底用串口还是网口用三菱自己的协议还是标准的Modbus我见过不少项目前期没想清楚做到一半发现通讯方式选错了回头重新改硬件、改程序工期白白浪费一两个星期。其实FX系列能走的通讯路子就三条编程口协议、Modbus RTU、以太网MC协议。每条的硬件成本、协议复杂度、适用场景差别很大我一个个拆开讲。1.1 编程口协议零成本但容易踩坑FX系列PLC自带一个编程口通常是个圆头的Mini-DIN 8针接口这个口在出厂时默认就能跑三菱自己的“编程口协议”。你买一条USB转编程口线也就是“USB-SC09”线或者FX-USB-AW驱动线插上就能和PC通讯不需要额外加任何扩展模块硬件成本几乎为零。编程口协议的好处是简单直接你对着接口发命令PLC就会回数据。坏处是它一次只能有一个主站如果PLC这边已经在用GX Works编程软件在线监控PC再想同时发命令基本会冲突造成通讯失败或者响应超时。所以用编程口协议做上位机数据采集最怕的就是现场人员一边开着编程软件一边问你怎么连不上。1.2 Modbus RTU兼容性最好的方案Modbus RTU是工业现场最通用的协议之一FX系列PLC只要加一个FX3U-485-BD通讯扩展板或者FX3G-485-BD就能作为Modbus RTU从站工作。PLC厂家在编程软件里都内置了Modbus指令支持你不需要自己处理太复杂的协议细节只需配置好站号、波特率、校验方式然后通过上位机按标准Modbus RTU帧格式读写即可。我选Modbus RTU的最大原因是它的通用性。你整套上位机代码写一次以后不管连三菱、西门子、台达、汇川还是国产PLC只要对方支持Modbus RTU通篇逻辑都能复用。对于经常做不同品牌混接项目的兄弟来说能省掉大量重复劳动。而且RS485总线支持多站挂接一条总线上你还能顺手挂变频器、仪表、温控器后续扩展很灵活。1.3 以太网/MC协议高速大数据的首选如果项目数据量大、通讯频率高或者需要同时供多台上位机采集那就得考虑以太网方案。FX3U配FX3U-ENET扩展模块或者直接上FX5U系列走三菱的MC协议SLMP协议的一种通过TCP/IP和PC通讯。MC协议的读写吞吐率比串口高一个量级实测在百兆局域网里频繁批量读写D寄存器基本没有压力。但它的协议封装比Modbus复杂地址映射也容易把新人绕晕D区、M区、X区各自对应的编码方式不同调试时头大。我一般是在项目对实时性要求高、或者说上位机数量超过2台时才用以太网方案普通单机采集用串口Modbus完全够。三种方式对比如下方便大家按项目实际情况做初步判断通讯方式硬件需求协议复杂度实时性适用场景编程口协议USB编程线低中小数据量、临时调试、单机采集Modbus RTUFX3U-485-BD扩展板中中通用采集、多设备RS485组网以太网MC协议FX3U-ENET或FX5U高高大数据量、多上位机、实时监控2. 通讯协议核心一帧一帧拆开看不管走哪条路通讯的本质都是“帧交换”。你发送一个请求帧PLC处理后返回一个响应帧。很多人协议写不对往往不是不会发而是不知道每一帧里的每个字节到底代表什么。把协议拆开看清楚后面写代码就顺理成章了。2.1 Modbus RTU报文长什么样Modbus RTU的帧结构非常规律地址码 功能码 数据区 CRC16校验。地址码就是PLC从站地址功能码告诉PLC你要干什么数据区放具体参数CRC16校验用来保证数据在传输过程中没被干扰。我平时最常用的三个功能码03读保持寄存器、06写单个寄存器、10写多个寄存器。FX系列的D寄存器、部分特殊功能寄存器都映射在保持寄存器区间通过03功能码一次最多能读连续的125个寄存器对大多数采集需求来说足够了。举个例子我要读从站地址1的PLC中D100开始连续5个字请求帧应该是这样字节内容说明101从站地址203功能码读保持寄存器3-400 63起始寄存器地址0x0063即99对应D100的协议地址5-600 05读取5个寄存器7-8CRC校验低字节在前这里有个特别容易出错的地方寄存器地址到底是不是直接填“100”不是。Modbus侧地址和PLC编程软件里的D编号存在一个映射偏移不同PLC型号、不同固件版本映射规则都不太一样。FX3U配上485-BD后官方手册里通常写D0对应保持寄存器地址400001或者400001的偏移实际换算时要根据你PLC固件版本查表确认。我建议第一次连接时先用支持Modbus调试的上位机软件比如Modbus Poll手动读一下确认地址偏移后再写正式代码。2.2 三菱编程口协议报文和累加校验编程口协议是三菱专用的它的帧结构和Modbus完全不同。如果只做过Modbus突然看到编程口协议的报文第一反应基本都是懵的。它用ASCII字符表示命令内容比如要从D100读数据发送的报文是类似“ENQ 命令码 软元件类型 起始地址 元素个数 校验 结束符”的组合。我实际用下来编程口协议最适合的场景是用GX Works把PLC程序调试好之后上位机自己直接通过编程口读数据不需要额外配Modbus。它的响应也是ASCII形式解析起来不算复杂但校验方式不是CRC而是一个“累加和校验”把前面命令字符串的每个字符ASCII码加起来取低两位十六进制数作为校验码。写代码时最烦的是地址转换。编程口协议里D100要拆成“D”这个软元件标识加上地址的ASCII码“0100”再加上要读的字节数“0004”注意有的协议里填的是字节数不是字数中间任何一个字符写错PLC直接返回NAK错误码。这也是我后来在正式项目里更多选择Modbus RTU的原因协议标准、参考资料多、上位机调试工具成熟排查问题相对省心。2.3 CRC16校验从零实现CRC16是Modbus RTU通讯里绕不开的东西。很多现成的通讯库都内置了CRC计算但你要是自己写代码或者需要排查为什么PLC老是返回错误响应就必须搞明白CRC16到底怎么算出来的。标准的Modbus CRC16算法不长C#里大概就是下面这个样子private static ushort Crc16(byte[] buffer, int length) { ushort crc 0xFFFF; for (int i 0; i length; i) { crc ^ buffer[i]; for (int j 0; j 8; j) { if ((crc 0x0001) ! 0) { crc (ushort)((crc 1) ^ 0xA001); } else { crc 1; } } } return crc; }调用时注意一个细节计算CRC的输入长度只包含“地址码、功能码、数据区”不包含CRC自身也不包含帧头和帧尾。算完得到的crc变量是个16位无符号整数发送时先发低字节再发高字节这就是Modbus RTU常说的“低字节在前”。我的排查习惯是把收到的原始字节流打印出来用上面的函数自己算一遍对比帧尾的CRC字节。能对上说明上位机收发和校验逻辑都没问题问题出在地址或数据对不上优先怀疑串口参数或线路干扰。3. 例程核心代码用C#写一个可用的PC通讯类看协议文档看十遍不如亲手写一遍。我自己项目里用的是C#写上位机所以下面这套例程就是C#代码跑在.NET Framework或者.NET 6/8上都可以。代码里我把串口封装、读D寄存器、写D寄存器、读M线圈都拆开了你拿到手改改站号和地址就能用。3.1 串口封装打开、关闭、收发第一步是把串口操作封装成一个类。直接用System.IO.Ports.SerialPort就行不需要引第三方库。我习惯把串口打开时的参数都做成可配置的方便现场改。using System; using System.IO.Ports; using System.Threading; public class PlcModbus { private SerialPort _serialPort; private readonly object _lockObj new object(); public void Open(string portName, int baudRate 9600, Parity parity Parity.Even, int dataBits 7, StopBits stopBits StopBits.One) { _serialPort new SerialPort(portName, baudRate, parity, dataBits, stopBits) { ReadTimeout 1000, WriteTimeout 1000 }; _serialPort.Open(); } public void Close() { if (_serialPort ! null _serialPort.IsOpen) _serialPort.Close(); } }串口参数这里要特别强调FX3U-485-BD做Modbus RTU从站时默认参数是“无校验、8数据位、1停止位”也就是8N1。但是很多人会用FX3U自带的编程口搭配“FX3U-232-BD”或者使用某些国产通讯线参数可能变成“偶校验、7数据位”这个一定要看现场PLC侧“D8400”或相应特殊寄存器里的通讯设置不能想当然。3.2 读D寄存器例程Modbus RTU 03功能码读写D寄存器是上位机最常用的操作。比如D里存着产量数据、温度、速度设定值上位机定时去读。先看读函数核心是拼接请求帧、发送、等待响应、解析数据public ushort[] ReadDRegisters(byte slaveAddr, int startAddr, ushort quantity) { if (quantity 125) throw new ArgumentException(一次最多读125个寄存器); byte[] frame new byte[8]; frame[0] slaveAddr; frame[1] 0x03; frame[2] (byte)((startAddr 8) 0xFF); frame[3] (byte)(startAddr 0xFF); frame[4] (byte)((quantity 8) 0xFF); frame[5] (byte)(quantity 0xFF); ushort crc Crc16(frame, 6); frame[6] (byte)(crc 0xFF); frame[7] (byte)((crc 8) 0xFF); lock (_lockObj) { _serialPort.DiscardInBuffer(); _serialPort.Write(frame, 0, frame.Length); Thread.Sleep(50); int expectedLength 5 quantity * 2; byte[] buffer new byte[expectedLength]; int offset 0; while (offset expectedLength) { int count _serialPort.Read(buffer, offset, expectedLength - offset); if (count 0) throw new TimeoutException(读取PLC响应超时); offset count; } if (buffer[0] ! slaveAddr || buffer[1] ! 0x03) throw new Exception($PLC返回异常功能码: 0x{buffer[1]:X2}); ushort[] values new ushort[quantity]; for (int i 0; i quantity; i) { int dataIndex 3 i * 2; values[i] (ushort)((buffer[dataIndex] 8) | buffer[dataIndex 1]); } return values; } }代码里的Thread.Sleep(50)是我故意加的一个小延时。PLC响应速度很快但如果上位机和PLC之间经过无线透传模块或者现场线路比较长响应会有延迟稍微等50毫秒再读可以大幅减少数据截半的情况。实测在普通RS485双绞线、9600波特率下这个延时稳妥够用。3.3 写D寄存器例程Modbus RTU 06/10功能码写入D寄存器要用06或10功能码。如果只写一个寄存器06功能码就够了要一次写连续的多个寄存器用10功能码。我常遇到设定值下发、配方切换这些场景一次要写好几十个寄存器肯定走10功能码。这里先给出06写单个寄存器的代码public void WriteSingleRegister(byte slaveAddr, int startAddr, ushort value) { byte[] frame new byte[8]; frame[0] slaveAddr; frame[1] 0x06; frame[2] (byte)((startAddr 8) 0xFF); frame[3] (byte)(startAddr 0xFF); frame[4] (byte)((value 8) 0xFF); frame[5] (byte)(value 0xFF); ushort crc Crc16(frame, 6); frame[6] (byte)(crc 0xFF); frame[7] (byte)((crc 8) 0xFF); lock (_lockObj) { _serialPort.DiscardInBuffer(); _serialPort.Write(frame, 0, frame.Length); Thread.Sleep(50); byte[] buffer new byte[8]; int offset 0; while (offset 8) { int count _serialPort.Read(buffer, offset, 8 - offset); if (count 0) throw new TimeoutException(写入PLC响应超时); offset count; } if (buffer[0] ! slaveAddr || buffer[1] ! 0x06) throw new Exception(PLC返回异常写入失败); } }写单个寄存器时PLC成功后会原样返回请求帧所以判断响应很简单返回的从站地址和功能码一致就说明写成功了。但要注意有些PLC程序里还会对写入值做范围判断你写入的数值超出设定范围PLC照样返回正常响应但内部逻辑会按默认值处理。所以写完后我习惯再读一遍确认PLC实际接受的值。写多个寄存器用10功能码请求帧里多了一个“字节数”字段public void WriteMultiRegisters(byte slaveAddr, int startAddr, ushort[] values) { int quantity values.Length; if (quantity 123) throw new ArgumentException(一次最多写123个寄存器); int frameLength 9 quantity * 2; byte[] frame new byte[frameLength]; frame[0] slaveAddr; frame[1] 0x10; frame[2] (byte)((startAddr 8) 0xFF); frame[3] (byte)(startAddr 0xFF); frame[4] (byte)((quantity 8) 0xFF); frame[5] (byte)(quantity 0xFF); frame[6] (byte)(quantity * 2); for (int i 0; i quantity; i) { frame[7 i * 2] (byte)((values[i] 8) 0xFF); frame[8 i * 2] (byte)(values[i] 0xFF); } ushort crc Crc16(frame, frameLength - 2); frame[frameLength - 2] (byte)(crc 0xFF); frame[frameLength - 1] (byte)((crc 8) 0xFF); lock (_lockObj) { _serialPort.DiscardInBuffer(); _serialPort.Write(frame, 0, frame.Length); Thread.Sleep(50); byte[] response new byte[8]; int offset 0; while (offset 8) { int count _serialPort.Read(response, offset, 8 - offset); if (count 0) throw new TimeoutException(写入PLC响应超时); offset count; } } }10功能码的正常响应帧固定是8个字节从站地址、功能码、起始地址、寄存器数量、CRC。如果PLC返回的字节数不对很可能是CRC错误或功能码不受支持建议打断点看响应内容。3.4 读M线圈例程Modbus RTU 02功能码除了D寄存器M线圈也是经常要读的状态量比如设备启动、停止、报警信号。Modbus协议里读离散量输入用02功能码读线圈状态用01功能码。三菱FX系列支持把M区映射到线圈地址具体映射要根据你用的扩展模块型号来定。读M线圈的响应有个特点PLC返回的不是一个字节对应一个线圈而是把多个线圈按位打包。比如读10个M线圈PLC只返回2个字节第0个线圈在第一个字节的bit0第7个线圈在bit7第8个线圈在第二个字节的bit0依此类推。public bool[] ReadMCoils(byte slaveAddr, int startCoilAddr, ushort quantity) { byte[] frame new byte[8]; frame[0] slaveAddr; frame[1] 0x01; // 读线圈如果从站映射为输入点则用0x02 frame[2] (byte)((startCoilAddr 8) 0xFF); frame[3] (byte)(startCoilAddr 0xFF); frame[4] (byte)((quantity 8) 0xFF); frame[5] (byte)(quantity 0xFF); ushort crc Crc16(frame, 6); frame[6] (byte)(crc 0xFF); frame[7] (byte)((crc 8) 0xFF); lock (_lockObj) { _serialPort.DiscardInBuffer(); _serialPort.Write(frame, 0, frame.Length); Thread.Sleep(50); int byteCount (quantity 7) / 8; int expectedLength 3 byteCount 2; byte[] buffer new byte[expectedLength]; int offset 0; while (offset expectedLength) { int count _serialPort.Read(buffer, offset, expectedLength - offset); if (count 0) throw new TimeoutException(读取M线圈响应超时); offset count; } bool[] coils new bool[quantity]; for (int i 0; i quantity; i) { int byteIndex i / 8; int bitIndex i % 8; coils[i] (buffer[3 byteIndex] (1 bitIndex)) ! 0; } return coils; } }这里要特别留意实际项目中M线圈地址偏移跟D寄存器一样也存在映射表。比如有的模块里M100对应的Modbus线圈地址可能是0x0064有的可能是0x0065。关于偏移我在第4章会详细讲。3.5 进阶示例通过PLC读写变频器频率聊到热词里的“三菱PLC读取写入变频器频率程序”其实这类需求大多是两层通讯PC和PLC通讯PLC再和变频器走RS485 Modbus通讯。PC只负责和PLC交互最终的下发命令由PLC的梯形图程序完成。PC侧要做的其实很简单把目标频率先写入PLC的某个D寄存器比如D200作为“频率设定缓存区”PLC程序检测到D200变化后自动用RS指令或者Modbus指令去写变频器的频率地址常见变频器频率地址是40001或者40002不同品牌有差异。反过来要读当前频率PLC定时从变频器读取频率值存到D210PC再从D210把数据读上来。这样做的好处很明显PC侧代码和变频器品牌无关我只需要面对PLC这个“中间人”。哪怕现场换了变频器品牌只要PLC程序里对应地址改一下参数上位机一行代码都不用动。设置流程大概是PLC侧用FX3U-485-BD或者FX3U-485ADP走Modbus RTU主站模式和变频器通讯。PLC程序里用ADPRW指令三菱FX3U的Modbus读写指令读写变频器频率。上位机通过PC和PLC的Modbus通讯读写PLC的D200、D210这类缓存寄存器。我在一个包装设备项目里就是按这个套路做的PLC程序里写变频器频率、读变频器电流电压上位机只需要定时读D区缓存稳定跑了大半年没出过通讯问题。需要强调的一点变频器在RS485总线上强烈建议加终端电阻否则电机运行状态下电磁干扰很容易让通讯帧出错这一条经验值不少钱。4. 实操排坑现场调试踩到的雷与解法代码写出来只是第一步能不能在现场稳定跑起来才是真考验。我自己在调试过程中踩过的坑整理出来比代码本身还值钱。4.1 串口参数对不上报错却莫名其妙最常见的问题没有之一。上位机软件里设置9600,8,N,1但PLC侧实际通讯参数是19200,7,E,1两边鸡同鸭讲收到的数据全是乱码。棘手的是串口参数不对有时候不一定会立刻报错而是表现为“偶尔连通、频繁超时”非常迷惑。排查方法很简单一是看PLC特殊寄存器D8120串口通讯设置对照手册确认当前波特率、校验方式、停止位数二是用串口调试助手监听上位机发出的原始字节流再对比PLC返回内容乱码基本就是参数不匹配。我吃过一次亏之后现在做项目第一件事就是先统一通讯参数并且把参数表写进项目需求文档里。4.2 寄存器地址偏移导致读到错误数据这问题比通讯没通还难排查因为通讯明明是好的功能和CRC都正确但读出来的数值明显不对。比如上位机想读D100的温度值读上来的却是D101的内容或者数值整体错位。原因还是出在地址映射。以三菱FX3U为例Modbus从站模式下D0对应保持寄存器的地址在不同固件版本中有不同定义从400001、400001到400101都出现过。我建议做项目时先用Modbus Poll这类调试软件扫描一遍PLC地址把“D号-协议地址”的映射表确认好然后在代码里定义一个地址常量以后要改也只需要改这一处。4.3 通讯超时与数据截半上位机发出请求后PLC的执行时间可能因为程序扫描周期、总线负载等原因产生几十到几百毫秒的延迟。如果上位机读超时时间设得太短比如200毫秒很容易导致请求发送成功但读取响应超时。还有一种情况是响应数据太长比如一次读120个寄存器如果不正确处理串口缓冲区的分包接收经常只读到半截数据。我的做法是把超时时间放宽到1000毫秒甚至2000毫秒串口接收用循环读取直到攒够期望字节数不依赖一次性读完。读多寄存器时先把期望长度算出来再循环接收直到长度满足要求。这样即使线路存在一定的延迟程序也能稳定拿到完整帧。4.4 常见问题速查表最后整理一张速查表是我自己现场排查问题的习惯路线能帮你快速定位大部分通讯故障现象可能原因检查与解决办法完全没有响应通讯线接错、从站地址不对检查A/B线是否接反确认站号响应乱码波特率、校验位不一致核对D8120参数和上位机设置能通但数据不对寄存器地址偏移用Modbus Poll扫描确认地址映射偶发超时线路干扰、无终端电阻加120欧终端电阻检查屏蔽层接地数据截半未处理串口分包循环接收直到收满期望长度写值无效值超出PLC程序设定范围写完后读回确认检查梯形图上下限最后再分享一个我自己的习惯所有上位机通讯代码里务必加一个“通讯状态标志”和“重试机制”。PLC采集中断或者现场总线上有设备掉线时上位机不能傻等更不能不报错一直重发。我通常的做法是通讯失败重试3次超过3次就置报警状态并在界面上提示操作工检查线路和设备状态而不是让后台程序无限循环。这个设计救了我很多次也让我在项目维护阶段少接了不少半夜的报障电话。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门