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

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 调用示例

/// <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 拼接帧数据,严格校验响应结束码,做好超时和异常处理。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