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