请教Modbus高手!我想根据Modbus03功能码代码修改出01功能码代码,求高手给个思路,感激不尽!!(串口/Modbus~RTU)

疯狂儿   2009-9-24 14:40 楼主
虽然是用C#语言编写的 但是懂Modbus的高手应该能看懂!!
这是一个将两个串口连接起来相互通信的一个程序(例如用串口工具把COM2和COM3连接起来。然后该程序连接的是COM2,设备连接的是COM3,

那么该程序就可以读取到设备中寄存器的值)。
下面的代码是实现功能码03的一个完整例子,已经调试成功!我把我能理解的东西已经写上注释,不理解的地方也写上了注释求高手帮忙分析。我的目标是通过03功能的实现,来修改出01功能。(03是读取寄存器中值是二进制值。01读取线圈当前状态值【0或1】)

我在后面把Modbus协议中关于03和01功能码及CRC校验的说明贴出来。

主界面Timer事件:
C# code
using System.IO.Ports;
modbus mb = new modbus();//modbus类的代码在后面给出
private SerialPort sp = new SerialPort();//声明串行端口sp
     private void timer1_Tick(object sender, EventArgs e)
       {
         this.listview1.Items.Clear();

short[] values = new short[Convert.ToInt32(txtRegisterQty.Text)];//txtRegisterQty是用来输入寄存器数量的TextBox
         ushort pollStart;//表示起始地址的变量
         ushort pollLength;//表示寄存器数量的变量

         if (txtStartAddr.Text != "")
pollStart = Convert.ToUInt16(txtStartAddr.Text);//txtStartAddr是用来输入寄存器的起始地址的TextBox(例如:如果写0,listview显示的第一项是设备中的第一个寄存器。如果写10,listview的第一项是设备中的第九个寄存器)
         else
             pollStart = 0;
         if (txtRegisterQty.Text != "")  
    pollLength = Convert.ToUInt16pollLength =Convert.ToUInt16(txtRegisterQty.Text);//txtRegisterQty是用来输入寄存器数量的TextBox(例如:如果写1,listview1中就会只显示设备中的第一个寄存器,如果写10,listview1就会显示10个寄存器)
         else
                pollLength = 20;

while (mb!SendFc3(Convert.ToByte(txtSlaveID.Text), pollStart, pollLength, ref values));//返回values,下面的for循环就是将返回的values加载到listview中(SendFc3方法内容详见后面的SendFc3方法代码)。

      for (int i =0; i < pollLength; i++)
                    {
listview1.Items.Add(new ListViewItem(new string[] { Convert.ToString(pollStart+i),Convert.ToString(pollStart + i + 40001), values.ToString()}));//将三条信息加载到listview1的三个列中,前两条信息是固定的,values的值是从上面的SendFc3方法返回的。
                    }
        }



下面的四个方法全部写在modbus类中。
C# code
using System.IO.Ports;
private SerialPort sp = new SerialPort();//声明串行端口sp
SendFc3方法:
public bool SendFc3(byte address, ushort start, ushort registers, ref short[] values)
        {
            if (sp.IsOpen)//当串口是打开状态时
              {

            byte[] message = new byte[8];
            byte[] response = new byte[5 + 2 * registers];//寄存器数量*2 +5的目的是什么?

BuildMessage(address, (byte)3, start, registers, ref message);//因为要用的是Modbus的03功能码去读,所以第二个参数是3。如果我想把03功能改成01功能,首先能肯定必须要改的就是这个方法中的第二个参数,既将3该写成1。(BuildMessage方法内容详见后面代码后面BuildMessage方法代码)。

    sp.Write(message, 0, message.Length);//BuildMessage方法返回来的message写入到串行端口
  GetResponse(ref response);//从串口把设备中寄存器的值读出来了(GetResponse方法内容详见后面的GetResponse方法代码)。

     for (int i = 0; i < (response.Length - 5) / 2; i++)//上面是将寄存器的数量*2加5这回-5/2还原了能理解,就是不明白上面为什么*2加5。
                {
//我能猜出下面这三行是把读出来的值进行处理,但不明白为什么这样处理。我感觉想要实现01功能码功能,这地方是关键!
                     values = response[2 * i + 3];//不明白
                     values <<= 8;//不明白
                     values += response[2 * i + 4];//不明白
                }
                return true;
         }
        else
         {
                    return false;
         }
        }

