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協議簡單通信示例

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