STM8学习笔记---Modbus通信协议简单移植

        Modbus是一种串行通信协议,在工业中应用是比较广泛的。关于Modbus的介绍网上资料很多,这里就不细说了。刚开始接触的时候看Modbus的介绍,光是协议的介绍有几百页,还有各种命令,各种链路层的应用,看了几天,越看越糊涂,越看越不会用。   

        最后在单片机上移植成功后才感觉Modbus协议没那么复杂,如果刚开始学的时候,没必要把Modbus协议中每个功能都去了解。就把它当做简单的串口协议,只使用最简单的几个命令就行了。熟悉之后再慢慢了解其他功能。

       下面就从单片机串口通信角度去理解Modbus协议,及如何将协议移植到单片机上。

       先看看Modbus的协议

从大的方面来讲,协议总共由4部分组成: 地址、功能、数据、校验。

地址1个字节,也就是设备的地址范围是 0 --- 255。

功能码也就是命令,也是一个字节,范围是0---255。

数据位在不同的情况下有不同的长度。

校验位一般用的是CRC校验。

下来看看功能码都有哪些

      常用的功能码有表格上面的这些,可以理解为一个数字代表的一种命令。给单片机移植时用03、06、16这三个命令就够用了。

       这里面读线圈、写单个线圈、写单个寄存器等等,到底什么是线圈?什么是寄存器?这些都是什么意思?

       简单的理解线圈就是位操作。比如说单片机控制了8路的继电器输出,为了方便表示继电器的状态,就用8个位来表示8个继电器的状态,比如0表示继电器断开,1表示继电器吸合。这样0x00就表示8路继电器全部断开,0xFF表示8路继电器全部吸合。

      寄存器是字节操作,比如传感器采集温度的时候用一个字节表示当前温度,比如当前温度28℃,就用0x1C表示。

      如果理解不了寄存器和线圈的含义就不用管它,就把他当做一个命令来看,在单片机中使用时03、06、16这三个命令就能满足基本需求,下面就单独分析一下这三个命令的含义。

     03是读多个保持寄存器值,读取的个数可以设置,比如有8组温度传感器采集数据,要读取温度值,可以一组一组去读,也可以一次性读多个值,读取的个数自己设置。

   先看看03的命令格式

请求就是单片机主机发送数据,正常响应就是主机发送的命令格式正确时,从机回复的数据。当主机发送的数据从机不能正确识别时,从机要返回异常响应数据,告诉主机发送的命令有错误。

      这里解释一下命令里面各个位的含义,这里是采集8组温度传感器的数值,假如一个从机有8路温度传感器,这个从机的地址就定义为0x01,这个地址根据实际项目可以自己定义。功能码为0x03,这里使用Modbus规定的功能码,意思是读多个寄存器。起始地址为两个字节,表示从第几个温度传感器开始读取数据,寄存器数量也为两个字节,表示要读取几个温度传感器的值。由于只有8路温度传感器,所以起始地址的范围就是 0x0000 ---- 0x0007。寄存器数量的范围为0x0001---0x0008,最少要读取一个寄存器的值,最多读8个寄存器的值。最后就是CRC校验, CRC具体的校验方式这里不用关心,使用的时候直接调用校验函数就行。

      这里要注意请求数据的时候要发送起始地址和请求数量,而返回数据的时候就没有请求地址了,只有发送的寄存器字节数。

比如现在要读取第一个温度传感器的值,那么请求数据格式如下:

从站地址     功能码   起始地址高位  起始地址低位  寄存器数量高位  寄存器数量低位  CRC校验高位  CRC校验低位

0x01             0x03            0x00              0x00                   0x00                   0x01                 xx                   xx

从0地址开始,读取1个寄存器的值,也就是读取第一个温度传感器的值。

正常响应返回数据格式如下

从站地址     功能码     字节数   寄存器数量高位  寄存器数量低位  CRC校验高位  CRC校验低位

0x01              0x03      0x02            0x00                  0x1E                  XX                      XX

