简介:面向工业自动化方向开发者与上位机编程学习者的C#实战项目源码,聚焦三菱FX5U PLC的以太网通信实现,可服务于自动化立体仓库等场景下的数据交互与控制需求。资源围绕3E帧报文结构展开,涵盖连接建立、报文构造、数据传输、协议解析、错误重试机制及WinForm界面设计等关键环节,适合具备一定C#基础、希望打通PLC通信链路的读者参考。压缩包共35个文件、约79KB,以cs源码为主体,辅以csproj、sln解决方案文件、config配置、resx与resources资源及exe可执行文件,构成可直接打开研究的完整工程结构。已有468人学习,便于对照理解报文封装与业务逻辑的组织方式,快速积累工业通信程序的排错与架构经验。
1. 三菱FX5U以太网通信包:一份能直接跑通的上位机通信底稿
手里有个三菱FX5U的PLC,上位机要用C#写WinForm去读写D/M/X/Y这些软元件,还要把报文抓出来核对——这事听起来简单,真动手时最耗时间的往往不是业务逻辑,而是通信层那几百行“到底该发什么字节”的摸索。这份名为“三菱FX5U上位机以太网通信.rar”的资源,核心价值就在这儿:它把FX5U的MC协议(MELSEC Communication Protocol)以太网通信封装成了一套可复用的C#代码,配套一个WinForm界面,能直接连PLC、发读写指令、看返回报文。适合两类人:一是做自动化立体仓库、产线设备上位机,需要跟FX5U对接的C#开发者;二是想搞明白MC协议报文结构、不想每次靠抓包猜字节的工程师。下面按“协议怎么立住、代码怎么跑、坑在哪”的顺序拆开讲。
2. MC协议报文拆解:3E帧的二进制结构与软元件寻址
2.1 为什么是3E帧而不是1E帧
FX5U走以太网做上位机通信,主流就两条路:一是MC协议,二是SLMP(Seamless Message Protocol,本质是MC协议的扩展)。MC协议里又分1E帧、3E帧、4E帧几种格式。1E帧是早期Q系列常用的,帧头短、不带网络号站号这些路由信息;3E帧在帧头里加了网络号、PLC号、请求目标模块IO号、请求目标模块站号,寻址能力更强,也是FX5U以太网通信里最常见的选择。这份资源用的就是3E帧二进制格式,原因很实际:FX5U的以太网口默认就支持3E帧,且二进制格式比ASCII格式报文短、解析快,读写批量软元件时优势明显。
3E帧的请求报文结构大致是这么几段:副帧头(5000)、网络号(00)、PLC号(FF)、请求目标模块IO号(03FF)、请求目标模块站号(00)、请求数据长度(2字节)、CPU监视定时器(2字节)、命令(2字节)、子命令(2字节)、请求数据区。响应报文则是副帧头(D000)、网络号、PLC号、IO号、站号、响应数据长度、结束代码、响应数据。这里最容易翻车的是“请求数据长度”这个字段——它算的是从CPU监视定时器开始到请求数据区结束的字节数,不含前面那9个字节的帧头。很多人第一次写,把整个报文长度填进去,PLC直接返回错误码或者干脆不回,抓包看半天以为是网线问题。
2.2 软元件寻址:D、M、X、Y的地址编码规则
MC协议里读写软元件,请求数据区要带三样东西:软元件代码(1字节)、起始软元件号(3字节,小端序)、软元件点数(2字节)。软元件代码是固定的:D寄存器是A8、M内部继电器是90、X输入是9C、Y输出是9D、T定时器当前值是C2、C计数器当前值是C5。起始软元件号要按十进制转成3字节小端,比如D100,就是0x64,写成64 00 00;M200是0xC8,写成C8 00 00。点数按2字节小端,读10个点就是0A 00。
这里有个血泪经验:X和Y的地址是十六进制表示的,但很多人从GX Works里看到的是八进制或者十进制显示,直接照抄就错位。比如X10,在MC协议里要按十六进制0x10处理,写成10 00 00,而不是十进制的0A。这个细节在资源代码里是用一个软元件地址转换方法处理的,把界面输入的“D100”“M200”“X10”这类字符串解析成协议需要的字节序列,省得每次手算。
2.3 批量读写与随机读写的取舍
MC协议支持批量读(命令0401)、批量写(命令1401)、随机读(命令0403)、随机写(命令1402)。批量读适合连续地址,比如D100到D109一次读10个,一个报文搞定;随机读适合地址不连续的场景,比如同时要D100、D200、M50,可以在一个报文里带多个软元件指定。资源里两种都实现了,实际用的时候有个判断:连续地址超过3个就用批量读,零散地址用随机读,能少发几个报文。自动化立体仓库那种场景,货位状态往往是一段连续的D寄存器,批量读更合适;而设备报警位散落在M区各处,随机读更省事。
3. C#通信层落地:从Socket连接到报文收发
3.1 连接管理与超时设置
FX5U的以太网通信是TCP长连接,默认端口号是5007(MC协议二进制)。资源里的连接管理用一个TcpClient封装,连接时设置SendTimeout和ReceiveTimeout,这两个值别设太小——PLC扫描周期加上网络抖动,500ms以下容易误判超时。我一般设2000ms,配合重试机制。代码里连接方法大致是这样:
// 建立与FX5U的TCP连接,端口5007为MC协议二进制默认端口 public bool Connect(string ip, int port = 5007, int timeoutMs = 2000) { try { _client = new TcpClient(); // 连接超时用异步等待控制,避免界面卡死 var task = _client.ConnectAsync(ip, port); if (!task.Wait(timeoutMs)) { _client.Close(); return false; } _stream = _client.GetStream(); _stream.ReadTimeout = timeoutMs; _stream.WriteTimeout = timeoutMs; return true; } catch (Exception ex) { // 记录异常,上层界面提示连接失败原因 LastError = ex.Message; return false; } }这段的关键在超时控制:TcpClient的Connect本身没有超时参数,直接调会在网络不通时卡很久,用ConnectAsync加Wait是常见做法。参数上port默认5007,如果PLC侧改过端口要对应改;timeoutMs给2000是经验值,现场网络差可以放到3000。注意连接成功后要把NetworkStream的读写超时也设上,否则读响应时可能一直阻塞。
3.2 请求报文组装与响应解析
报文组装是这份资源的核心。以批量读D寄存器为例,组装方法要把帧头、长度、命令、软元件指定逐段拼进去:
// 组装3E帧批量读请求,deviceCode如"D100",count为读取点数 public byte[] BuildBatchReadRequest(string deviceCode, ushort count) { var device = ParseDevice(deviceCode); // 解析出软元件代码和起始号 var data = new List<byte>(); // 请求数据区:软元件代码1字节 + 起始号3字节小端 + 点数2字节小端 data.Add(device.Code); data.AddRange(BitConverter.GetBytes(device.StartAddress).Take(3)); data.AddRange(BitConverter.GetBytes(count)); var frame = new List<byte>(); frame.AddRange(new byte[] { 0x50, 0x00 }); // 副帧头 frame.Add(0x00); // 网络号 frame.Add(0xFF); // PLC号 frame.AddRange(new byte[] { 0xFF, 0x03 }); // 请求目标模块IO号 03FF frame.Add(0x00); // 请求目标模块站号 // 请求数据长度 = CPU监视定时器(2) + 命令(2) + 子命令(2) + 请求数据区 ushort dataLen = (ushort)(2 + 2 + 2 + data.Count); frame.AddRange(BitConverter.GetBytes(dataLen)); frame.AddRange(new byte[] { 0x10, 0x00 }); // CPU监视定时器,单位250ms frame.AddRange(new byte[] { 0x01, 0x04 }); // 命令0401,小端 frame.AddRange(new byte[] { 0x00, 0x00 }); // 子命令0000,按字单位 frame.AddRange(data); return frame.ToArray(); }逻辑说明:副帧头5000是请求固定值,响应是D000;IO号03FF是FX5U以太网模块的固定值,站号00表示直接连CPU;CPU监视定时器1000表示250ms×16=4秒,PLC在这个时间内没收到后续报文会断开,设太小在慢网络下会掉线。响应解析时先读9字节帧头,取响应数据长度,再读剩余部分,最后2字节是结束代码,0000表示成功,非零要查错误码表——比如C059是软元件指定错误,往往就是地址编码写错了。
3.3 WinForm界面与通信层的解耦
资源里的WinForm界面没有把通信逻辑塞进按钮事件里,而是分了一层:界面只负责收集IP、端口、软元件地址、读写类型这些参数,调用通信类的Read/Write方法,拿返回结果刷新DataGridView。这种分法在实际项目里很重要——自动化立体仓库的上位机往往要同时连多台PLC,通信层做成独立类才能复用。界面上的报文显示区把发送和接收的字节按十六进制打印出来,调试时对着看,哪个字节不对一目了然。我一般还会在报文显示里加时间戳,排查间歇性通信失败时能对上PLC侧的日志。
4. 避坑与排查:5个现场高频翻车点
4.1 现象:连接成功但读写全部超时
原因:FX5U的以太网参数里,对象设备连接配置没给上位机开权限,或者端口号跟代码里不一致。FX5U默认的MC协议端口是5007,但如果在GX Works3的以太网端口设置里改过,代码里还写5007就连不上应用层。另外“对象设备连接配置设置”里要添加一个“SLMP连接设备”,分配好IP和端口,没配的话TCP能连上但MC报文不响应。
解决:先在GX Works3里确认以太网端口设置和对象设备连接配置,把上位机IP加进去;用网络调试工具发一条最简单的批量读报文,看有没有D000响应,排除代码问题。
4.2 现象:读回来的数据全是0或者明显错位
原因:软元件起始地址的字节序搞反了。3E帧里起始软元件号是3字节小端,有人按大端写,D100写成00 00 64,PLC解析成另一个地址,读回来自然是错的。X/Y地址的进制问题也归这一类。
解决:抓包对照请求报文,确认起始地址三字节是不是低位在前;X/Y地址统一按十六进制处理,界面输入时加个校验提示。
4.3 现象:批量读点数多了就报错
原因:请求数据长度字段算错,或者单次读取点数超过PLC允许上限。FX5U单次批量读的字数上限跟具体型号和协议有关,常见是960字左右,超了返回错误码。另外请求数据长度如果按整个报文长度填,PLC直接丢弃。
解决:请求数据长度只算CPU监视定时器到请求数据区结束;大批量读取拆成多次,每次不超过500字,稳一点。
4.4 现象:通信跑一段时间后突然断开
原因:CPU监视定时器设得太小,或者上位机长时间不发报文,PLC主动断开连接。也有人是没处理响应,发一条读一条,中间界面卡住导致超时。
解决:CPU监视定时器给1000(4秒)以上;通信层加心跳,空闲时定期读一个固定软元件保活;接收响应要完整读完,别读一半就丢。
4.5 现象:多线程同时读写时数据串包
原因:一个TcpClient连接上多个线程同时发请求,响应回来分不清是谁的。MC协议本身是请求-响应严格配对的,并发发多条,响应顺序可能错乱。
解决:通信层加锁,同一连接串行收发;或者每台PLC一个连接、一个收发队列。资源里的做法是加lock,简单但够用,高并发场景再考虑连接池。
5. 进阶:把通信层做成可复用的设备驱动
5.1 抽象出统一的读写接口
这份资源的代码直接能用,但要接到实际项目里,建议再抽一层。定义一个IPlcDevice接口,把Connect、ReadWords、WriteWords、ReadBits、WriteBits这几个方法定出来,FX5U的实现类去实现它。这样以后换Q系列、换其他品牌走MC协议的设备,上层业务代码不用动。接口方法签名大致是:
public interface IPlcDevice { bool Connect(string ip, int port); void Disconnect(); short[] ReadWords(string startDevice, ushort count); // 读D/M等字软元件 bool[] ReadBits(string startDevice, ushort count); // 读X/Y/M位软元件 bool WriteWords(string startDevice, short[] values); bool WriteBits(string startDevice, bool[] values); }参数上注意:ReadWords返回short数组,因为D寄存器是16位有符号;ReadBits返回bool数组,X/Y/M按位读时子命令要用0001而不是0000。这个区别在资源代码里是分开处理的,抽接口时别合并成一个方法。
5.2 报文日志与错误码映射
现场排查最有用的是报文日志。建议在通信层加一个日志回调,把每次发送和接收的十六进制报文、时间戳、耗时都记下来,写到本地文件按天滚动。错误码做一个字典映射,把C050、C051、C059、C05B这些常见码翻译成中文说明,界面上直接显示“软元件指定错误”比显示“C059”有用得多。我一般还会在日志里记上当前操作的软元件地址和点数,出问题时能直接定位是哪条指令。
5.3 验证方法:用模拟报文先跑通解析
没有PLC在手边时,可以先写个单元测试,用固定的响应字节数组喂给解析方法,验证解析结果对不对。比如构造一个D000开头的响应,数据区放10个字,看解析出来的short数组是不是预期值。这样通信层改代码时不怕改坏,也方便新人理解报文结构。等有PLC了,再用真实连接跑一遍读写,两边对得上就稳了。
从那以后我每次接新的PLC通信项目,都强制先把报文日志和错误码映射做进去,再写业务逻辑——现场排查时这两样东西就是后悔药,省下的时间远比前期多花的那点功夫值。希望帮到你。
本文还有配套的精品资源,点击获取