ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DLMS/COSEM电能表通讯协议调试实战:从建链到OBIS读取

DLMS/COSEM电能表通讯协议调试实战:从建链到OBIS读取

简介:一份面向电能量数据采集终端研发与调试人员的DLMS电能表通讯协议中文说明手册,内容对应IEC62056协议族,主要解决DLMS电能表数据交互中的协议理解与落地难题。资源共1个doc文件,压缩包约592KB,虽体量精简,但结构完整:从物理层、链路层到应用层逐层拆解,包含HDLC链路控制、ASN.1语法、BER与AXDR编码、AARQ/AARE连接协商,并给出请求电量、瞬时量、负荷曲线、时间等数据帧实例。文档还列出COSEM、OBIS及IEC62056-42/46/53/61等参考规范,方便开发者对照实现和排查问题。已有304人学习下载,适合智能电网、用电信息采集等领域的初中级开发者系统掌握DLMS协议要点,并可作为日常开发与现场调试的速查手册。

1. DLMS电能表通讯协议:一块表卡在串口助手上的现场

第一次正经调 DLMS 电能表,是在实验室接一块支持 DLMS/COSEM 的智能表。我以为手里有串口助手、有 USB 转 485 线,就能像读 Modbus 一样把电量读出来。结果表纹丝不动,换波特率、换校验位、换接线顺序,全都没反应。后来才明白,DLMS电能表通讯协议不是“报文格式复杂一点”那么简单,它把电能表抽象成一组对象,通讯前还要先建链接、再做应用关联,整个流程像登录系统而不是直接读寄存器。

这块协议主要解决一件事:让主站系统稳定地从电能表里读出电量、电压、电流,同时把费率、时钟、事件等参数管理起来。适合正在做表计集抄、能耗管理、充电桩计费对接的工程师。你需要的工具不多,一块支持 DLMS 的表、一个串口/网络调试工具、一点耐心,就能把最容易黑匣子的几步走通。这篇笔记按我自己调试时的顺序来写,先立概念,再谈建链,最后把最容易翻车的几个点位挨个说清楚。

2. 认识DLMS/COSEM协议栈:数据对象、OBIS码和通信层次先立住

想调通 DLMS,第一件事不是打开抓包工具,而是建立“电表是一个对象容器”的心智。DLMS/COSEM 的设计核心是把计量能力拆成一个个 COSEM 对象,主站通过读这些对象来拿业务数据。报文、帧格式、波特率都是后面的事,对象模型没理解,后面每一步都会走偏。

2.1 数据模型:COSEM对象是如何描述一块电能表的

COSEM 对象可以理解成表计内部的一个“功能模块”。每个对象由四部分描述:类别(Class)、OBIS 码、属性(Attribute)、方法(Method)。类别决定这个对象是什么类型,比如存储数值的 Register 类、带缩放的 ExtendedRegister 类、记录曲线数据的 ProfileGeneric 类;OBIS 码是这个对象在全表里的“身份证号”;属性是你可以读取的数据项;方法是你可以触发的动作,比如复位最大需量。

以最常用的“正向有功总电能”为例,它是一个 Register 类对象,OBIS 码通常是 1.0.1.8.0.255。属性 2 存的是累计电量的数值,属性 3 存缩放系数,属性 4 存单位。主站要做的,就是定位到这个对象、发起读取、把返回的字节按数据类型解析出来。

对象要素说明例子
类别 Class决定对象的行为和可用属性Register / ExtendedRegister / ProfileGeneric
OBIS 码对象在全表中的唯一标识1.0.1.8.0.255
属性 Attribute可读取的数据项数值、缩放系数、单位
方法 Method可触发的动作清零、冻结、复位

要提醒的是,不同厂商的注册表里同一个 OBIS 码对应属性的细节可能有差异。不要只凭经验写死“属性 2 就是数值”,接新表时先读一遍对象清单,再确认每个属性到底是什么。

2.2 通信栈四层与你的表对应

