简介:面向电表通信与自动抄表领域的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 和 APDU
DLMS/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/IP(IEC 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 之后那包“自我介绍”
AARQ(Application Association Request,应用关联请求)是这套协议里最容易被问“到底是什么内容”的一帧。它出现在 SNRM/UA 握手完成之后,由客户端发给服务端(表计),作用是申请建立一个应用层的“会话”。SNRM 建立的是链路层连接,只解决“咱们能按 HDLC 规则对话了”;AARQ 要解决的是“我是谁、我想用什么协议规约、我需要什么认证、咱们能协商多大报文”。
我把一次完整的连接建立拆成四个帧,看 AARQ 的落点:
| 顺序 | 帧 | 方向 | 干什么 |
|---|---|---|---|
| 1 | SNRM(HDLC U 帧) | 客户端 → 服务端 | 链路层握手,请求进入正常响应模式 |
| 2 | UA(HDLC U 帧) | 服务端 → 客户端 | 答应链路层握手 |
| 3 | AARQ(I 帧承载的 APDU) | 客户端 → 服务端 | 应用层身份声明和参数协商 |
| 4 | AARE(I 帧承载的 APDU) | 服务端 → 客户端 | 接受或拒绝这次应用连接 |
AARQ 的 APDU 标签是0x60,AARE 的标签是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 Book | APDU 标签表、AARQ/AARE 字段定义、关联状态机 |
| 对象模型 | IEC 62056-61(OBIS)、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,echo=0,link=/tmp/ttyVA \ pty,raw,echo=0,link=/tmp/ttyVB运行后,/tmp/ttyVA和/tmp/ttyVB是一对互相连通的口子:向 A 写入的内容从 B 读出,反之亦然。客户端连 A,模拟表计连 B,就是一条不需要真实串口线的链路。raw表示透传字节,echo=0关掉回显,避免程序读到自己刚发的数据。
这步做完,就可以进入第 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、1024 |
| MaxInfoRX | 接收方愿意接收的最大信息字段长度 | 通常和 MaxInfoTX 相等 |
| Window Size | 未确认的 I 帧最大个数 | 串口场景常见 1,TCP 场景可到 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.255(Association 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,反而一个下午就把问题定位到上下文名不匹配。希望这套从文档到源码、再到底层字节的走法能帮到你。
本文还有配套的精品资源,点击获取