欧姆龙 FINS 协议详解与 C# 实现

1. 协议概述

1.1 定义

FINS(Factory Interface Network Service)是欧姆龙(OMRON)专为工业自动化领域设计的现场总线通信协议,用于实现 PLC 之间、PLC 与上位机之间的跨网络数据交互,支持位 / 字数据的读写、强制操作、参数配置等核心功能,是欧姆龙工业通信的核心协议。

1.2 传输方式

FINS 协议可基于多种物理层 / 传输层实现,核心传输方式如下:

|----------------|--------------|----------------|---------------------|
| 传输类型 | 底层载体 | 应用场景 | 核心特征 |
| FINS/TCP | 以太网(TCP/IP) | 上位机与 PLC 以太网通信 | 基于 TCP 连接,端口 9600 |
| FINS/UDP | 以太网(UDP/IP) | 广播 / 多播通信 | 无连接,端口 9600 |
| FINS/RS-232C | 串口 | 近距离串口通信 | 点对点,速率 9600~115200 |
| FINS/DeviceNet | DeviceNet 总线 | 现场设备总线通信 | 工业总线,低延迟 |

重点:FINS/TCP

本文聚焦工程中最常用的 FINS/TCP 实现,其核心特征为:

  • 基于 TCP 可靠连接,默认端口 9600;
  • 所有 FINS 指令封装在 TCP 数据包中传输;
  • 通信前需完成 FINS/TCP 握手流程,建立逻辑通信通道。

1.3 请求 - 响应模型

FINS 采用主从式请求 - 响应模型

  • 上位机(主站)向 PLC(从站)发送 FINS 请求帧(含操作指令、地址、参数);
  • PLC 接收并解析请求,执行对应操作(读 / 写 / 强制);
  • PLC 向上位机返回 FINS 响应帧(含执行结果、数据、错误码);
  • 上位机解析响应帧,确认操作结果。

1.4 寻址规则(DNA/DA1/DA2/SNA/SA1/SA2)

FINS 协议通过 6 个字节的地址段唯一标识通信节点,核心字段定义如下:

|-----|--------|--------|-------------------------------------|
| 字段 | 长度(字节) | 名称 | 含义 |
| DNA | 1 | 目的网络地址 | 目标节点所在网络号(0~127,本地网络为 0) |
| DA1 | 1 | 目的节点地址 | 目标 PLC 节点号(以太网:IP 最后一段;串口:1~31) |
| DA2 | 1 | 目的单元地址 | 目标 PLC 单元号(CPU 单元默认 0x00,扩展单元按实际配置) |
| SNA | 1 | 源网络地址 | 上位机所在网络号(本地网络为 0) |
| SA1 | 1 | 源节点地址 | 上位机节点号(自定义,建议 1~254,避免与 PLC 冲突) |
| SA2 | 1 | 源单元地址 | 上位机单元号(默认 0x00) |

示例:上位机(IP 192.168.1.100)访问 PLC(IP 192.168.1.20),寻址配置为:

  • DNA=0x00,DA1=0x14(20 的十六进制),DA2=0x00;
  • SNA=0x00,SA1=0x64(100 的十六进制),SA2=0x00。

2. FINS/TCP 通信机制

2.1 握手流程(请求 / 响应帧)

FINS/TCP 通信分为两个阶段:TCP 连接建立FINS/TCP 握手FINS 指令交互

2.1.1 握手请求帧(上位机 → PLC)

|-------------|--------|--------|---------------------|
| 字段 | 长度(字节) | 固定值 | 含义 |
| Magic | 2 | 0x4649 | 固定标识 "FI"(FINS/TCP) |
| Length | 2 | 0x000C | 后续数据长度(12 字节) |
| Command | 2 | 0x0000 | 握手请求命令 |
| Error Code | 2 | 0x0000 | 保留,无错误 |
| Client Node | 2 | 自定义 | 上位机节点号(SA1) |
| Reserved | 4 | 0x0000 | 保留字段 |

2.1.2 握手响应帧(PLC → 上位机)

