《手把手教你學51單片機》之十八----RS485通信與Modbus協議

From:http://bbs.ickey.cn/group-topic-id-25006.html


第18章 RS485通信與Modbus協議

      在工業控制、電力通訊、智能儀表等領域,通常情況下是採用串口通信的方式進行數據交換。最初採用的方式是RS232接口,由於工業現場比較複雜,各種電氣設備會在環境中產生比較多的電磁干擾,會導致信號傳輸錯誤。除此之外,RS232接口只能實現點對點通信,不具備聯網功能,最大傳輸距離也只能達到十幾米,不能滿足遠距離通信要求。而RS485則解決了這些問題,數據信號採用差分傳輸方式,可以有效的解決共模干擾問題,最大距離可達1200米,並且允許多個收發設備接到同一條總線上。隨着工業應用通信越來越多,1979年施耐德電氣制定了一個用於工業現場的總線協議Modbus協議,現在工業中使用RS485通信場合很多都採用Modbus協議,本節課我們就來講解一下RS485通信和Modbus協議。


      單單使用一塊KST-51開發板是不能夠進行RS485實驗的,應很多同學的要求,把這節課作爲擴展課程講一下,如果要做本課相關實驗,需要自行購買USBRS485通信模塊,或連接其它的RS485主控設備進行


1.1 RS485通信

RS232標準是誕生於RS485之前的,但是RS232有幾處不足的地方:

1接口的信號電平值較高,達到十幾V使用不當容易損壞接口芯片,電平標準也與TTL電平不兼容。

2傳輸速率有侷限,不可以過高,一般到一兩百千比特每秒(Kb/s)就到極限了。

3接口使用信號線和GND與其它設備形成共地模式的通信,這種共地模式傳輸容易產生干擾,並且抗干擾性能也比較弱。

4傳輸距離有限,最多隻能通信幾十米。

5通信的時候只能兩點之間進行通信,不能夠實現多機聯網通信。


      針對RS232接口的不足,就不斷出現了一些新的接口標準,RS485就是其中之一,它具備以下的特點:

1、採用差分信號。我們在講A/D的時候,講過差分信號輸入的概念,同時也介紹了差分輸入的好處,最大的優勢是可以抑制共模干擾。尤其當工業現場環境比較複雜,干擾比較多時,採用差分方式可以有效的提高通信可靠性。RS485採用兩根通信線,通常用AB或者D+D-來表示。邏輯“1”以兩線之間的電壓差爲+(0.2~6)V表示,邏輯“0”以兩線間的電壓差爲-(0.2~6)V來表示,是一種典型的差分通信。

2RS485通信速率快,最大傳輸速度可以達到10Mb/s以上。

3RS485內部的物理結構,採用的是平衡驅動器和差分接收器的組合,抗干擾能力也大大增加。

4傳輸距離最遠可以達到1200米左右,但是它的傳輸速率和傳輸距離是成反比的,只有在100Kb/s以下的傳輸速度,才能達到最大的通信距離,如果需要傳輸更遠距離可以使用中繼。

5可以在總線上進行聯網實現多機通信,總線上允許掛多個收發器,從現有的RS485芯片來看,有可以掛3264128256等不同個設備的驅動器。

6RS485的接口非常簡單,與RS232所使用的MAX232是類似的,只需要一個RS485轉換器,就可以直接與單片機的UART串口連接起來,並且使用完全相同的異步串行通信協議。但是由於RS485是差分通信,因此接收數據和發送數據是不能同時進行的,也就是說它是一種半雙工通信。那我們如何判斷什麼時候發送,什麼時候接收呢?


      RS485轉換芯片很多,這節課我們以典型的MAX485爲例講解RS485通信,如圖18-1所示。

《手把手教你學51單片機》之十八----RS485通信與Modbus協議 

18-1  MAX485硬件接口


      MAX485是美信(Maxim)推出的一款常用RS485轉換器。其中5腳和8腳是電源引腳;6腳和7腳就是RS485通信中的AB兩個引腳;1腳和4腳分別接到單片機的RXDTXD引腳上,直接使用單片機UART進行數據接收和發送;2腳和3腳是方向引腳,其中2腳是低電平使能接收器,3腳是高電平使能輸出驅動器,我們把這兩個引腳連到一起,平時不發送數據的時候,保持這兩個引腳是低電平,讓MAX485處於接收狀態,當需要發送數據的時候,把這個引腳拉高,發送數據,發送完畢後再拉低這個引腳就可以了。爲了提高RS485的抗干擾能力,需要在靠近MAX485AB引腳之間並接一個電阻,這個電阻阻值從100歐到1K是可以。


      在這裏我們還要介紹一下如何使用KST-51單片機開發板進行外圍擴展實驗。我們的開發板只能把基本的功能給同學們做出來提供實驗練習,但是同學們學習的腳步不應該停留在這個實驗板上。如果想進行更多的實驗,就可以通過單片機開發板的擴展接口進行擴展實驗。大家可以看到藍綠色的單片機座周圍有32個插針,這32個插針就是把單片機的32IO引腳全部都引出來了。在原理圖上體現出來的就是J4J5J6J74個器件,如圖18-2所示。

