首页
社区
课程
招聘
基于C#实现CodesysV3符号变量访问
发表于: 7小时前 57

基于C#实现CodesysV3符号变量访问

7小时前
57

基于C#实现CodesysV3符号变量访问

CODESYS V3 是目前工业控制领域应用最广泛的软 PLC 运行时之一。本文基于一套完整的 C# 驱动实现,介绍如何从零实现 CODESYS V3 底层通信协议栈,完成连接、登录、符号变量树枚举以及变量批量读写,让上位机(SCADA、HMI、MES、数据采集)无需 OPC 服务器即可直接访问 PLC 符号变量。

1. 背景与动机

CODESYS V3 Runtime 运行在工业 PC 或嵌入式控制器上,变量默认以**符号(Symbol)**形式组织,例如 Application.PLC_PRG.nCounterApplication.GVL.TMP_BOOL。上位机访问这些变量的常见方案有:

方案 优点 缺点
OPC UA / OPC DA 服务器 标准、生态成熟 需要额外安装配置运行时组件,占用控制器资源
Modbus TCP 实现简单 只能访问被映射到寄存器区的少量变量,不支持符号名
厂商 SDK / 网关 稳定 授权费用高、平台受限、引入中间环节
直接实现 CODESYS V3 协议 无中间件、按符号名直接读写、批量性能好 需要逆向并正确实现协议栈细节

本文介绍的 CodeSysCommDriver 是一个纯 C#(.NET Framework 4.0)实现,覆盖了 CODESYS V3 协议从传输层到应用层的完整报文收发,并提供与 OPC 类似的符号浏览(Browse)/ 批量读 / 批量写 API。

2. CODESYS V3 通信协议分层

CODESYS V3 的在线通信协议是一套层层封装的二进制报文体系,每层都带自己的头与校验:

flowchart TB
    subgraph TCP 11740 / UDP 1740
        A["Block Driver Layer<br/>magic 0xE8170100 + 总长度"]
        B["Datagram Layer<br/>magic 0xC5、服务号、地址、4字节对齐填充"]
        C["Channel Layer<br/>OpenChannel / AppBlock / ACK / KeepAlive"]
        D["Service Layer<br/>命令组 CmdGroup + 子命令 SubCmd + SessionId"]
        E["Tag Layer<br/>TLV 结构(7bit 变长整数编码)"]
    end
    A --> B --> C --> D --> E
  • Block Driver Layer:仅 TCP 使用,8 字节头(4 字节 magic 0xE8170100 + 4 字节总长度),用于粘包拆包。
  • Datagram Layer:6 字节头,magic 为 0xC5,包含跳数、优先级、服务号(NS 名称服务 / 通道管理等)、收发地址长度等位域;地址后按 4 字节对齐填充。
  • Channel Layer:通道管理消息(打开/关闭通道、KeepAlive、通道错误)与应用数据块消息。大数据包被拆分为 ApplicationBlockFirst(20 字节头,含 CRC32 与剩余长度)+ 若干 ApplicationBlock(12 字节头),接收方对每个数据块回 ApplicationAck 做流控。
  • Service Layer:20 字节头,protocol_id 固定为 0xCD55,随后是命令组 CmdGroup(如 CmpIecVarAccess = 0x09CmpDevice = 0x01)、子命令 SubCmdsession_id、内容长度等。
  • Tag Layer:所有业务数据(变量名、类型、数值)都以 Tag 承载。Tag 是 TLV 结构:ID 与长度使用 7bit 变长整数编码,ID ≥ 0x80 的 Tag 为父节点,可嵌套子 Tag。

一个重要的工程约束是 PDU 最大长度 516 字节MAX_PDU_SIZE)。批量读写大量变量时,必须正确实现多包拆分与 ACK 流控,否则控制器会停止应答。

3. 整体架构设计

驱动按职责分为六层,从上到下依次为:

职责
CodeSysController 对外门面:连接/断开、符号树加载与浏览、批量读写、类型转换、缓存与并发控制
CodeSysV3Channel 通道会话:Send/Read 请求应答、SessionCreate、Login/Logout、设备信息
CodeSysV3Device 传输设备:TCP/UDP Socket、收发线程、粘包拆包、KeepAlive、ACK 流控
CodeSysV3Protocol 协议编解码:各层报文构建与解析、Tag 层编解码
CodeSysV3Structures 线格式结构体:Datagram/Channel/Service 各层头部、TagCrc32
CodeSysV3Encryption 登录口令加密(挑战-应答机制)
CdesysGlobalVarXmlFile 离线符号表:解析 CODESYS 导出的 Symbol configuration XML
CodesysVarTreeNode / ItemAddress / PValue 符号树节点、浏览结果项、类型化值包装

调用方只与 CodeSysControllerItemAddressPValue 三个公开类型打交道,其余均为 internal

4. 连接、会话与登录

4.1 传输层探测

CODESYS 控制器可能走 UDP(端口 1740)或 TCP(端口 11740)。驱动先通过 UDP 向端口 1740 发送 AddressResolve 名称服务请求,若得到目标地址则选 UDP;否则回退 TCP,向 11740 发送 NameResolve 请求验证。

4.2 获取设备信息与打开通道

连接成功后,通过名称服务(NS)获取设备信息:node_namedevice_namevendor_namefirmware_str。随后发送 OpenChannelRequest,报文包含 CRC32 校验和与接收缓冲区大小(0x0007d000),应答中携带通道号 channel_id,后续所有业务报文都基于该通道。

4.3 会话创建与登录

不同固件版本握手流程略有差异(实测固件 3.5.17 无需 SessionCreate,驱动按版本号自动判断跳过)。登录采用**挑战-应答(challenge-response)**机制:客户端将控制器下发的挑战值与登录口令一起做摘要计算后提交,控制器校验通过后返回 session_id,此后所有 Service Layer 请求都必须带上它。出于安全考虑,口令摘要算法的实现细节此处从略。

4.4 心跳保活

控制器会周期性向通道发送 KeepAlive 消息,驱动收到后原样回发确认。每累计 6 次 KeepAlive,驱动触发一次内部事件,重新执行 CmpIecVarAccess0x07 子命令刷新会话句柄(tag_0x38 / tag_0x3a)——这些句柄会随控制器会话状态变化而失效,必须周期性更新。

5. 符号变量的枚举与解析

5.1 在线符号树遍历

符号变量访问的核心命令组是 CmpIecVarAccess(0x09),驱动按如下流程构建符号树:

sequenceDiagram
    participant C as C# Driver
    participant P as CODESYS Runtime
    C->>P: CmpIecVarAccess 0x07(获取根符号)
    P-->>C: 0x30 根节点名 + 0x38/0x3a 会话句柄
    loop 按需展开
        C->>P: CmpIecVarAccess 0x08(0x40 标志 + 0x39 句柄 + 0x32 地址 + 0x30 路径)
        alt 中间节点
            P-->>C: 0x30 子节点列表
        else 叶子节点
            P-->>C: 0x31 类型信息 + 0x44
        end
    end
    C->>P: CmpIecVarAccess 0x09(0x32 + 0x33 块地址)
    P-->>C: 0xB4 块(0x31 成员 / 0x35 元素类型 / 0x36 数组信息)
  • 0x07:获取顶层符号(Application 等根节点);
  • 0x08:给定路径浏览子节点。响应中出现 0x30 说明是中间节点(如程序、功能块、全局变量表),出现 0x31 说明是叶子变量,其数据区第 3 字节即类型码tag_0x31.data[2]);
  • 0x09:对结构体(STRUCT)与数组(ARRAY)节点,携带节点类型信息末尾 8 字节作为块地址,请求其成员/元素布局。0x36 标签描述数组维数、各维起止下标与总字节数。

驱动对一维/二维/三维数组分别展开,按 arrName[0]arrName[i,j]arrName[i,j,k] 的形式为每个元素构造类型信息 Tag 并挂入符号树。

5.2 离线符号表(Symbol configuration XML)

