首页
社区
课程
招聘
[原创]C#实现西门子S71200与S71500控制器优化DB块符号访问
发表于: 8小时前 35

[原创]C#实现西门子S71200与S71500控制器优化DB块符号访问

8小时前
35

C#实现西门子S71200与S71500控制器优化DB块符号访问

本文基于 S7CommPlusDriver 代码库撰写,完整梳理从协议栈、符号寻址算法(SymbolCrc + LID)、在线符号浏览,到「离线工程浏览与在线浏览无缝统一」的架构设计与验证方法。文中所有数字与示例均来自真实 S7-1500 PLC 与真实博途工程的对拍测试。


目录

  1. 背景:优化 DB 块符号访问为什么难
  2. 整体架构
  3. 连接建立与安全通道
  4. 符号寻址核心:ItemAddress / LID / SymbolCrc
  5. 在线符号浏览:PObject 契约与按需懒加载
  6. 在线/离线一体化:一个函数让博途工程变成「虚拟 PLC」
  7. 离线解析器 TiaProjectParser
  8. 多 PLC 工程支持
  9. 类型边界:Variant 叶子
  10. 验证体系:四层对拍门禁
  11. 快速上手
  12. 结语与获取方式

一、背景:优化 DB 块符号访问为什么难

1.1 从绝对寻址到符号寻址

经典 S7-300/400 通信(如 S7.NET 生态)依赖绝对地址DB1.DBX0.0M10.2。但 S7-1200/1500 的博途工程默认给 DB 块开启**「优化的块访问」(Optimized Block Access)**,其根本变化是:

  • 变量在 PLC 内存中不再有固定的字节偏移,编译器可以自由重排、合并、按位打包;
  • 传统按偏移读写的报文(PEAK/POKE 式)对优化块完全失效——商用库 AGL 对优化块返回的偏移统一是 -1,这正是「没有偏移」的标准语义(AGL40_SYMBOLIC_DATABLOCK_IS_OPTIMIZED);
  • 唯一稳定的寻址方式是符号寻址:用「符号名 + 类型」算出的 32 位符号 CRC 加上逐层解析得到的 LID(逻辑索引目录)访问序列 来定位变量。

这带来两个技术门槛:其一,符号 CRC 与 LID 的算法需要逆向;其二,符号名只是入口,最终要拿到 PLC 内部的类型树(结构体/数组/UDT 的每一层),才能把 "DB1".Motor.Speed[2] 这样的字符串翻译成合法的访问地址。

1.2 为什么用开源方案

S7CommPlus 协议(S7-1200/1500 使用的通信协议,区别于经典 S7 协议)从未公开,官方只有闭源商业库(AGLink40)。S7CommPlusDriver 是一个开源 .NET 实现:

  • 协议栈上游基于 thomas-v2/S7CommPlusDriver(LGPL 许可,感谢 Thomas Wiens 的协议逆向工作);
  • 非 TLS 认证算法基于 bonk-dev/HarpoS7 的逆向实现;
  • 本项目在协议栈之上完成了大量工程化工作:真实 PLC 全量符号对拍、性能优化(密钥轮换微秒级)、以及本文重点的离线工程浏览与在线浏览无缝统一

⚠️ 重要声明:驱动仍处于开发与测试阶段,协议实现可能存在未知缺陷,请勿用于生产环境。

二、整体架构

解决方案 src/S7CommPlusDriver.sln 共 7 个项目:

S7CommPlusDriver.sln
├── S7CommPlusDriver/            # 核心协议库(net4.0 起,C# 7.3)
│   ├── Core/                    # S7CommPlus 协议对象模型(PObject、DatablockInfo、
│   │                            #   Softdatatype、PValue、PLCInformation…)
│   ├── ClientApi/               # 客户端 API
│   │   ├── Browser.cs           #   符号树浏览(在线)
│   │   ├── OfflineSymbolSource.cs # 离线符号源(博途工程"虚拟 PLC"引擎)
│   │   ├── PlcTag.cs / PlcTags.cs # 标签抽象 + ReadTags/WriteTags 封装
│   │   └── ItemAddress.cs       #   变量寻址结构(SymbolCrc + LID)
│   ├── Internal/                # S7CommPlusSymbolCrc(符号 CRC 算法)等
│   ├── Net/                     # 传输层(S7Client、MsgSocket)
│   ├── OpenSSL/                 # TLS P/Invoke 层(OpenSSL 3.x,x86/x64 双实现)
│   ├── Legitimation/            # 密码合法化认证
│   ├── Alarming/                # 报警处理
│   ├── Subscriptions/           # 订阅管理
│   ├── MmTimer.cs               # 高精度多媒体定时器(驱动密钥轮换)
│   └── S7CommPlusConnection.cs  # 主连接入口(唯一对外门面,约 2600 行)
├── HarpoS7Wrapper/              # 非 TLS 认证/加密 P/Invoke 封装(C#)
├── HarpoS7Native/               # 非 TLS 认证算法 C++ 原生实现
├── S7CommPlusGUIBrowser/        # WinForms 浏览器演示程序
├── DriverTest/                  # 无头功能测试 + 对拍验证工具
├── TiaProjectParser/            # 博途工程离线解析器
└── Zlib.net / TestHarpoS7Native # 依赖与原生库基准测试

三个值得注意的设计决策:

1. 统一连接门面。 S7CommPlusConnection 承载全部对外 API——Connect/DisconnectReadValues/WriteValuesBrowsegetTypeInfoByRelIdgetPlcTagBySymbolGetListOfDatablocksSetPlcOperatingStateGetCommentsXml、以及后文的主角 LoadSymbolsFromFile。上层(GUI、测试、用户代码)只见一个类。

2. 真 AnyCPU。 早期版本靠 x86/x64 两套编译配置 + _WIN64 条件编译硬编码原生 DLL 名,需要两次编译出两个包。改造后同一份编译产物按 IntPtr.Size 运行时分发:HarpoS7WrapperOpenSSL 都采用 Impl64/Impl32 嵌套静态类模式(DllImport 要求常量 DLL 名,故外层公开方法在运行时转发到对应实现),64 位 OS 自动加载 x64 原生库、32 位 OS 自动加载 x86。

3. 协议版本矩阵。 协议实现覆盖 ProtocolVersion V1/V2/V3;TLS 加密连接与非 TLS 路径按连接参数自动选择。

三、连接建立与安全通道

建立会话是一个多步骤握手过程(简化描述):

TCP 连接 → 会话协商(Hello/Version) → 认证(TLS 或非 TLS 两条路径)
→ 会话密钥协商 → 登录(可选密码合法化) → 通信资源协商
→ 读取 PLC 信息(型号/MLFB/固件/PLC 名) → 可开始读写与浏览

3.1 非 TLS 认证:HarpoS7

非 TLS 路径分 PlcSim(软 PLC)与 RealPlc(S7-1200/1500 实机)两套认证算法,核心原语包括:

  • GF(2^128) LUT 变换:4096 字节查找表,生成时涉及既约多项式 x^128 + x^7 + x^2 + x + 1 的常量拆分;
  • Monolith 布尔电路变换(自动生成的位操作代码);
  • 大整数压缩(BigIntCompressor)overflow × 0x2F 的 256 位乘法与低位对齐存储。

这些算法先在 C# 上逆向实现并真机验证,随后移植为 C++ 原生库以换取性能。为保证移植逐字节等价,配套了一套自动对拍工具链:22 个 API 测试用例(认证 8 / 合法化挑战 4 / 密钥导出 2 / 摘要 2 / 密钥存储 3 / 指纹识别 3)与 C# 参考实现逐字节比对,22/22 全部通过

对拍的价值在移植过程中被两次实锤:

  1. 大整数压缩存储位置错误harpo_bigint_compress()overflow * 0x2F 的乘积写到了 buffer[5] 之后的位置,而 C# BigInteger.Multiply 从低位 dword(buffer[0])起填充——压缩结果不一致,直接影响种子变换;
  2. LUT 生成器常量参数顺序错误make128(0x00008005, 0, 0x00000001, 0) 把 64 位 XOR 常量 0x01_0000_8005 错误拆到 lo/hi 两个 qword 上,导致 4096 字节 LUT 从第 2 项起全体错误,PLC 直接拒绝认证(校验和与 PLC 不一致)。

