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

DLMS电表通信协议栈解析:HDLC帧结构与ASN.1应用层调试

DLMS电表通信协议栈解析:HDLC帧结构与ASN.1应用层调试 ★ FEATURED ARTICLE
简介《DMLS协议中文版》是一份面向电能量数据采集终端研发、调试与运维人员的中文协议手册主要解决与遵循IEC62056/DLMS协议族电能表通信时的协议理解与报文对照问题适合需要实现采集功能、排查通信故障或学习协议分层的读者。内容按物理层、链路层、应用层三层模型组织说明HDLC链路层中的地址校验、帧长校验、CRC校验与长帧拆包组包并重点展开应用层的ASN.1语法、BER与AXDR编码、AARQ/AARE连接帧及数据请求过程同时给出请求电量、瞬时量电压、电流、功率、负荷曲线和时间的报文实例便于结合实际数据包理解class id、OBIS与字段含义。资源包共1个doc文件约620KB均为中文说明文档查阅方便。目前已有873人学习可作为DLMS协议入门和现场联调的参考材料。1. 一块电表读个数为什么要走三层DLMS 的分层不是仪式感抓一帧现场报文来看读一个正向有功电量链路上要来回跑十几帧其中大半字节跟电量这两个字毫无关系。DMLS 协议中文版这份手册要讲清楚的恰恰是这些无关字节为什么必须存在。DLMS 协议族对应 IEC 62056 系列把一次抄表拆成物理层、链路层、应用层不是为了好看而是因为电表侧那条通道随时会丢字节、被截断、被别的设备插话而应用层想要的却是一个结构化的对象值。分层的本质是把不可靠的字节流和可靠的数据模型隔离开。搞懂它的价值很直接现场十次通讯失败九次不是表坏了而是某一层的状态机没对齐——帧计数错了一位、地址长度猜错一个字节、编码用反了一套。这类问题不往协议栈内部看就只能靠重启和换线碰运气。2. HDLC 链路层两个 0x7e 之间到底塞了什么2.1 帧结构的两种形态有没有应用数据差了哪三段链路层的帧永远以 0x7e 开头、以 0x7e 结尾中间那一段才是真正的链路用户数据。不带应用数据的帧比如 SNRM、DISC、UA结构很朴素帧类型与帧长、目的地址、源地址、控制域、帧校验。一旦这帧里要携带应用层的东西中间就会多插三段帧头校验、LLC 帧头、用户数据。多出来的三段各有分工。帧头校验是对帧头四个字段帧类型与帧长、目的地址、源地址、控制域单独算的一次 CRC作用是让接收端在还没解析地址之前就能判断这帧的头部有没有被噪声打坏——如果头部错了地址解析会直接跑飞后面全白搭。LLC 帧头是固定三字节Client 发往 Server 用0xe6 0xe6 0x00Server 回往 Client 用0xe6 0xe7 0x00逻辑链路控制子层只做转发不碰内容。用户数据就是应用层交下来的东西默认最长 128 字节想要更长必须在建链时协商。提示帧长字段只统计两个 0x7e 之间的字节数不含这两个标志本身。很多解析器写成从 0x7e 数到 0x7e遇到用户数据里恰好出现 0x7e 就会把一帧劈成两半。正因为有帧长字段接收端可以按长度定界而不是扫描 0x7e 找结尾。常见的实现思路是收到 0x7e 后先取两字节帧头解出 11 位长度再老老实实读够这么多字节最后才校验 FCS。这个顺序不能反——先校验再定界等于让攻击面或者噪声决定你的缓冲区边界。2.2 扩展编址地址字节的最低位为什么是个开关HDLC 的地址域长度不固定靠的就是每个字节的最低位这一位为 0表示后面还有字节属于同一个地址为 1表示这个地址到此结束。也就是说一个地址字节真正能承载信息的只有高 7 位最低位是续行标志。这个设计省掉了单独的地址长度字段代价是解析必须逐字节判断。def parse_hdlc_address(buf: bytes, start: int 0): 解析扩展编址的 HDLC 地址域返回 (address_bytes, next_offset)。 规则只有一条某字节最低位为 1表示该地址域结束。 addr [] i start while True: b buf[i] addr.append(b) i 1 if b 0x01: # 最低位为 1 - 地址域结束 break if i - start 4: # 协议允许最长 4 字节超了基本是帧头错位 raise ValueError(address field too long, frame header misaligned) return addr, i这段代码里最值钱的不是循环而是那个防御分支。地址域一旦错位后面解析出来的控制字、HCS、长度全是垃圾而错误表现往往是电表不回应看起来像硬件问题。Client 端地址固定一字节最低位恒为 1所以可用地址只有 128 个够用。Server 端为了用一个物理口挂多个逻辑设备把地址拆成 upper逻辑地址和 lower物理地址两段可选的字节结构有三种一字节只放 upper两字节放 upper 一字节加 lower 一字节四字节放 upper 两字节加 lower 两字节。理论上还能更长实践中有上限。注意Server 地址结构不是自描述的接收方无法从帧里看出来对端用的是哪种长度。同一款表用一字节地址发 SNRM 可能连一个 DM 都不回换成四字节立刻就通。某款三相表实测只有四字节结构可用另外两种发出的帧在链路层就被静默丢弃。2.3 帧控制字RRR、SSS 两个三位计数与 P/F 位控制域负责两件事标识帧类型以及维护收发帧计数。计数只有 3 位分成 RRR接收帧计数和 SSS发送帧计数从 0 incremented 到 7 再回绕。建链之后第一次请求Client 侧两个计数都是 0Server 收到后回一帧RRR 变 1、SSS 仍是 0Client 再请求时两个都加 1如此往复。交互轮次Client 发出的 I 帧Server 回送的 I 帧第 1 次RRR0, SSS0RRR1, SSS0第 2 次RRR1, SSS1RRR2, SSS1第 3 次RRR2, SSS2RRR3, SSS2之所以只能到 7是因为一个字节里还要塞帧类型和 P/F 位。计数回绕带来一个副作用长报文跑几十帧之后计数会绕好几圈靠计数应该递增来判断链路是否健康是不可靠的必须结合帧序号和响应内容一起看。P/F 位是另一个容易看漏的字段。Poll 位由 Client 置置 1 表示这帧请你回应置 0 表示别急着回Final 位由 Server 置置 1 表示这一轮发送结束置 0 表示后面还有。原文说得很清楚Final 位只有在通讯窗口大于 1 时才有意义窗口等于 1 时 Server 返回的每一帧这一位恒为 1。这就解释了为什么默认配置下抓包看到的响应帧 P/F 位永远是 1——窗口是 1一帧就是一个回合。帧类型本身有十来种I 是信息传输帧RR 表示准备接收下一帧RNR 相当于忙帧SNRM 用于建链UA 是对 SNRM 和 DISC 的响应DISC 结束链路DM 表示已处于断开态UI 只用于保持链路、对链路控制没有任何影响FRMR 表示因为某种不确定原因拒收。这套帧类型和标准 HDLC 是同一套控制字编码也一致SNRM 是 0x83置上 Poll 位就是 0x93DISC 是 0x43置 Poll 位就是 0x53。看到 0x93 就该知道这是 Client 在请求建链而不是什么自定义指令。2.4 长帧分段S 位、RR 帧与 128 字节的天花板用户数据默认上限 128 字节这个数字写在帧头协商参数里不是写死的常量。一旦要传的内容超过这个数——请求负荷曲线时几乎必然超——就必须把长帧切成多个短帧依次发送。切分时把帧头里的 S 位segmentation bit置 1接收端看到这一位就知道后面还有后续分段要按分段逻辑拼接而不是当成独立报文处理。拼接的方向由谁发起取决于谁的数据更长。Client 要发长请求就自己切Server 要回长响应就切完逐帧送Client 收到一帧 S1 的响应后回一个 RR 帧表示我准备好了继续给下一段。这个 RR 帧不是可有可无的礼貌回复它是流控信号——没有它Server 不知道该不该发下一段。帧校验用的是 16 位 CRC具体多项式和生成表在 IEC 62056-46 的附录 A 里实现时照抄即可。真正容易翻车的是校验范围帧头校验覆盖到控制域为止帧校验覆盖哪一段不同实现的理解经常不一致。稳妥的做法是拿一帧已知正确的报文把自己的 CRC 算法从各个起点试一遍对上为止不要凭文档描述猜范围。3. 建链与断链SNRM 里的四个参数决定了后面所有帧3.1 SNRM 帧逐字节拆一个 33 字节的协商请求建链是 Client 主动发起的一帧 SNRM 发过去Server 回 UA 表示链路建立回 DM 表示链路当前是断开状态、连接没建成。这一帧里塞的东西比想象中多值得逐字节过一遍。snrm bytes.fromhex( 7ea0210022002303930b14 # flag 类型/长度 目的地址(4B) 源地址(1B) 控制字 HCS 818012 # 信息域标识 参数组总长 0x12 18 字节 050180 # 05: 发送方向最大信息域长度, 长度1, 值 0x80 128 060180 # 06: 接收方向最大信息域长度, 长度1, 值 0x80 128 070400000001 # 07: 发送窗口大小, 长度4, 值 1 080400000001 # 08: 接收窗口大小, 长度4, 值 1 655e # FCS 7e # flag )对应关系是这样的7e是标志a0 21两个字节里高 4 位固定为 A即 Frame Type 3第 5 位是 S 位这里为 0说明不分段剩下 11 位是长度 0x021即 33 字节00 22 00 23是目的地址四字节扩展编址最后一个字节最低位为 1 表示结束03是源地址一字节同样最低位为 193是控制字SNRM 置 Poll 位0b 14是帧头校验。信息域开头的81 80是 SNRM 帧的标识紧跟着的12是参数组长度 18正好等于后面四组参数的长度之和3 3 6 6 18。四组参数分别是发送方向最大信息域长度、接收方向最大信息域长度、发送窗口大小、接收窗口大小。注意窗口大小用了 4 字节而信息域长度只用了 1 字节这种参数长度自己带的格式很像 TLV扩展新参数时不用改帧格式——这就是为什么这套协议能活二十多年还在用。提示SNRM 帧里的信息域可以整个不出现含义是我接受你Server已经配置好的参数。这在只想读数据、不想改协商结果的场景下省事但会让你无法预知后续帧长上限读负荷曲线时容易撞上分段。3.2 两个参数的实际影响窗口大小与最大信息域长度WINDOW_SIZE描述通讯双方一次能连续发几帧默认 1。MAXIMUM_INFORMATION_FIELD_LENGTH描述单个链路帧里用户数据的长度默认 128 字节。这两个默认值不是随便定的而是冲着电表这类资源受限从站去的窗口为 1 意味着发一帧等一帧从站不需要维护接收缓冲队列信息域 128 字节意味着一个接收缓冲区 128 字节就够RAM 压力极小。UA 帧回来时会把 Server 侧的四个参数一并带回Client 要按双方都能接受的值来组织后续报文。这里有个隐蔽的坑SNRM 里写的是我提议的UA 里回来的是我确认的两者可能不同。如果 Client 按自己提议的 128 去切分而 Server 只认 64超出的部分会被静默丢弃或者触发 FRMR。窗口大于 1 时Final 位才开始承担实际语义Server 连发多帧前几帧 Final0最后一帧 Final1告诉 Client这一批发完了。窗口为 1 时这个信息是冗余的所以实现里经常偷懒不看这一位——等到某天对端配置成窗口 4代码立刻乱序。3.3 断开连接UA 和 DM 的区别在哪断开比建立简单得多因为不用协商参数。Client 发一帧 DISCServer 回 UA 或 DM。UA 的含义是收到你的 DISC现在已经断开DM 的含义是在你发 DISC 之前我本来就处于断开状态。disc bytes.fromhex(7ea00a00220023035306c77e) # a0 0a - 类型 A, S0, 长度 10 (从类型字段数到 FCS, 不含 7e) # 00 22 00 23 - 目的地址, 四字节 # 03 - 源地址 # 53 - 控制字, DISC 且 Poll1 # 06 c7 - FCS这两种回应的诊断价值完全不同。收到 UA说明这一轮通讯是干净收尾的收到 DM说明链路本来就是断的——上一轮没正常结束、对端刚上电、或者中间被别人插过一脚都可能是原因。现场排查时把 DM 当成正常响应忽略掉等于放弃了最早的一个异常信号。原文还提到一个实践建议通讯结束后应该主动发 DISC而不是靠 Server 的超时挂断机制收场。原因是超时期间 Server 不接新连接采集器下一轮进来会撞在门上表现就是每隔一轮失败一次这种极有规律的玄学现象。血泪经验是凡是看到周期性失败先看结束帧有没有发。4. 应用层AARQ/AARE 与 ASN.1 那一整套抽象语法4.1 为什么链路通了还要第三次握手链路层只保证字节完整地到了对面它不知道对面支持哪些服务、能收多大的包、要不要认证。这些信息必须再谈一次这个过程叫 Negotiation。Client 发 AARQServer 回 AARE谈成了才能发数据请求。谈的内容包括应用层上下文名决定用哪套对象模型和认证方式、协商结果、认证机制、以及嵌在 user-information 里的 XDLMS 初始化参数协议版本、一致性块 conformance、客户端最大接收 PDU 大小。一致性块是关键它是一张位图声明我支持 GET / SET / ACTION / 事件通知 / 分块传输里的哪几项。Server 回了什么Client 就只能用什么越界调用会直接被拒。数据请求阶段定位一个具体值靠两个东西class id 和 OBIS 码。class id 说明这是哪一类对象寄存器、曲线、时钟、脚本表OBIS 说明是这一类里的哪一个比如正向有功总电量、A 相电压。这套对象标识体系来自 IEC 62056-61好处是不同厂家的表用同一套坐标就能定位到同一个物理量采集器不用为每个型号写一套解析。4.2 ASN.1 的 Tag四个 class 与 IMPLICIT/EXPLICIT 的取舍应用层的帧不是用表格定义的而是用 ASN.1 这种抽象语法描述的。一个数据帧长这样名字、tag、数据类型、大括号里的若干数据项每项后面跟一个数字标识。Tag 包含两部分class type 和一个数字。class 有四种——Universal 表示这个定义在所有 DLMS 应用里含义唯一Application 表示含义与应用相关Private 表示厂家自定义范围Context-specific 表示含义依赖上下文同一个 tag 在不同结构里可能是不同东西。这个四分法不是学术洁癖它决定了接收方遇到不认识的 tag 时该跳过还是该报错Universal 不认识就是真的错Private 不认识可以安全跳过。IMPLICIT 和 EXPLICIT 描述的是子数据帧与父数据帧的关系。IMPLICIT 会替换掉父帧的 tagEXPLICIT 不替换、多包一层。不注明就是 EXPLICIT。这个区别在编码上体现得非常直接AARE 里的A1application-context-nameIMPLICIT编码出来只有一个 tag而AAauthentication-valueEXPLICIT后面还要再套一个80表示内层是 context 0。初学者照着文档写解析器最常在这一层多剥或少剥一个字节。数据类型有固定的 tag 编号常用的这些类型tag十进制tag十六进制INTEGER202BIT STRING303OCTET STRING404NULL505OBJECT IDENTIFIER606SEQUENCE / SEQUENCE OF1610SET / SET OF1711PrintableString1913T61String2014IA5String2216UTCTime2317SEQUENCE 表示内容按顺序排列这是绝大多数请求帧的形态CHOICE 表示从几个候选结构里选一个当前本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站