2、DL/T 645协议基础:协议帧结构、起始符与结束符、地址域格式、控制码定义、数据域长度规则

好,咱们正式开始聊DL/T 645协议。说实话,这个协议在电力行业里,就像普通话一样通用。你随便打开一个国网智能电表,里面跑的肯定是这个协议。我当年刚入行时,第一个任务就是解析这个协议的数据帧,啃了整整一周的文档。今天我把这些经验掰开揉碎了讲给你听。

2.1 协议帧结构——电表的“语言骨架”

先看整体结构。DL/T 645的帧结构,说白了就是一套固定的数据打包格式。电表和采集器之间通信,必须按这个格式来,否则谁也听不懂谁。

一个完整的帧长这样:

起始符(68H) + 地址域(A0~A5) + 起始符(68H) + 控制码(C) + 数据域长度(L) + 数据域(DATA) + 校验码(CS) + 结束符(16H)

嗯,这里要注意:起始符出现了两次。为什么?我个人理解是为了防止误判。你想啊,通信线路上可能有噪声,万一第一个68H是干扰呢?再来一个68H确认一下,可靠性就上去了。我在项目里遇到过因为线路干扰导致帧头误判的情况,后来加了二次确认逻辑才解决。

2.2 起始符与结束符——帧的“门卫”

起始符固定是68H,结束符固定是16H。这两个值不是随便定的,68H的二进制是01101000,16H是00010110,在串口通信中不容易和普通数据混淆。

重要提醒: 68H和16H是协议保留字节。如果数据域里恰好出现了这两个值,必须做转义处理。具体做法是:在数据前插入一个3AH,然后对原字节取反。这个坑我踩过,当时采集到的数据老是少几个字节,查了两天才发现是数据里有个68H被当成帧头解析了。

2.3 地址域格式——电表的“身份证号”

地址域占6个字节,从A0到A5。但这里有个容易搞混的地方:地址是BCD码格式,而且低位在前。

举个例子:电表表号是123456789012(12位),在帧里怎么放?

A0 = 12H
A1 = 34H
A2 = 56H
A3 = 78H
A4 = 90H
A5 = 12H

看到了吗?12位数字拆成6组,每组两位BCD码。发送时先发A0,再发A1……一直到A5。我刚开始做的时候,习惯性地按高位在前去解析,结果读出来的表号全是反的,闹了个笑话。

我的习惯: 收到地址域后,先把6个字节按顺序拼成一个字符串,然后每两位一组反转一下,就得到真实表号了。比如上面例子,收到的是"12 34 56 78 90 12",反转后就是"21 43 65 87 09 21"?不对,这里要小心——BCD码是整体反转,不是每两位反转。正确的做法是:把整个12位数字串"123456789012"直接读,就是表号。因为BCD码存储时,每个字节的高4位和低4位分别代表一个数字,所以A0=12H表示数字"1"和"2",A1=34H表示"3"和"4"……连起来就是123456789012。

2.4 控制码定义——告诉电表“你要干什么”

控制码C占1个字节,它决定了这条命令的类型。我把它分成三部分来看:

含义 说明
D7 传输方向 0=主站→从站,1=从站→主站
D6 从站应答标志 0=无后续数据,1=有后续数据
D5~D0 功能码 具体操作类型

常用的功能码我列一下:

  • 01H:读数据。最常用,抄表就靠它。
  • 02H:读后续数据。当数据太长一帧装不下时,用这个接着读。
  • 03H:写数据。参数设置时用。
  • 04H:写地址。给电表分配表号。
  • 0AH:广播校时。这个我特别提醒一下,广播校时不需要地址域匹配,所有电表都会响应。

举个例子:主站发一个读电压的请求,控制码就是01H(D7=0,D6=0,功能码=01H)。电表回复时,控制码变成81H(D7=1,D6=0,功能码=01H)。你看,D7位变了,表示这是应答帧。

曾经踩过的坑: 有一次我写程序,没判断D7位,直接把所有收到的帧都当成主站发的命令去处理。结果电表回复的数据被我当成新命令又发给了电表,导致电表死机。后来我养成了习惯:收到帧先看D7位,确认方向再处理。

2.5 数据域长度规则——别被“L”骗了

数据域长度L占1个字节,它表示的是数据域DATA的字节数,不包括L本身,也不包括其他字段。

这里有个容易犯错的点:L的值最大是多少?理论上1个字节最大255,但实际协议里L最大是200。为什么?因为帧太长容易出错,串口通信的缓冲区也有限。我建议你写程序时,如果收到的L大于200,直接丢弃这帧数据,别犹豫。

另外,L=0是合法的吗?是的。有些命令不需要数据域,比如广播校时,L=0,DATA为空。这时候帧里就没有数据域字段,直接从L跳到CS。

// 伪代码示例:解析数据域长度
uint8_t L = rx_buffer[7];  // 第8个字节是长度
if (L > 200) {
    // 帧异常,丢弃
    return FRAME_ERROR;
}
uint8_t data_len = L;  // 数据域实际字节数
uint8_t *data = &rx_buffer[8];  // 数据域起始位置

2.6 校验码CS——最后的“安全锁”

校验码CS是从起始符(第一个68H)开始,到数据域最后一个字节为止,所有字节的累加和,取低8位。公式很简单:

CS = (68H + A0 + A1 + ... + An + 68H + C + L + DATA[0] + ... + DATA[n-1]) & 0xFF

注意:CS只取低8位,超过的部分直接丢弃。我见过有人用16位累加和去校验,结果永远对不上。

调试小技巧: 如果你不确定自己算的CS对不对,可以用串口助手抓一帧正常通信的数据,手动算一遍。我当年就是这么干的,算完三帧数据后,对协议的理解就深多了。

2.7 完整帧示例——串起来看

咱们看一个实际例子:主站读取电表当前电压(数据标识为02010100)。

请求帧:

68 12 34 56 78 90 12 68 01 04 02 01 01 00 5A 16

拆开看:

  • 68:起始符
  • 12 34 56 78 90 12:地址域(表号123456789012)
  • 68:第二个起始符
  • 01:控制码(读数据)
  • 04:数据域长度(4个字节)
  • 02 01 01 00:数据域(数据标识)
  • 5A:校验码
  • 16:结束符

应答帧(假设电压是220.0V):

68 12 34 56 78 90 12 68 81 07 02 01 01 00 22 00 00 00 3E 16

注意看:控制码变成了81H(D7=1表示应答),数据域长度是07(4字节数据标识+3字节数据),数据域最后三个字节22 00 00表示电压值220.0(BCD码格式,单位0.1V)。

嗯,到这里DL/T 645的帧基础就讲完了。你可能会觉得这些细节有点琐碎,但相信我——搞通信协议就是这样,一个字节对不上,整个系统就瘫痪。我建议你找个真实的电表报文,对着这个结构一行一行地拆,拆完三帧你就彻底懂了。