C# HSL 与欧姆龙 CIP 协议(EtherNet/IP)的详细通信

一、CIP 协议与 HSL 的关系

1. CIP 协议是什么?

CIP 协议 的全称是 Common Industrial Protocol(通用工业协议) ,是一种面向工业自动化领域的开放、多厂商兼容 的应用层通信协议,由 ODVA(Open DeviceNet Vendors Association)组织管理和维护。它的核心目标是实现工业设备之间的无缝数据交互,涵盖控制、配置、诊断等全生命周期功能。

CIP 协议的核心特点
1.多网络适配性

CIP 是一个与底层物理网络无关的应用层协议,可以运行在多种工业网络上,形成不同的工业总线标准,典型包括:

  • EtherNet/IP:基于以太网的工业协议,是目前工业自动化领域应用最广泛的 CIP 衍生协议。
  • DeviceNet:基于 CAN 总线的底层设备通信协议,常用于连接传感器、执行器等现场设备。
  • ControlNet:用于控制器与 I/O 模块、人机界面(HMI)等的高速实时通信。
  • CompoNet:适用于低成本、小规模设备的通信网络。
2.对象导向架构

CIP 采用对象模型 来描述工业设备的功能,每台设备都被抽象为一系列标准化的对象(如电机对象、开关量输入对象、参数对象等),每个对象包含:

  • 属性:设备的参数或状态(如电机转速、输入点状态)。
  • 服务:对属性的操作(如读取转速、修改参数、复位设备)。这种架构让不同厂商的设备可以用统一的方式被访问和控制,极大提升了兼容性。
3.支持多种通信类型
  • I/O 消息:实时性要求高的短数据通信,用于控制器与现场设备的周期性数据交换(如传感器数据上传、执行器指令下发)。
  • 显式消息:非周期性的长数据通信,用于设备配置、诊断、参数读写等(如修改 PLC 的程序参数、读取设备故障代码)。
4.开放与多厂商兼容

协议规范完全开放,任何厂商都可以申请加入 ODVA 并获得协议授权,生产符合 CIP 标准的设备。这意味着不同品牌的 PLC、传感器、HMI 可以在同一网络中通信,无需专属驱动。

2. HSL 如何实现 CIP 通信?

HSL 的OmronCipNet类封装了 CIP 协议的底层细节(报文封装、会话管理、数据解析),上位机只需调用Write/Read等方法即可完成通信,无需手动处理 CIP 报文。

二、欧姆龙 CIP 通信的核心规则

1. 地址格式(与 PLC 寄存器对应)

|---------------------------------|----------------|-------------------|--------------------------|
| 寄存器类型 | 地址格式示例 | 数据宽度 | HSL 支持的 C# 类型 |
| |---------|---| | 数据寄存器 D | | | D100、D100.0(位) | 16 位 / 位 | short、int、float、string 等 |
| 全局寄存器 G | G200 | 16 位 | 同 D |
| 链接寄存器 B | B50 | 16 位 | 同 D |
| |-------|---| | 累加器 A | | | A1 | 32 位 | int、float 等 |
| 计数器 C | C10 | 16 位 | short |
| 定时器 T | |----| | T5 | | |------| | 16 位 | | |-------| | short | |

2. 通信前提(必须配置)

  • PLC 端:
    1. 启用 EtherNet/IP 协议(在 PLC 编程软件 CX-Programmer/NX Navigator 中开启);
    2. 配置 PLC 的 IP 地址(如192.168.1.100)、子网掩码、网关;
    3. 确认 PLC 的槽位号(Slot) :NJ/NX 系列默认0,CJ2 系列默认1
  • 上位机端:
    1. 引用HslCommunication包(NuGet 安装:Install-Package HslCommunication);
    2. 确保上位机与 PLC 在同一网段,或网络互通。

三、完整可运行案例(分场景)

以下案例基于欧姆龙 NJ501-1300 PLC (IP:192.168.1.100,Slot:0),可直接复制运行。

场景 1:基础数据读写(D 寄存器)

