1、协议
电表有个电力行业推荐标准《DLT645-2007多功能电能表通信协议》,电表都支持,通过该协议读取数据,不同的电表不需要考虑编码格式、数据地址、高低位转换等复杂情况,统一采集。
不方便的地方在于这个协议定义得有点小复杂,自己带有各种特殊性定义,编程时一堆的坑。
不少电表可以同时支持DLT645-2007和MODBUS RTU协议,但MODBUS协议在不同的电表中,地址都是不同的,需要查阅手册才能搞定。
DLT645不同的数据需要发送独立的请求,而Modbus数据地址连接的可以一次读取,各有所长和优势。
2、协议定义和报文
协议定义和报文,在协议文档中有详细说明,主要部分节选如下:
3、数据域定义
协议中对每个数据都定义了明确的地址和解析格式,如:
比如 00 00 00 00表示组合有功总电能。同一组的可以使用FF通配,表示读取整块的数据,00 00 FF 00表示一次读取 00 00 00 00 ~ 00 00 3F 00的所有数据。
但需要注意的事,不是所有电表都支持块数据读取。
4、编码验证
public class DLT645_2007
{
public enum CommandType
{
ReadData = 0x11,
WriteData = 0x14,
ReadAddress = 0x13,
WriteAddress = 0x15
}
public class Dlt645Addr
{
public string dataIndentifier;
public int dataLen;
public float divisor = 1.0f;
public string unit;
public string dataItemName;
public string dataName;
public string PascalType;
public Dlt645Addr(string dataIndentifier, int dataLen, float divisor, string unit, string dataItemName, string dataName= "", string PascalType = "FLOAT")
{
this.dataIndentifier = dataIndentifier;
this.dataLen = dataLen;
this.divisor = divisor;
this.unit = unit;
this.dataItemName = dataItemName;
this.dataName = dataName;
this.PascalType = PascalType;
}
}
public Dlt645Addr []addrList =
{
//电能量
new Dlt645Addr("00010000",4,100,"V" ,"正向有功总电量","Forth_Have_Power_Total"),// R4
new Dlt645Addr("00010100",4,100,"V" ,"正向有功尖电量","Forth_Have_Power_SPIKE"),// R4 以上四个名称待核实
new Dlt645Addr("00010200",4,100,"V" ,"正向有功峰电量","Forth_Have_Power_PEAK"),// R4
new Dlt645Addr("00010300",4,100,"V" ,"正向有功平电量","Forth_Have_Power_FLAT"),// R4
new Dlt645Addr("00010400",4,100,"V" ,"正向有功谷电量","Forth_Have_Power_VALLEY"),// R4
//变量数据
new Dlt645Addr("02010100",2, 10,"V","A项电压","Phase_A_Volt"),// R2
new Dlt645Addr("02010200",2, 10,"V","B项电压","Phase_B_Volt"),// R2
new Dlt645Addr("02010300",2, 10,"V","C项电压","Phase_C_Volt"),// R2
new Dlt645Addr("02020100",3,1000,"A","A项电流","Phase_A_Elec"),// R3
new Dlt645Addr("02020200",3,1000,"A","B项电流","Phase_B_Elec"),// R3
new Dlt645Addr("02020300",3,1000,"A","C项电流","Phase_C_Elec"),// R3
new Dlt645Addr("02030000",3,1000,"kW","瞬时总有功率功率","Instant_Have_Power_Rate_Total"),// R3
new Dlt645Addr("02030000",3,1000,"kW","瞬时A相有功率功率","Instant_Phase_A_Have_Power_Rate"),// R3
new Dlt645Addr("02030000",3,1000,"kW","瞬时B相有功率功率","Instant_Phase_B_Have_Power_Rate"),// R3
new Dlt645Addr("02030000",3,1000,"kW","瞬时C相有功率功率","Instant_Phase_C_Have_Power_Rate"),// R3
new Dlt645Addr("02040000",3,1000,"kwar","瞬时总无功率功率","Instant_None_Power_Rate_Total"),// R3
new Dlt645Addr("02040000",3,1000,"kwar","瞬时A相无功率功率","Instant_Phase_A_None_Power_Rate"),// R3
new Dlt645Addr("02040000",3,1000,"kwar","瞬时B相无功率功率","Instant_Phase_B_None_Power_Rate"),// R3
new Dlt645Addr("02040000",3,1000,"kwar","瞬时C相无功率功率","Instant_Phase_C_None_Power_Rate"),// R3
new Dlt645Addr("02050000",3,1000,"kVA","瞬时总视在功率","Instant_Apparent_Power_Rate_Total"),// R3
new Dlt645Addr("02050000",3,1000,"kVA","瞬时A相视在功率","Instant_Phase_A_Apparent_Power_Rate"),// R3
new Dlt645Addr("02050000",3,1000,"kVA","瞬时B相视在功率","Instant_Phase_B_Apparent_Power_Rate"),// R3
new Dlt645Addr("02050000",3,1000,"kVA","瞬时C相视在功率","Instant_Phase_C_Apparent_Power_Rate"),// R3
new Dlt645Addr("02060000",2,1000,"","总功率因数","Power_Rate_Factor_Total"),// R2
new Dlt645Addr("02060100",2,1000,"","A相功率因数","Phase_A_Power_Rate_Factor"),// R2
new Dlt645Addr("02060200",2,1000,"","B相功率因数","Phase_B_Power_Rate_Factor"),// R2
new Dlt645Addr("02060300",2,1000,"","C相功率因数","Phase_C_Power_Rate_Factor"),// R2
new Dlt645Addr("02070100",2, 10,"V","A相相角","Phase_A_Angle"),// R2
new Dlt645Addr("02070200",2, 10,"V","B相相角","Phase_B_Angle"),// R2
new Dlt645Addr("02070300",2, 10,"V","C相相角","Phase_C_Angle"),// R2
new Dlt645Addr("02080100",2, 100,"%","A相电压波形失真度","Phase_A_Volt_Waveform_Distortion"),// R2
new Dlt645Addr("02080200",2, 100,"%","B相电压波形失真度","Phase_B_Volt_Waveform_Distortion"),// R2
new Dlt645Addr("02080300",2, 100,"%","C相电压波形失真度","Phase_C_Volt_Waveform_Distortion"),// R2
new Dlt645Addr("02090100",2, 100,"%","A相电流波形失真度","Phase_A_Elec_Waveform_Distortion"),// R2
new Dlt645Addr("02090200",2, 100,"%","B相电流波形失真度","Phase_B_Elec_Waveform_Distortion"),// R2
new Dlt645Addr("02090300",2, 100,"%","C相电流波形失真度","Phase_C_Elec_Waveform_Distortion")// R2
//最大需量
//事件记录数
//负荷记录
//参变量
//安全认证
};
Dictionary<string,Dlt645Addr> dictAddr = new Dictionary<string, Dlt645Addr> ();
//是否需要唤醒 ,0xFE
private bool isNeedWakeUp = false;
// 创建串口对象
SerialPort serialPort = null;
int rxCount = 0;
byte[] rxdata = new byte[512];
Stopwatch swRead = new Stopwatch ();
public DLT645_2007()
{
foreach(var addr in addrList)
{
dictAddr[addr.dataIndentifier] = addr;
}
}
//SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
public bool OpenDevice(string portName, int baudRate= 2400, Parity parity = Parity.None, int dataBits=8, StopBits stopBits = StopBits.One)
{
try
{
serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
serialPort.Open();
serialPort.ReadTimeout = 500;
serialPort.DataReceived += SerialPort_DataReceived;
return true;
}catch(Exception ex)
{
return false;
}
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
byte[] rxTemp = new byte[1024];
int rdCount = serialPort.Read(rxTemp, 0, rxTemp.Length);
if (rdCount > 0)
{
for(int i = 0; i < rdCount; i++)
{
if (rxTemp[i] == 0xfe)
continue;
rxdata[rxCount++] = rxTemp[i];
if (rxTemp[i] == 0x16)
{
ParseData();
}
}
}
swRead.Stop();
}
public void ParseDataTest(string data)
{
data = data.Replace(" ", "");
rxCount = data.Length;
for(int i = 0; i < rxCount;i+=2)
{
rxdata[i/2] = Convert.ToByte(data.Substring(i, 2), 16);
}
ParseData();
}
private void ParseData()
{
//校验和标识检查
if(rxCount < 9 || rxdata[0] != 0x68 || rxdata[7] != 0x68)
{
Console.WriteLine($"校验和标识检查失败,len:{rxCount},{rxdata[0]:X},{rxdata[7]:X}");
rxCount = 0;
return;
}
//响应命令检查
if (rxdata[8] != 0x91 && rxdata[8] != 0xB1 )
{
Console.WriteLine($"响应命令检查失败,{rxdata[8]:X}");
rxCount = 0;
return;
}
string addrT = BitConverter.ToString(rxdata, 1, 6).Replace("-","");
string addr = addrT.Substring(8,2)+ addrT.Substring(6, 2) + addrT.Substring(4, 2) + addrT.Substring(2, 2) + addrT.Substring(0, 2);
string value = "";
int dataLen = rxdata[9];
for(int k = 0; k < dataLen; k++)
{
rxdata[10 + k] -= 0x33;
value += string.Format("{0:X2}", rxdata[10 + k]);
}
string index = value.Substring(6, 2)+ value.Substring(4, 2)+ value.Substring(2, 2)+ value.Substring(0, 2);
var dlt645 = dictAddr[index];
string value2 = "";
for(int k = dlt645.dataLen-1; k >= 0; k --)
{
value2 += value.Substring(8 + k * 2, 2);
}
float fv = float.Parse(value2);
Console.WriteLine($"表号:{addr},{dlt645.dataItemName},{dlt645.dataIndentifier},{fv/dlt645.divisor} [{dlt645.unit}]");
rxCount = 0;
}
public void CloseDevice()
{
serialPort.Close();
serialPort = null;
}
public void Read(List<string> addrMeter, string data)
{
if (isNeedWakeUp)
{
byte[] reqHe = { 0xFE, 0xFE };
serialPort.Write(reqHe, 0, reqHe.Length);
}
byte []req = BuildRequest(addrMeter[0], data, CommandType.ReadData);
serialPort.Write(req,0,req.Length);
swRead.Restart();
}
public byte[] BuildRequest(string addrMeter, string data, CommandType commandType)
{
// 构建请求帧
byte[] request = new byte[12 + 4];
int index = 0;
request[index++] = 0x68; // 帧起始符 68H
//地址域 A0~A5,地址域是用来表示电表地址,低位在前,高位在后
request[index++] = Convert.ToByte(addrMeter.Substring(10, 2), 16); // 地址域 A0
request[index++] = Convert.ToByte(addrMeter.Substring(8, 2), 16); // 地址域 A1
request[index++] = Convert.ToByte(addrMeter.Substring(6, 2), 16); // 地址域 A2
request[index++] = Convert.ToByte(addrMeter.Substring(4, 2), 16); // 地址域 A3
request[index++] = Convert.ToByte(addrMeter.Substring(2, 2), 16); // 地址域 A4
request[index++] = Convert.ToByte(addrMeter.Substring(0, 2), 16); // 地址域 A5
request[index++] = 0x68; // 帧起始符
request[index++] = 0x11; // 控制码
request[index++] = (byte)(4);// Convert.ToByte((data.Count * 4).ToString(), 16); // 数据域长度
for (int i = 3;i >=0; i --)
{
string sV = data.Substring(i*2, 2);
if (sV.Equals("FF"))
{
request[index++] = 0xff;
}
else
{
request[index++] = (byte)(Convert.ToByte(sV, 16) + 0x33); // 数据
}
}
byte crc = GetCRC(request, 0, request.Length-2); // 校验码
request[index++] = Convert.ToByte(crc.ToString("X"), 16);
request[index++] = 0x16; // 结束符
return request;
}
private byte GetCRC(byte[] data, int start, int length)
{
int I = 0;
for (int k = 0; k < length; k++)
{
I += data[start + k];
}
return (byte)(I % 256);
}
}