|-------------|--------|---------|-----------------|
| 字段 | 长度(字节) | 固定值 | 含义 |
| Magic | 2 | 0x4649 | 固定标识 "FI" |
| Length | 2 | 0x000C | 后续数据长度(12 字节) |
| Command | 2 | 0x0000 | 握手响应命令 |
| Error Code | 2 | 0x0000 | 0 = 成功,非 0 = 失败 |
| Server Node | 2 | PLC 节点号 | PLC 的 DA1 值 |
| Reserved | 4 | 0x0000 | 保留字段 |

2.1.3 握手流程时序
  • 上位机通过 TCP 连接 PLC 的 9600 端口;
  • 上位机发送握手请求帧;
  • PLC 返回握手响应帧,若 Error Code=0x0000,握手成功;
  • 进入 FINS 指令交互阶段;
  • 通信结束后,关闭 TCP 连接。

2.2 FINS/TCP Header(16 字节)逐字段表

FINS/TCP 所有数据帧(含握手、指令)均以 16 字节头部开头,字段定义如下:

|--------|------------|--------|-------|-----------------------------------------------------|
| 偏移(字节) | 字段名 | 长度(字节) | 数据类型 | 取值 / 说明 |
| 0-1 | Magic | 2 | 无符号短 | 固定 0x4649("FI"),标识 FINS/TCP 协议 |
| 2-3 | Length | 2 | 无符号短 | 整个 FINS/TCP 帧(含 Header)的总长度 - 4(即 Header 后数据长度),大端序 |
| 4-5 | Command | 2 | 无符号短 | 0x0000 = 握手,0x0001=FINS 指令转发,0x0002=FINS 指令响应 |
| 6-7 | Error Code | 2 | 无符号短 | 0x0000 = 无错误,非 0 = 错误码(见附录) |
| 8-9 | Node | 2 | 无符号短 | 握手阶段:请求 = 客户端节点,响应 = 服务器节点;指令阶段:保留 |
| 10-15 | Reserved | 6 | 无符号字节 | 保留字段,固定填充 0x00 |

字节序说明 :Magic/Length/Command/ErrorCode/Node 均为大端序(Big-Endian) ,C# 中需通过 IPAddress.HostToNetworkOrder 转换。

3. FINS 帧结构

FINS 指令帧封装在 FINS/TCP Header 之后,核心分为 FINS Header(10 字节)、Command(2 字节)、参数 / 数据域(可变长度)三部分。

3.1 FINS Header(10 字节)

|--------|-----|--------|-------|-------------|-----------------|-------------------------------------|
| 偏移(字节) | 字段名 | 长度(字节) | 数据类型 | 请求帧取值 | 响应帧取值 | 含义 |
| 0 | ICF | 1 | 无符号字节 | 0x80(请求) | 0xC0(响应) | 信息控制字段:bit7=1(有响应),bit6=0(请求)/1(响应) |
| 1 | RSV | 1 | 无符号字节 | 0x00 | 0x00 | 保留字段,固定 0x00 |
| 2 | GCT | 1 | 无符号字节 | 0x02(默认) | 0x02 | 网关计数,本地通信 = 0x02 |
| 3 | DNA | 1 | 无符号字节 | 目标网络地址 | 源网络地址(上位机 SNA) | 见 1.4 寻址规则 |
| 4 | DA1 | 1 | 无符号字节 | 目标节点地址 | 源节点地址(上位机 SA1) | 见 1.4 寻址规则 |
| 5 | DA2 | 1 | 无符号字节 | 目标单元地址 | 源单元地址(上位机 SA2) | 见 1.4 寻址规则 |
| 6 | SNA | 1 | 无符号字节 | 源网络地址 | 目标网络地址(PLC DNA) | 见 1.4 寻址规则 |
| 7 | SA1 | 1 | 无符号字节 | 源节点地址 | 目标节点地址(PLC DA1) | 见 1.4 寻址规则 |
| 8 | SA2 | 1 | 无符号字节 | 源单元地址 | 目标单元地址(PLC DA2) | 见 1.4 寻址规则 |
| 9 | SID | 1 | 无符号字节 | 自定义(0~255) | 与请求帧 SID 一致 | 会话标识,用于匹配请求 - 响应 |

