【工业总线-11】DL/T 645-2007 多功能电能表通信协议

前言:在物联网、智慧电力项目中,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

关键说明(嵌入式编程必看):

  1. 前导码:FE字节,数量可自定义(通常3-4个),用于同步通信时序;

  2. 地址域:6字节,采用BCD码编码,低字节在前、高字节在后(重点坑点);

  3. 控制码:核心指令,常用0x11(读数据)、0x81(读数据应答);

  4. 数据域:每个字节发送前需+0x33,接收后需-0x33还原(协议加密机制);

  5. 校验和:从第一个68字节开始,到数据域最后一个字节,所有字节累加求和(无进位);

  6. 结束符:固定为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 抓包示例(串口助手/逻辑分析仪)

以“读正向有功总电能”为例,抓包数据参考(十六进制):

  1. 发送帧(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。

  2. 应答帧(电表→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%的人会踩)

  1. 地址顺序错误:必须“低字节在前”,比如表号123456789012,转换为BCD后需反转,否则无法通信;

  2. 数据域偏移遗漏:发送时未+0x33、接收时未-0x33,导致解析出的数据乱码;

  3. 校验和计算错误:校验和从“第一个68”开始,而非从地址开始,遗漏会导致校验失败;

  4. 串口参数不匹配:波特率、校验位错误,导致接收不到应答或应答乱码;

  5. 前导码数量不足:前导码少于3个,可能导致电表无法同步帧起始,建议用4个FE。

4.2 调试技巧

  1. 用串口助手监听收发数据,确认帧格式是否正确;

  2. 若接收不到应答,先检查串口接线(TX/RX交叉连接)、电表电源;

  3. 若解析失败,打印接收缓冲区的原始数据,逐一核对帧结构、校验和;

  4. 可先固定电表地址,用工具发送标准帧,验证电表是否正常应答。

五、扩展说明(实战延伸)

  1. 数据标识扩展:本文仅提供3个常用DI,可参考DL/T 645-2007标准文档,添加更多参数(如无功电能、功率因数);

  2. MCU适配:代码可适配STM32、51单片机、ESP32等,只需替换串口收发函数;

  3. 异常处理:可添加帧重发机制(接收超时后重发)、错误计数,提升通信稳定性;

  4. 写数据功能:如需设置电表参数,可参考读数据帧,修改控制码为0x13,补充写数据逻辑(需电表权限)。


下期预告:

原创不易,如果本文对你有帮助,欢迎点赞、收藏、关注三连!有任何问题都可以在评论区留言,我会及时回复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值