读取到了2个字节寄存器的值,寄存器值为 0x001E, 0x001E对应的十进制数为30,说明第一个温度传感器的温度值为30℃。

那么异常响应是什么情况下会用到?假如请求数据发送的是读取第9个温度传感器的值,从机接收到数据后发现没有第9个传感器,说明主机发送的地址值超过范围了,那么从机这时就要给主机发送异常响应。常用的异常响应码有下面几种

从异常响应码中可以看出来,地址值不在范围内的异常码为0x02,Modbus规定返回异常响应时,差错码的值为功能码的值加上0x80,当前功能码为0x03,所以返回的差错码数值为0x83,差错码数值为0x02。

请求数据:

从站地址     功能码   起始地址高位  起始地址低位  寄存器数量高位  寄存器数量低位  CRC校验高位  CRC校验低位

0x01             0x03            0x00              0x09                   0x00                   0x01                 xx                   xx

异常响应:

从站地址    差错码   异常码   CRC校验   

0x01            0x83         0x02    xx

再看一个读取多个寄存器值的示例:

下面在看0x06写单个保持寄存器,就是给一个指定的寄存器中写入数据。通信格式如下:

通信示例如下:

可以看到写单个保持寄存器的请求命令和正常响应命令是完全相同的,这个就更好理解了。这块要注意 差错码的值为功能码的值加上0x80,当前功能码为0x06,所以返回的差错码数值为0x86。

下来在看看16(0x10)写多个保持寄存器,写多个保存寄存器和读多个寄存器基本一样,只不过一个是读,一个是写。

这块要注意 差错码的值为功能码的值加上0x80,当前功能码为0x10,所以返回的差错码数值为0x90。

通信示例如下:

响应命令只返回写的寄存器数量,而不返回写的寄存器值,这个和写单个寄存器是不同的。

通过上面的分析对Modbus就会有个大概的了解了,它也没有想得那么复杂。

下面就看看用代码如何实现上面这3个命令的功能。

首先看串口发送和接收代码的实现

#include "uart.h"
#include "stdio.h"
#include "main.h"

u8 ReceiveBuf[MaxDataLen] = {0};
u8 RecIndexLen = 0;
void Uart1_IO_Init( void )
{
    PD_DDR |= ( 1 << 5 ); //输出模式 TXD
    PD_CR1 |= ( 1 << 5 ); //推挽输出
    PD_DDR &= ~( 1 << 6 ); //输入模式 RXD
    PD_CR1 &= ~( 1 << 6 ); //浮空输入
}
//波特率最大可以设置为38400
void Uart1_Init( unsigned int baudrate )
{
    unsigned int baud;
    baud = 16000000 / baudrate;
    Uart1_IO_Init();
    UART1_CR1 = 0;
    UART1_CR2 = 0;
    UART1_CR3 = 0;
    UART1_BRR2 = ( unsigned char )( ( baud & 0xf000 ) >> 8 ) | ( ( unsigned char )( baud & 0x000f ) );
    UART1_BRR1 = ( ( unsigned char )( ( baud & 0x0ff0 ) >> 4 ) );
    UART1_CR2_bit.REN = 1;        //接收使能
    UART1_CR2_bit.TEN = 1;        //发送使能
    UART1_CR2_bit.RIEN = 1;       //接收中断使能
}
//阻塞式发送函数
void SendChar( unsigned char dat )
{
    while( ( UART1_SR & 0x80 ) == 0x00 ); //发送数据寄存器空
    UART1_DR = dat;
}
//发送一组数据
void Uart1_Send( unsigned char* DataAdd, unsigned char len )
{
    unsigned char i;
    for( i = 0; i < len; i++ )
    {
        SendChar( DataAdd[i] );
    }
    //SendChar(0x0d);     //发送回车换行,测试用
    //SendChar(0x0a);
}
//接收中断函数 中断号18
#pragma vector = 20             // IAR中的中断号,要在STVD中的中断号上加2
__interrupt void UART1_Handle( void )
{
    u8 res = 0;
    res = UART1_DR;
    ReceiveBuf[RecIndexLen++] = res;
    return;
}

          串口代码和常规的用法是一样的,初始化IO口和波特率,然后用中断接收数据,ReceiveBuf数组用来存放接收的数据,RecIndexLen用来统计接收数据的长度。

         一组数据接收完毕之后,调用数据处理函数,来处理接收到的数据。