3.2 Command(2 字节):MRC/SRC

Command 字段分为 MRC(主命令,1 字节)和 SRC(子命令,1 字节),核心指令如下:

|-----------|------------|------------|------|------------------|
| 功能 | MRC(16 进制) | SRC(16 进制) | 组合值 | 说明 |
| 读字数据 | 01 | 01 | 0101 | 读取指定地址的字数据 |
| 写字数据 | 01 | 02 | 0102 | 写入指定地址的字数据 |
| 读位数据 | 01 | 03 | 0103 | 读取指定地址的位数据 |
| 写位数据 | 01 | 04 | 0104 | 写入指定地址的位数据 |
| 强制置位位数据 | 23 | 01 | 2301 | 强制置位指定位 |
| 强制复位位数据 | 23 | 02 | 2302 | 强制复位指定位 |
| 读取 PLC 状态 | 06 | 01 | 0601 | 读取 PLC 运行 / 停止状态 |

(PS:这就像是在餐厅点菜:MRC(主命令)决定了你是要点"主食"还是"饮料",而SRC(子命令)则决定了你具体要点的"宫保鸡丁"还是"可乐"。在FINS协议中,它们共同组成一个完整的指令。)

核心逻辑:MRC 是大类,SRC 是具体动作

在FINS协议的 Command (2字节)字段中,这两个字节被拆解为:

  • MRC (Main Request Code,主命令) :1个字节。它定义了操作的大类。比如是读写内存、控制IO,还是查询状态。

  • SRC (Sub Request Code,子命令) :1个字节。它定义了在这个大类下,具体的执行动作。比如是读、写、强制置位,还是复位。

(总结:)

  • MRC 是门派:比如"少林派"(内存操作)、"武当派"(强制操作)。

  • SRC 是武功:在"少林派"里,你可以出"罗汉拳"(读)或者"易筋经"(写)。

3.3 参数 / 数据域

参数 / 数据域长度可变,随指令类型(读 / 写)不同而变化,核心布局如下:

3.3.1 读字请求帧(MRC=01, SRC=01)

|--------|------|--------|---------------------------------|
| 偏移(字节) | 字段名 | 长度(字节) | 取值 / 说明 |
| 0-1 | 区域代码 | 2 | 字区域代码(如 D 区 = 0x0200,见 4.1),大端序 |
| 2-4 | 起始地址 | 3 | 3 字节地址编码(如 D100=0x000064,见 4.2) |
| 5-6 | 读取数量 | 2 | 读取字的个数(1~1024),大端序 |

3.3.2 读字响应帧

|--------|-----|--------|-------------------------------|
| 偏移(字节) | 字段名 | 长度(字节) | 取值 / 说明 |
| 0-1 | 结束码 | 2 | 0x0000 = 成功,非 0 = 失败(见附录),大端序 |
| 2-... | 数据域 | N*2 | 读取的 N 个字数据,每个字 2 字节,大端序 |

3.3.3 写字请求帧(MRC=01, SRC=02)

|--------|------|--------|--------------------------|
| 偏移(字节) | 字段名 | 长度(字节) | 取值 / 说明 |
| 0-1 | 区域代码 | 2 | 字区域代码,大端序 |
| 2-4 | 起始地址 | 3 | 3 字节地址编码 |
| 5-6 | 写入数量 | 2 | 写入字的个数,大端序 |
| 7-... | 数据域 | N*2 | 待写入的 N 个字数据,每个字 2 字节,大端序 |

3.3.4 写字响应帧

|--------|-----|--------|--------------------------|
| 偏移(字节) | 字段名 | 长度(字节) | 取值 / 说明 |
| 0-1 | 结束码 | 2 | 0x0000 = 成功,非 0 = 失败,大端序 |

4. 内存区与地址编码

4.1 区域代码表(Bit/Word)

欧姆龙 PLC 内存区分为位区域 (Bit)和字区域(Word),核心区域代码如下:

