简介:DLMS协议库是一份面向电力与水务计量领域嵌入式开发者的通信协议实现资源,适合从事智能电表、水表固件开发及协议栈移植的工程师学习参考。该库已在欧盟和东南亚多个国家的电表、水表中批量应用,并通过CCT软件测试取得DLMS证书,同时满足MID与KEMA认证要求,符合最新CTT测试规范,实现了DLMS协会蓝皮书、绿皮书、黄皮书的最新内容。压缩包内共2个文件,包含1个C源文件与1个头文件,整体约41KB,属于轻量级协议实现,便于直接集成到嵌入式工程中阅读与移植。目前已有1101人学习下载,对于需要理解DLMS/COSEM协议分层结构、报文封装与对象模型实现细节的开发者而言,这份经过实际认证检验的代码具有较高的参考价值,可帮助快速掌握协议栈核心逻辑与工程落地要点。
1. 从一份 DLMS 协议库压缩包说起:它到底能解决什么
如果你做过智能电表、集中器或者能源管理终端的对接,大概率听过 DLMS/COSEM 这套协议。它不像 Modbus 那样几行代码就能跑通,光是 ACSE 关联、xDLMS 服务、COSEM 对象模型这几层叠起来,就够让第一次接触的人卡上好几周。我手里这份 DLMS 协议库.rar,就是冲着这个场景来的——它把协议栈里最磨人的那部分封装成了可调用的接口,让你不用从零去啃 IEC 62056 那一摞标准文本。
这份资源适合三类人:一是做电力采集终端固件、需要快速集成 DLMS 客户端的嵌入式开发者;二是做后台主站系统、要跟不同厂家电表做互操作的平台工程师;三是手里有协议一致性测试任务、需要对照标准逐条验证的测试人员。它解决的核心问题不是“教你 DLMS 是什么”,而是“给你一套能编译、能跑、能改的代码底子”,把关联建立、对象读取、事件上报这些高频动作变成可复用的模块。值得珍藏的通信协议库这个说法不算夸张,因为 DLMS 的开源实现本来就少,能直接拿来改的更少。
2. DLMS/COSEM 协议栈拆解:这份库把哪些层做掉了
2.1 从物理层到应用层,协议栈的分层逻辑
DLMS/COSEM 的完整模型分三层来看比较清楚。最底下是通信通道层,可以是 RS-485、红外、PLC 或者 TCP,这一层协议库通常不碰,留给你自己接。中间是 HDLC 或者 Wrapper 封装层,负责把上层数据打成帧、做地址解析和校验。最上面才是 DLMS 应用层,包含 ACSE 关联控制、xDLMS 服务(GET/SET/ACTION)、以及 COSEM 对象模型(比如寄存器、曲线、事件日志)。
这份库主要做掉的是中间封装层和上层应用层。我翻了一遍目录结构,HDLC 帧的组帧拆帧、LLC 子层、ACSE 的 AARQ/AARE 协商、xDLMS 的 Get-Request/Get-Response 编解码,这些都有独立文件。COSEM 对象部分它给了一套接口定义和几个典型对象的实现模板,比如数据对象、寄存器对象、事件对象。换句话说,你拿到手之后,需要自己补的是物理通道的读写和具体业务对象的映射。
2.2 库的文件组织与模块划分
解压后大致能看到这么几类文件:协议核心目录放的是编解码和状态机,接口目录放的是对外暴露的 API 头文件,示例目录里有针对 TCP 和串口的调用样例,配置目录里是编译选项和对象字典的配置模板。这种分法比较常规,好处是移植的时候你只需要动接口层和配置层,核心逻辑不用碰。
我一般会先看接口头文件里定义了哪些结构体和函数指针,这决定了你接入自己系统时要实现哪些回调。比如发送回调、接收回调、超时回调,这三个是必须自己填的。对象字典那块,它给了一个数组模板,你按自己设备的对象列表往里填 OID、类 ID、属性索引和访问权限就行。
2.3 关联建立与释放的完整流程
DLMS 通信的第一步是建立应用关联,也就是 ACSE 层的 AARQ 和 AARE 交互。这个过程里会协商认证方式、应用上下文、以及双方支持的 xDLMS 版本。库里的实现是把 AARQ 的各个字段做成可配置项,你填好之后调一个发送函数,然后等对端回 AARE,解析出结果码判断是否成功。
下面这段是典型的关联建立调用逻辑,我按库里的接口风格写了个示意:
/* 初始化协议栈上下文,传入通道读写回调 */ dlms_ctx_t ctx; dlms_init(&ctx, &my_send_cb, &my_recv_cb, &my_timeout_cb); /* 配置 AARQ 参数:认证方式、应用上下文、最大PDU */ dlms_aarq_cfg_t cfg; cfg.auth_mode = DLMS_AUTH_LOW; /* 低层认证,密码方式 */ cfg.app_context = DLMS_CTX_NO_CIPHER; /* 无加密的应用上下文 */ cfg.max_pdu_size = 512; /* 协商的最大应用层PDU */ cfg.client_sap = 16; /* 客户端SAP,通常16 */ cfg.server_sap = 1; /* 服务端SAP,电表侧常见为1 */ /* 发起关联请求,内部会组AARQ帧并通过send_cb发出 */ int ret = dlms_associate(&ctx, &cfg); if (ret != DLMS_OK) { /* 关联失败,检查通道是否通、SAP是否匹配、认证密码是否正确 */ return -1; }这段代码里几个参数值得说清楚。auth_mode 选 LOW 表示用密码做低层认证,如果电表侧配的是 HIGH 或者无认证,这里要对应改。app_context 选 NO_CIPHER 是最常见的明文场景,如果涉及加密就要换成带 ciphering 的上下文,但那会牵扯到密钥管理和 HLS 认证,复杂度上一个台阶。max_pdu_size 建议先设 512,太小会导致长数据要分片,太大有些老电表不认。client_sap 和 server_sap 是地址,不同厂家可能不一样,对不上就是关联被拒。
2.4 xDLMS 服务调用的参数与返回值处理
关联建好之后,读数据就是发 Get-Request。库里的接口把对象 OID、类 ID、属性索引、访问选择器这几个参数打包成一个请求结构。返回值里除了数据本身,还有 data_access_result 这个字段,用来区分成功、对象不存在、权限不够、类型不匹配这些情况。
/* 读取一个寄存器对象的当前值(类ID=3,属性2为值) */ dlms_get_req_t req; req.oid = 0x01000001; /* 对象标识,按对象字典填 */ req.class_id = 3; /* 3代表Register */ req.attr_index = 2; /* 属性2是value */ req.access_selector = 0; /* 无选择器,直接读全量 */ dlms_value_t val; int ret = dlms_get(&ctx, &req, &val); if (ret == DLMS_OK) { /* val.type 标识数据类型,val.data 指向实际值 */ /* 常见类型:double-long-unsigned(6)、octet-string(9)、structure(2) */ } else { /* 根据 ctx.last_error 判断是超时、拒绝还是解码失败 */ }这里有个容易翻车的地方:DLMS 的数据类型是带标签的,读回来不能直接当 int 用,要先看 val.type。比如电表里的电压可能是 double-long-unsigned,电流可能是 integer,时间可能是 octet-string 按特定格式编码。库里的解码函数会帮你把标签剥掉,但类型转换得自己做。另外 access_selector 在读曲线或事件日志时会用到,按索引或按时间范围取,参数结构会复杂一些,库里给了单独的接口。
3. 把库跑起来:从编译到第一次成功读表
3.1 编译环境与依赖确认
这份库是纯 C 写的,没有外部依赖,理论上只要有个 C99 编译器就能编。我一般先在 Linux 上用 gcc 过一遍,确认没有语法问题,再往目标平台上移。编译前先看配置头文件里几个宏:字节序、是否启用浮点、是否启用动态内存。嵌入式平台如果不想用 malloc,可以把动态内存宏关掉,库会走静态缓冲区。
# 在库根目录下,先编译核心源文件为目标文件 gcc -c -std=c99 -I./include -DLMS_USE_STATIC_BUF=1 src/*.c # 链接成静态库,方便集成到自己的工程 ar rcs libdlms.a *.o # 编译示例程序,验证基本流程 gcc -std=c99 -I./include examples/tcp_client.c -L. -ldlms -o dlms_demo编译选项里 -DLMS_USE_STATIC_BUF=1 是关掉动态内存,适合资源紧张的嵌入式环境。如果平台字节序是大端,还要加 -DLMS_BIG_ENDIAN。示例程序编出来之后先别急着连真表,可以用库自带的回环测试跑一遍,确认编解码逻辑没问题。
3.2 通道对接:TCP 与串口的回调实现
库本身不管通道,它通过回调函数往外发数据、往里收数据。你要做的就是实现这两个回调,把库给的字节流写到实际通道上,再把通道收到的字节流喂回库。TCP 场景下就是 socket 的 send 和 recv,串口场景下就是 read 和 write。
/* 发送回调:库要发数据时调用,你把 buf 里的 len 字节写到通道 */ int my_send_cb(const uint8_t *buf, uint16_t len, void *user_data) { int fd = *(int *)user_data; int sent = write(fd, buf, len); /* 串口或socket的写接口 */ return (sent == len) ? DLMS_OK : DLMS_ERR_SEND; } /* 接收回调:库要收数据时调用,你从通道读最多 max_len 字节 */ int my_recv_cb(uint8_t *buf, uint16_t max_len, void *user_data) { int fd = *(int *)user_data; int n = read(fd, buf, max_len); /* 阻塞或非阻塞取决于你的设计 */ return (n > 0) ? n : DLMS_ERR_RECV; }这两个回调的返回值语义要跟库的约定对齐。发送回调返回 DLMS_OK 表示全部发出去了,返回错误库会走重试或上报。接收回调返回实际读到的字节数,返回负值表示出错。如果是非阻塞通道,读不到数据时返回 0 还是错误码,要看库文档里的约定,搞错了会导致状态机卡死。
3.3 对象字典配置与第一次 Get 请求
对象字典是 DLMS 通信的“通讯录”,你告诉库你关心哪些对象、它们的 OID 和类 ID 是什么。库里的配置模板是一个数组,每项包含 OID、类 ID、版本号、属性访问权限。第一次跑通建议只配一个对象,比如一个寄存器,读它的值。
/* 对象字典条目:一个简单的寄存器对象 */ const dlms_obj_entry_t obj_dict[] = { { .oid = 0x01000001, /* 对象标识,按实际设备填 */ .class_id = 3, /* Register类 */ .version = 0, .attr_access = { /* 属性访问权限:0不可访问,1只读,2读写 */ [1] = 1, /* 属性1:逻辑名,只读 */ [2] = 1, /* 属性2:值,只读 */ }, }, /* 更多对象按同样格式追加 */ };配好字典之后,把关联建立、Get 请求、关联释放串起来跑一遍。如果关联成功但 Get 返回对象不存在,先查 OID 是不是写反了字节序,DLMS 的 OID 是 6 字节,常见写法是 0x01000001 这种,但有些文档写成十进制或十六进制混用,容易看错。如果返回权限不够,查 attr_access 里对应属性的值是不是设成了 0。
3.4 用回环测试验证编解码正确性
在连真表之前,我强烈建议先用回环测试把编解码逻辑过一遍。库的示例里通常带一个 loopback 程序,它把发出去的帧直接喂回给自己,模拟一个理想对端。这样能验证 AARQ 组帧、AARE 解析、Get 请求编码、Get 响应解码这些环节有没有低级错误。
# 运行回环测试,观察每一步的日志输出 ./dlms_demo --loopback --verbose # 正常输出应该能看到: # [SEND] AARQ frame, len=64 # [RECV] AARE frame, result=accepted # [SEND] Get-Request, oid=01000001, attr=2 # [RECV] Get-Response, type=double-long-unsigned, value=2201回环通了再连真表,能省掉大量“到底是通道问题还是协议问题”的排查时间。如果回环都不过,那肯定是库的配置或编译选项有问题,跟外部设备无关。
4. 避坑与排查:DLMS 对接中最容易翻车的五个点
4.1 关联被拒但看不出原因
现象是 AARQ 发出去之后收到 AARE,result 字段是 rejected,但具体原因码没打印。原因通常是认证方式不匹配、SAP 地址不对、或者应用上下文不支持。解决方法是把 AARE 里的 diagnostic 字段解出来,库一般有对应的解析函数,把 result_source 和 result_diagnostic 打出来,对照标准里的码表查。我习惯在关联失败时把整个 AARE 原始字节 dump 出来,手动对一遍。
4.2 读回来的数据跟电表显示对不上
现象是 Get 成功返回了值,但数值跟电表面板显示差一个量级或者符号反了。原因是 DLMS 的数值带 scaler 和 unit,电表返回的是原始值,需要乘以 10 的 scaler 次方才是工程值。比如返回 2201,scaler 是 -1,实际就是 220.1V。解决方法是读值的时候把 scaler 属性也读出来,或者查对象字典里该对象的 scaler 配置,在应用层做换算。库本身不做这个换算,它只负责把原始值给你。
4.3 长数据读取时分片处理出错
现象是读曲线或事件日志时,数据超过 max_pdu_size,库返回一个“还有后续帧”的标志,但后续帧的拼接逻辑写错了,导致数据错位。原因是 DLMS 的 block transfer 机制里,每个分片带一个 block number,拼接时要按序号排,还要判断 last block 标志。解决方法是仔细看库里的分片回调接口,它一般会给你 block number 和 is_last 两个参数,你按序号存到缓冲区,收到 last 之后再整体解码。别自己手动拼,容易漏掉边界。
4.4 超时设置不合理导致状态机卡死
现象是通信跑一段时间后没反应了,既不报错也不返回。原因是超时回调返回的时间跟实际通道的响应时间不匹配,或者接收回调在没数据时阻塞太久,把状态机拖死了。解决方法是把超时回调的返回值设成实际通道超时的 1.5 倍左右,接收回调如果是阻塞模式,要设一个合理的 socket 或串口读超时。我一般会在接收回调里加一层非阻塞轮询,读不到就立刻返回 0,让库自己决定重试还是上报。
4.5 不同厂家电表的对象 OID 不统一
现象是同一套代码在这块表上能读,换一块表就读不到。原因是 DLMS 标准只规定了对象模型的框架,具体 OID 分配各厂家有差异,尤其是厂商自定义对象。解决方法是把对象字典做成可配置的,不同表型加载不同配置。库里的配置模板支持多套字典切换,你按表型或按厂家建几个配置文件,运行时根据协商结果或预置参数加载对应的那份。别指望一套 OID 打天下。
5. 进阶用法:把协议库改造成可复用的采集框架
5.1 封装一层会话管理,支持多表轮询
单表跑通之后,下一步通常是多表轮询。我一般会在库外面包一层会话管理,维护一个表地址列表和对应的连接状态,按顺序对每块表做关联、读数据、释放。这层不碰协议细节,只调库的 associate、get、release 三个接口,加上超时和重试逻辑。
/* 简化的多表轮询框架 */ typedef struct { uint8_t meter_addr[6]; /* 表地址 */ int fd; /* 通道句柄 */ int associated; /* 关联状态 */ } meter_session_t; int poll_all_meters(meter_session_t *meters, int count) { for (int i = 0; i < count; i++) { if (!meters[i].associated) { /* 未关联的先建关联 */ if (dlms_associate(&ctx, &cfg) != DLMS_OK) { continue; /* 这块表跳过,不影响下一块 */ } meters[i].associated = 1; } /* 读该表的关键对象,失败则标记断线 */ if (read_meter_data(&meters[i]) != DLMS_OK) { meters[i].associated = 0; } } return 0; }这层框架的关键是错误隔离:一块表关联失败或读失败,不能影响其他表。每块表的会话状态独立维护,超时和重试次数按表配置。库本身不提供这层,但它的接口粒度足够细,包起来不费劲。
5.2 用事件日志对象做主动上报
DLMS 除了轮询读,还支持事件日志和主动上报。电表侧把事件记录在日志对象里,主站可以按时间范围读,也可以让电表在事件发生时主动推。库里的事件对象接口支持这两种模式。主动上报需要先配置电表的推送目标,然后在主站侧监听通道,收到推送帧之后按事件对象的结构解码。
我一般会先把事件日志的读取跑通,确认能按时间范围取到记录,再切到推送模式。推送模式对通道的实时性要求高,如果是串口轮询架构,推送容易丢,建议用 TCP 长连接。库里的推送解析接口跟 Get 响应类似,只是触发方式不同,解码逻辑可以复用。
5.3 验证协议一致性的几个实用检查点
如果你手头的任务是做一致性测试,这份库可以当参考实现来用。我一般会重点检查这几个点:AARQ 里的应用上下文和认证机制是否符合标准要求;Get 请求的访问选择器在曲线和日志对象上是否正确编码;分片传输的 block number 是否连续、last 标志是否在最后一帧置位;异常响应里的错误码是否跟标准定义一致。
| 检查项 | 标准要求 | 常见偏差 |
|---|---|---|
| 应用上下文 | 无加密场景用 0x03 | 误用 0x02 导致关联被拒 |
| 认证机制 | 低层认证密码 8 字节 | 密码长度不对或补位错误 |
| 分片传输 | block number 从 1 递增 | 从 0 开始或跳号 |
| 错误码 | 按 IEC 62056-5-3 定义 | 自定义码导致主站无法识别 |
这些检查点跑一遍,基本能覆盖大部分互操作问题。库里的日志功能可以打开,把每帧的原始字节和解析结果都打出来,对照标准逐字段看。
5.4 我踩过的一个坑:字节序和位域
最后说一个我自己的血泪经验。DLMS 的帧里有些字段是位域,比如 HDLC 的控制字段和地址字段,跨平台移植时如果编译器对位域的内存布局处理不一致,组出来的帧就是错的。我当初在 x86 上跑通了,移到 ARM 上就关联失败,查了两天才发现是位域顺序反了。从那以后我每次移植协议库,都强制先跑一遍回环测试,把关键帧的十六进制 dump 出来跟标准示例逐字节对,确认字节序和位域布局没问题再往下做。这个习惯帮我省掉了后面无数次“玄学”排查。希望帮到你。
本文还有配套的精品资源,点击获取