《手把手教你學51單片機》之十八----RS485通信與Modbus協議 

18-2  單片機擴展接口


      這32IO中並不是所有的都可以用來對外擴展,其中既作爲數據輸出,又可以作爲數據輸入的引腳是不可以用的,比如P3.2P3.4P3.6引腳,這三個引腳是不可用的。比如P3.2這個引腳,如果我們用來擴展,發送的信號如果和DS18B20的時序吻合,會導致DS18B20拉低引腳,影響通信。除這3IO口以外的其它29個,都可以使用杜邦線接上插針,擴展出來使用。當然了,如果把當前的IO口應用於擴展功能了,板子上的相應功能就實現不了了,也就是說需要擴展功能和板載功能之間二選一。


      在進行RS485實驗中,我們通信用的引腳必須是P3.0P3.1,此外還有一個方向控制引腳,我們使用杜邦線將其連接到P1.7上去。RS485的另外一端,大家可以使用一個USBRS485模塊,用雙絞線把開發板和模塊上的AB分別對應連起來,USB那頭插入電腦,然後就可以進行通信了。


      學習了第13章實用的串口通信方法和程序後,做這種串口通信的方法就很簡單了,基本是一致的。我們使用實用串口通信例程的思路,做了一個簡單的程序,通過串口調試助手下發任意個字符,單片機接收到後在末尾添加“回車+換行”符後再送回,在調試助手上重新顯示出來,先把程序貼出來。


      程序中需要注意的一點是:因爲平常都是將MAX485設置爲接收狀態,只有在發送數據的時候纔將MAX485改爲發送狀態,所以在UartWrite()函數開頭將MAX485方向引腳拉高,函數退出前再拉低。但是這裏有一個細節,就是單片機的發送和接收中斷產生的時刻都是在停止位的一半上,也就是說每當停止位傳送了一半的時候,RITI就已經置位並且馬上進入中斷(如果中斷使能的話)函數了,接收的時候自然不會存在問題,但發送的時候就不一樣了:當緊接着向SBUF寫入一個字節數據時,UART硬件會在完成上一個停止位的發送後,再開始新字節的發送,但如果此時不是繼續發送下一個字節,而是已經發送完畢了,要停止發送並將MAX485方向引腳拉低以使MAX485重新處於接收狀態時就有問題了,因爲這時候最後的這個停止位實際只發送了一半,還沒有完全完成,所以就有了UartWrite()函數內DelayX10us(5)這個操作,這是人爲的增加了50us的延時,這50us的時間正好讓剩下的一半停止位完成,那麼這個時間自然就是由通信波特率決定的了,爲波特率週期的一半。


/****************************RS485.c文件程序源代碼*****************************/

#include <reg52.h>

#include <intrins.h>

 

sbit RS485_DIR = P1^7;  //RS485方向選擇引腳

 

bit flagFrame = 0;  //幀接收完成標誌,即接收到一幀新數據

bit flagTxd = 0;    //單字節發送完成標誌,用來替代TXD中斷標誌位

unsigned char cntRxd = 0;   //接收字節計數器

unsigned char pdata bufRxd[64];  //接收字節緩衝區

 

extern void UartAction(unsigned char *buf, unsigned char len);

 

/* 串口配置函數,baud-通信波特率 */

void ConfigUART(unsigned int baud)

{

    RS485_DIR = 0; //RS485設置爲接收方向

    SCON  = 0x50;  //配置串口爲模式1

    TMOD &= 0x0F;  //清零T1的控制位

    TMOD |= 0x20;  //配置T1爲模式2

    TH1 = 256 - (11059200/12/32)/baud;  //計算T1重載值

    TL1 = TH1;     //初值等於重載值

    ET1 = 0;       //禁止T1中斷

    ES  = 1;       //使能串口中斷

    TR1 = 1;       //啓動T1

}

/* 軟件延時函數,延時時間(t*10)us */

void DelayX10us(unsigned char t)

