DLT645 这个规约名字,凡是做过电力抄表、能耗监测或者配电房改造的人,多少都绕不开。而一提到它,十有八九第一个卡住新人的地方不是波特率、不是校验方式,而是地址——表地址到底怎么读、怎么设、广播地址是什么、为什么我发出去的帧表就是不回。我自己最早接触这套规约的时候,就在地址上栽过跟头:拿着一串十二位的表号直接往帧里塞,结果现场十几块表一块都不应答,排查了大半天才发现是字节序和补位的问题。所以这篇东西,我想把 DLT645 里“地址”这一件事彻底讲透,从它为什么这么设计、帧里怎么摆、实操怎么读写、到现场踩过的坑,全部摊开来说。
这篇内容适合三类人看:一是刚入行做电力数据采集、能耗平台对接的工程师,二是需要自己写串口调试脚本或者上位机程序的开发者,三是现场做配电改造、需要手工配置仪表的运维人员。哪怕你之前完全没碰过 645,只要跟着把地址这一块啃下来,后面读电能、读电压电流基本就是照葫芦画瓢的事。下面我按“设计思路—帧结构细节—实操读写—问题排查”这条线来讲,中间会穿插大量我实际调试时的参数和记录,能直接抄去用的地方我会标出来。
1. 先搞懂 DLT645 地址到底是个什么东西
1.1 地址在规约里扮演的角色与设计初衷
DLT645 是一套主从结构的通信规约,一条总线上通常挂一块主站设备(采集终端、集中器或者你的电脑)和若干块从站表计。主站喊话,从站应答,那么问题来了:总线上挂着几十块表,主站发一帧出去,怎么让指定的那块表知道“这帧是找我的”?靠的就是地址。地址在这里的作用,跟快递单上的收件人一模一样——帧头里写着谁的门牌号,谁才拆包裹,其他人听见了也当没听见。
这套规约的地址设计有个很关键的特点:它是按表号(也就是电能表出厂编号)来编址的,而不是像很多工业总线那样用 1、2、3 这样的站号拨码。为什么这么设计?因为电力系统的表计数量太大了,一个台区几百块表,一个供电所几万块表,如果靠人工拨码分配站号,重号、漏号、换表后忘记改号这些问题会把人逼疯。而每块表出厂就有一个唯一的表号,直接拿表号当地址,换表的时候新表号天然不冲突,管理上省了太多事。这是理解 645 地址的第一把钥匙:地址即表号,表号即身份。
但表号通常是很长的一串数字(常见是 12 位),而 645 的地址域只给了 6 个字节。6 个字节按 BCD 码能表示 12 位十进制数字,刚好装得下。这就是为什么 645 的地址域固定是 6 字节、按 BCD 编码——它是为 12 位表号量身定做的。你要是拿到的表号不足 12 位,就得在前面补零凑够 12 位;超过 12 位的表号,那这套规约的地址域就装不下了,得走别的扩展方式。这个“6 字节 BCD 对应 12 位表号”的对应关系,是后面所有操作的基础,务必先记牢。
1.2 地址域的字节序:低字节在前,这是最大的坑
地址域 6 个字节,装 12 位表号,那到底怎么摆?这里有个极其容易出错的地方:645 规约的地址域是低字节在前(小端序)传输的。也就是说,如果你的表号是123456789012,你不能按从左到右的顺序往帧里填,而要按从右到左、每两位一个字节倒着填。
我拿一个具体例子走一遍。假设表号是000000000123(前面补零凑够 12 位),拆成 6 组两位:00 00 00 00 01 23。按低字节在前的规则,帧里地址域的顺序应该是:23 01 00 00 00 00。注意每个字节内部还是正常的 BCD 高位在前(比如23就是 0x23,01就是 0x01),只是字节与字节之间的排列顺序反过来了。
这个规则我第一次接触时觉得别扭,后来想明白了:这跟很多嵌入式系统里多字节数据的存储习惯是一致的,规约制定时沿用了这个约定。但别扭归别扭,它是硬性规定,填错了表就不认。我见过太多人在这里翻车——帧发出去格式看着完全正确,校验也对,就是不回,最后发现地址字节顺序反了。所以你在写代码拼帧的时候,一定要有一个明确的“表号转地址域”的函数,把倒序这件事封装进去,别每次手工拼,手工拼迟早出错。
1.3 广播地址、通配地址与特殊地址的含义
除了正常的表号地址,645 里还有几个特殊地址值得单独拎出来说,因为现场调试时它们特别好用。
广播地址是AA AA AA AA AA AA(6 个 0xAA)。主站发广播帧,总线上所有表都会接收并执行,但按规约通常不应答(避免总线上几十块表同时开口造成冲突)。广播地址最典型的用途是对时和广播冻结——比如你给整个台区的表统一校时,用广播地址发一条对时命令,所有表一起改时间,效率极高。但要注意,广播命令没有应答,你发出去不知道成没成功,所以关键操作后最好再逐块表读一次确认。
通配地址在某些实现里用99 99 99 99 99 99之类的特殊值表示,含义视具体厂商实现而定,有的用于“任意地址匹配”,有的用于特定测试场景。这个不像广播地址那样是规约强制的通用约定,用之前一定要查对应表计的通信协议手册,别想当然。
还有一个概念是地址的匹配规则:从站收到帧后,会拿帧里的地址域跟自己的地址逐字节比对,6 个字节全部一致才认为是发给自己的。这里没有“部分匹配”或者“掩码匹配”的说法(除非厂商做了私有扩展),所以地址必须精确。这也解释了为什么补零不能省——你少补一个零,6 字节的内容就变了,表就不认。
2. 地址域在帧结构中的位置与读写帧拆解
2.1 完整帧结构里地址域处在哪一段
要把地址用对,得先看清它在整帧里的位置。DLT645 的帧结构是固定的,我按顺序列一下,你对照着记:
| 段名 | 字节数 | 说明 |
|---|---|---|
| 起始符 | 1 | 固定 0x68 |
| 地址域 | 6 | 低字节在前的 BCD 表号 |
| 起始符 | 1 | 固定 0x68 |
| 控制码 | 1 | 决定是读、写、应答等 |
| 数据域长度 | 1 | 数据域的字节数 |
| 数据域 | N | 具体数据,读电能时是数据标识等 |
| 校验码 | 1 | 从第一个起始符到数据域末字节的算术和模 256 |
| 结束符 | 1 | 固定 0x16 |
可以看到,地址域紧跟在第一个起始符0x68后面,占 6 个字节,然后又是第二个0x68。这个“68 + 地址 + 68”的结构是 645 的标志性特征,抓帧的时候一眼就能认出来。地址域的位置固定,意味着你解析任何一帧,第 2 到第 7 个字节(从 0 开始数就是索引 1 到 6)就是地址,把它倒序还原再转成字符串,就是表号。
我实际抓过一帧读地址的应答,原始字节是这样的(十六进制):
68 23 01 00 00 00 00 68 91 08 33 33 34 33 78 56 34 12 AB 16拆开看:68起始,23 01 00 00 00 00是地址域,倒序还原就是00 00 00 00 01 23,即表号000000000123;第二个68;91是控制码(从站正常应答读数据);08是数据域长度 8 字节;后面 8 个字节是数据;AB是校验;16结束。这一帧把地址域的位置和还原方法演示得清清楚楚,你可以拿它当模板对照自己抓的帧。
2.2 读地址命令的构造与实操
读表地址是现场最常用的操作之一,尤其是新装表或者换表后,你得先确认表号对不对。645 里读地址有专门的命令,控制码是0x13(读通信地址),注意这个命令有个特殊之处:它的地址域可以用通配地址或者广播地址,因为目的就是让表把自己的地址报上来。
构造读地址帧,地址域填AA AA AA AA AA AA(广播),控制码0x13,数据域长度0x00,没有数据域。拼出来是:
68 AA AA AA AA AA AA 68 13 00 CS 16其中 CS 是校验码,算法是从第一个68到数据域末字节(这里就是到长度字节00)所有字节的算术和模 256。我算一下:0x68 + 0xAA*6 + 0x68 + 0x13 + 0x00。0xAA 是 170,乘 6 是 1020;0x68 是 104,两个是 208;0x13 是 19。总和 1020 + 208 + 19 = 1247,1247 mod 256 = 1247 - 4*256 = 1247 - 1024 = 223,即 0xDF。所以完整帧是:
68 AA AA AA AA AA AA 68 13 00 DF 16发出去之后,正常情况下总线上每块表都会回一帧,控制码0x93(应答),数据域里带着自己的 6 字节地址。这里有个实操细节:如果总线上挂了多块表,用广播地址读地址会导致多块表同时应答,帧在总线上撞车。所以正确做法是:要么总线上只挂一块表时用广播读,要么用你已知的地址逐个读,要么在调试阶段一块一块接上来读。我早期图省事,一条总线挂十几块表直接广播读地址,结果收到的全是乱码帧——多块表的应答在物理层上叠加了。这个坑很典型,记住:广播读地址只适合单表场景。
2.3 写地址(改表号)的帧构造与风险提示
有些场景需要改表号,比如表计返修后重新投运、或者批量初始化。645 里写通信地址的控制码是0x14(写通信地址),它的地址域填当前地址(也就是改之前的地址),数据域里放新地址(6 字节,同样低字节在前)。
举个例子,把表号000000000123改成000000000456。当前地址域是23 01 00 00 00 00,新地址倒序是56 04 00 00 00 00。数据域长度0x06,数据域就是这 6 个字节。帧大致是:
68 23 01 00 00 00 00 68 14 06 56 04 00 00 00 00 CS 16写地址这个操作我要重点提醒:它是不可逆的高风险操作。表号一旦改掉,如果你没记清新号,或者改的过程中通信中断导致表实际改了但你没收到确认,这块表就可能“失联”——你按老号找它找不到,按新号又不知道到底改没改成。所以我的做法是:改之前先把原表号、新表号、操作时间记在本子上或者系统日志里;改完立刻用新地址读一次地址命令确认;如果读不到,再用广播读地址(单表场景)把当前实际地址捞出来。另外,有些表厂对写地址做了保护,需要先做权限认证或者短接特定端子才能写,这个得看具体表的手册,别硬来。
注意:写地址命令在部分表计上需要配合密码或特定操作序列,盲目发送可能被拒绝甚至触发保护。动手前务必确认表计支持该命令且了解其前置条件。
3. 地址相关的实操全流程:从拿到表到读通数据
3.1 准备工作:串口参数与接线确认
在碰地址之前,物理层和串口参数得先对。645 常用的是 RS-485 半双工,接线是 A 接 A、B 接 B,这个接反了表不应答,很多人会误以为是地址问题。串口参数方面,645 的典型配置是:波特率 2400(也有 1200 和 9600 的,看表计手册)、数据位 8、停止位 1、校验位偶校验(Even)。注意校验位这一项,645 默认是偶校验,如果你设成无校验,帧的字节内容虽然没变,但物理层校验对不上,表可能直接丢弃。我见过有人调了半天地址,最后发现是串口校验位设错了。
接线和参数确认后,先别急着发读地址命令,可以先用串口助手发一帧最简单的读数据命令(比如读电压),看有没有应答。有应答说明物理层通了,再往下折腾地址才有意义。如果完全没反应,先查接线和波特率,别一上来就怀疑地址。
3.2 单表场景:广播读地址的完整操作记录
假设现场只有一块表,接线和串口都配好了。我的操作顺序是这样的:
第一步,打开串口助手,参数设 2400、8、1、偶校验,十六进制发送模式。
第二步,发送广播读地址帧68 AA AA AA AA AA AA 68 13 00 DF 16。
第三步,观察接收。正常情况下会收到类似68 XX XX XX XX XX XX 68 93 06 [6字节地址] CS 16的应答。把应答里的 6 字节地址倒序还原,就是表号。
我实际记录过一次应答:68 23 01 00 00 00 00 68 93 06 23 01 00 00 00 00 XX 16,地址域23 01 00 00 00 00倒序得00 00 00 00 01 23,表号000000000123。注意应答帧里地址域出现了两次——一次在帧头(标识这帧来自哪块表),一次在数据域(读地址命令的返回内容),两处应该一致,不一致说明帧有问题。
第四步,拿到表号后,用这个表号构造正常的读数据帧,验证能否读到电能等数据。这一步是闭环验证,确认地址不仅读对了,而且能用于后续通信。
3.3 多表场景:逐表寻址与地址管理
总线上挂多块表时,广播读地址行不通(前面说过会撞车)。这时候有两种做法。
第一种是逐表接入法:调试阶段一次只接一块表,广播读出地址,记下来,断开,接下一块。这个方法笨但可靠,适合新装表批量录入地址的场景。我做过一个台区二十多块表的录入,就是一块一块接上来读,虽然慢,但地址一个没错。
第二种是已知地址验证法:如果你手上有表号清单(比如从供电系统导出的台账),那就直接用清单里的表号构造读数据帧,逐块验证。能应答的说明地址正确、表在线;不应答的再单独排查。这个方法快,但前提是清单准确。实际中台账和现场对不上的情况不少(换过表没更新台账、表号抄错一位等),所以验证环节不能省。
多表场景还有一个地址管理的问题:同一总线上地址不能重复。虽然表号出厂唯一,但如果你手工改过地址,或者用了非正规渠道的表,理论上存在重号可能。重号会导致主站发命令时两块表同时应答,数据错乱。所以批量投运前,做一次地址查重是值得的——把所有表号读出来排个序,看有没有重复。
3.4 用脚本批量处理地址的示例
手工在串口助手里一帧一帧发,表多了受不了。我一般会写个小脚本批量处理。下面这段 Python 演示了表号与地址域互转、以及读地址帧的构造,逻辑很直白,你可以直接改成自己用的版本:
def meter_no_to_addr(meter_no: str) -> bytes: # 表号补零到12位 s = meter_no.zfill(12) # 每两位一组,从右往左取,组成6字节 groups = [s[i:i+2] for i in range(0, 12, 2)] # 从左到右6组 groups.reverse() # 倒序,实现低字节在前 return bytes(int(g, 16) for g in groups) def addr_to_meter_no(addr: bytes) -> str: # 地址域倒序还原成表号字符串 rev = list(addr)[::-1] return ''.join(f'{b:02X}' for b in rev) def build_read_addr_frame() -> bytes: body = bytes([0x68]) + bytes([0xAA]*6) + bytes([0x68, 0x13, 0x00]) cs = sum(body) & 0xFF return body + bytes([cs, 0x16]) # 验证 addr = meter_no_to_addr("123") print(addr.hex(' ').upper()) # 23 01 00 00 00 00 print(addr_to_meter_no(addr)) # 000000000123 print(build_read_addr_frame().hex(' ').upper())这段代码里meter_no_to_addr和addr_to_meter_no是一对互逆函数,建议你在自己的项目里也这么封装,所有涉及地址的地方都调这两个函数,杜绝手工拼字节。build_read_addr_frame算校验用的是sum(body) & 0xFF,等价于模 256,简单可靠。跑一下输出,23 01 00 00 00 00和000000000123都对得上,说明转换逻辑没问题。
4. 地址相关的常见问题与排查实录
4.1 发了帧没应答,先按这个顺序查
地址相关的“没应答”是最常见的问题,我整理了一个排查顺序,按这个走能覆盖九成情况:
| 排查项 | 可能原因 | 处理方式 |
|---|---|---|
| 物理接线 | A/B 接反、线松、终端电阻缺失 | 对调 A/B 试,检查接线端子,长距离加终端电阻 |
| 串口参数 | 波特率、校验位不匹配 | 按表计手册核对,重点查偶校验 |
| 地址字节序 | 未倒序或倒序错误 | 用互转函数核对,抓帧比对 |
| 地址补零 | 表号不足12位未补零 | 统一 zfill(12) 处理 |
| 地址内容 | 表号抄错、台账过期 | 广播读地址(单表)重新获取 |
| 命令控制码 | 控制码用错 | 读地址用 0x13,读数据用 0x11 |
| 校验码 | 校验算错 | 重新按算术和模256计算 |
这个表我建议打印出来贴工位上,现场排查时从上往下过一遍,比凭感觉猜快得多。特别注意前两项——很多人一没应答就怀疑地址,其实一半以上是接线或串口参数的问题。先确认物理层通了,再谈地址。
4.2 地址读出来是乱码或者对不上
有时候能收到应答,但还原出来的表号是一串看不懂的东西。这种情况我遇到过几次,原因主要有两个。
一是字节序搞反了。你以为收到的是23 01 00 00 00 00,实际帧里是00 00 00 00 01 23,你按错误的方向还原,自然对不上。解决办法很简单:把收到的 6 字节地址域正着还原一次、倒着还原一次,看哪个像正常的表号(通常表号是有规律的,比如以特定前缀开头),就能判断方向。
二是帧解析错位。如果帧头没对齐,你把数据域的内容当成了地址域,那读出来必然是乱的。645 帧有固定的68...68结构,解析时先找第一个68,再确认第 8 个字节(索引 7)也是68,两个68之间就是地址域。用这个规则定位,基本不会错位。我写解析代码时习惯先做帧同步:在接收缓冲区里找68,然后校验后续结构,校验不过就往后挪一个字节继续找,直到找到合法帧。
4.3 广播命令发了但表没反应
广播地址AA AA AA AA AA AA发出去,表没反应,可能的原因:一是这块表不支持广播命令(部分表厂对广播做了限制),二是广播命令本身不应答,你误以为没反应其实是执行了。区分方法:发广播对时后,立刻用单表地址读一次时间,看时间变没变。变了说明广播生效了,只是没应答;没变说明广播没被接受。
还有一种情况是广播帧的校验算错了。广播帧地址域是 6 个 0xAA,校验计算时这 6 个字节都要算进去,别漏。我见过有人算校验时把地址域当成了一个整体只加一次,结果校验错,表直接丢弃帧。
4.4 换表后地址对不上台账
换表是运维常态,新表的表号跟旧表不一样,如果系统里还留着旧地址,读数据自然失败。处理流程应该是:换表时记录新表号(表壳上一般有铭牌),换完后用新表号构造读数据帧验证,验证通过后更新台账和采集系统里的地址配置。这里有个容易忽略的点:有些采集终端会缓存地址,改了台账还得让终端重新加载配置,否则终端还在用旧地址发命令。我吃过这个亏,台账改了、终端没重启,折腾半天以为表有问题,最后重启终端才好。
提示:换表操作建议做成清单——记录新表号、验证通信、更新台账、刷新终端配置,四步都打勾才算完成,避免遗漏。
5. 几个让我印象深刻的实操心得
5.1 地址调试先单表后多表,别一上来就挂满总线
这是我用血泪换来的经验。早期为了省事,一条 485 总线挂十几块表,然后开始调地址,结果各种不应答、乱码,根本分不清是哪块表的问题。后来改成先单表调通——一块表、一根线、读地址、读数据全走通,确认地址逻辑没问题,再逐步加表。加表过程中如果出问题,新加的那块就是嫌疑对象,排查范围立刻缩小。这个“增量调试”的思路,在任何总线类调试里都适用,不只是 645。
5.2 把地址转换封装成函数,永远不要手工拼字节
前面代码里那对互转函数,看起来简单,但它是避免地址错误的最有效手段。手工拼字节的时候,人脑要同时处理“补零、分组、倒序、转十六进制”四件事,任何一步走神就错。封装成函数后,你只需要传表号字符串,剩下的交给代码,出错概率断崖式下降。我现在所有项目里,只要涉及 645 地址,第一件事就是把这两个函数写进去,后面所有拼帧、解析都调它们。
5.3 抓帧比对是最快的定位手段
当你怀疑地址有问题时,最快的办法不是猜,是抓帧。用串口助手或者逻辑分析仪把发送和接收的原始字节抓下来,逐字节对照帧结构表。发送帧看地址域是不是你要的表号倒序,接收帧看地址域是不是对得上。我调试时习惯把正常通信的帧存一份当“基准帧”,出问题时拿新抓的帧跟基准帧逐字节比,差异点往往就是问题所在。这个方法比反复改代码试错快得多。
5.4 表计手册永远比通用规约更权威
DLT645 是通用规约,但不同厂商的表在实现上会有差异——有的对广播有限制,有的写地址需要密码,有的地址域虽然 6 字节但高位有特殊含义。所以每次接触新品牌的表,我都会先翻它的通信手册,重点看地址相关的章节,确认有没有私有约定。通用规约告诉你“应该怎样”,表计手册告诉你“这块表实际怎样”,现场以手册为准。这个习惯帮我避开了不少“按规约发就是不通”的怪问题。
地址这块啃下来之后,645 剩下的读数据、读变量、对时这些操作,本质上都是“换个控制码和数据标识”的事,地址逻辑是共用的。所以别嫌地址琐碎,它是整套规约的地基。我个人的体会是,把地址的字节序、补零、广播这几个点吃透,再配上一对可靠的转换函数和一个抓帧比对的好习惯,现场九成的通信问题都能自己搞定。后面如果要做更复杂的多表轮询或者协议转换网关,地址管理还会衍生出地址映射表、地址缓存这些话题,那是另一个层面的工程问题了,等把基础打牢再往上走会顺很多。