DLMS/COSEM 不是单层协议,而是一组协议栈。从上到下可以分成四层:对象模型层、应用层、数据链路层、物理层。对象模型层回答“我读的是什么东西”,应用层回答“服务器返回了什么”,数据链路层负责把请求变成一帧一帧的 HDLC 报文,物理层就是光口、RS-485、TCP/IP 这些实际介质。

调不通的时候,先判断是哪一层出了问题,能省掉大量瞎试的时间。

层次职责出问题时的现象
对象模型层用 OBIS 码定位数据对象能通讯但读出的数据对不上
应用层APDU 请求/响应、关联管理AARE 拒绝,或拿到错误响应
数据链路层HDLC 帧组包、拆包、确认帧抓到了但校验不对,或没有 UA
物理层电平、光信号、串口参数电表完全没有反应

HDLC 帧可以理解成“信封”,APDU 是“信纸”。信封负责把信纸完整送到对方手里,信纸的内容才是你真正要的读数据请求。很多人一上来就在信封里折腾字节,却忽略了一个前提:HDLC 链路要先建立起来,应用关联要成功建立,否则信纸根本送不进去。

2.3 为什么拿Modbus思路调DLMS会翻车

做过 Modbus 的人很容易带着老经验上 DLMS。Modbus 是寄存器模型,你直接给个地址、发个功能码,从机就回你两个字节;DLMS 是对象模型,你要先发 SNRM 建 HDLC 链路,再发 AARQ 做应用关联,关联通过后才能发 Get 请求。流程多了一个“会话协商”,这是两套协议最本质的差别。

对比项ModbusDLMS/COSEM
数据寻址寄存器地址OBIS 码 + 类别 + 属性
访问方式直接读寄存器先建链、再关联、再读写
报文格式定长功能码报文HDLC 帧 + BER 编码 APDU
安全机制基本无认证可配置 HLS 高级认证
多费率支持需要应用层自己约定对象模型天然支持

所以用串口助手手动发一串 hex 去读 Modbus 寄存器没问题,但对 DLMS 来说远远不够:你得先组织 SNRM 帧、解析 UA 响应、拼装 AARQ、解析 AARE,还要处理分块确认。这不是什么玄学,只是协议设计目标不同,DLMS 从一开始就考虑多表种、多费率、多安全级别的场景。理解这一点之后,下一章就能动真格了。

3. 建立DLMS链路:物理参数、SNRM握手与应用关联的最小流程

建 DLMS 链路分两步:先在 HDLC 层建链,再在应用层做关联。很多初学者把这两步混为一谈,只看得到“连不上”,却分不清是物理层不响应、链路层没建起来、还是应用层被拒绝。这一章按顺序把每一步说透。

3.1 物理层参数与连接准备

DLMS 表常见的接入方式有三种:光口、RS-485、TCP/IP。光口一般用在现场抄表,用红外探头对准表计的光通讯窗,波特率常见从 300、600、1200、2400 开始试,偶校验、1 停止位是很多表计的默认组合。RS-485 适合集中安装场景,半双工,常见 9600 或 19200,接线要注意 A/B 别反。TCP/IP 适合厂站或实验室,很多表计会开放专用的通讯端口。

下表是我平时上场前会先确认的参数清单,每一项都直接影响能不能建立链路。

接入方式关键参数说明
光口波特率、校验位、探头对准常见 300~2400,偶校验 8E1
RS-485波特率、数据位、停止位、终端电阻常见 9600 8N1,长线需终端电阻
TCP/IPIP、端口、TCP 连接超时端口号以表计文档为准

准备工作里有一件事值得专门做:向表计厂家确认“客户端地址、服务端地址、应用上下文名、认证方式”。这些参数不匹配,后面 AARQ 必然被拒。我见过有人在现场花了两个小时调波特率,最后发现只是服务端地址填错了一位,纯属浪费时间。

3.2 抓一次SNRM/UA握手:先让HDLC链路通起来