|-------|---------|--------------|--------------|----|
| 内存区名称 | 用途 | 字区域代码(16 进制) | 位区域代码(16 进制) | 标识 |
| CIO | 输入输出继电器 | 30 | B0 | 核心 |
| D | 数据寄存器 | 02 | 82 | 常用 |
| H | 保持寄存器 | 32 | B2 | 常用 |
| W | 工作寄存器 | 31 | B1 | 常用 |
| EM | 扩展数据寄存器 | A0 | A1 | 扩展 |
| TIM | 定时器当前值 | 05 | 85 | 定时 |
| CNT | 计数器当前值 | 06 | 86 | 计数 |

说明

  • 字区域代码:用于读 / 写字数据(如 D100 是字地址);
  • 位区域代码:用于读 / 写位数据(如 CIO 20.15 是位地址);
  • 编码时需将区域代码转换为 2 字节(大端序),如 D 区字代码 = 0x0200,CIO 区位代码 = 0xB000。
4.2 3 字节地址规则(A2 A1 A0)

欧姆龙 PLC 地址采用 3 字节(24 位)编码,格式为 A2(高位)、A1、A0(低位),核心规则:

4.2.1 字地址编码

字地址 = 十进制地址 → 转换为 3 字节十六进制(高位补 0)。

示例 1:D100 字地址编码

  • 十进制地址:100 → 十六进制:0x64;
  • 3 字节编码:A2=0x00,A1=0x00,A0=0x64 → 完整编码:0x000064。

示例 2:CIO 20 字地址编码

  • 十进制地址:20 → 十六进制:0x14;
  • 3 字节编码:0x000014。
4.2.2 位地址编码

位地址 = 字地址 × 16 + 位号 → 转换为 3 字节十六进制。

示例:CIO 20.15 位地址编码

  • 字地址:20 → 位号:15;
  • 总位地址:20×16 +15 = 335 → 十六进制:0x14F;
  • 3 字节编码:0x00014F。

5. C# 实现核心

5.1 连接:TcpClient + 握手 + 超时

  • TcpClient:用于建立 TCP 连接,指定 PLC IP 和端口 9600;
  • 握手流程:连接成功后发送 FINS/TCP 握手请求,验证响应是否有效;
  • 超时处理:设置 TCP 连接超时、读写超时,避免阻塞;
  • 核心要点:握手失败则关闭连接,重试需重新建立 TCP 连接。

5.2 字节序:Big-Endian 处理

FINS 协议所有多字节字段均为大端序(网络字节序),C# 中需转换:

  • 小端序(主机序)→ 大端序:IPAddress.HostToNetworkOrder(short/int)
  • 大端序 → 小端序:IPAddress.NetworkToHostOrder(short/int)
  • 字节数组反转:对于 byte [] 类型,可通过 Array.Reverse() 实现。

5.3 打包:MemoryStream/BitConverter 使用要点

  • MemoryStream:用于拼接 FINS 帧的各个字段,动态构建字节流;
  • BitConverter:用于将数值类型(short/int)转换为字节数组;
  • 核心要点
    1. 转换后需检查字节序,非大端序则反转;
    2. 写入 MemoryStream 时按字段偏移顺序拼接;
    3. 位地址 / 字地址需拆分为 3 字节写入。

5.4 错误处理

  • 结束码校验:响应帧结束码 = 0x0000 为成功,否则按错误码排查;
  • Socket 异常:捕获 SocketException、IOException,处理断连、超时;
  • 数据长度校验:响应帧数据长度需与请求的读取数量匹配,避免解析错误;
  • 日志记录:记录请求 / 响应帧的原始字节,便于问题排查。

6. C# 示例骨架

6.1 核心类:OmronFinsTcpClient

cs 复制代码
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

