找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 12745|回復(fù): 5
打印 上一主題 下一主題
收起左側(cè)

第十章 習(xí)題練習(xí)與積累

  [復(fù)制鏈接]
跳轉(zhuǎn)到指定樓層
樓主
ID:1 發(fā)表于 2013-9-28 01:49 | 只看該作者 |只看大圖 回帖獎勵 |倒序瀏覽 |閱讀模式

      本章內(nèi)容主要通過一些相關(guān)例程,來提高大家的編程技巧,并且?guī)椭蠹疫M行一些算法上的積累。同學(xué)們在做這部分內(nèi)容的時候,還是那句話,一定要能夠達到不看教程,獨立把程序做出來的效果,那樣才能基本上掌握相關(guān)知識點和內(nèi)容。
10.1 數(shù)字秒表實驗10.1.1 不同數(shù)據(jù)間的類型轉(zhuǎn)換
C語言中,不同數(shù)據(jù)類型之間是可以混合運算的。當表達式中的數(shù)據(jù)類型不一致時,首先轉(zhuǎn)換為同一種類型,然后再進行計算。C語言有兩種方法實現(xiàn)類型轉(zhuǎn)換,一是自動類型轉(zhuǎn)換,另外一種是強制類型轉(zhuǎn)換。這塊內(nèi)容是比較繁雜的,因此我們根據(jù)我們常用的編程應(yīng)用來講部分相關(guān)內(nèi)容。
當不同數(shù)據(jù)類型之間混合運算的時候,不同類型的數(shù)據(jù)首先會轉(zhuǎn)換為同一類型,轉(zhuǎn)換的主要原則是:短字節(jié)的數(shù)據(jù)向長字節(jié)數(shù)據(jù)轉(zhuǎn)換。
比如:unsigned char  a ;  unsigned int b;  unsigned int c;  c = a *b;
在運算的過程中,程序會自動全部按照unsigned int型來計算。比如a=10,b=200,c的結(jié)果就是2000。那當a=100,b=700,那c70000嗎?新手最容易犯這種錯誤,大家要注意每個變量的數(shù)據(jù)類型,c的數(shù)據(jù)類型是unsigned int型,取值范圍是0~65535,70000超過65535溢出了,所以最終c的結(jié)果是(70000 - 65536) = 4464。
那要想讓c正常獲得70000這個結(jié)果,需要把c定義成一個unsigned long型。我們?nèi)绻麑懗桑?/font>unsigned char  a=100;  unsigned int  b=700;  unsigned long  c=0;  c = a *b;如果有做過實驗的同學(xué),會發(fā)現(xiàn)這個c的結(jié)果還是4464,這個是個什么情況呢?
大家注意,C語言不同類型運算的時候數(shù)值會轉(zhuǎn)換同一類型運算,但是每一步運算都會進行識別判斷,不會進行一個總的分析判斷。比如我們這個程序,ab相乘的時候,是按照unsigned int類型運算的,運算的結(jié)果也是unsigned int類型的4464,只是最終把unsigned int類型4464賦值給了一個unsigned long型的變量而已。我們在運算的時候如何避免這類問題的產(chǎn)生呢?可以采用強制類型轉(zhuǎn)換的方法。
在一個變量前邊加上一個變量類型,并且這個變量類型用小括號括起來,表示把這個變量強制轉(zhuǎn)換成括號里的變量類型。如 c = (unsigned long)a * b;由于強制類型轉(zhuǎn)換運算優(yōu)先級高于*,所以這個地方的運算是先把a轉(zhuǎn)換成一個unsigned long型的變量,而后與b相乘,根據(jù)C語言的規(guī)則b會自動轉(zhuǎn)換成一個unsigned long型的變量,而后運算完畢結(jié)果也是一個unsigned long型的,最終賦值給了c。
當不同類型變量相互賦值時,短字節(jié)的數(shù)據(jù)向長字節(jié)的變量賦值時,值不變,比如unsigned char  a=100;  unsigned int  b=700;  b = a;那么最終b的值就是100了。但是如果我們的程序是unsigned char  a=100;  unsigned int  b=700;  a=b;那么a的值僅僅是取了b的低8位,我們首先要把700變成一個16位的二進制數(shù)據(jù),然后取它的低8位出來,也就是188,這就是長字節(jié)給短字節(jié)賦值的結(jié)果。
51單片機里邊,有一種特殊情況,就是bit類型的變量,這個bit類型的強制類型轉(zhuǎn)換,是不符合上邊講的這個原則的,比如bit a = 0;  unsigned char b; a = (bit)b;這個地方要特別注意,使用bit做強制類型轉(zhuǎn)換,不是取b的最低位,而是他會判斷b這個變量是0還是非0的值,如果b0,那么a的結(jié)果就是0,如果b是任意非0的其他數(shù)字,那么a的結(jié)果都是1。
1.1.2 定時時間精準性調(diào)整
我們的6.5.2章節(jié)有一個數(shù)碼管秒表顯示程序,那個程序是1秒數(shù)碼管加1,但是細心的同學(xué)做了實驗后,經(jīng)過長時間運行會發(fā)現(xiàn),和我們的實際的時間有了較大誤差了,那如何去調(diào)整這種誤差呢?要解決問題,先找到問題是什么原因造成的。
先補充介紹一下我們我們前邊講的中斷的內(nèi)容。其實單片機做的很智能的,當我們在看電視的時候,突然發(fā)生了水開的中斷,我們必須去提水的時候,第一,我們從電視跟前跑到廚房需要一定的時間,第二,因為我們看的電視是智能數(shù)字電視,因此在去提水之前我們可以使用遙控器將我們的電視進行暫停操作,方便回來后繼續(xù)從剛才的劇情往下進行。那暫停電視,跑到廚房提水,這一點點時間是很短的,在實際生活中可以忽略不計,但是在單片機秒表系統(tǒng),誤差會累計的,每1秒鐘都差了幾個微妙,時間一久,造成的累計誤差就不可小覷了。
單片機系統(tǒng)里,硬件進入中斷需要一定的時間,大概是幾個機器周期,還有要進行原始數(shù)據(jù)保護,就是把進中斷之前程序運行的一些變量先保存起來,這個專業(yè)詞匯叫做中斷壓棧,進入中斷后,重新給定時器THTL賦值,也需要幾個機器周期,這樣下來就會消耗一定的時間,我們得把這些時間補償回來。
方法一,使用軟件debug進行補償。
我們前邊教程講過使用debug來觀察程序運行時間,那我們可以把我們2次進入中斷的時間間隔觀察出來,看看和我們實際定時的時間相差了幾個機器周期,然后在進行定時器初值賦值的時候,進行一個調(diào)整。我們用的是11.0592M的晶振,發(fā)現(xiàn)差了幾個機器周期,就把定時器初值加上幾個機器周期,這樣相當于進行了一個補償。
方法二,使用累計誤差計算出來。
有的時候,除了程序本身存在的誤差外,硬件精度也可能會影響到時鐘的精度,比如晶振,會隨著溫度變化出現(xiàn)溫漂現(xiàn)象,就是精度和標稱值要差一點。那么我們還可以采取累計誤差的方法來提高精度。比如我們可以讓時鐘運行半個小時或者一個小時,看看最終時間差了幾秒,然后算算一共進了多少次定時器中斷,然后把這差的幾秒平均分配到每次的定時器中斷中,就可以實現(xiàn)時鐘的調(diào)整。
大家要明白,這個世界上本就沒有絕對的精度,我們只能提高精度,但是不可能消除誤差的,如果在這個基礎(chǔ)上還感覺精度不夠的話,不要著急,后邊我們會專門講時鐘芯片的,通常時鐘芯片計時的精度比單片機的精度要高一些。
10.1.3 使用字節(jié)操作修改位的技巧
這里介紹個編程小技巧,在我們編程序的時候,有的情況下,想要操作一個字節(jié)中的某一位或者幾位的時候,但是又不想改變其他位原有的值,該如何操作呢?
比如我們學(xué)定時器的時候遇到一個寄存器TCON,這個寄存器是可以進行位操作的,比如我們可以直接寫TR0 =1;TR0TCON的一個位,因為這個寄存器是允許位操作,這樣寫是沒有任何問題的。還有一個寄存器TMOD,這個寄存器是不支持位操作的,那如果我們要使用T0的模式1,我們希望達到的效果是TMOD的低4位是0001,如果我們直接寫成TMOD = 0x01的話,實際上已經(jīng)同時操作到了高4位,即屬于T1的部分,設(shè)置成了0000,如果T1定時器沒有用到的話,那我們隨便怎么樣都行,但是如果程序中既用到了T0,又用到了T1,那我們設(shè)置T0的同時已經(jīng)干擾到了T1的模式配置,這我們不希望看到的結(jié)果。
在這種情況下,就可以用我們前邊學(xué)過的"&""|"運算了。對于二進制位操作來說,不管該位原來的值是0還是1,它跟"0"進行"&&"運算,得到的結(jié)果都是0,而跟"1"進行"&&"運算,將保持原來的值不變;不管該位原來的值是0還是1,它跟"1"進行"||"運算,得到的結(jié)果都是1,而跟"0"進行"||"運算,將保持原來的值不變。
利用上述這個規(guī)律,我們就可以著手解決剛才的問題了。如果我們現(xiàn)在要設(shè)置TMOD的定時器0工作在模式1下,又不干擾定時器1的配置,我們可以進行這樣的操作:TMOD = TMOD & 0xF0; TMOD = TMOD | 0x01;第一步與0xF0后,TMOD的高4位不變,低4位清零,變成了xxxx0000;然后再進行第二步和0x01進行或運算,那高7位均不變,最低位變成1了,這樣就完成了只將低4位的值修改位0001,而高4位保持原不變的任務(wù),即只設(shè)置了T0而不影響T1。熟練掌握并靈活運用這個方法,會給你以后的編程帶來便利。
另外,在C語言中,a &= b;等價于a = a&b;同理,a |= b;等價于a = a|b;那么前邊那一段代碼就可以寫成TMOD &= 0xF0;TMOD |= 0x01這樣的簡寫形式。這種寫法可以一定程度上簡化代碼,是C語言的一種編程風格。
10.1.4 數(shù)碼管刷新函數(shù)算法改進
在學(xué)習(xí)數(shù)碼管動態(tài)刷新的時候,為了方便大家理解,我們程序?qū)懙募氈乱恍,給大家引入了switch的用法,隨著我們編程能力的增強,對于74HC138這種非常有規(guī)律的數(shù)字器件,我們在編程上也可以改進一下邏輯算法,讓程序變的更簡潔。這種邏輯算法,通常不是靠學(xué)一下可以全部掌握的,而是通過不斷的編寫程序以及研究別人的程序一點點積累起來的,從今天開始,大家就要開始積累。
前邊動態(tài)刷新函數(shù)我們是這么寫的:
        switch(j)
            {
                        case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]]; break;
                        case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]]; break;
                        case 2:         ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]]; break;
                        case 3:         ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]]; break;       
                        case 4:         ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]]; break;
                        case 5:         ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]]; break;
        default: break;
            }