//BuildMessage方法:这个方法中message数组中的[0]~[7]的值是对应固定类型信息的(协议里的规定)。例如[0]代表地址、[1]代表功能。
private void BuildMessage(byte address, byte type, ushort start, ushort registers, ref byte[] message)
        {
            byte[] CRC = new byte[2];//声明两个字节的CRC数组

            message[0] = address;//设备地址为address
            message[1] = type;// 这条消息是要读取什么功能码,这里传进来的是03功能码。这是实现01功能还是03功能区别的地方
            message[2] = (byte)(start >> 8);//起始寄存器地址高8位(不理解start >> 8的意思)
            message[3] = (byte)start;//起始寄存器地址低8位
            message[4] = (byte)(registers >> 8);//读取的寄存器数高8位(不理解registers >> 8的意思)
            message[5] = (byte) registers;//读取的寄存器数低8位

          GetCRC(message, ref CRC);//GetCRC方法内容详见后面的GetCRC方法代码
          message[message.Length-2] = CRC[0];//CRC校验的低8位
          message[message.Length -1] = CRC[1];//CRC校验的高8位
        }


//GetCRC方法:在Modbus协议中这个函数叫CRC校验。我觉得这个方法的作用是把相对应的寄存器的值转化成CRC高位和低位,我在程序画面看到的值就是CRC高位+CRC低位的值,不知道理解的对不对!(CRC校验的含义详见后面的【生成CRC-16校验字节的步骤】)
        private void GetCRC(byte[] message, ref byte[] CRC)
        {
            ushort CRCFull = 0xFFFF;
            byte CRCHigh = 0xFF;
            byte CRCLow = 0xFF;
            char CRCLSB;
            for (int i = 0; i < (message.Length) - 2; i++)
            {
                CRCFull = (ushort)(CRCFull ^ message);
               
                for (int j = 0; j < 8; j++)
                {
                    CRCLSB = (char)(CRCFull & 0x0001);
                    CRCFull = (ushort)((CRCFull >> 1) & 0x7FFF);

                    if(CRCLSB ==1)
                        CRCFull =(ushort)(CRCFull^0xA001);
                }
            }
          CRC[1] =CRCHigh=(byte)((CRCFull >> 8) & 0xFF);
          CRC[0] = CRCLow = (byte)(CRCFull & 0xFF);
        }

//GetResponse方法:
        private void GetResponse(ref byte[] response)
        {
            for (int i = 0; i < response.Length; i++)
            {
                response = (byte)(sp.ReadByte());//从串口中把寄存器的值读出来
            }
        }

______________________________________________________________________________________________________________

【重要问题】要实现01功能,我猜想可能要要修改下CRC校验的那个方法。还有可能不用去修改CRC校验,而是去修改SendFc3方法的那个for循环。请高手指教,最好能写出代码,感激不尽!!!
PS:我用ModSim32这个软件来模拟设备的,在ModSim32界面中选择03功能,本程序可以正常运行实现读取寄存器值。
我的目的是想做到在ModSim32中选择01这个功能的时候,程序可以读取线圈状态值,实现对线圈状态值的读取(值是0或1)。
______________________________________________________________________________________________________________