/// <summary>
/// 欧姆龙 FINS/TCP 客户端,支持字数据读写
/// </summary>
public class OmronFinsTcpClient : IDisposable
{
    #region 配置参数
    private readonly string _plcIp; // PLC IP地址
    private readonly int _plcPort = 9600; // FINS/TCP 默认端口
    private readonly byte _dna = 0x00; // 目的网络地址
    private readonly byte _da1 = 0x14; // 目的节点地址(PLC IP最后一段,如 20=0x14)
    private readonly byte _da2 = 0x00; // 目的单元地址
    private readonly byte _sna = 0x00; // 源网络地址
    private readonly byte _sa1 = 0x64; // 源节点地址(上位机自定义,如 100=0x64)
    private readonly byte _sa2 = 0x00; // 源单元地址
    private readonly byte _sid = 0x01; // 会话标识
    private TcpClient _tcpClient;
    private NetworkStream _networkStream;
    private const int TimeoutMs = 5000; // 超时时间 5秒
    #endregion

    #region 构造函数
    /// <summary>
    /// 初始化 FINS/TCP 客户端
    /// </summary>
    /// <param name="plcIp">PLC IP地址</param>
    public OmronFinsTcpClient(string plcIp)
    {
        _plcIp = plcIp;
        _tcpClient = new TcpClient { ReceiveTimeout = TimeoutMs, SendTimeout = TimeoutMs };
    }
    #endregion

