简介:这份资源是面向电能量数据采集终端开发者、电能表通讯调试人员及电力计量方向学习者的 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 0x00,server 端发往 client 的数据要加0xE6 0xE7 0x00。这个方向对应关系是硬性的,写反了对方不会报错,只会当成无效帧丢弃。真正干活的是 MAC 子层:地址检查、CRC 校验、长帧的打包拆包,全在这里。
一份带应用层数据的完整帧长这样:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 0x7E | 1 | 帧头标志 |
| 帧类型与帧长 | 2 | 高 4 位 Frame Type 恒为 A,1 位 S,11 位帧长 |
| 目的地址域 | 1 / 2 / 4 | 扩展编址,末字节最低位为 1 |
| 源地址域 | 1 | client 端固定一字节 |
| 控制域 | 1 | 帧类型 + 收发帧计数 + P/F 位 |
| 帧头校验 HCS | 2 | 只覆盖帧头部分的 CRC |
| LLC 帧头 | 3 | E6 E6 00 或 E6 E7 00 |
| 用户数据信息 | ≤128 | 默认上限 128 字节,可在 SNRM 中协商 |
| 帧校验 FCS | 2 | 覆盖整帧的 16 位 CRC |
| 0x7E | 1 | 帧尾标志 |
不带应用层数据的帧(比如 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 0000,Frame Type = A,S = 0,帧长高 3 位 = 000;21是帧长低 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 22,lower 两字节是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 是0x53;DM(对 DISC 的响应,表示此前已处于断开状态)是0x1F;RR(准备接收,用来请求后续分片)是0x01;RNR 是接收没准备好,作用等价于别的协议里的忙帧;UI 可以用于保持链路,但对链路控制没有任何影响;FRMR 是在不确定原因下拒绝收到的帧。
这些帧里有几个是成对出现的:SNRM 对 UA 或 DM,DISC 对 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、S=0、帧长 0x21=33;00 22 00 23是目的地址,也就是 server 的四字节地址;03是源地址,client 自己的地址;93是控制字,标识 SNRM 且 P 位置位;0b 14是帧头校验 HCS,覆盖从帧类型到控制字这一段;接下来是信息域。
信息域开头三个字节81 80 12是固定前缀:81是 SNRM 的标识,80表示后面跟的是参数协商块,12是参数块总长度 18 字节。然后四组参数依次排列:
05 01 80:parameter id 05,长度 1 字节,值 0x80 = 128,表示发送方向的最大信息域长度。06 01 80:parameter id 06,长度 1 字节,值 128,表示接收方向的最大信息域长度。07 04 00 00 00 01:parameter id 07,长度 4 字节,值 1,表示发送窗口大小。08 04 00 00 00 07:parameter 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 size:S 位打开后收发双方怎么对账
用户数据默认上限 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-id(6 字节 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 会改变父帧的 tag,EXPLICIT 不改变,不注明时默认为 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 是 2(0x02)、BIT STRING 是 3、OCTET STRING 是 4、NULL 是 5、OBJECT IDENTIFIER 是 6、SEQUENCE 是 16(0x10)、SET 是 17(0x11)、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 位是 class(Universal 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-name:A1高 2 位 10 是 Context-specific、constructed 置位、tag 1;09是长度 9;内容里06 07 60 85 74 05 08 01 01是一个 OID,06是 OBJECT IDENTIFIER 的 tag,07是长度 7,七十…七个字节的值是应用上下文的标识。
再往下A2 03 02 01 00是 result:A2是 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] IMPLICIT,88= Context 10 + constructed 1 + tag 8;89 07 ...对应mechanism-name [9] IMPLICIT,值是一个 OID;AA 0A 80 08 41 42 43 44 45 46 47 48对应responding-authentication-value [10] EXPLICIT,注意这里是 EXPLICIT,所以多一层结构,AA里面先80再08长度,值是八个 ASCII 字节 41~48。
把这些串起来看,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: # 长格式:后续 length&0x7F 个字节表示长度 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。长格式分支不能省:短格式只能表示 0~127 字节,负荷曲线这类数据很容易超过,忽略这个分支就会把长度读成一个巨大的错值,然后解析器一路跑飞。
还有一个细节要注意: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是 SEQUENCE,08是内容长度 8;内容里02 02 12 34是第一个 INTEGER(tag 02、长度 02、值
本文还有配套的精品资源,点击获取