HDLC 层的建链通常从主站发送 SNRM(Set Normal Response Mode)开始,表计收到后回一个 UA(Unnumbered Acknowledgement)。这一步能通,说明物理参数、地址字段、控制字段基本没问题。

SNRM 的构成可以拆成几段:起始分隔符、长度字段、格式字符、目的地址、源地址、控制字段、校验字段。地址字段里,目的地址是这帧要发给谁,源地址是谁发的。我用一个示意帧来看结构,实际帧的校验字段需要按帧内容计算,这里用占位表示。

字段长度SNRM 示意值含义
起始分隔符1 字节0x7EHDLC 帧起始
长度字段2 字节0xA0 0x0A后续内容长度
格式字符1 字节0x93U 帧,SNRM
目的地址2 字节0x00 0x01服务端地址
源地址2 字节0x00 0x10客户端地址
控制字段1 字节0x21SNRM 控制字
HCS/FCS各 2 字节按实际计算校验

拿到串口助手或抓包工具里的原始 hex 后,建议先写个小脚本把它拆开看,而不是肉眼盯着十六进制猜。下面这段脚本按“2 字节地址 + 无扩展地址段”的常见情况拆帧,现场抓回来的帧直接替换 hexstr 就行。

def hdlc_field_dump(hexstr: str) -> None: raw = bytes.fromhex(hexstr) if len(raw) < 12 or raw[0] != 0x7e or raw[-1] != 0x7e: print("不是以 7E 包裹的 HDLC 帧,先检查抓包") return # 长度字段占两个字节,帧内长度用低 4 位叠加 frame_len = ((raw[1] & 0x0F) << 8) | raw[2] print("帧内容长度:", frame_len) print("格式字符:", hex(raw[3])) print("目的地址:", raw[4:6].hex()) print("源地址:", raw[6:8].hex()) print("控制字段:", hex(raw[8])) print("HCS:", raw[9:11].hex()) print("APDU:", raw[11:len(raw) - 3].hex()) print("FCS:", raw[len(raw) - 3:len(raw) - 1].hex()) if __name__ == "__main__": # 示意帧:7E A0 0A 93 00 01 00 10 21 HCS FCS 7E hdlc_field_dump("7ea00a930001001021aabbccdd7e")

脚本里最关键的是偏移计算。帧起始占了 1 字节,长度字段占 2 字节,格式字符占 1 字节,之后才是地址。因为地址按 2 字节处理,所以控制字段固定在索引 8,HCS 在 9、10,APDU 从 11 开始,FCS 在倒数第 3、第 2 字节。如果你遇到的表计用了扩展地址段,这个偏移会变,输出里“目的地址”会明显不对,这时候以表计实际报文格式为准调整切片即可。

3.3 从UA到AARE:应用关联是“登录”不是“连接”

SNRM 通了,只代表“线路能连通”,还不代表你能读数据。接下来主站要发 AARQ(应用关联请求),表计回复 AARE。只有 AARE 里返回成功,应用关联才算建立,之后才能发 Get、Set、Action 这些业务请求。把 SNRM 想成 TCP 建连,把 AARQ 想成登录认证,这个类比很方便理解。

下面是模拟这个时序的 Python 伪代码。实际工程中帧收发依赖串口库或网络库,但流程骨架是固定的。

# 伪代码:只表达 DLMS 连接时序,具体编解码依赖协议栈 link = open_transport() # 打开串口/TCP link.send(encode_hdlc(0x93, client_addr, server_addr)) # SNRM ua = link.wait_frame(timeout=5) assert ua.control == 0x73, "UA 未返回,HDLC 链路没建立" link.send(encode_hdlc(0x60, client_addr, server_addr, aarq_payload)) # AARQ aare = link.wait_frame(timeout=10) if aare.result_value() != 0: raise RuntimeError("关联被拒,错误码: %X" % aare.result_value()) print("关联成功,接下来可以发 Get/Set/Action 请求")