    #region 核心方法:连接(含握手)
    /// <summary>
    /// 建立 TCP 连接并完成 FINS/TCP 握手
    /// </summary>
    /// <exception cref="SocketException">TCP连接失败</exception>
    /// <exception cref="InvalidOperationException">握手失败</exception>
    public void Connect()
    {
        // 1. 建立 TCP 连接
        try
        {
            _tcpClient.Connect(_plcIp, _plcPort);
            _networkStream = _tcpClient.GetStream();
            _networkStream.ReadTimeout = TimeoutMs;
            _networkStream.WriteTimeout = TimeoutMs;
        }
        catch (SocketException ex)
        {
            throw new SocketException((int)ex.SocketErrorCode);
        }

        // 2. 构建握手请求帧(16字节 FINS/TCP Header)
        var handshakeRequest = new MemoryStream();
        // Magic: 0x4649 ("FI")
        handshakeRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)0x4649)), 0, 2);
        // Length: 0x000C(后续数据长度12字节)
        handshakeRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)0x000C)), 0, 2);
        // Command: 0x0000(握手请求)
        handshakeRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)0x0000)), 0, 2);
        // Error Code: 0x0000
        handshakeRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)0x0000)), 0, 2);
        // Client Node: 源节点地址 SA1
        handshakeRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)_sa1)), 0, 2);
        // Reserved: 6字节 0x00
        handshakeRequest.Write(new byte[6], 0, 6);

        // 3. 发送握手请求
        _networkStream.Write(handshakeRequest.ToArray(), 0, (int)handshakeRequest.Length);

        // 4. 接收握手响应
        var handshakeResponse = new byte[16];
        int readBytes = _networkStream.Read(handshakeResponse, 0, 16);
        if (readBytes != 16)
        {
            throw new InvalidOperationException("握手响应长度异常");
        }

        // 5. 验证握手响应
        // 检查 Magic 是否为 0x4649
        short magic = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(handshakeResponse, 0));
        if (magic != 0x4649)
        {
            Disconnect();
            throw new InvalidOperationException("握手响应 Magic 不匹配");
        }
        // 检查 Error Code 是否为 0x0000
        short errorCode = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(handshakeResponse, 6));
        if (errorCode != 0x0000)
        {
            Disconnect();
            throw new InvalidOperationException($"握手失败,错误码:0x{errorCode:X4}");
        }
    }
    #endregion

    #region 核心方法:读字数据
    /// <summary>
    /// 读取 PLC 字数据(如 D区、CIO区)
    /// </summary>
    /// <param name="areaCode">字区域代码(如 D区=0x02,CIO区=0x30)</param>
    /// <param name="startAddress">起始字地址(如 D100=100)</param>
    /// <param name="count">读取数量(1~1024)</param>
    /// <returns>读取的字数据数组(int16类型)</returns>
    /// <exception cref="InvalidOperationException">读取失败</exception>
    public short[] ReadWords(byte areaCode, int startAddress, int count)
    {
        if (!_tcpClient.Connected)
        {
            throw new InvalidOperationException("TCP连接未建立");
        }
        if (count < 1 || count > 1024)
        {
            throw new ArgumentOutOfRangeException(nameof(count), "读取数量需在1~1024之间");
        }

        // 1. 构建 FINS 指令请求帧
        var finsRequest = new MemoryStream();

        // --------------------------
        // 第一部分:FINS/TCP Header(16字节)
        // --------------------------
        // Magic: 0x4649
        finsRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)0x4649)), 0, 2);
        // Length: FINS 帧长度(10+2+参数域长度)= 10+2+(2+3+2) = 19 → 总长度-4=16+19-4=31 → 0x001F
        short finsFrameLength = (short)(10 + 2 + 2 + 3 + 2); // FINS Header + Command + 参数域
        short tcpHeaderLength = (short)(16 + finsFrameLength - 4); // Length = 总长度 -4
        finsRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(tcpHeaderLength)), 0, 2);
        // Command: 0x0001(FINS 指令转发)
        finsRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)0x0001)), 0, 2);
        // Error Code: 0x0000
        finsRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)0x0000)), 0, 2);
        // Node: 保留 0x0000
        finsRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)0x0000)), 0, 2);
        // Reserved: 6字节 0x00
        finsRequest.Write(new byte[6], 0, 6);

        // --------------------------
        // 第二部分:FINS Header(10字节)
        // --------------------------
        finsRequest.WriteByte(0x80); // ICF: 请求帧
        finsRequest.WriteByte(0x00); // RSV: 保留
        finsRequest.WriteByte(0x02); // GCT: 网关计数
        finsRequest.WriteByte(_dna); // DNA: 目的网络地址
        finsRequest.WriteByte(_da1); // DA1: 目的节点地址
        finsRequest.WriteByte(_da2); // DA2: 目的单元地址
        finsRequest.WriteByte(_sna); // SNA: 源网络地址
        finsRequest.WriteByte(_sa1); // SA1: 源节点地址
        finsRequest.WriteByte(_sa2); // SA2: 源单元地址
        finsRequest.WriteByte(_sid); // SID: 会话标识

        // --------------------------
        // 第三部分:Command(2字节):0101(读字)
        // --------------------------
        finsRequest.WriteByte(0x01); // MRC: 01
        finsRequest.WriteByte(0x01); // SRC: 01

        // --------------------------
        // 第四部分:参数域(7字节)
        // --------------------------
        // 区域代码:2字节(大端序,字区域代码+0x00)
        short areaCodeWord = (short)(areaCode << 8); // 如 D区=0x02 → 0x0200
        finsRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(areaCodeWord)), 0, 2);
        // 起始地址:3字节(A2 A1 A0)
        byte[] addressBytes = BitConverter.GetBytes(startAddress);
        // 转换为3字节(高位补0),大端序
        finsRequest.WriteByte((byte)((startAddress > 0xFFFF) ? (byte)(startAddress >> 16) : 0x00));
        finsRequest.WriteByte((byte)(startAddress >> 8));
        finsRequest.WriteByte((byte)startAddress);
        // 读取数量:2字节(大端序)
        finsRequest.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)count)), 0, 2);

        // 2. 发送 FINS 请求帧
        byte[] requestBytes = finsRequest.ToArray();
        _networkStream.Write(requestBytes, 0, requestBytes.Length);

        // 3. 接收 FINS 响应帧
        // 先读取 FINS/TCP Header(16字节),获取总长度
        byte[] tcpHeader = new byte[16];
        int tcpHeaderBytes = _networkStream.Read(tcpHeader, 0, 16);
        if (tcpHeaderBytes != 16)
        {
            throw new InvalidOperationException("响应帧 TCP Header 读取失败");
        }
        // 解析 Length 字段,获取 FINS 帧长度
        short responseLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(tcpHeader, 2));
        int finsFrameBytes = responseLength - 12; // FINS 帧长度 = Length - 保留字段长度
        byte[] finsResponse = new byte[finsFrameBytes];
        int finsResponseBytes = _networkStream.Read(finsResponse, 0, finsFrameBytes);
        if (finsResponseBytes != finsFrameBytes)
        {
            throw new InvalidOperationException("响应帧 FINS 数据读取失败");
        }

        // 4. 解析响应帧
        // 检查结束码(偏移0-1)
        short endCode = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(finsResponse, 0));
        if (endCode != 0x0000)
        {
            throw new InvalidOperationException($"读取失败,结束码:0x{endCode:X4}");
        }

        // 解析数据域(偏移2开始,每个字2字节)
        short[] result = new short[count];
        for (int i = 0; i < count; i++)
        {
            int offset = 2 + i * 2;
            result[i] = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(finsResponse, offset));
        }

        return result;
    }
    #endregion

    #region 辅助方法:断开连接
    /// <summary>
    /// 断开 TCP 连接
    /// </summary>
    public void Disconnect()
    {
        _networkStream?.Close();
        _tcpClient?.Close();
        _tcpClient = new TcpClient { ReceiveTimeout = TimeoutMs, SendTimeout = TimeoutMs };
    }
    #endregion

    #region IDisposable 实现
    public void Dispose()
    {
        Disconnect();
        _tcpClient?.Dispose();
        _networkStream?.Dispose();
    }
    #endregion
}