{

    do {

        _nop_();

        _nop_();

        _nop_();

        _nop_();

        _nop_();

        _nop_();

        _nop_();

        _nop_();

    } while (--t);

}

/* 串口數據寫入,即串口發送函數,buf-待發送數據的指針,len-指定的發送長度 */

void UartWrite(unsigned char *buf, unsigned char len)

{

    RS485_DIR = 1;  //RS485設置爲發送

    while (len--)   //循環發送所有字節

    {

        flagTxd = 0;       //清零發送標誌

        SBUF = *buf++;    //發送一個字節數據

        while (!flagTxd); //等待該字節發送完成

    }

    DelayX10us(5);  //等待最後的停止位完成,延時時間由波特率決定

    RS485_DIR = 0;  //RS485設置爲接收

}

/* 串口數據讀取函數,buf-接收指針,len-指定的讀取長度,返回值-實際讀到的長度 */

unsigned char UartRead(unsigned char *buf, unsigned char len)

{

    unsigned char i;

    

    if (len > cntRxd)  //指定讀取長度大於實際接收到的數據長度時,

    {                     //讀取長度設置爲實際接收到的數據長度

        len = cntRxd;

    }

    for (i=0; i<len; i++)  //拷貝接收到的數據到接收指針上

    {

        *buf++ = bufRxd[i];

    }

    cntRxd = 0;  //接收計數器清零

    

    return len;  //返回實際讀取長度

}

/* 串口接收監控,由空閒時間判定幀結束,需在定時中斷中調用,ms-定時間隔 */

void UartRxMonitor(unsigned char ms)

{

    static unsigned char cntbkp = 0;

    static unsigned char idletmr = 0;

 

    if (cntRxd > 0)  //接收計數器大於零時,監控總線空閒時間

    {

        if (cntbkp != cntRxd)  //接收計數器改變,即剛接收到數據時,清零空閒計時

        {

            cntbkp = cntRxd;

            idletmr = 0;

        }

        else                     //接收計數器未改變,即總線空閒時,累積空閒時間

        {

            if (idletmr < 30)  //空閒計時小於30ms時,持續累加

            {

                idletmr += ms;

                if (idletmr >= 30)  //空閒時間達到30ms時,即判定爲一幀接收完畢

                {

                    flagFrame = 1;  //設置幀接收完成標誌

                }

            }

        }

    }

    else

    {

        cntbkp = 0;

    }

}

/* 串口驅動函數,監測數據幀的接收,調度功能函數,需在主循環中調用 */

void UartDriver()

{

    unsigned char len;

    unsigned char pdata buf[40];

 

    if (flagFrame) //有命令到達時,讀取處理該命令

    {

        flagFrame = 0;

        len = UartRead(buf, sizeof(buf)-2); //將接收到的命令讀取到緩衝區中

        UartAction(buf, len);  //傳遞數據幀,調用動作執行函數

    }

}

/* 串口中斷服務函數 */

void InterruptUART() interrupt 4

{

    if (RI)  //接收到新字節

    {

        RI = 0;  //清零接收中斷標誌位

        if (cntRxd < sizeof(bufRxd)) //接收緩衝區尚未用完時,

        {                                 //保存接收字節,並遞增計數器

            bufRxd[cntRxd++] = SBUF;

        }

    }

    if (TI)  //字節發送完畢

    {

        TI = 0;   //清零發送中斷標誌位

        flagTxd = 1;  //設置字節發送完成標誌

    }

}

/*****************************main.c文件程序源代碼******************************/

#include <reg52.h>

 

unsigned char T0RH = 0;  //T0重載值的高字節

unsigned char T0RL = 0;  //T0重載值的低字節

 

void ConfigTimer0(unsigned int ms);

extern void UartDriver();

extern void ConfigUART(unsigned int baud);

extern void UartRxMonitor(unsigned char ms);

extern void UartWrite(unsigned char *buf, unsigned char len);

 

void main()

{

    EA = 1;              //開總中斷

    ConfigTimer0(1);   //配置T0定時1ms

    ConfigUART(9600);  //配置波特率爲9600

    

    while (1)

    {

        UartDriver();  //調用串口驅動

    }

}

/* 串口動作函數,根據接收到的命令幀執行響應的動作

   buf-接收到的命令幀指針,len-命令幀長度 */

void UartAction(unsigned char *buf, unsigned char len)

{

    //在接收到的數據幀後添加換車換行符後發回

    buf[len++] = 'r';

    buf[len++] = 'n';

    UartWrite(buf, len);

}

/* 配置並啓動T0ms-T0定時時間 */

void ConfigTimer0(unsigned int ms)