首先我們進行第一步改進,寫成:
        switch(j)
            {
                        case 0: ADDR0=0; ADDR1=0; ADDR2=0; break;
                        case 1: ADDR0=1; ADDR1=0; ADDR2=0; break;
                        case 2:         ADDR0=0; ADDR1=1; ADDR2=0; break;
                        case 3:         ADDR0=1; ADDR1=1; ADDR2=0; break;       
                        case 4:         ADDR0=0; ADDR1=0; ADDR2=1; break;
                        case 5:         ADDR0=1; ADDR1=0; ADDR2=1; break;
        default: break;
            }                                                            
P0=LedChar[LedNumber[j++]];  
if(6==j) j=0;
這種寫法已經(jīng)比上邊那種寫法簡單多了,我們還要繼續(xù)簡化。我們來看,ADDR0P1的第0位,ADDR1P1的第1位,ADDR2P1的第2位,我們可以看出來,程序中的case 0case 5的過程中,P1的這低3位的值分別是000,001,010,011,100101。轉(zhuǎn)換成十進制,也就是從05。那我們程序就可以進一步改進,寫成以下函數(shù)形式:
void LedScan()  //LED顯示掃描函數(shù)
{
    static unsigned char index = 0;
   
    P0 = 0xFF;                 //關(guān)閉所有段選位,顯示消隱
    P1 = (P1 & 0xF8) | index;  //位選索引值賦值到P1口低3
    P0 = LedNumber[index];       //相應(yīng)顯示緩沖區(qū)的值賦值到P0
    if (index < 5)             //位選索引0-5循環(huán),因有6個數(shù)碼管
        index++;
    else
        index = 0;
}
大家看看,P1 = (P1 & 0xF8) | index;這行代碼就利用了上面講到的"&""|"運算來將index的低3位直接賦值到P1口的低3位上,這樣寫是不是要簡潔的多,也巧妙的多,同樣可以完美實現(xiàn)動態(tài)刷新的功能。
10.1.5 秒表程序
做了一個秒表程序給同學(xué)們做參考,程序中涉及到的知識點我們幾乎都講過了,涉及到了定時器、數(shù)碼管、中斷、按鍵等多個知識點。此程序是多知識點同時應(yīng)用到一個程序中的小綜合,因此需要大家完全消化掉。這種小綜合也是將來做大項目程序的一個基礎(chǔ),因此還是老規(guī)矩,大家邊抄邊理解,理解透徹后獨立寫出來就算此關(guān)通過。
#include <reg52.h>

sbit  KEY1 = P2^4;

sbit  KEY2 = P2^5;

sbit  KEY3 = P2^6;

sbit  KEY4 = P2^7;

sbit  ADDR3 = P1^3;

sbit  ENLED = P1^4;

unsigned char code LedChar[] = {  //數(shù)碼管顯示字符轉(zhuǎn)換表

    0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,

    0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E

};

unsigned char LedBuff[6] = {  //數(shù)碼管顯示緩沖區(qū)

    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF

};

unsigned char KeySta[4] = {  //按鍵狀態(tài)緩沖區(qū)

    1, 1, 1, 1

};