6.2 调用示例

cs 复制代码
/// <summary>
/// 测试读取 D100-D102 字数据
/// </summary>
public static void TestReadDWords()
{
    try
    {
        // 初始化客户端(PLC IP 192.168.1.20)
        using (var finsClient = new OmronFinsTcpClient("192.168.1.20"))
        {
            // 建立连接并握手
            finsClient.Connect();
            Console.WriteLine("连接成功");

            // 读取 D100-D102(D区字代码=0x02,起始地址100,数量3)
            short[] dWords = finsClient.ReadWords(0x02, 100, 3);
            Console.WriteLine($"D100: {dWords[0]}, D101: {dWords[1]}, D102: {dWords[2]}");

            // 断开连接
            finsClient.Disconnect();
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"操作失败:{ex.Message}");
    }
}

6.3 代码关键说明

  1. TCP 连接与握手Connect() 方法完成 TCP 连接和 FINS/TCP 握手,验证响应有效性;
  2. 字节序处理 :所有多字节字段通过 IPAddress.HostToNetworkOrder 转换为大端序;
  3. 地址编码:3 字节地址拆分为高位、中位、低位字节写入;
  4. 响应解析:先读取 TCP Header 获取长度,再读取 FINS 帧,校验结束码后解析数据;
  5. 资源释放 :实现 IDisposable 接口,确保连接正常关闭。

7. 常见问题

7.1 节点不匹配

  • 现象:握手失败或指令无响应;
  • 原因:DA1(PLC 节点号)配置错误,或 SA1(上位机节点号)与网络中其他节点冲突;
  • 解决
    1. 确认 PLC 节点号(以太网节点号 = IP 最后一段,如 192.168.1.20 → DA1=0x14);
    2. 上位机 SA1 避免使用 PLC 节点号,建议使用 100+;
    3. 检查 PLC 网络配置,确保节点号未被占用。

7.2 字节序错误

  • 现象:读取数据值异常(如 D100=1 → 读取为 256);
  • 原因:多字节字段未转换为大端序,或解析时未转回小端序;
  • 解决
    1. 发送前:所有 short/int 字段通过 HostToNetworkOrder 转换;
    2. 接收后:所有 short/int 字段通过 NetworkToHostOrder 转换;
    3. 验证:抓包确认发送的地址 / 数量字段为大端序。

7.3 地址越界

  • 现象:响应结束码 = 0x0202(地址错误)或 0x0203(范围错误);
  • 原因
    1. 地址超出 PLC 内存区范围(如 D 区仅 1000 字,读取 D2000);
    2. 位地址位号超出 0-15(如 CIO 20.16);
  • 解决
    1. 确认 PLC 内存区容量(如 D 区容量可在 PLC 编程软件中查看);
    2. 位地址位号限制为 0-15;
    3. 读取数量不超过内存区剩余长度。