功能:写 / 读 D100(16 位)、D101(32 位 float)
cs 复制代码
using System;
using HslCommunication.Profinet.Omron;
using HslCommunication;

class OmronCipDemo
{
    static void Main(string[] args)
    {
        // 1. 创建CIP通信对象
        OmronCipNet plc = new OmronCipNet();
        plc.Slot = 0; // PLC槽位号(NJ/NX填0,CJ2填1)
        plc.CommunicationPipe = new HslCommunication.Core.Pipe.PipeTcpNet(
            "192.168.1.100", // PLC的IP地址
            44818            // CIP协议默认端口
        )
        {
            ConnectTimeOut = 5000,  // 连接超时5秒
            ReceiveTimeOut = 10000  // 接收超时10秒
        };

        // 2. 连接PLC(可选,HSL会自动连接)
        OperateResult connectResult = plc.ConnectServer();
        if (!connectResult.IsSuccess)
        {
            Console.WriteLine($"连接PLC失败:{connectResult.Message}");
            Console.ReadKey();
            return;
        }
        Console.WriteLine("连接PLC成功!");


        // -------------------------- 写操作 --------------------------
        // 2.1 写16位short到D100(占1个D寄存器)
        short d100Value = 12345;
        OperateResult writeD100 = plc.Write("D100", d100Value);
        if (writeD100.IsSuccess)
            Console.WriteLine($"写D100成功,值:{d100Value}");
        else
            Console.WriteLine($"写D100失败:{writeD100.Message}");

        // 2.2 写32位float到D101(占D101+D102两个寄存器)
        float d101Value = 123.456f;
        OperateResult writeD101 = plc.Write("D101", d101Value);
        if (writeD101.IsSuccess)
            Console.WriteLine($"写D101成功,值:{d101Value}");
        else
            Console.WriteLine($"写D101失败:{writeD101.Message}");


        // -------------------------- 读操作 --------------------------
        // 3.1 读D100的16位short
        OperateResult<short> readD100 = plc.ReadInt16("D100");
        if (readD100.IsSuccess)
            Console.WriteLine($"读D100成功,值:{readD100.Content}");
        else
            Console.WriteLine($"读D100失败:{readD100.Message}");

        // 3.2 读D101的32位float
        OperateResult<float> readD101 = plc.ReadFloat("D101");
        if (readD101.IsSuccess)
            Console.WriteLine($"读D101成功,值:{readD101.Content}");
        else
            Console.WriteLine($"读D101失败:{readD101.Message}");


        // 4. 断开连接
        plc.ConnectClose();
        Console.WriteLine("已断开与PLC的连接");
        Console.ReadKey();
    }
}

场景 2:位操作(D 寄存器的某一位)

功能:写 / 读 D100 的第 5 位(D100.5)
cs 复制代码
// (接场景1的PLC对象)
// 写D100.5为True
OperateResult writeBit = plc.Write("D100.5", true);
if (writeBit.IsSuccess)
    Console.WriteLine("写D100.5为True成功");
else
    Console.WriteLine($"写D100.5失败:{writeBit.Message}");

// 读D100.5的状态
OperateResult<bool> readBit = plc.ReadBool("D100.5");
if (readBit.IsSuccess)
    Console.WriteLine($"读D100.5成功,值:{readBit.Content}");
else
    Console.WriteLine($"读D100.5失败:{readBit.Message}");

场景 3:字符串读写

功能:写 ASCII 字符串到 D200,读回验证
cs 复制代码
// (接场景1的PLC对象)
// 写字符串"HelloOmron"到D200(占5个D寄存器:200-204,每个D存2个字符)
string strValue = "HelloOmron";
OperateResult writeStr = plc.Write("D200", strValue);
if (writeStr.IsSuccess)
    Console.WriteLine($"写字符串到D200成功,值:{strValue}");
else
    Console.WriteLine($"写字符串失败:{writeStr.Message}");

