《手把手教你學51單片機》之十三------1602液晶與串口的應用實例

第13章 1602液晶與串口的應用實例

      理論上的內容要想逐步消化掌握,必須得通過大量的實踐進行鞏固,否則時間一長,極容易忘掉。尤其是一些編程相關的技巧,就是靠不停的寫程序,不停的參考別人的程序慢慢積累成長起來的。這節課帶領大家學習一下1602的例程和實際開發中比較實用的串口通信程序。


1.1 通信時序解析

      隨着我們對通信技術的深入學習,大家要逐漸在頭腦中建立起時序這種概念。所謂“時序”從字面意義上來理解,一是“時間問題”,二是“順序問題”。

先說“順序問題”,這個相對簡單一些。我們在學UART串口通信的時候,先1位起始位,再8位數據位,最後1位停止位,這個先後順序不能錯。我們在學1602液晶的時候,比如寫指令,RS=LR/W=LD0~D7=指令碼,這三者的順序是無所謂的,但是最終的E=高脈衝,必須是在這三條程序之後,這個順序一旦錯誤,寫的數據也可會出錯。


      “時間問題”內容相對複雜。比如UART通信,每一位的時間寬度是1/baud。我們初中就學過一個概念,世界上沒有絕對的準確。那麼每一位的時間寬度1/baud要求精確到什麼範圍內呢?


      前邊教程我提到過,單片機讀取UARTRXD引腳數據的時候,一位數據,單片機平均分成了16份,取其中的789三次讀到的結果,這三次中有2次是高電平那這一位就是1,有2次是低電平,那這一次就是0。如果我們的波特率稍微有些偏差,只要累計下來到最後一位停止位,這789還在範圍內即可。如圖13-1所示。

《手把手教你學51單片機》之十三------1602液晶與串口的應用實例 

13-1  UART信號採集時序圖


      我們用三個箭頭來表示789這三次的採集位置,大家可以注意到,當採集到D7的時候,已經有一次採集偏出去了,但是我們採集到的數據還是不會錯,因爲有2次採集正確。至於這個偏差允許多大,大家自己可以詳細算一下。實際上UART通信的波特率是允許一定範圍內誤差存在的,但是不能過大,否則就會採集錯誤。大家在計算波特率的時候,發現沒有整除,有小數部分的時候,就要特別小心了,因爲小數部分是一概被舍掉的,於是計算誤差就產生了。 我們用11.0592M晶振計算的過程中,11059200/12/32/9600得到的是一個整數,如果用12M晶振計算12000000/12/32/9600就會得到一個小數,大家可以算一下誤差多少,是否在誤差範圍內。

      1602的時序問題,大家要學會通過LCD1602的數據手冊提供的時序圖和時序參數表格來進行研究,而且看懂時序圖是學習單片機所必須掌握的一項技能,如圖13-2所示。

《手把手教你學51單片機》之十三------1602液晶與串口的應用實例 

13-2  1602時序圖


      大家看到這種圖的時候,不要感覺害怕。說句不過分的話,單片機這些邏輯上的問題,只要小學畢業就可以理解的,很多時候是因爲大家把問題想象的太難才學不下去的。


      我們先來看一下讀操作時序的RS引腳和R/W引腳,這兩個引腳先進行變化,因爲是讀操作,所以R/W引腳首先要置爲高電平,而不管它原來是什麼。讀指令還是讀數據,都是讀操作,而且都有可能,所以RS引腳既有可能是置爲高電平,也有可能是置爲低電平,大家注意圖上的畫法。而RSR/W變化了經過Tsp1這麼長時間後,使能引腳E才能從低電平到高電平發生變化。


      而使能引腳E拉高經過了tD這麼長時間後,LCD1602輸出DB的數據就是有效數據了,我們就可以來讀取DB的數據了。讀完了之後,我們要先把使能E拉低,經過一段時間後RSR/WDB纔可以變化繼續爲下一次讀寫做準備了。


      而寫操作時序和讀操作時序的差別,就是寫操作時序中,DB的改變是由單片機來完成的,因此要放到使能引腳E的變化之前進行操作,其它區別大家可以自行對比一下。


      細心的同學會發現,這個時序圖上還有很多時間標籤。比如E的上升時間tR,下降時間時間tF,使能引腳E從一個上升沿到下一個上升沿之間的長度週期tC,使能E下降沿後,R/WRS變化時間間隔tHD1等等很多時間要求,這些要求怎麼看呢?放心,只要是正規的數據手冊,都會把這些時間要求給大家標記出來的。我們來看一下表13-1


                                                                              表13-1  1602時序參數

時序參數

符號

極限值

單位

測試條件

最小值

典型值

最大值

E信號週期

tC

400

--

--

ns

引腳E

E脈衝寬度

tPW

150

--

--

ns

E上升沿/下降沿時間

tR, tF

--

--

25

ns

地址建立時間

tSP1

30

--

--

ns

引腳ERSR/W

地址保持時間

tHD1

10

--

--

ns

數據建立時間()

tD

--

--

100

ns

引腳DB0~DB7

數據保持時間()

tHD2

20

--

--

ns

數據建立時間()

tSP2

40

--

--

ns

數據保持時間()

tHD2

10

--

--

ns


      大家要善於把手冊中的這個表格和時序圖結合起來看。表13-1中的數據,都是時序參數,本節課的所有時序參數,我都一點點的給大家講出來,以後遇到同類時序圖,就不再講了,只是提一下,但是大家務必要學會自己看時序圖,這個很重要,此外,看以下解釋需要結合圖13-2來看。


      tC:指的是使能引腳E從本次上升沿到下次上升沿的最短時間是400ns,而我們單片機因爲速度較慢,一個機器週期就是1us多,而一條C語言指令肯定是一個或者幾個機器週期的,所以這個條件完全滿足。


      tPW:指的是使能引腳E高電平的持續時間最短是150ns,同樣由於我們的單片機比較慢,這個條件也完全滿足。


      tR, tF:指的是使能引腳E的上升沿時間和下降沿時間,不能超過25ns,別看這個數很小,其實這個時間限值是很寬裕的,我們實際用示波器測了一下開發板的這個引腳上升沿和下降沿時間大概是10ns15ns之間,完全滿足。


      tSP1:指的是RSR/W引腳使能後至少保持30ns,使能引腳E纔可以變成高電平,這個條件同樣也完全滿足。


     tHD1:指的是使能引腳E變成低電平後,至少保持10ns之後,RSR/W才能進行變化,這個條件也完全滿足。


     tD:指的是使能引腳E變成高電平後,最多100ns後,1602就把數據送出來了,那麼我們就可以正常去讀取狀態或者數據了。


     tHD2:指的是讀操作過程中,使能引腳E變成低電平後,至少保持20nsDB數據總線纔可以進行變化,這個條件也完全滿足。


      tSP2:指的是DB數據總線準備好後,至少保持40ns,使能引腳E纔可以從低到高進行使能變化,這個條件也完全滿足。


      tHD2:指的是寫操作過程中,要引腳E變成低電平後,至少保持10nsDB數據總線纔可以變化,這個條件也完全滿足。


      好了,表13-1這個LCD1602的時序參數表已經解析完成了,看完之後,是不是感覺比你想象的要簡單,沒有你想的那麼困難。大家自己也得慢慢學會看這種時序圖和表格,在今後的學習中,這方面的能力尤爲重要。如果以後換用了其它型號的單片機,那麼就根據單片機的執行速度來評估你的程序是否滿足時序要求,整體上來說器件都是有一個最快速度的限制,而沒有最慢限制,所以當換用高速的單片機後通常都是靠在各步驟間插入軟件延時來滿足較慢的時序要求。


1.2 1602整屏移動

      我們前邊學第7章點陣LED的時候,可以實現上下移動,左右移動等。而對於1602液晶來說,也可以進行屏幕移動,實現我們想要的一些效果,那我們來用一個例程實現字符串在1602液晶上的左移。每個人都不要只瞪着眼看,一定要認真抄下來,甚至抄幾遍,邊抄邊理解,要想真正學好,一定要根據我的方法來做。

#include <reg52.h>

 

#define LCD1602_DB  P0

sbit LCD1602_RS = P1^0;

sbit LCD1602_RW = P1^1;

sbit LCD1602_E  = P1^5;

 

bit flag500ms = 0;   //500ms定時標誌

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

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

//待顯示的第一行字符串

unsigned char code str1[] = "Kingst Studio";

//待顯示的第二行字符串,需保持與第一行字符串等長,較短的行可用空格補齊

unsigned char code str2[] = "Let's move...";

 

void ConfigTimer0(unsigned int ms);

void InitLcd1602();

void LcdShowStr(unsigned char x, unsigned char y,

                   unsigned char *str, unsigned char len);

 

void main()

{

    unsigned char i;

    unsigned char index = 0;  //移動索引

    unsigned char pdata bufMove1[16+sizeof(str1)+16]; //移動顯示緩衝區1

    unsigned char pdata bufMove2[16+sizeof(str2)+16]; //移動顯示緩衝區2

 

    EA = 1;              //開總中斷

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

    InitLcd1602();     //初始化液晶

    //緩衝區開頭一段填充爲空格

    for (i=0; i<16; i++)

    {

        bufMove1[i] = ' ';

        bufMove2[i] = ' ';

    }

    //待顯示字符串拷貝到緩衝區中間位置

    for (i=0; i<(sizeof(str1)-1); i++)

    {

        bufMove1[16+i] = str1[i];

        bufMove2[16+i] = str2[i];

    }

    //緩衝區結尾一段也填充爲空格

    for (i=(16+sizeof(str1)-1); i<sizeof(bufMove1); i++)

    {

        bufMove1[i] = ' ';

        bufMove2[i] = ' ';

    }

    

    while (1)

    {

        if (flag500ms)  //500ms移動一次屏幕

        {

            flag500ms = 0;

            //從緩衝區抽出需顯示的一段字符顯示到液晶上

            LcdShowStr(0, 0, bufMove1+index, 16);

            LcdShowStr(0, 1, bufMove2+index, 16);

            //移動索引遞增,實現左移

            index++;

            if (index >= (16+sizeof(str1)-1))

            {   //起始位置達到字符串尾部後即返回從頭開始

                index = 0;

            }

        }

    }

}

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

void ConfigTimer0(unsigned int ms)

{

    unsigned long tmp;  //臨時變量

    

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

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

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

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

    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

}

/* 等待液晶準備好 */

void LcdWaitReady()

{

    unsigned char sta;

    

    LCD1602_DB = 0xFF;

    LCD1602_RS = 0;

    LCD1602_RW = 1;

    do {

        LCD1602_E = 1;

        sta = LCD1602_DB; //讀取狀態字

        LCD1602_E = 0;

    } while (sta & 0x80); //bit7等於1表示液晶正忙,重複檢測直到其等於0爲止

}

/* LCD1602液晶寫入一字節命令,cmd-待寫入命令值 */

void LcdWriteCmd(unsigned char cmd)

