news 2026/9/28 1:33:22

国网698.45报文解析实战:从字节流到电能数据的完整拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
国网698.45报文解析实战:从字节流到电能数据的完整拆解

说实话,刚接触国网698.45报文那会儿,我盯着串口调试助手里的十六进制串,整整懵了一下午。一堆68开头、16结尾的字节,看着像帧,又不知道哪个字节是地址、哪个字节是数据。后来摸清楚了帧结构、数据标识和CS校验这套东西,才发现它其实是个设计得相当规整的协议。这篇就来聊聊怎么用开源工具把电表数据帧一层层扒开,从起始符、地址域、APDU到实际电能读数,一步步讲清楚为什么协议这样设计,以及遇到解析异常时怎么排查。

这个内容适合四种人:一是刚接手用电信息采集系统的电力行业工程师,二是做能源物联网平台、需要对接多种表计协议的开发,三是有大量老旧设备需要做协议适配的集成商,四是对电力通信协议感兴趣的嵌入式开发者。如果你之前只玩过DL/T 645或者Modbus,想往更复杂的面向对象协议走,这篇也可以当作一个跳板。

1. 先搞清楚698.45报文在整个链路里站在什么位置

很多人在解析报文之前,犯的第一个错误就是直接打开调试工具对着十六进制一通猜。其实698.45并不是一个简单的物理层协议,它身上叠了好几层东西,先搞清楚它站在哪里、和谁说话、说给谁听,后面解析才不会跑偏。

1.1 从采集终端到后台主站,报文是怎么流转的

智能电表的数据链路,通常分成三段:一是电表与集中器之间的本地通信,常见的是RS-485总线,也有载波、微功率无线等方式;二是集中器与采集终端之间的远程通信,GPRS、4G、光纤都有;三是采集终端与省级主站系统之间的数据交换。

DL/T 698.45全称是《电能信息采集与管理系统 第4-5部分:通信协议 — 面向对象的数据交换协议》,它主要用在集中器与主站之间,也覆盖终端与电表之间的对象化数据交互。你可以把主站、集中器、电表想象成公司里的三级架构:主站是总部,集中器是省经理,电表是一线员工。698.45报文就是总部和省经理之间的官方公文格式,格式定得越清晰,两层之间沟通就越顺畅。

与它经常被放在一起提的DL/T 645,是电表和采集设备之间用的老牌协议。645更像个“命令表”,你发一个特定命令码,表返回一段特定数据,简单直接。698.45则换成“面向对象”的思路,把所有数据都建模成对象和属性,你用一套统一的规则去访问任何一类数据,扩展性比645高出一大截。

1.2 698.45和645、DLMS/COSEM有什么关系

如果你接触过国际上的DLMS/COSEM标准,再看698.45会觉得很亲切。DLMS/COSEM是国际电工委员会IEC 62056系列里定义的面向对象电能计量协议,698.45在思路上一脉相承:都用接口类、对象标识、属性描述来组织数据,都通过“请求-响应”模式访问数据,都支持分帧传输和加密认证。

不同的是,698.45针对国内电网的实际业务做了裁剪和本地化,比如地址域里加入了行政区划码、终端逻辑地址,数据标识体系也重新做了编排。所以不能把DLMS/COSEM的工具直接拿来解析698.45报文,但理解DLMS/COSEM的人学698.45会快很多。

还有一点很容易踩坑:698.45里面,链路层和应用层的边界比你想象的要靠后。一个完整的报文,不光是链路层的起始符、长度、地址域,还包括应用层的APDU(应用层协议数据单元)、数据标识DIT(数据标识符)、以及具体的对象属性数据。很多教程只讲到链路层就停了,实际业务数据都在APDU里,这一步不打通,你解析出来的只是一堆没有业务含义的结构体。

1.3 为什么手边需要一套能动手的开源工具链

既然是做协议解析,总得有个“解剖台”。商业协议分析软件又贵又重,而且很多时候厂家给的报文是抓包抓回来的、带着时序关系的原始字节流,商业软件不一定能直接识别。

开源工具的价值在于三件事:一是透明,每一条解析规则都写在明面上,你能知道每个字节是怎么被解读的;二是可改,遇到厂家私有扩展字段,直接改脚本就行,不用等软件厂商更新;三是可复现,把抓包文件和解析脚本放一起,换了环境也能还原现场,后面排查问题、交接给同事都方便很多。

我实际用到的主要是两套:Wireshark加自定义Lua解析插件来做离线抓包分析,Python脚本搭配pyserial来做实时串口采集和业务字段提取。下面两节分别讲它们怎么搭、怎么用、各自擅长什么。