{

    unsigned long tmp;  //臨時變量

    

    tmp = 11059200 / 12;       //定時器計數頻率

    tmp = (tmp * ms) / 1000;  //計算所需的計數值

    tmp = 65536 - tmp;         //計算定時器重載值

    tmp = tmp + 33;            //補償中斷響應延時造成的誤差

    T0RH = (unsigned char)(tmp>>8);  //定時器重載值拆分爲高低字節

    T0RL = (unsigned char)tmp;

    TMOD &= 0xF0;   //清零T0的控制位

    TMOD |= 0x01;   //配置T0爲模式1

    TH0 = T0RH;     //加載T0重載值

    TL0 = T0RL;

    ET0 = 1;        //使能T0中斷

    TR0 = 1;        //啓動T0

}

/* T0中斷服務函數,執行串口接收監控 */

void InterruptTimer0() interrupt 1

{

    TH0 = T0RH;  //重新加載重載值

    TL0 = T0RL;

    UartRxMonitor(1);  //串口接收監控

}


      現在看這種串口程序,是不是感覺很簡單了呢?串口通信程序我們反反覆覆的使用,加上隨着學習的模塊越來越多,實踐的越來越多,原先感覺很複雜的東西,現在就會感到簡單了。從設備管理器裏可以查看所有的COM口號,我們下載程序用的是COM4,而USBRS485虛擬的是COM5,通信的時候我們用的是COM5口,如圖18-3所示。

《手把手教你學51單片機》之十八----RS485通信與Modbus協議 

18-3  RS485通信試驗設置和結果

1.2 Modbus通信協議介紹

 我們前邊學習UARTI2CSPI這些通信協議,都是最底層的協議,是“位”級別的協議。而我們在學習13做實用串口通信程序的時候,我們通過串口發給單片機三條指令,讓單片機做了三件不同的事情,分別是“buzz on”、“buzz off”和“showstr”。隨着系統複雜性的增加,我們希望可以實現更多的指令。而指令越來越多,帶來的後果就是非常雜亂無章,尤其是這個人喜歡寫成“buzz on”、“buzz off”,而另外一個人喜歡寫成“on buzz”、“off buzz”。導致不同開發人員寫出來的程序代碼不兼容,不同廠家的產品不能掛到一條總線上通信。

隨着這種矛盾的日益嚴重,就會有聰明人提出更合理的解決方案,提出一些標準來,今後我們的編程必須按照這個標準來,這種標準也是一種通信協議,但是和UARTI2CSPI通信協議不同的是,這種通信協議是字節級別的,叫做應用層通信協議。在1979年由Modicon(現爲施耐德電氣公司的一個品牌)提出了全球第一個真正用於工業現場總線的協議,就是Modbus協議。

1.2.1 Modbus協議特點

      Modbus協議是應用於電子控制器上的一種通用語言。通過此協議,控制器相互之間、控制器經由網絡(例如以太網)和其他設備之間可以通信,已經成爲一種工業標準。有了它,不同廠商生產的控制設備可以連成工業網絡,進行集中監控。這種協議定義了一種控制器能夠認識使用的數據結構,而不管它們是經過何種網絡進行通信的。它描述了控制器請求訪問其它設備的過程,如何迴應來自其它設備的請求,以及怎樣偵測錯誤記錄,它制定了通信數據的格局和內容的公共格式。


       在進行多機通信的時候,Modbus協議規定每個控制器必須要知道它們的設備地址,識別按照地址發送過來的數據,決定是否要產生動作,產生何種動作,如果要回應,控制器將生成的反饋信息用Modbus協議發出。


      Modbus協議允許在各種網絡體系結構內進行簡單通信,每種設備(PLC、人機界面、控制面板、驅動程序、輸入輸出設備等)都能使用Modbus協議來啓動遠程操作,一些網關允許在幾種使用Modbus協議的總線或網絡之間的通信,如圖18-4所示。

《手把手教你學51單片機》之十八----RS485通信與Modbus協議 

18-4  Modbus網絡體系結構實例


      Modbus協議的整體架構和格式比較複雜和龐大,在我們的課程裏,我們重點介紹數據幀結構和數據通信控制方式,作爲一個入門級別的瞭解。如果大家要詳細瞭解,或者使用Modbus開發相關設備,可以查閱相關的國標文件再進行深入學習。

1.1.1 RTU協議幀數據

      Modbus有兩種通信傳輸方式,一種是ASCII模式,一種是RTU模式。由於ASCII模式的數據字節是7bit數據位,51單片機無法實現,而且實際應用的也比較少,所以這裏我們只用RTU模式。兩種模式相似,會用一種另外一種也就會了。一條典型的RTU數據幀如圖18-5所示。

 

