DLT645-2007 协议快速入门
@
DLT645-2007 协议快速入门
1. 什么是DLT645-2007 协议
DLT645目前主要使用的有两个版本,DLT645-1997 和 DLT645-2007。DLT645协议是一种 问答式(主从式)通信规约 。在这种通信模式下,通常存在一个主站和一个或多个从站。主站负责发起通信请求,从站则根据主站的请求提供相应的响应。
问答式规约的通信过程通常包括以下几个步骤:
- 主站发起请求:主站发送一个请求报文给特定的从站,请求报文中包含了需要从站执行的操作,如读取点能量、读实时参数等
- 从站接收请求:从站接收到主站的请求后,根据请求的内容进行处理。
- 从站准备响应:从站根据请求的内容准备相应的数据,并构造一个响应报文。
- 从站发送响应:从站将构造好的响应报文发送回主站。
- 主站接收响应:主站接收到从站的响应后,根据响应内容进行响应处理。
DLT645-2007 是中国电力行业标准,全称为《多功能电能表通信协议》,主要用于电力系统中电能表的通信。这个标准定义了电能表与数据终端设备之间进行数据交换时的物理层、链路层以及应用层的通信协议。DLT645-2007协议采用主-从结构的半双工通信模式,硬件接口通常使用 RS-485.
2. 帧格式
DLT645-2007协议为主-从结构的半双工通信模式。手持单元或其他数据终端为主站,多功能电能表为从站。每个多功能电能表均有各自的地址编码。通信链路的建立与接触均由主站发出的信息帧来控制。每帧由起始符、从站地址域、控制码、数据域长度、数据域、帧信息纵向校验码以及帧结束符7个域组成。每部分由若干十六进制码组成。
2.1 帧起始符
DLT645协议的数据帧每帧的开始都固定为 0x68,作为数据的起始符方便接收方做数据解析。
2.2 地址域
地址域为上图的 A0-A5,由6个字节构成。地址域是用来标识电表地址,低位在前,高位在后;在485总线上可能挂多个645设备,要找到指定的设备,必须要根据设备的地址查找。每台设备出厂会有自己的地址,也可以修改设备的通信地址。如下图,设备地址就是 220514030093,在传输时由于低位在前,高位在后,所以实际传输时地址为 930003140522.
需注意:
- 通信地址 999999999999H 为广播地址,只针对特殊命令有效,如广播校时、广播冻结等。广播命令不要求从站应答
- 地址域支持缩位寻址,即从若干低位起,剩余高位补AAH作为通配符进行读表操作,从站应答帧的地址域返回实际通信地址
- 地址域传输时字节低字节在前,高字节在后
2.3 控制码
控制码长度为1个字节,控制码需转成8位的二进制码来解析命令,如0x11,对应的8位二进制码就是 00010001,对应下图则是表示
-
D7(0) :主站发出的命令帧
-
D6(0):总站正确应答
-
D5(0):无后续数据帧
-
D4-D0(10001):读数据
3.4 数据长度
1个字节,表示数据域的字节数。读数据时 L≤200,写数据时L≤50,L--=0表示无数据域
3.5 数据域
数据域包括数据标识、密码、、操作者代码、数据、帧序号等,其结构随控制码的功能而改变。传输时按字节进行加33H处理,接收方按字节进行减33H处理。
*数据标识
数据标识编码用四个字节区分不同数据项,四字节分别用DI3、DI2、DI1和DI0代表,每字节采用十六进制编码。数据类型分为七类:电能量、最大需量及发生时间、变量、时间记录、参变量、冻结量、负荷记录
-
DI3
DI3标识符 对应数据类型 00 电能量 01 最大需量及发生时间 02 变量数据 (遥测等) 03 事件记录 04 参变量数据 05 冻结量 06 负荷记录
举例
发送端
需要发送 0x02 0x01 0x01 0x00
对应数据标识 DI3 DI2 DI1 DI0
需要发送的数据域 0x00 0x01 0x01 0x02 (发送时,数据标识低位在前,高位在后)
对应数据标识 DI0 DI1 DI2 DI3 (发送时,数据标识低位在前,高位在后)
实际发送数据域: 0x33 0x34 0x34 0x35 (发送端数据域中的数据需作+33H的操作)
接收端返回数据: 0x33 0x34 0x34 0x35 0x73 0x55 0x76 0x55 0x78 0x55
实际返回的数据: 0x00 0x01 0x01 0x02 0x40 0x22 0x43 0x22 0x45 0x22 (高位在后,接收端数据域中的数据需作-33H的操作)
拼接好高低位的数据标识 0x02 0x01 0x01 0x00
拼接好高低位的数据(假设数据格式为XXX.X) ,则返回数数据为
224.5(2245,格式转为XXX.X)
224.3(2243,格式转为XXX.X)
224.0(2240,格式转为XXX.X)
2.6 校验码 CS
占2个字节,从第一个帧起始符开始到校验码之前的所有各字节的模 256 的和,即各字节二进制算术和,不计超过 256 的溢出值。
/**
* @description: 计算校验码
* @author WXP
* @date 2024/10/11 14:44
* @version 1.0
*/
public static byte calculateChecksum(byte[] data) {
int count = 0;
int len = data.length - 2;
for (int i = 0; i < len; i++) {
count += data[i];
}
byte b = (byte) (count & 0xFF); // 0xFF 等于二进制 1111 1111 只取二进制后8位,既不计超过256的溢出值
return b;
}
2.7 结束符
占一个字节,固定为 16H,标识一帧信息的结束。
2.8 传输事项
-
前导字节:在主站发送帧信息前,先发送4个字节 FEH,以唤醒接收方
-
传输次序:所有数据项均先传送低位字节,后传送高位字节。
-
传输响应:每次通信都是由主站向按信息帧地址域选择的从站发出请求命令帧开始,被请求的从站接收到命令后做出的响应
-
差错控制:字节校验为偶校验,帧校验为纵向信息校验和,接收方无论检测到偶校验出错或纵向信息校验和出错,均放弃该信息帧,不予响应
3. 报文解析
实际设备厂家都是定制化的协议,根据厂家的设备协议文档进行解析,主要区别在于数据域上,会定义自己的一套规则。下面以常见的读与写数据为例对报文进行解析,
3.1 读数据
发送:68 11 11 11 11 11 11 68 11 04 33 32 34 35 19 16
- 68:帧起始符,标识这是一帧信息的开始
- 11 11 11 11 11 11:电表地址,低位在前,高位在后,实际地址为反过来
- 68:帧起始符,标识前面地址信息结束
- 11:控制码,对应二进制为 00010001,按前面控制码规则为:主站发送的命令帧(0),从站正确应答(0),无后续数据帧(0),读电表数据(10001)
- 04:数据域字节数,此处数据域字节数为4
- 33 32 34 35:数据域,此为数据标识,高位在前,低位在后,传输时都进行了加33H的操作,实际数据为 02 01 FF 00
- 19:校验码,通过前面算法可以算出
- 16:结束符,标识一帧信息的结束
接收:68 11 11 11 11 11 11 68 91 0A 33 32 34 35 C8 55 CB 55 33 56 65 16
-
68:帧起始符,标识这是一帧信息的开始
-
11 11 11 11 11 11:电表地址,低位在前,高位在后,实际地址为反过来
-
68:帧起始符,标识前面地址信息结束
-
91:控制码,对应二进制为 10010001,按前面控制码规则为:从站发送的命令帧(1),从站正确应答(0),无后续数据帧(0),读电表数据(10001)
-
0A:数据域字节数,此处数据域字节数为10
-
33 32 34 35 C8 55 CB 55 33 56:数据域,高位在前,低位在后,传输时都进行了加33H的操作
-
33 32 34 35 为数据标识,-33H并调整高低位后为 02 01 FF 00
-
C8 55 CB 55 33 56 为返回的数据,-33H并调整高低位后为 23 00 22 98 22 95,按(XXX.X)的数据格式则为 230.0 229.8 229.5
-
-
65:校验码,通过前面算法可以算出
-
16:结束符,标识一帧信息的结束
3.2 写数据
发送:68 11 11 11 11 11 11 68 14 0E 36 45 33 37 35 33 33 33 44 44 44 44 38 34 87 16
-
68:帧起始符,标识这是一帧信息的开始
-
11 11 11 11 11 11:电表地址,低位在前,高位在后,实际地址为反过来
-
68:帧起始符,标识前面地址信息结束
-
14:控制码,对应二进制为 00010001,按前面控制码规则为:主站发送的命令帧(0),从站正确应答(0),无后续数据帧(0),写电表数据(10100)
-
0E:数据域字节数,此处数据域字节数为14
-
36 45 33 37 35 33 33 33 44 44 44 44 38 34:数据域,此为数据标识,高位在前,低位在后,传输时都进行了加33H的操作,实际数据为 03 12 00 04 02 00 00 00 11 11 11 11 05 01
-
36 45 33 37:数据标识符,对应反转后控制码为 04 00 12 03
-
35 33 33 33
-
35:密码权限,这里为 02
-
33 33 33 :密码买这里为 000000
-
44 44 44 44:操作者代码,这里为 111111
-
38 34:修改后的数据,这里为 01 05
-
-
87:校验码,通过前面算法可以算出
-
16:结束符,标识一帧信息的结束
回复:68 11 11 11 11 11 11 68 94 00 CA 16
- 68:帧起始符,标识这是一帧信息的开始
- 11 11 11 11 11 11:电表地址,低位在前,高位在后,实际地址为反过来
- 68:帧起始符,标识前面地址信息结束
- 94:控制码,对应二进制为 00010001,按前面控制码规则为:从站发送的命令帧(1),从站正确应答(0),无后续数据帧(0),写数据(10100)
- 00:数据域字节数,此处数据域字节数为00
- CA:校验码,通过前面算法可以算出
- 16:结束符,标识一帧信息的结束
4. 代码实例
此为Java实现的一个Demo,需导入 jSerialComm 串口通信的包。若接收的信息不完整,则调整接收报文前的线程睡眠时间,或者更改成用while循环监听串口数据流的回复。
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.9.2</version>
</dependency>
package org.xp;
import com.fazecast.jSerialComm.SerialPort;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author sam
* @version 1.0
* @description: TODO
* @date 2024/10/11 10:16
*/
public class DLT645Demo {
private final static String testData = "68111111111111681104333434358616";
// 设备通讯地址
private final static String address = "111111111111";
// 控制码
private static final int controlCode = 0x11;
private static final int updateControlCode = 0x14;
// 数据长度
private static final int dataLength = 0x4;
private static final int updateDataLength = 0x4;
// 控制码中从机回复成功的功能码
private static final String[] successCode = {"00000", "10001", "10010", "10011", "10100", "10101", "01000", "10110", "10111", "11000", "11001", "11010"
, "11011", "11100", "11101", "00011"};
// 帧起始符
private static final int startCharacter = 0x68;
// 结束符
private static final int endCharacter = 0x16;
// 数据加减操作
private static final int codeChange = 0x33;
public static void main(String[] args) {
// 设置通信端口、波特率、数据大小、校验位、停止位
SerialPort serialPort = SerialPort.getCommPort("COM3");
serialPort.setBaudRate(2400); // 此电表波特率为2400
serialPort.setNumDataBits(8);
serialPort.setParity(SerialPort.EVEN_PARITY); // 偶校验
serialPort.setNumStopBits(1);
// 初始化发送数据
ArrayList<Integer> dataList = new ArrayList<>();
/* dataList.add(0x0);
dataList.add(0x0);
dataList.add(0x1);
dataList.add(0x0);
dataList.add(0x86);
dataList.add(0x1);
dataList.add(0x0);
dataList.add(0x0);*/
// byte[] bytes = buildDLT645Request("930003140522", 0x91, 0x8, dataList);
dataList.add(0x00);
// dataList.add(0x01);
dataList.add(-0x01);
dataList.add(0x01);
dataList.add(0x02);
byte[] request = {};
// 打开串口,传输数据
if (serialPort.openPort()) {
// 发送
request = buildDLT645Request(address, controlCode, dataLength, dataList);
System.out.println("发送的报文:" + Arrays.toString(request));
StringBuilder hexString = new StringBuilder();
for (byte b : request) {
hexString.append(toHex(b)).append(" ");
}
System.out.println(hexString);
System.out.println(toHex(request[request.length - 2]));
serialPort.writeBytes(request, request.length);
}
// 睡眠 300 毫秒,等待从机响应
try {
Thread.sleep(300); // 若传输回来的数据不完整,需调整睡眠时间,回传的报文越长,需要睡眠的时间也要越长
} catch (Exception e) {
e.printStackTrace();
}
// 接收
byte[] bytes = new byte[255];
serialPort.readBytes(bytes, bytes.length);
System.out.println("接收的报文:" + Arrays.toString(bytes));
// 将 接收的数据放进新的合适大小的byte数组,去掉开头4个FE Integer.parseInt(toHex(bytes[9])) 数据域长度
byte[] response = Arrays.copyOfRange(bytes, 4, 4 + 10 + Integer.parseInt(toHex(bytes[13]), 16) + 2);
StringBuilder hexString2 = new StringBuilder();
for (byte b : response) {
hexString2.append(toHex(b)).append(" ");
}
System.out.println("报文:" + hexString2);
if (!hexString2.substring(hexString2.length() - 3, hexString2.length() - 1).equals("16")) {
System.out.println("结束符的数据有误,未接收到结束符");
}
// 处理报文
String[] split = splitData(receiveMessage(response));
// 提取数据 高位在后 低位在前
System.out.println("拆分后数据:" + Arrays.toString(split));
System.out.println("A相电压:" + (Integer.parseInt(split[1]) * 10 + Float.parseFloat(split[0]) / 10));
System.out.println("B相电压:" + (Integer.parseInt(split[3]) * 10 + Float.parseFloat(split[2]) / 10));
System.out.println("C相电压:" + (Integer.parseInt(split[5]) * 10 + Float.parseFloat(split[4]) / 10));
/**
* 写数据
*/
System.out.println("\n----------------写数据操作---------------------\n");
ArrayList<Integer> updateDateList = new ArrayList<>();
// 数据标识
updateDateList.add(0x03);
updateDateList.add(0x12);
updateDateList.add(0x00);
updateDateList.add(0x04);
// 设备密码
updateDateList.add(0x02);
updateDateList.add(0x00);
updateDateList.add(0x00);
updateDateList.add(0x00);
// 操作者代码
updateDateList.add(0x11);
updateDateList.add(0x11);
updateDateList.add(0x11);
updateDateList.add(0x11);
// 数据
updateDateList.add(0x05);
updateDateList.add(0x01);
// 打开串口发送数据
if (serialPort.openPort()) {
// 发送
request = buildDLT645Request(address, updateControlCode, updateDateList.size(), updateDateList);
System.out.println("发送的报文:" + Arrays.toString(request));
StringBuilder hexString = new StringBuilder();
for (byte b : request) {
hexString.append(toHex(b)).append(" ");
}
System.out.println(hexString);
System.out.println(toHex(request[request.length - 2]));
serialPort.writeBytes(request, request.length);
}
// 睡眠 300 毫秒,等待从机响应
try {
Thread.sleep(300); // 若传输回来的数据不完整,需调整睡眠时间,回传的报文越长,需要睡眠的时间也要越长
} catch (Exception e) {
e.printStackTrace();
}
// 接收
bytes = new byte[255];
serialPort.readBytes(bytes, bytes.length);
System.out.println("接收的报文:" + Arrays.toString(bytes));
// 将 接收的数据放进新的合适大小的byte数组,去掉开头4个FE Integer.parseInt(toHex(bytes[9])) 数据域长度
response = Arrays.copyOfRange(bytes, 4, 4 + 10 + Integer.parseInt(toHex(bytes[13]), 16) + 2);
hexString2 = new StringBuilder();
for (byte b : response) {
hexString2.append(toHex(b)).append(" ");
}
System.out.println("报文:" + hexString2);
if (!hexString2.substring(hexString2.length() - 3, hexString2.length() - 1).equals("16")) {
System.out.println("结束符的数据有误,未接收到结束符");
}
// 关闭串口
serialPort.closePort();
}
/**
* @description: 将数据域中的数据进行提取
* @param: message
* @return:
* @author WXP
* @date: 2024/10/16 12:01
*/
private static String[] splitData(StringBuilder message) {
String dataMessage = message.toString().split(";")[1];
return dataMessage.substring(12, dataMessage.length() - 1).split(" ");
}
/**
* @description: byte 字符转换成十六进制字符串
* @param: b
* @return:
* @author WXP
* @date: 2024/10/14 12:14
*/
private static String toHex(byte b) {
// Convert byte to unsigned int and then to hex string
return String.format("%02X", b & 0xFF);
}
/**
* @description: 构建645请求报文
* @param: address
* controlCode
* dataLength
* dataList
* @return:
* @author WXP
* @date: 2024/10/15 10:29
*/
private static byte[] buildDLT645Request(String address, int controlCode, int dataLength, List<Integer> dataList) {
// 数据长度校验
if (dataLength < 0) {
return new byte[0];
}
if (dataList == null && dataLength > 0) {
return new byte[0];
}
if (dataList != null && (dataList.isEmpty() && dataLength > 0 || dataList.size() != dataLength)) {
return new byte[0];
}
byte[] bytes = new byte[dataList != null && !dataList.isEmpty() ? 12 + dataList.size() : 12];
// 帧起始符
bytes[0] = (byte) startCharacter;
// 增加地址
bytes[1] = (byte) Integer.parseInt(address.substring(0, 2), 16);
bytes[2] = (byte) Integer.parseInt(address.substring(2, 4), 16);
bytes[3] = (byte) Integer.parseInt(address.substring(4, 6), 16);
bytes[4] = (byte) Integer.parseInt(address.substring(6, 8), 16);
bytes[5] = (byte) Integer.parseInt(address.substring(8, 10), 16);
bytes[6] = (byte) Integer.parseInt(address.substring(10, 12), 16);
// 帧起始符
bytes[7] = (byte) startCharacter;
// 控制符
bytes[8] = (byte) controlCode;
// 数据长度
bytes[9] = (byte) dataLength;
// 数据域 发送端需 + 0x33 处理
if (dataLength > 0) {
for (int i = 0, j = 10; i < dataList.size(); i++, j++) {
bytes[j] = (byte) (dataList.get(i) + codeChange);
}
}
// 校验码
bytes[bytes.length - 2] = calculateChecksum(bytes);
// 结束符
bytes[bytes.length - 1] = (byte) endCharacter;
return bytes;
}
/**
* @description: 接收从站回传报文
* @param: bytes
* @return:
* @author WXP
* @date: 2024/10/15 10:28
*/
private static StringBuilder receiveMessage(byte[] bytes) {
System.out.println("-----------接收------------------");
// 存储解析好后的十进制数据,每一部分用 "," 隔开,存在 ";"则代表有回复的数据,";" 到 ";" 之间的为数据域,每个数据采用 空格隔开
StringBuilder message = new StringBuilder();
// 帧起始符
System.out.println("帧起始符:" + toHex(bytes[0]));
message.append(toHex(bytes[0])).append(",");
// 地址解析
byte[] addressArray = new byte[6];
for (int i = 0; i < 6; i++) {
addressArray[i] = bytes[6 - i];
}
StringBuilder address = new StringBuilder();
for (byte b : addressArray) {
address.append(toHex(b));
}
System.out.println("地址为:" + address);
message.append(address).append(",");
// 帧起始符
System.out.println("帧起始符:" + toHex(bytes[7]));
message.append(toHex(bytes[7])).append(",");
// 控制符
parseControl(bytes[8]);
message.append(toHex(bytes[8])).append(",");
// 数据长度
// 检验报文中的数据长度和数据长度位是否一致
System.out.println("数据长度 :" + Integer.toHexString(bytes[9]));
message.append(toHex(bytes[9])).append(",");
// 数据域 发送端需 + 0x33 处理
byte[] dataArray = new byte[bytes.length - 12];
StringBuilder sb = new StringBuilder();
if (!toHex(bytes[9]).equals("00")) {
message.append(";");
for (int i = 0, j = 10; j < bytes.length - 2; i++, j++) {
// -33H
dataArray[i] = (byte) (bytes[j] - codeChange);
sb.append(toHex(dataArray[i])).append(" ");
}
System.out.println("数据域:" + sb);
System.out.println("数据标识:" + sb.substring(0, 12));
System.out.println("实际数据:" + sb.substring(12, sb.length() - 1));
message.append(sb).append(";");
message.append(sb).append(",");
}
// 校验码
System.out.println("校验码:" + toHex(bytes[bytes.length - 2]));
message.append(toHex(bytes[bytes.length - 2])).append(",");
if (toHex(bytes[bytes.length - 2]).equals(toHex(calculateChecksum(bytes)))) {
System.out.println("校验无误");
}
// 结束符
System.out.println("结束符" + toHex(bytes[bytes.length - 1]));
message.append(toHex(bytes[bytes.length - 1])).append(",");
return message;
}
// 将十六进制字符串转换为二进制字符串
public static String hexToBinary(String hex) {
StringBuilder binary = new StringBuilder();
// 遍历十六进制字符串的每一个字符
for (int i = 0; i < hex.length(); i++) {
// 获取当前字符的十六进制值(0-9, A-F)
char c = hex.charAt(i);
int hexValue = Character.digit(c, 16);
// 将十六进制值转换为4位二进制字符串,并添加到结果中
String binaryString = Integer.toBinaryString(hexValue);
// 计算需要补0的个数, Integer.toBinaryString(hexValue) 不会进行自动补0,比如4是 100,拼接前面的字符串后就是 xxxx100,正确应该是 xxxx0100
int zero = 4- binaryString.length();
if (zero != 0){
for (int j = 0; j < zero; j++) {
binaryString = "0" + binaryString;
}
}
binary.append(binaryString);
}
return binary.toString();
}
/**
* @description: 计算校验码
* @author WXP
* @date 2024/10/11 14:44
* @version 1.0
*/
public static byte calculateChecksum(byte[] data) {
int count = 0;
int len = data.length - 2;
for (int i = 0; i < len; i++) {
count += data[i];
}
byte b = (byte) (count & 0xFF);
return b;
}
/**
* 解析控制码
*/
public static void parseControl(byte control) {
// 校验控制符是否为从站应答成功
String binary = hexToBinary(toHex(control));
boolean flag = false;
for (String s : successCode) {
if (binary.substring(3).equals(s)) {
flag = true;
break;
}
}
if (flag) {
System.out.println("控制码:" + toHex(control));
// D0-D4:功能码
String function = Integer.toBinaryString(control & 0x1F);
// D5:后续帧标志
String next = String.format("%d", control >> 5 & 0x01);
// D6:从站应答标志
String response = String.format("%d", control >> 6 & 0x01);
// D7:传输方向标志
String direction = String.format("%d", control >> 7 & 0x01);
// 一起解析
System.out.println("功能码:" + function + ",后续帧标志:" + next + ",从站应答标志:" + response + ",传输方向标志:" + direction);
if ("1".equals(response)) {
System.out.println("从站异常应答,程序退出");
System.exit(0);
}
} else {
System.out.println("控制码错误,程序退出");
System.exit(0);
}
}
}
5. 报文解析工具
645报文解析小工具,可以方便提取报文和理解报文。下面是百度网盘连接以及工具界面
链接:https://pan.baidu.com/s/1oMu9lLO5e__HHbYWnzEkqw
提取码:ojzw
主机发送的报文解析:68 11 11 11 11 11 11 68 11 04 33 32 34 35 19 16
从机回复的报文解析:68 11 11 11 11 11 11 68 91 0A 33 32 34 35 B9 55 BC 55 C4 55 D7 16