前面讲了“DL645”协议的实例解析,现在看另外一个主流的工业现场总线协议“Modbus”。
Modbus是由Modicon(现为施耐德电气公司的一个品牌)在1979年发明的,是全球第一个真正用于工业现场的总线协议。为更好地普及和推动Modbus在基于以太网上的分布式应用,目前施耐德公司已将Modbus协议的所有权移交给IDA(Interface for Distributed Automation,分布式自动化接口)组织,并成立了Modbus-IDA组织,为Modbus今后的发展奠定了基础。在中国,Modbus已经成为国家标准GB/T19582-2008。据不完全统计:截止到2007年,Modbus的节点安装数量已经超过了1000万个。(百度百科)
一、进制转化
在计算机硬件处理时,都是“0”和“1”,通讯上也是“0”和“1”。但是,“0”和“1”在开发环境中不好表示,也不好计算,所以就用了十进制和十六进制的表现形式。尤其是在通讯上的各种编码大多都是用十六进制数表示的二进制编码,比如寄存器的地址,计算数据的长度(字节)。
(1)十与十六
0~255 = 0~FF
1+2+4+8 = 15 = 0xF
byte a = 255;
byte a = 0xff;
(2)
二与十六
1111 1111 1111 1111
F F F F
(3) 二进制、十进制、十六进制表示“1000”
二进制
0000 0011 1110 1000
十进制 十六进制
1000 0x03E8
二、移位运算和取余运算
(1)高地位和高低字节
在通讯中大多处理的是字节数据,对于字节有以下几种计算方式。
0001 0010 | 0011 0100
1 2 | 3 4
高八位 低八位
0x34 | 0x45
低字节 高字节
Modbus地址寄存器都是双字节:有可能先发高字节,也可能先低字节。
(2)移位操作,取高字节和低字节
“右移八位”操作(移位操作)
0000 0000 0001 0010 (取高八位,高八位右移。发送的时候只发低八位“地址的高八位”。)
做“与”操作
0001 0010 0011 0100 (0x1234,高八位“0001 0010”和低八位“0011 0100”,取底八位。)
0000 0000 1111 1111
结果:
0000 0000 0011 0100(发送的时候只发底八位“地址的底八位”。)
1000 = 0x03E8
取高八位
byte a = (byte)(1000 >> 8)
取底八位
byte b = (byte)(1000 & 0x00ff)
(3)取余操作去高字节和低字节
取高八位
byte a = (byte)(1000 / 256)
取底八位
byte b = (byte)(1000 % 256)
串口直接可以发十六进制,也可以发十进制,还可以发二进制,因为本质都是“0”和“1”。
比如有一个地址为“38H”的寄存器,那么可以发
byte a = 0x38 (十六进制)
byte a = 3*16+8=56 (十进制)
byte a = 00111000b (二进制)
三、“发送报文的实例”概述
0x01 0x03 0x004D 0x000c 0x01
下行(发给电表) DDSD读全部数据 DDSD读有功总电能
地址 1~254(0x01) 1~254(0x01)
功能码 0x03 0x03
数据区:起始地址 0x0048 0x004D
数据长度(寄存器个数) 0x000C(12) 0x0002
校验码:CRC16(16位,两个字节) MODBUS协议,校验码低字节在前,高字节在后
字节可以理解为256进制
位==Bit==0~1
字节=Byte(8Bit)==0~255
字=Word(16Bit)==0~65535,汉字编码双字节
双字=DWord(32Bit)==0~4294967295
1111 1111 1111 1111
0000 0000 1111 1111
0x22 0x01
十进制和十六进制之间的转化:
List<byte> buf = new List<byte>();
buf.Add(0x08);
buf.Add(0xCE);//十六进制表现
int iBase = buf[0] * 256 + buf[1];//十六进制到十进制转化 buf[0],buf[1]内存中已
经是十进制(都是01)。所以Buf[0]可以乘256,buf[1]也可以直接加上。
float fVal = iBase * 0.1F;
四、协议实例
报文格式:地址(一字节)+ 功能码(一字节)+ 数据区(n字节,数据区包含“寄存器起始地址”和“数据长度”) + CRC校验
以取AcrelDDSF1352 电表数据为例(地址“022”),取其“当前总电能”、“当前峰电能”、“当前平电能”、“当前谷电能”、“反向电能”、“无功电能”、“电压”、“电流”、“有功功率”、“无功功率”、“功率因数”、“频率”为例(借用某公司电能表的协议)。
发送报文:
1、 表具地址
022(十进制)
16(十六进制)
地址:0x16
2、 功能码
0x03 读寄存器
3、 数据区
(1) 起始地址 0000H = 0x0000
先发高字节,再发低字节,也就是先发0x00,再发0x00
(2) 数据长度,寄存器个数。(两个字节表示) 18个= 0x12个 = 0x0012
4、 校验码
一般用CRC16(16位,两个字节),根据前面的报文计算出,比如“0xC4A2”
Mudbus先发低字节,再发高字节。
0x A2 0xC4
最后发送报文为:
0x16 //表具起始地址
0x03 //功能码
0x00 0x00 //寄存器地址
0x00 0x12 //数据长度,寄存器个数
0xA2 0xC4 //CRC校验码
报文可以按照十进制和二进制替换,计算机中都是“0”和“1”。
编码程序(发送报文):
拼接地址码
拼接功能码
拼接寄存器地址
拼接数据长度(寄存器个数)
计算CRC校验码
拼接CRC校验码低八位(低字节)
拼接CRC校验码高八位(高字节)
返回报文的格式
返回报文和发送报文相似,只是“数据区”的内容不同。
数据区包含“数据长度”和“数据内容”。
数据内容长度(一个字节):6*4 +6*2 = 18 * 2 = 36字节 = 24H= 0x24
数据内容(假想实例):
当前总电能(高位在前,比如“2636.00 kWh” ,带小数十进制编码为“263600”= “000405B0H”=“0x000405B0”),对应的报文字节为:0x00 0x04 0x05 0xB0。
当前峰电能、当前平电能、当前谷电能、反向电能:同上
无功电能(保留):没有实际读数。可以是任意的4个字节,比如0x00000000
电压:
电压比如(“333.3V”,带小数十进制编码为“3333” = “D05H”= “0xD05”=“0x0D05”)对应的报文是:0x 0D 0x05
电流:
电流比如(“44.44A”,带小数十进制编码为“4444”=“115CH”=“0x115C”),对应的报文是:0x11 0x5C
有功功率、无功功率、功率因数、频率都是“Word”类型数据,也就是双字节。无实际值,返回的可能是“0x0000”
最终返回报文为:
0x16 //表具地址
0x03 //功能码
0x24 //数据长度
0x00 0x04 0x05 0xB0 //当前总电能
0x00 0x04 0x05 0xB0 //当前峰电能
0x00 0x04 0x05 0xB0 //当前平电能
0x00 0x04 0x05 0xB0 //当前谷电能
0x00 0x04 0x05 0xB0 //反向电能
0x00 0x00 0x00 0x00 //无功电能
0x 0D 0x05 //电压
0x11 0x5C //电流
0x00 0x00 //有功功率
0x00 0x00 //无功功率
0x00 0x00 //功率因数
0x00 0x00 //频率
0xA2 0xC4 //CRC校验码
五、解码程序(以取总电能为例,“轮询状态”思想)
解码循环
{
//地址
If 长度==1
{
If(字节!=地址)
{
清空字节数
切换状态等待
}
}
//功能码
If 长度==2
{
If(字节!=3)
{
清空字节数
切换状态等待
}
}
//返回数据的长度(一个字节)
If 长度=3
{
If(字节!=数据内容长度)
{
清空字节数
切换状态等待
}
}
//判断报文总长度
If 长度>=9
{
计算CRC校验码
//判断校验码
If(校验不通过)
{
清空字节数
切换状态等待
}
}
根据数据内容中接收到的字节,将数值转化为十进制数
计算小数位
存储数据值
}
编码程序:
public List<byte> GenTxBuf()
{
ushort usChk;
List<byte> bufSend = new List<byte>();
bufSend.Add(bAddr);
bufSend.Add(0x03);
bufSend.Add(0x00);
bufSend.Add(0x00);
bufSend.Add(0x00);
bufSend.Add(0x02);
usChk = GenCRC16(bufSend);
bufSend.Add((byte)(usChk & 0xff));
bufSend.Add((byte)(usChk >> 8));
if (commCode != null)
{
CodeEventArgs cea = new CodeEventArgs("Tx:", bufSend);
commCode(this, cea);
}
return bufSend;
}
解码程序
public enDeviceResult AddByte(byte bt)
{
bufReceive.Add(bt);
if (bufReceive.Count == 1)
{
if (bufReceive[0] != bAddr)
{
bufReceive.RemoveAt(0);
return enDeviceResult.等待;
}
}
if (bufReceive.Count == 2)
{
if (bufReceive[1] != 3)
{
bufReceive.Clear();
return enDeviceResult.等待;
}
}
if (bufReceive.Count == 3)
{
if (bufReceive[2] != 4)
{
bufReceive.Clear();
return enDeviceResult.等待;
}
}
if (bufReceive.Count >= 9)
{
ushort usChk = GenCRC16(bufReceive, 7);
if ((bufReceive[7] != (byte)(usChk & 0xff)) ||
(bufReceive[8] != (byte)(usChk >> 8)))
{
bufReceive.Clear();
return enDeviceResult.校验错误;
}
}
else
{
return enDeviceResult.等待;
}
if (commCode != null)
{
CodeEventArgs cea = new CodeEventArgs("Rx:", bufReceive);
commCode(this, cea);
}
int pnt = 3;
long lBase;
float fVal = 0;
lBase = bufReceive[pnt++];
lBase = lBase * 256 + bufReceive[pnt++];
lBase = lBase * 256 + bufReceive[pnt++];
lBase = lBase * 256 + bufReceive[pnt++];
fVal = (float)(lBase / 100.0);
foreach (MeterInfo mi in arrMeter)
{
if (mi.DataID == 0)
{
if (dataOK != null)
{
DataEventArgs dea = new DataEventArgs(new MeterData(DateTime.Now.ToString(), mi.MeterNo, mi.MeterType, (fVal * mi.Coefficient).ToString()));
dataOK(this, dea);
}
}
}
bufReceive.Clear();
return enDeviceResult.通讯结束;
}
六、看协议的方法
电能中带可能带有浮点数(以浮点数表示,需要根据二进制数独立算数十进制数值),有可能是做“*0.000X”等计算算出小数位(先以整型表示,然后算出小数位。)。所以要看协议中的具体“数据类型”的计算方法和表现形式。
取一次侧电能,电能量以浮点数表示。(DL645协议中以BCD码表示)
取出的数据带浮点数(小数点不是自己点的),比如计算方法如下。
从电表读出的数据是带浮点数的,但它是用“01”表示的。这个时候如果符合“IEEE754”数据格式,那么就可以直接从二进制数直接转化为对应的浮点数(C#,BitConverter.ToSingle(buf, 0);)。
buf[3] = bufReceive[pnt++];
buf[2] = bufReceive[pnt++];
buf[1] = bufReceive[pnt++];
buf[0] = bufReceive[pnt++];
float fVal = BitConverter.ToSingle(buf, 0);//算出单精度浮点数
如果不知道字节之间的排列次序,还要自己尝试。有“4×3×2×1=24”种可能。一般情况下,根据字节传递的次序是“3、2、1、0”,“2、3、、0、1”,“0、1、2、3”,“1、0、3、2”等。
二次侧电能,是没有乘倍率的电能,所以我们需要在程序中乘以倍率。
电能量 = 电能量(点小数之前的整型) / 1000(取小数位) ×PT(电压倍率,一般为1,注意字节数)×CT(电流倍率,注意字节数)
lBase = bufReceive[pnt++];
lBase = lBase * 256 + bufReceive[pnt++];
lBase = lBase * 256 + bufReceive[pnt++];
lBase = lBase * 256 + bufReceive[pnt++];
fVal = lBase * 0.001F * PTConvert * CTConvert;
看协议的方法:
1、 设备是什么类型的。“DL645”、“Modbus”,还是自定义的协议。
2、 取什么数据,找寄存器的地址(Mosbus下),或找数据类型(DL645下)。
3、取出数据的表现方法,看应该如何计算的。
附录(电力行业名词解释):
电压 U=220V
电流 I=50A
有功功率 P=10kW
无功功率 Q=10kVar (电压和电流有夹角)
视在功率 S=10kVA (总功率 = 有功功率+无功功率)
功率因数 Cos=PF=0.999 电压和电流之间夹角的余玄值
频率 Freg=50.00HZ (一秒钟多少个正玄波,也就是一秒钟发电机转了多少圈。)
PT 电压互感器,倍率一般是 “1”和“10kV/100v=100”
CT 电流互感器,倍率一般是 “400A/5A=80”和“400A/1A=400”
一次侧电能:是乘了倍率的电能
二次侧电能:是没有乘倍率的电能