18-5  RTU數據幀


      與之前我們講解實用串口通信程序時用的原理相同,一次發送的數據幀必須是作爲一個連續的數據流進行傳輸。我們在實用串口通信程序中採用的方法是定義30ms,如果數據接收時超過了30ms還沒有接收到下一個字節,我們就認爲這次的數據結束。而ModbusRTU模式規定不同數據幀之間的間隔是3.5個字節通信時間以上。如果在一幀數據完成之前有超過3.5個字節時間的停頓,接收設備將刷新當前的消息並假定下一個字節是一個新的數據幀的開始。同樣的,如果一個新消息在小於3.5個字節時間內接着前邊一個數據開始,接收設備將會認爲它是前一幀數據的延續。這將會導致一個錯誤,因此大家看RTU數據幀最後還有16bit的CRC校驗。


       起始位和結束符:圖18-5上代表的是一個數據幀,前後都至少有3.5個字節的時間間隔,起始位和結束符實際上沒有任何數據,T1-T2-T3-T4代表的是時間間隔3.5個字節以上的時間,而真正有意義的第一個字節是設備地址。


      設備地址:很多同學不理解,在多機通信的時候,數據那麼多,我們依靠什麼判斷這個數據幀是哪個設備的呢?沒錯,就是依靠這個設備地址字節。每個設備都有一個自己的地址,當設備接收到一幀數據後,程序首先對設備地址字節進行判斷比較,如果與自己的地址不同,則對這幀數據直接不予理會,如果與自己的地址相同,就要對這幀數據進行解析,按照之後的功能碼執行相應的功能。如果地址是0x00,則認爲是一個廣播命令,就是所有的從機設備都要執行的指令。


      功能代碼:在第二個字節功能代碼字節中,Modbus規定了部分功能代碼,此外也保留了一部分功能代碼作爲備用或者用戶自定義,這些功能碼大家不需要去記憶,甚至都不用去看,直到你用到的那天再過來查這個表格即可,如表18-1所示。


                                                                           表18-1 Modbus功能碼

功能碼

名稱

作用

01

讀取線圈狀態

取得一組邏輯線圈的當前狀態(ON/OFF)

02

讀取輸入狀態

取得一組開關輸入的當前狀態(ON/OFF)

03

讀取保持寄存器 

在一個或多個保持寄存器中取得當前的二進制值

04

讀取輸入寄存器

在一個或多個輸入寄存器中取得當前的二進制值

05

強置單線圈

強置一個邏輯線圈的通斷狀態

06

預置單寄存器

把具體二進值裝入一個保持寄存器 

07

讀取異常狀態

取得8個內部線圈的通斷狀態,這8個線圈的地址由控制器決定,用戶邏輯可以將這些線圈定義,以說明從機狀態,短報文適宜於迅速讀取狀態 

08

回送診斷校驗

把診斷校驗報文送從機,以對通信處理進行評鑑

09

編程(只用於484)

使主機模擬編程器作用,修改PC從機邏輯

10

控詢(只用於484)

可使主機與一臺正在執行長程序任務從機通信,探詢該從機是否已完成其操作任務,僅在含有功能碼 的報文發送後,本功能碼才發送 

11

讀取事件計數

可使主機發出單詢問,並隨即判定操作是否成功,尤其是該命令或其應答產生通信錯誤時 

12

讀取通信事件記錄

使主機檢索每臺從機的ModBus事務處理通信事件記錄。如果某項事務處理完成,記錄會給出有關錯誤

13

編程(184/384 484 584 )

可使主機模擬編程器功能修改PC從機邏輯 

14

探詢(184/384 484 584)

可使主機與正在執行任務的從機通信,定期控詢該從機是否已完成其程序操作,僅在含有功能13的報文發送後,本功能碼才得發送

15

強置多線圈

強置一串連續邏輯線圈的通斷

16

預置多寄存器

把具體的二進制值裝入一串連續的保持寄存器

17

報告從機標識

可使主機判斷編址從機的類型及該從機運行指示燈的狀態

18

884 MICRO 84

可使主機模擬編程功能,修改PC狀態邏輯

19

重置通信鏈路

發生非可修改錯誤後,是從機復位於已知狀態,可重置順序字節 

20

讀取通用參數(584L)

顯示擴展存儲器文件中的數據信息

21

寫入通用參數(584L)

把通用參數寫入擴展存儲文件,或修改

22~64

保留作擴展功能備用

 

65~72

保留以備用戶功能所用

留作用戶功能的擴展編碼 

73~119

非法功能

 