bit StopwatchRunning = 0;  //秒表運行標志

bit StopwatchRefresh = 1;  //秒表計數(shù)刷新標志

unsigned char DecimalPart = 0;  //秒表的小數(shù)部分

unsigned int  IntegerPart = 0;  //秒表的整數(shù)部分

unsigned char T0RH = 0;  //T0重載值的高字節(jié)

unsigned char T0RL = 0;  //T0重載值的低字節(jié)

void ConfigTimer0(unsigned int ms);

void StopwatchDisplay();

void KeyAction();

void main ()

{

    P2 = 0xFE;  //選擇第4行按鍵以進行掃描

    P0 = 0xFF;  //P0口初始化

    ADDR3 = 1;  //選擇數(shù)碼管

    ENLED = 0;  //LED總使能

    EA = 1;     //開總中斷

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

   

    while(1)

    {

        KeyAction();

        StopwatchDisplay();

    }

}

void ConfigTimer0(unsigned int ms)  //T0配置函數(shù)

{

    unsigned long tmp;

   

    tmp = 11059200 / 12;      //定時器計數(shù)頻率

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

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

    tmp = tmp + 19;           //修正中斷響應(yīng)延時造成的誤差,運行30分鐘修正值

   

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

    T0RL = (unsigned char)tmp;

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

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

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

    TL0 = T0RL;

    ET0 = 1;        //使能T0中斷

    TR0 = 1;        //啟動T0

}

void StopwatchDisplay()  //秒表計數(shù)顯示函數(shù)

{

        unsigned char i;

    unsigned char buff[6];

   

    if (StopwatchRefresh)

    {

        StopwatchRefresh = 0;

        

        i = DecimalPart % 10;   //小數(shù)部分轉(zhuǎn)換到低2位

        buff[0] = LedChar[ I];

        i = DecimalPart / 10;

        buff[1] = LedChar[ I];

        

        buff[2] = IntegerPart % 10;         //整數(shù)部分轉(zhuǎn)換到高4位

        buff[3] = (IntegerPart / 10) % 10;

        buff[4] = (IntegerPart / 100) % 10;

        buff[5] = (IntegerPart / 1000) % 10;

        

        for (i=5; i>=3; i--) //高位的0轉(zhuǎn)換為空字符

        {

            if (buff[ I] == 0)

                buff[ I] = 0xFF;

            else

                break;

        }

        for ( ; i>=2; i--) //有效數(shù)字位轉(zhuǎn)換顯示字符

        {

            buff[ I] = LedChar[buff[ I]];

        }

        buff[2] &= 0x7F;  //點亮小數(shù)點

        

        for (i=0; i<=5; i++) //一次性拷貝到顯示緩沖區(qū),消除可能存在的顯示抖動

        {

            LedBuff[ I] = buff[ I];

        }

    }

}

void StopwatchAction()  //秒表啟停函數(shù)

{

    if (StopwatchRunning)    //已啟動則停止

        StopwatchRunning = 0;

    else                     //未啟動則啟動

        StopwatchRunning = 1;

}

void StopwatchReset()  //秒表復(fù)位函數(shù)

{

    StopwatchRunning = 0;  //停止秒表

    DecimalPart = 0;       //清零計數(shù)值

    IntegerPart = 0;

    StopwatchRefresh = 1;  //置刷新標志

}

void KeyAction()  //按鍵動作函數(shù)

{

    unsigned char i;

    static unsigned char backup[4] = {1,1,1,1};

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

    {

        if (backup[ I] != KeySta[ I])

        {

            if (backup[ I] != 0)  //按鍵按下時執(zhí)行動作

            {

                switch (i)

                {

                    case 1: StopwatchReset(); break;   //Esc鍵復(fù)位秒表

                    case 2: StopwatchAction(); break;  //回車鍵啟停秒表

                    default: break;

                }

            }

            backup[ I] = KeySta[ I];

        }

    }

}

void LedScan()  //LED顯示掃描函數(shù)

{

    static unsigned char index = 0;

   

    P0 = 0xFF;                 //關(guān)閉所有段選位,顯示消隱

    P1 = (P1 & 0xF8) | index;  //位選索引值賦值到P1口低3位

    P0 = LedBuff[index];       //相應(yīng)顯示緩沖區(qū)的值賦值到P0口

    if (index < 5)             //位選索引0-5循環(huán),因有6個數(shù)碼管

        index++;

    else

        index = 0;

}

void KeyScan()  //按鍵掃描函數(shù)

{

    unsigned char i;

    static unsigned char keybuf[4] = {  //按鍵掃描緩沖區(qū),保存一段時間內(nèi)的掃描值

        0xFF, 0xFF, 0xFF, 0xFF

    };

   

    //按鍵值移入緩沖區(qū)

    keybuf[0] = (keybuf[0] << 1) | KEY1;

    keybuf[1] = (keybuf[1] << 1) | KEY2;

    keybuf[2] = (keybuf[2] << 1) | KEY3;

    keybuf[3] = (keybuf[3] << 1) | KEY4;

    //消抖后更新按鍵狀態(tài)

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

    {

        if (keybuf[ I] == 0x00)

        {

            KeySta[ I] = 0;

        }

        else if (keybuf[ I] == 0xFF)

        {

            KeySta[ I] = 1;

        }

    }

}

void StopwatchCount()  //秒表計數(shù)函數(shù)

{

    if (StopwatchRunning)

    {

        DecimalPart++;          //小數(shù)部分+1

        if (DecimalPart >= 100) //小數(shù)部分計到100時進位到整數(shù)部分

        {

            DecimalPart = 0;

            IntegerPart++;

            if (IntegerPart >= 10000)  //整數(shù)部分計到10000時歸零

            {

                IntegerPart = 0;

            }

        }

        StopwatchRefresh = 1;

    }

}

void InterruptTimer0() interrupt 1  //T0中斷服務(wù)函數(shù)

