
1. SLMP协议与三菱FX5U PLC基础认知第一次接触三菱FX5U PLC的SLMP协议时我对着手册研究了整整三天。这个看似复杂的协议本质上就是PLC和上位机之间的对话规则。想象一下如果你要和外国朋友交流要么学会对方的语言要么找个翻译——SLMP协议就是这个翻译。SLMPSeamless Message Protocol协议最大的特点是基于标准以太网这意味着我们可以直接用网线连接PLC和电脑不再需要专门的编程电缆。FX5U系列PLC内置的以太网端口支持10/100Mbps通信实测下来ping值能稳定在1ms以内比传统的串口通信快了一个数量级。协议底层采用TCP/IP传输但三菱对数据包格式做了特殊规定。每个数据包都包含副头部固定为0x5000网络/站号信息通常默认FF03指令代码读写操作用0401/1401软元件地址如D100、M800等数据内容写入值或读取结果举个例子当我们要读取D100开始的10个寄存器时实际发送的报文是这样的50 00 FF FF 03 00 0C 00 10 00 01 04 00 00 64 00 A8 0A 00其中64 00对应D100的地址十六进制0x0064A8是数据寄存器的类型码0A 00表示读取10个点。2. 开发环境搭建与PLC配置记得第一次配置FX5U时我在GX Works3里迷路了半小时。现在我把关键步骤整理成清单开发工具准备Visual Studio 2019社区版即可.NET Framework 4.6.1兼容性最好GX Works3 1.070Y需与PLC固件版本匹配PLC网络配置关键步骤在GX Works3中打开参数-FX5UCPU-模块参数-以太网端口IP地址设置建议设为静态IP如192.168.1.10应用设置中启用SLMP服务端口号默认为4999可自定义对象设备连接配置里添加SLMP设备点击反映设置并关闭后写入PLC常见坑点修改网络参数后必须断电重启PLC防火墙需放行指定端口实测关闭防火墙最省事如果使用VPN或特殊网络环境需要禁用其他网络适配器这里有个小技巧先用ping命令测试基础连通性再用Wireshark抓包看是否收到PLC响应。我曾遇到PLC能ping通但无法通信的情况最后发现是杀毒软件拦截了端口。3. C#核心类封装实战3.1 报文构造器实现报文构造是协议封装中最繁琐的部分。我设计了一个MessageBuilder类用枚举类型提升代码可读性public enum SoftDeviceType : byte { X 0x9C, // 输入继电器 Y 0x9D, // 输出继电器 M 0x90, // 内部继电器 D 0xA8 // 数据寄存器 // 其他类型见手册... } public class MessageBuilder { public byte[] BuildReadCommand(SoftDeviceType type, ushort address, ushort count) { var stream new MemoryStream(); using (var writer new BinaryWriter(stream)) { // 副头部 writer.Write(new byte[] { 0x50, 0x00 }); // 网络/站号默认值 writer.Write(new byte[] { 0xFF, 0xFF, 0x03, 0x00 }); // 请求数据长度先占位 writer.Write((ushort)0); // CPU监视定时器默认1000ms writer.Write((ushort)0x1000); // 指令代码0401表示批量读 writer.Write((ushort)0x0104); // 子指令0000表示按字读取 writer.Write((ushort)0x0000); // 起始地址3字节低位在前 writer.Write((byte)(address 0xFF)); writer.Write((byte)((address 8) 0xFF)); writer.Write((byte)0x00); // 软元件类型 writer.Write((byte)type); // 读取点数 writer.Write(count); // 回填请求数据长度 var length stream.Length - 9; stream.Position 2; writer.Write((ushort)length); } return stream.ToArray(); } }3.2 Socket通信管理TCP通信最怕的就是连接不稳定。我的解决方案是加入自动重连机制public class PlcSocket : IDisposable { private TcpClient _client; private NetworkStream _stream; private readonly string _ip; private readonly int _port; private readonly int _timeout; public PlcSocket(string ip, int port, int timeout 2000) { _ip ip; _port port; _timeout timeout; } public async Task ConnectAsync() { if (_client?.Connected true) return; _client new TcpClient(); var task _client.ConnectAsync(_ip, _port); if (await Task.WhenAny(task, Task.Delay(_timeout)) task) { _stream _client.GetStream(); } else { throw new TimeoutException(PLC连接超时); } } public async Taskbyte[] SendAndReceiveAsync(byte[] data) { await ConnectAsync(); try { // 发送数据 await _stream.WriteAsync(data, 0, data.Length); // 先读取11字节的响应头 var header new byte[11]; await _stream.ReadAsync(header, 0, header.Length); // 检查错误码第9-10字节 if (header[9] ! 0 || header[10] ! 0) throw new PlcException($PLC返回错误码: {header[9]:X2}{header[10]:X2}); // 读取剩余数据 var dataLen BitConverter.ToUInt16(header, 7); var buffer new byte[dataLen]; await _stream.ReadAsync(buffer, 0, buffer.Length); return buffer; } catch (IOException ex) { // 发生异常时重置连接 Dispose(); throw new PlcException(通信中断, ex); } } public void Dispose() { _stream?.Close(); _client?.Close(); } }3.3 数据转换处理PLC返回的数据需要按类型解析这里用到了C#的BitConverter类public static class DataConverter { public static short ToInt16(byte[] data, int offset) { // PLC数据是低位在前 return BitConverter.ToInt16(new byte[] { data[offset 1], data[offset] }, 0); } public static float ToFloat(byte[] data, int offset) { // 浮点数需要先转成两个short var low ToInt16(data, offset); var high ToInt16(data, offset 2); return BitConverter.ToSingle(BitConverter.GetBytes( ((uint)high 16) | (ushort)low), 0); } public static bool ToBool(byte[] data, int offset, byte mask) { return (data[offset] mask) ! 0; } }4. 高级功能实现技巧4.1 批量读写优化直接循环读写单个寄存器效率太低。通过分析协议手册我发现可以一次性读写多个连续地址public async Taskfloat[] ReadFloatArrayAsync(string device, int start, int count) { var type GetDeviceType(device); // 解析D/M/X等前缀 var address ushort.Parse(device.Substring(1)); var cmd _msgBuilder.BuildReadCommand(type, address, (ushort)(count * 2)); var response await _socket.SendAndReceiveAsync(cmd); var result new float[count]; for (int i 0; i count; i) { result[i] DataConverter.ToFloat(response, i * 4); } return result; }实测对比读取100个浮点数单次读取耗时约1200ms批量读取仅需80ms4.2 异常处理机制工业现场最怕程序崩溃。我设计了三级错误处理通信层网络超时、连接中断等协议层PLC返回的错误代码应用层数据范围校验try { var values await plc.ReadFloatArrayAsync(D100, 10); } catch (PlcException ex) when (ex.ErrorCode 0401) { // 处理特定错误码 Logger.Error($设备地址错误{ex.Message}); } catch (TimeoutException) { // 重试逻辑 await Task.Delay(1000); await ReconnectAsync(); } catch (Exception ex) { // 全局捕获 Logger.Fatal(ex); Environment.FailFast(PLC通信致命错误); }5. 实战Demo与性能调优5.1 监控界面开发用WPF实现的简易监控界面Window x:ClassPlcMonitor.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml TitleFX5U监控 Height450 Width800 Grid DataGrid x:NameDataGrid AutoGenerateColumnsFalse DataGrid.Columns DataGridTextColumn Header地址 Binding{Binding Address}/ DataGridTextColumn Header值 Binding{Binding Value}/ DataGridTextColumn Header更新时间 Binding{Binding Timestamp}/ /DataGrid.Columns /DataGrid /Grid /Window后台用定时器实现轮询private readonly System.Timers.Timer _timer; private readonly PlcService _plc new PlcService(192.168.1.10); public MainWindow() { InitializeComponent(); _timer new System.Timers.Timer(100); _timer.Elapsed async (s, e) { var data await _plc.ReadDeviceBlockAsync(D100, 10); Dispatcher.Invoke(() DataGrid.ItemsSource data); }; _timer.Start(); }5.2 性能优化记录通过多次测试发现的优化点Socket复用保持长连接比频繁开关快3倍缓冲区管理预分配字节数组减少GC压力异步处理用async/await避免UI卡顿批量操作合并读写请求减少网络往返优化前后对比读取1000点数据优化项耗时(ms)CPU占用原始版本120025%长连接80018%批量读取1505%内存优化1203%6. 常见问题解决方案Q1连接PLC时出现超时错误检查网线是否插好确认PLC IP与PC在同一网段关闭防火墙测试在GX Works3中确认SLMP服务已启用Q2读取的数据值不正确确认软元件类型代码正确如D寄存器是0xA8检查字节顺序三菱用低位在前浮点数需要特殊处理见4.1节Q3通信偶尔中断增加心跳包机制每5秒读取PLC时间实现自动重连如代码3.2所示检查网络设备交换机会不会丢包Q4如何提高通信速度增大单次读取点数最大960字/次使用UDP协议需PLC支持升级到千兆网络FX5U只支持百兆有次在现场遇到PLC能ping通但无法通信的问题后来发现是IP地址冲突。建议在程序中加入ARP检测public static bool CheckIpConflict(string ip) { var process new Process { StartInfo new ProcessStartInfo { FileName arp, Arguments $-a {ip}, RedirectStandardOutput true, UseShellExecute false } }; process.Start(); var output process.StandardOutput.ReadToEnd(); return output.Contains(冲突); }7. 扩展应用场景这套通信库不仅能用于监控还能实现配方管理public async Task DownloadRecipeAsync(int recipeNo) { var data await ReadFloatArrayAsync(D1000, 100); var recipe JsonConvert.DeserializeObjectRecipe(data); // 应用到本地配置... }报警历史记录public async TaskListAlarm GetAlarmsAsync() { var alarms new ListAlarm(); for (int i 0; i 16; i) { if (await ReadBoolAsync($M{i100})) { alarms.Add(new Alarm { Code await ReadInt16Async($D{i*2200}), Time DateTime.Now }); } } return alarms; }与数据库集成public void StartDataLogging() { _timer new Timer(1000); _timer.Elapsed async (s,e) { var values await ReadDeviceBlockAsync(D500, 10); _db.Insert(PLC_DATA, values); }; }8. 最终封装建议经过多个项目验证我总结出最佳实践分层设计底层Socket通信3.2节中间层协议解析3.1节应用层业务功能7.1节配置化{ PlcConfig: { Ip: 192.168.1.10, Port: 4999, PollingInterval: 100, Devices: [ { Name: Temperature, Address: D100, Type: Float } ] } }日志记录public class PlcLogger { public static void LogCommunication(byte[] send, byte[] recv) { File.AppendAllText(comm.log, $[{DateTime.Now:HH:mm:ss.fff}] Send: {BitConverter.ToString(send)}\n $Recv: {BitConverter.ToString(recv)}\n\n); } }把所有这些经验整合起来最终可以打包成一个NuGet库方便团队复用。我在实际项目中用这套方案实现了200台设备的集中监控稳定运行超过2年日均通信量超百万次。