120~127

保留 

留作內部作用

128~255

保留

用於異常應答


      程序對功能碼的處理,就是來檢測這個字節的數值,然後根據其數值來做相應的功能處理。


      數據:跟在功能代碼後邊的是n8bit的數據。這個n值的到底是多少,是功能代碼來確定的,不同的功能代碼後邊跟的數據數量不同。舉個例子,如果功能碼是0x03,也就是讀保持寄存器,那麼主機發送數據n的組成部分就是:2個字節的寄存器起始地址,加2個字節的寄存器數量N。從機數據n的組成部分是:1個字節的字節數,因爲我們回覆的寄存器的值是2個字節,所以這個字節數也就是2N個,再加上2N個寄存器的值,如圖18-6示。

 

18-6  讀保持寄存器數據結構


      CRC校驗:CRC校驗是一種數據算法,是用來校驗數據對錯的。CRC校驗函數把一幀數據除最後兩個字節外,前邊所有的字節進行特定的算法計算,計算完後生成了一個16bit的數據,作爲CRC校驗碼,添加在一幀數據的最後。接收方接收到數據後,同樣會把前邊的字節進行CRC計算,計算完了再和發過來的16bitCRC數據進行比較,如果相同則認爲數據正常,沒有出錯,如果比較不相同,則說明數據在傳輸中發生了錯誤,這幀數據將被丟棄,就像沒收到一樣,而發送方會在得不到迴應後做相應的處理錯誤處理。


      RTU模式的每個字節的位是這樣分佈的:1個起始位、8個數據位,最小有效位先發送、1個奇偶校驗位(如果無校驗則沒有這一位)、1位停止位(有校驗位時)或者2個停止位(無校驗位時)。

1.1 Modbus多機通信例程

      給從機下發不同的指令,從機去執行不同的操作,這個就是判斷一下功能碼即可,和我們前邊學的實用串口例程是類似的。多機通信,無非就是添加了一個設備地址判斷而已,難度也不大。我們找了一個Modbus調試精靈,通過設置設備地址,讀寫寄存器的地址以及數值數量等參數,可以直接替代串口調試助手,比較方便的下發多個字節的數據,如圖18-7所示。我們先來就圖中的設置和數據來對Modbus做進一步的分析,圖中的數據來自於調試精靈與我們接下來要講的例程之間的交互。

 

18-7  Modbus調試精靈


     如圖,我們的USBRS485模塊虛擬出的是COM5,波特率9600,無校驗位,數據位是8位,1位停止位,設備地址假設爲1


      寫寄存器的時候,如果我們要把01寫到一個地址是0000的寄存器地址裏,點一下“寫入”,就會出現發送指令:01 06 00 00 00 01 48 0A。我們來分析一下這幀數據,其中01是設備地址,06是功能碼,代表寫寄存器這個功能,後邊跟00 00表示的是要寫入的寄存器的地址,00 01就是要寫入的數據,48 0A就是CRC校驗碼,這是軟件自動算出來的。而根據Modbus協議,當寫寄存器的時候,從機成功完成該指令的操作後,會把主機發送的指令直接返回,我們的調試精靈會接收到這樣一幀數據:01 06 00 00 00 01 48 0A

  

      假如我們現在要從寄存器地址0002開始讀取寄存器,並且讀取的數量是2個。點一下“讀出”,就會出現發送指令:01 03 00 02 00 02 65 CB。其中01是設備地址,03是功能碼,代表讀寄存器這個功能,00 02就是讀寄存器的起始地址,後一個00 02就是要讀取2個寄存器的數值,65 CB就是CRC校驗。而接收到的數據是:01 03 04 00 00 00 00 FA 33。其中01是設備地址,03是功能碼,04代表的是後邊讀到的數據字節數是4個,00 00 00 00分別是地址爲00 0200 03的寄存器內部的數據,而FA 33就是CRC校驗了。


      似乎越來越明朗了,所謂的Modbus通信協議,無非就是主機下發了不同的指令,從機根據指令的判斷來執行不同的操作而已。由於我們的開發板沒有Modbus功能碼那麼多相應的功能,我們在程序中定義了一個數組regGroup[5],相當於5個寄存器,此外又定義了第6個寄存器,控制蜂鳴器,通過下發不同的指令我們改變寄存器組的數據或者改變蜂鳴器的開關狀態。在Modbus協議裏寄存器的地址和數值都是16位的,即2個字節,我們默認高字節是0x00,低字節就是數組regGroup對應的值。其中地址0x00000x0004對應的就是regGroup數組中的元素,我們寫入的同時把數字又顯示到1602液晶上,而0x0005這個地址,寫入0x00,蜂鳴器就不響,寫入任何其它數值,蜂鳴器就報警。我們單片機的主要工作也就是解析串口接收的數據執行不同操作。