// 读D200开始的10个字符(占5个D寄存器)
OperateResult<string> readStr = plc.ReadString("D200", 10, System.Text.Encoding.ASCII);
if (readStr.IsSuccess)
    Console.WriteLine($"读字符串成功,值:{readStr.Content}");
else
    Console.WriteLine($"读字符串失败:{readStr.Message}");

场景 4:批量读写多个寄存器

功能:批量读 D300-D302 的 3 个 short,批量写 D303-D305 的 3 个 short
cs 复制代码
// (接场景1的PLC对象)
// 批量读D300-D302的3个short
OperateResult<short[]> readBatch = plc.ReadInt16("D300", 3);
if (readBatch.IsSuccess)
{
    Console.WriteLine("批量读D300-D302成功:");
    for (int i = 0; i < readBatch.Content.Length; i++)
    {
        Console.WriteLine($"D{300+i}:{readBatch.Content[i]}");
    }
}
else
{
    Console.WriteLine($"批量读失败:{readBatch.Message}");
}

// 批量写D303-D305为[666,777,888]
short[] writeBatchValues = new short[] { 666, 777, 888 };
OperateResult writeBatch = plc.Write("D303", writeBatchValues);
if (writeBatch.IsSuccess)
    Console.WriteLine("批量写D303-D305成功");
else
    Console.WriteLine($"批量写失败:{writeBatch.Message}");

四、常见问题与排查

1.连接失败

  • 检查 PLC IP 是否正确、网络是否互通(ping PLC IP);
  • 确认 PLC 的 EtherNet/IP 协议已启用;
  • 槽位号是否正确(NJ/NX 填 0,CJ2 填 1)。

2.读写失败

  • 地址格式是否正确(如 D100 而非 D100a);
  • PLC 的寄存器是否被其他程序占用;
  • 数据类型宽度是否匹配(如写 float 必须占 2 个 16 位寄存器)。

3.数据不一致

  • 检查 HSL 的字节序(默认大端,欧姆龙 PLC 默认大端,无需修改);
  • 确认读写的寄存器地址范围无重叠。

另外要理解代码中MES_STRING/MES_BitToPC这类 "自定义名称地址" 和之前讲的Dxxx数字地址的区别,核心是欧姆龙 PLC 的 "符号地址(别名)" 机制------ 这是工业项目中标准化、易维护的核心写法。

