• “Modbus工业现场的总线协议”实例解析


         前面讲了“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”

    一次侧电能:是乘了倍率的电能

    二次侧电能:是没有乘倍率的电能

  • 相关阅读:
    浅析 c# Queue
    c# Stack操作类
    delegate,event, lambda,Func,Action以及Predicate
    推翻MMSOA与WEBService,使用MEMBRANE soap Monitor检查 wsdl文件。
    JS打印表格(HTML定义格式)
    坑爹的META 刷新页面 框架页面中TOP页面提示刷新 meta元素设置而不基于 JS 的坑爹写法。
    Silverlight载入动画(简易)
    WPF先开了再说
    跨域WEB Service 调用(摘自ASP.NET高级编程第三版)
    安装程序找不到Office.zhcn\OfficeMUI问题
  • 原文地址:https://www.cnblogs.com/ssol/p/2890684.html
Copyright © 2020-2023  润新知