//处理接收到的数据
// 接收: [地址][功能码][起始地址高][起始地址低][总寄存器数高][总寄存器数低][CRC低][CRC高]
void DisposeReceive( void )
{
    u16 CRC16 = 0, CRC16Temp = 0;
    if( ReceiveBuf[0] == SlaveID )                                 //地址等于本机地址 地址范围:1 - 32
    {
        CRC16 = App_Tab_Get_CRC16( ReceiveBuf, RecIndexLen - 2 );  //CRC校验 低字节在前 高字节在后 高字节为报文最后一个字节
        CRC16Temp = ( ( u16 )( ReceiveBuf[RecIndexLen - 1] << 8 ) | ReceiveBuf[RecIndexLen - 2] );
        if( CRC16 != CRC16Temp )
        {
            err = 4;                                               //CRC校验错误
        }
        StartRegAddr = ( u16 )( ReceiveBuf[2] << 8 ) | ReceiveBuf[3];
        if( StartRegAddr > 0x07 )
        {
            err = 2;                                               //起始地址不在规定范围内 00 - 07    1 - 8号通道
        }
        if( err == 0 )
        {
            switch( ReceiveBuf[1] )                                //功能码
            {
                case 3:                                            //读多个寄存器
                {
                    Modbus_03_Slave();
                    break;
                }
                case 6:                                            //写单个寄存器
                {
                    Modbus_06_Slave();
                    break;
                }
                case 16:                                           //写多个寄存器
                {
                    Modbus_16_Slave();
                    break;
                }
                default:
                {
                    err = 1;                                       //不支持该功能码
                    break;
                }
            }
        }
        if( err > 0 )
        {
            SendBuf[0] = ReceiveBuf[0];
            SendBuf[1] = ReceiveBuf[1] | 0x80;
            SendBuf[2] = err;                                      //发送错误代码
            CRC16Temp = App_Tab_Get_CRC16( SendBuf, 3 );           //计算CRC校验值
            SendBuf[3] = CRC16Temp & 0xFF;                         //CRC低位
            SendBuf[4] = ( CRC16Temp >> 8 );                       //CRC高位
            Uart1_Send( SendBuf, 5 );
            err = 0;                                               //发送完数据后清除错误标志
        }
    }
}

根据Modbus协议解析数据,第一个数据为地址,如果地址等于本机的地址才开始处理数据,否则就不处理数据。地址正确的话,要检查校验位是否正确,将接收的数据经过 CRC 校验,然后比较计算出来的校验位和接收到的校验位是否相同,如果校验位相同说明接收的数据正确,否则说明接收的数据出现了错误,要返回异常代码。接下来读取起始地址,检查起始地址是否在范围内。起始地址正确时,然后读取功能码,根据不同的功能码调用对应的函数。最后是处理异常响应,接收到的数据错误时,发送一组异常响应数据。

下来是功能码处理函数

