C#通过UDP基于FINS协议读取欧姆龙PLC DM区的实现方法
C# 基于FINS UDP协议读取欧姆龙PLC DM存储区实现方案
核心问题修正
你现有代码存在两个关键错误,必须先调整才能正常通信:
- 本地
UdpClient不能绑定9600端口,9600是PLC侧FINS UDP服务的默认监听端口,本地客户端应绑定系统分配的闲置端口,否则会出现端口冲突导致通信失败 - 不能用
Encoding.UTF8.GetString解析接收数据,FINS是二进制协议,所有帧都是字节流结构,文本解码会直接损坏数据
FINS UDP读DM区帧规则
读取DM区使用FINS标准的内存区读命令(命令码0x0101),帧结构固定:
- DM区对应区域码为
0x02,单通道长度2字节(16位无符号整数),地址采用3字节大端编码 - 本地直连场景下网络号固定填0,节点号取IP地址最后一段,比如PLC IP为
192.168.1.10则PLC节点号为0x0A,PC IP为192.168.1.20则本地节点号为0x14 - 单次读取通道数建议不超过500,避免超过PLC单次通信长度限制
完整读请求帧字节定义:
| 字节位置 | 内容说明 | 固定值/规则 |
|---|---|---|
| 0-3 | FINS固定标识 | 0x46 0x49 0x4E 0x53(ASCII字符FINS) |
| 4-9 | 帧头控制字段 | 固定填0x00 0x00 0x00 0x0C 0x00 0x00 |
| 10-16 | 寻址字段 | 依次填0x00、PLC节点号、0x00、0x00、本地节点号、0x00、自增SID(请求序号,应答会原样返回用于匹配) |
| 17-18 | 命令码 | 内存读固定为0x01 0x01 |
| 19 | 区域码 | DM区固定为0x02 |
| 20-22 | 起始地址 | 3字节大端格式,按字寻址时最后一位填0,比如读DM100则填0x00 0x64 0x00 |
| 23-24 | 读取长度 | 2字节大端格式,比如读10个通道填0x00 0x0A |
可直接复用的实现代码
代码需要引用System.Linq命名空间做字节序转换,WPF/WinForm均可直接使用:
using System; using System.Net; using System.Net.Sockets; using System.Collections.Generic; using System.Linq; namespace OMRON_FINS { public class FinsUdpClient { private readonly UdpClient _udpClient; private readonly IPEndPoint _plcEndPoint; private readonly byte _localNode; private readonly byte _plcNode; private byte _sid = 0; public FinsUdpClient(string plcIp, int plcPort = 9600) { _plcEndPoint = new IPEndPoint(IPAddress.Parse(plcIp), plcPort); // 取本机/PLC IP最后一段作为FINS节点号 var localIp = GetLocalIPAddress(); _localNode = byte.Parse(localIp.Split('.')[3]); _plcNode = byte.Parse(plcIp.Split('.')[3]); // 绑定本地任意闲置端口 _udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); _udpClient.Client.ReceiveTimeout = 2000; } public static string GetLocalIPAddress() { var host = Dns.GetHostEntry(Dns.GetHostName()); foreach (var ip in host.AddressList) { if (ip.AddressFamily == AddressFamily.InterNetwork) { return ip.ToString(); } } throw new Exception("本机无可用IPv4地址"); } /// <summary> /// 读取DM区连续通道值 /// </summary> /// <param name="startDm">起始DM地址,读DM100传100即可</param> /// <param name="length">读取通道数量</param> /// <returns>按地址顺序返回的16位无符号值数组</returns> public ushort[] ReadDmArea(int startDm, int length) { if (length < 1 || length > 500) throw new ArgumentException("单次读取长度范围为1-500"); _sid++; List<byte> frame = new List<byte>(); // 组装固定头 frame.AddRange(new byte[] {0x46,0x49,0x4E,0x53,0x00,0x00,0x00,0x0C,0x00,0x00}); // 组装寻址段 frame.AddRange(new byte[] {0x00, _plcNode, 0x00, 0x00, _localNode, 0x00, _sid}); // 组装读命令 frame.AddRange(new byte[] {0x01, 0x01, 0x02}); // 组装起始地址(C#默认小端,需转大端) frame.AddRange(BitConverter.GetBytes((ushort)startDm).Reverse()); frame.Add(0x00); // 组装读取长度 frame.AddRange(BitConverter.GetBytes((ushort)length).Reverse()); // 发送请求并接收应答 _udpClient.Send(frame.ToArray(), frame.Count, _plcEndPoint); byte[] recvBuffer = _udpClient.Receive(ref _plcEndPoint); // 校验应答合法性 if (recvBuffer.Length < 23 + length * 2) throw new Exception("应答帧长度非法"); if (recvBuffer[19] != _sid) throw new Exception("应答SID不匹配"); ushort endCode = BitConverter.ToUInt16(recvBuffer.Skip(21).Take(2).Reverse().ToArray(),0); if (endCode != 0) throw new Exception($"FINS返回错误码:{endCode:X4}"); // 解析通道数据 List<ushort> result = new List<ushort>(); for (int i = 23; i < 23 + length*2; i+=2) { result.Add(BitConverter.ToUInt16(recvBuffer.Skip(i).Take(2).Reverse().ToArray(),0)); } return result.ToArray(); } } }
开发注意事项
- WPF应用中不要在UI线程直接调用读取方法,需用
Task.Run放到后台线程执行,读取完成后通过Dispatcher更新UI做可视化绑定,避免界面卡顿 - 实时采集建议间隔不小于100ms,过高的请求频率会导致PLC通信负载过高
- 如果需要解析32位整数、单精度浮点数,将连续2个/4个DM通道的字节按大端顺序拼接后转换即可,欧姆龙默认使用大端字节序
- 跨网段通信时需要将代码中网络号字段替换为PLC路由配置中对应的实际值,本地直连场景填0即可
内容的提问来源于stack exchange,提问作者H.Daniel
相关产品推荐
相关产品推荐

