
简介本资源是一套面向工业自动化开发者的C#与欧姆龙PLC通信实战方案聚焦FINS协议在TCP/IP网络下的底层实现适用于具备基础C#编程能力及初步工控知识的工程师、自动化专业学生和PLC集成项目开发者。资源包共45个文件涵盖9个核心C#源码文件含完整连接、读写寄存器逻辑、8张原理图与界面截图如网络模型、协议格式、fins通讯流程、4个可执行测试工具含NetAssist.exe、1份《通讯手册.pdf》及1份教学课件文档辅以配置文件、资源文件和Visual Studio解决方案.sln/.csproj结构完整便于工程复用与调试验证。目前已有1118人学习下载。读者可直接获取可运行的FINS通信框架、协议帧构造与解析示例、PLC地址映射对照参考以及基于真实场景的异常处理与调试思路显著降低从理论到产线对接的实践门槛。1. 为什么用 C# 直连欧姆龙 PLC 不选 OPC 而选 FINS——一个被低估的工业现场落地选择很多同学做毕设或课设时一提“PLC 通信”下意识就去搜 OPC UA、Kepware 或 Modbus TCP结果卡在授权、配置、防火墙、证书链上整整两周。而真实产线里某高校自动化实验室做的模拟项目X要求在无第三方中间件、不装任何 OPC 服务器、仅靠一台工控机直连欧姆龙 CP1E-E30DR-A 的前提下5 分钟内读出 32 个 DI 状态 16 个 AI 值并写入 8 个 DO 控制位——最后他们用纯 C# 实现了核心就是 FINS 协议。这不是炫技而是工业现场最常遇到的“轻量级直连”场景没有 IT 部门配 OPC 服务没有网络策略放开 4840 端口甚至 PLC 连的是老旧的 CX-Programmer 工程网段默认只开 9600 端口。FINS 是欧姆龙原生协议走 UDP/TCP 封包极简C# 的 Socket BinaryReader 组合就能稳压 20ms 周期轮询比加一层 OPC UA SDK 更可控、更易调试、答辩时也更容易讲清字节流走向。这份资源不是教你怎么配 Kepware而是给你一套可编译、可断点、可改地址、可进产线实测的 C# FINS 实战方案。2. FINS 协议通信设计从帧结构解析到 C# 封包逻辑闭环2.1 FINS 帧结构与欧姆龙地址映射规则非 ModbusFINS 协议不是 Modbus RTU/ASCII 那套寄存器编号逻辑。欧姆龙用的是“节点号 网络号 单元号 地址类型 地址偏移”五层定位比如读取 D100 的完整地址是0x00 0x00 0x00 0x82 0x00 0x64其中前 3 字节0x00 0x00 0x00是目标节点本地 PLC 默认为 0第 4 字节0x82是内存区域代码0x82 DM 区0x80 CIO 区0x90 WR 区后 2 字节0x00 0x64是地址偏移D100 十进制 100 0x64。提示CIO 区对应输入输出物理点CIO0.00 ~ CIO9.15DM 区是数据存储区HR 区是保持寄存器代码中用0x90。别把 D100 当成 Modbus 的 40101 —— 这是初学者翻车第一坑。2.2 C# 封装 FINS 请求帧TcpClient BinaryWriter 构建可复用请求体以下代码封装了标准 FINS 读命令0x0101支持单次读多个连续地址如 D100~D103 共 4 个字public static byte[] BuildFinsReadCommand(byte memoryArea, ushort startAddress, ushort wordCount) { // FINS 头ICF(1) RSV(1) GCT(1) DNA(1) DA1(1) DA2(1) SNA(1) SA1(1) SA2(1) SID(1) var header new byte[10] { 0x80, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; // 命令体服务ID(2) 内存区(1) 起始地址(2) 读取字数(2) var body new byte[7]; BitConverter.GetBytes((ushort)0x0101).CopyTo(body, 0); // 读命令 body[2] memoryArea; // 如 0x82 表示 DM 区 BitConverter.GetBytes(startAddress).CopyTo(body, 3); // 小端序 BitConverter.GetBytes(wordCount).CopyTo(body, 5); // 小端序 var packet new byte[header.Length body.Length]; header.CopyTo(packet, 0); body.CopyTo(packet, header.Length); return packet; }这段代码的关键在于所有整型字段必须小端序Little-Endian且 FINS 命令头固定为 10 字节不能省略RSV0x00或误填GCT0x00实际应为0x06表示本机到本机通信。我一般会把BuildFinsReadCommand封进FinsClient类再加一个SendAndReceive方法统一处理超时和重试。2.3 响应帧解析用 BinaryReader 拆解状态码与数据区FINS 响应帧前 10 字节是响应头第 11~12 字节是状态码0x0000表示成功之后才是数据。解析 D 区读取结果的典型逻辑如下public static ushort[] ParseFinsReadResponse(byte[] response, int expectedWordCount) { if (response.Length 12) throw new InvalidOperationException(响应帧过短); var status BitConverter.ToUInt16(response, 10); // 状态码位置固定 if (status ! 0) throw new Exception($FINS 错误码: 0x{status:X4}); var dataStart 12; var result new ushort[expectedWordCount]; for (int i 0; i expectedWordCount; i) { result[i] BitConverter.ToUInt16(response, dataStart i * 2); // 每个字 2 字节小端 } return result; }注意expectedWordCount必须与请求一致否则dataStart i*2会越界。我在某跨平台系统中曾因没校验response.Length导致IndexOutOfRangeException后来强制加了长度断言if (response.Length 12 expectedWordCount * 2) throw...。3. 模块化实现FinsClient 类设计与多地址批量读写实战3.1 FinsClient 核心类连接管理 命令队列 异常熔断一个能进产线的 C# FINS 客户端绝不能是“发一次请求建一次 TcpClient”。我们设计FinsClient类包含三个关键能力连接池管理内部持有一个TcpClient实例Connect()只初始化一次Dispose()时释放命令队列用ConcurrentQueueFinsRequest缓冲读写请求避免高并发下 Socket 争抢熔断机制连续 3 次TimeoutException自动触发Reconnect()并记录日志。以下是构造函数与连接逻辑精简版public class FinsClient : IDisposable { private TcpClient _client; private NetworkStream _stream; private readonly string _ip; private readonly int _port; private readonly int _timeoutMs 3000; public FinsClient(string ip, int port 9600) { _ip ip; _port port; } public void Connect() { _client new TcpClient(); _client.Connect(_ip, _port); _client.ReceiveTimeout _timeoutMs; _client.SendTimeout _timeoutMs; _stream _client.GetStream(); } public void Dispose() { _stream?.Close(); _client?.Close(); } }注意欧姆龙默认 FINS 端口是 9600TCP不是 Modbus 的 502。有些 CP 系列 PLC 需在 CX-Programmer 中手动启用“FINS 服务”否则Connect()会直接超时。3.2 批量读取一次请求读取 CIODMHR 三区共 24 个字工业现场常需同步读取输入状态CIO、过程变量DM、设定值HR。FINS 支持单次请求读多个非连续地址但需拼接多个子命令。我们封装ReadMultipleAreas方法public ushort[][] ReadMultipleAreas(params (byte area, ushort start, ushort count)[] areas) { var request BuildMultiAreaReadCommand(areas); var response SendAndReceive(request); return ParseMultiAreaResponse(response, areas); } private byte[] BuildMultiAreaReadCommand((byte, ushort, ushort)[] areas) { var header new byte[10] { 0x80, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; var bodyList new Listbyte(); foreach (var (area, start, count) in areas) { var subCmd new byte[7]; BitConverter.GetBytes((ushort)0x0101).CopyTo(subCmd, 0); subCmd[2] area; BitConverter.GetBytes(start).CopyTo(subCmd, 3); BitConverter.GetBytes(count).CopyTo(subCmd, 5); bodyList.AddRange(subCmd); } var packet new byte[header.Length bodyList.Count]; header.CopyTo(packet, 0); bodyList.CopyTo(packet, header.Length); return packet; }调用示例var results client.ReadMultipleAreas( (0x80, 0, 8), // CIO0.00 ~ CIO0.078 个字即 16 位 (0x82, 100, 4), // DM100 ~ DM1034 个字 (0x90, 0, 2) // HR0 ~ HR12 个字 ); // results[0] 是 CIO 区 8 个 ushortresults[1] 是 DM 区 4 个results[2] 是 HR 区 2 个这种设计让主程序逻辑干净不用反复Connect/Disconnect不用拆包拼地址一行代码覆盖多区读取。3.3 写操作实现支持位写CIO与字写DM/HR双模式FINS 写命令分两种0x0102字写和0x0103位写。CIO 区常需单独控制某一位如 CIO1.05不能只写整个字public void WriteBitToCio(ushort address, bool value) { var bitNo (byte)(address % 16); // 位号 0~15 var wordAddr (ushort)(address / 16); // 字地址 var cmd BuildFinsWriteBitCommand(0x80, wordAddr, bitNo, value); SendAndReceive(cmd); } private byte[] BuildFinsWriteBitCommand(byte area, ushort wordAddr, byte bitNo, bool value) { var header new byte[10] { 0x80, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; var body new byte[8]; BitConverter.GetBytes((ushort)0x0103).CopyTo(body, 0); // 位写命令 body[2] area; BitConverter.GetBytes(wordAddr).CopyTo(body, 3); body[5] bitNo; body[6] value ? (byte)0x01 : (byte)0x00; // ... 合并 headerbody return packet; }这里有个血泪经验bitNo是 0~15不是 0~7。CIO0.00 对应wordAddr0, bitNo0CIO0.15 对应wordAddr0, bitNo15。曾有 A 同学把bitNo address 0x0F写成address % 8导致高位 8 位永远写不进去。4. 常见问题排查5 个真实翻车现场与根因修复4.1 现象Connect()成功但SendAndReceive()一直超时Wireshark 显示 PLC 回了 RST 包原因PLC 的 FINS 服务未启用或 IP 地址不在允许访问列表中。欧姆龙 CP 系列默认关闭 FINS需用 CX-Programmer 连接后在【PLC 系统设置】→【FINS 设置】中勾选“启用 FINS 服务”并确认“允许远程访问”已打开。部分型号还需设置“允许的源 IP”为工控机网段如192.168.1.*。4.2 现象读取 DM100 返回全 0但用 CX-Server 能正常读到值原因地址类型填错。DM 区代码是0x82不是0x80CIO或0x90HR。检查BuildFinsReadCommand中传入的memoryArea参数是否硬编码错误。建议在构造请求前加日志Console.WriteLine($Reading {memoryArea:X2} at {startAddress})。4.3 现象BitConverter.ToUInt16(response, pos)抛IndexOutOfRangeException原因响应帧长度不足。FINS 响应头固定 10 字节状态码 2 字节之后才是数据。若请求读 10 个字20 字节响应至少需1022032字节。但网络抖动或 PLC 忙碌时可能截断。解决在ParseFinsReadResponse开头加长度校验if (response.Length 12 expectedWordCount * 2) throw new Exception($响应长度不足期望{12 expectedWordCount * 2}实际{response.Length});4.4 现象写入 CIO0.00 后PLC 物理输出点不动作原因CIO 区写入的是“输出映像区”不是物理输出锁存器。需确认 PLC 程序中是否有MOV指令将 CIO0.00 的值传给实际输出继电器如OUT 100.00。这是 PLC 编程逻辑问题不是 C# 通信问题。建议先用 CX-Programmer 在线监控 CIO0.00 值是否变化再查梯形图。4.5 现象程序运行 2 小时后卡死CPU 占用 100%原因TcpClient未正确释放或NetworkStream读取时未设超时。_client.ReceiveTimeout必须在Connect()后立即设置且不能为 0。更稳妥的做法是每次SendAndReceive前检查_client.Connected断开时自动重连而非死等。我在某图像处理 Demo 中加了心跳包每 30 秒发0x0000命令彻底规避了长连接假死。5. 方案交付与答辩验收从代码到报告的 4 个硬核交付物5.1 可运行的最小可交付单元MVP清单一份能直接交毕设/课设的 FINS 方案必须包含以下 4 个文件缺一不可文件名类型说明FinsClient.csC# 类文件核心通信类含Connect/Read/Write/Dispose全方法Program.csC# 主程序演示如何读 CIO、写 DM、批量读三区含清晰注释和异常捕获test-plc-config.txt文本文件记录测试用 PLC 型号CP1E-E30DR-A、IP192.168.1.10、端口9600、已启用的 FINS 设置截图路径README.mdMarkdown 文档含编译命令dotnet build -c Release、运行依赖.NET 6 Runtime、常见问题速查表提示答辩时老师大概率会问“如果 PLC IP 变了怎么改”答案必须指向Program.cs中new FinsClient(192.168.1.10)这一行——说明配置与代码分离不是硬编码。5.2 答辩演示脚本3 分钟讲清技术深度不要一上来就点“启动程序”。按这个节奏走第一分钟问题锚定—— “老师好本项目解决的是工业现场无 OPC 环境下 C# 直连欧姆龙 PLC 的实时数据采集问题。重点不是‘能不能通’而是‘怎么稳、怎么快、怎么查’。”第二分钟技术亮点—— 打开FinsClient.cs指出三处设计①ConcurrentQueue避免高并发 Socket 冲突② 小端序 地址偏移校验防止越界③ 熔断重连逻辑指向Reconnect()方法。第三分钟实测证据—— 切到 Wireshark 窗口展示过滤tcp.port9600后的请求/响应帧标出 FINS 头、状态码0x0000、DM 区数据载荷证明字节级可控。5.3 验收标准自检表答辩前必过用这张表逐项打钩确保不被问住验收项检查方式通过标志✅ 单地址读取运行Program.cs修改ReadDm(100, 1)输出D100 1234控制台打印非零值且 CX-Server 同步显示相同值✅ 位写控制调用WriteBitToCio(0, true)用万用表测 CIO0.00 物理电压输出点电压从 0V 跳变至 24V✅ 批量读取调用ReadMultipleAreas((0x80,0,2),(0x82,100,2))返回二维数组results[0].Length2 results[1].Length2✅ 断网恢复拔掉 PLC 网线 10 秒再插回程序日志出现Reconnecting...3 秒内恢复读取✅ 超时防护在FinsClient中临时设_timeoutMs 1控制台抛TimeoutException不卡死从那以后我每次交课设都强制走一遍这张表——哪怕只剩 10 分钟答辩也要先跑通这 5 条。因为老师不会看你写了多少行代码只会验证你是否真正掌控了通信的每个字节。希望帮到你。本文还有配套的精品资源点击获取