简介:本资源面向电力自动化、智能计量与物联网方向的开发者,聚焦62056协议族中DLMS应用层与ASN.1编码、HDLC链路控制的结合实现,帮助读者理解智能电表与采集系统间的标准化数据交换机制。压缩包共24个文件,约20KB,以C源码与头文件为主(10个.c、9个.h),另含ASN.1模块定义、Makefile构建脚本及示例文件,覆盖AARQ-apdu、COSEMpdu、ACSE-requirements、Application-context-name等协议对象,便于在C/C++环境下搭建DLMS协议栈原型。已有363人学习下载,适合作为协议栈移植与编码调试的参考素材。通过研读源码,读者可掌握ASN.1编解码规则、HDLC帧定界与错误检测思路,以及对象访问模型在电表读写、事件报告中的落地方式,为实际项目开发提供可复用的代码骨架与排错线索。
1. DLMS/COSEM 协议栈拆解:从 62056-47 到 ASN.1 编解码的完整链路
如果你手头有一份dlms.rar,里面散落着62056-47、62056-62、asn.1、hdlc这些关键词,大概率你正面对一个电能表或采集终端的通信协议栈。DLMS/COSEM 是电力行业事实上的国际标准,IEC 62056 系列把物理层到应用层全部定义清楚。其中 62056-47 管的是 HDLC 链路层怎么在串口上跑,62056-62 管的是接口类对象怎么建模,而 ASN.1 负责把数据结构序列化成字节流。这三者串起来,才是一条完整的抄表或参数下发通道。很多人卡在 HDLC 帧的 AARQ 字段上,不知道里面到底装了什么,其实它就是 ASN.1 编码后的应用层连接请求。这篇笔记按实际调试顺序,把协议栈从底层到上层拆开,给出可复现的代码和参数配置,适合正在对接 DLMS 电表、做采集终端固件或协议测试工具的工程师。
2. HDLC 链路层在 62056-47 里的帧结构与 AARQ 定位
2.1 62056-47 定义的 HDLC 帧格式与关键字段
IEC 62056-47 把 HDLC 帧的每个字节都安排得明明白白。一个标准帧从标志位0x7E开始,接着是帧格式域、目的地址、源地址、控制域、头校验 HCS、用户数据、帧校验 FCS,最后再以0x7E结束。地址域在 DLMS 里通常是 1 字节或 4 字节,取决于地址长度配置。控制域用0x10表示信息帧 I 帧,0x03表示无编号帧 U 帧,AARQ 就藏在 I 帧的用户数据里。
实际调试时,串口抓到的原始字节流经常因为转义处理出错。HDLC 规定,凡是数据里出现0x7E,必须转义成0x7D 0x5E;出现0x7D,转义成0x7D 0x5D。这个转义规则在 62056-47 里叫字节填充,很多新手直接拿原始数据去解析,结果帧长对不上,校验全错。
下面是一个用 Python 解析 HDLC 帧的最小实现,重点看地址域和控制域的处理:
import struct def parse_hdlc_frame(raw: bytes): # 去掉首尾标志位 if raw[0] != 0x7E or raw[-1] != 0x7E: raise ValueError("帧标志位错误") payload = raw[1:-1] # 反转义 unescaped = bytearray() i = 0 while i < len(payload): if payload[i] == 0x7D: unescaped.append(payload[i+1] ^ 0x20) i += 2 else: unescaped.append(payload[i]) i += 1 # 解析帧格式域 frame_format = unescaped[0] addr_len = (frame_format >> 3) & 0x03 # 地址长度指示 # 目的地址和源地址 dest_addr = int.from_bytes(unescaped[1:1+addr_len], 'big') src_addr = int.from_bytes(unescaped[1+addr_len:1+2*addr_len], 'big') # 控制域 ctrl = unescaped[1+2*addr_len] # 用户数据从 HCS 之后开始 user_data = unescaped[1+2*addr_len+3:-2] return { "dest": dest_addr, "src": src_addr, "ctrl": hex(ctrl), "user_data": user_data.hex() }这段代码先做反转义,再按帧格式域里的地址长度指示动态取地址。参数addr_len来自帧格式域的第 3、4 位,常见值是 0 表示 1 字节地址,1 表示 2 字节,2 表示 4 字节。控制域0x10就是 I 帧,用户数据里才是完整的 LLC PDU,AARQ 就在其中。
2.2 AARQ 在 HDLC 帧里的位置与内容构成
AARQ 是应用层连接请求,全称 Association Request。它不在 HDLC 层定义,而是由 62056-62 和 ASN.1 描述,封装在 HDLC 的信息帧里。一个典型的 AARQ 用户数据从 LLC 头开始:0xE6 0xE6 0x00,其中0xE6表示 LLC 帧格式,0x00是 LLC 控制域。紧接着就是 AARQ 的 ASN.1 编码字节。
AARQ 的内容包括:应用上下文名称、发送方 ACSE 要求、用户信息。应用上下文名称通常是一个 OID,比如2.16.756.5.8.1.1表示 DLMS/COSEM 的 ACSE 上下文。发送方 ACSE 要求里包含认证机制、认证值、AP 标题等。用户信息里才是 xDLMS 的初始化请求,比如协商最大 PDU 长度。
用asn1tools库可以把这个结构解析出来:
import asn1tools # 假设已经从 62056-62 标准里提取了 ASN.1 模块定义 spec = asn1tools.compile_files('dlms_asn1.asn', 'ber') decoded = spec.decode('AARQ', user_data_bytes) print(decoded)参数说明:dlms_asn1.asn需要包含AARQ类型的完整定义,通常从 62056-62 的 ASN.1 模块里摘录。ber是基本编码规则,DLMS 在 AARQ 阶段默认用 BER,后续 xDLMS 服务可能用 A-XDR。解码出来的字典里,application-context-name就是 OID,sender-acse-requirements里的authentication-mechanism-name决定后续是否要跑 HLS 认证。
注意:AARQ 里的认证机制如果是
2.16.756.5.8.1.2表示低层安全,2.16.756.5.8.1.3表示高层安全。选错机制会导致后续认证帧被电表直接丢弃。
3. 62056-62 接口类与 ASN.1 编解码的配合方式
3.1 62056-62 定义的接口类对象模型
IEC 62056-62 把电表里的所有数据都抽象成对象,每个对象属于一个接口类。比如数据对象用类 ID 1,寄存器用类 ID 3,需求寄存器用类 ID 5,活动电能表用类 ID 17。每个对象有属性、方法、事件。属性用索引访问,比如逻辑名是属性 1,值属性是属性 2。方法用索引调用,比如复位方法索引 1。
这些对象的访问路径由 OBIS 码标识,比如1.0.1.8.0.255表示正向有功总电能。在协议交互里,客户端先发 AARQ 建立连接,再发 GET 请求读取某个对象的属性。GET 请求的 ASN.1 结构里包含类 ID、OBIS 码、属性索引。
下面是一个构造 GET 请求的示例,用 ASN.1 编码:
import asn1tools spec = asn1tools.compile_files('dlms_asn1.asn', 'ber') get_request = { 'class-id': 3, 'instance-id': bytes([0x01, 0x00, 0x01, 0x08, 0x00, 0xFF]), 'attribute-id': 2 } encoded = spec.encode('Get-Request', get_request) print(encoded.hex())参数说明:class-id是接口类编号,instance-id是 OBIS 码的 6 字节表示,attribute-id是属性索引。编码出来的字节流会作为 xDLMS PDU 的一部分,再套上 LLC 和 HDLC 头发送。注意 OBIS 码的字节顺序,标准里是从左到右依次排列,但有些电表厂商会做字节序调整,调试时先用已知对象验证。
3.2 ASN.1 模块的提取与编译
62056-62 标准附录里给出了完整的 ASN.1 模块定义,但直接复制出来往往编译不过,因为里面引用了其他模块的类型。常见做法是只提取需要的类型,比如AARQ、AARE、Get-Request、Get-Response、Set-Request,把依赖的类型也一并摘出来,形成一个独立的.asn文件。
用asn1tools编译时,如果报Type not found,说明依赖没摘全。可以先用asn1tools.compile_string快速测试:
asn1_str = """ DLMS-ASN1 DEFINITIONS ::= BEGIN AARQ ::= [APPLICATION 0] IMPLICIT SEQUENCE { application-context-name [0] EXPLICIT ApplicationContextName, sender-acse-requirements [1] EXPLICIT ACSE-Requirements OPTIONAL, ... } ... END """ spec = asn1tools.compile_string(asn1_str, 'ber')参数说明:[APPLICATION 0]是 AARQ 的标签,BER 编码时会变成0x60。IMPLICIT和EXPLICIT决定标签是替换还是嵌套,DLMS 里 AARQ 用 IMPLICIT,所以编码后第一个字节是0x60,后面跟长度。如果编译时标签类型写错,编码出来的字节会多一层,电表解析失败。
提示:不同版本的 62056-62 在 ASN.1 模块上可能有细微差异,比如某些可选字段的标签号变了。对接新电表时,先拿 AARQ 做连通性测试,确认标签和长度都对得上,再往下做 GET/SET。
4. 从 AARQ 到 AARE:一次完整连接建立的调试步骤
4.1 构造并发送 AARQ 帧的实操流程
先准备串口参数:波特率 9600 或 19200,数据位 8,停止位 1,偶校验。DLMS 在串口上默认用偶校验,有些电表支持无校验,但 62056-47 推荐偶校验。打开串口后,先发一个 U 帧做链路层握手,比如0x7E 0xA0 0x1E 0x7E是 SNRM 命令,用来协商 HDLC 参数。
链路建立后,构造 AARQ。AARQ 的 ASN.1 编码结果通常以0x60开头,后面是长度和内容。把 AARQ 字节放进 HDLC 信息帧:目的地址填电表地址,源地址填客户端地址,控制域填0x10,然后计算 HCS 和 FCS。HCS 是对帧格式域到控制域做 CRC-16/X-25,FCS 是对整个帧(除标志位)做同样的 CRC。
下面是一个完整的发送函数:
import serial import crcmod crc16 = crcmod.mkCrcFun(0x11021, initCrc=0xFFFF, rev=True, xorOut=0xFFFF) def build_hdlc_frame(dest, src, ctrl, user_data): frame_format = 0xA0 # 地址长度 1 字节,帧格式 header = bytes([frame_format, dest, src, ctrl]) hcs = crc16(header).to_bytes(2, 'little') payload = header + hcs + user_data fcs = crc16(payload).to_bytes(2, 'little') frame = b'\x7E' + payload + fcs + b'\x7E' # 转义 escaped = bytearray() for b in frame: if b == 0x7E: escaped.extend([0x7D, 0x5E]) elif b == 0x7D: escaped.extend([0x7D, 0x5D]) else: escaped.append(b) return bytes(escaped) ser = serial.Serial('COM3', 9600, parity=serial.PARITY_EVEN, timeout=1) aarq_bytes = bytes.fromhex('60 36 A1 09 06 07 60 85 74 05 08 01 01 8A 02 07 80 8B 07 60 85 74 05 08 02 01 AC 0A 80 08 41 42 43 44 45 46 47 48 BE 10 04 0E 01 00 00 00 06 5F 1F 04 00 00 00 00 00 00 00') frame = build_hdlc_frame(0x01, 0x10, 0x10, aarq_bytes) ser.write(frame) response = ser.read(256) print(response.hex())参数说明:dest是电表地址,常见为 1 或 16;src是客户端地址,通常为 16 或 1;ctrl用0x10表示 I 帧。aarq_bytes里的60 36是 AARQ 标签和长度,A1 09是应用上下文名称,8A 02 07 80是认证机制,8B 07是认证值,AC 0A是用户信息。这些字节需要根据实际电表的配置调整,比如认证机制换成 HLS 时,8A 02 07 80要改成对应的 OID。
4.2 解析 AARE 响应与常见错误码
电表收到 AARQ 后,如果接受,会回一个 AARE 帧。AARE 的标签是0x61,内容里包含应用上下文名称、结果、诊断信息、用户信息。结果字段0x00表示接受,0x01表示拒绝永久,0x02表示拒绝暂时。诊断信息里会给出具体原因,比如0x01表示应用上下文不支持,0x02表示认证失败。
解析 AARE 时,先按 HDLC 帧解析拿到用户数据,再用 ASN.1 解码:
aare_bytes = bytes.fromhex('61 29 A1 09 06 07 60 85 74 05 08 01 01 A2 03 02 01 00 A3 05 A1 03 02 01 00 BE 10 04 0E 08 00 06 5F 1F 04 00 00 00 00 00 00 00') decoded = spec.decode('AARE', aare_bytes) print(decoded)参数说明:A2 03 02 01 00里的00就是结果字段,表示接受。A3 05 A1 03 02 01 00是诊断信息,00表示无诊断。如果结果是01,诊断里会给出具体错误码,比如01表示应用上下文不支持,需要检查 AARQ 里的 OID 是否和电表匹配。
注意:有些电表在 AARE 之后还会发一个 HDLC 的 RR 帧做流控确认,如果客户端没回 RR,后续的 GET 请求可能被丢弃。调试时看到 AARE 后先别急着发 GET,等一个 RR 或主动发一个 RR。
5. 避坑与排查:HDLC 和 ASN.1 调试中的五个血泪教训
5.1 帧校验失败但数据看起来没错
现象:串口抓到的帧,手动算 CRC 和帧里的 FCS 对不上,但数据内容完全正确。原因:HDLC 的 CRC 是 CRC-16/X-25,多项式0x1021,初始值0xFFFF,结果异或0xFFFF,而且输入输出都反转。很多人用标准的 CRC-16/CCITT 算,结果全错。解决:用crcmod时明确指定rev=True和xorOut=0xFFFF,或者直接查表实现。
5.2 AARQ 发出去电表不回任何数据
现象:串口写入了 AARQ 帧,但读不到任何响应,超时。原因:HDLC 地址配错,或者串口校验位不对。DLMS 电表默认偶校验,如果客户端用无校验,电表会直接忽略。另外,目的地址和源地址如果和电表里配置的不一致,电表也不回。解决:先用 U 帧 SNRM 测试链路,如果 SNRM 有响应,说明物理层和地址基本对;再检查 AARQ 的认证机制是否和电表要求的一致。
5.3 ASN.1 解码报长度错误
现象:用asn1tools解码 AARQ 时,报Length mismatch或Unexpected end of data。原因:AARQ 的 BER 编码里,长度字段可能是短形式或长形式。短形式一个字节表示 0 到 127,长形式第一个字节最高位为 1,低 7 位表示后续长度字节数。如果手动截取用户数据时多截或少截了字节,长度就对不上。解决:先打印用户数据的十六进制,确认0x60后面的长度字节和实际剩余字节数一致。不一致就检查 HDLC 解析里的用户数据偏移。
5.4 认证阶段 HLS 失败
现象:AARQ 被接受,但后续 HLS 认证的挑战响应帧被电表拒绝。原因:HLS 认证需要客户端和电表用相同的密钥和算法。常见错误是密钥填错,或者挑战字节的顺序搞反。解决:先确认电表支持的 HLS 机制,通常是2.16.756.5.8.1.3。然后检查密钥是否按标准要求的高层安全密钥,挑战响应计算时注意字节序。
5.5 GET 请求返回数据访问错误
现象:AARE 接受后,发 GET 请求,电表回错误码0x03表示对象未定义。原因:OBIS 码或类 ID 写错,或者属性索引不对。比如读正向有功总电能,类 ID 应该是 3,OBIS 是1.0.1.8.0.255,属性索引是 2。如果类 ID 写成 1,电表就找不到对象。解决:先用电表的对象列表读取服务,或者查电表手册确认 OBIS 和类 ID 的对应关系。有些电表对 OBIS 的字节序有特殊要求,需要逐字节验证。
6. 用 asn1tools 做协议模糊测试与自动化验证
6.1 构造异常 AARQ 测试电表健壮性
协议栈调通之后,下一步是验证电表对异常输入的处理。用asn1tools可以方便地构造各种边界情况的 AARQ,比如长度字段故意写错、标签号改成非法值、认证机制填一个不存在的 OID。观察电表是回 AARE 拒绝还是直接无响应。这个手段在对接不同厂商电表时特别有用,能快速摸清电表的容错边界。
import asn1tools spec = asn1tools.compile_files('dlms_asn1.asn', 'ber') # 正常 AARQ normal = { 'application-context-name': (2, 16, 756, 5, 8, 1, 1), 'sender-acse-requirements': {'authentication-mechanism-name': (2, 16, 756, 5, 8, 1, 2)}, 'user-information': b'\x04\x0E\x01\x00\x00\x00\x06\x5F\x1F\x04\x00\x00\x00\x00\x00\x00\x00' } encoded = spec.encode('AARQ', normal) # 异常:认证机制改成不存在的 OID abnormal = dict(normal) abnormal['sender-acse-requirements'] = {'authentication-mechanism-name': (1, 2, 3, 4)} try: bad_encoded = spec.encode('AARQ', abnormal) print("异常编码:", bad_encoded.hex()) except Exception as e: print("编码失败:", e)参数说明:application-context-name用元组表示 OID,sender-acse-requirements里的authentication-mechanism-name也是 OID。异常测试时,把 OID 改成非法值,看电表是否回 AARE 里的诊断信息。如果电表直接无响应,说明它对非法 OID 不做处理,实际部署时客户端要加超时重试。
6.2 自动化回归测试脚本的搭建
把 AARQ 发送、AARE 解析、GET 请求、响应校验串成一个脚本,每次修改参数后自动跑一遍。用pytest做测试框架,串口操作封装成 fixture。这样换电表或改配置时,能快速知道哪一步出了问题。
import pytest import serial @pytest.fixture def meter(): ser = serial.Serial('COM3', 9600, parity=serial.PARITY_EVEN, timeout=2) yield ser ser.close() def test_aarq_accept(meter): aarq = build_aarq() frame = build_hdlc_frame(0x01, 0x10, 0x10, aarq) meter.write(frame) resp = meter.read(256) assert resp[0] == 0x7E aare = parse_hdlc_frame(resp)['user_data'] decoded = spec.decode('AARE', bytes.fromhex(aare)) assert decoded['result'] == 0参数说明:timeout=2给电表足够的响应时间,有些电表在 AARQ 后要等 1 秒以上才回。assert decoded['result'] == 0校验 AARE 的结果字段。如果失败,打印decoded看诊断信息。这个脚本可以扩展成多个测试用例,覆盖不同认证机制、不同 PDU 长度、不同 OBIS 对象。
我自己的习惯是,每对接一款新电表,先跑一遍这个回归脚本,把 AARQ 和 AARE 的字节流存下来做基线。后面再出问题,直接和基线对比,能省很多抓包时间。希望帮到你。
本文还有配套的精品资源,点击获取