简介:本资源是一套面向工业自动化开发者的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 帧结构与欧姆龙地址映射规则(非 Modbus!)
FINS 协议不是 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.15),DM 区是数据存储区,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 字节,不能省略RSV=0x00或误填GCT=0x00(实际应为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()时释放; - 命令队列:用
ConcurrentQueue<FinsRequest>缓冲读写请求,避免高并发下 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 端口是 9600(TCP),不是 Modbus 的 502。有些 CP 系列 PLC 需在 CX-Programmer 中手动启用“FINS 服务”,否则
Connect()会直接超时。
3.2 批量读取:一次请求读取 CIO+DM+HR 三区共 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 List<byte>(); 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.07(8 个字,即 16 位) (0x82, 100, 4), // DM100 ~ DM103(4 个字) (0x90, 0, 2) // HR0 ~ HR1(2 个字) ); // results[0] 是 CIO 区 8 个 ushort,results[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; // ... 合并 header+body return packet; }这里有个血泪经验:bitNo是 0~15,不是 0~7。CIO0.00 对应wordAddr=0, bitNo=0,CIO0.15 对应wordAddr=0, bitNo=15。曾有 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,不是0x80(CIO)或0x90(HR)。检查BuildFinsReadCommand中传入的memoryArea参数是否硬编码错误。建议在构造请求前加日志:Console.WriteLine($"Reading {memoryArea:X2} at {startAddress}")。
4.3 现象:BitConverter.ToUInt16(response, pos)抛IndexOutOfRangeException
原因:响应帧长度不足。FINS 响应头固定 10 字节,状态码 2 字节,之后才是数据。若请求读 10 个字(20 字节),响应至少需10+2+20=32字节。但网络抖动或 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.cs | C# 类文件 | 核心通信类,含Connect/Read/Write/Dispose全方法 |
Program.cs | C# 主程序 | 演示如何读 CIO、写 DM、批量读三区,含清晰注释和异常捕获 |
test-plc-config.txt | 文本文件 | 记录测试用 PLC 型号(CP1E-E30DR-A)、IP(192.168.1.10)、端口(9600)、已启用的 FINS 设置截图路径 |
README.md | Markdown 文档 | 含编译命令(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.port==9600后的请求/响应帧,标出 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].Length==2 && results[1].Length==2 |
| ✅ 断网恢复 | 拔掉 PLC 网线 10 秒,再插回 | 程序日志出现Reconnecting...,3 秒内恢复读取 |
| ✅ 超时防护 | 在FinsClient中临时设_timeoutMs = 1 | 控制台抛TimeoutException,不卡死 |
从那以后我每次交课设,都强制走一遍这张表——哪怕只剩 10 分钟答辩,也要先跑通这 5 条。因为老师不会看你写了多少行代码,只会验证你是否真正掌控了通信的每个字节。希望帮到你。
本文还有配套的精品资源,点击获取