这里的 0x60 是 AARQ 的常见应用层标签,UA 的控制字段 0x73 也是 HDLC 层常见约定。实际表计里,AARQ 里还会带应用上下文名、客户端 SAP、服务器 SAP、认证方式等字段,任何一个和表计配置不一致,AARE 都可能回拒绝。所以做工程对接时,不要只想着“把帧拼出来”,先确认这组协商参数拿到没有。

注意:有些表计要求必须先收到合法的 SNRM 才响应 AARQ。跳过 SNRM 直接发 AARQ,表计可能直接丢弃整帧,这也是很多“发请求没响应”案例的原因之一。

4. 用OBIS码组织读取请求:从Get命令到数值解析的完整链路

链路建立起来之后,真正干活的部分是业务数据的读取。这一章从 OBIS 码的组成讲起,再拆一次 Get 请求的字段,最后落到响应解析。读完这一章,你应该能独立完成“读一块表的正向有功电量”这个小任务。

4.1 OBIS码:找到你要读的对象

OBIS 码由 6 组数字组成,每组代表不同含义。A 组表示用途,电能是 1;B 组表示通道或费率;C 组最重要,表示被测量的物理量,比如 1.8 是电量、2.8 也是电量但代表反方向、31.7 是电流、32.7 是电压;D 组表示数据的聚合方式;E 组表示费率;F 组一般是 255,代表总或者当前。

对象含义OBIS 码
正向有功总电能1.0.1.8.0.255
反向有功总电能1.0.2.8.0.255
正向无功总电能1.0.3.8.0.255
反向无功总电能1.0.4.8.0.255
A 相电压1.0.32.7.0.255
A 相电流1.0.31.7.0.255

这个表是大家常用的对象映射,但不是所有厂商都负责把对象按这里一一配齐,尤其电流、电压这类瞬时量,不同表计可能在通道号上有出入。新表到手先读对象清单,不要假设某个 OBIS 码一定存在。

4.2 一次Get请求怎么组织

DLMS 里读数据的核心请求是 Get-Request。以读正向有功总电能为例,对象是 1.0.1.8.0.255,属性通常取 2。请求里要包含对象类别、OBIS 码、属性编号,还要带 invoke-id 来关联请求和响应。这里发出去的是 APDU 内容,实际传输时还要包上 HDLC 帧头帧尾,产生校验字段,如果启用了加密还要先加密再组包。

字段区域说明典型值/取值
Get-Request 标签标识这是一次普通读请求0xC0 附近
invoke-id请求关联号,响应里原样返回0xC1、0xC2 递增
object class-id对象类别编号按对象清单
OBIS 码要读的数据对象1.0.1.8.0.255
attribute-id读第几个属性2

组装的时候最常犯的错是把 OBIS 码写成十进制 ASCII,比如把 1.0.1.8.0.255 直接转成字符串字节。OBIS 码在报文中应该按 6 个字节的裸数字编码,而不是文本。这也是很多人照着文档拼报文却一直失败的原因。

4.3 响应里的数值解析:不要上来就转float

请求发出去,表计返回的是一段 BER 编码的响应,数据本身带类型标签。读电能数据时常见的是 double-long-unsigned,也就是 4 字节无符号整数,标签 0x06;读设备标识时常见 octet-string,标签 0x09。解析时要先看标签,再决定按什么方式解码,不能拿到 4 个字节直接用 float 去解释。

def dlms_value_from_payload(payload_hex: str): data = bytes.fromhex(payload_hex) # 简化示意:跳过 Get-Response 容器标签后定位数据标签 tag = data[0] if tag == 0x06: # double-long-unsigned:1字节标签 + 1字节长度 + 4字节数值 length = data[1] if length == 4: return int.from_bytes(data[2:6], "big") return None if tag == 0x09: # octet-string:按长度读取原始字节 length = data[1] return data[2:2 + length].hex() if tag == 0x0C: # visible-string:可能是 ASCII 文本 length = data[1] return data[2:2 + length].decode("ascii", errors="ignore") raise ValueError("暂不支持的标签: 0x%02X" % tag) if __name__ == "__main__": print(dlms_value_from_payload("0604 000003E8".replace(" ", "")))