这两个 bug 都属于「算法正确、细节错位」类型,只有逐字节对拍能抓出来——这是协议实现工作的核心方法论。

3.2 密钥轮换

连接生命周期内会话密钥每 30 分钟轮换一次,由高精度多媒体定时器驱动。早期每次轮换都重新生成密钥材料并执行完整变换(PlcSim ~0.1 ms、RealPlc ~4.5 ms),改造为**密钥材料复用(save-once-reuse)**后——同一连接内复用 seed/IV、仅 challenge 变化——轮换耗时压缩到 20 μs 以内(PlcSim 路径 14 μs,加速 7.6 倍)。轮换流程还对齐了商用库语义:challenge(GetVarSubStreamed)与 SetVariable 共用同一个 SequenceNumber,不自增。

四、符号寻址核心:ItemAddress / LID / SymbolCrc

这是本文主题「优化 DB 块符号访问」的核心。一次变量访问最终要落到一个 ItemAddress 上:

4.1 ItemAddress 结构

public class ItemAddress : IS7pSerialize
{
    public UInt32 SymbolCrc;      // 符号 CRC(见 4.3)
    public UInt32 AccessArea;     // 访问区域:DB 块 = 0x8A0E0000 + 块号
    public UInt32 AccessSubArea;  // 子区域:DB_ValueActual / ControllerArea_ValueActual
    public List<UInt32> LID;      // 逻辑索引目录(逐层成员索引)
}

序列化按 SymbolCrc → AccessArea → LID 个数+1 → AccessSubArea → LID[] 顺序,全部用 VLQ(变长量)编码进请求报文。

  • AccessAreaSetAccessAreaToDatablock(number)0x8A0E0000 + 块号;I/Q/M/T/C 区则用各自的 NativeObjects rid(theIArea_Rid 等);
  • 访问串GetAccessString() 输出十六进制点分形式,如 8A0E0001.78.F——8A0E0001 = DB1,.78.F 是两层成员的 LID。解析函数 new ItemAddress("8A0E0001.A") 支持反向构造。

4.2 LID 访问序列:符号名如何翻译成地址

getPlcTagBySymbol("\"DB1\".Motor.Speed") 的翻译过程是一个逐层递归

parseSymbolLevel() 取出第一层 "DB1" → 命中块
browsePlcTagBySymbol(ti_relid, symbol, varInfo):
  1. getTypeInfoByRelId(ti_relid) 取该层 PObject
  2. parseSymbolLevel() 取出下一层名(引号/点号/下标三段式解析)
  3. 在 VarnameList.Names 中定位成员 → 得到 PVartypeListElement
  4. varInfo.AccessSequence += "." + varType.LID      ← 逐层追加 LID
  5. 同时把该层 (名, 类型) 记入 SymbolCrcPath
  6. 若成员是数组 → 展开下标(见下)
  7. 若成员是结构/数组元素 → 递归进入下一层

数组下标展开有专门的算法:布尔数组元素数向上对齐到 8 的倍数(PLC 按位打包);行主序的 dimSize 累乘把多维下标 [i,j] 折成线性索引;数组元素若是结构体,访问序列末尾追加 .1。最终 "DB1".Motor.Speed[2] 可能翻译成 8A0E0001.78.F.2——每个数字都是协议层真实的逻辑索引。

4.3 符号 CRC 算法

符号 CRC 是优化块的「变量身份证」。驱动实现(Internal/S7CommPlusSymbolCrc.cs)与西门子公开资料中 S7-1500 符号寻址的算法一致:

  • CRC32,多项式 0xF4ACFB13,MSB-first,初始值 0,无结果异或;
  • 单段(一个成员名):inner = CRC32(UTF8(名字) + TypeCode字节),最终值 = CRC32(inner 按 LE32 写入)
  • 数组段inner = CRC32(UTF8(名字) + 0x10(数组类型码) + 元素TypeCode + 下限(LE32))
  • 多段路径CRC32(inner0 | 0x89 | inner1 | … | innerN)——层与层之间插入结构子分隔字节 0x89
  • TypeCode 映射BBOOL → BOOL(1)AOMIDENT/EVENTATT/HWINT 等 → DWORD(6)HWANY/HWIO 等 → WORD(4)OB* → INT(5)PORT/RTM/DBANY 等 → UINT(6);其余类型用自身枚举值。