/*
函数功能:读保持寄存器  03
主站请求报文:      0x01 0x03   0x0000  0x0001  0x840A     读从0开始的1个保持寄存器
从站正常响应报文:  0x01 0x03   0x02    0x09C4  0xBF87     读到的2字节数据为 0x09C4
*/
void Modbus_03_Slave( void )
{
    u16 RegNum = 0;
    u16 CRC16Temp = 0;
    u8 i = 0;
    RegNum = ( u16 )( ReceiveBuf[4] << 8 ) | ReceiveBuf[5];        //获取寄存器数量
    if( ( StartRegAddr + RegNum ) < 9 )                            //寄存器地址+寄存器数量 在规定范围内
    {
        SendBuf[0] = ReceiveBuf[0];
        SendBuf[1] = ReceiveBuf[1];
        SendBuf[2] = RegNum * 2;
        for( i = 0; i < RegNum; i++ )                              //读取保持寄存器内的值
        {
            SendBuf[3 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2];
            SendBuf[4 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2 + 1];
        }
        CRC16Temp = App_Tab_Get_CRC16( SendBuf, RegNum * 2 + 3 );  //获取CRC校验值
        SendBuf[RegNum * 2 + 3] = CRC16Temp & 0xFF;                //CRC低位
        SendBuf[RegNum * 2 + 4] = ( CRC16Temp >> 8 );              //CRC高位
        Uart1_Send( SendBuf, RegNum * 2 + 5 );
    }
    else
    {
        err = 3;                                                   //寄存器数量不在规定范围内
    }
}

如果是读取多个寄存器命令,就要知道读取的起始地址和寄存器数量,由于起始地址在接收函数中已经计算出来了,所以这里只需要计算寄存器数量就行了。下来就根据起始地址和寄存器数量从保持寄存器中读取数据。保持寄存器值存储在HoldReg数组中,温度传感器读取到的温度值就存储在这个数组中。寄存器数据读取完成就后计算要发送的数据校验值,校验值计算范围是从第一个数据开始到校验值前一位,通过调用App_Tab_Get_CRC16()这个函数计算CRC校验值。最后返回读取到的寄存器数据。通过Uart1_Send()函数发送数据。

下面是写单个寄存器

/*
函数功能:写单个保持寄存器 06
主站请求报文:      0x01 0x06    0x0000  0x1388    0x849C   写0号寄存器的值为0x1388
从站正常响应报文:  0x01 0x06    0x0000  0x1388    0x849C    0号寄存器的值为0x1388
*/
void Modbus_06_Slave( void )
{
    u16  RegValue = 0;
    u16 CRC16Temp = 0;
    RegValue = ( u16 )( ReceiveBuf[4] << 8 ) | ReceiveBuf[5];      //获取寄存器值
    if( RegValue < 1001 )                                          //寄存器值不超过1000
    {
        HoldReg[StartRegAddr * 2] = ReceiveBuf[4];                 //存储寄存器值
        HoldReg[StartRegAddr * 2 + 1] = ReceiveBuf[5];
        SendBuf[0] = ReceiveBuf[0];
        SendBuf[1] = ReceiveBuf[1];
        SendBuf[2] = ReceiveBuf[2];
        SendBuf[3] = ReceiveBuf[3];
        SendBuf[4] = ReceiveBuf[4];
        SendBuf[5] = ReceiveBuf[5];
        CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 );               //获取CRC校验值
        SendBuf[6] = CRC16Temp & 0xFF;                             //CRC低位
        SendBuf[7] = ( CRC16Temp >> 8 );                           //CRC高位
        Uart1_Send( SendBuf, 8 );
    }
    else
    {
        err =  3;                                                  //寄存器数值不在规定范围内
    }
}

这个函数就比较简单,将寄存器的值直接写到保持寄存器的对应位置就行。

最后是写多个寄存器

