AB EtherNet/IP 通讯
Darra PLC 通过 EtherNetIPProtocolAdapter 支持 Allen-Bradley (Rockwell) EtherNet/IP 协议, 基于 CIP (Common Industrial Protocol) 实现 Tag 级数据访问。
支持型号
| 系列 | 型号 | 说明 |
|---|---|---|
| ControlLogix | 1756-L6x/L7x/L8x | 机架式, 大型应用 |
| CompactLogix | 1769-L1x/L2x/L3x | 紧凑型 |
| Micro800 | Micro820/850 | 小型应用 |
| PLC-5 | 1785-Lx | 传统型号 (SLC 兼容) |
| SLC 500 | 1747-Lx | 传统型号 |
协议特点
EtherNet/IP 采用 Tag-Based 寻址方式, 与传统寄存器寻址不同:
| 特性 | 说明 |
|---|---|
| 寻址方式 | Tag 名称 (符号寻址) |
| 数据封装 | CIP (Common Industrial Protocol) |
| 传输层 | TCP 端口 44818 |
| 隐式消息 | I/O 连接 (周期性数据交换) |
| 显式消息 | 消息连接 (读写请求/响应) |
EtherNet/IP 使用 Tag 名称直接访问变量, 例如 MyMotor.Speed、TemperatureArray[5]。不需要知道内存地址偏移。
Scanner 与 Adapter 模式
EtherNet/IP 定义了两种角色:
| 角色 | 别名 | 职责 | 示例 |
|---|---|---|---|
| Scanner (主站) | Originator | 发起连接, 主动读写 IO 数据 | PLC、PAC、PC 控制软件 |
| Adapter (从站) | Target | 响应连接, 提供 IO 数据 | 远程 IO 模块、驱动器、阀岛 |
Darra PLC 默认作为 Scanner 接入 AB PLC 和远程 IO 设备。Darra 也可作为 Adapter 被第三方 Scanner 访问 (需配置, 见下文)。
Scanner 模式
// Darra 作为 Scanner, 读取 AB PLC 的 Tag
var scanner = new EtherNetIPScanner();
scanner.AddTarget(new EIPTargetConfig {
Host = "192.168.1.40",
Slot = 0,
PlcType = EIPPlcType.ControlLogix,
RpiMs = 50, // 50 ms 周期
TransportClass = TransportClass.Class1 // 隐式 I/O 连接
});
await scanner.StartAsync();
Adapter 模式
// Darra 作为 Adapter, 暴露 IO 给外部 Scanner
var adapter = new EtherNetIPAdapter();
adapter.Configure(new EIPAdapterConfig {
Port = 44818,
AssemblyInput = new AssemblyConfig { // 输入 (T→O)
Instance = 100,
Size = 512,
Mapping = "%IX200.0~%IX263.7" // PLC 地址映射
},
AssemblyOutput = new AssemblyConfig { // 输出 (O→T)
Instance = 101,
Size = 512,
Mapping = "%QX200.0~%QX263.7"
},
AssemblyConfig = new AssemblyConfig { // 配置数据
Instance = 102,
Size = 0
}
});
隐式与显式报文
隐式报文 (I/O 连接)
隐式报文是 CIP 的周期性数据交换通道, 使用 UDP 2222 端口, 特点是低延迟、高频率:
| 属性 | 说明 |
|---|---|
| 传输 | UDP 多播/单播 |
| 端口 | 2222 (IP 协议) |
| 周期 | 可配置 RPI (Request Packet Interval), 典型 1~100 ms |
| 方向 | 双向 (输入/输出) |
| 连接 | 建立后持续刷新, 无需重复握手 |
| 数据 | 由 Assembly 对象定义 |
隐式报文格式 (CPF - Common Packet Format):
┌─────────────────┐
│ ENIP Header │ 4 字节
├─────────────────┤
│ CPF Item Count │ 2 字节
├─────────────────┤
│ Item 1: O→T │ 序列号 + 类型 + 数据
├─────────────────┤
│ Item 2: T→O │ 序列号 + 类型 + 数据
├─────────────────┤
│ Pad (可选) │ 对齐
└─────────────────┘
显式报文 (消息连接)
显式报文是按需的请求/响应式通讯, 使用 TCP 44818 端口:
| 属性 | 说明 |
|---|---|
| 传输 | TCP |
| 端口 | 44818 |
| 触发 | 按需调用 |
| 方向 | 请求/响应 |
| 连接 | 可选 Unconnected 或 Connected |
| 数据 | CIP 服务包 (读/写/创建/删除) |
显式报文适合 Tag 读写、配置、诊断等非周期性操作。
选择指南
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 远程 IO 数据 | 隐式报文 (Class 1) | 周期确定, 低延迟 |
| 周期性 Tag 采集 | 隐式报文 (Class 0/1) | 减少 CPU 开销 |
| 配方读写 | 显式报文 (Class 3) | 按需, 大数据量 |
| 设备诊断 | 显式报文 (Unconnected) | 低频, 无需建连接 |
| 报警/事件 | 显式报文 (Connected) | 可靠传输 |
CIP 对象模型
CIP 的核心是面向对象的设备模型。每个 CIP 设备都是一个对象集合, 对象由 Class (类), Instance (实例), Attribute (属性) 三层结构描述。
标准 CIP 对象
| 对象 (Class ID) | 功能 | 必须 |
|---|---|---|
| Identity (0x01) | 设备标识 (厂商、型号、序列号、版本) | ✅ |
| Message Router (0x02) | 消息路由, 将请求分发到各对象 | ✅ |
| Connection Manager (0x06) | 连接管理 (Forward Open/Close) | ✅ |
| Assembly (0x04) | IO 数据打包, 隐式报文的载体 | ✅ |
| Connection (0x05) | 连接状态 | ✅ |
| Register (0x07) | 注册服务 | — |
| Parameter (0x0F) | 设备参数 | — |
对象访问示例
// 读取 Identity 对象 (Class 0x01, Instance 1)
var identity = await adapter.ReadObjectAsync(0x01, 1);
Console.WriteLine($"Vendor: 0x{identity.GetAttribute(1):X4}"); // Vendor ID
Console.WriteLine($"Device Type: 0x{identity.GetAttribute(2):X4}");
Console.WriteLine($"Product Code: {identity.GetAttribute(3)}");
Console.WriteLine($"Serial: {identity.GetAttribute(6)}");
// 读取 Assembly 对象 (Class 0x04, Instance 100)
var assembly = await adapter.ReadObjectAsync(0x04, 100);
Console.WriteLine($"Assembly Data: {BitConverter.ToString(assembly.Data)}");
厂商扩展对象
CIP 允许厂商自定义对象 (Class ID ≥ 0x64), 用于设备特有功能:
Rockwell 扩展对象示例:
Class 0x64: 轴对象 (Axis Object)
Class 0x65: 运动控制对象 (Motion Control)
Class 0x6B: 伺服驱动器对象 (Servo Drive)
Class 0x6C: 配置对象 (Configuration)
连接配置
Forward Open 参数
隐式连接建立时, 通过 Forward Open 服务协商参数:
var fwd = new ForwardOpenConfig {
ConnPath = new byte[] { 0x20, 0x04, 0x24, 0x01 }, // Assembly 100
RpiTicks = 50000, // 50 ms (us)
ConnTimeoutMultiplier = 4, // 超时 = 4 × RPI
ConnSize = 500, // 连接数据大小 (字节)
TransportClass = TransportClass.Class1, // 隐式 I/O
Priority = Priority.Low,
Owner = ConnectionOwner.Exclusive // 排他连接
};
var connId = await scanner.ForwardOpenAsync(fwd);
连接类型
| Transport Class | 类型 | 确认 | 重传 |
|---|---|---|---|
| Class 0 | 多播 I/O | — | — |
| Class 1 | 点对点 I/O | — | — |
| Class 2 | 点对点 I/O | ✅ | — |
| Class 3 | 显式连接 | ✅ | ✅ |
连接超时行为
连接超时 (Connection Timeout) 可通过 ConnTimeoutMultiplier 配置:
超时时间 = ConnTimeoutMultiplier × RPI
例: RPI = 50 ms, Multiplier = 4 → 超时 = 200 ms
超时后的行为:
- Scanner 模式: 触发
OnConnectionTimeout事件, 报告 IOPS Bad - Adapter 模式: 输出保持最后值 (可配置安全值)
实时 IO 数据交换
IO 数据流
Darra 作为 Scanner 时的实时 IO 数据流:
扫描周期开始:
1. 从 AB PLC 接收隐式报文 (输入)
2. 解析 Assembly 数据 → 写入 PLC 输入映像 (%IX / %IW)
3. 执行 PLC 用户程序
4. 从 PLC 输出映像 (%QX / %QW) 读取数据
5. 封装为 Assembly 数据 → 发送隐式报文 (输出)
扫描周期结束
Assembly 映射
Assembly 对象是隐式报文的载体, 将多个 IO 数据填充到连续缓冲区:
// 配置 Assembly 映射
scanner.AssemblyMappings = new List<AssemblyMapping> {
new AssemblyMapping {
Instance = 100, // 输入 Assembly
Direction = AssemblyDirection.Input,
PlcStartAddress = "%IX100",
DataLayout = new[] {
new DataField { Offset = 0, Size = 32, PlcType = "BOOL", Count = 32 }, // 32 DI
new DataField { Offset = 4, Size = 16, PlcType = "INT", Count = 8 }, // 8 AI (INT)
new DataField { Offset = 20, Size = 32, PlcType = "DINT", Count = 8 }, // 8 DINT
}
},
new AssemblyMapping {
Instance = 101, // 输出 Assembly
Direction = AssemblyDirection.Output,
PlcStartAddress = "%QX100",
DataLayout = new[] {
new DataField { Offset = 0, Size = 16, PlcType = "BOOL", Count = 16 }, // 16 DO
new DataField { Offset = 2, Size = 16, PlcType = "INT", Count = 4 }, // 4 AO (INT)
}
}
};
小数据优化
Assembly 数据量较小时, 启用 Header 压缩 减少以太网开销:
// 启用 Header 压缩 (仅用于小数据, < 32 字节)
scanner.EnableHeaderCompression = true;
// 压缩后的帧: 去掉 Sequence Number 和 Status 字段
诊断与故障排除
连接诊断
var stats = scanner.GetConnectionStats();
Console.WriteLine($"状态: {stats.ConnectionState}"); // Running / Idle / Timeout
Console.WriteLine($"RPI: {stats.ActualRpiUs} μs"); // 实际 RPI
Console.WriteLine($"输入计数: {stats.InputPacketCount}");
Console.WriteLine($"输出计数: {stats.OutputPacketCount}");
Console.WriteLine($"丢包: {stats.LostPacketCount}");
Console.WriteLine($"IOPS: {stats.IopsProvider}"); // Good / Bad / Uncertain
连接状态机
┌──────────────┐
│ CLOSED │
└──────┬───────┘
│ Forward Open
┌──────┴───────┐
┌───►│ Establishing │
│ └──────┬───────┘
│ │
│ ┌──────┴───────┐
│ │ RUNNING │◄──── 正常数据交换
│ └──────┬───────┘
│ │ 超时 / 断线
│ ┌──────┴───────┐
│ │ TIMED_OUT │──► 重连 (自动/手动)
│ └──────┬───────┘
│ │ Forward Close
│ ┌──────┴───────┐
└────┤ CLOSED │
└──────────────┘
诊断 API
// 获取设备诊断
var diag = adapter.GetDiagnosis();
Console.WriteLine($"Identity: {diag.Vendor} {diag.DeviceType}");
Console.WriteLine($"ENIP Session: 0x{diag.SessionHandle:X8}");
Console.WriteLine($"连接数: {diag.ActiveConnectionCount}");
// 逐个连接诊断
foreach (var conn in diag.Connections)
{
Console.WriteLine($" O→T: {conn.OTPacketRate} pkt/s, 延迟={conn.OTLatencyUs} μs");
Console.WriteLine($" T→O: {conn.TOPacketRate} pkt/s, 延迟={conn.TOLatencyUs} μs");
Console.WriteLine($" IOPS: {conn.IopsProvider}/{conn.IopsConsumer}");
}
常见问题
| 症状 | 原因 | 处理 |
|---|---|---|
| Register Session 失败 | 端口 44818 被防火墙阻挡 | 检查防火墙规则 |
| Forward Open 失败 0x02 | 连接数已达上限 | 减少连接数或增大控制器连接上限 |
| Read Tag 返回 0x04 | Tag 名称拼写错误或大小写不对 | 用 ReadSymbolList() 验证 Tag 名 |
| 隐式 I/O 无数据 | UDP 2222 被防火墙阻挡 | 开放 UDP 2222 端口 |
| 连接频繁断开 (IOPS Bad) | RPI 太短或网络抖动 | 增大 RPI 或超时倍数 |
| 大 UDT 读取失败 | CIP PDU 超出 500 字节 | 改用 ReadFragmentedAsync |
| 数据交换延迟 > RPI 的 2 倍 | 网络负载过高 | 检查交换机 QoS, 增加带宽 |
| Assembly 长度不匹配 | 控制器侧和数据侧配置不一致 | 检查 GSDML/EDS 或手动的 Assembly 配置 |
| IOPS 显示 Uncertain | 数据质量不可靠, 连接不稳定 | 检查物理层, 确认网线/交换机正常 |
| 多播风暴 | 大量隐式多播连接 | 使用单播模式 (UDP 单播替代多播) |
性能优化建议
| 场景 | 优化 |
|---|---|
| 多个小 Tag 频繁读取 | 合并为多服务包 (Multiple Service Packet), 减少往返 |
| 大 UDT 或长数组 | 用分段读 (Read Fragmented) |
| 周期性 Tag 采集 | 用隐式 I/O 连接替代显式请求 |
| ControlLogix 连接 | 持久化 Forward Open, 避免反复建连 |
| 符号表初始化 | 首次连接后缓存符号表, 后续验证 Tag 名 |
| 高吞吐采集 | 启用连接分片, 增大 CIP PDU 大小 |
连接参数
Forward Open 时需要协商:
| 参数 | 常见值 |
|---|---|
| RPI (Request Packet Interval) | 10 ms ~ 100 ms, 周期数据间隔 |
| Transport Class | Class 3 (显式), Class 1 (隐式) |
| Priority | Low / High |
| Timeout Multiplier | 4 × RPI = 超时 |
| Connection Size | 500 字节默认 |
路由
CIP 支持通过消息路由器访问子网上的设备, 路径 (Path) 格式:
ControlLogix 机架里的 DCS 板卡 (插槽 4, 端口 3 到子网 1 的 Node 5):
Path = [01 04] [04 01] [01 05]
端口 1, 槽 4 端口 4, 子 1 端口 1, 地址 5
常见场景不需手写路径, 适配器的 Slot 参数已覆盖 "CPU 在本机架插槽 N" 这个最常用情况。
与 ControlLogix 的优化
- 用 Tag 名直接访问: 不要按地址偏移, CIP 本身用 Tag
- 读取符号表: 首次连接后用
ReadSymbolList()缓存全部 Tag 名, 后续验证 - 大 UDT 分片: 单 UDT > 500B 时用 Read Fragmented
- 多服务批量: 小 Tag 合并到一个请求
- 连接持久化: 持有 Forward Open, 避免反复建连
与 Micro800 的差异
Micro800 系列走 符号化路由, 需要启用 "CIP Generic" 消息模式, 且不支持全部 CIP 服务。建议:
- 只用简单 Tag (无 UDT 嵌套)
- 禁用连接分片
- 用显式消息, 不用周期 I/O
错误码
| 状态码 | 含义 |
|---|---|
| 0x00 | 成功 |
| 0x01 | 连接失败 (通用) |
| 0x02 | 资源不可用 (连接数上限) |
| 0x04 | 无效路径 |
| 0x05 | 无效路径 (段) |
| 0x08 | 无效服务 (不支持的命令) |
| 0x0C | 对象状态冲突 |
| 0x0E | 属性不可设 |
| 0x10 | 设备状态冲突 |
| 0x11 | 响应数据过大 |
| 0x13 | 请求数据不足 |
| 0x1C | 属性列表包含无效属性 |
| 0x1E | 服务出错 (通用) |
| 0x26 | Path 大小无效 |
排错
| 症状 | 处理 |
|---|---|
| Register Session 失败 | 端口 44818 被挡, 或设备不支持 ENIP |
| Forward Open 失败 0x02 | 连接数上限 |
| Read Tag 返回 0x04 | Tag 名拼写错或大小写 |
| STRING 读乱码 | 需要按 LEN+DATA 结构读, 不能直接当字节 |
| 连接频繁断开 | RPI 太小, 或网络抖动; 增大超时倍数 |
| 隐式 I/O 无数据 | UDP 2222 被防火墙阻 |
| Assembly 数据错位 | 输入/输出 Assembly 映射配置不对应 |