# PLC 配置文件(XML)解析与生成说明文档 > **面向对象**: QT 上位机开发人员 > **配置文件**: `config/PLC/Reclose_logic.xml` > **文件编码**: UTF-8 > **文档日期**: 2026-07-08 --- ## 一、概述 PLC 逻辑配置使用 XML 格式存储。一个 XML 文件定义一个 PLC 逻辑工程,由若干条**逻辑链(Chain)**组成。每条链描述一组输入信号经逻辑门运算后输出到目标信号的过程。 QT 上位机需要实现: 1. **解析(读取)**:将 XML 文件解析为内存中的逻辑图数据结构,用于画布展示和编辑 2. **生成(写入)**:将用户编辑完的逻辑图保存/导出为 XML 文件,下发到 RTU 设备 --- ## 二、XML 整体结构 ``` ... ``` ### 2.1 根元素 `` | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | `name` | string | 是 | 工程名称,如 `"PLC Logic"` | | `key` | string | 是 | 工程唯一标识,建议使用时间戳毫秒数(如 `"1783070862205"`) | | `checksum` | string | 是 | 32 位 CRC 校验值,大写十六进制字符串(如 `"3486BE04"`)。通常使用 CRC-32/MPEG-2 算法对整个 XML 内容(不含 checksum 属性本身)计算得到 | --- ## 三、元素详解 ### 3.1 `` — 逻辑链 每条 `` 代表一组相关的逻辑运算。可独立存在,也可通过信号编号组合。 | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | `gates` | int | 是 | 链内逻辑门总数(含顶层 Gate 和 Comb 内部 Gate) | | `inputs` | int | 是 | 链内输入端信号总数 | | `outputs` | int | 是 | 链内输出端信号总数 | **两种形态**: | 形态 | 子元素 | 场景 | |------|--------|------| | **简单链** | `` + `` + `` | 单层逻辑:若干输入信号经一个逻辑门运算后输出 | | **组合链** | `` + `` + `` | 多层嵌套逻辑:Comb 内部可嵌套子 Comb | ### 3.2 `` — 输入信号集 ```xml ``` | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | `no` | int | 是 | **信号编号**,对应 RTU 数据中心 out 信号的序号(dc_signal_out 的 `no` 字段) | | `x` | int | 否 | 可视化画布 X 坐标像素值,解析/生成时可忽略(仅用于前端画布渲染) | | `y` | int | 否 | 可视化画布 Y 坐标像素值,解析/生成时可忽略 | > **关键映射关系**:`Signal.no` ↔ 数据中心 `dc_signal_out` 记录的 `no` 字段。QT 上位机需通过 WebSocket 或 HTTP API 获取信号列表(`signal_type` = `"out"`),从中取得各信号的 `no` 和 `saddr`(信号地址)。 ### 3.3 `` — 输出信号集 ```xml ``` 与 `` 结构完全相同。区别在于语义:`` 中的信号编号对应 **PLC 输出信号**(通常 `no >= 1361` 为 RTU 内置预留号段)。 ### 3.4 `` — 逻辑门 ```xml ``` | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | `type` | string | 是 | 逻辑门类型,取值见下表 | | `id` | int | 是 | 同类型门内序号,从 0 起递增,同类型门内唯一 | | `x` | int | 否 | 可视化画布 X 坐标 | | `y` | int | 否 | 可视化画布 Y 坐标 | | `delayMs` | int | 否 | **延迟毫秒数**(仅 `T62P`/`T62D` 有效),默认 1000 | | `R` | string | 否 | **复位输入来源**(仅 `SR`/`RS` 有效),格式见下方说明 | | `S` | string | 否 | **置位输入来源**(仅 `SR`/`RS` 有效),格式见下方说明 | #### 逻辑门类型对照表 | `type` 字符串 | 含义 | 输入数 | 输出数 | 说明 | |---------------|------|--------|--------|------| | `OR` | 或门 | 1~10 | 1~10 | 任一输入为 1 则输出 1 | | `AND` | 与门 | 1~10 | 1~10 | 全部输入为 1 则输出 1 | | `NOT` | 非门 | 1 | 1~10 | 输入取反输出 | | `T62P` | 62p 延时 | 1 | 1~10 | 输入 1 延时 `delayMs` ms 后输出 1,输入 0 立即输出 0 | | `T62D` | 62d 延时 | 1 | 1~10 | 输入 1 立即输出 1,输入 0 延时 `delayMs` ms 后输出 0 | | `RISING` | 上升沿 | 1 | 1~10 | 0→1 跳变时输出脉冲 | | `FALLING` | 下降沿 | 1 | 1~10 | 1→0 跳变时输出脉冲 | | `SR` | SR 触发器 | 2 | 1~10 | S 置位优先。其中引脚顺序为 pin0=S(置位),pin1=R(复位) | | `RS` | RS 触发器 | 2 | 1~10 | R 复位优先。其中引脚顺序为 pin0=R(复位),pin1=S(置位) | #### T62P/T62D 延时示例 ```xml ``` #### SR/RS 触发器的 R/S 属性 `R` 和 `S` 的值是**来源描述字符串**,格式为 `Comb#N` 或 `Input#N`: | 值格式 | 含义 | |--------|------| | `Input#59` | 该引脚由输入信号 `no=59` 直接驱动 | | `Comb#0` | 该引脚由 `id=0` 的 Comb 块内的 Gate 输出驱动 | ```xml ``` > **注意**:SR/RS 的两个引脚各有独立角色(S/R),连线时每个引脚只能接一根线。QT 上位机需分别显示两个输入引脚并标注 S/R 标签。 ### 3.5 `` — 组合逻辑块(核心难点) `` 是**可递归嵌套**的逻辑组合块,用于表达多层级的复杂逻辑。每个 `` 必须有唯一的 `id` 属性。 | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | `id` | int | 是 | Comb 块唯一标识,从 0 起递增。自底向上分配(最内层 Comb id=0,向外递增)。用于 RS/SR 引脚引用(`R="Comb#0"`) | ```xml ``` **解析规则**: 1. **本层 Inputs**:直接连到本层 Gate 的输入端 2. **嵌套子 Comb**:子 Comb 的 Gate 输出 → 连到本层 Gate 的输入端(一根线) 3. **本层 Gate**:接收上述所有输入,运算后输出到 Outputs 4. **递归终止**:最内层 Comb 没有嵌套子 Comb,只有 Inputs → Gate → Outputs 5. **Comb id 分配**:后序遍历(自底向上),最内层 id=0,向外逐层递增 **数据流示意**: ``` [本层Inputs] ──┐ ├──→ [本层 Gate] ──→ [本层 Outputs] [子Comb Gate] ─┘ ``` ### 3.6 组合链中的链级元素 与之前不同,组合链现已支持在 `` 块与链级 `` 之间插入链级 ``,用于 SR/RS 触发器等需要同时接收 Comb 输出和直接 Input 信号的场景: ```xml ``` **组合链子元素顺序**:`` → ``(可选)→ `` → `` --- ## 四、完整解析算法(C++ / Qt) ### 4.1 数据结构定义 ```cpp #include #include #include #include // 逻辑门类型枚举 enum GateType { GATE_OR = 2, GATE_AND = 3, GATE_NOT = 4, GATE_T62P = 5, // 62p 延时 GATE_T62D = 6, // 62d 延时 GATE_RISING = 7, // 上升沿 GATE_SR = 8, // SR 触发器 GATE_RS = 9, // RS 触发器 GATE_FALLING = 10 // 下降沿 }; // 信号节点 struct PlcSignal { int no; // 信号编号(映射 dc_signal_out.no) int x, y; // 画布坐标 }; // 逻辑门 struct PlcGate { GateType type; int id; // 同类型门内序号 int x, y; // 画布坐标 int delayMs; // 延时毫秒数(仅 T62P/T62D,默认 1000) QString rsSSrc; // S 引脚来源(仅 SR/RS,如 "Input#13"、"Comb#0") QString rsRSrc; // R 引脚来源(仅 SR/RS) int combId; // 所在 Comb 块的 id(用于生成 R/S 引用) }; // 连线 struct PlcLink { int srcType; // 源节点类型编码 int srcId; // 源节点 ID int dstType; // 目标节点类型编码 int dstId; // 目标节点 ID int dstPin; // 目标输入引脚序号(仅 SR/RS 有效:SR: 0=S/1=R, RS: 0=R/1=S; 其他门默认 -1) }; // Comb 组合块 struct PlcComb { int id; // Comb 块标识(自底向上编号) QVector inputs; // 本层输入信号 QVector subCombs; // 嵌套子 Comb PlcGate gate; // 本层逻辑门 QVector outputs; // 本层输出信号 }; // 逻辑链 struct PlcChain { int gatesCount; // gates 属性 int inputsCount; // inputs 属性 int outputsCount; // outputs 属性 bool isComb; // true=组合链, false=简单链 QVector inputs; // (简单链/组合链) 链级输入信号 QVector topCombs; // (组合链) 顶层 Comb 列表 PlcGate gate; // 链级逻辑门(组合链的顶层Gate,或简单链的Gate) QVector outputs; // 输出信号 }; // 整个配置文件 struct PlcConfig { QString name; QString key; QString checksum; QVector chains; }; ``` ### 4.2 XML 解析实现 ```cpp #include #include #include // 从 XML 字符串解析逻辑门类型 static GateType parseGateType(const QString &s) { if (s == "OR") return GATE_OR; if (s == "AND") return GATE_AND; if (s == "NOT") return GATE_NOT; if (s == "T62P") return GATE_T62P; if (s == "T62D") return GATE_T62D; if (s == "RISING") return GATE_RISING; if (s == "SR") return GATE_SR; if (s == "RS") return GATE_RS; if (s == "FALLING") return GATE_FALLING; return GATE_OR; // 默认 } // 从 Gate 类型得到字符串 (生成 XML 用) static QString gateTypeToString(GateType t) { switch (t) { case GATE_OR: return "OR"; case GATE_AND: return "AND"; case GATE_NOT: return "NOT"; case GATE_T62P: return "T62P"; case GATE_T62D: return "T62D"; case GATE_RISING: return "RISING"; case GATE_SR: return "SR"; case GATE_RS: return "RS"; case GATE_FALLING: return "FALLING"; } return "OR"; } // 解析 元素 static PlcSignal parseSignal(const QDomElement &el) { PlcSignal s; s.no = el.attribute("no").toInt(); s.x = el.attribute("x").toInt(); s.y = el.attribute("y").toInt(); return s; } // 解析 元素 static PlcGate parseGate(const QDomElement &el) { PlcGate g; g.type = parseGateType(el.attribute("type")); g.id = el.attribute("id").toInt(); g.x = el.attribute("x").toInt(); g.y = el.attribute("y").toInt(); g.delayMs = el.attribute("delayMs", "1000").toInt(); // 默认 1000ms g.rsSSrc = el.attribute("S", ""); // SR/RS 的 S 来源 g.rsRSrc = el.attribute("R", ""); // SR/RS 的 R 来源 g.combId = -1; // 由解析过程赋值 return g; } // 解析 元素 static QVector parseSignalList(const QDomElement &parent, const QString &tagName) { QVector list; QDomElement container = parent.firstChildElement(tagName); if (!container.isNull()) { QDomElement sig = container.firstChildElement("Signal"); while (!sig.isNull()) { list.append(parseSignal(sig)); sig = sig.nextSiblingElement("Signal"); } } return list; } // 递归解析 元素 static bool parseComb(const QDomElement &combEl, PlcComb &comb) { if (combEl.isNull()) return false; // 0. 读取 Comb id 属性 comb.id = combEl.attribute("id", "0").toInt(); // 1. 本层直接输入 comb.inputs = parseSignalList(combEl, "Inputs"); // 2. 递归解析嵌套子 Comb QDomElement subComb = combEl.firstChildElement("Comb"); while (!subComb.isNull()) { PlcComb sub; if (parseComb(subComb, sub)) { comb.subCombs.append(sub); } subComb = subComb.nextSiblingElement("Comb"); } // 3. 本层 Gate QDomElement gateEl = combEl.firstChildElement("Gate"); if (!gateEl.isNull()) { comb.gate = parseGate(gateEl); comb.gate.combId = comb.id; // 回填所在 Comb 的 id } // 4. 本层输出 comb.outputs = parseSignalList(combEl, "Outputs"); return true; } // 解析整条 static PlcChain parseChain(const QDomElement &chainEl) { PlcChain chain; chain.gatesCount = chainEl.attribute("gates").toInt(); chain.inputsCount = chainEl.attribute("inputs").toInt(); chain.outputsCount = chainEl.attribute("outputs").toInt(); // 判断是否有 Comb(组合链) QDomElement combEl = chainEl.firstChildElement("Comb"); chain.isComb = !combEl.isNull(); if (chain.isComb) { // 组合链: 解析所有顶层 Comb(可能有多个) QDomElement cEl = combEl; while (!cEl.isNull()) { PlcComb comb; parseComb(cEl, comb); chain.topCombs.append(comb); cEl = cEl.nextSiblingElement("Comb"); } // 链级 Inputs(Comb 之后,Gate 之前,如 RS/SR 的直接 Input 连接) chain.inputs = parseSignalList(chainEl, "Inputs"); // 链级 Gate QDomElement gEl = chainEl.firstChildElement("Gate"); if (!gEl.isNull()) chain.gate = parseGate(gEl); // 链级 Outputs chain.outputs = parseSignalList(chainEl, "Outputs"); } else { // 简单链: Inputs → Gate → Outputs chain.inputs = parseSignalList(chainEl, "Inputs"); QDomElement gEl = chainEl.firstChildElement("Gate"); if (!gEl.isNull()) chain.gate = parseGate(gEl); chain.outputs = parseSignalList(chainEl, "Outputs"); } return chain; } // 主解析函数: 从文件路径加载 XML PlcConfig parseXmlFile(const QString &filePath) { PlcConfig config; QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Cannot open file:" << filePath; return config; } QDomDocument doc; if (!doc.setContent(&file)) { qWarning() << "XML parse error:" << filePath; file.close(); return config; } file.close(); QDomElement root = doc.documentElement(); if (root.tagName() != "PLCConfig") { qWarning() << "Root element is not "; return config; } config.name = root.attribute("name"); config.key = root.attribute("key"); config.checksum = root.attribute("checksum"); QDomElement chainEl = root.firstChildElement("Chain"); while (!chainEl.isNull()) { config.chains.append(parseChain(chainEl)); chainEl = chainEl.nextSiblingElement("Chain"); } qDebug() << "Parsed" << config.chains.size() << "chains from" << filePath; return config; } ``` --- ## 五、XML 生成算法(C++ / Qt) ### 5.1 生成实现 ```cpp #include #include #include // 计算 CRC-32/MPEG-2(与 RTU 一致) // 简化版: 实际应使用完整的 CRC32 MPEG2 查表算法 // 此处仅示意,生产环境请使用完整 CRC 实现库(如 Qt 的 qChecksum 或 zlib crc32) // 生成 元素 static QDomElement createSignalEl(QDomDocument &doc, const PlcSignal &sig) { QDomElement el = doc.createElement("Signal"); el.setAttribute("no", sig.no); el.setAttribute("x", sig.x); el.setAttribute("y", sig.y); return el; } // 生成 元素 static QDomElement createGateEl(QDomDocument &doc, const PlcGate &gate) { QDomElement el = doc.createElement("Gate"); el.setAttribute("type", gateTypeToString(gate.type)); el.setAttribute("id", gate.id); el.setAttribute("x", gate.x); el.setAttribute("y", gate.y); // T62P/T62D: 延时毫秒 if (gate.type == GATE_T62P || gate.type == GATE_T62D) { el.setAttribute("delayMs", gate.delayMs); } // SR/RS: 输出 R/S 属性(格式 "Comb#N" 或 "Input#N") if (gate.type == GATE_SR || gate.type == GATE_RS) { if (!gate.rsSSrc.isEmpty()) el.setAttribute("S", gate.rsSSrc); if (!gate.rsRSrc.isEmpty()) el.setAttribute("R", gate.rsRSrc); } return el; } // 生成 / 容器 static QDomElement createSignalListEl(QDomDocument &doc, const QString &tagName, const QVector &signals) { QDomElement el = doc.createElement(tagName); for (const auto &s : signals) { el.appendChild(createSignalEl(doc, s)); } return el; } // 递归生成 元素 static QDomElement createCombEl(QDomDocument &doc, const PlcComb &comb) { QDomElement el = doc.createElement("Comb"); el.setAttribute("id", comb.id); // Comb id 属性 // Inputs if (!comb.inputs.isEmpty()) { el.appendChild(createSignalListEl(doc, "Inputs", comb.inputs)); } // 嵌套子 Comb for (const auto &sub : comb.subCombs) { el.appendChild(createCombEl(doc, sub)); } // Gate el.appendChild(createGateEl(doc, comb.gate)); // Outputs if (!comb.outputs.isEmpty()) { el.appendChild(createSignalListEl(doc, "Outputs", comb.outputs)); } return el; } // 生成 元素 static QDomElement createChainEl(QDomDocument &doc, const PlcChain &chain) { QDomElement el = doc.createElement("Chain"); el.setAttribute("gates", chain.gatesCount); el.setAttribute("inputs", chain.inputsCount); el.setAttribute("outputs", chain.outputsCount); if (chain.isComb) { // 组合链: 所有顶层 Comb for (const auto &comb : chain.topCombs) { el.appendChild(createCombEl(doc, comb)); } // 链级 Inputs(如有直接连接,如 RS/SR 的 Input 直连) if (!chain.inputs.isEmpty()) { el.appendChild(createSignalListEl(doc, "Inputs", chain.inputs)); } // 链级 Gate el.appendChild(createGateEl(doc, chain.gate)); // 链级 Outputs if (!chain.outputs.isEmpty()) { el.appendChild(createSignalListEl(doc, "Outputs", chain.outputs)); } } else { // 简单链: Inputs → Gate → Outputs if (!chain.inputs.isEmpty()) { el.appendChild(createSignalListEl(doc, "Inputs", chain.inputs)); } el.appendChild(createGateEl(doc, chain.gate)); if (!chain.outputs.isEmpty()) { el.appendChild(createSignalListEl(doc, "Outputs", chain.outputs)); } } return el; } // 主生成函数: 将配置写入 XML 文件 bool writeXmlFile(const PlcConfig &config, const QString &filePath, const QString &checksumHex) { QDomDocument doc; // XML 声明 QDomProcessingInstruction pi = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""); doc.appendChild(pi); // 根元素 QDomElement root = doc.createElement("PLCConfig"); root.setAttribute("name", config.name); root.setAttribute("key", config.key); root.setAttribute("checksum", checksumHex); // CRC32 十六进制大写 doc.appendChild(root); // 所有 Chain for (const auto &chain : config.chains) { root.appendChild(createChainEl(doc, chain)); } // 写入文件 QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "Cannot write file:" << filePath; return false; } QTextStream stream(&file); stream.setCodec("UTF-8"); doc.save(stream, 4); // 4 空格缩进 file.close(); qDebug() << "XML saved to" << filePath; return true; } ``` ### 5.2 checksum 计算方法 ```cpp #include // 或使用 Qt + 自定义 CRC32 MPEG2 表 // CRC-32/MPEG-2 多项式: 0x04C11DB7, 初始值: 0xFFFFFFFF // 注意: 计算时应对 XML 字符串的内容计算,**不含** checksum 属性自身 // 实战中建议调用 RTU 侧接口获取 checksum 值,或在生成 XML 后 // 通过 RTU 命令行方式让设备重新计算 checksum ``` > **建议**:`key` 字段用 `QDateTime::currentMSecsSinceEpoch()` 生成时间戳毫秒字符串。`checksum` 字段可调用 RTU 侧提供的 CRC 计算接口,或在生成 XML 后由 RTU 自动校验/更新。 --- ## 六、简单链与组合链对照 ### 6.1 简单链示例 ```xml ``` **逻辑等效**:`out[1361] = in[59] OR in[60]` ### 6.2 组合链示例 ```xml ``` **逻辑等效**: ``` 内层1 Gate(AND id=0): out[1363..1365] = in[62] AND in[63] AND in[64] AND in[65] 内层2 Gate(AND id=1): out[1366] = in[66] AND 内层1_Gate 外层 Gate(OR id=1): out[1367] = in[67] OR 内层2_Gate 链级 Gate(NOT id=1): out[1368] = NOT 外层_Gate ``` ### 6.3 时序门简单链示例 ```xml ``` ### 6.4 RS/SR 触发器示例 ```xml ``` --- ## 七、信号编号(no)的获取 QT 上位机需要通过 WebSocket 与 RTU 通信获取当前已注册的信号列表。 ### 7.1 请求信号列表 ```json { "curd": "get", "signal_type": "out" } ``` ### 7.2 响应格式 RTU 返回 out 信号数组,每条信号包含: | 字段 | 类型 | 说明 | |------|------|------| | `no` | int | 信号序号(即 XML 中的 `Signal.no`) | | `saddr` | string | 信号地址,如 `"plc.st.out.0"` | | `desc` | string | 信号描述,如 `"plc输出0"` | | `val` | variant | 当前值 | | `data_type` | string | 数据类型 | ### 7.3 输出信号号段约定 - **RTU 内置 PLC 输入信号**:`no` 在 **59–67** 左右(实现中通过 `out` 信号表动态注册) - **RTU 内置 PLC 输出信号**:`no` 在 **1361–1368** 左右 - QT 上位机应**动态读取信号列表**而非硬编码号段 --- ## 八、注意事项 | 编号 | 注意点 | |------|--------| | 1 | `` 最多嵌套 **3 层**(见示例),更深层级需验证 RTU 固件支持。**每个 Comb 必须有唯一 `id` 属性**,后序遍历自底向上从 0 开始分配 | | 2 | `checksum` 必须使用 CRC-32/MPEG-2 算法,且大写十六进制格式 | | 3 | `key` 建议使用 `QDateTime::currentMSecsSinceEpoch()` 生成,保证唯一性 | | 4 | `x`/`y` 坐标可不填写(设为 0),RTU 解析时忽略这些字段 | | 5 | XML 文件编码必须为 **UTF-8**,不要使用 UTF-8 BOM | | 6 | `` 的 `gates`/`inputs`/`outputs` 属性值为统计数量,生成时必须准确填写 | | 7 | **T62P/T62D** 的 `delayMs` 为可选属性,默认 1000ms。范围 0~600000 | | 8 | **SR/RS 触发器**有两个固定输入引脚:SR 的 pin0=S(置位)/pin1=R(复位),RS 的 pin0=R(复位)/pin1=S(置位)。每个引脚只能接一根线 | | 9 | **SR/RS 的 R/S 属性**仅在 Gate 上输出,格式为 `Comb#N` 或 `Input#N`。若 RS/SR 所在链为组合链且有直接 Input 连接,需在链级 `` 块中包含对应信号 | | 10 | 输入信号 `no` 必须是 datacenter out 信号表中的有效序号。QT 上位机需通过 WebSocket 动态获取信号列表,不可硬编码号段 | | 11 | Comb 内 Gate 的 `id` 表示**同类型逻辑门内的序号**(OR/AND/NOT/T62P/T62D/... 分别计数,从 0 起递增) | | 12 | 一个 `` 可含多条 ``,目前 RTU 将所有 Chain 合并为一个逻辑图处理 | --- ## 九、附录:完整 XML 示例文件 ```xml ``` --- > **文档维护**:本文档基于 RTU 固件 [src/system/libplc/src/plc.cpp](src/system/libplc/src/plc.cpp) 中的 XML 解析逻辑编写。配置文件格式如随固件升级变更,请同步更新本文档。