Modbus协议中对01和03功能码及CRC校验的分析
【01功能码分析】
01功能码:读可读写数字量寄存器(取得一组逻辑线圈的当前状态(ON/OFF)):
计算机发送命令:[设备地址] [命令号01] [起始寄存器地址高8位] [低8位] [读取的寄存器数高8位] [低8位] [CRC校验的低8位] [CRC校验的

高8位]
例:[11][01][00][13][00][25][CRC低][CRC高]
意义如下:
<1>设备地址:在一个485总线上可以挂接多个设备,此处的设备地址表示想和哪一个设备通讯。例子中为想和17号(十进制的17是十六进制的11)通讯。
<2>命令号01:读取数字量的命令号固定为01。
<3>起始地址高8位、低8位:表示想读取的开关量的起始地址(起始地址为0)。比如例子中的起始地址为19。
<4>寄存器数高8位、低8位:表示从起始地址开始读多少个开关量。例子中为37个开关量。
<5>CRC校验:是从开头一直校验到此之前。在此协议的最后再作介绍。此处需要注意,CRC校验在命令中的高低字节的顺序和其他的相反。
设备响应:[设备地址] [命令号01] [返回的字节个数][数据1][数据2]...[数据n][CRC校验的低8位] [CRC校验的高8位]
例:[11][01][05][CD][6B][B2][0E][1B][CRC低][CRC高]
意义如下:
<1>设备地址和命令号和上面的相同。
<2>返回的字节个数:表示数据的字节个数,也就是数据1,2...n中的n的值。
<3>数据1...n:由于每一个数据是一个8位的数,所以每一个数据表示8个开关量的值,每一位为0表示对应的开关断开,为1表示闭合。比如例子中,表示20号(索引号为19)开关闭合,21号断开,22闭合,23闭合,24断开,25断开,26闭合,27闭合...如果询问的开关量不是8的整倍数,那么最后一个字节的高位部分无意义,置为0。
<4>CRC校验同上。

【03功能码分析】
03功能码:读可读写模拟量寄存器(在一个或多个保持寄存器中取得当前的二进制值):
计算机发送命令:[设备地址] [命令号03] [起始寄存器地址高8位] [低8位] [读取的寄存器数高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
例:[11][03][00][6B][00][03][CRC低][CRC高]
意义如下:
<1>设备地址和上面的相同。
<2>命令号:读模拟量的命令号固定为03。
<3>起始地址高8位、低8位:表示想读取的模拟量的起始地址(起始地址为0)。比如例子中的起始地址为107。
<4>寄存器数高8位、低8位:表示从起始地址开始读多少个模拟量。例子中为3个模拟量。注意,在返回的信息中一个模拟量需要返回两个字节。
设备响应:[设备地址] [命令号03] [返回的字节个数][数据1][数据2]...[数据n][CRC校验的低8位] [CRC校验的高8位]
例:[11][03][06][02][2B][00][00][00][64][CRC低][CRC高]
意义如下:
<1>设备地址和命令号和上面的相同。
<2>返回的字节个数:表示数据的字节个数,也就是数据1,2...n中的n的值。例子中返回了3个模拟量的数据,因为一个模拟量需要2个字节所以共6个字节。
<3>数据1...n:其中[数据1][数据2]分别是第1个模拟量的高8位和低8位,[数据3][数据4]是第2个模拟量的高8位和低8位,以此类推。例子中返回的值分别是555,0,100。
<4>CRC校验同上。

【生成CRC-16校验字节的步骤】
①装如一个16位寄存器,所有数位均为1。
②该16位寄存器的高位字节与开始8位字节进行“异或”运算。运算结果放入这个16位寄存器。
③把这个16寄存器向右移一位。
④若向右(标记位)移出的数位是1,则生成多项式1010000000000001和这个寄存器进行“异或”
运算;若向右移出的数位是0,则返回③。
⑤重复③和④,直至移出8位。
⑥另外8位与该十六位寄存器进行“异或”运算。
⑦重复③~⑥,直至该报文所有字节均与16位寄存器进行“异或”运算,并移位8次。
⑧这个16位寄存器的内容即2字节CRC错误校验,被加到报文的最高有效位。

回复评论 (3)

从机对03的应答是:
01 03 02 dh dl 所以有5个字节。是头后跟 xx xx 检验码。
点赞  2009-9-25 19:35
modbus mb = new modbus();//modbus类的代码在后面给出
modbus类的代码呢?
点赞  2010-4-10 17:05
高手,有没有源码。传一个
点赞  2010-5-17 16:11
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复