ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

DLMS/COSEM与HDLC协议解析:从报文抓包到联调避坑指南

DLMS/COSEM与HDLC协议解析:从报文抓包到联调避坑指南 简介面向电表通信与自动抄表领域的DLMS/COSEM协议资料包内含中英文标准文档和HDLC协议实现源码可支撑数据采集、设备互操作及系统集成场景。压缩包共195个文件以60个h头文件与39个c源码文件为主另有12份doc、11份pdf规范文档、6个cpp示例、14个mk构建脚本及32个zbak备份文件整体约36MB目录分层清晰便于按模块查阅。已有41人学习。资料既覆盖IEC62056相关中英文规范也包含可直接分析的C语言实现如aes、cmac、gcm等加密模块csm_association、queue、tasks等任务与关联管理逻辑配合HDLC链路层源码适合嵌入式工程师、协议开发者和计量系统相关专业的学生用于理解DLMS/COSEM协议栈、开展自动抄表AMR系统研发或课程设计。1. DLMSCOSEM 和 HDLC 这套资料读什么、怎么用才不浪费手头拿到一个压缩包名字叫“DLMSCOSEM通信协议文档资料软件源码HDLC协议资料和软件源码”里面大概率是标准 PDF、厂商文档、示例工程和一堆“试了能通但不知道为什么”的代码。这套东西真正对应的是智能电表、集中器、采集主站之间那套通信规则DLMS/COSEM 负责“说什么”HDLC 负责“怎么把话说完整”。做电表协议栈、做采集主站联调、或者要给非标设备加抄表能力的人都会撞上它。一个反直觉的建议拿到资料先别急着编译源码先抓一轮真实报文。源码是别人对协议的理解而 DLMS/COSEM 加 HDLC 这套东西的坑几乎全藏在“字节怎么拼、帧怎么拆、AARQ 为什么被拒”这些只有报文能看见的细节里。把文档和代码当成字典把人家的抓包记录当成地图你才不会被一整个资料包带偏。2. 协议是谁在说话DLMS/COSEM 分层和 HDLC 的定位2.1 先把“DLMS/COSEM”拆开对象模型、XDLMS 和 APDUDLMS/COSEM 不是单个协议而是一整套面向对象的计量通信架构。它先定义了一个对象模型电表里的正向有功电能、电压曲线、开关状态、报警记录全部被抽象成“COSEM 对象”。每个对象有一个 OBIS 码做地址比如正向有功电能通常是1.0.0.0有功功率是1.0.1.0当前时间是0.0.1.0。对象内部有属性、有方法客户端的每一次操作本质上都是“读某个对象的某个属性”或“调用某个对象的方法”。这套模型的一个关键特点是“面向对象”落到 APDU应用协议数据单元上。客户端说要读数据不是发一句人类可读的“把电量给我”而是构造一个 Get-Request 请求 APDU里面写明 OBIS 码、属性号、期望的数据类型。服务端收到后回一个 Get-Response APDU同样是一串按 ASN.1 风格编码的字节。抓包时看到的C0、C4开头的一段数据就是这类 APDU 的标签。所以 DLMS/COSEM 资料包里的“文档”最核心的不是营销册子和演示 PPT而是描述这些 APDU 格式的标准文本。要读懂它你得先接受一个心智模型主站和表计之间只有两种东西在流动一类是管理连接的握手消息SNRM、AARQ一类是读写对象的数据消息Get、Set、Wait。所有业务功能最后都落到后者。2.2 HDLC 在 DLMS 里的角色一条可靠的“字节管道”DLMS/COSEM 可以跑在多种承载上串口、RS-485、电力线载波、TCP/IPIEC 62056-47。当它跑在串口这类面向字节的链路上时默认用的就是 HDLC 数据链路层。这个 HDLC 不是让两台路由器互通的通用 HDLC而是 DLMS 规定的一套精简子集用0x7E做帧起始和结束标志帧里面有地址、控制字段、Header Checksum、信息字段和帧校验 FCS。HDLC 的存在是为了解决串口通信里三个最麻烦的问题怎么知道一段字节流的边界怎么区分“这是链路管理帧还是数据帧”怎么保证数据传错之后能发现帧边界靠0x7E链路管理靠 U 帧SNRM、UA、DISC数据承载靠 I 帧信息帧和 S 帧接收就绪/未就绪。抓包时如果只看到一堆0x7E 7E 7E在刷屏通常就是物理链路已经通了但 HDLC 状态机还没建立起来。在整套资料里的“HDLC 协议资料”这一块你重点只需要抓住四种帧SNRM发起链路连接、UA应答连接、I 帧承载应用层数据、DISC断开连接。其余的 RR、RNR 这类监督帧在串口抄表场景里用得少可以在排障时再翻。2.3 AARQ 到底是什么内容SNRM 之后那包“自我介绍”AARQApplication Association Request应用关联请求是这套协议里最容易被问“到底是什么内容”的一帧。它出现在 SNRM/UA 握手完成之后由客户端发给服务端表计作用是申请建立一个应用层的“会话”。SNRM 建立的是链路层连接只解决“咱们能按 HDLC 规则对话了”AARQ 要解决的是“我是谁、我想用什么协议规约、我需要什么认证、咱们能协商多大报文”。我把一次完整的连接建立拆成四个帧看 AARQ 的落点顺序帧方向干什么1SNRMHDLC U 帧客户端 → 服务端链路层握手请求进入正常响应模式2UAHDLC U 帧服务端 → 客户端答应链路层握手3AARQI 帧承载的 APDU客户端 → 服务端应用层身份声明和参数协商4AAREI 帧承载的 APDU服务端 → 客户端接受或拒绝这次应用连接AARQ 的 APDU 标签是0x60AARE 的标签是0x61。AARQ 内部是若干 TLV 结构的字段0x80字段是 Application Context Name协商用哪一套协议子集例如 “1.1.2.2.47” 这类 OID具体取值要以设备固件支持的清单为准0xA1之类是可选的调用/被调用应用实体名0xBE是用户信息里面常携带最大 APDU 长度、协商版本这类参数。提示抓包时如果只盯着 HDLC 层会觉得 AARQ “藏”在一个普普通通的 I 帧里看不到。正确做法是把 I 帧的信息字段提出来再看第一个字节0x60开头才是 AARQ。2.4 为什么先连链路、再连应用两层握手的分工很多新手会问既然 SNRM 都握手成功了为什么还要再发一次 AARQ因为两者管的事完全不同。SNRM/UA 是 HDLC 层在确认“物理字节流可靠、帧校验可用、序号机制就绪”它不知道对方是电能表还是水表也不知道你要用哪种上下文。AARQ/AARE 才是 DLMS/COSEM 应用层在确认“咱们用的是同一套对象模型、同一套认证规则、同一套 APDU 版本”。这个分工直接决定了排查问题的方向如果卡在 SNRM 之后没有 UA问题在串口参数、地址配置、HDLC 状态机如果 UA 有了但 AARQ 被拒问题在应用上下文名、认证参数、最大 APDU 长度如果 AARQ 过了但 Get 数据报错问题在 OBIS 对象模型。把这四层边界划清楚一个资料包里的文档和源码才能各就各位。3. 从文档到源码把“资料包”整理成最小可跑工程3.1 文档资料先挑这三类剩下的先别管收到资料包先做减法。按我的习惯文档部分只保留三类其他先归档类别认准什么要从中提取什么数据链路层IEC 62056-46 或对应章节HDLC 帧格式、FCS 算法、SNRM/UA 状态机应用层IEC 62056-53 或 DLMS UA 的 Blue/Green BookAPDU 标签表、AARQ/AARE 字段定义、关联状态机对象模型IEC 62056-61OBIS、62056-62接口类OBIS 码表、属性号、单位、数据类型映射很多厂商资料会把“通信协议规范”和“产品用户手册”混在一本 PDF 里。用户手册里那些“波特率默认 9600、表地址默认 1”是配置项不是协议原理单独记到笔记本里就行不必对着它啃协议。而你真正要反复翻的是那几张 APDU 标签表和 OBIS 码表建议直接打印或拆成活页。3.2 源码选型开源库、厂商 SDK、自己写的边界“软件源码”这部分最考验判断力。常见方案有三类成熟开源库Gurux.DLMS 系列、dlms.js 这类适合快速做协议栈验证和主站侧工具。优点是不用从零啃 APDU缺点是底层 HDLC 被封装得很深出了问题不好查。尤其你如果要做嵌入式移植开源库的 C#/Python 实现不能直接搬只能当“行为参照”。厂商 SDK电表和集中器厂商通常会提供配套源码或二进制库。优点是默认参数和自家固件匹配缺点是授权方式、编译链绑定、以及“库里面到底做了什么”是个黑匣子。联调出怪问题时你会很想要一颗后悔药。自己从标准文档写最小栈不推荐做完整版但强烈推荐写一个“能拆帧、能解析 AARQ 首字段”的教学脚本。它在定位问题时比任何库都直观。选择标准只有一个你的交付物是产品固件还是测试工具。做产品优先考虑厂商 SDK 加开源库对照做测试工具直接站在开源库肩膀上把精力留给业务逻辑。3.3 最小工程目录客户端与模拟表计分开放拿到一堆源码第一件事不是编译而是把它整理成一个“能自我验证”的目录。我会这样放dlms-lab/ ├── conf/ │ └── client.ini # 串口设备名、波特率、客户端/服务端地址 ├── docs/ │ ├── 46-hdlc.md # 从 PDF 里提炼的 HDLC 帧格式笔记 │ ├── 53-apdu.md # APDU 标签与 AARQ 字段笔记 │ └── 62-obis.md # 常用 OBIS 码与属性对照 ├── src/ │ └── dlms/ │ ├── hdlc.py # HDLC 拆帧与组帧教学级 │ ├── apdu.py # APDU 标签解析 │ └── obis.py # OBIS 码字符串与字节互转 ├── sim/ │ └── meter_sim.py # 模拟表计响应 SNRM/AARQ/Get └── tools/ └── hexdump.py # 打印字节流的辅助工具这样分目录的逻辑是强迫“协议栈代码”和“设备业务逻辑”分开。hdlc.py只管帧边界和校验apdu.py只管应用层解析meter_sim.py假装自己是电表。后面联调真实表计出问题时你可以先在模拟器上复现同样的问题把“协议问题”和“表计固件问题”切开。3.4 让源码先跑在虚拟串口上socat 建一条调试链路没有硬件时用 Linux 的 socat 建一对虚拟串口让客户端和模拟表计各占一头是最快的启动方式sudo socat -d -d pty,raw,echo0,link/tmp/ttyVA \ pty,raw,echo0,link/tmp/ttyVB运行后/tmp/ttyVA和/tmp/ttyVB是一对互相连通的口子向 A 写入的内容从 B 读出反之亦然。客户端连 A模拟表计连 B就是一条不需要真实串口线的链路。raw表示透传字节echo0关掉回显避免程序读到自己刚发的数据。这步做完就可以进入第 4 章的帧级分析了。整条调试链路从“物理串口”变成了“文件描述符”写代码时不用考虑驱动差异排查范围一下子小很多。4. HDLC 帧级实战拆帧脚本和数据从哪一层“冒”出来4.1 用一段 Python 把 0x7E 帧从字节流里拆出来HDLC 的帧边界是0x7E。串口上可能一次收到半帧、两帧粘在一起、甚至中间混入噪声字节所以第一步永远是“从字节流里把完整帧分离出来”。我习惯保留一个极简的拆帧脚本不依赖任何库HDLC_FLAG 0x7E def extract_hdlc_frames(buf: bytes): 从字节流中提取由 0x7E 包裹的完整 HDLC 帧。 返回帧列表每帧不含起始/结束标志。 frames [] opened False start -1 for i, b in enumerate(buf): if b HDLC_FLAG and not opened: start i 1 # 起始标志之后才是帧内容 opened True elif b HDLC_FLAG and opened: frames.append(buf[start:i]) # 遇到结束标志收帧 opened False return frames raw bytes.fromhex( 7E 93 03 12 34 7E # 这是一段模拟的 SNRM 帧 ) for fr in extract_hdlc_frames(raw): print(fr.hex( ))这段逻辑是状态机式扫描遇到第一个0x7E进入“帧内容”状态记录起始位置遇到下一个0x7E就把中间的内容截出来。两个连续标志7E 7E表示“上一帧结束紧接着下一帧开始”在这个脚本里会被正确处理第一个7E收帧第二个7E重新开帧。常见的坑是忘记 HDLC 支持“转义字节”。DLMS HDLC 里如果信息字段里出现0x7E发送前会被拆成0x7D加异或0x20的形式。教学脚本没做去转义所以抓真实串口流量时要先对帧内容做一次反向处理否则拆出来的帧长度会偏大FCS 校验必败。4.2 从 I 帧信息字段里还原 AARQ先认 APDU 标签拆完帧之后下一个动作是判断“这个 HDLC 帧里面装的是不是应用层数据”。链路管理帧SNRM、UA、DISC没有信息字段而承载 AARQ、Get-Request 的 I 帧有。所以解析顺序是先拆 HDLC 帧再看有没有信息字段最后对信息字段做 APDU 解析。下面这段代码演示如何把信息字段当作 APDU 来读标签和长度def parse_apdu(info_field: bytes): 解析 DLMS APDU 的 Tag/Length/Body。 只处理短格式长度和常见的长格式长度完整规则见 IEC 62056-53。 if not info_field: return None tag info_field[0] ln info_field[1] if ln 0x80: # 长格式低 7 位表示后面还有几个字节是真实长度 n ln 0x7F body_len int.from_bytes(info_field[2:2 n], big) body info_field[2 n:2 n body_len] else: body_len ln body info_field[2:2 body_len] tag_names { 0x60: AARQ, 0x61: AARE, 0xC0: Get-Request, 0xC4: Get-Response, } return { tag: tag_names.get(tag, hex(tag)), length: body_len, body: body.hex( ), } # 假设从抓包里挑出的 I 帧信息字段是这一串 info bytes.fromhex(60 1E 80 ...) print(parse_apdu(info))参数说明ln 0x80是判断 ASN.1 长度编码的短/长格式。小于 128 的长度用一个字节直接写大于等于 128 时第一个字节最高位置 1低 7 位表示后续长度字节数。AARQ 本身通常不长但客户端协商大报文后Get-Response 的长度很容易超过 127这时就要老老实实走长格式分支。实际项目里我不会为每个 APDU 手写这种解析而是用开源库已经调试好的部分来做。但保留这个脚本的意义在于当库解析报错时你能手工确认“第一个字节确实是0x60”从而判断是库的问题还是报文本身不合法。4.3 三个必调参数MaxInfoTX、窗口大小和分帧长度DLMS HDLC 联调时最常改的不是业务逻辑而是下面三个参数参数作用经验值MaxInfoTX发送方单帧能携带的最大信息字段长度常见 128、256、1024MaxInfoRX接收方愿意接收的最大信息字段长度通常和 MaxInfoTX 相等Window Size未确认的 I 帧最大个数串口场景常见 1TCP 场景可到 4 或更高如果一次要发送超过 MaxInfoTX 的 APDU比如读历史冻结数据发送方要把 APDU 拆成多帧给每帧编序号接收方必须按序号重组。窗口大小决定了在等确认之前最多能发几帧窗口为 1就是发一帧等一帧慢但稳窗口大了传输效率高但半双工 RS-485 上可能因为收发电平切换不及时而翻车。注意AARQ/AARE 这类控制帧不受窗口太小的影响但长 Get-Response 拆帧后如果序号对不上现象往往是“最后一帧丢了”或“数据拼起来是花的”。先在模拟器上把这三个参数固定成最保守的值128、128、1再往下调能省很多排障时间。5. DLMSCOSEM 联调避坑四个从“假通”到“真通”的现场记录5.1 UA 迟迟不来链路层根本没建起来现象串口数据能发出去逻辑分析仪上能看到波形但客户端发出 SNRM 之后服务端一直不回 UA直到超时。原因多半不是 HDLC 协议本身的问题而是底下的物理层或地址配置没对上。我遇到过三次一次是波特率标称 9600 实际固件配置 19200一次是客户端地址配成了 0x10服务端期望的地址是 0x10 但校验掩码不对还有一次是 RS-485 半双工的收发电平切换延时不够服务端收到 SNRM 时卡在“正在发送”状态根本来不及回。解决先用串口调试助手手工发一段7E 93 01 02 03 7E这类测试帧看服务端回不回 UA回说明物理层和 HDLC 状态机是通的问题在客户端组帧不回用示波器或 USB 转 TTL 模块确认电平再把波特率、地址校验规则逐个查一遍。5.2 AARQ 被拒AARE 里的 Result 不是 success现象SNRM/UA 这步秒过但紧接着客户端发 AARQ 后服务端回的 AARE 里 Result 字段是 reject应用连接建立失败。原因最常见的是 Application Context Name 不匹配。客户端写死的 OID 是某一版协议子集而表计固件只支持另一版其次是服务端不允许“无认证”连接AARQ 里又没带认证机制再有一个隐蔽原因是 AARQ 里的最大 APDU 长度大于服务端能力服务端认为无法协商就直接拒了。解决翻开表计的产品手册或“对象目录”确认支持的 Application Context Name把认证方式设成服务端默认值临时把最大 APDU 长度从 2048 改到 128 重试。注意 AARE 的 Result Reason 字段会区分“permanent rejected”和“transient rejected”前者是参数不对后者是服务端正在忙或资源不足处理方式完全不同。5.3 报文一长就 CRC 错分帧重组和序列号没有复位现象读1.0.0.0这种短对象一切正常一旦读几十 KB 的历史曲线服务端回的第一段数据能收到后面几段要么校验错、要么拼出来是乱的。原因APDU 超过单帧承载能力后发送方需要把它按 MaxInfoTX 拆成多个 I 帧每帧带递增的序号。接收方只把序号连续的帧拼到一起。常见的坑有三个一是拆帧时没有更新 HDLC 帧格式字里的分段标志二是异常断线后客户端状态机没有复位重连后用旧序号继续发服务端按新序号接收帧全部对不上三是半双工模式下收发切换延时不够第二帧发得快了服务端还没切到接收。解决重连或异常复位后强制先发 DISC 让双方回到断开状态再重新 SNRM把序列号归零。抓包时重点看每个 I 帧的控制字段低 4 位是否从 0 递增到窗口上限后回绕如果发现没有回绕而是继续往上走就是序列号处理写错了。5.4 Get-Request 发出去了回的是 0xDA 而不是 0xC4现象链路层、应用层连接全都正常读某个 OBIS 码时服务端回了一段0xDA开头的 APDU而不是预期的0xC4数据响应。原因0xDA是 Data Access Error说明对象访问被服务端拒绝了。我碰到过的原因按概率排序OBIS 码写错读1.0.0.0写成了1.0.0.1属性号写错电能值的 attribute 是 2写成了 1对象存在但当前状态不允许读取比如某些电量数据需要先建立“读取会话”还有数据访问权限不够AARQ 里没能通过认证。解决不要拿协议栈文档去查直接读表计的对象列表。用客户端去访问 OBIS0.0.40.0.0.255Association LN 对象把服务端自报的对象清单拉出来逐个核对对象类型和可用属性。这个方法能快速区分“协议没写好”和“数据本来就不该读”。5.5 ASN.1 长度字段的暗坑短格式和长格式现象读小数据一切正常读一个大字符串时客户端报“APDU 长度解析失败”或者收到的数据整体往后错了一位。原因APDU 的长度字段不是“低 7 位就是长度”。当长度大于 127 时必须使用长格式编码。有些源码为了省事只实现了短格式或者反过来把长格式的次长字节当成了内容数据结果所有大报文都解析错位。解决在parse_apdu这类解析函数里先处理0x80位再把后续长度字节读出来。验证方法很简单构造一个超过 127 字节的 Get-Response看解析出的长度字段是不是按“1 字节标记 2 字节长度”正确展开的。这个坑在纯 C 的实现里尤其隐蔽因为很多人会用一个uint8_t直接存长度。6. 把一个读取 1.0.0.0 的最小客户端跑通再做加法6.1 五次状态迁移从 SNRM 到 Get-Response所有 DLMS/COSEM 联调本质上都是把下面这个状态机走完一遍def run_minimal_session(transport): # 1. 链路层握手 transport.write(b\x7e build_snrm() b\x7e) ua transport.read_frame() assert ua.control 0x73 # UA 控制字段 # 2. 应用层握手 transport.write(b\x7e build_aarq() b\x7e) aare transport.read_frame() assert aare.payload[0] 0x61 # AARE tag # 3. 读正向有功电能 OBIS 1.0.0.0属性 2 transport.write(b\x7e build_get_request([1, 0, 0, 0, 0, 255], 2) b\x7e) resp transport.read_frame() assert resp.payload[0] 0xC4 # Get-Response tag return parse_get_response(resp.payload)用代码里的assert卡住每一个里程碑比打印一堆日志更有效UA 没过说明链路层有问题AARE 没过说明应用上下文不对最后0xC4出来才说明整条链路真的走通了。任何一步失败直接回到第 5 章对应的避坑记录里找原因。6.2 验证技巧把控制字节和时间戳一起打出来我最后的习惯是在收发函数的边界统一加一个带时间戳的十六进制打印输出包含三个信息——方向、控制字段、APDU 首字节。aare 的 tag、控制字段值、时间间隔这三个东西能筛掉 80% 的“假通”问题。比如 UA 后 AARQ 迟迟不发看时间戳就能发现是收到了 UA 但状态机没切到应用层不是网络超时。这套方案从零搭到今天我用的全都是“最小可跑工程加分步验证”的思路。最初踩过最深的一个坑是把厂商 SDK 当作黑匣子直接上线结果 AARQ 一被拒就是三天。后来老老实实拆帧、印 tag、卡 assert反而一个下午就把问题定位到上下文名不匹配。希望这套从文档到源码、再到底层字节的走法能帮到你。本文还有配套的精品资源点击获取
返回列表