十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#串口读写三菱PLC实战:MC协议帧格式与地址计算详解

C#串口读写三菱PLC实战:MC协议帧格式与地址计算详解 简介面向C#开发者的三菱PLC串口通信示例工程覆盖单个布尔、批量布尔、Word、Dword读写以及心跳信号检测等典型需求可直接用作工业上位机开发的参考模板尤其适合需要长时间监控通信状态的项目。压缩包共49个文件体积仅184KB以cs源文件为主并包含exe示例程序、dll依赖库、config配置文件、pdb调试符号、resources资源文件等整体结构紧凑便于理解与二次编译。目前已有2153人学习下载。工程内含DebugClass与MitsubshiPLCSerialPort两个项目前者提供frmMain主窗体后者提供MitsubshiPLC.cs、SerialPortClass.cs核心通信类及frmDemo演示窗体系统演示了串口参数配置、二进制数据收发、类型转换、DataReceived事件监听、连接状态监控以及多线程读写等关键环节。通过参考完整源码可快速掌握三菱PLC串口协议的数据帧处理思路减少自行摸索底层通信细节的时间适合具备一定C#基础、需要快速落地PLC通信功能的开发者借鉴复用。 前阵子接了个老客户的改造需求设备本体还行控制柜里躺着一台三菱FX系列PLC就是通讯口太少客户预算又卡得死加不了以太网模块。最后方案落到了串口上用C#写个上位机通过串口直接读写三菱PLC。项目做下来发现网上关于读单个bool、批量bool、Word、Dword的串口实现讲得都比较零散要么只说协议不给代码要么给了代码但地址计算一笔带过。这篇就把整个C#串口读写三菱PLC的过程掰开揉碎讲清楚从协议帧结构、地址计算到C#代码落地、心跳信号设计再到联调排错一次性说透。1. 为什么还要用串口场景、线缆和PLC型号1.1 网口满天飞的年代串口依然存在的理由很多人一上来就问三菱PLC不带网口吗答案是大部分FX系列老型号、以及一部分Q系列早期型号真就没有以太网口。就算带了现场也可能存在网口被占用、路由器端口映射被IT部门管死、或者客户压根不愿意动网络架构的情况。更现实的是很多设备是十年前甚至更早的更换CPU模块的成本够买好几台上位机了。串口的另一个优势是简单直接一根线一对参数没有IP冲突、没有防火墙拦截、没有交换机Down机。对工控场景来说可靠优先串口这种点对点的物理链路反而更让人踏实。如果你的PLC刚好在柜子里距离在15米以内用RS232或者RS422其实非常省心。1.2 先核对PLC型号和通讯模块不同PLC的串口协议入口不一样这个坑我踩过一次所以写在最前面三菱PLC的串口协议不止一种编程口协议和MC协议也叫计算机链接协议是两回事。FX3U之前的FX系列比如FX1N、FX2N圆头编程口默认走的不是MC协议而是三菱自己的编程口协议帧格式完全不同——没有STX/ETX开头是STX后面跟站号那种格式。所以你要做的第一件事不是写代码而是确认你的PLC上MC协议从哪个口出FX3U本体自带的编程口可以通过PLC参数设置切换到MC协议部分固件版本支持扩展的通讯模块比如FX3U-232-BD、FX3U-485-BD可以直接使用MC协议Q系列通过串口通讯模块如QJ71C24N使用时模块上需要配置协议为MC协议FX5系列虽然主打网口但本体串口也支持MC协议我在项目里用的是FX3U-232-BD模块232口接电脑PLC参数里把通讯协议设置为MC协议格式设置为格式4——这个格式4对应的就是带帧头STX/帧尾ETX的帧结构。如果你的PLC侧设置成格式1无帧头直接发那上位机帧构造方式也要跟着变。这一点一定要提前确认不然代码写得再漂亮也是白搭。1.3 通讯线缆与接口转换RS232/RS422/RS485别接错三菱FX3U的编程口以及FX3U-232-BD模块电气接口其实是RS422不是RS232。电脑的九针串口或者USB转串口线是RS232电平直接接上去收不到数据甚至可能烧东西。必须用一个USB转RS422的转换器或者确认你用的232-BD模块内部已经做了电平转换——多数品牌的原装模块出来后确实是232电平但国产兼容模块不一定最好拿示波器或者串口调试助手先验证一下。RS485则要注意A/B线别接反工程上最常见的问题就是A/B互换之后时通时不通偶尔能收到几帧回包过一会儿又完全没反应。这种情况排查起来极其烧脑建议接线时用万用表确认A/B极性不要凭颜色猜。2. MC协议帧结构读写三菱PLC绕不开的ASCII帧2.1 一个完整读帧长什么样MC协议串口帧设计得很有意思它把所有内容都转成了ASCII字符传输也就是每个字节对应一个可见字符。这样做的好处是方便调试拿串口助手抓包一眼就能看懂内容。一个标准的读软元件帧长这样STX CMD 起始地址 位数 ETX SUM 0x02 00 0064 0001 0x03 两位ASCII拆开解释STX0x02表示帧开始CMD读命令是00写命令是01注意这个00是ASCII字符的0和0不是字节0x00起始地址4个ASCII字符表示软元件地址的十六进制值位数2个ASCII字符表示读取点数ETX0x03表示帧结束SUM2个ASCII字符从CMD开始到ETX为止所有字节累加后取低八位转换成大写十六进制举个实际例子读M100这一个点的完整发送帧就是02 30 30 30 30 36 34 30 30 30 31 03 43 36这里面0064就是M100的十六进制地址0001表示读1点C6是累加校验值。你可以拿任何串口调试助手手工发这一帧如果PLC侧设置没问题它会回一帧STX开头的数据。2.2 帧尾的SUM校验低八位累加转ASCIISUM校验的计算逻辑很简单把从CMD开始到ETX为止包括ETX的所有字节按ASCII码值累加取累加和的低八位转成两位大写十六进制字符串。C#实现就三五行public static string CalculateSum(string frameBody) { // frameBody 从CMD开始到ETX为止的ASCII字符串 byte[] bytes Encoding.ASCII.GetBytes(frameBody); int sum 0; foreach (byte b in bytes) { sum b; } return (sum 0xFF).ToString(X2); }有个细节要注意累加用的是ASCII编码值不是字符本身代表的数值。比如字符0的ASCII码是48不是0。很多第一次写的人直接把字符串转成16进制字节数组去累加结果算出来的校验和永远对不上。2.3 回帧也分三种情况正常应答、异常应答、无应答PLC收到读命令后正常应答帧是STX 数据区 ETX SUM 0x02 数据 0x03 两位ASCII数据区的内容和你的请求类型有关读位软元件返回的是多个ASCII字符0或1读字软元件返回的是十六进制ASCII字符串。如果请求的地址不存在或者格式错PLC会回异常应答NAK 错误代码 0x15 两位ASCII常见错误码包括02地址越界/软元件不存在、05请求数据长度超限、18帧格式错误等。收到NAK帧时通过错误码去查协议手册能快速定位问题。最难受的是第三种情况——没有任何应答。这通常意味着线没通、参数不匹配或者PLC侧根本没进入MC协议模式。我在第5章会专门讲这类问题的排查顺序。3. 软元件地址计算把M/X/Y/D翻译成协议里的十六进制地址地址计算是整个串口通讯里最容易被忽略、也最容易翻车的环节。起始地址不是你想当然的十进制转十六进制不同软元件类型的转换规则不一样。3.1 位软元件地址M是一次函数X/Y有八进制陷阱M继电器地址计算公式十六进制地址 软元件编号的十进制值比如软元件协议地址M00000M1000064M50001F4这个规则简单就是十进制编号直接转十六进制高位补零到4位。X输入和Y输出的地址计算就有坑了三菱PLC的X/Y编号本身是八进制的也就是说没有X8、X9、X18X0到X7之后直接是X10。协议地址的计算规则相当于先把八进制编号转成十进制数值再转十六进制实际效果是地址步进为8软元件协议地址X00000X70007X100008X17000FX200010软元件协议地址Y00000Y100008Y200010这个陷阱最常见的表现是你按十进制理解X10的地址应该是0x000A但实际应该是0x0008导致读回来的数据整体错位。3.2 字软元件地址与Dword组合规则D寄存器是16位字软元件协议地址计算和M一样十进制编号直接转十六进制软元件协议地址D00000D1000064D100003E8读D寄存器时再读一个D就得到一个16位的值。但PLC程序里真正的32位数据比如Dword、浮点数通常占用连续两个D寄存器。三菱的常见组合规则有两种(D0, D1)表示DwordD1是高16位、D0是低16位。究竟是哪个寄存器作高字完全取决于PLC程序里怎么写。有的老工程师习惯把高字放前有的放后上位机这边必须和PLC程序对好口径。如果你直接读D0和D1去拼DwordC#代码基本是这样// PLC侧D1为高16位D0为低16位 int dwordValue (int)((uint)registers[1] 16 | (uint)registers[0]);但如果你发现读出来的数值和触摸屏上的对不上十有八九是高字和低字搞反了换个顺序再来一次即可。3.3 批量读的长度控制与地址边界问题批量读的本质是告诉PLC从哪个地址开始、连续读多少个点。但不同软元件类型的批量读范围有限制比如M继电器一次请求最多读256点D寄存器一次最多读64个字具体数值不同型号有差异以手册为准。超过上限PLC会直接返回NAK。另一个容易踩的边界问题是M100到M199连续100个点可以一次性读完但M8000到M8060这类特殊功能继电器虽然也在M编号段内内部存放的是系统状态数据读出来有些位是只读的、有些行为还和你预期不符。批量读时如果地址范围跨越到了特殊继电器区最好明确范围不要图省事一口气读一大片。4. C#实现串口管理、帧构造与四种读法4.1 串口初始化参数、超时、打开顺序一个都不能错C#中SerialPort的初始化看着简单但参数错了后面全是坑。和PLC通信的参数必须和PLC侧设置的保持一致。以FX3U-232-BD为例常用的参数组合是波特率9600、数据位8、停止位1、偶校验但三菱设备不少现场实际用的是7位数据位偶校验。用错校验位时串口调试助手能收到乱码或完全无应答。SerialPort port new SerialPort(); port.PortName COM3; port.BaudRate 9600; port.DataBits 7; port.StopBits StopBits.One; port.Parity Parity.Even; port.ReadTimeout 1000; port.WriteTimeout 1000; port.Open();建议在Open之前把ReadTimeout和WriteTimeout都设置好否则默认值是-1无限等待一旦PLC断电或者线断了你的程序会卡死在ReadLine上连线程都没法正常退出。4.2 请求-响应模型并发锁与AutoResetEvent串口是半双工设备不自己维护同一时间只有一个请求在途这个约束的话两条命令一交错回包就全乱了——A命令的响应会被当成B命令的数据。最简单的做法是给每次读写操作加锁保证同一时间只有一个请求在发并且请求发出后立即等待响应private readonly object _commLock new object(); private AutoResetEvent _dataReceivedEvent new AutoResetEvent(false); private Listbyte _buffer new Listbyte(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { int len _port.BytesToRead; byte[] data new byte[len]; _port.Read(data, 0, len); _buffer.AddRange(data); _dataReceivedEvent.Set(); } public byte[] SendCommand(string command) { lock (_commLock) { _buffer.Clear(); _dataReceivedEvent.Reset(); _port.Write(command); if (!_dataReceivedEvent.WaitOne(1000)) { throw new TimeoutException(PLC无应答); } // 这里要做帧完整性解析确认STX/ETX return _buffer.ToArray(); } }为什么用AutoResetEvent而不是直接Sleep等待因为Sleep(100)要么不够、要么太多。PLC响应时间通常在10-50ms用信号量等待可以做到数据一到就继续执行效率高得多。这个模型是整个串口通信稳定性的基石。4.3 四种读取需求的实际代码bool、批量bool、Word、Dword把前面协议的各个部分组合起来一个通用的读软元件方法可以这样写public string BuildReadFrame(string cmd, string addressHex, string pointsHex) { string body cmd addressHex pointsHex; string frame \x02 body \x03 CalculateSum(body \x03); return frame; }读单个bool比如M100string frame BuildReadFrame(00, 0064, 0001); byte[] resp SendCommand(frame); // resp内容: 02 0/1 03 C5 bool value Encoding.ASCII.GetString(resp, 1, 1) 1;读批量bool比如M100到M103共4个点string frame BuildReadFrame(00, 0064, 0004); byte[] resp SendCommand(frame); string data Encoding.ASCII.GetString(resp, 1, 4); // data形如 1001从左到右对应M100~M103 bool[] bits data.Select(c c 1).ToArray();读Word比如D100的16位值string frame BuildReadFrame(00, 0064, 0001); byte[] resp SendCommand(frame); string hex Encoding.ASCII.GetString(resp, 1, 4); ushort value Convert.ToUInt16(hex, 16);读Dword假设PLC侧是D100低字 D101高字string frame BuildReadFrame(00, 0064, 0002); byte[] resp SendCommand(frame); string hex Encoding.ASCII.GetString(resp, 1, 8); uint raw Convert.ToUInt32(hex, 16); // 注意MC协议返回的字节顺序是高位在前直接转UInt32得到的就是 // 高16位在前的数值如果PLC侧D100是低字需要交换 int low (int)(raw 0xFFFF); int high (int)((raw 16) 0xFFFF); int dwordValue high 16 | low;解析Dword这块有个非常容易出错的地方Convert.ToUInt32(hex, 16)默认把字符串当大端解析——字符串最左边的字符是最高位。但PLC里D100和D101谁高谁低取决于PLC程序一定要先在触摸屏或编程软件里确认再定代码里的组合方式。4.4 心跳信号不是定时读一把那么简单心跳信号的作用是双向的。上位机需要通过心跳知道PLC还活着PLC程序也需要知道上位机是否还在正常通信——特别是在自动化设备场景如果上位机崩溃了PLC还在自动运行可能会出安全事故。我的建议是把心跳拆成两层链路层心跳上位机定时比如每2秒读取一个固定寄存器比如D200。能读到正常值说明串口链路和PLC通讯模块没问题。应用层心跳上位机周期性把递增计数写入D201PLC程序里用一个比较指令检查D201的值是否持续变化超过N秒没有变化就判定上位机掉线执行安全停机。第二层往往被很多人忽略。只做读不做写PLC侧永远不知道上位机状态。写心跳的代码很简单public void HeartbeatTick(int seq) { string addressHex 00C9; // D201 string dataHex seq.ToString(X4); string body 01 addressHex 0001 dataHex; string frame \x02 body \x03 CalculateSum(body \x03); SendCommand(frame); }用Timer或者后台线程定时调用这个方法即可。注意写心跳的周期不要太短一般1-3秒比较合适太频繁会让串口长期占用其它指令排队等待反而显得系统卡顿。5. 联调过程遇到的典型坑与排查思路代码写完之后真正联调才是噩梦的开始。这里分享一下我在现场踩过、也帮别人解决过的几个高频问题。5.1 死活收不到回包先查线再查D8120最后查协议类型一次调试中上位机发出去的命令用手抓包确认是对的但PLC就是没反应。排查顺序我建议固定为三条第一步看线。串口线有没有交叉232口的TXD、RXD有没有接反RS422的T、T-、R、R-四条线是否一一对应这是最高发的问题。用万用表量通断不要靠肉眼看颜色。第二步看PLC侧通讯格式寄存器D8120。这个寄存器的位配置直接决定了PLC的串口通讯格式比如波特率、数据位、校验位。如果上位机用9600 8E1而D8120里配的是19200 7E1那么PLC根本不会正确接收帧头。用三菱编程软件在线监视D8120的值和上位机参数逐位核对。第三步看协议模式。D8120配置正确仍然没反应那就要回到1.2节说的问题——确认当前串口是否真的运行在MC协议模式。FX3U编程口如果没有修改PLC参数里的协议设置默认可能是编程口协议。这种情况下你发的MC协议帧它当然看不懂。5.2 回包解析出来数值对不上字节序与ASCII转换有次读D100串口调试助手显示回包是STX 31 32 33 34 ETX按ASCII翻译是1234这意味着D100的十六进制值是0x1234十进制4660。如果直接按Convert.ToInt16(1234)去解析得到的是1234数值当然就对不上。必须用Convert.ToUInt16(1234, 16)以16进制解析。还有一次读32位数据触摸屏上显示100000上位机读出来却是很大的奇怪数字。最后查到PLC程序里D0和D1的组合方式跟我想的相反高字低字一交换数值立刻对上了。遇到这种问题最快的方法是在PLC里写一个已知的测试值比如D00x0001、D10x0002然后看上位机读出来两个寄存器各自的值马上就能推断出字节顺序。5.3 地址边界和八进制软元件的隐蔽坑有朋友问过为什么读X20到X27地址从0x0010开始连续读8个点结果读回来的位和实际PLC输入端子对不上。因为X20到X27对应的协议地址是0x0010到0x0017这没错但如果你为了省事从X17开始读8个点那么X17的地址是0x000FX20的地址是0x0010——连续读8个点会把X17也包括进来而X17和X20之间物理上并不是连续的端子排列数据解释自然就歪了。X/Y这类八进制软元件批量读的起始地址和点数必须算准最好一组一组按八进制区块来读。D寄存器批量读也有个隐蔽问题如果PLC程序动态修改了D寄存器地址映射比如用了变址寄存器Z你在上位机读到的地址可能不是物理D而是变址后的D。这种情况只能和PLC程序开发人员确认没有通用解法。5.4 用串口调试助手定位问题比自己盲猜快串口调试助手不是只能用来调试串口硬件它最适合用来做上位机和PLC之间的裁判。方法很简单上位机里把发送的命令用日志记下来复制到串口调试助手里手动发送一遍。如果PLC回了正确应答说明上位机代码的帧构造逻辑没问题问题出在代码里的串口参数、发送时机或数据处理上。如果手动发送也没回应那就直奔线缆和PLC设置去查。反过来也一样PLC侧如果配了通讯监视工具可以直接看到它收到了什么、回了什么。没有的话用一个USB转串口模块并联在PLC通讯线上抓包看实际线上的数据流很多时候问题一下就清楚了。这套流程帮我省了很多时间也帮了不少网友。串口这东西看着老但用对了就是稳定可靠。写程序写累了拿串口助手抓两帧数据看看比盯着代码素猜高效太多。本文还有配套的精品资源点击获取
返回列表