按驱动算法实际计算的例子(可直接用驱动复现):

符号 计算路径 SymbolCrc
Speed : REAL 单段,TypeCode=8 0x888EC040
MotorSpeed : REAL 单段,TypeCode=8 0xA132E807
Motor.Speed : REAL.REAL 两段,0x89 分隔 0x37AC4EA5
Setpoints : Array[0..9] of REAL 数组段(0x10 + 8 + 下限0) 0x521E174E

一个微妙但关键的点:含数组元素下标的路径 CRC 恒为 0ContainsIndexedArray → SymbolCrc = 0)——元素定位信息已经完整编码在 LID 里,CRC 只对「无下标」的符号路径负责。

4.4 批量读写

ReadValues 的完整流程:

  1. 校验许可证 → 初始化返回值与错误列表(错误初值 0xFFFFFFFFFFFFFFFF);
  2. TagsPerReadRequestMax 分块(PDU 尺寸感知,长列表自动切多请求);
  3. 每块构造 GetMultiVariablesRequest——TLS 或安全连接用 ProtocolVersion V2,普通连接用 V3
  4. 发送并等待响应 → GetMultiVariablesResponse.DeserializeFromPducheckResponseWithIntegrity 完整性校验;
  5. 逐项回填值与错误码(单变量失败不影响同批其它变量,ReturnValue 非 0 时错误明细在 ErrorValues 里)。

WriteValuesSetMultiVariablesRequest(AddressListVar + ValueList),同样分块、同样完整性校验。上层封装 PlcTags.ReadTags(conn, tags) 让调用方只面对 PlcTag 抽象。

五、在线符号浏览:PObject 契约与按需懒加载

5.1 浏览流程

Browse(out varInfoList)
├── 探索 PLC 对象树(OMS/OMM):PLCProgram(ExploreParents=1)
│   ├── 递归收集 Failsafe 层级(FailsafeRuntimeGroup 8157 / FailsafeControl 7842)
│   │   └── 下的 DB 块(ClassId=4600=MC_DB_Class_Rid,安全型 DB)
│   └── ObjectOMSTypeInfoContainer 一次性取得全部类型信息
│       ├── 存入 Browser.m_objs(浏览缓存)
│       └── 同步填充 typeInfoList(getTypeInfoByRelId 缓存,见下)
└── 逐块/逐区展开全部叶子 → VarInfo{Name, AccessSequence, Softdatatype,
                                        SymbolCrcPath, ContainsIndexedArray}

5.2 PObject 契约:每展开一层取一层

浏览模型围绕一个核心契约——getTypeInfoByRelId(relid) → PObject,给定关系 ID 返回当前一层的成员清单:

PObject
├── VarnameList.Names[]     # 本层成员名(数组父只占一个条目)
└── VartypeList.Elements[]  # 每个成员的类型形态(OffsetInfoType):
    ├── POffsetInfoType_Std        # 叶子(标准变量)
    ├── POffsetInfoType_Struct     # 结构成员 → RelationId 指向下一层类型
    ├── POffsetInfoType_Array1Dim  # 一维数组 {下限, 元素数, RelationId}
    ├── POffsetInfoType_ArrayMDim  # 多维数组 {各维下限/元素数, RelationId}
    └── POffsetInfoType_FbArray    # FB 实例数组

GUI 树每展开一层,就用节点上挂的 relid 调一次 getTypeInfoByRelId,按元素形态生成下一层节点——与 PLC 的真实交互模型一致:每展开一层取一层。配合 Browse 阶段的类型信息缓存(typeInfoList),后续展开零网络请求。这里曾有一个隐蔽 bug:Browse 只把类型信息存进 Browser 内部缓存、typeInfoList 恒空,导致 GUI 每次展开都实时向 PLC 发 ExploreRequest——网络抖动即抛 "Could not get type info";修复后展开全部走本地缓存。

