简介:这是一份面向电力行业采集系统开发者的 DL/T 698.45 规约源代码,专注解决主站、采集终端及电能表之间的互操作性数据交换需求。资源涵盖协议完整实现,包括通信架构、数据链路层、应用层、接口类与对象标识等模块,适合从事 698.45 协议开发、调试与学习的工程师参考。压缩包内共 288 个文件,主要以 C 源文件(119 个 .c)和头文件(85 个 .h)构成,搭配 project/cproject 工程配置、cfg 配置脚本及少量辅助脚本,目录结构清晰,便于直接导入 IDE 阅读、交叉编译验证或按模块二次开发。包体仅 2.2MB,结构紧凑,按功能模块组织,并包含典型接口调用与对象操作示例,方便对照理解协议交互流程。已有 6244 人学习浏览,是一份贴合国网规范、可落地的协议实现参考,能帮助开发者快速搭建协议栈原型、排查数据帧处理逻辑,节省从零读规约的时间。
1. 这套 dlt698.45 规约源代码解决的是抄表通道里的什么难题
在代码仓库里搜 dlt698.45 规约源代码的人,多半是刚从集中器调试现场回来:主站报文发出去了,从站不回;回了,又解析不出表里的正向有功电能。698.45 和早年的 645 规约完全不同,它不把每个数据固定在某条报文的偏移位上,而是把电表抽象成对象,数据挂在对象属性下,读取动作变成“定位对象、请求属性、解析返回值”。这套源代码解决的就是这件事:链路层怎么成帧、应用层怎么描述请求、数据标识怎么组、返回数据按什么格式拆。适合正在做用电信息采集主站或设备交互、手里有抓包数据但缺一份能落地的代码骨架的从业者,也适合被厂家 SDK 黑匣子折腾过的集成工程师。
2. 从报文到对象模型:698.45 的源码结构该怎么拆
2.1 链路层四类帧与帧格式:代码先搭帧头再谈业务
拿到源码先别急着读业务逻辑,链路层是绕不过去的第一道门。698.45 链路层里有几类帧在来回切换,按方向分:主站发给从站的发送/确认类、主站请求从站响应的请求/响应类、从站主动上报类,以及对应回复的确认类。代码里如果用状态机来识别这几种帧,基本就能覆盖集中器与主站之间的大部分交互。
链路层的帧结构不复杂,但每个字节的位置都是约定好的,常见实现长这样:
| 位置 | 内容 | 说明 |
|---|---|---|
| 1 | 0x68 | 帧起始符 |
| 2~3 | 帧长 L | 从控制域到校验域之间的字节数 |
| 4 | 0x68 | 帧起始符复读 |
| 5 | 控制域 C | 标识链路方向与帧类型 |
| 6~6+N | 地址域 A | 设备地址,长度可变 |
| 后 | 链路用户数据 | 应用层 APDU,可能为空 |
| 倒数第 2 | 校验 CS | 控制域到 APDU 字节累加和的低 8 位 |
| 最后 | 0x16 | 结束符 |
这个 L 是新手最容易算错的地方。有人把两个 0x68 也算进帧长里,结果从站按错误长度切帧,直接静默丢弃。写代码时先把这个公式焊死在注释里:L = 1 个控制域字节 + 地址域字节数 + 应用层 APDU 字节数 + 1 个校验字节。
2.2 应用层 APDU 与对象模型:OAD 和接口类的关系
链路层之上是应用层,APDU 里的内容才是业务重点。698.45 的请求类型常见有 GET、SET、ACTION 几类,分别对应读数据、写参数、触发某个动作。另一条线是事件上报和异常响应,比如数据变化主动上报、从站异常状态通知,这部分源码通常单独拆成一个模块,避免和主动查询流程互相干扰。
为什么源码里满屏都是 OAD 和接口类?因为 698.45 是面向对象设计,电表不再按“数据项编号”暴露字段,而是按对象的属性组织数据。例如一个测量点对象拥有多个属性:电压、电流、功率、电能量,每一个属性通过 OAD 来定位。接口类则是同类对象的通用行为抽象,主站发出的 GET 命令不是直接读某个寄存器,而是对一个接口类的实例发起属性访问。
2.3 拿到源码先分清方向:主站协议栈、从站协议栈还是报文工具
很多人下载到一份“dlt698.45规约源代码”,打开发现根本跑不起来,往往不是代码坏,而是拿错了对象。源码按用途分三类,入口完全不同:
| 源码形态 | 典型入口 | 调试场景 |
|---|---|---|
| 主站协议栈 | 主动发请求、解析响应、定时采集 | 主站软件、采集网关 |
| 从站协议栈 | 接收请求、组响应、事件上报 | 电表、终端设备固件 |
| 报文解析工具 | 读抓包文件、离线拆帧、字段展示 | 调试分析、协议测试 |
我一般先看工程里有没有 main 函数、有没有设备地址配置、有没有 socket 或串口初始化的代码,就能快速判断这份源码是哪个方向。主站协议栈的核心是“组织请求动作”和“异步等待响应”,从站协议栈的核心是“分帧收流”和“按请求构造应答”。方向上错了,后面怎么调都是翻车现场。
3. 搭建最小可编译的 698.45 主站报文收发骨架
3.1 定义帧结构体与序列化:把字节序先焊死
写主站代码的第一步,是先把链路层成帧函数写出来。这一段我从不让别人代写,因为字节序、地址域长度、校验算法每家工程的约定都可能不同。下面是一个常见的 Python 实现骨架,适合先跑通流程再移植到 C 或 Java:
def make_oad(oa: int, od: int, row: int, col: int) -> bytes: # OA:对象属性编号,占2字节,网络字节序 # OD:属性特征,1字节,通常为0 # row/col:对象下的行号和列号,从1开始计数 return oa.to_bytes(2, "big") + bytes([od, row, col])OAD 总长度是 5 字节:OA 占 2 字节,OD 占 1 字节,行号 1 字节,列号 1 字节。OA 的高低位顺序一定要用大端,因为在链路层和应用层里非对称字节序的坑太多,统一大端处理,后面解析响应时能少一半问题。
def wrap_link_frame(addr: bytes, apdu: bytes, ctrl: int = 0x13) -> bytes: # addr: 3字节设备地址,可按实际系统改为1/2/4字节 # apdu: 应用层数据单元,链路用户数据 # ctrl: 控制域,0x13仅是工程示例,实际由链路状态位组合 length = 1 + len(addr) + len(apdu) + 1 # 控制域+地址域+APDU+校验 if length > 255: raise ValueError("frame too long") head = b"\x68" + bytes([length, length]) + b"\x68" body = bytes([ctrl]) + addr + apdu cs = sum(body) & 0xFF # 累加和,丢弃进位 return head + body + bytes([cs, 0x16])这段代码把帧长 L 严格限制为“控制域 + 地址域 + APDU + 校验”的长度,两个 0x68 不算进去。校验 CS 是从控制域开始到 APDU 结束的字节累加和,取低 8 位。地址域长度是可变参数,工程里常见 3 字节,现场如果遇到 4 字节地址的设备,直接改 addr 传入长度即可。
3.2 一个最简的读对象属性请求发送流程
链路层封装好了,接下来就是构造应用层 GET 请求。先写一个最简版本,不叠加时间戳和完整协商上报,只把“读某个对象属性”这条路打通:
def build_get_request(oad: bytes, acd: int = 0x07) -> bytes: # 0x87: 应用层请求帧标志 # acd: 应用层控制域,0x07为工程示例值 # 请求体:OAD + 数据项个数(这里固定读1个) return bytes([0x87, acd]) + oad + bytes([1])然后通过 socket 发出去,示例地址和端口按现场环境改:
import socket addr_bytes = b"\x00\x12\x34" # 3字节设备地址,按实际表计修改 oad = make_oad(0x0100, 0, 1, 0) # 示例OAD:读某对象第1行第0列属性 apdu = build_get_request(oad) frame = wrap_link_frame(addr_bytes, apdu) sock = socket.create_connection(("127.0.0.1", 6600), timeout=3) sock.sendall(frame) resp = sock.recv(512) sock.close()这段流程里最需要注意的是 recv 一次不一定能拿到完整帧,尤其是大块数据回传时。初版脚本可以先 recv 一次,跑通后必须改成循环接收。OAD 示例里的 0x0100 只是占位,不同厂商设备对同一类数据的 OA 分配不一定一样,调试时要先用真实设备的说明文档核对。
3.3 接响应:链路状态字与返回码判断
接收端的解析函数比发送端更容易翻车,因为字节流里可能混入干扰数据,也可能一帧被拆成两次传输。下面是我常用的拆帧骨架:
def parse_link_frame(buf: bytes): # 累计缓冲,解析出一帧后就返回链路用户数据和剩余字节 while len(buf) >= 6: if buf[0] != 0x68: buf = buf[1:] # 干扰字节,丢弃后继续找帧头 continue if buf[2] != 0x68: buf = buf[1:] continue length = buf[1] # 帧长L if len(buf) < 5 + length: return None, buf # 帧未收全,等下次数据到达 body = buf[4:4 + length] if body[-1] != (sum(body[:-1]) & 0xFF): return None, buf[1:] # 校验失败,丢帧头重找 rest = buf[5 + length:] return body[:-1], rest return None, buf这里用 5 + length 作为整帧总长度,是因为完整帧是:0x68 + L + L + 0x68 + body + 0x16,body 长度等于 L。解析完成后 body[:-1] 里的内容就是控制域、地址域和应用层 APDU。拿到 APDU 后还要看返回码,常见实现里 0x00 表示请求被正常处理,非 0 值可能是对象不存在、数据类型错误、数据块号越界,具体含义以厂家定义为准。
4. 数据标识与对象寻址:抄表脚本里最容易写错的一段
4.1 OAD 的五个字节怎么组:OA、OD、行、列
OAD 是 698.45 里最核心的概念,也是报错率最高的地方。它由四部分组成:OA 两字节、OD 一字节、行号一字节、列号一字节。行号定位一个对象下的多条同类数据,列号一般用于扩展字段,普通工程里列号通常写 0。
def parse_oad(data: bytes) -> tuple: if len(data) != 5: raise ValueError("OAD must be 5 bytes") oa = int.from_bytes(data[0:2], "big") od = data[2] row = data[3] col = data[4] return oa, od, row, col我在现场遇到过一种很隐蔽的情况:主站发的 OAD 正确,从站也回了正确数据,但主站解析时把 OA 当成小端组合,高低字节位置一颠倒,读出来的行号和列号全对不上。所以组 OAD 和拆 OAD 必须用同一套字节序,建议在工程里抽取成公共函数,禁止在业务代码里手工拼 bytes。
4.2 对象属性优先级与返回数据格式:先读懂返回码再动解析
应用层返回的数据不是裸的数值,前面会带数据格式描述信息。有些源码里把这部分叫数据模板,它会告诉你:这个返回值是 8 位无符号整数、16 位无符号整数、字符串还是 BCD 码。不少人拿到数据后直接按整型解析,结果电能量数据变成了天文数字。
| 返回码 | 常见含义 | 处理建议 |
|---|---|---|
| 0x00 | 请求正常,返回有效数据 | 继续按数据模板解析 |
| 0x01 | 对象不存在或数据不可访问 | 检查 OAD 的行号列号 |
| 0x02 | 数据类型不匹配 | 核对 OA 对应的属性定义 |
| 0x03 | 数据块号或范围越界 | 检查分帧请求参数 |
同一份源码里,返回码定义很可能和表计厂商的文档有出入。我习惯在拿到新设备后先抓一包正确请求的响应,对照抓包工具里的返回码值,把代码里的映射表修正一遍。这一步不花时间,却能省掉后面排错的好几天。
4.3 常用抄表对象的数据集写法:电能量和电压的 OAD 样例
工程里抄得最多的就是电压和电能量。下面以某型设备为例,列一组常见对象的 OAD 写法,注意 OA 仅为示例:
| 抄读内容 | OA 示例 | OD | 行 | 列 | 说明 |
|---|---|---|---|---|---|
| A 相电压 | 0x0102 | 0 | 1 | 0 | 测量点 1 的 A 相电压属性 |
| B 相电压 | 0x0103 | 0 | 1 | 0 | 不同设备 OA 可能有差异 |
| 正向有功电能 | 0x0200 | 0 | 1 | 0 | 总电量,部分表按费率分几行 |
| 当前电流 | 0x0105 | 0 | 1 | 0 | 结合列号区分 A/B/C 相 |
写采集脚本时,把每个抄读项配置成一条记录:名称、OA、OD、行号、列号、数据类型。不要硬编码在业务代码里,因为现场表计换厂家后,OAD 大概率要批量调整。配置化之后,换设备只改配置文件,协议代码不动。
5. 常见问题与避坑:从报文异常到对象不生效的排查记录
5.1 报文发出后从站静默:先把帧长 L 和地址域长度对上
现象:主站把读请求发出,从站连链路层的确认都不回,抓包工具里只有主站侧的发包记录。原因几乎都是帧长 L 计算错误。常见的是把开头两个 0x68 也算进长度,或者地址域实际是 4 字节但代码里写死 3 字节,从站按错误长度截取后直接丢弃。解决方法是打印整帧 hex,手工数一遍:L 必须等于 1(控制域) + 地址域长度 + APDU 长度 + 1(校验)。换个地址域长度的设备时,重点检查这里,别只顾着改业务数据。
5.2 数据读回来了但解析乱码:数据模板里的类型没对上
现象:响应帧收到了,数据长度也对,但解析出来的电压值是 40000 多,或者电能量小数点位置错了好几位。原因是返回值不是普通的 16 位整型,而是 BCD 码或 4 字节浮点格式,代码却按无符号整数直接拆。解决方法是先找到响应里的数据模板字段,确认数据类型后,再选择对应的解析分支。我在工程里会为每一种数据类型写独立的解析函数,绝不在主流程里混合判断。
5.3 写参数返回成功,重启后参数恢复原值:少了确认流程
现象:用 SET 写了费率或阈值,从站返回成功,但设备断电重启后参数恢复成旧值。原因是一些终端要求更严格的“写后确认”流程,写完不能直接收工,要立刻发起读请求,把写入值读回来比对;还有部分设备的写操作要求先解锁或进入特定状态。解决方法是把“写值、读回、比对”封装成一个完整事务,比对不一致时重试两次,并记录告警。这个流程不是规约强制要求的,但在现场可靠性上有明显差别。
5.4 多帧响应拼不出完整数据:帧序号和结束标志没处理
现象:读一大块负荷数据,从站分多帧回传,拼接后中间缺了一段,或尾部和下一帧数据混在一起。原因是分帧回传时每帧带有帧序号和结束标志,解析代码如果只按帧长拆,不检查序号连续性,就会丢帧或重复拼接。解决方法是维护一个接收状态机:记录当前已收到的帧序号、总帧数、每帧的结束标志。当帧序号比期望值大,说明中间丢帧,整块数据作废重新请求。
5.5 下载回来的源码编译不过:先确认它是不是完整工程
现象:源码文件看起来目录齐全,一编译几百个错误。原因很常见:这份代码只是某个模块的片段,比如只有应用层解析部分,没有链路层驱动;或是依赖了某厂家私有的加密中间件,而对方没有发布头文件。解决方法是开工前先扫一遍工程文件,找到主入口、依赖项说明、外部库目录。如果连构建脚本都没有,那这份代码更适合当阅读资料,而不是工程基础。
6. 只抄一个表读数,要不要把整套框架搬进工程里
很多刚接触这套规约的人,问的第一个实际问题是:我只是想抄一个电表的电压和电能,需要把整套主站协议栈搬到工程里吗?我的建议是分阶段。第一阶段用一个脚本把链路层封帧、应用层 GET、响应解析三件事跑通,数据能打印出来就够了。这时候不要想着什么多线程采集、事件上报、对象注册表,把直读脚本做成可复用的命令行工具就行。
第二阶段再考虑配置化。我给自己的工具留了一个配置项:OAD 列表文件,格式就是“名称,OA,OD,行号,列号,数据类型”,脚本启动后按配置逐条读取。换表计时只改配置,不动代码。这个过程里最有价值的不是代码本身,而是积累下来的对照表——不同厂家对同一类数据的 OA 分配差异很大,这份记录就是后续所有调试的数据库。
第三阶段才谈得上把整套框架搬进去。如果今天只是做一次数据核对的临时任务,完全没有必要先把从站协议栈和事件上报都接上;真正要长期跑采集服务时,再引入链路状态机、响应超时重发、帧序号拼接都不迟。代码量不是越全越好,能扛住现场的重试机制才重要。
我自己的习惯是保留一个日志回放脚本,专门把抓包文件里的 hex 数据喂给 parse_link_frame,验证新版本的解析函数没有改坏旧逻辑。
for line in open("trap.log", encoding="utf-8"): hex_data = line.strip().split(":")[-1] payload, _ = parse_link_frame(bytes.fromhex(hex_data)) if payload: print(f"frame ok: {payload.hex()}")回归验证跑一遍,再拿着同一个日志对照厂家原厂调试工具的输出,两边一致才敢放到现场用。这个习惯帮我挡掉了好几次改字节序改出解析事故的后悔药。整套思路总结下来就是:先跑通一条直读链路,再配置化数据标识,最后验证回归。方向对了,剩下的都是时间。希望帮到你。
本文还有配套的精品资源,点击获取