
简介这份资源面向电能表通信协议开发者、自动抄表系统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 0x5E0x7D 转成 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_index2): # 服务标签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_addr0x10, server_addr0x01): 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, parityserial.PARITY_EVEN, bytesize8, stopbits1, timeout2) 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 里的结果字段是 0x01Wireshark 会直接标出“认证失败”比对着十六进制猜快得多。如果没有 Wireshark 插件也可以自己写一个简单的解析脚本按 HDLC 帧格式切分再按 APDU 标签解析。这套资源里的文档有完整的 ASN.1 定义配合脚本能覆盖大部分调试场景。5.3 一个具体技巧用对象列表服务摸清电表支持的所有 OBIS 码很多电表支持“对象列表”服务发一个 GET 请求读对象列表的 OBIS 码电表会返回所有支持的对象。这个技巧能省去查厂家文档的时间。对象列表的 OBIS 码通常是 0x00 0x00 0x28 0x00 0x00 0xFF属性索引 0x02。读回来是一个数组每个元素包含 OBIS 码、类 ID、版本号和属性数量。拿到这个列表后你就知道电表里有哪些对象可读不用一个个试。我一般会在关联建立后先读对象列表把结果存成 JSON 文件后续调试直接查这个文件。这样即使换电表也能快速适配。这套资源里的源码如果有对象列表解析模块直接复用没有的话按数组类型递归解析也不复杂。从那以后我每次拿到新的 DLMS 电表都强制先走一遍“SNRM → AARQ → 对象列表”这三步确认链路和对象字典都对了再写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取