{

    static unsigned char tmr10ms = 0;

    TH0 = T0RH;  //將重載值賦值到計數(shù)器

    TL0 = T0RL;

    KeyScan(); //按鍵掃描

    LedScan(); //數(shù)碼管掃描顯示

    //定時10ms進行一次秒表計數(shù)

    tmr10ms++;

    if (tmr10ms >= 5)

    {

        tmr10ms = 0;

        StopwatchCount();

    }

}10.2 PWM的學(xué)習(xí)
PWM在我們今后的單片機應(yīng)用中非常非常多,應(yīng)用的方向也很多,它的原理很簡單,但是往往應(yīng)用于不同場合上意義不完全一樣,這里我先把基本概念和基本原理給大家介紹一下,后邊遇到用的時候起碼知道是個什么東西。
PWMPulse Width Modulation的縮寫,它的中文名字是脈沖寬度調(diào)制,一種說法是它利用微處理器的數(shù)字輸出來對模擬電路進行控制的一種有效的技術(shù),其實就是使用數(shù)字信號達到一個模擬信號的效果。這是個什么概念呢?我們一步步來介紹。
首先從它的名字來看,脈沖寬度調(diào)制,就是改變脈沖寬度來實現(xiàn)不同的效果。我們先來看三組不同的脈沖信號,如圖10-1所示。
10-1 PWM波形
這是一個周期是10ms,即頻率是100Hz的波形,但是每個周期內(nèi),高低電平脈沖各不相同,這就是PWM的本質(zhì)。在這里大家要記住一個概念,叫做“占空比”。占空比是指高電平的時間占整個周期的比例。比如第一部分波形的占空比是40%,第二部分波形占空比是60%,第三部分波形占空比是80%,這就是PWM的解釋。
那為何它會對模擬電路進行控制呢?大家想一想,我們數(shù)字電路里,只有01兩種狀態(tài),比如我們教程第二課學(xué)會的點亮LED小燈那個程序,當我們寫一個LED = 0;小燈就會長亮,當我們寫一個LED = 1;小燈就會滅掉。當我們讓小燈亮和滅間隔運行的時候,小燈是閃爍。如果我們把這個間隔不斷的減小,減小到我們的肉眼分辨不出來,也就是100Hz以上的頻率,這個時候小燈表現(xiàn)出來的現(xiàn)象就是既保持亮的狀態(tài),但是亮度沒有LED = 0;的時候亮度高。那我們不斷改變時間參數(shù),讓LED = 0;的時間大于或者小于LED = 1;的時間,會發(fā)現(xiàn)亮度都不一樣,這就是模擬電路的感覺了,不再是純粹的01,還有亮度不斷變化。大家會發(fā)現(xiàn),如果我們是100Hz的信號,如圖10-1所示,假如高電平熄滅小燈,低電平點亮小燈的話,第一部分波形熄滅4ms,點亮6ms,亮度最高,第二部分熄滅6ms,點亮4ms,亮度次之,第三部分熄滅8ms,點亮2ms,亮度最低。我們用程序驗證一下。
#include <reg52.h>
sbit  PWMOUT = P0^0;
sbit  ADDR0 = P1^0;
sbit  ADDR1 = P1^1;
sbit  ADDR2 = P1^2;
sbit  ADDR3 = P1^3;
sbit  ENLED = P1^4;
unsigned char HReloadH = 0;  //高電平重載值的高字節(jié)
unsigned char HReloadL = 0;  //高電平重載值的低字節(jié)
unsigned char LReloadH = 0;  //低電平重載值的高字節(jié)
unsigned char LReloadL = 0;  //低電平重載值的低字節(jié)
void ConfigPWM(unsigned int fr, unsigned char dc);
void ClosePWM();
void main ()
{
    unsigned int i;
    P0 = 0xFF;  //P0口初始化
    ADDR0 = 0;  //選擇獨立LED
    ADDR1 = 1;
    ADDR2 = 1;
    ADDR3 = 1;
    ENLED = 0;  //LED總使能
    EA = 1;     //開總中斷
   
    while(1)
    {
        ConfigPWM(100, 10);  //頻率100Hz,占空比10%
        for (i=0; i<40000; i++);
        ClosePWM();
        ConfigPWM(100, 40);  //頻率100Hz,占空比40%
        for (i=0; i<40000; i++);
        ClosePWM();
        ConfigPWM(100, 90);  //頻率100Hz,占空比90%
        for (i=0; i<40000; i++);
        ClosePWM();
        for (i=0; i<40000; i++);
    }
}
void ConfigPWM(unsigned int fr, unsigned char dc)  //PWM配置函數(shù),fr-頻率,dc-占空比
{
    unsigned int  high, low;
    unsigned long tmp;
   
    tmp  = (11059200 / 12) / fr;  //計算一個周期所需的計數(shù)值
    high = (tmp * dc) / 100;      //計算高電平所需的計數(shù)值
    low  = tmp - high;            //計算低電平所需的計數(shù)值
    high = 65536 - high + 13;     //計算高電平的定時器重載值并修正
    low  = 65536 - low  + 13;     //計算低電平的定時器重載值并修正
   
    HReloadH = (unsigned char)(high >> 8);  //高電平重載值拆分為高低字節(jié)
    HReloadL = (unsigned char)high;
    LReloadH = (unsigned char)(low >> 8);   //低電平重載值拆分為高低字節(jié)
    LReloadL = (unsigned char)low;
   
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0為模式1
    TH0 = HReloadH; //加載T0重載值
    TL0 = HReloadL;
    ET0 = 1;        //使能T0中斷
    TR0 = 1;        //啟動T0
    PWMOUT = 1;     //輸出高電平
}
void ClosePWM()  //關(guān)閉PWM
{
    TR0 = 0;     //停止定時器
    ET0 = 0;
    PWMOUT = 1;  //輸出高電平
}
void InterruptTimer0() interrupt 1  //T0中斷服務(wù)函數(shù)
{
    if (PWMOUT == 1)  //當前輸出為高電平時,裝載低電平值并輸出低電平
    {
        TH0 = LReloadH;
        TL0 = LReloadL;
        PWMOUT = 0;
    }
    else              //當前輸出為低電平時,裝載高電平值并輸出高電平
    {
        TH0 = HReloadH;
        TL0 = HReloadL;
        PWMOUT = 1;
    }
}
大家下載了這個程序,會發(fā)現(xiàn)小燈從最亮到滅一共4個亮度等級。如果我們讓亮度等級更多,并且讓亮度等級連續(xù)起來,會產(chǎn)生一個小燈漸變的效果,和人呼吸有點類似,所以我們習(xí)慣上稱之為呼吸燈,程序代碼如下,這個程序用了2個定時器2個中斷,這是我們第一次這樣用,大家可以學(xué)習(xí)一下。我們來試試這個程序,試完了大家一定要自己關(guān)閉教程把程序?qū)懗鰜恚杏洝?/font>
#include <reg52.h>
sbit  PWMOUT = P0^0;
sbit  ADDR0 = P1^0;
sbit  ADDR1 = P1^1;
sbit  ADDR2 = P1^2;
sbit  ADDR3 = P1^3;
sbit  ENLED = P1^4;
unsigned long PeriodCnt = 0; //PWM周期計數(shù)值
unsigned char HReloadH = 0;  //高電平重載值的高字節(jié)
unsigned char HReloadL = 0;  //高電平重載值的低字節(jié)
unsigned char LReloadH = 0;  //低電平重載值的高字節(jié)
unsigned char LReloadL = 0;  //低電平重載值的低字節(jié)
unsigned char T1RH = 0;   //T1重載值的高字節(jié)
unsigned char T1RL = 0;   //T1重載值的低字節(jié)
void ConfigTimer1(unsigned int ms);
void ConfigPWM(unsigned int fr, unsigned char dc);
void main ()
{
    P0 = 0xFF;  //P0口初始化
    ADDR0 = 0;  //選擇獨立LED
    ADDR1 = 1;
    ADDR2 = 1;
    ADDR3 = 1;
    ENLED = 0;  //LED總使能
    EA = 1;     //開總中斷
   
    ConfigPWM(100, 10); //配置并啟動PWM
    ConfigTimer1(50);   //T1定時調(diào)整占空比
   
    while(1);
}
void ConfigTimer1(unsigned int ms)  //T1配置函數(shù)
{
    unsigned long tmp;
   
    tmp = 11059200 / 12;      //定時器計數(shù)頻率
    tmp = (tmp * ms) / 1000;  //計算所需的計數(shù)值
    tmp = 65536 - tmp;        //計算定時器重載值
    tmp = tmp + 11;           //修正中斷響應(yīng)延時造成的誤差
   
    T1RH = (unsigned char)(tmp >> 8);  //定時器重載值拆分為高低字節(jié)
    T1RL = (unsigned char)tmp;
    TMOD &= 0x0F;   //清零T1的控制位
    TMOD |= 0x10;   //配置T1為模式1
    TH1 = T1RH;     //加載T1重載值
    TL1 = T1RL;
    ET1 = 1;        //使能T1中斷
    TR1 = 1;        //啟動T1
}
void ConfigPWM(unsigned int fr, unsigned char dc)  //PWM配置函數(shù),fr-頻率,dc-占空比
{
    unsigned int high, low;
   
    PeriodCnt = (11059200 / 12) / fr;  //計算一個周期所需的計數(shù)值
    high = (PeriodCnt * dc) / 100;     //計算高電平所需的計數(shù)值
    low  = PeriodCnt - high;           //計算低電平所需的計數(shù)值
    high = 65536 - high + 13;          //計算高電平的定時器重載值并修正
    low  = 65536 - low  + 13;          //計算低電平的定時器重載值并修正
   
    HReloadH = (unsigned char)(high >> 8);  //高電平重載值拆分為高低字節(jié)
    HReloadL = (unsigned char)high;
    LReloadH = (unsigned char)(low >> 8);   //低電平重載值拆分為高低字節(jié)
    LReloadL = (unsigned char)low;
   
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0為模式1
    TH0 = HReloadH; //加載T0重載值
    TL0 = HReloadL;
    ET0 = 1;        //使能T0中斷
    TR0 = 1;        //啟動T0
    PWMOUT = 1;     //輸出高電平
}
void AdjustDutyCycle(unsigned char dc)  //占空比調(diào)整函數(shù),頻率不變只調(diào)整占空比
{
    unsigned int  high, low;
   
    high = (PeriodCnt * dc) / 100;     //計算高電平所需的計數(shù)值
    low  = PeriodCnt - high;           //計算低電平所需的計數(shù)值
    high = 65536 - high + 13;          //計算高電平的定時器重載值并修正
    low  = 65536 - low  + 13;          //計算低電平的定時器重載值并修正
    HReloadH = (unsigned char)(high >> 8);  //高電平重載值拆分為高低字節(jié)
    HReloadL = (unsigned char)high;
    LReloadH = (unsigned char)(low >> 8);   //低電平重載值拆分為高低字節(jié)
    LReloadL = (unsigned char)low;
}
void InterruptTimer0() interrupt 1  //T0中斷服務(wù)函數(shù),產(chǎn)生PWM
{
    if (PWMOUT == 1)  //當前輸出為高電平時,裝載低電平值并輸出低電平
    {
        TH0 = LReloadH;
        TL0 = LReloadL;
        PWMOUT = 0;
    }
    else              //當前輸出為低電平時,裝載高電平值并輸出高電平
    {
        TH0 = HReloadH;
        TL0 = HReloadL;
        PWMOUT = 1;
    }
}
void InterruptTimer1() interrupt 3  //T1中斷服務(wù)函數(shù),定時動態(tài)調(diào)整占空比
{
    static bit br = 0;
    static unsigned char index = 0;
    unsigned char code table[13] = {  //占空比調(diào)整表
        5, 18, 30, 41, 51, 60, 68, 75, 81, 86, 90, 93, 95
    };
    TH1 = T1RH;  //重新加載T1重載值
    TL1 = T1RL;
   
    AdjustDutyCycle(table[index]); //調(diào)整PWM的占空比
    if (br == 0)  //逐步增大占空比
    {
        index++;
        if (index >= 12)
        {
            br = 1;
        }
    }
    else          //逐步減小占空比
    {
        index--;
        if (index == 0)
        {
            br = 0;
        }
    }
}
    呼吸燈寫出來后,其他各種效果的燈光閃爍都應(yīng)該可以做出來,大家看到的KTV里邊那絢麗的燈光閃爍,其實就是采用的PWM技術(shù)控制的。
