前言:在物联网、智慧电力项目中,DL/T 645协议是多功能电能表远程抄表的核心标准,常用版本为2007版(兼容1997版)。本文提供完整可直接集成的C语言实例,基于STM32/通用MCU编写,包含帧构建、解析、BCD码转换、串口通信适配,附抓包示例和常见坑点,新手也能快速上手。
注:很多同学会混淆DL/T 645和DL/T 654,这里明确区分:
-
DL/T 645:电力行业标准,用于电能表与采集器/网关的通信(本文重点);
-
DL/T 654:《火电机组寿命评估技术导则》,与通信编程无关。
一、DL/T 645-2007 协议帧结构(核心简化)
协议帧采用异步串行通信,帧格式固定,核心结构如下(从左到右):
[前导码 FE]*N + 68 + 地址(6B) + 68 + 控制码C + 数据长度L + 数据(DATA) + 校验和CS + 16
关键说明(嵌入式编程必看):
-
前导码:FE字节,数量可自定义(通常3-4个),用于同步通信时序;
-
地址域:6字节,采用BCD码编码,低字节在前、高字节在后(重点坑点);
-
控制码:核心指令,常用0x11(读数据)、0x81(读数据应答);
-
数据域:每个字节发送前需+0x33,接收后需-0x33还原(协议加密机制);
-
校验和:从第一个68字节开始,到数据域最后一个字节,所有字节累加求和(无进位);
-
结束符:固定为0x16。
常用控制码补充(实战高频):
|
控制码 |
功能描述 |
适用场景 |
|---|---|---|
|
0x11 |
读数据 |
读取电能、电压、电流等参数 |
|
0x81 |
读数据应答 |
电表响应读数据指令,返回参数 |
|
0x13 |
写数据 |
设置电表地址、费率等(需权限) |
|
0x83 |
写数据应答 |
电表响应写数据指令,返回执行结果 |
二、C语言实战代码(嵌入式通用,STM32示例)
代码采用模块化设计,可直接复制到MCU工程,只需适配串口收发函数即可使用。包含:类型定义、地址设置、帧构建、帧解析、BCD码转换、主流程示例。
2.1 头文件与宏定义
#include <stdint.h>
#include <string.h>
#include <stdio.h>
// 协议核心宏定义
#define DLT645_68 0x68 // 帧起始符
#define DLT645_16 0x16 // 帧结束符
#define DLT645_ADDR_LEN 6 // 电表地址长度(6字节)
#define DLT645_MAX_LEN 64 // 最大帧长度(适配大多数场景)
#define DLT645_PRE_LEN 4 // 前导码数量(4个FE)
// 常用数据标识(DI,4字节,可根据需求扩展)
#define DI_FORWARD_ACTIVE_ENERGY {0x00, 0x01, 0x00, 0x00} // 正向有功总电能(kWh)
#define DI_VOLTAGE_PHASE_A {0x00, 0x02, 0x00, 0x00} // A相电压(V)
#define DI_CURRENT_PHASE_A {0x00, 0x03, 0x00, 0x00} // A相电流(A)
// DLT645协议句柄(管理收发缓冲区、地址、帧长度)
typedef struct {
uint8_t addr[DLT645_ADDR_LEN]; // 电表地址(BCD码,低字节在前)
uint8_t tx_buf[DLT645_MAX_LEN]; // 发送缓冲区
uint8_t rx_buf[DLT645_MAX_LEN]; // 接收缓冲区
uint8_t tx_len; // 发送帧长度
uint8_t rx_len; // 接收帧长度
} DLT645_HandleTypeDef;
// 串口收发函数声明(需根据自身MCU适配,此处为示例)
void UART_Send(uint8_t *buf, uint8_t len);
uint8_t UART_Receive(uint8_t *buf, uint32_t timeout); // 超时时间(ms)
2.2 电表地址设置(12位表号→6字节BCD码)
电表表号通常为12位数字(如123456789012),需转换为6字节BCD码,且遵循“低字节在前”规则。
/**
* @brief 设置电表地址(12位表号转换为6字节BCD码,低字节在前)
* @param handle: DLT645协议句柄
* @param meter_no: 12位电表表号字符串(如"123456789012")
* @retval 无
*/
void DLT645_SetAddr(DLT645_HandleTypeDef *handle, const char *meter_no) {
uint8_t bcd[6] = {0}; // 临时存储BCD码(高字节在前)
// 12位字符串→6字节BCD码(每2位数字对应1字节BCD)
for (int i = 0; i < 12; i += 2) {
uint8_t high = meter_no[i] - '0'; // 高4位
uint8_t low = meter_no[i+1] - '0'; // 低4位
bcd[i/2] = (high << 4) | low; // 组合为1字节BCD码
}
// 2. 反转BCD码:低字节在前(DLT645协议要求)
for (int i = 0; i < 6; i++) {
handle->addr[i] = bcd[5 - i];
}
}
2.3 构建读数据帧(核心函数)
根据协议格式,构建读数据指令帧,自动处理前导码、地址、校验和、数据域偏移(+0x33)。
/**
* @brief 构建DL/T 645读数据帧
* @param handle: DLT645协议句柄
* @param di: 数据标识(4字节,如DI_FORWARD_ACTIVE_ENERGY)
* @retval 发送帧长度
*/
uint8_t DLT645_BuildReadFrame(DLT645_HandleTypeDef *handle, const uint8_t *di) {
uint8_t *buf = handle->tx_buf;
uint8_t len = 0;
// 1. 前导码(4个FE,用于同步)
for (int i = 0; i < DLT645_PRE_LEN; i++) {
buf[len++] = 0xFE;
}
// 2. 起始符:68
buf[len++] = DLT645_68;
// 3. 电表地址(6字节,低字节在前)
memcpy(&buf[len], handle->addr, DLT645_ADDR_LEN);
len += DLT645_ADDR_LEN;
// 4. 起始符:68(协议规定,地址后需再跟一个68)
buf[len++] = DLT645_68;
// 5. 控制码:0x11(读数据)
buf[len++] = 0x11;
// 6. 数据长度L:数据域长度(此处为4字节DI)
buf[len++] = 0x04;
// 7. 数据域:DI +0x33(协议要求,数据字节偏移)
for (int i = 0; i< 4; i++) {
buf[len++] = di[i] + 0x33;
}
// 8. 校验和CS:从第一个68开始,到数据域最后一字节累加
uint8_t cs = 0;
for (int i = DLT645_PRE_LEN; i < len; i++) { // 跳过前导码,从第一个68开始
cs += buf[i];
}
buf[len++] = cs;
// 9. 结束符:16
buf[len++] = DLT645_16;
// 更新发送帧长度
handle->tx_len = len;
return len;
}
2.4 解析应答帧(核心函数)
接收电表应答帧,验证帧格式、控制码、校验和,还原数据域(-0x33),返回有效数据长度。
/**
* @brief 解析DL/T 645应答帧
* @param handle: DLT645协议句柄
* @param data_out: 解析后的数据缓冲区(输出)
* @retval 有效数据长度(失败返回0)
*/
uint8_t DLT645_ParseFrame(DLT645_HandleTypeDef *handle, uint8_t *data_out) {
uint8_t *buf = handle->rx_buf;
uint8_t len = handle->rx_len;
// 1. 最小帧长度检查(应答帧最小长度:前导4+68+地址6+68+控制1+长度1+数据n+校验1+结束1 = 14)
if (len < 14) {
printf("解析失败:帧长度不足\r\n");
return 0;
}
// 2. 查找第一个68(起始符)
int pos = 0;
while (pos < len && buf[pos] != DLT645_68) {
pos++; // 跳过前导码,找到第一个68
}
// 3. 验证帧结构:68 + 6字节地址 + 68(第二个起始符)
if (pos + 7 >= len || buf[pos+7] != DLT645_68) {
printf("解析失败:帧结构错误\r\n");
return 0;
}
// 4. 验证控制码:应答帧控制码应为0x81
uint8_t ctr = buf[pos+8];
if (ctr != 0x81) {
printf("解析失败:控制码错误(0x%02X)\r\n", ctr);
return 0;
}
// 5. 读取数据长度L,验证帧完整性
uint8_t L = buf[pos+9];
if (pos + 10 + L + 2 > len) { // 10=68+地址6+68+控制1+长度1;+L=数据域;+2=校验+结束
printf("解析失败:帧不完整\r\n");
return 0;
}
// 6. 校验和验证
uint8_t cs = 0;
for (int i = pos; i < pos + 10 + L; i++) { // 从第一个68到数据域最后一字节
cs += buf[i];
}
if (cs != buf[pos+10+L]) {
printf("解析失败:校验和错误(计算0x%02X,接收0x%02X)\r\n", cs, buf[pos+10+L]);
return 0;
}
// 7. 数据域还原:-0x33
for (int i = 0; i < L; i++) {
data_out[i] = buf[pos+10+i] - 0x33;
}
return L; // 返回有效数据长度
}
2.5 BCD码转浮点数(电能/电压/电流解析)
电表返回的数据为BCD码,需转换为浮点数(如正向有功电能:XX.XXXXX kWh)。
/**
* @brief BCD码转浮点数(适配4字节BCD,对应电能、电压等参数)
* @param bcd: BCD码缓冲区
* @param len: BCD码长度(通常4字节)
* @retval 转换后的浮点数(单位:kWh/V/A)
*/
float DLT645_BCD2Float(const uint8_t *bcd, uint8_t len) {
float val = 0.0f;
// BCD码转换为十进制数(每字节对应2位十进制)
for (int i = 0; i < len; i++) {
val = val * 100 + ((bcd[i] >> 4) & 0x0F) * 10 + (bcd[i] & 0x0F);
}
// 电能参数:4字节BCD对应XX.XXXXX kWh(除以100);可根据参数调整除数
return val / 100.0f;
}
2.6 主流程示例(完整调用)
适配STM32串口,实现“发送读电能指令→接收应答→解析数据→打印结果”完整流程。
/**
* @brief DL/T 645读电表正向有功总电能示例
* @param 无
* @retval 无
*/
void DLT645_ReadEnergy_Demo(void) {
DLT645_HandleTypeDef hdl;
memset(&hdl, 0, sizeof(DLT645_HandleTypeDef)); // 初始化句柄
// 1. 设置电表表号(12位,替换为实际电表表号)
DLT645_SetAddr(&hdl, "123456789012");
// 2. 构建读正向有功总电能帧(数据标识:DI_FORWARD_ACTIVE_ENERGY)
const uint8_t di[4] = DI_FORWARD_ACTIVE_ENERGY;
uint8_t tx_len = DLT645_BuildReadFrame(&hdl, di);
printf("发送读电能指令,帧长度:%d字节\r\n", tx_len);
// 3. 串口发送帧数据
UART_Send(hdl.tx_buf, tx_len);
// 4. 串口接收应答帧(超时时间1000ms)
hdl.rx_len = UART_Receive(hdl.rx_buf, 1000);
if (hdl.rx_len == 0) {
printf("接收失败:超时未收到应答\r\n");
return;
}
printf("接收应答帧,帧长度:%d字节\r\n", hdl.rx_len);
// 5. 解析应答帧,获取电能数据
uint8_t data[4];
uint8_t data_len = DLT645_ParseFrame(&hdl, data);
if (data_len > 0) {
// BCD码转浮点数,得到正向有功总电能(kWh)
float energy = DLT645_BCD2Float(data, data_len);
printf("正向有功总电能:%.2f kWh\r\n", energy);
} else {
printf("应答帧解析失败\r\n");
}
}
// 主函数调用(示例)
int main(void) {
// 1. 初始化:串口(2400 8O1)、GPIO等(根据自身MCU实现)
// UART_Init();
// 2. 循环读取电表数据(每隔5秒读一次)
while (1) {
DLT645_ReadEnergy_Demo();
HAL_Delay(5000); // STM32延时函数,其他MCU替换为对应延时
}
}
三、串口参数与抓包示例(实战调试必备)
3.1 串口参数(DL/T 645标准参数)
电表默认串口参数(必配,否则无法通信):
-
波特率:2400bps(部分电表可配置为4800/9600,需确认电表参数);
-
数据位:8位;
-
校验位:偶校验(O);
-
停止位:1位;
-
流控:无。
3.2 抓包示例(串口助手/逻辑分析仪)
以“读正向有功总电能”为例,抓包数据参考(十六进制):
-
发送帧(MCU→电表):
FE FE FE FE 68 12 90 78 56 34 12 68 11 04 33 34 33 33 9A 16解析:前导4个FE → 68 → 地址12 90 78 56 34 12(对应表号123456789012)→ 68 → 控制码11 → 数据长度04 → 数据33 34 33 33(DI+0x33)→ 校验和9A → 结束16。 -
应答帧(电表→MCU):
FE FE FE FE 68 12 90 78 56 34 12 68 81 04 55 66 77 88 E0 16解析:控制码81(应答)→ 数据长度04 → 数据55 66 77 88(还原后为22 33 44 55)→ 校验和E0 → 结束16。
四、常见坑点与调试技巧(避坑指南)
4.1 常见坑点(90%的人会踩)
-
地址顺序错误:必须“低字节在前”,比如表号123456789012,转换为BCD后需反转,否则无法通信;
-
数据域偏移遗漏:发送时未+0x33、接收时未-0x33,导致解析出的数据乱码;
-
校验和计算错误:校验和从“第一个68”开始,而非从地址开始,遗漏会导致校验失败;
-
串口参数不匹配:波特率、校验位错误,导致接收不到应答或应答乱码;
-
前导码数量不足:前导码少于3个,可能导致电表无法同步帧起始,建议用4个FE。
4.2 调试技巧
-
用串口助手监听收发数据,确认帧格式是否正确;
-
若接收不到应答,先检查串口接线(TX/RX交叉连接)、电表电源;
-
若解析失败,打印接收缓冲区的原始数据,逐一核对帧结构、校验和;
-
可先固定电表地址,用工具发送标准帧,验证电表是否正常应答。
五、扩展说明(实战延伸)
-
数据标识扩展:本文仅提供3个常用DI,可参考DL/T 645-2007标准文档,添加更多参数(如无功电能、功率因数);
-
MCU适配:代码可适配STM32、51单片机、ESP32等,只需替换串口收发函数;
-
异常处理:可添加帧重发机制(接收超时后重发)、错误计数,提升通信稳定性;
-
写数据功能:如需设置电表参数,可参考读数据帧,修改控制码为0x13,补充写数据逻辑(需电表权限)。
下期预告:
原创不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连!有任何问题都可以在评论区留言,我会及时回复。
4037

被折叠的 条评论
为什么被折叠?



