简介:这份资源面向智能变电站自动化与IEC 61850协议的学习者与开发者,聚焦SCD配置文件解析与MMS Lite通信技术,帮助读者理解变电站逻辑节点、通信网络与IED设备配置的处理方法。压缩包共18个文件,以10个.h头文件与8个.c源文件为主,整体约91KB,涵盖SCL解析、时间字符串转换、数据编解码、配置存储管理、字符串工具、链表生成及日志记录等模块,头文件则勾勒出各模块间的接口与整体架构。目前已有668人学习下载。通过研读这套代码,读者可掌握基于IEC 61850标准的SCD解析流程、XML数据处理与异常检查思路,并了解MMS Lite在设备配置与状态监控中的实现细节,为电力系统自动化编程与通信机制实践提供可参考的素材。
1. 拆开一份智能变电站 SCD 解析源码:从 XML 到 MMS Lite 的落地路径
智能变电站的二次系统调试里,SCD 文件几乎是绕不开的一环。它是一份基于 IEC 61850 的 XML 配置描述,把站内逻辑节点、通信参数、IED 能力、数据集和报告控制块全塞进一个文本文件里。问题是,用文本编辑器打开它只能看到一堆嵌套标签,想批量提取某个间隔的 GOOSE 订阅关系、或者核对 MMS 报告控制块的触发条件,手工翻页基本等于自虐。ScdParse.zip 这套源码就是冲着这个场景来的:它用 C 语言把 SCD 解析、SCL 对象建模、时间字符串转换和 MMS Lite 通信这几块串成了一条可编译的链路。适合谁?做变电站自动化后台工具、规约转换网关、或者想从零理解 IEC 61850 数据模型的嵌入式开发者。如果你只打算调现成的组态软件,这份代码可能偏底层;但如果你想搞清楚 SCD 里的一个DataSet到底怎么变成内存里的链表、再通过 MMS 服务发出去,它值得花一个周末拆一遍。
2. 源码结构与模块职责:先看清 sclparse.c 和 sx_enc.c 的分工
2.1 从文件清单反推架构分层
拿到一个 C 源码包,我习惯先按后缀和命名把文件分成几组,再判断哪些是入口、哪些是工具、哪些是协议实现。ScdParse.zip 里的文件大致可以这样归类:
| 文件 | 推测职责 | 在链路中的位置 |
|---|---|---|
sclparse.c/scl.h | SCL 文档解析主逻辑,读取 XML 节点并填充结构体 | 入口层 |
sclstore.c | 解析结果的存储管理,可能涉及内存或磁盘缓存 | 数据层 |
sx_enc.c/sx_dec.c/sx_defs.h/sx_arb.h | 按 IEC 61850 格式做数据编解码,处理 ASN.1 或自定义 TLV | 协议编解码层 |
time_str.c/time_str.h | 时间字符串与时间戳互转,处理 UTC 偏移和毫秒精度 | 基础工具层 |
str_util.c/str_util.h | 字符串查找、分割、去空白,配合 XML 文本处理 | 基础工具层 |
genlists.c/gen_list.h | 链表或队列的生成与管理,承载大量配置项 | 数据结构层 |
slog.c/slog.h | 日志记录,用于解析过程追踪和错误定位 | 辅助层 |
mem_chk.h/sysincs.h/glbtypes.h | 内存检查宏、系统头文件封装、全局类型定义 | 编译支撑层 |
这个分法不是官方文档给的,是我按常见 C 项目习惯推出来的。实际编译时,sysincs.h和glbtypes.h往往是第一个要看的地方,因为它们决定了int是 16 位还是 32 位、有没有引入平台相关的类型重定义。如果这两个头文件里出现了#pragma pack或者自定义的BOOL,那说明代码可能来自较老的嵌入式工具链,移植到 GCC 时得留意对齐问题。
2.2 sclparse.c 的解析流程与关键参数
sclparse.c大概率是围绕一个 XML 解析库展开的。摘要里提到 libxml2,这是 Linux 下最常见的做法。常见流程是:用xmlReadFile加载 SCD,拿到xmlDocPtr,然后递归遍历节点,遇到IED、LDevice、LN、DataSet、GSEControl、ReportControl这些标签时,分别调用对应的填充函数。下面这段伪代码是我按这个思路还原的骨架,不是源码原文,但能说明参数该怎么传:
/* 伪代码:SCL 解析主循环的典型结构 */ #include <libxml/parser.h> #include <libxml/tree.h> #include "scl.h" #include "slog.h" int scl_parse_file(const char *scd_path, SclModel *model) { xmlDocPtr doc = xmlReadFile(scd_path, NULL, XML_PARSE_NOBLANKS); if (doc == NULL) { slog_error("SCD 加载失败: %s", scd_path); return -1; } xmlNodePtr root = xmlDocGetRootElement(doc); if (!root || xmlStrcmp(root->name, BAD_CAST "SCL")) { slog_error("根节点不是 SCL"); xmlFreeDoc(doc); return -1; } /* 遍历第一层子节点:Header / Communication / IED / DataTypeTemplates */ for (xmlNodePtr cur = root->children; cur; cur = cur->next) { if (cur->type != XML_ELEMENT_NODE) continue; if (!xmlStrcmp(cur->name, BAD_CAST "IED")) { parse_ied(cur, model); /* 填充 IED 列表 */ } else if (!xmlStrcmp(cur->name, BAD_CAST "Communication")) { parse_communication(cur, model); /* 填充子网和接入点 */ } else if (!xmlStrcmp(cur->name, BAD_CAST "DataTypeTemplates")) { parse_datatypes(cur, model); /* 填充 LNodeType / DOType */ } } xmlFreeDoc(doc); return 0; }逻辑说明:xmlReadFile的第三个参数XML_PARSE_NOBLANKS会忽略格式化空白,减少后续遍历时的无效节点。parse_ied这类函数内部通常要处理name、desc、type等属性,属性值用xmlGetProp取出来之后必须xmlFree,否则解析大文件时内存会持续上涨。参数方面,SclModel结构体里一般会有ied_list、subnet_list、lnode_type_list这几个链表头,具体字段名得看scl.h的定义。如果编译时报undefined reference to xmlReadFile,检查 Makefile 里有没有加-lxml2,以及xml2-config --cflags输出的头文件路径是否包含进去了。
2.3 sx_enc.c 与 sx_dec.c 的编解码边界
sx_enc.c和sx_dec.c这对文件,从命名看是 encode/decode 的缩写。在 IEC 61850 场景下,它们可能负责两件事:一是把解析后的内存结构序列化成 MMS 可传输的字节流,二是把收到的 MMS 报文还原成结构体。MMS Lite 作为简化版,通常只实现读、写、报告这几个核心服务,所以编解码的复杂度比完整 MMS 低不少。常见做法是定义一个 TLV 格式:Tag 用一字节标识数据类型,Length 用一或两字节表示后续字节数,Value 按类型解释。sx_defs.h里应该能看到类似SX_TYPE_INT32、SX_TYPE_VISIBLE_STRING的宏定义。移植时要注意字节序:如果代码里直接用了memcpy把int拷进缓冲区,在大端和小端平台之间通信会翻车,稳妥的做法是统一转成网络字节序再发。
3. 编译与运行:把 sclparse.c 跑起来的最小闭环
3.1 搭建编译环境与依赖检查
这套代码是纯 C,没有看到构建系统文件,所以得自己写 Makefile 或者直接用命令行编译。依赖方面,libxml2 是大概率要装的,另外slog.c如果用了可变参数宏,可能需要 C99 支持。下面是我在 Linux 下验证依赖的步骤:
# 检查 libxml2 开发包是否就位 xml2-config --version # 如果没有输出,安装开发包(以常见发行版为例) # sudo apt-get install libxml2-dev # 检查编译器对 C99 的支持 gcc -std=c99 -dM -E - < /dev/null | grep __STDC_VERSION__逻辑说明:xml2-config --version能输出版本号,说明头文件和库路径都注册好了。gcc -std=c99那行是确认编译器默认标准,如果输出199901L就说明 C99 可用。参数上,编译时建议加-Wall -Wextra把警告打开,这类老代码里常见的隐式类型转换和未初始化变量,靠警告能提前发现一批。
3.2 编译命令与常见链接错误处理
假设所有.c文件都在同一目录,一个能跑通的编译命令大概长这样:
gcc -std=c99 -Wall -Wextra -I. \ $(xml2-config --cflags) \ sclparse.c sclstore.c sx_enc.c sx_dec.c \ time_str.c str_util.c genlists.c slog.c \ -o scdparse \ $(xml2-config --libs)逻辑说明:-I.让编译器在当前目录找scl.h、sx_defs.h这些头文件。$(xml2-config --cflags)展开后通常是-I/usr/include/libxml2,少了它就会报libxml/parser.h: No such file or directory。链接阶段$(xml2-config --libs)展开成-lxml2,如果报undefined reference to xmlFreeDoc之类的错误,八成是这行没加。另外,如果sx_enc.c里用了数学库的pow或fabs,还得补-lm。编译通过后运行./scdparse,如果程序需要指定 SCD 文件路径,常见做法是加一个-f参数或者直接把路径作为第一个位置参数,具体看main函数怎么写的——这份源码里没有单独的main.c,所以入口可能在sclparse.c末尾,也可能需要自己写一个调用scl_parse_file的测试桩。
3.3 用一份最小 SCD 验证解析结果
跑通编译只是第一步,真正要确认解析逻辑对不对,得准备一份结构完整的 SCD。最小可用的 SCD 至少包含Header、Communication、一个IED和对应的DataTypeTemplates。下面是一个虚构的片段,用来测试sclparse.c能不能正确读出 IED 名称和接入点:
<?xml version="1.0" encoding="UTF-8"?> <SCL xmlns="http://www.iec.ch/61850/2003/SCL"> <Header id="test_project" version="1.0"/> <Communication> <SubNetwork name="subnet1" type="8-MMS"> <ConnectedAP iedName="IED1" apName="ap1"> <Address> <P type="IP">192.168.1.10</P> </Address> </ConnectedAP> </SubNetwork> </Communication> <IED name="IED1" desc="测试装置" type="Protection"> <AccessPoint name="ap1"/> </IED> <DataTypeTemplates/> </SCL>逻辑说明:把这段保存为test.scd,然后运行解析程序。如果sclparse.c里的parse_communication正确实现了,应该能在日志里看到subnet1和192.168.1.10。注意ConnectedAP的iedName属性要和IED的name对上,否则关联关系会断。参数上,type="8-MMS"是常见写法,表示该子网承载 MMS 通信。如果解析结果里 IP 是空的,检查xmlGetProp取属性时有没有把P节点的type和文本内容分开处理——P节点的值在children->content里,不在属性里。
4. 避坑与排查:SCD 解析和 MMS Lite 联调中的五个血泪经验
4.1 现象:解析大 SCD 时内存持续上涨,最终 OOM
原因:libxml2 的xmlGetProp、xmlNodeGetContent返回的字符串需要手动xmlFree,很多示例代码只取不释放。另外,如果sclstore.c里用链表存了所有节点但没提供销毁函数,解析完不释放也会泄漏。 解决:在每次xmlGetProp之后配对xmlFree,并在sclstore.c里补一个scl_model_free函数,遍历所有链表逐个free。用valgrind --leak-check=full ./scdparse test.scd跑一遍,能直接定位泄漏点。
4.2 现象:时间字符串转时间戳后差了 8 小时
原因:time_str.c里可能用了mktime而不是timegm,mktime会把输入当本地时间处理,而 SCD 里的t属性通常是 UTC。 解决:确认time_str.c里解析2024-01-01T00:00:00.000这类字符串时,是否显式设置了tm_isdst = 0并调用timegm。如果没有timegm,可以用setenv("TZ", "UTC", 1); tzset();再调mktime作为替代。改完之后用已知 UTC 时间做单元测试,别靠肉眼对。
4.3 现象:MMS Lite 编码后的报文对端解不出来
原因:sx_enc.c里写整数时直接memcpy了本机字节序,而 MMS 默认要求大端。如果发送端是小端机器,对端按大端解析就会得到完全错误的数值。 解决:在sx_enc.c里加一层htons/htonl转换,或者统一用逐字节移位的方式写入。解码侧sx_dec.c对应地用ntohs/ntohl。改完后用xxd看一眼编码结果,确认高位字节在前。
4.4 现象:编译时报conflicting types for 'xxx'
原因:sysincs.h或glbtypes.h里可能重定义了标准类型,比如typedef unsigned char BOOL;和某个系统头文件冲突,或者函数原型在scl.h和sx_defs.h里声明不一致。 解决:先用gcc -E预处理出展开后的源码,搜索冲突符号的定义位置。如果是类型重定义,把自定义类型改成项目前缀,比如SCL_BOOL。如果是函数原型不一致,以scl.h里的为准,把另一个头文件里的声明删掉或对齐。
4.5 现象:解析含Private元素的 SCD 时直接跳过,导致部分配置丢失
原因:IEC 61850 允许厂商在 SCL 里插入Private元素存放私有配置,sclparse.c的遍历逻辑如果只认标准标签,遇到Private就continue,里面的信息就丢了。 解决:在遍历循环里对Private做特殊处理,根据type属性分发给不同的扩展解析函数。如果暂时不需要私有配置,至少用slog记一条 debug 日志,方便日后排查为什么某个参数没读到。
5. 进阶技巧:用 genlists.c 的链表思路做配置比对
5.1 把解析结果导出成可 diff 的文本
genlists.c和gen_list.h提供了一套链表管理机制,常见接口有list_create、list_append、list_foreach。这套东西的价值在于:解析完 SCD 之后,你可以遍历链表把关键字段按固定格式打印出来,生成一份可 diff 的文本快照。比如每次变电站改配置,用同一份代码导出两份快照,diff一下就能看出哪个 IED 的 IP 变了、哪个 DataSet 的成员增删了。下面是一个导出函数的写法示例:
/* 遍历 IED 链表,输出 name 和 desc,每行一条 */ void dump_ied_list(const SclModel *model, FILE *fp) { ListNode *node = model->ied_list; while (node) { IedInfo *ied = (IedInfo *)node->data; fprintf(fp, "IED\t%s\t%s\n", ied->name ? ied->name : "", ied->desc ? ied->desc : ""); node = node->next; } }逻辑说明:ListNode和IedInfo的具体定义要看gen_list.h和scl.h,但遍历模式是通用的。参数上,fp可以指向stdout也可以指向文件,建议输出用\t分隔,方便后续用awk或cut提取字段。如果链表里存的是指针,注意ied->name可能为NULL,打印前判空能避免段错误。
5.2 用时间戳对齐 MMS 报告和本地日志
time_str.c除了做字符串转换,还可以用来对齐不同来源的时间。MMS Lite 上报的事件通常带一个时间戳,而本地slog.c记录的日志也有时间。如果两边时区处理不一致,排查问题时就会对不上号。我的习惯是:在slog.c里统一用 UTC 时间输出,格式和time_str.c解析的格式保持一致,这样报告里的时间和日志里的时间可以直接比较。具体做法是在slog初始化时调用一次time_str_set_timezone(0)(如果代码里有这个接口),或者直接在日志格式化函数里用gmtime而不是localtime。
5.3 一个我踩过的坑:链表节点释放顺序
genlists.c的链表如果存的是嵌套结构,比如一个IED节点下面挂着AccessPoint链表,释放的时候必须先释放子链表再释放父节点。我一开始图省事,直接从头到尾free父节点,结果子链表全泄漏了。后来每次写释放函数都强制走一遍「先递归子节点、再释放自身」的顺序,用valgrind确认没有definitely lost才收工。从那以后我每次拿到带链表的 C 代码,都先看它的销毁函数写没写对,这比看解析逻辑还重要——解析错了能改,内存泄漏在长时间运行的后台进程里就是定时炸弹。希望帮到你。
本文还有配套的精品资源,点击获取