/*
函数功能:写多个连续保持寄存器值 16
主站请求报文:       0x01 0x10    0x7540  0x0002  0x04  0x0000 0x2710    0xB731  写从0x7540地址开始的2个保持寄存器值 共4字节
从站正常响应报文:   0x01 0x10    0x7540  0x0002  0x5A10                         写从0x7540地址开始的2个保持寄存器值
*/
void Modbus_16_Slave( void )
{
    u16 RegNum = 0;
    u16 CRC16Temp = 0;
    u8 i = 0;
    RegNum = ( u16 )( ReceiveBuf[4] << 8 ) | ReceiveBuf[5];        //获取寄存器数量
    if( ( StartRegAddr + RegNum ) < 9 )                            //寄存器地址+寄存器数量 在规定范围内
    {
        for( i = 0; i < RegNum; i++ )                              //存储寄存器设置值
        {
            HoldReg[StartRegAddr * 2 + i * 2] = ReceiveBuf[i * 2 + 7];
            HoldReg[StartRegAddr * 2 + 1 + i * 2] = ReceiveBuf[i * 2 + 8];
        }
        SendBuf[0] = ReceiveBuf[0];
        SendBuf[1] = ReceiveBuf[1];
        SendBuf[2] = ReceiveBuf[2];
        SendBuf[3] = ReceiveBuf[3];
        SendBuf[4] = ReceiveBuf[4];
        SendBuf[5] = ReceiveBuf[5];
        CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 );               //获取CRC校验值
        SendBuf[6] = CRC16Temp & 0xFF;                             //CRC低位
        SendBuf[7] = ( CRC16Temp >> 8 );                           //CRC高位
        Uart1_Send( SendBuf, 8 );
    }
    else
    {
        err = 3;                                                   //寄存器数量不在规定范围内
    }
}

根据寄存器的地址将对应值写入到保持寄存器就行,由于起始地址和寄存器数量都是变化的,所以这里要动态计算写入的寄存器地址,起始地址和寄存器数量都是两位,所以计算时要乘以2,这里不好理解的话,就代入一个固定值计算一下就明白了。

到这里Modbus的协议处理就完了,最后看看主函数

while( 1 )
    {
        if( RecIndexLen_tem != RecIndexLen )            //接收到一次数据,就将计时器清0一次
        {
            RecIndexLen_tem = RecIndexLen;
            time_cnt = 0;
        }
        if( time_cnt > 5 )                              //计时超过5ms
        {
            if( RecIndexLen_tem > 0 )                   //数据长度大于0 说明接收到数据
            {
                RecIndexEnd = RecIndexLen;              //存储本次接收数据长度
                //Uart1_Send( ReceiveBuf, RecIndexEnd );  //发送接收到的数据
                DisposeReceive();                       //处理接收到的数据
                RecIndexLen = 0;
            }
            else                                        //未接收到数据
            {
                time_cnt = 0;
            }
        }
    }

       由于Modbus协议没有指定的开始标志和结束标志,不能通道数据直接判断出来一组数据的开始和结束。这里用时间间隔来判断一组数据是否接收完成。实现思路为,在定时器中每1ms给计数器加1,串口中有数据进来就将这个计数器清0,如果串口一直在接收数据,那么这个计数器的值一直就会被清零。如果串口接收数据结束时,这个计数器没有被清零就会一直累加,当计数器累加到一定值后,说明在此时间内串口一直没有新的数据进来,那么此时就认为一组串口数据接收完成了。

         为了方便判断串口中是否有新的数据接收,在主函数中不停的读取串口接收数据长度, RecIndexLen为串口接收到的数据长度,RecIndexLen_tem为串口上一次接收到的数据长度,如果这两个值不相等说明串口新接收到了数据,将新的数据长度存储,并清零计数器。如果串口一直没有新的数据进来,并且计数器的值为5时,说明5ms串口都没有接收新的数据,就认为一组数据接收完成,开始处理接收到的数据。这个时间长度根据实际情况自己定义,参考标准就是,这个时间要大于两个位发送的间隔时间。考虑到线路传输和系统延时的话,间隔越长越好。间隔时间越长,判断一组数据接收完成就越准确,出现误判的可能行就越低。但是也不能太长,间隔时间太长系统响应速度就会比较慢。比如波特率为9600时,1秒钟发送9600/8=1200个字节的数据,发送一个字节需要0.83ms左右。这里数据间隔使用5ms就足够了。但是要注意两组数据之间的发送间隔也要大于5ms,否则数据发送频率过高,两组数据间隔小于5ms,程序就不能分辨出接收一组数据什么时候结束,引起错误。

         通过上面的分析可以看到,从串口通信角度去看Modbus协议,也没有那么难,只要学会使用其中的一个功能码,其他功能码的使用也就变得简单了。

源码下载地址 STM8S003单片机modbus协议简单通信示例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章