浏览正确性还修过一批:Failsafe 安全型 DB 完全缺失(PLC 在 ExploreParents=0 时不返回 Failsafe 子树)、FB 实例数组不展开(Is1Dim()/IsMDim() 被标 TODO)、嵌套 UDT/FB/Struct 子节点静默丢失(新增 TypeInfoFetcher 按需回源)、类型信息里空名称条目污染节点列表(SanitizeVarnameList 倒序清理)。

六、在线/离线一体化:一个函数让博途工程变成「虚拟 PLC」

6.1 问题

早期版本里离线浏览博途工程(.apXX 归档或工程目录)与在线浏览是两套完全不同的代码:离线有独立的建树/展开/选中/路径拼接分支;展开一个块就把整棵子树一次转完(ConvertOfflineNode 全递归)、展开任一个区就一次走完全部标签表、启动时还要后台预热全部叶子;树根还额外包了一层项目名。用户操作繁琐,体验与在线割裂。

6.2 设计:LoadSymbolsFromFile 单入口

改造目标一句话:S7CommPlusDriver 只增加一个公开函数 LoadSymbolsFromFile(工程路径),调用之后浏览符号变量树、GUI 构建变量树与在线方式完全一致;旧用户从不调用它,行为零变化。

var conn = new S7CommPlusConnection();

// 加载博途工程(目录或 .apXX 归档),连接对象从此成为"虚拟 PLC"
int res = conn.LoadSymbolsFromFile(@"D:\Projects\tiaProject18");

// 传 null 即卸载工程,回纯在线浏览
conn.LoadSymbolsFromFile(null);

实现上,连接对象持有 OfflineSymbolSource _offlineSymbols;当它非空时,四处浏览 API 在许可证检查之前路由到离线源,未命中回落在线:

API 离线路由行为
GetListOfDatablocks 返回工程内 DB 块清单(映射为 DatablockInfo),不发任何网络请求
getTypeInfoByRelId 查离线 relid 注册表,命中即返回 PObject;未命中回落在线(支持「离线树 + 在线补充」混合场景)
Browse 返回离线全量叶子清单
getPlcTagBySymbol 先做离线符号解析(树走查 + 对拍叶缓存 CRC),成功即返回 PlcTag;失败回落在线

由此自然成立四个特性:

  • GUI 零分支S7CommPlusGUIBrowser 删掉全部离线专用代码(负载类、独立展开/选中/路径拼接、全叶预热),在线/离线共用同一套 BuildProjectTree() / AfterExpand / AfterSelect / btnRead——离线树只是在线树的另一个数据源。对旧用户无感是硬指标,不是口号。
  • 离线浏览 + 在线读值闭环:离线打开工程 → Connect 真实 PLC → 选中离线树的叶子直接 Read 出值。Connect/Disconnect 不清离线源:断开只影响在线读取、树保留。
  • 树根统一从 PLC 名开始:在线连接第 8 步已读取 PLC 名,现存入 PLCInformation.Name(5 字段:Name/PLCFamily/PLCType/Firmware/MLFB);离线从工程 PLC 信息镜像同一组字段。GUI 树根 = PLC 名,不再外面包一层项目名
  • 一个连接对象贯穿始终:btnConnect 复用已有 conn(不 new),不丢已加载的工程。

6.3 真正的按需懒加载

离线侧的关键约束:不能为了建树而全量解析工程。TiaProjectParser 打开一个大型 V19 工程会产出 14 万行符号,全量展开在内存与启动时间上都无法接受。改造后的引擎:

  1. Parse() 去掉 ParseAllObjects 全量解析:只做 Open(打开存储容器)+ ExtractAll(抽取符号表与块接口元数据)+ 块清单扫描(FindDataBlocks 只读块的存储对象列表,不展开接口);PlcInfoExtractor 树走查失败自动回落表扫描;
  2. relid 注册表:从 0x80000000 起按需「铸币」——块首次被展开时分配一个 relid 并缓存其符号树,之后该 relid 换成树节点继续往下走;区节点复用在线固定 relid 0x90010000/0x90020000/0x90030000/0x90050000/0x90060000(I/Q/M/T/C),不进注册表;
  3. 逐层 BuildPObjectgetTypeInfoByRelId 只把当前一层子节点转成 PObject——数组父单条目(VarnameList 只含数组父一条,元素节点由 GUI 自己生成,与在线同款语义)、结构 RelationId 指向子层类型、MDim 维度反序、叶子 Softdatatype 用树节点 Type(已实测与在线 sd 值一致)——契约与在线同形,GUI 的 AfterExpand 一行不用改
  4. 懒加载正确性的硬证据:懒路径的全量导出与改造前基线逐字节一致(83259 行 cmp 门禁)。

首次展开一个块 ≈ 在线一次类型信息请求的代价;之后的每一层展开都是零成本缓存读取。

6.4 离线读值为什么敢给 CRC

在线读优化块/非优化块都依赖 32 位符号 CRC 与 LID 访问序列,任何算错都会读到错误地址。离线读值的 VarInfo 一律取自对拍叶缓存ExportBlockLeaves/ExportTagLeaves 产出的、经真实 PLC 全量对拍校准的那批叶子),不从树节点现算 CRC——从根上消除「离线 CRC 算错 → 读错地址」的风险。对拍叶缓存本身按块/按区懒生成,且多 PLC 场景下按 PLC 键控。

七、离线解析器 TiaProjectParser

TiaProjectParser 直接解析博途工程文件,无需 TIA Portal 与任何西门子运行时:

  • 输入:工程目录或 .apXX 归档(解包后的同一套容器结构,两者逐字节一致已验证);
  • 容器PEData.plf + 分层 XML 元数据(Database.FileHeader.ProductVersion 二进制头恒在,V13=1300.109.701.1 → 版本刻度 1300)+ 存储对象流;
  • 磁盘格式只有 3 个家族:FileFormat V10 / V11 / V14——V12 起至今全部走 V14 族解析器,库内版本分支全是 <= FileFormat.V10/V11/V14,无 V19+ 专属逻辑。这解释了为什么一个 2017 年的解析器能打开 V21 工程;
  • 实测版本矩阵:V13×2、V14×2、V15×2、V19(141634 行、25 DB)、V20×3(S7-1200/1200 G2/1500)、V21(Modbus SCL 例程,3383 行)全部打开/解析/导出成功;
  • 语义细节:最新流(博途当前视图)解析——每 InstId 取最新副本、关系解析最新胜出;AT 视图子成员输出;Variant(63) 平坦行;InOut 区 DB 引用成员 → Pointer 平坦行;数组元素逐项展开;
  • 优化块的 -1 偏移:V19 工程所有偏移 -1 是正确行为(优化块本就没有静态字节偏移),不是解析失败。

八、多 PLC 工程支持

真实工程可含多台 PLC(如 tiaProject18 含 2 台:CC1 253 个块/113 个输入标签、CC2 27 个块/3 个输入标签)。设计要点:

  • relid 编码扩展:区 relid = 区基址 + plcIndex × 0x100(plcIndex 0 与原固定值完全相同,向后兼容;每区窗口 0x10000、步长 0x100,上限 256 台 PLC);PLC 节点哨兵 = 0x90070000 + plcIndex × 0x100
  • 块表按 PLC 分组GetListOfPlcs() 列 PLC 名;SelectPlc(name) 切换活动块表并镜像该 PLC 的 5 字段信息;GetListOfDatablocks(plcIndex) 按索引取块;
  • GUI 树:多 PLC 时每台 PLC 名直接作为并列树根(不包工程名层,与单 PLC「根 = PLC 名」一致),各带 "Loading..." + PLC 哨兵自动展开一层(块列表 + 5 个区);选中哪个 PLC 节点就显示哪个 PLC 的信息与符号,符号遍历经过 PLC 子树自动带出活动 PLC,Read 落在正确的 PLC 上。

一个值得记下的 API 设计教训:GetListOfDatablocks() 二参数重载曾误委托到索引 0,导致 SelectPlc 切换后仍返回 PLC[0] 的块表。语义约定必须明确:二参数 = 活动 PLC,三参数 = 按索引——并写进 --conn 门禁逐 PLC 检查(selectSwitch 一致性)。

九、类型边界:Variant 叶子