2. 开源工具选型:Wireshark插件还是Python脚本

选工具这件事,很多新人恨不得找一个“万能软件”把什么协议都解析了,结果往往是把时间花在了研究工具而不是研究协议上。我的建议是:抓包分析用Wireshark,业务提取用Python,两者配合,各管一段。

2.1 工具能力对比与适用场景

先说Wireshark。它本来是网络抓包工具,但它的Lua插件机制非常强大,专门有人为各种工控协议写dissector。698.45没有一个官方的Wireshark解析器,需要自己写一个或者找社区分享的Lua脚本。好处是界面直观,帧与帧之间的时序、长度、地址域都能一目了然,非常适合“搞清楚一帧报文长什么样”这个阶段。

再说Python脚本。串口采集、文件批处理、对接数据库、生成报表,这些场景Wireshark就不太行。Python的好处是流程可以完全自动化,从串口读字节流,到校验CS、解析APDU、提取电能数据、写入CSV,一条命令跑完。特别是你后面要批量解析几百个终端的上报数据,脚本方式是唯一现实的选择。

工具选择的判断标准,我总结成三条:

  • 你是“看一帧”还是“跑一批”。看一帧用Wireshark,跑一批用Python。
  • 你是“人工定位问题”还是“程序化输出结果”。人工定位用Wireshark,程序化输出用Python。
  • 你是“解析给自己看”还是“解析给别人用”。自己看用哪个都行,给别人用必须上可复用的脚本。

2.2 Wireshark加Lua插件的搭建思路

Lua插件本质就是告诉Wireshark:遇到某个协议特征时,从第几个字节开始、每段多少位、按什么方式展示。下面是简化版的698.45 dissector骨架,只是示例,帮你理解插件结构,实际使用你还需要按协议文档把字段一节一节补全。

local p_698 = Proto("dlt69845", "DL/T 698.45") local f_start = ProtoField.uint8("dlt69845.start", "起始符", base.HEX) local f_len = ProtoField.uint16("dlt69845.len", "长度", base.DEC) local f_control = ProtoField.uint8("dlt69845.control", "控制域", base.HEX) local f_addr = ProtoField.bytes("dlt69845.addr", "地址域") local f_user = ProtoField.bytes("dlt69845.user", "用户数据") local f_cs = ProtoField.uint8("dlt69845.cs", "帧尾CS", base.HEX) local f_end = ProtoField.uint8("dlt69845.end", "结束符", base.HEX) p_698.fields = { f_start, f_len, f_control, f_addr, f_user, f_cs, f_end } function p_698.dissector(buf, pkt, tree) if buf:len() < 8 then return false end if buf(0,1):uint() ~= 0x68 then return false end local t = tree:add(p_698, buf(0)) t:add(f_start, buf(0,1)) t:add(f_len, buf(1,2)) t:add(f_control, buf(3,1)) t:add(f_addr, buf(4,6)) t:add(f_user, buf(10, buf:len() - 12)) t:add(f_cs, buf(buf:len() - 2, 1)) t:add(f_end, buf(buf:len() - 1, 1)) end local wtap_encap_table = DissectorTable.get("wtap_encap") wtap_encap_table:add(wtap_encap.USER0, p_698)

写完后放到Wireshark的plugins目录里,重启Wireshark,再打开包含698.45报文的pcap文件,就能看到协议的字段树。这段代码比较粗糙,地址域长度、长度字段的单双字节判断都没做,但作为入门模板够用了。

2.3 Python脚本的灵活性到底强在哪

Wireshark适合“看”,但真正的“解析服务”我几乎全用Python。理由有三个:一是串口场景Wireshark不好介入,二是业务字段提取逻辑通常要跟具体数据标识DIT绑定,脚本改起来快,三是可以直接输出结构化数据给上层平台用。

下面是我常用的Python依赖清单:pyserial负责串口通信,crcmod提供通用校验计算,如果要导出Excel可以用openpyxl。这几个都是成熟的开源库,pip直接装。脚本的核心思路就是“按字节流顺序,一个字段一个字段地消费”,后面第4节会给完整代码。

一个有参考价值的经验是:不要一上来就写完整的解析类,先写一个半成品脚本,对着真实报文逐步加逻辑。因为698.45里帧有I帧、S帧、U帧之分,还有单字节长度和双字节长度的差异,一个字段判断写错,后面全乱。与其一次写完再慢慢debug,不如边抓帧边写,用真实数据驱动。

