简介:这份资源面向电能表通信协议开发者、自动抄表系统(AMR)设计人员及嵌入式软件工程师,围绕DLMS/COSEM与HDLC两套通信标准,提供从协议规范到代码实现的完整参考。DLMS/COSEM作为IEC 62056系列国际标准,用于解决计量系统中数据采集、仪表安装维护与系统集成等互操作问题,资源同时收录中英文文档与配套源码,便于对照理解协议细节。压缩包共161个文件,约31.15MB,以C语言源文件与头文件为主体,辅以Makefile构建脚本、PDF与Word技术文档、Markdown说明及少量C++实现,覆盖AES加密、关联建立、定时器与队列等模块,可直接用于协议栈移植与调试。目前已有802人学习下载,适合需要快速搭建开发环境、研究报文交互流程或排查通信问题的中高级读者参考。
1. 拆开 DLMS/COSEM 与 HDLC 这套源码包:电表通信协议栈到底怎么跑起来
做电力采集终端、智能电表或能源管理网关的兄弟,大概率都碰过 DLMS/COSEM 这套协议。它不像 Modbus 那样几行代码就能跑通,光是 ACSE 关联、xDLMS 服务、HDLC 帧格式这几层叠在一起,就够让人头大。我手上这份资源包,把 DLMS/COSEM 通信协议文档、软件源码,连同 HDLC 协议资料和源码一起打包了,等于把从物理链路层到应用层的整条链路都摊开给你看。它解决的核心问题很直接:不用再从零啃 IEC 62056 那几百页标准,也不用满世界找一份能编译、能对着抓包调试的参考实现。适合谁?做电表集抄、充电桩计费、光伏逆变器数据上云的嵌入式或后端工程师,尤其是需要自己实现协议栈、或者要对接第三方 DLMS 设备却卡在帧解析和关联建立上的那批人。这套东西不是玩具 demo,是能直接进项目里当底座的料。
2. DLMS/COSEM 协议栈分层:从 HDLC 帧到 xDLMS 服务的完整链路
2.1 为什么这套协议要分四层,少一层都跑不通
DLMS/COSEM 的通信模型是典型的四层结构,从下往上依次是物理层、HDLC 数据链路层、COSEM 应用层(含 ACSE 和 xDLMS)。很多人第一次看标准会懵:为什么不能像 Modbus 那样一个函数发出去就完事?原因在于电表场景对可靠性和互操作性要求极高。物理层负责电气信号,HDLC 负责把字节流切成帧、做 CRC 校验和重传,ACSE 负责建立和释放应用关联(也就是常说的“握手”),xDLMS 才是真正读写 OBIS 对象的那一层。少任何一层,要么帧对不上,要么关联建不起来,要么读回来的数据不知道对应哪个对象。
我一般会建议按这个顺序理解:先看 HDLC 怎么组帧,再看 ACSE 怎么协商认证,最后看 xDLMS 的 GET/SET 怎么封装成 APDU。这套资源里的文档部分,正好是按这个层次组织的,源码也对应分了模块。你拿到手先别急着编译,花半小时把目录结构和头文件里的结构体定义扫一遍,比直接跑 demo 效率高得多。
2.2 HDLC 帧结构拆解:标志位、地址域、控制域和 FCS 到底怎么填
HDLC 是这套协议里最容易翻车的地方,因为它的帧格式和常见串口协议差别很大。一个标准 HDLC 帧长这样:起始标志 0x7E、地址域、控制域、信息域(也就是上层 APDU)、帧校验序列 FCS、结束标志 0x7E。地址域在 DLMS 里通常是客户端地址和服务端地址的组合,控制域则区分 I 帧、RR 帧、SNRM 帧等类型。下面这段是组帧的核心逻辑,我按常见实现方式写出来:
// HDLC 帧组装示例:以 SNRM 帧为例 // 地址域:客户端地址 0x10,服务端地址 0x01(低字节在前) uint8_t frame[64]; int idx = 0; frame[idx++] = 0x7E; // 起始标志 frame[idx++] = 0x10; // 客户端地址低字节 frame[idx++] = 0x01; // 服务端地址低字节 frame[idx++] = 0x93; // 控制域:SNRM 帧 frame[idx++] = 0x00; // 信息域长度高字节 frame[idx++] = 0x0F; // 信息域长度低字节,SNRM 参数长度 // 信息域:SNRM 参数(最大信息域长度、窗口大小等) frame[idx++] = 0x81; // 参数标识:最大信息域发送长度 frame[idx++] = 0x80; // 长度 frame[idx++] = 0x02; // 值:0x0200 = 512 字节 frame[idx++] = 0x00; // ... 其余参数省略 uint16_t fcs = crc16_x25(frame + 1, idx - 1); // FCS 计算范围不含标志位 frame[idx++] = fcs & 0xFF; frame[idx++] = (fcs >> 8) & 0xFF; frame[idx++] = 0x7E; // 结束标志这段代码里几个参数必须说清楚。地址域在 DLMS 里不是随便填的,客户端地址通常由上位机分配,服务端地址是电表出厂设定,两者必须和电表实际配置一致,否则帧会被直接丢弃。控制域 0x93 代表 SNRM,这是建立 HDLC 链路的第一步,发完这个帧电表才会回 UA 帧。FCS 用的是 CRC-16/X-25,多项式 0x1021,初值 0xFFFF,结果取反,计算范围从地址域开始到信息域结束,不包含前后标志位。很多人第一次写就是 FCS 算错,导致电表一直不回帧,还以为是串口线接反了。
提示:HDLC 帧里的 0x7E 如果出现在数据区,需要做转义处理,规则是 0x7E 转成 0x7D 0x5E,0x7D 转成 0x7D 0x5D。这个细节文档里一般会写,但源码里如果没处理,遇到特定数据就会丢帧。
2.3 ACSE 关联建立:AARQ 和 AARE 里哪些字段不能填错
HDLC 链路通了之后,下一步是 ACSE 关联。客户端发 AARQ,电表回 AARE,这一步决定了后续能不能读写数据。AARQ 里关键字段包括应用上下文名称、认证机制、用户信息等。应用上下文名称在 DLMS 里是固定的 OID,比如 0x06 0x07 0x60 0x85 0x74 0x05 0x08 0x01 0x01 对应“逻辑名引用无认证”的场景。认证机制如果选低安全等级,一般是 0x00 表示无认证,或者 0x02 表示低等级密码认证。用户信息里放的是 xDLMS 初始化请求,也就是常说的 xDLMS InitiateRequest。
我见过太多人卡在关联这一步,现象是电表回了 AARE 但带错误码,或者干脆不回。常见原因有三个:一是应用上下文名称写错,二是认证密码长度不对,三是 xDLMS 版本号不匹配。这套资源里的源码把 AARQ 和 AARE 的编解码单独抽了模块,你对着文档里的 ASN.1 定义看,能省很多查标准的时间。
2.4 xDLMS 服务:GET/SET 请求怎么封装成 APDU
关联建立后,真正的数据读写靠 xDLMS 服务。一个 GET 请求的 APDU 结构大致是:服务标签(0xC0 表示 GET-Request)、请求类型(0x01 表示 GET-Request-Normal)、优先级、对象标识符(OBIS 码)、属性索引。OBIS 码是电表对象的唯一标识,比如 0x28 0x00 0x01 0x00 0x00 0xFF 对应有功总电能。属性索引 0x02 通常表示值属性。下面是一个 GET 请求的封装示例:
# xDLMS GET 请求 APDU 封装示例 def build_get_request(obis, attr_index=2): # 服务标签:GET-Request apdu = bytearray([0xC0, 0x01, 0x00]) # 对象标识符:OBIS 码,6 字节 apdu.extend(obis) # 属性索引 apdu.append(attr_index) # 访问选择:无 apdu.append(0x00) return bytes(apdu) # 读取有功总电能,OBIS 0x28 0x00 0x01 0x00 0x00 0xFF obis_code = bytes([0x28, 0x00, 0x01, 0x00, 0x00, 0xFF]) request = build_get_request(obis_code) print(request.hex()) # 输出:c001002800010000ff0200这段代码里,0xC0 是 GET 请求的服务标签,0x01 是 Normal 类型,0x00 是优先级。OBIS 码必须和电表里的对象字典一致,不同厂家可能有细微差异,但 0x28 0x00 0x01 0x00 0x00 0xFF 这个有功总电能是标准里定义的,通用性最好。属性索引 0x02 对应“值”,0x03 对应“单位”,读数据一般用 0x02。最后的 0x00 是访问选择描述符,表示不指定选择性访问。发出去之后,电表回 GET-Response,里面带数据类型和实际值,解析时要注意数据类型标签,比如 0x06 表示双精度浮点,0x12 表示无符号 32 位整数。
3. 把源码跑起来:编译、串口配置和第一个 SNRM 帧的发送
3.1 源码目录结构:先看头文件再看实现
这套资源里的软件源码,我拿到手先看目录。通常会有 hdlc、acse、xdlms、hal 这几个模块,hal 是硬件抽象层,负责串口收发。头文件里定义了帧结构体、状态机和回调函数指针。建议先编译一个最小可执行文件,只跑 HDLC 层,把 SNRM 帧发出去,看电表回不回 UA。如果这一步通了,后面 ACSE 和 xDLMS 就是顺水推舟。
编译环境一般是 GCC 或 Keil,取决于你是跑在 Linux 网关还是 STM32 上。串口参数必须和电表一致:波特率常见 9600 或 19200,数据位 8,停止位 1,校验位偶校验(Even)。DLMS 在串口上默认是偶校验,这个和 Modbus 的默认无校验不一样,很多人在这里翻车。
3.2 串口配置与 SNRM 发送:一个最小可运行示例
下面这段是串口初始化和发送 SNRM 帧的示例,基于 Linux 下的 termios:
#include <termios.h> #include <fcntl.h> #include <unistd.h> int open_serial(const char *port) { int fd = open(port, O_RDWR | O_NOCTTY); struct termios tty; tcgetattr(fd, &tty); cfsetospeed(&tty, B19200); // 波特率 19200 cfsetispeed(&tty, B19200); tty.c_cflag |= (CLOCAL | CREAD); tty.c_cflag &= ~CSIZE; tty.c_cflag |= CS8; // 8 数据位 tty.c_cflag |= PARENB; // 使能校验 tty.c_cflag &= ~PARODD; // 偶校验 tty.c_cflag &= ~CSTOPB; // 1 停止位 tty.c_cc[VMIN] = 1; tty.c_cc[VTIME] = 10; // 读超时 1 秒 tcsetattr(fd, TCSANOW, &tty); return fd; } // 发送 SNRM 帧后读取 UA 响应 void send_snrm_and_wait(int fd) { uint8_t snrm[] = {0x7E, 0x10, 0x01, 0x93, 0x00, 0x0F, 0x81, 0x80, 0x02, 0x00, 0x00, 0x7E}; write(fd, snrm, sizeof(snrm)); uint8_t buf[256]; int n = read(fd, buf, sizeof(buf)); if (n > 0) { // 检查是否为 UA 帧,控制域 0x73 if (buf[3] == 0x73) { // UA 帧收到,HDLC 链路建立成功 } } }这段代码里,串口配置的关键是 PARENB 和 PARODD 的组合,PARENB 使能校验,PARODD 清零表示偶校验。SNRM 帧里的 0x93 是控制域,0x00 0x0F 是信息域长度,后面跟的是 SNRM 参数。UA 帧的控制域是 0x73,收到它就说明 HDLC 链路通了。如果 read 返回 0 或超时,先查串口线是不是交叉接法(TX 接 RX),再查波特率和校验位。
3.3 从 UA 到 AARE:关联建立的完整时序
HDLC 链路通了之后,紧接着发 AARQ。AARQ 的 APDU 会作为 I 帧的信息域发出去,控制域是 0x10 或 0x30 这种 I 帧格式。电表回 AARE,如果关联成功,AARE 里的结果字段是 0x00。这一步的时序很关键:发完 AARQ 后要等 AARE,不能紧接着发 GET 请求,否则电表会认为你违反了状态机。源码里一般用状态机来管理,状态从 IDLE 到 HDLC_CONNECTED 再到 ASSOCIATION_ESTABLISHED,每个状态只允许特定操作。
我一般会在代码里加日志,把每一步收发的原始字节打印出来。这样一旦关联失败,对着日志看是 AARQ 哪个字段不对,比盲猜快得多。这套资源里的文档有 ASN.1 的字段说明,配合日志看,基本能定位到具体字节。
4. 避坑与排查:HDLC 帧、关联和 xDLMS 解析里的五个血泪教训
4.1 现象:电表不回 UA 帧,串口无任何响应
原因:最常见的是串口参数不匹配,尤其是校验位。DLMS 默认偶校验,如果设成无校验,电表收到的帧 CRC 会错,直接丢弃。其次是地址域填反,客户端和服务端地址搞混。还有一种情况是 SNRM 帧的 FCS 计算范围错了,把起始标志也算进去了。
解决:先用示波器或串口助手确认物理层有波形,再逐项核对波特率、数据位、停止位、校验位。地址域按“客户端地址在前、服务端地址在后”的顺序填,具体值查电表说明书。FCS 计算范围严格从地址域第一个字节到信息域最后一个字节,不含标志位。
4.2 现象:收到 UA 帧但 AARE 返回错误码 0x01
原因:AARQ 里的应用上下文名称或认证机制不匹配。电表可能只支持特定认证方式,比如只支持低等级密码认证,而你发的是无认证。或者 xDLMS 版本号填错,电表不支持该版本。
解决:查电表文档确认支持的应用上下文和认证机制。如果是密码认证,检查密码长度和编码方式,DLMS 里密码通常以八位字节串形式传递。xDLMS 版本号一般填 0x06,对应 DLMS 6.0 版本。
4.3 现象:GET 请求发出去,电表回错误码 0x02(不支持的对象)
原因:OBIS 码写错,或者属性索引不对。不同厂家的 OBIS 码可能有差异,标准里定义的有功总电能是 0x28 0x00 0x01 0x00 0x00 0xFF,但有些电表用 0x28 0x00 0x01 0x00 0x00 0x00。属性索引 0x02 是值,0x03 是单位,读数据用 0x02。
解决:先用关联后的“对象列表”服务获取电表支持的所有 OBIS 码,再从中选目标对象。如果电表不支持对象列表,就查厂家提供的对象字典文档。属性索引不确定时,先读 0x01(逻辑名)确认对象存在,再读 0x02。
4.4 现象:数据能读回来但解析出来是乱码
原因:xDLMS 的数据类型标签没处理对。比如 0x06 是双精度浮点,占 8 字节;0x12 是无符号 32 位整数,占 4 字节。如果按固定长度解析,遇到变长类型(如数组、结构体)就会错位。
解决:解析时先读数据类型标签,再根据标签决定后续字节数。数组和结构体要递归解析,注意长度字段。这套资源里的源码有数据类型解析模块,可以直接参考。我一般会写一个通用的解析函数,按标签分派,避免硬编码长度。
4.5 现象:长时间运行后通信断掉,重启才能恢复
原因:HDLC 窗口机制没处理好,或者没有做超时重传。DLMS 的 HDLC 支持滑动窗口,如果发送方一直发但没收到确认,窗口会满,后续帧发不出去。另外,电表可能主动发断开请求,客户端没处理就会挂死。
解决:实现超时重传和窗口管理,发送方维护一个待确认队列,收到 RR 帧后滑动窗口。同时监听电表发来的 DISC 帧,收到后主动释放关联并重新建立。源码里如果有状态机,检查状态转换是否覆盖了断开和重连场景。
5. 进阶用法:用 Python 快速验证协议栈和抓包对比
5.1 用 Python 脚本模拟客户端,快速验证电表响应
如果你不想每次都编译 C 代码,可以用 Python 快速搭一个测试脚本。pyserial 负责串口收发,自己实现 HDLC 组帧和 FCS 计算,就能在几分钟内验证电表是否响应。下面是一个最小示例:
import serial import struct def crc16_x25(data): crc = 0xFFFF for b in data: crc ^= b for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0x8408 else: crc >>= 1 return crc ^ 0xFFFF def build_snrm(client_addr=0x10, server_addr=0x01): body = bytes([client_addr, server_addr, 0x93, 0x00, 0x0F, 0x81, 0x80, 0x02, 0x00, 0x00]) fcs = crc16_x25(body) frame = b'\x7E' + body + struct.pack('<H', fcs) + b'\x7E' return frame ser = serial.Serial('/dev/ttyUSB0', 19200, parity=serial.PARITY_EVEN, bytesize=8, stopbits=1, timeout=2) ser.write(build_snrm()) resp = ser.read(256) print(resp.hex())这段脚本里,crc16_x25 函数实现了 CRC-16/X-25 算法,多项式 0x8408 是反向表示。build_snrm 组装 SNRM 帧,body 部分从地址域到信息域结束,FCS 用小端序附加。串口打开时 parity 设为 PARITY_EVEN,和电表一致。发出去后读响应,如果收到 UA 帧,控制域会是 0x73。这个脚本适合快速验证物理层和 HDLC 层,确认通了再上 C 代码做完整协议栈。
5.2 抓包对比:用串口助手和 Wireshark 定位问题
DLMS 的调试离不开抓包。我一般用串口助手把原始字节存成二进制文件,再用 Wireshark 的 DLMS 解析插件打开。Wireshark 能自动识别 HDLC 帧、ACSE 关联和 xDLMS 服务,把每一层的字段都展开。对比你发的帧和电表回的帧,能快速定位是哪个字段不对。比如 AARE 里的结果字段是 0x01,Wireshark 会直接标出“认证失败”,比对着十六进制猜快得多。
如果没有 Wireshark 插件,也可以自己写一个简单的解析脚本,按 HDLC 帧格式切分,再按 APDU 标签解析。这套资源里的文档有完整的 ASN.1 定义,配合脚本能覆盖大部分调试场景。
5.3 一个具体技巧:用对象列表服务摸清电表支持的所有 OBIS 码
很多电表支持“对象列表”服务,发一个 GET 请求读对象列表的 OBIS 码,电表会返回所有支持的对象。这个技巧能省去查厂家文档的时间。对象列表的 OBIS 码通常是 0x00 0x00 0x28 0x00 0x00 0xFF,属性索引 0x02。读回来是一个数组,每个元素包含 OBIS 码、类 ID、版本号和属性数量。拿到这个列表后,你就知道电表里有哪些对象可读,不用一个个试。
我一般会在关联建立后先读对象列表,把结果存成 JSON 文件,后续调试直接查这个文件。这样即使换电表,也能快速适配。这套资源里的源码如果有对象列表解析模块,直接复用;没有的话,按数组类型递归解析也不复杂。
从那以后我每次拿到新的 DLMS 电表,都强制先走一遍“SNRM → AARQ → 对象列表”这三步,确认链路和对象字典都对了,再写业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取