cs 复制代码
  /// <summary>
        /// HightPot位数据地址
        /// </summary>
        public static string HipotRealAddress = "MES_Hipot";
        public static float[] MES_Hipot = new float[10];
        //10个字符串数组
        public static string ToPlc_BatchBarcodeAddress = "MES_WINDING_STRING";
        public static string[] ToPlc_MaterialBarcode = new string[10];

        //10个物料码字符串数组
        public static string Plc_BarcodeAddress = "MES_STRING";
        public static string[] MES_STRING = new string[10] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };


        public static string MES_CcdFilePATH_Address = "MES_CcdFilePath_STRING";
        public static string MES_CcdFilePATH_STRING = "";

        /// <summary>
        /// 因为Bit 是按照16位int类型来访问的,数组最好定义成16的整数倍来访问
        /// </summary>
        public static string BitInPut_StartAddress = "Mes_Alarm";
        public static bool[] WindingPlcAlarm = new bool[640];
        /// <summary>
        /// 从PLC获取交互信号
        /// </summary>
        public static string BitFrPlc_StartAddress = "MES_BitToPC";
        public static IoStatus[] WindingPlcBitInPut = new IoStatus[64];


        /// <summary>
        /// PC给PLC的交互信号
        /// </summary>
        public static string BitOutPut_StartAddress = "MES_BitFromPC";
        public static IoStatus[] WindingPlcBitOutPut = new IoStatus[64];
        public static bool[] WindingPlcBitOut = new bool[64];




  public static bool ReadWritePLC()
        {
            bool successflag = false;
            if (WindPLC != null)
            {
                bool[] readin_Bool = null;
                bool[] readin_Alarm = null;
                Single[] WindingFloat_Temp1 = null;
                Single[] WindingFloat_Temp2 = null;
                try
                { 
                    readin_Bool = WindPLC.ReadBool(BitFrPlc_StartAddress + "[0]", 32).Content;
                    readin_Alarm = WindPLC.ReadBool(BitInPut_StartAddress + "[0]", 640).Content;

                    WindingFloat_Temp1 = WindPLC.ReadFloat(MesReal + "[0]", 144).Content;
                    WindingFloat_Temp2 = WindPLC.ReadFloat(MesReal2 + "[0]", 144).Content;
                    string WindingBarcode_Temp_1 = WindPLC.ReadString(Plc_BarcodeAddress + "[0]").Content;
                    string WindingBarcode_Temp_2 = WindPLC.ReadString(Plc_BarcodeAddress + "[1]").Content;
                    string WindingBarcode_Temp_3 = WindPLC.ReadString(Plc_BarcodeAddress + "[2]").Content;
                    string WindingBarcode_Temp_4 = WindPLC.ReadString(Plc_BarcodeAddress + "[3]").Content;
                    string WindingBarcode_Temp_5 = WindPLC.ReadString(Plc_BarcodeAddress + "[4]").Content;
                    string WindingBarcode_Temp_6 = WindPLC.ReadString(Plc_BarcodeAddress + "[5]").Content;
                    string MES_CcdFilePATH_STRING_Temp = WindPLC.ReadString(MES_CcdFilePATH_Address).Content;
                    
                    Single[] WindingFloat_New_Add = WindPLC.ReadFloat(MesData + "[0]", 40).Content;
                    if (WindingFloat_New_Add != null)
                    {
                        Copy(WindingFloat_New_Add, MES_Data);
                    }
                   
                    if (readin_Bool != null && readin_Alarm != null && WindingFloat_Temp1 != null && WindingFloat_Temp2 != null)//&& readout_Bool!=null
                    {
                        //解码BOOL数组,放入PlcIn数组中。2021.5.13
                        DecodeIn(readin_Bool);
                        //解析报警信号
                        SendToAlarm(readin_Alarm);
                        //CCD数据保存到采集值
                        Copy(WindingFloat_Temp1, MES_REAL);
                        Copy(WindingFloat_Temp2, MES_REAL2);

                        MES_STRING[0] = WindingBarcode_Temp_1;
                        MES_STRING[1] = WindingBarcode_Temp_2;
                        MES_STRING[2] = WindingBarcode_Temp_3;
                        MES_STRING[3] = WindingBarcode_Temp_4;
                        MES_STRING[4] = WindingBarcode_Temp_5;
                        MES_STRING[5] = WindingBarcode_Temp_6;
                        MES_CcdFilePATH_STRING = MES_CcdFilePATH_STRING_Temp;

                        //获取输出变量,放入BOOL数组中。2021.5.13
                        bool[] OutBoolArray = EncodeOut();
                       
                        //当前Cip的这个控件并不支持批量写入的功能。
                        // var res = WindPLC.Write(BitOutPut_StartAddress+"[0]", OutBoolArray);
                        // 打印 MES_BitFromPC[3] 的状态
                        bool res = WriteBool_Array(BitOutPut_StartAddress, OutBoolArray);
                      
                        if (res == true)
                        {
                            successflag = true;
                        }
                        else
                        {
                            successflag = false;
                        }

                        Thread.Sleep(10);
                    }
                    else
                    {
                        successflag = false;                      
                    }
                }
                catch (Exception ex)
                {
                    successflag = false;
                    Logs.Error($"[{DateTime.Now:HH:mm:ss}] ReadWritePLC执行异常: {ex.Message} | 堆栈信息: {ex.StackTrace}");
                }            
            }
            else
            {
                successflag = false;
               Logs.Error($"[{DateTime.Now:HH:mm:ss}] WindPLC对象为null,无法执行读写");
            }

          
            if (successflag == false)
            {
                showAction?.Invoke($"读写IP:{WindingIP},端口号:{WindingPort} PLC出现异常:", false);
                Logs.Error($"[{DateTime.Now:HH:mm:ss}] 读写IP:{WindingIP},端口号:{WindingPort} PLC出现异常:");
            }
             return successflag;
        }