7.4 抓包(Wireshark)建议

  • 抓包过滤规则tcp.port == 9600,仅捕获 FINS/TCP 数据包;
  • 分析要点
    1. 检查 TCP 连接是否建立(SYN → SYN+ACK → ACK);
    2. 验证握手请求 / 响应帧的 Magic 字段是否为 0x4649;
    3. 检查 FINS 指令帧的地址 / 数量字段是否正确;
    4. 查看响应帧结束码是否为 0x0000;
  • 工具:Wireshark 可直接解析 FINS/TCP 协议,便于定位字段错误。

8. 附录

8.1 错误码速查表

|------------|--------|---------------------|
| 结束码(16 进制) | 含义 | 排查方向 |
| 0000 | 成功 | - |
| 0201 | 不支持的命令 | MRC/SRC 组合错误 |
| 0202 | 地址错误 | 区域代码 / 地址编码错误 |
| 0203 | 数据范围错误 | 读取 / 写入数量超出范围 |
| 0204 | 数据长度错误 | 参数域长度不匹配 |
| 0301 | 访问权限不足 | PLC 处于停止状态或保护模式 |
| 0401 | 服务未启动 | PLC 未启用 FINS/TCP 服务 |
| 0501 | 节点不存在 | DA1/DNA 配置错误 |

8.2 区域代码表

|-----|-------|------------|------------|-------------|
| 内存区 | 类型 | 字代码(16 进制) | 位代码(16 进制) | 备注 |
| CIO | 字 / 位 | 30 | B0 | 输入输出继电器 |
| WR | 字 / 位 | 31 | B1 | 工作寄存器 |
| HR | 字 / 位 | 32 | B2 | 保持寄存器 |
| AR | 字 / 位 | 33 | B3 | 辅助寄存器 |
| DM | 字 / 位 | 02 | 82 | 数据寄存器 |
| EM0 | 字 / 位 | A0 | A1 | 扩展数据寄存器 0 区 |
| EM1 | 字 / 位 | A2 | A3 | 扩展数据寄存器 1 区 |
| EM2 | 字 / 位 | A4 | A5 | 扩展数据寄存器 2 区 |
| EM3 | 字 / 位 | A6 | A7 | 扩展数据寄存器 3 区 |
| TIM | 字 / 位 | 05 | 85 | 定时器当前值 / 触点 |
| CNT | 字 / 位 | 06 | 86 | 计数器当前值 / 触点 |
| TMR | 字 / 位 | 07 | 87 | 定时器设置值 |
| CNR | 字 / 位 | 08 | 88 | 计数器设置值 |

总结

  1. FINS 协议核心:基于请求 - 响应模型,FINS/TCP 是工程主流实现,需先完成 TCP 连接和 FINS/TCP 握手,再交互指令帧;
  2. 编码关键:多字节字段需使用大端序,3 字节地址需按高位到低位拆分,区域代码区分字 / 位类型;
  3. C# 实现要点 :通过 TcpClient 建立连接,MemoryStream 拼接帧数据,严格校验响应结束码,做好超时和异常处理。
相关推荐
夏霞3 小时前
c# Signalr报错:消息大小超出了最大限制32kB。消息大小可在AddHubOptions中配置。
开发语言·c#
步步为营DotNet3 小时前
基于.NET 11 与C# 14的高性能安全客户端应用开发
安全·c#·.net
人工智能AI技术3 小时前
C# Runner+OpenClaw,用C#打造工业级AI智能体
人工智能·c#
布伦鸽3 小时前
C#检测文本编码格式
开发语言·c#
ZoeJoy83 小时前
WPF 从入门到实践:基础、ModernUI 与 MVVM 完全指南
c#·wpf
第二层皮-合肥18 小时前
基于C#的工业测试控制软件-总体框架
开发语言·c#
steins_甲乙20 小时前
C# 通过共享内存与 C++ 宿主协同捕获软件窗口
开发语言·c++·c#·内存共享
似水明俊德1 天前
12-C#.Net-加密解密-学习笔记
笔记·学习·oracle·c#·.net
阿蒙Amon1 天前
C#常用类库-详解SSH.NET
c#·ssh·.net