/***************************Lcd1602.c文件程序源代碼*****************************/

(此處省略,可參考之前章節的代碼)

/****************************RS485.c文件程序源代碼*****************************/

(此處省略,可參考之前章節的代碼)

/****************************CRC16.c文件程序源代碼****************************/

/* CRC16計算函數,ptr-數據指針,len-數據長度,返回值-計算出的CRC16數值 */

unsigned int GetCRC16(unsigned char *ptr,  unsigned char len)

    unsigned int index;

    unsigned char crch = 0xFF;  //CRC字節

    unsigned char crcl = 0xFF;  //CRC字節

    unsigned char code TabH[] = {  //CRC高位字節值表

        0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  

        0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  

        0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  

        0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  

        0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  

        0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,  

        0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  

        0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  

        0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  

        0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  

        0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  

        0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  

        0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  

        0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  

        0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  

        0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,  

        0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  

        0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  

        0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  

        0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  

        0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,  

        0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,  

        0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,  

        0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,  

        0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,  

        0x80, 0x41, 0x00, 0xC1, 0x81, 0x40  

    } ;  

    unsigned char code TabL[] = {  //CRC低位字節值表

        0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,  

        0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,  

        0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,  

        0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,  

        0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,  

        0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,  

        0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,  

        0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,  

        0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,  

        0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,  

        0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,  

        0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,  

        0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,  

        0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,  

        0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,  

        0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,  

        0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,  

        0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,  

        0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,  

        0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,  

        0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,  

        0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,  

        0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,  

        0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,  

        0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,  

        0x43, 0x83, 0x41, 0x81, 0x80, 0x40  

    } ;

 

    while (len--)  //計算指定長度的CRC

    {

        index = crch ^ *ptr++;

        crch = crcl ^ TabH[index];

        crcl = TabL[index];

    }

    

    return ((crch<<8) | crcl);  

}

關於CRC校驗的算法,如果不是專門學習校驗算法本身,大家可以不去研究這個程序的細節,直接使用現成的函數即可。 

/*****************************main.c文件程序源代碼******************************/

#include <reg52.h>

 

sbit BUZZ = P1^6;

 

bit flagBuzzOn = 0;   //蜂鳴器啓動標誌

unsigned char T0RH = 0;  //T0重載值的高字節

unsigned char T0RL = 0;  //T0重載值的低字節

unsigned char regGroup[5];  //Modbus寄存器組,地址爲0x000x04

 

void ConfigTimer0(unsigned int ms);

extern void UartDriver();

extern void ConfigUART(unsigned int baud);

extern void UartRxMonitor(unsigned char ms);

extern void UartWrite(unsigned char *buf, unsigned char len);

extern unsigned int GetCRC16(unsigned char *ptr,  unsigned char len);

extern void InitLcd1602();

extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);

 

void main()

{

    EA = 1;              //開總中斷

    ConfigTimer0(1);   //配置T0定時1ms

    ConfigUART(9600);  //配置波特率爲9600

    InitLcd1602();     //初始化液晶

    

    while (1)

    {

        UartDriver();  //調用串口驅動

    }

}

/* 串口動作函數,根據接收到的命令幀執行響應的動作

   buf-接收到的命令幀指針,len-命令幀長度 */

void UartAction(unsigned char *buf, unsigned char len)