一、核心本质:数字地址 vs 符号地址(别名)

|------|------------------------|-------------------|---------------|
| 类型 | | 示例 | |----| | | 本质 | |----| | 适用场景 |
| 数字地址 | D100、D200.5、B50 | PLC 寄存器的 "物理地址" | 快速测试、简单项目 |
| 符号地址 | MES_STRING、MES_BitToPC | 给物理地址起的 "别名 / 标签" | 工业项目(易维护、易理解) |

代码中的MES_STRING/MES_BitToPC并非 "新的寄存器类型",而是PLC 工程师在 PLC 编程软件中给Dxxx等数字地址定义的 "符号别名" ------ 比如:

  • PLC 里配置:MES_STRING = D500(字符串起始地址);
  • PLC 里配置:MES_BitToPC = D600(BOOL 数组起始地址);
  • 上位机用MES_STRING访问,本质还是访问D500,只是不用记数字,直接按 "业务含义" 读写。

二、为什么工业项目要用符号地址(核心原因)

作为上位机开发者,你会发现这类命名的核心优势:

1.见名知意,降低维护成本

  • MES_BitToPC(PLC 给 PC 的交互信号),比写D600更容易理解业务含义;
  • Mes_Alarm(报警信号),比写D700更直观,新人接手不用查 "D700 是干啥的"。

2.地址变更无需改代码

  • 若 PLC 工程师调整物理地址(比如MES_STRINGD500改成D800),只需在 PLC 侧修改别名映射,上位机代码不用动;
  • 若用数字地址,改D500D800需要全代码替换,极易漏改。

3.标准化管理

工业项目中会把 "交互信号、报警、条码、CCD 路径" 等按业务分类命名,比如:

  • MES_BitFromPC:PC 给 PLC 的控制信号;
  • MES_CcdFilePath_STRING:CCD 文件路径字符串;
  • Mes_Alarm:报警 BOOL 数组;
  • 所有上位机 / PLC 工程师按统一别名开发,避免地址冲突。

三、代码中符号地址的解析逻辑(HSL + 欧姆龙 CIP)

HSL 的OmronConnectedCipNet/OmronCipNet原生支持欧姆龙的符号地址解析,核心流程:

1.PLC 侧:工程师在 CX-Programmer/NX Works 中,给Dxxx/Gxxx等物理地址创建 "符号表",比如:

|------------------------|-------------------|-----------------------------------|
| 符号名 | 物理地址 | 数据类型 |
| MES_STRING | |------| | D500 | | |--------------| | STRING(10 字) | |
| MES_BitToPC | |------| | D600 | | |------------| | BOOL[32] | |
| Mes_Alarm | D700 | |-------------| | BOOL[640] | |
| MES_CcdFilePath_STRING | |------| | D800 | | |--------------| | STRING(20 字) | |

2.上位机侧:HSL 通过 CIP 协议向 PLC 发送 "符号名查询请求",PLC 返回该符号对应的物理地址;

  1. HSL 自动按物理地址读写数据,对外表现为 "直接用符号名访问"。

四、代码中关键符号地址的业务含义

|------------------------|-------------------------------------------|-----------------|
| 符号地址 | | 代码中的用途 | |--------| | 对应物理地址类型 |
| MES_STRING | 存储 6 个条码字符串(WindingBarcode_Temp_1~6) | D 寄存器(字符串数组) |
| MES_CcdFilePath_STRING | 存储 CCD 文件路径字符串 | D 寄存器(单个字符串) |
| Mes_Alarm | 存储 640 个报警 BOOL 信号(readin_Alarm 数组) | D 寄存器(BOOL 数组) |
| MES_BitToPC | 存储 32 个 PLC 给 PC 的交互 BOOL 信号(readin_Bool) | D 寄存器(BOOL 数组) |
| MES_BitFromPC | PC 给 PLC 的 64 个控制 BOOL 信号(OutBoolArray) | D 寄存器(BOOL 数组) |
| MES_REAL/MesReal | 存储 144 个浮点型 CCD 采集数据 | D 寄存器(float 数组) |
| MES_Data/MesData | 存储 40 个新增浮点型数据 | D 寄存器(float 数组) |

