首页 / 资讯中心 / 文章详情

DLMS电能表抄表协议栈:HDLC帧与ASN.1/BER编码解析

DLMS电能表抄表协议栈:HDLC帧与ASN.1/BER编码解析 ★ FEATURED ARTICLE
简介这份资源是面向电能量数据采集终端开发者、电能表通讯调试人员及电力计量方向学习者的 DLMS 协议中文说明手册用于解决 IEC62056 协议族资料以英文标准为主、上手门槛高的问题。内容围绕三层协议模型展开涵盖物理层的端口初始化与收发控制、链路层基于 HDLC 的地址校验与 CRC 校验及长帧拆包组包、应用层的 ASN.1 语法、BER 与 AXDR 编码、AARQ 与 AARE 建链帧以及数据请求流程并附请求电量、瞬时量电压、电流、功率、负荷曲线与时间的实际报文范例可对照抓包逐帧分析。压缩包内为 1 个 doc 文档约 620KB体量轻便适合作为案头速查与技术入门读物。该资源已有 873 人学习说明在同类协议资料中具备一定参考价值适合需要快速理解 DLMS 链路建立与数据交互流程的初、中级技术人员。1. 三层协议栈为什么一条抄表指令要走三次握手串口抓包最反直觉的一幕一条“读当前电量”的指令Modbus 只要 8 个字节一问一答而在 DLMS也常被写作 DMLS协议族的电能表上线上跑了七八个回合、上百个字节才拿到一个数。这份 2003 年的 DLMS 协议中文手册把 IEC62056 系列里被英文文档层层包裹的东西——HDLC 帧的 0x7E 边界、地址的扩展编址、控制字的帧计数、ASN.1 语法与 BER/AXDR 两套编码——逐字节摊开。它不是使用说明是一份能让你对着抓包结果反推“电表为什么不应答”的底层说明书。搞懂它的价值在于现场排障时你面对的不再是一串十六进制玄学而是一条能逐层拆开的因果链。2. HDLC 帧结构0x7E 之间到底装了什么链路层是这套协议里最容易被跳过、又最容易翻车的一层。应用层的数据再规整只要 HDLC 这一层的地址、计数或校验错一个位电表就是一声不吭串口上什么也看不到——没有错误码没有拒绝帧就是沉默。所以读 DLMS 的帧必须先把 0x7E 之间的布局背下来。链路层由两个子层构成LLC 子层和 MAC 子层。LLC 子层只做转发不做处理它的全部存在感体现在三个字节的帧头上client 端发往 server 的数据要加0xE6 0xE6 0x00server 端发往 client 的数据要加0xE6 0xE7 0x00。这个方向对应关系是硬性的写反了对方不会报错只会当成无效帧丢弃。真正干活的是 MAC 子层地址检查、CRC 校验、长帧的打包拆包全在这里。一份带应用层数据的完整帧长这样字段长度字节说明0x7E1帧头标志帧类型与帧长2高 4 位 Frame Type 恒为 A1 位 S11 位帧长目的地址域1 / 2 / 4扩展编址末字节最低位为 1源地址域1client 端固定一字节控制域1帧类型 收发帧计数 P/F 位帧头校验 HCS2只覆盖帧头部分的 CRCLLC 帧头3E6 E6 00 或 E6 E7 00用户数据信息≤128默认上限 128 字节可在 SNRM 中协商帧校验 FCS2覆盖整帧的 16 位 CRC0x7E1帧尾标志不带应用层数据的帧比如 SNRM、DISC 这类链路控制帧没有 HCS、LLC 帧头和用户数据这三段帧头直接接控制域后面跟 FCS。这个差异很关键控制帧比数据帧短得多用同一个解析器一把梭很容易在控制帧上读越界。2.1 帧类型与帧长恒为 A 的高四位和那个 S 位帧类型与帧长字段占两个字节内部被切成三段4 位 Frame Type、1 位 S、11 位帧长。HDLC 本身定义了多种帧类型DLMS 只用其中一种Frame Type 恒为1010也就是十六进制的 A。所以任何一帧的第一个字节高四位必然是 A低四位里藏着 S 位和帧长的高 3 位。以手册里的 SNRM 帧头a0 21为例a0拆开是1010 0000Frame Type AS 0帧长高 3 位 00021是帧长低 8 位合起来帧长 0x021 33 字节正好等于两个 0x7E 之间除标志位外的全部字节数。再看一个 DISC 帧头a0 0a帧长 10 字节。这个“帧长不含两个 0x7E”的定义要在写解析器时钉死差 2 就会把帧尾标志吃进数据里。S 位segmentation bit是分片的开关。它只有一位含义却贯穿整个长帧传输流程置 0 表示这一帧是完整的置 1 表示这一帧是被切开的片段之一。接收端一旦检测到 S 位置位就必须按分片逻辑处理——先把片段缓存起来再主动发 RR 帧去要下一段而不是把这段数据直接丢给应用层。2.2 地址域与扩展编址一个字节为什么只能放 128 个地址HDLC 用的是扩展编址地址不是一个定长大端整数而是一串字节每个字节只有低 7 位承载地址信息最低位是“还有没有后续字节”的标志。最低位为 0 表示这个地址域没结束后面还有字节最低位为 1 表示到这里为止。这个规则直接决定了 client 地址的上限。client 端地址永远只有一个字节而这一字节的最低位又被固定为 1表示结束剩下 7 位有效所以 client 地址只能有 128 个。这不是设计者随手定的数是编址规则推出来的硬上限。server 端则被拆成两部分upper HDLC address 表示逻辑地址lower HDLC address 表示物理地址从而实现“一个物理表对应多个逻辑设备”。upper 总是要有lower 在确认不需要时可以省略。三种可用结构是一字节只有 upper、两字节upper 一字节 lower 一字节、四字节upper 两字节 lower 两字节。手册里那个 SNRM 帧的地址段值得逐字节看一遍00 22 00 23。前三个字节00、22、00的最低位全是 0只有最后一个23的最低位是 1——这就是目的地址域的结束标记四个字节共同构成一个地址域而不是两个独立地址。取低 7 位拼起来upper 两字节是00 22lower 两字节是00 23。第一次看这段十六进制十有八九会以为00 22和00 23是两组地址然后在解析器里写错偏移。解析这段地址的逻辑常见的实现思路是这样def decode_hdlc_address(buf, pos): 从 buf[pos] 开始解析一个 HDLC 地址域返回 (有效地址字节, 下一位置) raw bytearray() while True: b buf[pos] raw.append(b 0x7F) # 低 7 位才是地址有效位 pos 1 if b 0x80: # 最低位为 1本地址域结束 break return bytes(raw), pos def split_server_address(raw): server 地址按长度拆成逻辑地址与物理地址 if len(raw) 1: return raw, b # 只有 upper half len(raw) // 2 return raw[:half], raw[half:]decode_hdlc_address的关键在 0x7F和 0x80这两次掩码前者剥掉标志位拿到真实地址后者判断结束。对00 22 00 23调用会一路吃到第四个字节才 break返回00 22 00 23四个字节split_server_address再把它切成 upper00 22与 lower00 23。这段代码没有任何协议常量全靠位规则驱动所以 client 的一字节地址03用同一个函数也能正确解析成一个字节。手册里有一句实测结论非常值钱那款被测电表只有四字节 server 地址结构可用一字节和两字节结构都不通。这意味着地址长度不是“协议允许就能用”还得看具体设备的实现。协议栈里做地址协商之前先拿目标表实测一遍地址结构能省掉一整天的排查。2.3 控制字与帧计数RRR/SSS 的滚动和 P/F 位控制字一个字节装了三样东西帧类型、收发帧计数、P/F 位。帧计数分两组RRR 是接收帧计数N(R)SSS 是发送帧计数N(S)。它们的滚动规则是这套协议里最需要拿笔算一遍的部分。链路层连接建立之后第一次请求数据时在 client 端包括发送 AARQ 那一帧RRR 置 0、SSS 置 0。Server 收到后返回响应RRR 变 1、SSS 仍为 0。Client 再请求时 RRR 加 1、SSS 加 1。Server 收到后再各自加 1如此往复直到 client 拿全数据。少加一次、多加一次或者把“响应帧里的 RRR 和 SSS 该谁加”搞混表现都是电表返回 FRMR 或者干脆不回。这类问题没有错误提示只能靠把控制字一位一位摆出来核对。第二个是 P/F 位。Poll bit 由 client 置位置 1 表示请 server 回应置 0 表示不要求回应Final bit 由 server 置位置 1 表示本次发送结束置 0 表示还没发完。手册特别点明F 位只在通讯窗口window size大于 1 时才有意义窗口等于 1 时server 返回的每一帧这一位总是 1。原因很直白——窗口为 1 意味着一次只允许一帧在途每一帧天然就是“最后一帧”F 位没有区分能力。帧类型的编码可以从手册给的实际字节反推。建立链路用 SNRM控制字0x93响应帧 UA 是0x73结束链路 DISC 是0x53DM对 DISC 的响应表示此前已处于断开状态是0x1FRR准备接收用来请求后续分片是0x01RNR 是接收没准备好作用等价于别的协议里的忙帧UI 可以用于保持链路但对链路控制没有任何影响FRMR 是在不确定原因下拒绝收到的帧。这些帧里有几个是成对出现的SNRM 对 UA 或 DMDISC 对 UA 或 DM。UA 在这里身兼两职——既可能是“SNRM 收到链路建好了”也可能是“DISC 收到链路断了”DM 则表示“你发 DISC 之前我本来就是断的”。所以解析 UA 的时候不能只看帧类型还得结合上下文和里面的参数块判断这次握手成功与否。3. 建链、分片与请求一次抄表在链路上到底发生了什么DLMS 的通讯过程是一条严格串行的链先建物理层连接打开端口、初始化 Modem再建链路层连接SNRM/UA再建应用层连接AARQ/AARE 的 Negotiation然后才轮到数据请求最后 DISC 断链、关物理口。这条链上任何一环缺失后面都走不通。跳过链路层直接发应用层帧电表不会理你链路建好了但没做应用层协商就发 Get-Request电表同样不会给你数据。手册明确写了链路层连接建立之后在 DLMS 协议中还要建立应用层连接才可进行数据通讯这个建立过程被称为 Negotiation。三段连接各自管各自的事谁也不替谁兜底。3.1 SNRM 帧逐字节拆解参数协商谈的到底是什么把手册那帧 SNRM 完整摆开7e a0 21 00 22 00 23 03 93 0b 14 81 80 12 05 01 80 06 01 80 07 04 00 00 00 01 08 04 00 00 00 07 65 5e 7e逐段对应7e帧头a0 21是帧类型 A、S0、帧长 0x213300 22 00 23是目的地址也就是 server 的四字节地址03是源地址client 自己的地址93是控制字标识 SNRM 且 P 位置位0b 14是帧头校验 HCS覆盖从帧类型到控制字这一段接下来是信息域。信息域开头三个字节81 80 12是固定前缀81是 SNRM 的标识80表示后面跟的是参数协商块12是参数块总长度 18 字节。然后四组参数依次排列05 01 80parameter id 05长度 1 字节值 0x80 128表示发送方向的最大信息域长度。06 01 80parameter id 06长度 1 字节值 128表示接收方向的最大信息域长度。07 04 00 00 00 01parameter id 07长度 4 字节值 1表示发送窗口大小。08 04 00 00 00 07parameter id 08长度 4 字节值 7表示接收窗口大小的提议值。帧尾65 5e是 FCS最后7e收尾。整帧 33 字节与帧长字段完全对上。这四组参数的结构高度规律id 长度 值长度字段本身就告诉你后面要读几个字节。所以解析参数块不需要为每个 id 写死偏移写一个通用的 TLV 循环就够了def parse_snrm_params(info: bytes, pos: int 3): info 前 3 字节是 81 80 len从第 3 字节开始逐组解析 params {} while pos len(info): pid info[pos] plen info[pos 1] value int.from_bytes(info[pos 2: pos 2 plen], big) params[pid] value # 05/06 是长度07/08 是窗口大小 pos 2 plen return paramsint.from_bytes(..., big)这个细节不能省窗口大小参数虽然只有 1但占用 4 个字节高字节在前按大端拼出来才是 1。曾经见过把 4 字节直接取最后一个字节当值的写法在值小于 256 时碰巧能跑一旦窗口调大就崩。值得单独拎出来说的是07和08这两组。SNRM 里发送窗口提议 1、接收窗口提议 7而 UA 回应里两个都变成了 1。这就是协商的本质不是照单全收而是双方各报能力最终取一个双方都能接受的值。写协议栈时如果把 SNRM 里的提议值直接当成生效值使用窗口行为就会和电表实际行为对不上——你以为能连发 7 帧电表却按 1 帧在等。3.2 长帧分片与 window sizeS 位打开后收发双方怎么对账用户数据默认上限 128 字节。请求电量、请求时间这类单值数据一帧足够但请求负荷曲线几乎必然超限——手册里直接写明请求负荷曲线时一定是使用长数据帧。超限就得切片。切片流程是把大包切成若干短帧依次发出每片的帧类型与帧长字段里 S 位置 1接收端检测到 S 位后进入分片处理状态把各片缓存起来client 通过发送 RR 数据帧来请求被分割数据的其余部分全部收齐后重新组装成一个完整数据包交给应用层。这里有个容易忽略的时序约束请求数据结束后还要再发送 RR 帧收到确认之后才可以发 DISC 帧结束链路。手册里专门强调了这一点。省掉这个 RR 直接 DISC电表可能还在等后续分片收到 DISC 后按异常流程处理链路状态和预期不一致。window size 与分片是两个独立但会互相影响的东西。窗口描述的是“一次允许几帧在途”分片描述的是“一个逻辑数据包拆成几帧”。窗口为 1 时每发一帧都得等对方确认才能发下一帧分片传输就退化成严格的一问一答效率低但实现简单窗口大于 1 时多帧可以连发F 位开始承担“本批发完没有”的语义。手册给的建议很克制默认值就是窗口 1、信息域 128 字节。这个默认值的取向很明确——先保证能在绝大多数表上跑通性能让位给兼容性。组装分片时最需要小心的是边界分片是按“用户数据”切的不是按整帧切的。HCS、LLC 帧头、FCS 每片都要有不能被切进用户数据里。拼装的时候只拼用户数据段别把中间那些帧头也接进去。3.3 从 AARQ 到 Get-Request应用层连接为什么不能省链路层只保证字节可靠送达它不知道对面这个连接上要谈什么业务。应用层连接Association解决的就是这件事client 发 AARQ 帧server 回 AARE 帧双方在这一步把应用上下文、认证方式、PDU 大小等参数谈定。手册对这个过程给的定义是 Negotiation措辞很准确——这不是一次简单的能力查询是双向议价。AARQ/AARE 是整套协议里少数用 DLMS 编码而非 XDLMS 编码的帧。手册点明DLMS 中只有 AARQ 与 AARE 数据帧的部分内容是使用 DLMS 协议的绝大多数的应用层数据通讯使用 XDLMS。这句话解释了一个常见困惑——为什么抓包看到的 AARQ 是 BER 编码的 TLV 结构而后面 Get-Request 的响应却是紧凑的平铺字节。两套编码在同一个连接上前后脚出现不是实现出错是协议本身就这么分的。应用层连接建立后才是数据通讯。client 发请求帧server 回响应帧。请求不同数据时要用特定数据独有的 class id 和 OBIS 来标识数据类型——这两个字段是应用层的“地址”链路层的地址找到表class id OBIS 找到表里的哪一个量。手册给出的四类典型请求是请求电量、请求瞬时量电压、电流、功率、请求负荷曲线、请求时间。这四类恰好覆盖了三种数据形态单值标量电量、时间、多值标量组瞬时量三项、结构化数组负荷曲线。负荷曲线之所以必然触发长帧就是因为它是一个随时间累积的数组元素个数取决于冻结周期和存储深度。Get-Request 的语法结构在手册里给得很清楚Get-Request-Normal :: SEQUENCE { invoke-id-and-priority Invoke-Id-And-Priority, cosem-attribute-descriptor Cosem-Attribute-Descriptor, access-selection-parameters Selective-Access-Descriptor OPTIONAL }按顺序读先是 invoke-id 与优先级打包在一个字节里高 4 位优先级、低 4 位 invoke-id用来把请求和响应配对然后是属性描述符里面依次是 class-id、instance-id6 字节 OBIS和 attribute-id最后是可选的访问选择参数负荷曲线按时间范围取数时就要靠它。GET-Request 本身是个 CHOICE 类型只能是三个分支之一get-request-normal、get-request-next、get-request-with-list。get-request-next 用于取分片数据的下一块get-request-with-list 用于一次请求多个对象。这个 CHOICE 结构解释了一个现象为什么有些请求的响应里只有一个值有些是一串值——不是编码差异是选了不同的分支。4. 应用层三件套ASN.1 描述、BER 与 AXDR 编码理解 DLMS 应用层得先接受一个前提它不用表格加固定帧格式来描述协议而是用一种抽象语法语言来描述。手册对这个选择的评价是“极大的提高了协议的抽象性和通用性有利于程序移植”。代价是抽象——你没法像看 Modbus 那样对着表格数第几个字节是什么得先读懂语法再看编码怎么落地。三件套的分工是ASN.1 负责“描述结构”BER 和 AXDR 负责“把结构变成字节”。DLMS 用 BER 实现XDLMS 用 AXDR 实现。三者串起来才是一帧完整的应用层数据。4.1 ASN.1 的 tag class 与 IMPLICIT/EXPLICIT为什么同一个字段在不同结构里含义不同一个 ASN.1 描述的帧大概长这样Name :: [tag] IMPLICIT/EXPLICIT Data type { null-data [0] IMPLICIT NULL, item1 [1] IMPLICIT/EXPLICIT Data type A1 OPTIONAL, item2 [2] Data type A2 OPTIONAL, ... }Name 是帧名花括号里是数据项每项后面跟的数字序号就是它的 tag。tag 分两部分class type 和一个数字。class type 有四种区别在于“这个 tag 的含义在多大范围内唯一”。Universal 表示在所有 DLMS 应用中含义唯一Application 表示含义与具体应用有关Private 表示只在某厂家自定义范围内有效Context-specific 表示含义依赖上下文——同一个 tag 在不同的结构里可能有完全不同的意思。最后这一种是理解 DLMS 应用层的关键也是读帧时最容易踩坑的地方看到一个[1]不能条件反射地认为它是某个固定字段得看它所在的父结构是什么。IMPLICIT 与 EXPLICIT 描述的是子帧与父帧的 tag 继承关系。IMPLICIT 会改变父帧的 tagEXPLICIT 不改变不注明时默认为 EXPLICIT。这个区别落到字节上就是“外层多一层壳还是不多一层壳”——EXPLICIT 会额外编码一个 tag 和长度IMPLICIT 则直接用父级的 tag 顶替。手册里 AARE 的定义中protocol-version [0] IMPLICIT BIT STRING、application-context-name [1]、responding-authentication-value [10] EXPLICIT authentication-value三种写法混在一起不是随意为之每一处的选择都对应着字节数上的取舍。两种复合类型要分清。SEQUENCE 表示内容顺序排列Get-Request-Normal 就是典型CHOICE 表示从几个类型里选一个GET-Request 是典型三个分支只能填一个。读 CHOICE 类型的帧第一步永远是确定当前是哪个分支否则后面所有字段的偏移都是错的。常用简单类型和它们的 tag number 手册里列了一张表INTEGER 是 20x02、BIT STRING 是 3、OCTET STRING 是 4、NULL 是 5、OBJECT IDENTIFIER 是 6、SEQUENCE 是 160x10、SET 是 170x11、PrintableString 是 19、T61String 是 20、IA5String 是 22、UTCTime 是 23。这些数字在解析 BER 编码的帧时会反复出现比如06 07 60 85 74 05 08 01 01里的06就是 OBJECT IDENTIFIER后面07是它的长度再后面七个字节是 OID 的值。4.2 BER 编码从一帧 AARE 看 TLV 是怎么落地的BER 的基本结构是三段数据标识、数据长度、数据内容。内容部分可以再嵌套一个完整的 BER 结构递归下去就形成了树。手册为了让人记住三段的相对位置把它压成一个 16 位位串来看高 2 位是 class type第 3 位是 primitive/constructed低 13 位是长度。落到真实字节流里这个位置关系仍然成立只是分居在两个字节上首字节高 2 位是 classUniversal 00、Application 01、Context-specific 10、Private 11第 3 位是 constructed 标志0 表示简单类型1 表示复合类型也就是数组或结构低 5 位是 tag number紧跟的长度字节若最高位为 0低 7 位就是长度。拿手册的 AARE 帧走一遍61 42 A1 09 06 07 60 85 74 05 08 01 01 A2 03 02 01 00 A3 05 A1 03 02 01 00 88 02 07 80 89 07 60 85 74 05 08 02 01 AA 0A 80 08 41 42 43 44 45 46 47 48 BE 04 0E 08 00 06 5F 1F 04 00 00 08 1D 21 34 00 07 00 07首字节61拆开0110 0001高 2 位 01 是 Application class第 3 位 1 表示 constructed低 5 位 00001 就是 tag number 1正对应 AARE 定义里的[APPLICATION 1]。第二字节42最高位为 0低 7 位 0x42 66 就是整个 AARE 内容的长度。这两个字节合起来就是手册说的“AARE tag 61、AARE len 42”。往下A1 09是 application-context-nameA1高 2 位 10 是 Context-specific、constructed 置位、tag 109是长度 9内容里06 07 60 85 74 05 08 01 01是一个 OID06是 OBJECT IDENTIFIER 的 tag07是长度 7七十…七个字节的值是应用上下文的标识。再往下A2 03 02 01 00是 resultA2是 context tag 2、长度 3、内容02 01 00是一个 INTEGER 值 0表示关联成功。A3 05 A1 03 02 01 00是 result-source-diagnostic里面嵌套了一层A1再里面是 INTEGER 0。最后几段很能说明“可选字段是否出现由上下文决定”88 02 07 80对应responding-acse-requirements [8] IMPLICIT88 Context 10 constructed 1 tag 889 07 ...对应mechanism-name [9] IMPLICIT值是一个 OIDAA 0A 80 08 41 42 43 44 45 46 47 48对应responding-authentication-value [10] EXPLICIT注意这里是 EXPLICIT所以多一层结构AA里面先80再08长度值是八个 ASCII 字节 4148。把这些串起来看BER 的解析逻辑其实只有十几行def ber_next(buf, pos): 读一个 BER 的 TLV返回 ((class, constructed, tag_no, value), next_pos) tag buf[pos]; pos 1 cls (tag 6) 0x03 # 00 Universal / 01 Application / 10 Context / 11 Private constructed (tag 5) 0x01 tag_no tag 0x1F length buf[pos]; pos 1 if length 0x80: # 长格式后续 length0x7F 个字节表示长度 n length 0x7F length int.from_bytes(buf[pos:pos n], big) pos n return (cls, constructed, tag_no, buf[pos:pos length]), pos length这里(tag 6) 0x03和(tag 5) 0x01就是把首字节按位切开tag 0x1F取 tag number。长格式分支不能省短格式只能表示 0127 字节负荷曲线这类数据很容易超过忽略这个分支就会把长度读成一个巨大的错值然后解析器一路跑飞。还有一个细节要注意AARE 的尾部BE 04 0E 08 00 06 5F 1F 04 00 00 08 1D 21 34 00 07 00 07里BE对应user-information [30] IMPLICIT而它内部装的内容已经是 XDLMS 的 AXDR 编码了。手册在这里明确标注了“以下内容为 XDLMS 用 AXDR 描述”两套编码在同一个帧里交接的边界就在这个BE上。解析器到这里必须切换解析器实例继续用 BER 规则读下去得到的结果一定不对。4.3 AXDR 与 BER 的差别少一层壳省多少字节用同一个数据结构对比最直观。两个整数值分别是 0x1234 和 0x5678组成一个序列。BER 编码30 08 02 02 12 34 02 02 56 78拆开看30是 SEQUENCE08是内容长度 8内容里02 02 12 34是第一个 INTEGERtag 02、长度 02、值本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站