3. 手把手拆解698.45数据帧:从起始符到CS校验

现在进入核心内容。我把一帧698.45报文拆成链路层、应用层、数据体三层来讲,每一层你都要能独立看懂,后面写代码就是顺水推舟的事。

3.1 帧结构总览:三个层级的嵌套关系

一帧完整的698.45报文,从直观的字节排列来看,大概是这样的:

字段字节数说明
起始符1固定为0x68
长度L1或2表示长度字段之后的数据字节数,有单双字节之分
控制域C1帧类型、传输方向、启动标志等
地址域A可变行政区划码、终端地址、主站地址和组地址标志
帧头CS1地址域之前的校验字节
用户数据可变承载应用层APDU
帧尾CS1控制域、地址域、用户数据区的异或校验
结束符1固定为0x16

这里要特别强调长度字段。如果长度值小于256,用一个字节表示;如果大于等于256,则用两个字节。怎么区分?看长度字段的第一个字节的最高位,或者根据他的具体取值来判断,这一点各厂商实现略有不同,所以我解析时从来不会写死“长度就在偏移2的位置”,而是先做一次长度解析,确认单双字节,再继续往下走。

CS校验是很多新人经常忽略的坑。帧头CS和帧尾CS的作用范围不一样,帧头CS只管控制域和地址域这一段,帧尾CS管的是控制域、地址域、用户数据这一段。我见过不少解析失败的案例,最后查下来不是协议理解错,而是CS校验范围搞错了。

3.2 地址域和控制域的字段逐个看

控制域C虽然只有一个字节,但它承载的信息量很大。高位到低位依次包含:传输方向位、启动标志位、帧计数位、帧计数有效位、分帧标志位,以及低两位的帧类型标志。帧类型标志用于区分I帧(信息帧)、S帧(监视帧)、U帧(无编号帧)。I帧是正儿八经带用户数据的数据帧,S帧负责确认,U帧负责链路建立和释放。

地址域不像控制域那样固定一个字节,它是变长的。典型的地址域组成包括:

  • 行政区划码,通常用BCD码编码,表示电表/终端所在的区域;
  • 厂商代码或者运营商代码,标识设备厂家;
  • 终端逻辑地址,是定位具体终端的关键;
  • 主站地址和组地址标志,bit7如果是1表示这是组播帧,低7位是组地址,用于一次给多个终端下发指令。

地址域后面还有个容易被忽略的“时钟”扩展,有些厂家会追加时间字段来支持对时功能。解析地址域时,不要凭感觉数字节,要从长度字段反推。先根据长度计算出整个帧的边界,再倒推地址域占了多少,这样最稳。

3.3 APDU与数据标识DIT:面向对象的关键

链路层剥完,用户数据里装的就是APDU,这部分才是业务的核心。APDU内部包含应用层控制域、应用层地址、数据单元标识,以及实际的对象属性数据。

DL/T 698.45采用面向对象建模,数据通过“接口类 + 对象标识 + 属性描述”来组织。简单理解:接口类是“模板”,定义了某类数据有哪些属性和方法;对象标识是“具体实例”,定位到某一块表或者某一个测量点;属性描述则指明你要访问的是这个对象的哪个具体属性,比如当前值、单位、时标。

数据标识DIT也有自己的一套编址方式。常见的数据标识首字节类别包括:电能量、最大需量、变量、事件、参变量、任务、冻结、运行信息。方向、费率、时标信息则通过后面的字节更细致地展开。这意味着拿到一个数据标识后,不能只按“前两位是类别,后两位是序号”这样死记,要去协议文档里查分类表,结合上下文判断含义。

3.4 一个完整读取电能数据的解析实例

为了让你更直观地理解,我演示一个读取电表当前正向有功电能的过程。假设请求帧的十六进制大致是:

68 0F 0F 68 44 00 01 00 00 00 00 00 00 00 00 00 01 02 00 00 01 00 00 00 78 16

这个例子是简化示意,不同厂家、不同终端的字段细节会有差异。但结构可以对照来看:

  • 第一个0x68是起始符;
  • 0F 0F是长度部分,这里按简化帧处理,表示后续数据的长度;
  • 第二个0x68是扩展帧标志或帧类型标识;
  • 0x44是控制域,传输方向位表示主站到终端,低两位是I帧;
  • 后续一大段是地址域和应用层APDU;
  • 倒数第二个字节0x78是帧尾CS,最后一个0x16是结束符。

请求发出后,电表返回的数据帧里,除了帧头、地址域、控制域外,用户数据区会包含请求对应的数据标识和数值。例如返回数据区里出现:

00 01 00 00 03 12 34 56

这里的前四字节是DIT,标识“正向有功电能”,最后几个字节就是数据,按BCD码或整数类型解析出电能值。具体是BCD还是整数,取决于返回数据里的数据类型标志,这也是解析时必须看协议文档确认的地方。

4. 实操过程:用Python脚本从串口抓取电表数据并解析

读完理论,总得动手跑一遍。这一节我用一个真实可用的Python采集脚本,把从串口读取一帧数据、校验CS、定位APDU、提取DIT数据标识和业务数值的完整链路走一遍。你拿到手可以直接改参数用。

4.1 搭建串口采集环境

硬件方面,最常用的是USB转RS-485模块,接到电表的485口或者集中器的调试口。注意电表侧的485接线有A、B之分,接反了大概率收不到数据,而且带电插拔容易烧模块,先断电接好线再上电。

串口参数我实测下来,绝大多数设备的默认配置是波特率2400、8个数据位、偶校验、1个停止位,也就是常见的2400 8E1。但也有设备用4800甚至9600,还有无校验的情况。如果读取结果全是乱码或者根本收不到帧,第一件事不是改解析逻辑,而是确认串口参数。

采集时,可以先把主站发送的请求帧通过串口调试工具发出去,再用串口监听工具抓返回帧,把字节流存成十六进制文本。这一步相当于给协议解析留了“现场证据”。我建议每个调试现场都保存一份原始报文日志,后面不管是排查还是复现,都不用重新去现场等一个偶发问题。

4.2 采集与解析脚本的实现说明

下面是简化版的Python脚本,功能包括:读取一帧数据、按协议格式拆解字段、计算CS校验、打印解析结果。代码用到的库只有pyserial,没有额外依赖。

import serial def calc_cs(data: bytes) -> int: cs = 0 for b in data: cs ^= b return cs def fetch_frame(ser): # 查找起始符 0x68,并处理超时 tmp = ser.read(1) while tmp != b'\x68': tmp = ser.read(1) # 简化:读取长度区后再读完整帧 header = ser.read(2) # 这里只处理单字节长度的简化帧 length = header[0] body = ser.read(length) return b'\x68' + header + body def parse_frame(frame: bytes): result = {} result['start'] = frame[0] offset = 1 # 判断长度字段长度 if frame[offset] & 0x80: result['len_field_len'] = 2 result['length'] = ((frame[offset] & 0x7F) << 8) | frame[offset + 1] offset += 2 else: result['len_field_len'] = 1 result['length'] = frame[offset] offset += 1 result['control'] = frame[offset] offset += 1 # 地址域实际长度需要从长度值和后面的字段反推,这里简化处理 # 用户数据从 offset 打到帧尾CS前 result['user_data'] = frame[offset:-2] cs = frame[-2] result['calculated_cs'] = calc_cs(frame[1:-2]) result['frame_cs'] = cs result['end'] = frame[-1] result['cs_ok'] = (cs == result['calculated_cs']) return result if __name__ == '__main__': ser = serial.Serial('COM5', 2400, timeout=3, parity='E') frame = fetch_frame(ser) info = parse_frame(frame) print(info)

这段脚本的定位是“骨架”而不是“完整产品”,因为地址域的精确长度、长度字段的单双字节判断、APDU内部字段拆解,都需要结合你手里设备的具体报文来做细节调整。但结构是对的,拿到真实帧后按报文字节往这个框架里填就行。

4.3 解析结果与参数核对方法

跑完上面的脚本,输出里最该先看的是cs_ok字段。如果CS校验通过,说明帧边界和字段长度切分基本是对的,下一步才有意义;如果CS校验不通过,先别急着解析数据,回头检查长度字段取值、地址域长度,以及帧尾CS计算范围。

CS通过后,再看user_data区里的DIT。打印出字节流,对照3.3节的数据标识分类,确定这段数据是电能量、变量还是冻结数据。电能类数据常见的是BCD编码,比如返回12 34 56,结合精度标志可能表示1234.56 kWh。变量类数据则可能是整数或者浮点数,需要按对应数据类型解析。

我自己的经验是,每解析一个新厂家的终端,第一次都会遇到一两个字段对不上的情况。这时候不要改完整解析器,先写一个临时脚本把原始帧按字节打印成表格,一个字节一个字节地对照协议文档核对。把字段对齐了,再回填到正式解析逻辑里,能省下很多调试时间。

5. 常见问题与排查技巧实录