这段代码的价值不是让你直接抄进生产项目,而是演示一个关键习惯:按数据标签解析。电能值一般是大端无符号整数,解析出来的数还要乘上对象的缩放系数才是业务值。漏掉缩放系数,是读出来的数字“看起来差不多但永远差几位”的主要原因,属于典型的血泪经验。

注意:真实响应最外层还有 Get-Response、Data-Result 等容器标签,需要一层层解到数据标签。上面代码为了表达解析逻辑省略了外层剥壳,工程实现时要逐步定位,不能直接取 payload[0]。

4.4 先读对象清单,再建正式任务

拿到一只陌生表,别急着按需求清单去读。先读一遍对象列表,把支持的类、OBIS 码、属性定义拉出来,和需求对照后再开发。对象列表在不少表计上可以通过读固定 OBIS 码拿到,具体码位以表计文档为准。这个步骤能把“读错了对象”“属性编号猜错”“数据类型理解错”这三个问题一次性排除掉一大半。

读对象列表这一步完成后,你就知道这只表支持哪些业务量、哪些在哪个属性、每个属性返回什么类型。之后再写正式的采集任务,基本就是一锤定音。把这一步放在所有开发前面,是我个人在 DLMS 项目里学到的最值得坚持的做事顺序。

5. DLMS调试避坑五连:从链路不通到读数错位的排查路径

DLMS 调试难,不是因为帧格式真的复杂到学不会,而是问题会伪装成各种样子。这一章把最常见的五个问题按“现象、原因、解决”写清楚,每一条都是现场真实会遇到的。对照一下你卡在哪一步,能少走很多弯路。

5.1 现象:SNRM 发出后电表无响应

现象很直白:主站发了 SNRM,帧看起来没问题,但表计完全没有反应,UA 一直不来。这时候最容易被怀疑成“协议栈写错了”,实际上大部分原因是物理层参数没对上。

原因有几种:光口探头没对准,红外收发位置偏了;波特率和表计不一致,常见表计默认 300、600、1200、2400,不是随便选 9600 就能通;RS-485 的 A/B 线接反;TCP 场景下 IP 通但端口不对。还有一种少见但存在的情况,表计刚上电还在启动,没有进入通讯服务状态。

解决思路是先排除物理层。光口场景,从 300 波特开始逐档试,同时确认偶校验、停止位;RS-485 场景,用万用表量一下 A/B 间电压,正常空闲差压在 1V 以上;TCP 场景先 ping 通,再 telnet 到约定端口看能不通。物理层确认没问题,再去抓帧看是否发出了正确的 HDLC 帧头 0x7E。

5.2 现象:SNRM/UA 正常但 AARE 被拒

SNRM 和 UA 都成功了,说明 HDLC 链路已经建好,但发完 AARQ 之后,返回的 AARE 结果不是成功状态,或者干脆超时。这是“物理层通了、应用层被拒”的典型表现。

原因是协商参数不一致:客户端地址和服务端地址没有对齐,AARQ 里带的认证方式表计不支持,应用上下文名不匹配,或者表计已经有一个未释放的关联会话。还有些表计默认要求高等级安全认证,你以无认证方式发起关联,自然会被拒。

解决方法是把 AARQ 里的每一项配置都核对一遍,尤其是应用上下文名和认证方式。如果表计文档提供了“客户端连接参数确认表”,照着逐项填比猜更靠谱。遇到“关联会话未释放”的情况,先发 ABORT 释放旧会话,等几秒重新发起关联,大多能恢复。

5.3 现象:读请求超时或收到分块长度超限

应用关联建立成功,发 Get 请求也正常,但某些对象读不出来,一直超时,或者抓到的响应里有分块标记但客户端没处理。这通常是块大小和窗口大小协商不一致导致的。