在线遍历每个节点都需要一次网络往返。若工程较大,可在 CODESYS IDE 中为 Application 添加 Symbol configuration 对象,导出 XML 符号表;驱动解析其 TypeList(TypeSimple / TypeArray / TypeUserDef)与 NodeList,在本地构建同样的符号树,实现零网络开销的浏览。解析器做了两点关键处理:

  • 延迟展开:数组与嵌套结构体通过 Func<Node, List<Node>> 展开动作在首次访问 Children 时才生成,避免加载时递归构建整个大数组;
  • 循环引用保护:结构体间可能出现间接类型循环(如 T_A → ARRAY OF T_B → T_B → T_A),展开链携带 visitedTypes 集合,检测到环立即停止展开。

6. 变量读取流程

读取分两步:注册(可选、可缓存)读取

6.1 变量注册(SubCmd 0x01)

按变量名列表组织 Tag 请求:

Tag tag_0x13 = new Tag(0x13, new byte[] { 0x48, 0x00, 0x01, 0x00 }); // 标志
tag_lst.Add(tag_0x13);
tag_lst.Add(tag_0x3a);                                          // 会话句柄
tag_lst.Add(new Tag(0x18, BitConverter.GetBytes((uint)count))); // 变量个数
foreach (var name in variableNames)
{
    data_buffer.AddRange(BitConverter.GetBytes((ushort)(varLen + 1)));
    data_buffer.AddRange(encoding.GetBytes(name));
    data_buffer.AddRange(generateSuffix(varLen));               // 补齐到4字节边界
    tag_lst.Add(new Tag(0x19, data_buffer.ToArray(), 0x42));    // 名称+类型请求
}

响应中的 0x10 Tag 是本次注册列表的读取句柄。驱动以变量名列表的 SHA256 为键缓存该句柄(Tag_0x10Cache),同一组变量的重复读取不再注册。

6.2 读取(SubCmd 0x03)

携带注册句柄发送 0x03 请求,响应中每个变量的数据位于 0x1d Tag(跳过 4 字节头部即为原始字节数据),错误信息位于 0x20。驱动按变量类型把原始字节转换为 .NET 对象。

sequenceDiagram
    participant C as C# Driver
    participant P as CODESYS Runtime
    Note over C: 首次:按变量名列表构建 0x13/0x3a/0x18/0x19
    C->>P: CmpIecVarAccess 0x01(注册变量组)
    P-->>C: 0x10 读取句柄(缓存于 Tag_0x10Cache)
    C->>P: CmpIecVarAccess 0x03(携带 0x10 句柄)
    P-->>C: 0x1d 各变量数据 + 0x20 错误码

6.3 多包传输与 ACK 流控

由于 PDU 上限 516 字节,批量读取的应答会被拆成多个 ApplicationBlock。接收线程按 blk_id 递增顺序拼接载荷,每收到一个块即回发 ApplicationAck(blk_id),控制器收到 ACK 后继续发送下一块;ApplicationBlockFirst 头中的 remaining_data_size 用于判断整条消息是否收齐,收齐后一次性唤醒等待的调用方。

7. 变量写入流程

写入使用 SubCmd 0x04,报文组织为:

Tag tag_0x13 = new Tag(0x13, new byte[] { 0x00, 0x00, 0x00, 0x00 });
Tag tag_0x21 = new Tag(0x21, nameBytes + suffix); // 变量名
Tag tag_0x1e = new Tag(0x1e, writeData);          // 值数据

0x1e 的值数据布局为:偏移 0 处 2 字节为数据长度,偏移 4 处开始为数值本体(布尔与 8 字节以下类型用 8 字节缓冲,64 位类型用 12 字节缓冲)。应答中的 0x20 状态码为 0 表示写入成功。

字符串写入有一个容易踩坑的细节:CODESYS 端字符串缓冲区长度固定,写入前必须先读取一次该变量获取缓冲长度,再按 [长度-3][数据...] 的格式打包(WSTRING 为 [长度-2] + Unicode 数据)。驱动用 stringVarResLenCache 缓存每个字符串变量的缓冲长度,避免每次写入都多一次读取。

8. 数据类型映射与转换

驱动定义 CodesysVarType 枚举与 CODESYS IEC 类型码一一对应,读取时将原始字节按小端序转换为 .NET 类型:

IEC 类型 类型码 C# 类型 IEC 类型 类型码 C# 类型
BOOL 0x00 bool USINT 0x0A byte
BYTE 0x02 byte UINT 0x0B ushort
WORD 0x03 ushort UDINT 0x0C uint
DWORD 0x04 uint ULINT 0x0D ulong
LWORD 0x05 ulong REAL 0x0E float
SINT 0x06 sbyte LREAL 0x0F double
INT 0x07 short STRING 0x10 string (ASCII)
DINT 0x08 int WSTRING 0x11 string (Unicode)
LINT 0x09 long TIME/DATE 等 0x12+ uint/ulong

类型解析采用三级回退:先查浏览缓存 varCache,再查 XML 符号树,最后沿 PLC 在线树按路径查找,保证无论符号表来自在线遍历还是离线文件都能正确转换。

9. 工程化细节

这个驱动在生产环境打磨过,几个值得借鉴的细节:

  • 并发控制SendAndRecv 全程持 _lock,串行化「发请求-等应答」,杜绝通道上请求应答错位;符号树结构修改由 _treeLock 保护;批量加载期间置 _isBulkLoading 抑制心跳请求,避免心跳与遍历争抢锁导致级联延迟。
  • 句柄管理:对外暴露的 IntPtr 句柄实为 GCHandle 指针,每个节点只分配一次并缓存;重建符号树时以栈迭代递归释放全部句柄(防止深树栈溢出与内存泄漏)。
  • 收包热路径:后台线程 BlockingCollection 解耦 socket 接收与协议解析;拆包用位运算读取小端长度、Buffer.BlockCopy 搬运,避免 LINQ 临时分配。
  • 缓存体系:注册句柄缓存(Tag_0x10Cache,SHA256 键)、结构体/数组块缓存(specificBlockCache)、字符串缓冲长度缓存、浏览结果缓存(varCache)各司其职。
  • 异常隔离:心跳失败、单次遍历通信失败均被捕获隔离,不影响主流程;协议层统一抛 CodeSysProtocolV3Exception

10. 使用示例

// 1. 连接控制器
var controller = new CodeSysController();
controller.UserName = "admin";
controller.Password = "admin";
controller.Connect("192.168.250.187");

// 2. 加载符号树:在线遍历
IntPtr root = controller.LoadSymbolsFromPLC();
// 或加载 CODESYS 导出的符号配置 XML(离线,零网络开销)
// IntPtr root = controller.LoadSymbolsFromFile(@"D:\PLC\App.Device.Application.xml");

// 3. 收集全部叶子变量
List<ItemAddress> items = controller.LoadAllSymbolicInfo();

// 4. 类型化单点读写
controller.ReadInt("Application.PLC_PRG.nCounter", out int n);
controller.WriteInt("Application.PLC_PRG.nCounter1", 234);
controller.WriteBool("Application.GVL.TMP_BOOL", true);
controller.WriteString("Application.GVL.TMP_STR", "OKay1");

// 5. 批量读写(一次注册、持续轮询)
var names = new List<string> { "Application.GVL.TMP_BOOL", "Application.GVL.TMP_DINT" };
controller.Read(names, out List<PValue> values);          // 批量读,类型化结果
controller.Write("Application.GVL.TMP_DINT", new ValueDInt(102)); // PValue 写入

// 6. 断开
controller.Disconnect();

典型批量读性能:10 个变量一组单次往返约 1ms 量级(取决于网络与控制器负载),配合注册句柄缓存可满足高频轮询场景。

11. 总结

本文完整梳理了 CODESYS V3 协议的层次结构、连接登录、符号树遍历、批量读写与类型转换,并总结了驱动在并发、缓存、句柄管理上的工程实践。相比 OPC 方案,直连协议栈没有中间件依赖,符号名直读直写,非常适合对部署轻量性与响应时延有要求的工业上位机场景。驱动还附带 PLCShell 远程命令与文件上传下载(CmpFileTransfer)模块,可在此基础上继续扩展。

相关测试 Demo(含 GUI 浏览器)见仓库链接:

34bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8W2k6g2)9J5k6h3y4G2L8g2)9J5c8X3I4A6M7X3y4&6i4K6u0r3b7$3!0V1k6i4y4&6M7#2j5K6c8s2u0A6N6X3g2J5i4K6u0W2k6$3W2@1


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回