最后这部分是我这几年解析各种电表报文时踩过的坑,按频率从高到低排,基本上覆盖了新手到入门会遇到的绝大部分问题。

5.1 CS校验不过,多半是字段范围搞错了

CS校验失败是最常见的问题。原因不外乎三类:长度字段读错了,导致帧边界切错;校验范围算错,帧头CS和帧尾CS没区分;地址域里有厂家扩展字段,多读或少读了一个字节。

排查技巧是,先手工对着十六进制算一遍CS。用最笨的办法,把帧头CS对应的那一段字节逐个异或,看结果是否一致。如果手工算都对,脚本算不对,那就是脚本里字段范围写错了。如果手工算也不对,那说明帧本身可能就有问题,再去抓一次包。

5.2 长度字段是单字节还是双字节,别想当然

我见过很多新手拿着单字节长度解析逻辑去解析双字节长度帧,结果后面所有字段全部错位。698.45支持两种长度格式,解析前必须先判断。判断方法看第一个长度字节,不同实现有不同标志位,有的看最高位,有的看数值范围。稳妥做法是:先按第一种方式解析,如果后续CS校验失败,再换另一种方式试,用校验结果反向验证长度判断是否合理。

这个“用CS反向验证字段边界”的思路,在解析任何未知协议帧时都非常好用。

5.3 压缩格式和位图,批量数据识别不了

698.45支持压缩格式,数据标识DIT可以用位图来表示连续或者不连续的多个对象。目的是减少报文长度,但代价是解析复杂度上了一个台阶。如果你看到一段数据里有位图字节,后面跟着多个对象的属性值,就要按压缩规则展开,不能简单按“一个DIT加一个数据”的结构去读。

遇到压缩格式,我建议先解出位图,把所有被选中的对象标识还原出来,再逐个去匹配数据。这一步逻辑不复杂,但分支多,最容易出bug,一定要用带多个对象的真实报文测试。

5.4 常见问题速查表

下面这张表是我整理的排查速查表,问题、可能原因、处理办法三列,基本覆盖了日常会遇到的坑。

问题现象可能原因处理办法
CS校验失败长度判断错误或地址域长度不对手工异或核对,反向验证帧边界
收到FF或00刷屏串口参数不匹配确认波特率、校验位、停止位
控制域看起来不像I帧抓到了S帧或U帧单独处理确认帧和链路管理帧
数据全是乱码BCD码和二进制混用根据数据类型标志选择解析方式
批量数据读不出来压缩格式未解位图先解位图再匹配对象属性
地址域对不上厂家扩展字段找厂家协议补充文档逐字节核对

5.5 实操心得:先存原始报文,再写解析器

最后分享一个小技巧,也是我踩过几次坑之后才养成的习惯:无论现场多着急,先完整保存一份原始报文,再做任何解析和判断。因为很多解析问题,不是你协议看不懂,而是你手里的报文本身不完整,比如串口工具把长帧截断了、缓冲区溢出了、或者半包过来了你还在按全帧解析。

保存原始报文用最简单的十六进制文本就行,不带任何加工,附带时间戳最好。后面无论是写解析脚本、复现问题,还是和厂家工程师对齐字段含义,这份原始报文都是最有力的证据。我有一次排查一个偶发上报异常,就是靠翻三个月前的报文日志找到规律的,这在现场调试中非常关键。

这套解析流程现在基本是我的肌肉记忆了:拿到一帧数据,先做CS校验,再看控制域判断帧类型,接着抠地址域,最后去DIT里找业务数据。踩过几次坑之后,最大的体会是不要凭肉眼去数十六进制串的字节位置,越是复杂的协议,越要依赖字段长度的解析和校验结果来定边界。如果你也是第一次被698.45报文折磨,建议先别急着抄完整解析器,找来一份真实报文,对照协议文档一个字段一个字段地过。等你能徒手把一帧报文拆到字节级,再回头看其它电力通信协议,都会觉得轻松不少。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 1:33:20

SPWM三种调制方式对比:原理、Simulink实现与工程选型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:33:20

STM32与Zigbee智能家居环境监测系统实战:从选型到组网

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:33:00

IMU标定实战:kalibr_allan、imu_tk与imu_utils三大工具对比指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:32:38

I2C信号排查四步法:万用表、示波器与逻辑分析仪协同诊断

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:32:13

TCP-RDT2.2.zip 拆解:从三次握手到可靠传输的最小闭环实验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:32:13

大疆Pocket 3直播推流方案:ENCSHV2硬件编码器RTMP配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华