DLT645协议是一种国内电表远传协议,与Modbus协议类似,采用请求应答式交互模型,采集器和电表一问一答进行通信,电气层可以是经典的RS485有线通信,也可以是非接触式红外无线通信。DLT645是国内电力行业通信标准,国网南网的表基本都支持,距今为止有两个版本,DL/T645-1997协议和DL/T645-2007协议,以下简称97协议和07协议,两者帧结构类似,新出厂的表具都会支持07协议,电表厂商为了向前兼容,支持07的表,会同时支持97协议,用两种协议都可以进行通信。
目录
第一部分 DL645协议 介绍
DL645协议,全称为DL/T645协议,是中国电力行业用于电能表与数据采集设备之间通信的标准协议。它主要用于远程抄表,采用主-从结构的半双工通信模式,硬件接口通常使用RS-485,协议帧报文和使用方法与Modbus RTU类似。DL645协议有两个主要版本:DL/T645-1997和DL/T645-2007,通常简称为97协议和07协议。新出厂的电表通常支持07协议,而电表厂商为了向前兼容,也会支持97协议。
DL645协议的数据帧由起始符、地址域、控制码、数据域长度、数据域、检验码和结束符组成。具体来说:
- 帧起始符:固定为0x68,标识一帧的开始。
- 地址域:表示电表地址,用于在485总线上识别多个设备。地址域由6个字节组成,低字节在前,高字节在后。
- 控制码:1个字节,用于指示操作类型,如读取数据的控制码为0x11,读取通信地址的控制码为0x13。
- 数据长度:1个字节,表示数据域的字节数。读取数据时长度不超过200字节,写数据时不超过50字节,长度为0表示无数据域。
- 数据域:变长,字节数由数据长度决定。在传输过程中,发送方需要在每个数据字节前加上0x33,接收方需要减去0x33。
- 校验码:1个字节,采用算术和进行校验,确保数据传输的准确性。
- 结束符:固定为0x16,标识一帧的结束。
DL645协议支持多种物理通信方式,包括RS-485总线通信、红外通信和无线通信等。它广泛应用于电力系统中的远程抄表系统、负荷控制系统以及电能数据管理系统,为电子式电能表与上位机之间的数据交互提供了标准化的接口和通信规程。通过统一的通信协议,DL/T 645-2007解决了不同厂商电能表之间的互操作性问题,确保了电力系统内电子式电能表的通信标准化,提升了智能电网的自动化水平,降低了电能表数据采集的复杂度。
第二部分 📖 如何学习并掌握DL645协议?
要学习并掌握DL645协议,可以遵循以下步骤:
- 理解DL645协议的基本概念
:DL645协议是中国电力行业用于电能表与数据采集设备之间通信的标准协议。它支持多种物理通信方式,如RS-485总线通信、红外通信和无线通信等。了解其与Modbus协议的相似性,以及它在智能电网中的作用和重要性。
- 获取官方文档
:访问中国电力企业联合会或相关电力行业标准网站,获取DL/T645-1997和DL/T645-2007协议的官方文档。这些文档详细描述了协议的数据格式、通信方式和命令集等内容。
- 学习数据帧格式
:熟悉DL645协议的数据帧格式,包括起始符、地址域、控制码、数据域长度、数据域、检验码和结束符。了解如何构建请求帧和解析应答帧。
- 实践操作
:使用支持DL645协议的电能表和数据采集设备进行实际操作。尝试使用红外读表器或RS485通信线连接电能表,并发送请求帧以读取电表数据。
- 参加专业论坛和社区
:加入相关的专业论坛和社区,如CSDN博客、中国工控网论坛等,参与讨论,解决学习过程中遇到的问题。这些平台上有许多经验丰富的专业人士分享的教程和案例分析,是学习DL645协议的宝贵资源。
- 使用仿真工具和库
:利用DL645MasterSimulator等仿真工具和库进行开发和测试。这些工具可以帮助你理解协议的具体实现,并提供调试软件以辅助学习。
- 编写代码
:学习如何使用编程语言(如C#)编写与DL645协议兼容的代码。了解如何发送请求帧、读取应答帧以及处理电表数据。
- 持续更新知识
:DL645协议可能会有更新和改进,因此需要定期查看最新的行业动态和技术文章,以保持知识的更新。
通过上述步骤,你可以系统地学习并掌握DL645协议。记住,实践是学习的关键,因此尝试亲自操作和编写代码是非常重要的。
第三部分 学习DL/T645-2007 原文件内容
第四部分 协议解析
1.DL/T645-2007协议格式
- 每条数据由:帧起始符、从站地址域、控制码、数据域长度、数据域、帧信息纵向校验码及帧结束符7个域组成,每部分由若干字节组成
-
一般在起始符前面会有0~4个FE不等,程序主站发送指令时,直接发送4个FE即可,但是从站回复不一定带几个FE或不带FE
68 AA AA AA AA AA AA 68 11 04 33 33 34 33 AE 16
帧起始符:68
地址域: AA AA AA AA AA AA
帧起始符:68
控制码:11
数据域长度:04
数据域:33 33 34 33
校验码:AE
结束符:16
2、地址域
地址域由6个字节组成,地址域传输时低字节在前,高字节在后。以下图智能电表为例,编号为220208005371,该厂家定义的电表地址为71 53 00 08 02 22。
3、控制码
这部分类似于Modbus的功能码,但是比功能码更加复杂,所携带的信息更多,程序根据这个控制码C判断后续的操作。
- 以主站读数据的功能码为例,根据上图可以得出D7~D0二进制为0001 0001,转化为16进制为:11
- 以主站写数据的功能码为例,根据上图可以得出D7~D0二进制为0001 0100,转化为16进制为:14
- 以从站读数据的功能码为例,根据上图可以得出D7~D0二进制为1001 0001,转化为16进制为:91
4、数据域长度
04,表示包含4个字节的数据。
5、数据域
- 数据域包括数据标识、密码、操作者代码、数据、帧序号等,其结构随控制码的功能而改变
- 数据域传输时低字节在前,高字节在后,传输时发送方按字节进行加33H处理,接收方按字节进行减33H处理
以读取电压为例:
发送 68 71 53 00 08 02 22 68 11 04 33 32 34 35 A3 16
发送的数据域为33 32 34 35,按减33H处理后为 00 FF 01 02,低位在前高位在后,对应协议标准文件中的电压数据块
应答 68 71 53 00 08 02 22 68 91 0A 33 32 34 35 8C 55 33 33 33 33 D6 16
应答数据域为33 32 34 35 8C 55 33 33 33 33
其中33 32 34 35为数据标识处理后为02 01 FF 00,即电压数据块
8C 55 33 33 33 33数据按减33H处理后得到59 22 00 00 00 00 ,转为十进制为22818,小数点往前移两位得到228.18V
常用的数据标识有:
电压数据:0201FF00
电流数据:0202FF00
瞬时视在功率:0205FF00
瞬时总有功功率:0203FF00
瞬时总无功功率:0204FF00
瞬时总视在功率:0205FF00
功率因数数据块:0206FF00
零线电流:02800001
正向有功总电能:0001FF00
反向有功总电能:0002FF00
组合无工1总电能:0003FF00
组合无工2总电能:0004FF00
正向有功总最大需量及发生时间:01010000
运行状态字:040005FF
电网频率:02800002
当前有功需量:02800004
6、校验码
从第一个帧起始符开始到校验码之前的所有各字节的模 256 的和,即各字节二进制算术和,不计超过 256 的溢出值。
第五部分 DLT645 java代码解析
环境说明:
1. 虚拟串口 VSPD
2.虚拟电表 645MeterV2.7.1.exe
3.CommMonitor 串口监控精灵(监控串口信息)
说明:用虚拟串口模拟串口,虚拟电表作为从站,程序作为主站(调式用串口监控也可以作为主站,向虚拟电表发送请求)
虚拟表 是com2 端口,地址0000001111,程序主站是com5 端口,以下是简短调试java 代码,后续再整理:
package com.yele.design2.test;
import com.fazecast.jSerialComm.SerialPort;
public class SerialPortExample {
public static void main(String[] args) throws Exception {
SerialPort comPort = SerialPort.getCommPort("COM5"); // 替换为你的串口名称
comPort.setBaudRate(9600); // 设置波特率
comPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 10, 1); // 设置超时
comPort.openPort(); // 打开串口
// 发送数据 68 11 11 00 00 00 00 68 11 04 33 33 34 33 D4 16
byte[] dataToSend = {0x68, 0x11, 0x11, 0x00, 0x00, 0x00, 0x00, 0x68, 0x11, 0x04, 0x33, 0x33, 0x34, 0x33, (byte) 0xD4, 0x16};
comPort.writeBytes(dataToSend, dataToSend.length); // 发送数据
// 接收数据
byte[] readBuffer = new byte[1000]; // 根据需要调整缓冲区大小
int numRead = comPort.readBytes(readBuffer, readBuffer.length);
String hex = bytesToHex(readBuffer, numRead);
if (numRead > 0) {
System.out.println("Received data: " + hex);
}
AgreementDemo.analysis(hex);
comPort.closePort(); // 关闭串口
}
private static String bytesToHex(byte[] bytes, int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(String.format("%02X ", bytes[i]));
}
return sb.toString();
}
}
package com.yele.design2.test;
import com.fazecast.jSerialComm.SerialPort;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public class DLT645Analysis {
public static SerialPort comPort;
public static void analysis(String command) throws Exception {
String[] commands = command.trim().split(" ");
List<String> list = new ArrayList<String>();
for (int i = 0; i < commands.length; i++) {
if (!commands[i].equalsIgnoreCase("FE")) {
list.add(commands[i]);
}
}
//解析报文格式
String[] newCommands = list.toArray(new String[list.size()]);
/*for (int i = 0; i < newCommands.length; i++) {
//System.out.println(Integer.parseInt(newCommands[newCommands.length-1]));
System.out.print(newCommands[i]+" ");
}*/
if (newCommands.length < 16 || newCommands.length > 22 || Integer.parseInt(newCommands[0]) != 68 || Integer.parseInt(newCommands[newCommands.length - 1]) != 16) {
System.err.print("非法帧,无法解析!");
return;
} else {
System.out.println("您的输入:" + command);
System.out.println("原始地址:" + list);
System.out.println("帧起始符:" + newCommands[0]);
System.out.println("电表地址:" + Byte.parseByte(newCommands[6]) + Byte.parseByte(newCommands[5]) + Byte.parseByte(newCommands[4]) + Byte.parseByte(newCommands[3]) + Byte.parseByte(newCommands[2]) + Byte.parseByte(newCommands[1]));
System.out.println("控制域:" + newCommands[8]);
System.out.println("数据域长度:" + newCommands[9]);
System.out.println("校验码:" + newCommands[newCommands.length - 2]);
System.out.println("停止位:" + newCommands[newCommands.length - 1]);
//int DTID=newCommands[newCommands.length - 2 - newCommands[9]];
//解析数据标识
List<String> list2 = new ArrayList<String>();
for (int i = 0; i < 4; i++) {
list2.add(Integer.toHexString(Integer.parseInt(newCommands[newCommands.length - 3 - i - (Integer.parseInt(newCommands[9], 16) - 4)], 16) - 51));
}
String[] DTID = list2.toArray(new String[list2.size()]);
StringBuffer sbr = new StringBuffer();
for (int i = 0; i < DTID.length; i++) {
if (DTID[i].length() == 1) {
DTID[i] = String.format("%02d", Integer.parseInt(DTID[i]));
} else if (DTID[i].length() == 8) {
DTID[i] = "FF";
}
sbr.append(DTID[i]);
}
//InputStream is=this.getClass().getClassLoader().getResourceAsStream("resource/config.properties");
//加载文件,取值
InputStream is = new BufferedInputStream(new FileInputStream("src/main/resources/config.properties"));
InputStreamReader isr = new InputStreamReader(is, "GBK");
Properties properties = new Properties();
try {
properties.load(isr);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(sbr.toString());
System.out.println("数据项名称:" + properties.getProperty(sbr.toString()));
//解析返回数据
if (newCommands.length > 16) {
int DTID0 = Integer.parseInt(DTID[0]);
int DTID1 = Integer.parseInt(DTID[1]);
List<String> list3 = new ArrayList();
for (int i = 0; i < Integer.parseInt(newCommands[9], 16) - 4; i++) {
list3.add(newCommands[newCommands.length - 3 - i]);
}
String[] data = list3.toArray(new String[list3.size()]);
//System.out.println((this.DataFormat(data)).toString());
long num = Long.parseLong((DataFormat(data)).toString());
//System.out.println((this.DataFormat(data)).toString());
BigDecimal bigDecimal = new BigDecimal(num);
if (DTID0 == 2 && DTID1 == 1 && !String.valueOf(DTID[2]).equals("FF")) { //电压0.1v
//new BigDecimal()
System.out.println(properties.getProperty(sbr.toString()) + ":" + bigDecimal.multiply(new BigDecimal("0.1")) + "v");
} else if (DTID0 == 2 && DTID1 == 2) { //电流0.001A
System.out.println(properties.getProperty(sbr.toString()) + ":" + bigDecimal.multiply(new BigDecimal("0.001")) + "A");
} else if ((DTID0 == 2 && DTID1 == 3) || (DTID0 == 2 && DTID1 == 4) || (DTID0 == 2 && DTID1 == 5)) { //有无功功率0.0001
System.out.println(properties.getProperty(sbr.toString()) + ":" + bigDecimal.multiply(new BigDecimal("0.0001")));
} else if (DTID0 == 2 && DTID1 == 6) { //功率因数0.001
System.out.println(properties.getProperty(sbr.toString()) + ":" + bigDecimal.multiply(new BigDecimal("0.001")));
} else if ((DTID0 == 0 && DTID1 == 0) || (DTID0 == 0 && DTID1 == 1) || (DTID0 == 0 && DTID1 == 2) || (DTID0 == 0 && DTID1 == 3) || (DTID0 == 0 && DTID1 == 4) || (DTID0 == 0 && DTID1 == 5) || (DTID0 == 0 && DTID1 == 6) || (DTID0 == 0 && DTID1 == 7) || (DTID0 == 0 && DTID1 == 8)) { //有无功总电能、四象限无功总电能0.01
System.out.println(properties.getProperty(sbr.toString()) + ":" + bigDecimal.multiply(new BigDecimal("0.01")));
} else if (DTID0 == 2 && DTID1 == 1 && String.valueOf(DTID[2]).equals("FF")) { //电压数据块
System.out.println(String.valueOf(num));
System.out.println(String.valueOf(num).substring(0, 4));
System.out.println(String.valueOf(num).substring(4, 8));
System.out.println(String.valueOf(num).substring(8));
System.out.println("C相电压" + new BigDecimal(String.valueOf(num).substring(0, 4)).multiply(new BigDecimal("0.1")));
System.out.println("B相电压" + new BigDecimal(String.valueOf(num).substring(4, 8)).multiply(new BigDecimal("0.1")));
System.out.println("A相电压" + new BigDecimal(String.valueOf(num).substring(8)).multiply(new BigDecimal("0.1")));
} else {
System.out.println(properties.getProperty(sbr.toString()) + ":" + num);
}
}
}
}
public static StringBuffer DataFormat(String data[]) {
StringBuffer sbr = new StringBuffer();
for (int i = 0; i < data.length; i++) {
String data1 = String.valueOf(Integer.parseInt(data[i].substring(0, 1), 16) - 3);
String data2 = String.valueOf(Integer.parseInt(data[i].substring(1), 16) - 3);
sbr.append(data1);
sbr.append(data2);
}
return sbr;
}
}
运行结果: