简介本资源面向电力自动化、智能计量与物联网方向的开发者聚焦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[i1] ^ 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:1addr_len], big) src_addr int.from_bytes(unescaped[1addr_len:12*addr_len], big) # 控制域 ctrl unescaped[12*addr_len] # 用户数据从 HCS 之后开始 user_data unescaped[12*addr_len3:-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 PDUAARQ 就在其中。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就是 OIDsender-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-25FCS 是对整个帧除标志位做同样的 CRC。下面是一个完整的发送函数import serial import crcmod crc16 crcmod.mkCrcFun(0x11021, initCrc0xFFFF, revTrue, xorOut0xFFFF) 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, parityserial.PARITY_EVEN, timeout1) 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 或 16src是客户端地址通常为 16 或 1ctrl用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时明确指定revTrue和xorOut0xFFFF或者直接查表实现。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 应该是 3OBIS 是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用元组表示 OIDsender-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, parityserial.PARITY_EVEN, timeout2) 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参数说明timeout2给电表足够的响应时间有些电表在 AARQ 后要等 1 秒以上才回。assert decoded[result] 0校验 AARE 的结果字段。如果失败打印decoded看诊断信息。这个脚本可以扩展成多个测试用例覆盖不同认证机制、不同 PDU 长度、不同 OBIS 对象。我自己的习惯是每对接一款新电表先跑一遍这个回归脚本把 AARQ 和 AARE 的字节流存下来做基线。后面再出问题直接和基线对比能省很多抓包时间。希望帮到你。本文还有配套的精品资源点击获取