tiaProject4 的 MB_SERVER_DB(TCON.CONNECTTSEND.DATATRCV.ADDR24 个 Variant 叶)曾出现选中后数据类型与符号地址双双空白。根因有三层,每一层都独立成立:

  1. 树与对拍缓存缺口:离线符号树(非对拍模式)含 Variant 节点,但对拍叶缓存故意排除 Variant(镜像在线 Browse 不产出该类行)→ 树走查找到节点后按 Access 回查缓存落空,返回 null;
  2. TagFactory 缺 casePlcTags.TagFactory 的 Variant case 被原作者注释掉,注释理由是「Variant isn't added inside of the instance-db as a variable!」——tiaProject4 证明该假设不成立,default 分支返回 null;
  3. 协议层未实现PValue.Deserialize 对 Variant 抛 NotImplementedException,驱动不支持 Variant 值反序列化。

修复策略是「显示与取值解耦」:缓存落空时从树节点合成显示用 VarInfo(Access/Type 取树节点,CRC=0——非优化块 LID 读仍可尝试,优化块读在 PLC 侧干净失败);补 PlcTagVariant 显示用标签类(写值抛 NotSupportedException);ReadTags 捕获 NotImplementedException 返回 -1 防 GUI 崩溃(GUI 选中叶子即触发读)。结果:类型显示 Variant、地址显示 8A0E0001.78.F,普通叶子路径零回归。

十、验证体系:四层对拍门禁

协议实现工作的第一原则:没有对拍,寸步难行。本项目有四层门禁,全部自动化:

第一层:真实 PLC 在线对拍(verify7)

以 192.168.0.250 的 S7-1500(PLC 名「Powin储能项目C区」)为真值,离线解析结果逐叶对比在线 Browse:

SUMMARY online=75042 offline=83259 matched=75042 onlineOnly=0
        offlineOnly=8217 segMismatch=0 crcMismatch=0 ok=75042 GATE=PASS
READ tried=50 ok=50 fail=0
  • onlineOnly=0:离线不漏一个在线符号;
  • segMismatch=0 / crcMismatch=075042 个符号的 CRC 与访问序列全部一致——这是「离线解析的优化块地址与真机完全一致」的最强证据;
  • READ 50/50:随机抽样 50 个变量实读 PLC 全部成功;
  • offlineOnly=8217 有明确解释:PLC 上残留 8 个旧版本块(8A0E0064/012C/0190/03F2/03FC/0406/0410/041A)在离线工程里已被删除——不是解析缺口;
  • 叶清单固化为字节基线文件,每次回归 cmp

第二层:商用库对拍(AGL 名字真值)

以 AGLink40(原生 C++ 树 dump,AglTree 工具)为名字真值,对 25 个真实工程(tiaProject1-25,覆盖 V13~V21)全量矩阵对拍,0 真实缺口。要点:

  • V19 工程(141634 行导出):叶子名 35316/35427(99.7%)精确命中,AGL-only 111 行全是 In_EventStatus(S7_Pointer 叶子,我方按当前视图展开成其结构成员,AGL 只列叶子本身);
  • AGL-only 未命中叶子在全部 25 个工程中无一例外是 S7_Pointer——全部可解释;
  • p18 双 PLC 分开验证:CC2 块 2357 公共 + 142 全命中、标签叶名 143/143;
  • 对拍还双向发现真值库自身的问题:AGL 6.1.0.5 完全漏读 p18 CC1 的 F 安全子系统(F 块 DB209_C、F02000_-KE61 及标签全体缺席),我方完整;
  • AGL 无法加载 V21(-393099)——V21 无 AGL 真值,改用 plf 内嵌 XML 元数据交叉验证(12/12 顶层名、39 行 Modbus 结构逐一吻合)。

第三层:统一 API 门禁(--conn)

headless 验证 LoadSymbolsFromFile 后的行为与直用离线引擎完全等价:

  • Browse 叶数、块表块数、首块与 Inputs 区的 getTypeInfoByRelId 成员数逐项一致(tiaProject18:Browse 159165/159165 完全相等);
  • 多 PLC 逐台检查:每台块表 + 区 relid 变体 + SelectPlc 切换后活动块表一致性;
  • --sym <symbol> 符号解析探针全链路抽查,如 '"MB_SERVER_DB".TCON.CONNECT' → addr=8A0E0001.78.F type=Variant → OK
  • 报告写入 <out>.conn_check.txt,任何一项 FAIL 即门禁失败。