{

    LcdWaitReady();

    LCD1602_RS = 0;

    LCD1602_RW = 0;

    LCD1602_DB = cmd;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

/* LCD1602液晶寫入一字節數據,dat-待寫入數據值 */

void LcdWriteDat(unsigned char dat)

{

    LcdWaitReady();

    LCD1602_RS = 1;

    LCD1602_RW = 0;

    LCD1602_DB = dat;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

/* 設置顯示RAM起始地址,亦即光標位置,(x,y)-對應屏幕上的字符座標 */

void LcdSetCursor(unsigned char x, unsigned char y)

{

    unsigned char addr;

    

    if (y == 0)  //由輸入的屏幕座標計算顯示RAM的地址

        addr = 0x00 + x;  //第一行字符地址從0x00起始

    else

        addr = 0x40 + x;  //第二行字符地址從0x40起始

    LcdWriteCmd(addr | 0x80);  //設置RAM地址

}

/* 在液晶上顯示字符串,(x,y)-對應屏幕上的起始座標,

   str-字符串指針,len-需顯示的字符長度 */

void LcdShowStr(unsigned char x, unsigned char y, 

                   unsigned char *str, unsigned char len)

{

    LcdSetCursor(x, y);   //設置起始地址

    while (len--)          //連續寫入len個字符數據

    {

        LcdWriteDat(*str++);  //先取str指向的數據,然後str自加1

    }

}

/* 初始化1602液晶 */

void InitLcd1602()

{

    LcdWriteCmd(0x38);  //16*2顯示,5*7點陣,8位數據接口

    LcdWriteCmd(0x0C);  //顯示器開,光標關閉

    LcdWriteCmd(0x06);  //文字不動,地址自動+1

    LcdWriteCmd(0x01);  //清屏

}

/* T0中斷服務函數,定時500ms */

void InterruptTimer0() interrupt 1

{

    static unsigned char tmr500ms = 0;

    

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

    TL0 = T0RL;

    tmr500ms++;

    if (tmr500ms >= 50)

    {

        tmr500ms = 0;

        flag500ms = 1;

    }

}


     通過這個程序,大家首先要學會for語句在數組中的靈活應用,這個其實在數碼管顯示有效位的例程中已經有所體現了。其次,隨着我們後邊程序量的增大,大家得學會多個函數之間相互調用的靈活應用,體會其中的奧妙。

1.3 多.c文件的初步認識

      我們上一節的這個液晶滾屏移動程序,大概有160行左右。隨着我們硬件模塊使用的增多,程序量的增大,我們往往要把程序寫到多個文件裏,方便代碼的編寫、維護和移植。


      比如這個液晶滾屏程序,我們就可以把1602底層的功能函數專門寫到一個.c文件內,         如LcdWaitReadyLcdWriteCmdLcdWriteDatLcdShowStrLcdSetCursorInitLcd1602這些函數,都是屬於液晶底層驅動的程序代碼,我們要使用液晶功能的時候,只有兩個函數對我們實際功能實現部分有用,一個是InitLcd1602這個函數,因爲需要先初始化液晶,另外一個就是LcdShowStr這個函數,我們只需要把要顯示的內容通過參數傳遞給這個函數,這個函數就可以實現我們想要的顯示效果,所以我們把這幾個底層的液晶驅動程序都放到另外一個文件Lcd1602.c文件中,而我們想實現的一些比如滾動實現、中斷等上層功能程序全部都放到main.c中,但是main.c文件如何調用Lcd1602.c文件中的函數呢?


      C語言中,有一個extern關鍵字,它有兩個基本作用。

1、當一個變量的聲明不在文件的開頭,在它聲明之前的函數想要引用的話,則應該用extern進行“外部變量”聲明。用一個簡單的程序給大家介紹一下,知道這麼回事,能看懂別人寫的就行,自己寫就別這麼用了。

    #include <reg52.h>

    sbit LED = P0^0;

    void  main()

    {

        extern unsigned int i;

        while(1)

        {

             LED = 0;                 //點亮小燈

             for(i=0;i<30000;i++);  //延時

             LED = 1;                 //熄滅小燈

             for(i=0;i<30000;i++); //延時

        }

    }

    unsigned  int  i = 0;

    ... ...


     變量的作用域,是從聲明這個變量開始往後所有的程序,如果我們調用在前,聲明在後,那麼就是這麼用。但是實際開發過程中,我們一般都不會這樣做,所以僅僅是表達一下extern的這個用法,但它並不實用。


    2、在一個工程中,我們爲了方便管理和維護代碼,用了多個.c源文件,如果其中一個main.c文件要調用Lcd1602.c文件裏的變量或者函數的時候,我們就必須得在main.c裏邊進行一下外部聲明,告訴編譯器這個變量或者函數是在其它文件中定義的,可以直接在這個文件中進行調用。


     多.c文件的編程方式,大家不要想象的太複雜。首先新建一個工程,一個工程代表一個完整的單片機程序,只能生成一個hex,但是一個工程可以有很多個.c源文件組成共同參與編譯。工程建立好之後,新建文件並且保存取名爲main.c文件,再新建一個文件並且保存取名爲Lcd1602.c文件,下面我們就可以在兩個不同文件中分別編寫代碼了。當然,在編寫程序的過程中,不是說我們要先把main.c的文件全部寫完,再進行1602.c程序的編寫,而往往是交互的。比如我們先寫Lcd1602.c文件中部分Lcd1602液晶的底層函數LcdWaitReadyLcdWriteCmdLcdWriteDatInitLcd1602,然後編寫main.c文件中的功能程序,在編寫main.c文件中程序時,又有對Lcd1602.c底層程序的綜合調用,這個時候需要Lcd1602.c文件提供一個被調用的函數比如LcdShowStr,我們就可以再到Lcd1602.c中把這個函數完成。當然了,這僅僅是一個說明例子而已,順序完全是沒有一個標準的,實際應用中如果對程序邏輯需求瞭解透徹,根據自己的理解去寫程序即可。那我們把1602整屏移動的程序改造成爲多文件的程序,大家先初步認識一下。


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

#include <reg52.h>

 

#define LCD1602_DB  P0

sbit LCD1602_RS = P1^0;

sbit LCD1602_RW = P1^1;

sbit LCD1602_E  = P1^5;

 

/* 等待液晶準備好 */

void LcdWaitReady()

{

    unsigned char sta;

    

    LCD1602_DB = 0xFF;

    LCD1602_RS = 0;

    LCD1602_RW = 1;

    do {

        LCD1602_E = 1;

        sta = LCD1602_DB;  //讀取狀態字

        LCD1602_E = 0;

    } while (sta & 0x80); //bit7等於1表示液晶正忙,重複檢測直到其等於0爲止

}

/* LCD1602液晶寫入一字節命令,cmd-待寫入命令值 */

void LcdWriteCmd(unsigned char cmd)

{

    LcdWaitReady();

    LCD1602_RS = 0;

    LCD1602_RW = 0;

    LCD1602_DB = cmd;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

/* LCD1602液晶寫入一字節數據,dat-待寫入數據值 */

void LcdWriteDat(unsigned char dat)

{

    LcdWaitReady();

    LCD1602_RS = 1;

    LCD1602_RW = 0;

    LCD1602_DB = dat;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

/* 設置顯示RAM起始地址,亦即光標位置,(x,y)-對應屏幕上的字符座標 */

void LcdSetCursor(unsigned char x, unsigned char y)

{

    unsigned char addr;

    

    if (y == 0)  //由輸入的屏幕座標計算顯示RAM的地址

        addr = 0x00 + x;  //第一行字符地址從0x00起始

    else

        addr = 0x40 + x;  //第二行字符地址從0x40起始

    LcdWriteCmd(addr | 0x80);  //設置RAM地址

}

/* 在液晶上顯示字符串,(x,y)-對應屏幕上的起始座標,

   str-字符串指針,len-需顯示的字符長度 */

void LcdShowStr(unsigned char x, unsigned char y, 

                unsigned char *str, unsigned char len)

{

    LcdSetCursor(x, y);   //設置起始地址

    while (len--)          //連續寫入len個字符數據

    {

        LcdWriteDat(*str++);

    }

}

/* 初始化1602液晶 */

void InitLcd1602()

{

    LcdWriteCmd(0x38);  //16*2顯示,5*7點陣,8位數據接口

    LcdWriteCmd(0x0C);  //顯示器開,光標關閉

    LcdWriteCmd(0x06);  //文字不動,地址自動+1

    LcdWriteCmd(0x01);  //清屏

}

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

#include <reg52.h>

 

bit flag500ms = 0;   //500ms定時標誌

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

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

//待顯示的第一行字符串

unsigned char code str1[] = "Kingst Studio";

//待顯示的第二行字符串,需保持與第一行字符串等長,較短的行可用空格補齊

unsigned char code str2[] = "Let's move...";

 

void ConfigTimer0(unsigned int ms);

extern void InitLcd1602();

extern void LcdShowStr(unsigned char x, unsigned char y,

                       unsigned char *str, unsigned char len);

 

void main()

{

    unsigned char i;

    unsigned char index = 0;  //移動索引

    unsigned char pdata bufMove1[16+sizeof(str1)+16]; //移動顯示緩衝區1

    unsigned char pdata bufMove2[16+sizeof(str2)+16]; //移動顯示緩衝區2

 

    EA = 1;              //開總中斷

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

    InitLcd1602();     //初始化液晶

    //緩衝區開頭一段填充爲空格

    for (i=0; i<16; i++)

    {

        bufMove1[i] = ' ';

        bufMove2[i] = ' ';

    }

    //待顯示字符串拷貝到緩衝區中間位置

    for (i=0; i<(sizeof(str1)-1); i++)

    {

        bufMove1[16+i] = str1[i];

        bufMove2[16+i] = str2[i];

    }

    //緩衝區結尾一段也填充爲空格

    for (i=(16+sizeof(str1)-1); i<sizeof(bufMove1); i++)

    {

        bufMove1[i] = ' ';

        bufMove2[i] = ' ';

    }

    

    while (1)

    {

        if (flag500ms)  //500ms移動一次屏幕

        {

            flag500ms = 0;

            //從緩衝區抽出需顯示的一段字符顯示到液晶上

            LcdShowStr(0, 0, bufMove1+index, 16);

            LcdShowStr(0, 1, bufMove2+index, 16);

            //移動索引遞增,實現左移

            index++;

            if (index >= (16+sizeof(str1)-1))

            {   //起始位置達到字符串尾部後即返回從頭開始

                index = 0;

            }

        }

    }

}

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

void ConfigTimer0(unsigned int ms)

{

    unsigned long tmp;  //臨時變量

    

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

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

    tmp = 65536 - tmp;       

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

    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中斷服務函數,定時500ms */

void InterruptTimer0() interrupt 1

{

    static unsigned char tmr500ms = 0;

    

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

    TL0 = T0RL;

    tmr500ms++;

    if (tmr500ms >= 50)

    {

        tmr500ms = 0;

        flag500ms = 1;

    }

}

     我們在main.c中要調用Lcd1602.c文件中的InitLcd1602()LcdShowStr這兩個函數,只需要在main.c中進行extern聲明即可。大家用Keil軟件編程試試,真正的感覺一下多.c源文件的好處。如果這個程序給你的感覺還不深刻,那下面我們來做一個稍微大點的程序來體會一下。

1.1 計算器實例

     按鍵和液晶,可以組成我們最簡易的計算器。下面我們來寫一個簡易整數計算器提供給大家學習。爲了讓程序不過於複雜,我們這個計算器不考慮連加,連減等連續計算,不考慮小數情況。加減乘除分別用上下左右來替代,回車表示等於,ESC表示歸0。程序共分爲三部分,一部分是1602液晶顯示,一部分是按鍵動作和掃描,一部分是主函數功能。

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

#include <reg52.h>

 

#define LCD1602_DB  P0

sbit LCD1602_RS = P1^0;

sbit LCD1602_RW = P1^1;

sbit LCD1602_E  = P1^5;

 

/* 等待液晶準備好 */

void LcdWaitReady()

{

    unsigned char sta;

    

    LCD1602_DB = 0xFF;

    LCD1602_RS = 0;

    LCD1602_RW = 1;

    do {

        LCD1602_E = 1;

        sta = LCD1602_DB;  //讀取狀態字

        LCD1602_E = 0;

    } while (sta & 0x80); //bit7等於1表示液晶正忙,重複檢測直到其等於0爲止

}

/* LCD1602液晶寫入一字節命令,cmd-待寫入命令值 */

void LcdWriteCmd(unsigned char cmd)

{

    LcdWaitReady();

    LCD1602_RS = 0;

    LCD1602_RW = 0;

    LCD1602_DB = cmd;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

/* LCD1602液晶寫入一字節數據,dat-待寫入數據值 */

void LcdWriteDat(unsigned char dat)

{

    LcdWaitReady();

    LCD1602_RS = 1;

    LCD1602_RW = 0;

    LCD1602_DB = dat;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

/* 設置顯示RAM起始地址,亦即光標位置,(x,y)-對應屏幕上的字符座標 */

void LcdSetCursor(unsigned char x, unsigned char y)

{

    unsigned char addr;

    

    if (y == 0)  //由輸入的屏幕座標計算顯示RAM的地址

        addr = 0x00 + x;  //第一行字符地址從0x00起始

    else

        addr = 0x40 + x;  //第二行字符地址從0x40起始

    LcdWriteCmd(addr | 0x80);  //設置RAM地址

}

/* 在液晶上顯示字符串,(x,y)-對應屏幕上的起始座標,str-字符串指針 */

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

{

    LcdSetCursor(x, y);   //設置起始地址

    while (*str != '')  //連續寫入字符串數據,直到檢測到結束符

    {

        LcdWriteDat(*str++);

    }

}

/* 區域清除,清除從(x,y)座標起始的len個字符位 */

void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len)

{

    LcdSetCursor(x, y);   //設置起始地址

    while (len--)          //連續寫入空格

    {

        LcdWriteDat(' ');

    }

}

/* 整屏清除 */

void LcdFullClear()

{

    LcdWriteCmd(0x01);

}

/* 初始化1602液晶 */

void InitLcd1602()

{

    LcdWriteCmd(0x38);  //16*2顯示,5*7點陣,8位數據接口

    LcdWriteCmd(0x0C);  //顯示器開,光標關閉

    LcdWriteCmd(0x06);  //文字不動,地址自動+1

    LcdWriteCmd(0x01);  //清屏

}

Lcd1602.c文件中根據上層應用的需要增加了2個清屏函數:區域清屏——LcdAreaClear,整屏清屏——LcdFullClear

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

#include <reg52.h>

 

sbit KEY_IN_1  = P2^4;

sbit KEY_IN_2  = P2^5;

sbit KEY_IN_3  = P2^6;

sbit KEY_IN_4  = P2^7;

sbit KEY_OUT_1 = P2^3;

sbit KEY_OUT_2 = P2^2;

sbit KEY_OUT_3 = P2^1;

sbit KEY_OUT_4 = P2^0;

 

unsigned char code KeyCodeMap[4][4] = { //矩陣按鍵編號到標準鍵盤鍵碼的映射表

    { '1',  '2',  '3', 0x26 }, //數字鍵1、數字鍵2、數字鍵3、向上鍵

    { '4',  '5',  '6', 0x25 }, //數字鍵4、數字鍵5、數字鍵6、向左鍵

    { '7',  '8',  '9', 0x28 }, //數字鍵7、數字鍵8、數字鍵9、向下鍵

    { '0', 0x1B, 0x0D, 0x27 }  //數字鍵0ESC鍵、  回車鍵、 向右鍵

};

unsigned char pdata KeySta[4][4] = {  //全部矩陣按鍵的當前狀態

    {1, 1, 1, 1},  {1, 1, 1, 1},  {1, 1, 1, 1},  {1, 1, 1, 1}

};

 

extern void KeyAction(unsigned char keycode);

 

/* 按鍵驅動函數,檢測按鍵動作,調度相應動作函數,需在主循環中調用 */

void KeyDriver()

{

    unsigned char i, j;

    static unsigned char pdata backup[4][4] = {  //按鍵值備份,保存前一次的值

        {1, 1, 1, 1},  {1, 1, 1, 1},  {1, 1, 1, 1},  {1, 1, 1, 1}

    };

    

    for (i=0; i<4; i++)  //循環檢測4*4的矩陣按鍵

    {

        for (j=0; j<4; j++)

        {

            if (backup[i][j] != KeySta[i][j])    //檢測按鍵動作

            {

                if (backup[i][j] != 0)            //按鍵按下時執行動作

                {

                    KeyAction(KeyCodeMap[i][j]); //調用按鍵動作函數

                }

                backup[i][j] = KeySta[i][j];     //刷新前一次的備份值

            }

        }

    }

}

/* 按鍵掃描函數,需在定時中斷中調用,推薦調用間隔1ms */

void KeyScan()

{

    unsigned char i;

    static unsigned char keyout = 0;   //矩陣按鍵掃描輸出索引

    static unsigned char keybuf[4][4] = {  //矩陣按鍵掃描緩衝區

        {0xFF, 0xFF, 0xFF, 0xFF},  {0xFF, 0xFF, 0xFF, 0xFF},

        {0xFF, 0xFF, 0xFF, 0xFF},  {0xFF, 0xFF, 0xFF, 0xFF}

    };

 

    //將一行的4個按鍵值移入緩衝區

    keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;

    keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;

    keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;

    keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;

    //消抖後更新按鍵狀態

    for (i=0; i<4; i++)  //每行4個按鍵,所以循環4

    {

        if ((keybuf[keyout][i] & 0x0F) == 0x00)

        {   //連續4次掃描值爲0,即4*4ms內都是按下狀態時,可認爲按鍵已穩定的按下

            KeySta[keyout][i] = 0;

        }

        else if ((keybuf[keyout][i] & 0x0F) == 0x0F)

        {   //連續4次掃描值爲1,即4*4ms內都是彈起狀態時,可認爲按鍵已穩定的彈起

            KeySta[keyout][i] = 1;

        }

    }

    //執行下一次的掃描輸出

    keyout++;          //輸出索引遞增

    keyout &= 0x03;   //索引值加到4即歸零

    switch (keyout)   //根據索引,釋放當前輸出引腳,拉低下次的輸出引腳

    {

        case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;

        case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;

        case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;

        case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;

        default: break;

    }

}

keyboard.c是對之前已經用過多次的矩陣按鍵驅動的封裝,具體到某個按鍵要執行的動作函數都放到上層的main.c中實現,在這個按鍵驅動文件中只負責調用上層實現的按鍵動作函數即可。

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

#include <reg52.h>

 

unsigned char step = 0;  //操作步驟

unsigned char oprt = 0;  //運算類型

signed long num1 = 0;    //操作數1

signed long num2 = 0;    //操作數2

signed long result = 0;  //運算結果

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

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

 

void ConfigTimer0(unsigned int ms);

extern void KeyScan();

extern void KeyDriver();

extern void InitLcd1602();

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

extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);

extern void LcdFullClear();

 

void main()

{

    EA = 1;             //開總中斷

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

InitLcd1602();    //初始化液晶

    LcdShowStr(15, 1, "0");  //初始顯示一個數字0

    

    while (1)

    {

        KeyDriver();  //調用按鍵驅動

    }

}

/* 長整型數轉換爲字符串,str-字符串指針,dat-待轉換數,返回值-字符串長度 */

unsigned char LongToString(unsigned char *str, signed long dat)

{

    signed char i = 0;

    unsigned char len = 0;

    unsigned char buf[12];

    

    if (dat < 0)  //如果爲負數,首先取絕對值,並在指針上添加負號

    {

        dat = -dat;

        *str++ = '-';

        len++;

    }

    do {          //先轉換爲低位在前的十進制數組

        buf[i++] = dat % 10;

        dat /= 10;

    } while (dat > 0);

    len += i;     //i最後的值就是有效字符的個數

    while (i-- > 0)   //將數組值轉換爲ASCII碼反向拷貝到接收指針上

    {

        *str++ = buf[i] + '0';

    }

    *str = '';  //添加字符串結束符

    

    return len;   //返回字符串長度

}

/* 顯示運算符,顯示位置y,運算符類型type */

void ShowOprt(unsigned char y, unsigned char type)

{

    switch (type)

    {

        case 0: LcdShowStr(0, y, "+"); break;  //0代表+

        case 1: LcdShowStr(0, y, "-"); break;  //1代表-

        case 2: LcdShowStr(0, y, "*"); break;  //2代表*

        case 3: LcdShowStr(0, y, "/"); break;  //3代表/

        default: break;

    }

}

/* 計算器復位,清零變量值,清除屏幕顯示 */

void Reset()

{

    num1 = 0;

    num2 = 0;

    step = 0;

    LcdFullClear();

}

/* 數字鍵動作函數,n-按鍵輸入的數值 */

void NumKeyAction(unsigned char n)

{

    unsigned char len;

    unsigned char str[12];

    

    if (step > 1)  //如計算已完成,則重新開始新的計算

    {

        Reset();

    }

    if (step == 0)  //輸入第一操作數

    {

        num1 = num1*10 + n;               //輸入數值累加到原操作數上

        len = LongToString(str, num1);  //新數值轉換爲字符串

        LcdShowStr(16-len, 1, str);     //顯示到液晶第二行上

    }

    else            //輸入第二操作數

    {

        num2 = num2*10 + n;               //輸入數值累加到原操作數上

        len = LongToString(str, num2);  //新數值轉換爲字符串

        LcdShowStr(16-len, 1, str);     //顯示到液晶第二行上

    }

}

/* 運算符按鍵動作函數,運算符類型type */

void OprtKeyAction(unsigned char type)

{

    unsigned char len;

    unsigned char str[12];

    

    if (step == 0)  //第二操作數尚未輸入時響應,即不支持連續操作

    {

        len = LongToString(str, num1); //第一操作數轉換爲字符串

        LcdAreaClear(0, 0, 16-len);    //清除第一行左邊的字符位

        LcdShowStr(16-len, 0, str);    //字符串靠右顯示在第一行

        ShowOprt(1, type);               //在第二行顯示操作符

        LcdAreaClear(1, 1, 14);         //清除第二行中間的字符位

        LcdShowStr(15, 1, "0");         //在第二行最右端顯示0

        oprt = type;                      //記錄操作類型

        step = 1;

    }

}

/* 計算結果函數 */

void GetResult()

{

    unsigned char len;

    unsigned char str[12];

    

    if (step == 1) //第二操作數已輸入時才執行計算

    {

        step = 2;

        switch (oprt)  //根據運算符類型計算結果,未考慮溢出問題

        {

            case 0: result = num1 + num2; break;

            case 1: result = num1 - num2; break;

            case 2: result = num1 * num2; break;

            case 3: result = num1 / num2; break;

            default: break;

        }

        len = LongToString(str, num2);  //原第二操作數和運算符顯示到第一行

        ShowOprt(0, oprt);

        LcdAreaClear(1, 0, 16-1-len);

        LcdShowStr(16-len, 0, str);

        len = LongToString(str, result);  //計算結果和等號顯示在第二行

        LcdShowStr(0, 1, "=");

        LcdAreaClear(1, 1, 16-1-len);

        LcdShowStr(16-len, 1, str);

    }

}

/* 按鍵動作函數,根據鍵碼執行相應的操作,keycode-按鍵鍵碼 */

void KeyAction(unsigned char keycode)

{

    if  ((keycode>='0') && (keycode<='9'))  //輸入字符

    {

        NumKeyAction(keycode - '0');

    }

    else if (keycode == 0x26)  //向上鍵,+

    {

        OprtKeyAction(0);

    }

    else if (keycode == 0x28)  //向下鍵,-

    {

        OprtKeyAction(1);

    }

    else if (keycode == 0x25)  //向左鍵,*

    {

        OprtKeyAction(2);

    }

    else if (keycode == 0x27)  //向右鍵,÷

    {

        OprtKeyAction(3);

    }

    else if (keycode == 0x0D)  //回車鍵,計算結果

    {

        GetResult();

    }

    else if (keycode == 0x1B)  //Esc鍵,清除

    {

        Reset();

        LcdShowStr(15, 1, "0");

    }

}

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

void ConfigTimer0(unsigned int ms)

{

    unsigned long tmp;  //臨時變量

    

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

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

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

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

    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;

    KeyScan();   //按鍵掃描

}

      main.c文件實現所有應用層的操作函數,即計算器功能所需要信息顯示、按鍵動作響應等,另外還包括主循環和定時中斷的調度。

      通過這樣一個程序,大家一方面學習如何進行多個.c文件的編程,另外一個方面學會多個函數之間的靈活調用。可以把這個程序看成是一個簡單的小項目,學習一下項目編程都是如何進行和佈局的。不要把項目想象的太難,再複雜的項目也是這種簡單程序的組合和擴展而已。

1.1 串口通信機制和實用的串口例程

      我們前邊學串口通信的時候,比較注重的是串口底層時序上的操作過程,所以例程都是簡單的收發字符或者字符串。在實際應用中,往往串口還要和電腦上的上位機軟件進行交互,實現電腦軟件發送不同的指令,單片機對應執行不同操作的功能,這就要求我們組織一個比較合理的通信機制和邏輯關係,用來實現我們想要的結果。


      本節所提供程序的功能是,通過電腦串口調試助手下發三個不同的命令,第一條指令:buzz on可以讓蜂鳴器響;第二條指令:buzz off可以讓蜂鳴器不響;第三條指令:showstr ,這個命令空格後邊,可以添加任何字符串,讓後邊的字符串在1602液晶上顯示出來,同時不管發送什麼命令,單片機收到後把命令原封不動的再通過串口發送給電腦,以表示“我收到了……你可以檢查下對不對”。這樣的感覺是不是更像是一個小項目了呢?


      對於串口通信部分來說,單片機給電腦發字符串好說,有多大的數組,我們就發送多少個字節即可,但是單片機接收數據,接收多少個才應該是一幀完整的數據呢?數據接收起始頭在哪裏,結束在哪裏?這些我們在接收到數據前都是無從得知的。那怎麼辦呢?


      我們的編程思路基於這樣一種通常的事實:當需要發送一幀(多個字節)數據時,這些數據都是連續不斷的發送的,即發送完一個字節後會緊接着發送下一個字節,期間沒有間隔或間隔很短,而當這一幀數據都發送完畢後,就會間隔很長一段時間(相對於連續發送時的間隔來講)不再發送數據,也就是通信總線上會空閒一段較長的時間。於是我們就建立這樣一種程序機制:設置一個軟件的總線空閒定時器,這個定時器在有數據傳輸時(從單片機接收角度來說就是接收到數據時)清零,而在總線空閒時(也就是沒有接收到數據時)時累加,當它累加到一定時間(例程裏是30ms)後,我們就可以認定一幀完整的數據已經傳輸完畢了,於是告訴其它程序可以來處理數據了,本次的數據處理完後就恢復到初始狀態,再準備下一次的接收。那麼這個用於判定一幀結束的空閒時間取多少合適呢?它取決於多個條件,並沒有一個固定值,我們這裏介紹幾個需要考慮的原則:第一,這個時間必須大於波特率週期,很明顯我們的單片機接收中斷產生是在一個字節接收完畢後,也就是一個時刻點,而其接收過程我們的程序是無從知曉的,因此在至少一個波特率週期內你絕不能認爲空閒已經時間達到了。第二,要考慮發送方的系統延時,因爲不是所有的發送方都能讓數據嚴格無間隔的發送,因爲軟件響應、關中斷、系統臨界區等等操作都會引起延時,所以還得再附加幾個到十幾個ms的時間。我們選取的30ms是一個折中的經驗值,它能適應大部分的波特率(大於1200)和大部分的系統延時(PC機或其它單片機系統)情況。


      我先把這個程序最重要的UART.c文件中的程序貼出來,一點點給大家解析,這個是實際項目開發常用的用法,大家一定要認真弄明白。

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

#include <reg52.h>

 

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)

{

    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

}

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

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

{

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

    {

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

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

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

    }

}

/* 串口數據讀取函數,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));  //將接收到的命令讀取到緩衝區中

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

    }

}

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

void InterruptUART() interrupt 4

{

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

    {

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

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

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

            bufRxd[cntRxd++] = SBUF;

        }

    }

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

    {

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

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

    }

}

      大家可以對照註釋和前面的講解分析下這個Uart.c文件,在這裏指出其中的兩個要點希望大家多注意下。


      1、接收數據的處理,在串口中斷中,將接收到的字節都存入緩衝區bufRxd中,同時利用另外的定時器中斷通過間隔調用UartRxMonitor來監控一幀數據是否接收完畢,判定的原則就是我們前面介紹的空閒時間,當判定一幀數據結束完畢時,設置flagFrame標誌,主循環中可以通過調用UartDriver來檢測該標誌,並處理接收到的數據。當要處理接收到的數據時,先通過串口讀取函數UartRead把接收緩衝區bufRxd中的數據讀取出來,然後再對讀到的數據進行判斷處理。也許你會說,既然數據都已經接收到bufRxd中了,那我直接在這裏面用不就行了嘛,何必還得再拷貝到另一個地方去呢?我們設計這種雙緩衝的機制,主要是爲了提高串口接收到響應效率:首先如果你在bufRxd中處理數據,那麼這時侯就不能再接收任何數據,因爲新接收的數據會破壞原來的數據,造成其不完整和混亂;其次,這個處理過程可能會耗費較長的時間,比如說上位機現在就給你發來一個延時顯示的命令,那麼在這個延時的過程中你都無法去接收新的命令,在上位機看來就是你暫時失去響應了。而使用這種雙緩衝機制就可以大大改善這個問題,因爲數據拷貝所需的時間是相當短的,而只要拷貝出去後,bufRxd就可以馬上準備去接收新數據了。


2、串口數據寫入函數UartWrite,它把數據指針buf指向的數據塊連續的由串口發送出去。雖然我們的串口程序啓用了中斷,但這裏的發送功能卻沒有在中斷中完成,而是仍然靠查詢發送中斷標誌flagTxd(因中斷函數內必須清零TI,否則中斷會重複進入執行,所以另置了一個flagTxd來代替TI)來完成,當然也可以採用先把發送數據拷貝到一個緩衝區中,然後再在中斷中發緩衝區數據發送出去的方式,但這樣一是要耗費額外的內存,二是使程序更復雜。這裏也還是想告訴大家,簡單方式可以解決的問題就不要搞得更復雜。


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

#include <reg52.h>

 

sbit BUZZ = P1^6;  //蜂鳴器控制引腳

 

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

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);

extern void InitLcd1602();

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

extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);

 

void main()

{

    EA = 1;              //開總中斷

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

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

    InitLcd1602();     //初始化液晶

    

    while (1)

    {

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

    }

}

/* 內存比較函數,比較兩個指針所指向的內存數據是否相同,

   ptr1-待比較指針1ptr2-待比較指針2len-待比較長度

   返回值-兩段內存數據完全相同時返回1,不同返回0 */

bit CmpMemory(unsigned char *ptr1, unsigned char *ptr2, unsigned char len)

{

    while (len--)

    {

        if (*ptr1++ != *ptr2++)  //遇到不相等數據時即刻返回0

        {

            return 0;

        }

    }

    return 1;  //比較完全部長度數據都相等則返回1

}

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

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

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

{

    unsigned char i;

    unsigned char code cmd0[] = "buzz on";   //開蜂鳴器命令

    unsigned char code cmd1[] = "buzz off";  //關蜂鳴器命令

    unsigned char code cmd2[] = "showstr ";  //字符串顯示命令

    unsigned char code cmdLen[] = {           //命令長度彙總表

        sizeof(cmd0)-1, sizeof(cmd1)-1, sizeof(cmd2)-1,

    };

    unsigned char code *cmdPtr[] = {          //命令指針彙總表

        &cmd0[0],  &cmd1[0],  &cmd2[0],

    };

 

    for (i=0; i<sizeof(cmdLen); i++)  //遍歷命令列表,查找相同命令

    {

        if (len >= cmdLen[i])  //首先接收到的數據長度要不小於命令長度

        {

            if (CmpMemory(buf, cmdPtr[i], cmdLen[i]))  //比較相同時退出循環

            {

                break;

            }

        }

    }

    switch (i)  //循環退出時i的值即是當前命令的索引值

    {

        case 0:

            flagBuzzOn = 1; //開啓蜂鳴器

            break;

        case 1:

            flagBuzzOn = 0; //關閉蜂鳴器

            break;

        case 2:

            buf[len] = '';  //爲接收到的字符串添加結束符

            LcdShowStr(0, 0, buf+cmdLen[2]);  //顯示命令後的字符串

            i = len - cmdLen[2];                //計算有效字符個數

            if (i < 16)  //有效字符少於16時,清除液晶上的後續字符位

            {

                LcdAreaClear(i, 0, 16-i);

            }

            break;

        default:   //未找到相符命令時,給上機發送“錯誤命令”的提示

            UartWrite("bad command.rn", sizeof("bad command.rn")-1);

            return;

    }

    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;

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

        BUZZ = ~BUZZ;

    else

        BUZZ = 1;

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

}

      main函數和主循環的結構我們已經做過很多了,就不多說了,這裏重點把串口接收數據的具體解析方法給大家分析一下,這種用法具有很強的普遍性,掌握並靈活運用它可以使你將來的開發工作事半功倍。


      首先來看CmpMemory函數,這個函數很簡單,就是比較兩段內存數據,通常都是數組中的數據,函數接收兩段數據的指針,然後逐個字節比較——if (*ptr1++ != *ptr2++),這行代碼既完成了兩個指針指向的數據的比較,又在比較完後把兩個指針都各自+1,從這裏是不是也能領略到一點C語言的簡潔高效的魅力呢。這個函數的用處自然就是用來比較我們接收到的數據和事先放在程序裏的命令字符串是否相同,從而找出相符的命令了。


      接下來是UartAction函數對接收數據的解析和處理方法,先把接收的數據與所支持的命令字符串逐條比較,這個比較中首先要確保接收的長度大於命令字符串的長度,然後再用上述的CmpMemory函數逐字節比較,如果比較相同就立即退出循環,不同則繼續對比下一條命令。當找到相符的命令字符串時,最終i的值就是該命令在其列表中的索引位置,當遍歷完命令列表都沒有找到相符的命令時,最終i的值將等於命令總數,那麼接下來就用switch語句根據i的值來執行具體的動作,這個就不需要再詳細說明了。


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

#include <reg52.h>

 

#define LCD1602_DB  P0

sbit LCD1602_RS = P1^0;

sbit LCD1602_RW = P1^1;

sbit LCD1602_E  = P1^5;

 

/* 等待液晶準備好 */

void LcdWaitReady()

{

    unsigned char sta;

    

    LCD1602_DB = 0xFF;

    LCD1602_RS = 0;

    LCD1602_RW = 1;

    do {

        LCD1602_E = 1;

        sta = LCD1602_DB; //讀取狀態字

        LCD1602_E = 0;

    } while (sta & 0x80); //bit7等於1表示液晶正忙,重複檢測直到其等於0爲止

}

/* LCD1602液晶寫入一字節命令,cmd-待寫入命令值 */

void LcdWriteCmd(unsigned char cmd)

{

    LcdWaitReady();

    LCD1602_RS = 0;

    LCD1602_RW = 0;

    LCD1602_DB = cmd;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

/* LCD1602液晶寫入一字節數據,dat-待寫入數據值 */

void LcdWriteDat(unsigned char dat)

{

    LcdWaitReady();

    LCD1602_RS = 1;

    LCD1602_RW = 0;

    LCD1602_DB = dat;

    LCD1602_E  = 1;

    LCD1602_E  = 0;

}

/* 設置顯示RAM起始地址,亦即光標位置,(x,y)-對應屏幕上的字符座標 */

void LcdSetCursor(unsigned char x, unsigned char y)

{

    unsigned char addr;

    

    if (y == 0)  //由輸入的屏幕座標計算顯示RAM的地址

        addr = 0x00 + x;  //第一行字符地址從0x00起始

    else

        addr = 0x40 + x;  //第二行字符地址從0x40起始

    LcdWriteCmd(addr | 0x80);  //設置RAM地址

}

/* 在液晶上顯示字符串,(x,y)-對應屏幕上的起始座標,str-字符串指針 */

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

{

    LcdSetCursor(x, y);   //設置起始地址

    while (*str != '')  //連續寫入字符串數據,直到檢測到結束符

    {

        LcdWriteDat(*str++);

    }

}

/* 區域清除,清除從(x,y)座標起始的len個字符位 */

void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len)

{

    LcdSetCursor(x, y);   //設置起始地址

    while (len--)          //連續寫入空格

    {

        LcdWriteDat(' ');

    }

}

/* 初始化1602液晶 */

void InitLcd1602()

{

    LcdWriteCmd(0x38);  //16*2顯示,5*7點陣,8位數據接口

    LcdWriteCmd(0x0C);  //顯示器開,光標關閉

    LcdWriteCmd(0x06);  //文字不動,地址自動+1

    LcdWriteCmd(0x01);  //清屏

}

      液晶文件與上一個例程的液晶文件基本是一樣的,唯一的區別是刪掉了一個本例中用不到的全屏清屏函數,其實留着這個函數也沒關係,只是Keil會提示一個警告,告訴你有未被調用的函數而已,可以不理會它。

      經過這幾個多文件工程的練習後,大家是否發現,在採用多文件模塊化編程後,不光是某些函數,甚至整個c文件,如有需要,我們都可以直接複製到其它的新工程中使用,非常方便功能程序的移植,這樣隨着實踐積累的增加,你會發現工作效率變得越來越高了。

1.2 練習題

1、將通信時序的邏輯理解透徹,並且能夠自己獨立看懂其它器件的時序圖。

2、根據1602整屏移動程序,改寫成整屏右移的程序。

3、掌握多.c源文件編寫代碼的方法以及調用其它文件中變量和函數的方法。

4、徹底理解實用的串口通信機制程序,能夠完全解析明白實用串口通信例程,爲今後自己獨立編寫類似程序打下基礎。



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