五、关键细节:符号地址的偏移写法([0]/[1]

代码中MES_STRING[0]/MES_STRING[1]不是 "数组下标",而是HSL 的 "偏移访问" 规则

  • MES_STRING[0]:从MES_STRING对应的物理地址(如 D500)开始,取第 1 个字符串;
  • MES_STRING[1]:从MES_STRING的物理地址 + 字符串长度偏移,取第 2 个字符串;
  • 同理:Mes_Alarm[0]表示从Mes_Alarm起始地址取 640 个 BOOL,MesReal[0]表示从MesReal起始地址取 144 个 float。

六、上位机开发的实操注意事项

1.获取符号表

必须向 PLC 工程师索要《PLC 符号地址表》,表中会标注:

✅ 符号名(如 MES_STRING);

✅ 对应物理地址(如 D500);

✅ 数据类型 / 长度(如 STRING [20]、BOOL [640]、FLOAT [144]);

  • 没有符号表,无法确认MES_STRING对应哪个Dxxx,也无法排查地址错误。

2.HSL 支持符号地址的前提

  • PLC 侧必须启用 "符号地址访问权限"(CX-Programmer 中配置);
  • 欧姆龙 CIP 协议(EtherNet/IP)原生支持符号地址,OmronConnectedCipNet/OmronCipNet无需额外配置,直接用符号名读写即可。

3.和数字地址的兼容

  • 若临时测试,可直接把代码中的MES_STRING替换为对应的物理地址(如D500),读写效果完全一致;
  • 比如:WindPLC.ReadString("MES_STRING[0]")WindPLC.ReadString("D500", 20, Encoding.ASCII)(20 为字符串长度)。

七、举例:符号地址和数字地址的等价替换

假设 PLC 符号表配置:MES_STRING = D500(每个字符串占 10 个 D 寄存器),则:

cs 复制代码
// 符号地址写法(代码中的写法)
string barcode1 = WindPLC.ReadString("MES_STRING[0]").Content; // 读D500开始的字符串
string barcode2 = WindPLC.ReadString("MES_STRING[1]").Content; // 读D510开始的字符串

// 等价的数字地址写法
string barcode1 = WindPLC.ReadString("D500", 20, Encoding.ASCII).Content; // 20字符=10个D寄存器
string barcode2 = WindPLC.ReadString("D510", 20, Encoding.ASCII).Content;
相关推荐
老蒋新思维9 小时前
创客匠人峰会深度解析:知识变现的 “信任 - 效率” 双闭环 —— 从 “单次交易” 到 “终身复购” 的增长密码
大数据·网络·人工智能·tcp/ip·重构·数据挖掘·创客匠人
林杜雨都9 小时前
Action和Func
开发语言·c#
工程师00710 小时前
TPL如何自动调整执行效率
c#·tpl
CreasyChan11 小时前
C# 反射详解
开发语言·前端·windows·unity·c#·游戏开发
c#上位机11 小时前
halcon求区域交集——intersection
图像处理·人工智能·计算机视觉·c#·halcon
布谷歌11 小时前
在java中实现c#的int.TryParse方法
java·开发语言·python·c#
老蒋新思维14 小时前
创客匠人峰会新解:AI 时代知识变现的 “信任分层” 法则 —— 从流量到高客单的进阶密码
大数据·网络·人工智能·tcp/ip·重构·创始人ip·创客匠人
用户44884667106016 小时前
.NET进阶——深入理解Lambda表达式(2)手搓LINQ语句
c#·.net
Smile_25422041820 小时前
解决本地 Windows 开发机无法注册到 PowerJob 服务器的问题
java·tcp/ip