{

    unsigned char i;

    unsigned char cnt;

    unsigned char str[4];

    unsigned int  crc;

    unsigned char crch, crcl;

    

    if (buf[0] != 0x01) //本例中的本機地址設定爲0x01

    {                      //如數據幀中的地址字節與本機地址不符,

        return;           //則直接退出,即丟棄本幀數據不做任何處理

    }

    //地址相符時,再對本幀數據進行校驗

    crc = GetCRC16(buf, len-2);  //計算CRC校驗值

    crch = crc >> 8;

    crcl = crc & 0xFF;

    if ((buf[len-2]!=crch) || (buf[len-1]!=crcl))

    {

        return;   //CRC校驗不符時直接退出

    }

    //地址和校驗字均相符後,解析功能碼,執行相關操作

    switch (buf[1])

    {

        case 0x03:  //讀取一個或連續的寄存器

            if ((buf[2]==0x00) && (buf[3]<=0x05)) //只支持0x00000x0005

            {

                if (buf[3] <= 0x04)

                {

                    i = buf[3];      //提取寄存器地址

                    cnt = buf[5];    //提取待讀取的寄存器數量

                    buf[2] = cnt*2;  //讀取數據的字節數,爲寄存器數*2

                    len = 3;          //幀前部已有地址、功能碼、字節數共3個字節

                    while (cnt--)

                    {

                        buf[len++] = 0x00;            //寄存器高字節補0

                        buf[len++] = regGroup[i++]; //寄存器低字節

                    }

                }

                else  //地址0x05爲蜂鳴器狀態

                {

                    buf[2] = 2;  //讀取數據的字節數

                    buf[3] = 0x00;

                    buf[4] = flagBuzzOn;

                    len = 5;

                }

                break;

            }

            else  //寄存器地址不被支持時,返回錯誤碼

            {

                buf[1] = 0x83;  //功能碼最高位置1

                buf[2] = 0x02;  //設置異常碼爲02-無效地址

                len = 3;

                break;

            }

            

        case 0x06:  //寫入單個寄存器

            if ((buf[2]==0x00) && (buf[3]<=0x05)) //只支持0x00000x0005

            {

                if (buf[3] <= 0x04)

                {

                    i = buf[3];               //提取寄存器地址

                    regGroup[i] = buf[5];   //保存寄存器數據

                    cnt = regGroup[i] >> 4; //顯示到液晶上

                    if (cnt >= 0xA)

                        str[0] = cnt - 0xA + 'A';

                    else

                        str[0] = cnt + '0';

                    cnt = regGroup[i] & 0x0F;

                    if (cnt >= 0xA)

                        str[1] = cnt - 0xA + 'A';

                    else

                        str[1] = cnt + '0';

                    str[2] = '';

                    LcdShowStr(i*3, 0, str);

                }

                else  //地址0x05爲蜂鳴器狀態

                {

                    flagBuzzOn = (bit)buf[5]; //寄存器值轉爲蜂鳴器的開關

                }

                len -= 2; //長度-2以重新計算CRC並返回原幀

               &nb

break;

            }

            else  //寄存器地址不被支持時,返回錯誤碼

            {

                buf[1] = 0x86;  //功能碼最高位置1

                buf[2] = 0x02;  //設置異常碼爲02-無效地址

                len = 3;

                break;

            }

            

        default:  //其它不支持的功能碼

            buf[1] |= 0x80;  //功能碼最高位置1

            buf[2] = 0x01;   //設置異常碼爲01-無效功能

            len = 3;

            break;

    }

    crc = GetCRC16(buf, len); //計算返回幀的CRC校驗值

    buf[len++] = crc >> 8;    //CRC高字節

    buf[len++] = crc & 0xFF;  //CRC低字節

    UartWrite(buf, len);      //發送返回幀

}

/* 配置並啓動T0ms-T0定時時間 */

void ConfigTimer0(unsigned int ms)

{

    unsigned long tmp;  //臨時變量

    

    tmp = 11059200 / 12;       //定時器計數頻率

    tmp = (tmp * ms) / 1000;  //計算所需的計數值

    tmp = 65536 - tmp;         //計算定時器重載值

    tmp = tmp + 33;            //補償中斷響應延時造成的誤差

    T0RH = (unsigned char)(tmp>>8);  //定時器重載值拆分爲高低字節

    T0RL = (unsigned char)tmp;

    TMOD &= 0xF0;   //清零T0的控制位

    TMOD |= 0x01;   //配置T0爲模式1

    TH0 = T0RH;     //加載T0重載值

    TL0 = T0RL;

    ET0 = 1;        //使能T0中斷

    TR0 = 1;        //啓動T0

}

/* T0中斷服務函數,執行串口接收監控和蜂鳴器驅動 */

void InterruptTimer0() interrupt 1

{

    TH0 = T0RH;  //重新加載重載值

    TL0 = T0RL;

    if (flagBuzzOn)  //執行蜂鳴器鳴叫或關閉

        BUZZ = ~BUZZ;

    else

        BUZZ = 1;

    UartRxMonitor(1);  //串口接收監控

}

大家可以看到負責解析協議的UartAction函數很長,因爲協議解析本來就是一件很繁瑣的事情。我們的例程僅解析執行了兩個功能命令,就已經有近百行程序了,如果你需要解析更多的功能命令的話,那麼建議把每個功能都做一個函數,然後在相應的case分支裏調用即可,這樣就不會使單個函數過於龐大而難以維護。

1.1 練習題

1、瞭解RS485通信以及和RS232的不同用法。

2、瞭解Modbus協議以及RTU數據幀的規則。

3、寫一個電子鐘程序,並且可以通過485調試器校時。


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