第四层:构建与字节门禁

VS2017 MSBuild 0 错误;TiaSymbolExport 导出与基线逐字节一致(任何解析器改造不得改变一行导出输出);HarpoS7Native 22/22 逐字节对拍(见第三节)。

十一、快速上手

11.1 在线:连接 + 浏览 + 符号读值

using S7CommPlusDriver;
using S7CommPlusDriver.ClientApi;

var conn = new S7CommPlusConnection();
int res = conn.Connect("192.168.0.1", password: "your_password");
if (res != 0) throw new Exception($"Connect failed: {res}");

// 全量浏览:拿到所有叶子(含 CRC 与访问序列)
conn.Browse(out var leaves);
Console.WriteLine($"leaves: {leaves.Count}");

// 按符号名解析标签("DB1".Motor.Speed → 8A0E0001.78.F + SymbolCrc)
var tag = conn.getPlcTagBySymbol("\"DB1\".Motor.Speed");
Console.WriteLine($"{tag.Address.GetAccessString()}  type={tag.Datatype}");

// 读值(内部自动分块、按项返回错误)
int r = PlcTags.ReadTags(conn, new[] { tag });
Console.WriteLine(r == 0 ? $"value: {tag}" : $"read failed: {r}");

11.2 离线:工程文件当 PLC 用

var conn = new S7CommPlusConnection();

// 加载博途工程(目录或 .apXX),连接对象成为"虚拟 PLC"
conn.LoadSymbolsFromFile(@"D:\Projects\tiaProject18");

// 多 PLC:列出、切换、逐台浏览
foreach (var plc in conn.GetListOfPlcs())
{
    conn.SelectPlc(plc);
    conn.GetListOfDatablocks(out var blocks);
    Console.WriteLine($"{plc}: {blocks.Count} blocks");
}

// 与在线完全同一套 API:按符号解析
var tag = conn.getPlcTagBySymbol("\"DB1\".MotorSpeed");
Console.WriteLine($"{tag.Address.GetAccessString()}  {tag.Datatype}");

11.3 离线浏览 + 在线读值闭环

// 离线打开工程 → 连接真实 PLC → 离线树的叶子直接读真值
conn.LoadSymbolsFromFile(@"D:\Projects\tiaProject1");
conn.Connect("192.168.0.250");
var tag = conn.getPlcTagBySymbol("\"DB1\".SetLimitsHW.Value");
PlcTags.ReadTags(conn, new[] { tag });
// 断开不影响已加载的工程与树
conn.Disconnect();

11.4 纯地址读写(底层)

var addr = new ItemAddress("8A0E0001.A");   // 或 SetAccessAreaToDatablock(1) + LID
conn.ReadValues(new List<ItemAddress> { addr }, out var values, out var errors);

十二、结语与获取方式

从协议栈到工程浏览,S7CommPlusDriver 走通了一条完整的技术路线:逆向 → C# 参考实现 → C++ 原生移植逐字节对拍 → 真机全量验证 → 离线/在线一体化。其中「一个函数让博途工程变成虚拟 PLC」的设计说明:好的抽象不是加一层胶水,而是让两个数据源共享同一套契约——PObject 契约 + relid 语义 + 对拍叶缓存 CRC 就是那个契约:在线是网络实现,离线是工程文件实现。

优化 DB 块的符号访问,本质是三道坎:符号 CRC 算法要算对、LID 访问序列要逐层走对、类型树要按需取对。本文给出的实现全部经过真实 PLC 与 25 个真实工程的对拍检验。

驱动仍在演进(订阅、报警、TLS 路径等方向),欢迎试用与反馈。再次提醒:勿用于生产环境。

测试 Demo 下载(本文全部验证场景对应的工程与程序):

git clone 9f3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8W2k6g2)9J5k6h3y4G2L8g2)9J5c8X3I4A6M7X3y4&6i4K6u0r3f1K6N6o6L8$3#2E0f1r3I4#2M7#2j5K6c8s2u0A6N6X3g2J5i4K6u0W2k6$3W2@1

传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

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