一、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 端:
- 启用 EtherNet/IP 协议(在 PLC 编程软件 CX-Programmer/NX Navigator 中开启);
- 配置 PLC 的 IP 地址(如
192.168.1.100)、子网掩码、网关; - 确认 PLC 的槽位号(Slot) :NJ/NX 系列默认
0,CJ2 系列默认1。
- 上位机端:
- 引用
HslCommunication包(NuGet 安装:Install-Package HslCommunication); - 确保上位机与 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_STRING从D500改成D800),只需在 PLC 侧修改别名映射,上位机代码不用动; - 若用数字地址,改
D500为D800需要全代码替换,极易漏改。
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 返回该符号对应的物理地址;
- 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;