10.3 交通燈實驗
同學(xué)們在學(xué)習(xí)技術(shù)的時候,一定要多動腦筋,遇到問題后,三思而后問。有些時候你考慮的和真理就差一點點了,沒有堅持下去,別人告訴你后你才恍然大悟。這樣得到的結(jié)論,可以讓你學(xué)到知識,但是卻培養(yǎng)不了你的邏輯思維能力。不是不能問,而是要在認真思考的基礎(chǔ)上提問。
有同學(xué)有疑問,板子上只有8個流水燈,那如果我要做很多個流水燈一起花樣顯示怎么辦呢?那我們在講課的時候其實都提到過了,板子上是有8個流水燈,還有6個數(shù)碼管,還有1個點陣LED,一個數(shù)碼管相當于8個小燈,一個點陣LED相當于64個小燈,那如果全部算上的話,我們板子上實際共接了8+6*8+64=120個小燈,你如果單獨只接小燈,花樣燈就做出來了。
還有同學(xué)問,板子上流水燈和數(shù)碼管可以一起工作嗎?如何一起工作呢?我們剛說了,一個數(shù)碼管是8個小燈,但是大家反過來想一想,8個流水燈是不是相當于一個數(shù)碼管嗎。那板子上6個數(shù)碼管我們可以讓他們同時亮,7個數(shù)碼管就不會了嗎?當然了,思考的習(xí)慣是要慢慢培養(yǎng)的,想不到的同學(xué)繼續(xù)努力,每天前進一小步,堅持一段時間后回頭看看,就會發(fā)現(xiàn)你學(xué)會了很多。
發(fā)一個交通燈的程序給大家做學(xué)習(xí)參考。因為板子資源有限,所以我把左邊LED8LED9一起亮作為綠燈,把LED5LED6一起亮作為黃燈,把LED2LED3一起亮作為紅燈,用數(shù)碼管做倒計時,做了一個簡易的交通燈程序給大家做參考學(xué)習(xí),讓LED和數(shù)碼管同時參與工作。
#include <reg52.h>
sbit  KEY1 = P2^4;
sbit  KEY2 = P2^5;
sbit  KEY3 = P2^6;
sbit  KEY4 = P2^7;
sbit  ADDR3 = P1^3;
sbit  ENLED = P1^4;
unsigned char code LedChar[] = {  //數(shù)碼管顯示字符轉(zhuǎn)換表
    0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
    0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[7] = {  //數(shù)碼管+獨立LED顯示緩沖區(qū)
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
bit flag1s = 1;          //1秒定時標志
unsigned char T0RH = 0;  //T0重載值的高字節(jié)
unsigned char T0RL = 0;  //T0重載值的低字節(jié)
void ConfigTimer0(unsigned int ms);
void TrafficLight();
void main ()
{
    P2 = 0xFE;  //選擇第4行按鍵以進行掃描
    P0 = 0xFF;  //P0口初始化
    ADDR3 = 1;  //選擇數(shù)碼管
    ENLED = 0;  //LED總使能
    EA = 1;     //開總中斷
            ConfigTimer0(1);  //配置T0定時1ms
   
    while(1)
    {
        if (flag1s)
        {   //每秒執(zhí)行一次
            flag1s = 0;
            TrafficLight();
        }
    }
}
void ConfigTimer0(unsigned int ms)  //T0配置函數(shù)
{
    unsigned long tmp;
   
    tmp = 11059200 / 12;      //定時器計數(shù)頻率
    tmp = (tmp * ms) / 1000;  //計算所需的計數(shù)值
    tmp = 65536 - tmp;        //計算定時器重載值
    tmp = tmp + 17;           //修正中斷響應(yīng)延時造成的誤差
   
    T0RH = (unsigned char)(tmp >> 8);  //定時器重載值拆分為高低字節(jié)
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0為模式1
    TH0 = T0RH;     //加載T0重載值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中斷
    TR0 = 1;        //啟動T0
}
void TrafficLight()
{
    static unsigned char color = 2;  //交通燈顏色索引,0-綠色,1-黃色,2-紅色
    static unsigned char timer = 0;  //交通燈倒計時定時器
   
    if (timer == 0) //倒計時到0時,切換交通燈
    {
        switch (color)
        {   //左端兩個LED代表綠燈,中間兩個LED代表黃燈,右端兩個LED代表紅燈,
            case 0: color=1; LedBuff[6]=0xE7; timer=2;  break;  //切換到黃色,亮3
            case 1: color=2; LedBuff[6]=0xFC; timer=29; break;  //切換到紅色,亮30
            case 2: color=0; LedBuff[6]=0x3F; timer=39; break;  //切換到綠色,亮40
            default: break;
        }
    }
    else //倒計時未到0時,遞減其計數(shù)值
    {
        timer--;
    }
    LedBuff[0] = LedChar[timer%10];  //倒計時數(shù)值個位顯示
    LedBuff[1] = LedChar[timer/10];  //倒計時數(shù)值十位顯示
}
void LedScan()  //LED顯示掃描函數(shù)
{
    static unsigned char index = 0;  //LED位選索引
   
    P0 = 0xFF;                 //關(guān)閉所有段選位,顯示消隱
    P1 = (P1 & 0xF8) | index;  //位選索引值賦值到P1口低3
    P0 = LedBuff[index];       //相應(yīng)顯示緩沖區(qū)的值賦值到P0
    if (index < 6)             //位選索引0-6循環(huán),因有6個數(shù)碼管+一組獨立LED
        index++;
    else
        index = 0;
}
void InterruptTimer0() interrupt 1  //T0中斷服務(wù)函數(shù)
{
    static unsigned int tmr1s = 0;  //1秒定時器
    TH0 = T0RH;  //定時器重新加載重載值
    TL0 = T0RL;
    LedScan(); //LED掃描顯示
    tmr1s++;   //1秒定時的處理
    if (tmr1s >= 1000)
    {
        tmr1s = 0;
        flag1s = 1;
    }
}
10.4 長短按鍵的應(yīng)用10.4.1 51單片機RAM區(qū)域劃分
前邊介紹單片機資源的時候,我們提到過我們的STC89C52RC共有512字節(jié)的RAM,就是用來保存數(shù)據(jù)的,如我們定義的變量都是直接存在RAM里邊。51單片機的這512字節(jié)的RAM數(shù)據(jù)是分塊的,因此我們在訪問的時候,也要注意一些問題。
51單片機的RAM分為兩個部分,一塊是片內(nèi)RAM,一塊是片外RAM。標準的51的片內(nèi)RAM地址從0x00H~0x7F128個字節(jié),而現(xiàn)在我們用的51系列的單片機都是帶擴展片內(nèi)RAM的,RAM是從0x00~0xFF256個字節(jié)。片外RAM最大可以擴展到0x0000~0xFFFF64K字節(jié)。這里有一點大家要明白,片內(nèi)RAM和片外RAM的地址不是連起來的,片內(nèi)是從0x00開始,片外也是從0x0000開始的。以下是幾個Keil C51語言中的關(guān)鍵字,代表了RAM不同區(qū)域的劃分,大家先記一下。
data:片內(nèi)RAM0x00~0x7F
idata:片內(nèi)RAM0x00~0xFF
pdata:片外RAM0x0000~0x00FF
xdata:片外RAM0x0000~0xFFFF
大家可以看出來,dataidata的一部分,pdataxdata的一部分。為什么還這樣去區(qū)分呢?因為RAM分塊的訪問方式主要和匯編語言有關(guān),因此這塊內(nèi)容大家了解一下即可,只需要記住如何訪問速度更快即可。
我們定義一個變量a,可以這樣:unsigned char data a=0,而我們前邊定義變量時都沒有加data這個關(guān)鍵字,是因為我們在Keil默認設(shè)置下,data是可以省略的,即什么都不加的時候變量就是定義到data區(qū)域中的。data區(qū)域RAM的訪問在匯編語言中用的是直接尋址,訪問運行速度是最快的。如果你定義成idata,不僅僅可以訪問data區(qū)域,還可以訪問0x80H~0xFF的范圍,但加了idata關(guān)鍵字后,訪問的時候是利用了51單片機的通用寄存器進行間接尋址,速度較data速度慢一些,而且我們平時大多數(shù)情況下不太希望訪問到0x80H~0xFF,因為這塊通常用于中斷和函數(shù)調(diào)用的堆棧,所以在絕大多數(shù)情況下,我們使用內(nèi)部RAM的時候,只用data就可以了。
對于外部RAM來說,使用pdata定義的變量存到了外部RAM0x00~0xFF的地址范圍里,這塊地址的訪問和idata類似,都是用8位的通用寄存器進行間接尋址,而如果你定義成xdata,可以訪問的范圍更廣泛,從064k的地址都可以訪問到,但是它需要使用28位的寄存器DPTRHDPTRL來進行間接尋址,速度是最慢的。
我們的STC89C52RC共有512字節(jié)的RAM,256字節(jié)的片內(nèi)RAM256字節(jié)的片外RAM。一般情況下,我們是使用data區(qū)域,data不夠用了,我們就用xdata,如果希望程序執(zhí)行效率點,可以使用pdata關(guān)鍵字來定義。其他型號的,有更大的RAM51系列單片機,如果要使用更大的RAM,就必須得用xdata來訪問了。
10.4.2 長短按鍵
在我們的單片機系統(tǒng)中,如果我們按下一次按鍵加1,那我們第八章學(xué)到的技術(shù)就可以完成,但是我們想連續(xù)加很多數(shù)字的時候,要一次次按下這個按鍵確實不方便,我們希望我們按住按鍵的時候,數(shù)字會持續(xù)增加,這就是這節(jié)課的長短按鍵實例。
當按下一個按鍵持續(xù)時間低于1秒的時候,運行一次按鍵動作,當按下按鍵持續(xù)時間超過1秒后,每經(jīng)過200ms則自動再執(zhí)行一次按鍵動作,形成一個長按鍵效果。這個程序做的是一個定時炸彈的效果,打開開關(guān)后,數(shù)碼管顯示數(shù)字0,按下向上的按鍵數(shù)字加1,按下向下的按鍵數(shù)字減1,長按向上按鍵1秒后,數(shù)字會持續(xù)增加,長按向下按鍵1秒后,數(shù)字會持續(xù)減小。設(shè)定好數(shù)字后,按下回車按鍵,時間就會進行倒計時,當?shù)褂嫊r到0的時候,用蜂鳴器和板子上的8LED小燈做炸彈效果,蜂鳴器持續(xù)響,LED小燈全亮。
#include <reg52.h>
sbit BUZZ = P1^6;       //蜂鳴器控制引腳
sbit KEY_IN_1  = P2^4;  //矩陣按鍵的掃描輸入引腳1
sbit KEY_IN_2  = P2^5;  //矩陣按鍵的掃描輸入引腳2
sbit KEY_IN_3  = P2^6;  //矩陣按鍵的掃描輸入引腳3
sbit KEY_IN_4  = P2^7;  //矩陣按鍵的掃描輸入引腳4
sbit KEY_OUT_1 = P2^3;  //矩陣按鍵的掃描輸出引腳1
sbit KEY_OUT_2 = P2^2;  //矩陣按鍵的掃描輸出引腳2
sbit KEY_OUT_3 = P2^1;  //矩陣按鍵的掃描輸出引腳3
sbit KEY_OUT_4 = P2^0;  //矩陣按鍵的掃描輸出引腳4
sbit ADDR3 = P1^3;      //LED選擇地址線3
sbit ENLED = P1^4;      //LED總使能引腳
unsigned char code LedChar[] = {  //數(shù)碼管顯示字符轉(zhuǎn)換表
    0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
    0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[7] = {  //數(shù)碼管+獨立LED顯示緩沖區(qū)
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
unsigned char code KeyCodeMap[4][4] = { //矩陣按鍵編號到PC標準鍵盤鍵碼的映射表
    { '1',  '2',  '3', 0x26 }, //數(shù)字鍵1、數(shù)字鍵2、數(shù)字鍵3、向上鍵
    { '4',  '5',  '6', 0x25 }, //數(shù)字鍵4、數(shù)字鍵5、數(shù)字鍵6、向左鍵
    { '7',  '8',  '9', 0x28 }, //數(shù)字鍵7、數(shù)字鍵8、數(shù)字鍵9、向下鍵
    { '0', 0x1B, 0x0D, 0x27 }  //數(shù)字鍵0、ESC鍵、  回車鍵、 向右鍵
};
unsigned char KeySta[4][4] = {  //全部矩陣按鍵的當前狀態(tài)
    {1, 1, 1, 1},
    {1, 1, 1, 1},
    {1, 1, 1, 1},
    {1, 1, 1, 1}
};
unsigned long pdata KeyDownTime[4][4] = {  //每個按鍵按下的持續(xù)時間,單位ms
    {0, 0, 0, 0},
    {0, 0, 0, 0},
    {0, 0, 0, 0},
    {0, 0, 0, 0}
};
bit enBuzz = 0;  //蜂鳴器使能標志
unsigned char T0RH = 0;  //T0重載值的高字節(jié)
unsigned char T0RL = 0;  //T0重載值的低字節(jié)
bit flag1s = 0;     //1秒定時標志
bit flagStart = 0;  //倒計時啟動標志
unsigned int CountDown = 0; //倒計時計數(shù)器
void ConfigTimer0(unsigned int ms);
void DisplayNumber(unsigned int dat);
void KeyDrive();
void main(void)
{
    P0 = 0xFF;  //P0口初始化
    ADDR3 = 1;  //選擇數(shù)碼管
    ENLED = 0;  //LED總使能
    EA = 1;     //開總中斷
    ConfigTimer0(1);  //配置T0定時1ms
    DisplayNumber(CountDown);
       
    while(1)
    {
        KeyDrive();
        if (flagStart && flag1s) //倒計時啟動且1秒定時到達時,處理倒計時
        {
            flag1s = 0;
            if (CountDown > 0)   //倒計時未到0時,計數(shù)器遞減
            {
                CountDown--;
                DisplayNumber(CountDown);
                if (CountDown == 0)    //減到0時,執(zhí)行聲光報警
                {
                    enBuzz = 1;        //啟動蜂鳴器發(fā)聲
                    LedBuff[6] = 0x00; //點亮獨立LED
                }
            }
        }
    }
}
void ConfigTimer0(unsigned int ms)  //T0配置函數(shù)
{
    unsigned long tmp;
   
    tmp = 11059200 / 12;      //定時器計數(shù)頻率
    tmp = (tmp * ms) / 1000;  //計算所需的計數(shù)值
    tmp = 65536 - tmp;        //計算定時器重載值
    tmp = tmp + 31;           //修正中斷響應(yīng)延時造成的誤差
   
    T0RH = (unsigned char)(tmp >> 8);  //定時器重載值拆分為高低字節(jié)
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0為模式1
    TH0 = T0RH;     //加載T0重載值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中斷
    TR0 = 1;        //啟動T0
}
void DisplayNumber(unsigned int dat)  //將一個無符號整型數(shù)轉(zhuǎn)到數(shù)碼管顯示緩沖區(qū)以供顯示
{
    signed char i;
    unsigned char buf[6];
   
    for (i=0; i<6; i++)  //拆分為十進制的位
    {
        buf[ i] = dat % 10;
        dat /= 10;
    }
    for (i=5; i>=1; i--) //高位的0不予顯示
    {
        if (buf [ i] == 0)
            LedBuff[ i] = 0xFF;
        else
            break;
    }
    for ( ; i>=0; i--)   //有效數(shù)據(jù)位轉(zhuǎn)換為顯示字符
    {
        LedBuff[ i] = LedChar[buf[ i]];
    }
}
void KeyAction(unsigned char keycode)  //按鍵動作函數(shù),根據(jù)鍵碼執(zhí)行相應(yīng)動作
{
    if  ((keycode>='0') && (keycode<='9'))  //本程序中對0-9的數(shù)字按鍵不做響應(yīng)
    {}
    else if (keycode == 0x26)  //向上鍵,倒計時設(shè)定值遞增
    {
        if (CountDown < 9999)  //最大計時9999
        {
            CountDown++;
            DisplayNumber(CountDown);
        }
    }
    else if (keycode == 0x28)  //向下鍵,倒計時設(shè)定值遞減
    {
        if (CountDown > 1)     //最小計時1
        {
            CountDown--;
            DisplayNumber(CountDown);
        }
    }
    else if (keycode == 0x0D)  //回車鍵,啟動倒計時
    {
        flagStart = 1;         //啟動倒計時
    }
    else if (keycode == 0x1B)  //Esc鍵,取消倒計時
    {
        enBuzz = 0;            //關(guān)閉蜂鳴器
        LedBuff[6] = 0xFF;     //關(guān)閉獨立LED
        flagStart = 0;         //停止倒計時
        CountDown = 0;         //倒計時數(shù)歸零
        DisplayNumber(CountDown);
    }
}
void KeyDrive()  //按鍵動作驅(qū)動函數(shù)
{
    unsigned char i, j;
    static unsigned char backup[4][4] = {  //按鍵值備份,保存前一次的值
        {1, 1, 1, 1},
        {1, 1, 1, 1},
        {1, 1, 1, 1},
        {1, 1, 1, 1}
    };
    static unsigned long pdata TimeThr[4][4] = {  //保持按下時啟動快速輸入的時間閾值
        {1000, 1000, 1000, 1000},
        {1000, 1000, 1000, 1000},
        {1000, 1000, 1000, 1000},
        {1000, 1000, 1000, 1000}
    };
   
    for (i=0; i<4; i++)  //循環(huán)掃描4*4的矩陣按鍵
    {
        for (j=0; j<4; j++)
        {
            if (backup[ i][j] != KeySta[ i][j])  //檢測按鍵動作
            {
                if (backup[ i][j] != 0)  //按鍵按下時執(zhí)行動作
                {
                    KeyAction(KeyCodeMap [ i][j]);  //調(diào)用按鍵動作函數(shù)
                }
                backup[ i][j] = KeySta[ i][j];
            }
            if (KeyDownTime[ i][j] > 0)  //檢測執(zhí)行快速輸入
            {
                if (KeyDownTime[ i][j] >= TimeThr[ i][j]) //按下時間達到閾值時執(zhí)行一次動作
                {
                    KeyAction(KeyCodeMap[ i][j]);  //調(diào)用按鍵動作函數(shù)
                    TimeThr[ i][j] += 200;         //間隔200ms執(zhí)行下一次動作
                }
            }
            else //按鍵彈起時復(fù)位閾值時間
            {
                TimeThr[ i][j] = 1000;  //啟動快速輸入的條件為持續(xù)按下超過1000ms
            }
        }
    }
}
void LedScan()  //LED顯示掃描函數(shù)
{
    static unsigned char index = 0;
   
    P0 = 0xFF;                 //關(guān)閉所有段選位,顯示消隱
    P1 = (P1 & 0xF8) | index;  //位選索引值賦值到P1口低3
    P0 = LedBuff[index];       //相應(yīng)顯示緩沖區(qū)的值賦值到P0
    if (index < 6)             //位選索引0-6循環(huán),因有6個數(shù)碼管+一組獨立LED
        index++;
    else
        index = 0;
}
void KeyScan()  //按鍵掃描函數(shù)
{
    unsigned char i;
    static unsigned char keyout = 0;  //矩陣按鍵掃描輸出計數(shù)器
    static unsigned char keybuf[4][4] = {  //按鍵掃描緩沖區(qū),保存一段時間內(nèi)的掃描值
        {0xFF, 0xFF, 0xFF, 0xFF},
        {0xFF, 0xFF, 0xFF, 0xFF},
        {0xFF, 0xFF, 0xFF, 0xFF},
        {0xFF, 0xFF, 0xFF, 0xFF}
    };
    //將一行的4個按鍵值移入緩沖區(qū)
    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;
    //消抖后更新按鍵狀態(tài)
    for (i=0; i<4; i++)  //每行4個按鍵,所以循環(huán)4
    {
        if ((keybuf[keyout][ i] & 0x0F) == 0x00)
        {   //連續(xù)4次掃描值為0,即16ms(4*4ms)內(nèi)都只檢測到按下狀態(tài)時,可認為按鍵已按下
            KeySta[keyout][ i] = 0;
            KeyDownTime[keyout][ i] += 4;  //按下持續(xù)時間累加
        }
        else if ((keybuf[keyout] [ i] & 0x0F) == 0x0F)
        {   //連續(xù)4次掃描值為1,即16ms(4*4ms)內(nèi)都只檢測到彈起狀態(tài)時,可認為按鍵已彈起
            KeySta[keyout][ i] = 1;
            KeyDownTime[keyout][ i] = 0;   //按下的持續(xù)時間清零
        }
    }
   
    //執(zhí)行下一次的掃描輸出
    keyout++;
    keyout &= 0x03;
    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;
    }
}
void InterruptTimer0() interrupt 1  //T0中斷服務(wù)函數(shù)
{
    static unsigned int tmr1s = 0;  //1秒定時器
   
    TH0 = T0RH;  //定時器重新加載重載值
    TL0 = T0RL;
   
    if (enBuzz)  //蜂鳴器發(fā)聲處理
        BUZZ = ~BUZZ;  //驅(qū)動蜂鳴器發(fā)聲
    else
        BUZZ = 1;  //關(guān)閉蜂鳴器
   
    KeyScan();  //按鍵掃描
    LedScan();  //LED掃描顯示
   
    if (flagStart) //倒計時啟動時處理1秒定時
    {
        tmr1s++;
        if (tmr1s >= 1000)
        {
            tmr1s = 0;
            flag1s = 1;
        }
    }
    else //倒計時為啟動時1秒定時器始終歸零
    {
        tmr1s = 0;
    }
}
長按鍵功能實現(xiàn)的重點有兩個:第一,是在原來的矩陣按鍵掃描函數(shù)KeyScan內(nèi),當檢測到按鍵按下后,持續(xù)的對一個時間變量進行累加,其目的是用這個時間變量來記錄按鍵按下的時間;第二,是在按鍵驅(qū)動函數(shù)KeyDrive里,除了原來的檢測到按鍵按下這個動作時執(zhí)行按鍵動作函數(shù)KeyAction外,還監(jiān)測表示按鍵按下時間的變量,根據(jù)它的值來完成長按時的連續(xù)快速按鍵動作功能。
1.5 作業(yè)
1、將第一個例程進行倒計時處理,從9999.99開始進行倒計時,并且只顯示有效位。
2、理解PWM的實質(zhì),在點陣上實現(xiàn)不同亮度的小燈的花樣排列。
3、實現(xiàn)數(shù)碼管計時和流水燈同時運行的效果。
4、學(xué)會長短按鍵的用法,獨立把本章程序全部寫出來。
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏9 分享淘帖 頂1 踩

相關(guān)帖子

回復(fù)

使用道具 舉報

沙發(fā)
ID:64270 發(fā)表于 2014-7-23 14:38 | 只看該作者
這個好像全是金沙灘那個宋老師的吧
回復(fù)

使用道具 舉報

板凳
ID:88716 發(fā)表于 2015-8-29 17:01 | 只看該作者
超級贊,非常有用
回復(fù)

使用道具 舉報

地板
ID:85630 發(fā)表于 2016-2-29 16:02 | 只看該作者
看到PWM一開始有點暈,抄一遍,自己寫一遍以后,對定時器的運用理解更深刻了
回復(fù)

使用道具 舉報

5#
ID:93625 發(fā)表于 2018-7-6 08:28 | 只看該作者
循序漸進,繼續(xù)學(xué)習(xí)。
回復(fù)

使用道具 舉報

6#
ID:702974 發(fā)表于 2020-3-14 10:24 | 只看該作者
這一章內(nèi)容屬于編程技巧,還需慢慢來
回復(fù)

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規(guī)則

手機版|小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機教程網(wǎng)

快速回復(fù) 返回頂部 返回列表