原因在于,DLMS 的大数据对象,比如负荷曲线、事件记录,返回数据可能很长。主站和表计之间有一个 negotiated block size,一次只能传一块,块传完要客户端确认后再传下一块。如果客户端没有按块发送确认帧,服务端会一直等,表现出来就是超时。

解决办法是把客户端的块大小调小,常见从 16 或 32 开始,先保证小数据量对象能读通;同时确认客户端实现了分块确认逻辑,不要只处理单块响应。在抓包里能明显看到“response 带分块标记,客户端没有下一步动作”时,优先补这个逻辑。

5.4 现象:通讯正常但读出的数值和表显对不上

链路全通,AARE 成功,Get 也拿到响应,但读到的数字和表上显示的不一致。比如表上显示 1234.56,你读回来的是 123456,或者完全错位。能通讯但数据错,问题几乎都出在对象映射和解析环节。

原因通常有几类:OBIS 码选错,比如把反向有功当成正向有功读了;属性编号选错,读到的不是当前值而是其他数据;没有按数据类型解析,把 4 字节当成 8 字节读;更常见的是漏了缩放系数,读出来的是整数原始值,没有乘上表计定义的比例因子。

解决方法是回到对象清单,确认目标对象的 OBIS 码、类 ID、属性 ID、数据类型、缩放系数,再按清单逐项解析。解析时不要眼睛盯着 hex 猜,把标签识别做出来,每个对象单独验证,数据对不上的问题很快能定位到具体环节。

5.5 现象:运行一段时间后连接悄悄断掉

采集程序跑了一段时间,突然开始报错,重连也连不上。重启程序又能恢复,过几小时又断。这种情况多半不是网络问题,而是应用关联会话有超时,表计主动断开了会话。

原因有两个:一是表计侧对关联会话设置了空闲超时,一段时间没有请求就自动释放;二是客户端断线后没有正确释放旧关联,重新发起 AARQ 时表计还在维护旧会话,因此拒绝新关联。

解决方法是给客户端加心跳机制,定时发送一个轻量的读请求,比如读当前时间或一个标量对象,保持会话活跃;同时在重连逻辑里,收到 AARE 拒绝时先发送 ABORT 释放旧会话,再重新走关联流程。生产环境的采集程序应该把重连做成状态机,而不是简单循环发请求。

6. 进阶验证:用模拟服务端跑通时序,再养成抓包习惯

DLMS 客户端开发最危险的阶段,是直接带着代码上现场,面对一个不会告诉你“你哪里错了”的黑盒子。更稳的做法是在实验室先用模拟服务端把客户端时序跑通,再把抓包变成习惯。

实验环境里可以搭一个最小模拟服务端,按状态机回包:收到 SNRM 回 UA,收到 AARQ 回成功 AARE,收到 Get 请求回固定数据。客户端的时序正确性在这个环境里能快速验证,比直接上真表高效得多。下面这个对照表可以用来快速定位客户端状态。

客户端当前状态期望服务端响应收不到时先查什么
刚发 SNRMUA物理参数、地址字段
刚发 AARQAARE应用上下文名、认证方式
刚发 GetGet-ResponseOBIS、属性编号、分块逻辑

另一个值得坚持的习惯,是把“抓包”放在“撸代码”前面。到现场无论多急,先抓一帧表计返回的原始报文,确认帧结构、地址、标签都符合文档,再开始写业务代码。很多通讯问题不是逻辑复杂,而是从一开始就在错误的方向上反复试错。

我自己第一次做 DLMS 接入时,上来就写完整客户端,结果在光口波特率上耗了整整两天,后来才发现只是 2400 和 4800 的档位和表计配置不一致。那次以后我养成了习惯:进现场先抓一帧,对着帧结构确认参数,再谈后续开发。协议是不会骗人的,骗人的往往是我们跳过步骤的急躁。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进