close

這一陣子都在研究有關嵌入式系統 None OS C語言的寫法
因為之前都沒有寫過,所以其實不清楚怎麼入手,害我有一點慌張@@
其實市面上,有這種教學的書其實還蠻少的,直到最近才有一點了解,
下禮拜有空應該跟學長借那一本書,偷看了一下,感覺還不錯XD

前天我發現了這一篇文章
發現其實對要剛入門『嵌入式C語言開發』的人其實蠻受用的。

如果有需要的人,記得要看一下唷
 

===================

=轉載位置:單片機C語言-網上入門教程

===================
作者:不詳 來源:百度空間  酷勤網收集 2007-08-23 摘要

酷勤網   本文只是最簡單的一些東西,單片機的功能非常之強大,只要你能想得到,就一定可以用單片機來實現的。當然單片機和外部其他的芯片還有很多,比如數字溫度傳感器DS18B20,實時時鍾芯片DS1302,還有比如訪問AT24CXX的EEPROM存儲器等,更多的電路,還要靠大家在平時的學習過 (一)       相信很多愛好電子的朋友,對單片機這個詞應該都不會陌生了吧。不過有些朋友可能只聽說他叫單片機,他的全稱是什麼也許並不太清楚,更不用說他的英文全稱和簡稱了。單片機是一塊在集成電路芯片上集成了一台有一定規模的微型計算機。簡稱為:單片微型計算機或單片機(Single Chip Computer)。單片機的應用到處可見,應用領域廣泛,主要應用在智能儀表、實時控制、通信、家電等方面。不過這一切都沒什麼關係,因為我(當然也包括任何人)都是從不知道轉變成知道的,再轉變成精通的。現在我只想把我學習單片機的經歷,詳細地講敘給大家聽聽,可能有些大蝦會笑話我,想:那麼簡單的東西還在這裡賣弄。但是你錯了,我只是把我個人學習的經歷講述一遍而已,僅僅對那些想學習單片機,但又找不到好方法或者途徑的朋友,提供一個幫助,使他們在學習過程中,儘量少走些彎路而已!
      首先,你必須有學習單片機的熱情,不是說今天去圖書館看了一個下午關於單片機的書,而明天玩上半天,後天就不知道那個本書在講什麼東西了。還是先說說我吧,我從大二的第一個學期期末的時候才開始接觸單片機,但在這之前,正如上面所說的:我知道有種芯片叫單片機,但是具體長成什麼樣子,卻一點也不知道!看到這裡很多朋友一定會忍不住發笑。嘿嘿,你可千萬別笑,有些大四畢業的人也同樣不知道單片機長成什麼樣子呢!而我對單片機的痴迷更是常人所不能想像的地步,大二的期末考試,我全放棄了複習,每當室友拿著書在埋頭複習的時候,我卻捧著自己從圖書館借的單片機書在那看,雖然有很多不懂,但是我還是堅持了下來,當時我就想過,為了單片機值不值得我這樣去付出,或許這也是在一些三流學校的好處吧,考試掛科後,明年開學交上幾十元一門的補考費,應該大部分都能過了。於是,我橫下一條心,堅持看我的單片機書和資料。
      當你明白了單片機是這麼一回事的時候,顯而易見的問題出來了:我要選擇那種語言為單片機編寫程序呢?這個問題,困擾了我好久。具體選擇C51還是A51呢?彙編在我們大二之前並沒有開過課,雖然看著人家的講解,很容易明白單片機的每一時刻的具體工作情況,但是一合上書或者資料,自己卻什麼也不知道了,根本不用說自己寫程序了。於是,我最終還是決定學C51,畢竟C51和我們課上講的C語言,有些類似,編程的思想可以說是相通的。而且C51還有更大的優點就是編寫大程序時的優越性更不言而喻,當然在那時,我並沒有想的那麼深遠,C51的特點,還是在後來的實踐過程中,漸漸體會到的!朋友如果你選擇了C51,那麼請繼續往下看,如果你選擇了A51,那麼你可以不要看了!因為下面講的全是C方面的,完全在浪費你的時間!      呵呵 ^_^
      第二,既然你想學好單片機,你必須得捨得花錢,如果不買些芯片回來自己動手焊焊拆拆的(但是在後期會介紹給大家一個很好用的硬件仿真軟件,並不需要你用實驗板和仿真器了,直接在你的PC上完成,但是軟件畢竟是軟件,從某個特定的意義上來說是並不能代替硬件的),即使你每天捧著本書,把那本書翻爛,也永遠學不會單片機的!剛接觸單片機的朋友,看了資料,一定會對以下幾個詞見的比較多,但是具體的概念還是比較模糊,現作如下說明:
     (1)編程器    編程器是用來燒單片機芯片的,是把HEX或者BIN文件燒到單片機ROM裡的,供單片機運行的。
     (2)實驗板    實驗板是專為初學者根據某些要求而特做的板,一般上面就有一個單片機的最小系統,使用者只需寫好程序,燒好芯片,放到上面加以驗證的這麼一個工具。有了實驗板,對與初學者來說,省去了焊個最小系統的麻煩。但是對於電子開發人員來說,作用並不是很大
     (3)仿真器    仿真器是直接把HEX或者BIN文件暫時放在一個芯片裡,再通過這個芯片的引腳連接到實驗板或者係統上工作。這樣以來,可以省去了來回插拔芯片帶來的不必要麻煩。
      我一開始也不知道上面3個的概念和作用,嘿嘿,原本想買個實驗板(不想焊板,因為不可能為了點亮幾個流水燈,而去焊個單片機的最小系統)的,可是結果,確和我想的正好相反,人家出售的是編程器。等貨物寄到後,才知道自己搞錯了!汗。。。嘿嘿。現在想想實在是又氣又笑。我花了160大樣買了個編程器(很不幸的是,這個編程器更本用不了,一燒芯片,芯片就燒壞了)把我給氣的,這個編程器,現在還躺在我的抽屜裡呢不過,現在想想,唯一讓我覺得欣慰的是,那個老闆每次能解答我的問題,連那種超級幼稚的問題,他也能不嫌麻煩地儘量幫我解答!這點讓我很感動!
      第三,想學單片機的必需品--PC。因為寫程序,編譯或者是仿真都是通過PC完成的。如果沒有PC,什麼也做不了!!!有了PC最好還要問以上網,因為如果你沒有可以和你交流單片機的人,遇到自己解決不了的問題,一直都想不通,那麼估計你學習單片機的熱情就會隨著時間的推移而慢慢耗盡。如果你能上網通過論壇或者QQ群,問題就很快得到解決。這樣的學習效率一定很高!真正的高手是從論壇中泡出來的!
      有了上述3個條件後,你就可以開始學你的單片機了。但是,真的做起來並沒有我所說的那麼簡單。你一定會遇到很多很多的問題。比如為了讓單片機實現某個功能,你可能不知道怎麼去寫某個程序。或是你看懂了資料上某個相似的程序,你自己卻寫不出來。遇到類似的情況,記住:千萬不要急噪,就行!

(二)

      說了這麼多了,相信你也看了很多資料了,手頭應該也有必備的工具了吧!(不要忘了上面講過幾個條件的哦)。那個單片機究竟有什麼功能和作用呢?先不要著急!接下來讓我們點亮一個LED(搞電子的應該知道LED是什麼吧^_^)
      我們在單片機最小系統上接個LED,看我們能否點亮它!對了,上面也有好幾次提到過單片機最小系統了,所謂單片機最小系統就是在單片機上接上最少的外圍電路元件讓單片機工作。一般只須連接晶體、VCC、GND、RST即可,一般情況下,AT89C51的31腳須接高電平。
#include<reg51.h>         //頭文件定義。或用#include<at89x51.h>其具體的區別在於:後者定義了更多的地址空間。
      //在Keil安裝文件夾中,找到相應的文件,比較一下便知!
      sbit P1_0 = P1 ^ 0;
      void main (void)
{
    while(1)
    {
     P1_0 = 0;//低電平有效,如果把LED反過來接那麼就是高電平有效
    }
}

      就那麼簡單,我們就把接在單片機P1_0上的LED點亮了,當然LED是低電平,才能點亮。因為我們把LED的正通過電阻接至VCC。
      P1_0 = 0; 類似與C語言中的賦值語句,即把 0 賦給單片機的P1_0引腳,讓它輸出相應的電平。那麼這樣就能達到了我們預先的要求了。
while(1)語句只是讓單片機工作在死循環狀態,即一直輸出低電平。如果我們要試著點亮其他的LED,也類似上述語句。這裡就不再講了。
      點亮了幾個LED後,是不是讓我們聯想到了繁華的街區上流動的綵燈。我們是不是也可以讓幾個LED依次按順序亮呢?答案是肯定的!其實顯示的原理很簡單,就是讓一個LED滅後,另一個立即亮,依次輪流下去。 假設我們有8個LED分別接在P1口的8個引腳上。硬件連接,在
P1_1--P1_7上再接7個LED即可。例程如下:
#include<reg51.h>

sbit P1_0 = P1 ^ 0;
sbit P1_1 = P1 ^ 1;
sbit P1_2 = P1 ^ 2;
sbit P1_3 = P1 ^ 3;
sbit P1_4 = P1 ^ 4;
sbit P1_5 = P1 ^ 5;
sbit P1_6 = P1 ^ 6;
sbit P1_7 = P1 ^ 7;
void Delay(unsigned char a)
{
    unsigned char i;
    while( --a != 0)
    {
     for(i = 0; i < 125; i++);    //一個 ; 表示空語句,CPU空轉。
    }            //i 從0加到125,CPU大概就耗時1毫秒
}
void main(void)
{
    while(1)
    {
     P1_0 = 0;
      Delay(250);
     P1_0 = 1;

     P1_1 = 0;
      Delay(250);
     P1_1 = 1;

     P1_2 = 0;
      Delay(250);
     P1_2 = 1;

     P1_3 = 0;
      Delay(250);
     P1_3 = 1;

     P1_4 = 0;
      Delay(250);
     P1_4 = 1;

     P1_5 = 0;
      Delay(250);
     P1_5 = 1;

     P1_6 = 0;
      Delay(250);
     P1_6 = 1;

     P1_7 = 0;
     Delay(250);
     P1_7 = 1;
    }
}
      sbit 定義位變量,unsigned char a 定義無符字符型變量a,以節省單片機內部資源,其有效值為0~255。main函數調用Delay()函數。
Delay函數使單片機空轉,LED持續點亮後,再滅,下一個LED亮。while(1)產生循環。

(三)

      上面我們講了如何使LED產生流動,但是你是否發現一個問題:寫的太冗長了!能不能再簡單點呢?可以!可以使用C51的內部函數INTRINS.H實現。函數unsigned char _crol_(unsigned char a, unsigned char n) 可以使變量a循環左移n位,如果我們先給P1口賦0000 0001那麼當n為1時,便會產生和上面一樣的效果!
#include<intrins.h>
#include<reg51.h>

void Delay(unsigned char a)
{
    unsigned char i;
    while( --a != 0)
    {
     for(i = 0; i < 125; i++);
    }
}

void main(void)
{
    unsigned char b, i;
    while(1)
    {
     b = 0xfe;
     for(i = 0; i < 8; i++)
     {
      P1 = _crol_(b, 1);
                b = P1;
      Delay(250);
     }
    }
}

      INTRINS.H函數中的unsigned char _cror_(unsigned char a, unsigned char n)右移也可以實現同樣的效果!這裡就不再累述。
      流水燈的花樣很多,我還寫過那種拉幕式的流動等,程序很簡單,有興趣的朋友,可以自己試著寫寫!
      對了,講了那麼多,有些朋友一定還不知道編譯軟件怎麼用?這裡給大家介紹幾個吧?WAVE(偉福)大家一定聽說過吧!還有一個
就是KEIL2,我用的就是KEIL2,下面就來講講如何使用KEIL2這個編譯軟件!
     1.安裝軟件,這個應該不用再講了吧!
     2.安裝完後,啟動KEIL軟件左擊Project-->New Project-->輸入文件名-->選擇我們所以使用的芯片(這裡我們一般用到Atmel的AT89C51或AT89C2051,點確定。
     3.點File-->New-->輸入我們編寫的程序,保存為.C文件。(一般情況下,我們保存的文件名和前面的工程名一樣。)

     4.展開Target 1 -->右擊Source Group 1 -->Add Files to Group 'Source Group 1'-->選擇剛才保存的.C文件點擊ADD後,關閉對話框。這樣.C文件就被加到了Source Group 1 下。
     5.右擊Target 1-->Options    for 'Target 1' -->Target中填寫晶體的大小,Output中,在Create HEX Files 前打上鉤,點確定。
     6.點Project-->Rebuild All Traget Files ,若提示
       creating hex file from "XXX"...
         "XXX" - 0 Error(s), 0 Waring(s).
表示編譯和生成HEX文件成功!接下來的就是把HEX文件燒到單片機中,或是仿真器上,看是否達到預先的目的!
     嘿嘿!現在是否自己好有成就感了,如果讓你去做個流水綵燈,開發一個簡單的產品,只要加上驅動電路,就可以做出漂亮的流動綵燈了!到現在為止,你應該知道單片機的功能有多強大了吧,如果單純的用數字電路或模擬電路的知識去設計一個流動綵燈,可能要花點工夫和時間才行,有了單片機,那就不一樣了,你只要寫程序控制他就行!有人說過這樣一句話,也並不無道理的,學單片機,程序思想很重要!

(四)

      呵呵,朋友!相信你的流水燈也做的不錯了吧,現在能玩出幾種花樣了?你可能會說,只要你想得到,想怎麼流就怎麼流!呵呵,是的。但是工程師們設計這麼一個單片機,並不是只為了讓它做流水燈的,那樣也太浪費點了吧 ... ^_^
      學過數字電路的朋友,一定動手做過8路或者6路的搶答器。用純粹的數字電路知識來做,自己設計電路,感到比較困難!搶答器上用的顯示器多為7段數碼管,這裡我們來講講,如何用單片機讓數碼管顯示0-9。搶答器的實現,我們放到後面再來探討,因為搶答器還涉及了鍵盤的內容。8段數碼管分為共陰和共陽兩種。8段數碼管是由8個LED組成(還包括一個小數點)。若為共陽,則8個LED的陽級是連接在一起的,同理若為共陰,則陰極連接在一起。8個LED對應的標號如下:
             a
            __
         f |    | b   
           |__|
           |g | c
         e |__| . dp
            d
      一般情況下,為了計算或取碼的方便,我們把a-dp依次接到單片機某個口上的Px.0--Px.7上。x表示0,1,2,3其中的一個。這樣我們只要給某個口,賦一個值,則相應的LED段就被點亮,但是在硬件連接上要注意了:單片機可能不能直接驅動LED,所以我們可以通過控制三級管的導通或截止,來控制LED的亮與滅!
      如果我們把共陰的數碼管的a--dp依次接到單片機的P0.0--P0.7上,注意:P0口需接上拉電阻。何為上拉電阻,簡單的說,就是把電平拉高,以提高驅動能力。那麼比如:P0 = 0X3F;則顯示為數字 0 。因為0X3F 即為2進制的 0011 1111 我們低位往高位數,依次為1111 1100,其I/O的電平分別為高、高、高、高、高、高、低、低,即對應的a--dp 為亮、亮、亮、亮、亮、亮、滅、滅,由上圖我們可以看出g和dp段不亮其他段均亮,即為我們所看到的數字 0 字樣。其他的數字或字符,也同理可以得到。但是有些朋友就會問,那我們每取一個字模,豈不是很麻煩?還有自己考慮高低電平什麼的?^-^ 呵呵,其實網上有很多LED取模軟件,如果有一定計算機編程語言的朋友,也可以試著自己寫個取模的程序,讓計算機為我們計算,諸如上述0X3F的數值。
#include<reg51.h>
void Delay(unsigned char a)
{
    unsigned char i;
    while( --a != 0)
    {
     for(i = 0; i < 125; i++);
    }
}
void main(void)
{
    P0 = 0X3F;    //顯示 0
    Delay(250);//延時
    P0 = 0X00;//短暫的關閉顯示,若不關閉,可能會造成顯示模糊不清。
    P0 = 0X06; //顯示 1
    Delay(250);
    P0 = 0X00;
    ...    //以下顯示數字2-F,略。
}

      看到這裡,想必大家一定可以把0-F顯示出來了吧!但是如果要你顯示兩位數,三位數呢?或許,有的朋友會這麼想:在P0口上接一個數碼管,再在P1口上接個數碼管!但是,如果要顯示4位、5位的數字呢?那豈不是一塊AT8951都接不過來!難到就不能接4位或5位以上的嗎?肯定不是的!
      說到這裡,我們來講講數碼管的顯示方式,可分為兩種:動態掃瞄和靜態顯示。上面我們所說的即為靜態顯示。但是如果我們採用動態掃瞄顯示,那麼就可以解決上面的問題,即可以顯示多個數碼管了。上面我們所說的靜態顯示把數碼管的COM腳接至VCC或GND端,其他的接至PX口上,這樣只要PX口上輸出相應的高低電平,就可以顯示對應的數字或字符。但是如果我們採用動態掃瞄的方法,比如顯示6個數碼管,硬件連接可以這樣解決:a--dp還是接至P0.0--P0.7上,還有6個COM腳再接至另外口的P2.0--P2.5。P0口作段選(控制數字字符)P2口作位選(選通哪個數碼管導通)這樣我們控制P0和P2口就可以控制6個數碼管了。但是,細心的朋友,會問這樣的問題:P2位選,是讓數碼管一個一個亮
的,那還是不能控制6個一起亮或滅嘛!? ^_^ 想想好像是對的哦?怎麼辦...難道錯了?
      嘿嘿,問你個問題?黑夜裡,拿著一支菸,在你面前快速的晃動,你會發現什麼樣的現象?是不是原本不連續的點變成了一條看上去連續的曲線或者直線!再回過頭來,仔細想想我們的數碼管!原理是一樣的,你可別忘了,我們的單片機可是一個計算機哦,計算機的運算速度,大家可想而知吧!
      這裡再說說51單片機的機器週期和時鍾週期等概念。所謂機器週期就是訪問一次存儲器的時間。而1個機器週期包括12個時鍾週期。如果單片機工作在12M晶體下,那麼一個時鍾週期為:1/12微妙。一個機器週期12*1/12 = 1微妙。如果晶體為6M,時鍾週期和機器週期各是多少呢?在彙編中,我們還要關心,指令執行的機器週期長短不一,有1個週期、2個週期和4個週期等。
      說著說著,跑了這麼遠了...還是回到原來的話題,如果我們把位選的P2也看作上面的「煙」一劃而過,那麼我們看到的是不是6個一起亮或一起滅了!    ^_^    哈哈,原來如此...        記住,在任何某一時刻,有且只有一個數碼管能發光。如果你能把這句話理解了,你是真明白我的意思了!朋友,現在給你個任務,讓6個數碼管分別顯示1、2、3、4、5、6。看你自己可以搞定不?你自己先試著寫寫看咯...

#include<reg51.h>
void Delay(unsigned char a)
{
    unsigned char i;
    while( --a != 0)
    {
     for(i = 0; i < 125; i++);
    }
}

void main(void)
{
    while(1)
    {
     P0 = 0x06;//1的碼段
     P2 = 0x01;//選通一位,或者P2_0 = 1;
     Delay(20);//延時約20毫秒
     P0 = 0X00;//關閉顯示

     P0 = 0x5b;//2的碼段
     P2 = 0x02; //選通一位,或者P2_1 = 1;
     Delay(20);
     P0 = 0X00;
     P0 = 0x4f;//3的碼段
     P2 = 0x04; //選通一位,或者P2_2 = 1;
     Delay(20);
     P0 = 0X00;
     P0 = 0x66;//4的碼段
     P2 = 0x08; //選通一位,或者P2_3 = 1;
     Delay(20);
     P0 = 0X00;

     P0 = 0x6d;//5的碼段
     P2 = 0x10;//選通一位,或者P2_4 = 1;
     Delay(20);
     P0 = 0X00;
     P0 = 0x7d;//6的碼段
     P2 = 0x20;//選通一位,或者P2_5 = 1;
     Delay(20);
     P0 = 0X00;
    }
}

(五)

       相信大家一定見過數字時鍾,教學樓大廳一定有吧。每次路過,基本上只是隨便瞟上一眼,根本沒去想過他的工作原理什麼。但是今天你也可以把他做出來了,是不是覺得自己很有成就感呢!呵呵! ^_^
       接上面所講的,我們先來做個簡單的實驗:在一個數碼管上輪流顯示0--9這10個數字。還楞著幹什麼,快動手寫程序呀!好像有點難哦,要不先不要往下看了,嘿嘿,關機吧,自己先去想想,怎麼樣?
#include<reg51.h>
unsigned char code SEG_TAB[ ] ={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; //0-9數字

void Delay(unsigned int a)     //unsigned int 定義為無符整形,取值範圍為0--32768
{
     unsigned char i;
     while( --a != 0)
     {
      for(i = 0; i < 125; i++);
     }
}

void main(void)
{
     unsigned char i;
     while(1)
     {
      for(i = 0; i < 10; i++)
      {
       P0 = SEG_TAB[ i ];     //取SEG_TAB數組中的值
       P2 = 0X01;
       Delay(1000);
      }
     }
}

       是不是顯示從0--9,跳動顯示,你的心是不是也跟著一起跳呀,離我們的目標又邁進了一步!不錯,繼續努力!
       上面只顯示了一個數碼管的數字0--9,但是怎麼樣要讓他顯示6個數字呢?這樣我們就可以做個時鍾出來玩玩了!還記不記得我們前面講過的P2口的位選作用!嘿嘿,沒忘記就好!
#include<reg51.h>

unsigned char hour = 12, min = 0, sec = 0;
unsigned char code SEG_TAB[ ] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; //0-9數字
void Delay(unsigned char a)
{
     unsigned char i;
     while( --a != 0)
     {
      for(i = 0; i < 125; i++);
     }
}

void disp(void)
{
     P0 = SEG_TAB[ sec % 10 ];//顯示秒的個位
     P2 = 0X01;
     Delay(15);
     P2 = 0;

     P0 = SEG_TAB[ sec / 10 ];//顯示秒的十位
     P2 = 0X02;
     Delay(15);
     P2 = 0;

     P0 = SEG_TAB[ min % 10 ];//顯示分的個位
     P2 = 0X04;
     Delay(15);
     P2 = 0;

     P0 = SEG_TAB[ min / 10 ];//顯示分的十位
     P2 = 0X08;
     Delay(15);
     P2 = 0;

     P0 = SEG_TAB[ hour % 10 ];//顯示時的個位
     P2 = 0X10;
     Delay(15);
     P2 = 0;

     P0 = SEG_TAB[ hour / 10 ];//顯示時的十位
     P2 = 0X20;
     Delay(15);
     P2 = 0;
}
void main(void)
{
     while( 1 )
     {
      disp( );
     }
}
       編譯燒錄芯片後,觀察運行現象。矣...怎麼一直顯示12:00:00,難道是時鍾沒有啟動?還是,另外的原因呢? 哦,原來是3個變量sec,min,hour初始化後,其值一直沒有改變!那我們怎麼樣才能讓他改變數值呢?有的朋友一定會這麼認為:讓秒個位延時1秒,後加1,而秒十位延時10秒後,再加1,一直加到6,分個位加1,依次類推...這樣的想法是不錯,但是朋友你有沒有想過C語言的一般延時(除非你把他放到中斷裡)極不精確!這樣累計下來,一天24小時的誤差,肯定很大很大,我曾經也用延時的方法寫過時鍾,1個小時誤差8秒,那是個什麼概念!一天24小時就要24*8=192,約為3分鍾,一個月就是10分鍾...有沒有其他的方法可以改進些呢?有!這裡就要涉及到單片機中另一個比較重要的核心部分:單片機的中斷和定時器的運用!想寫出比較精確(這裡說的只的相對前面的做法而言比較精確而已,如果要做更加精確的時鍾,用時鍾芯片比較好點,常用的有DS12887和DS1302等)的時鍾程序,就一定要調用中斷和定時器。還是大家先看看教材和書吧,畢竟人家出的書,肯定比我要寫的系統多了,下面我們再來簡單的講講!

(六)

       什麼是中斷呢?講個比較通俗的例子:比如你正在家中看電視,突然電話響了,你的第一反應是什麼?是不是先跑過去接電話!接完電話後,繼續看電視。這就是個中斷的例子,中斷是由電話引起了,你跑過去就是響應中斷,接電話就是中斷的處理!接完電話後,接續看電視,即恢復中斷,等待下個中斷的到來!
       但是這個好像和單片機沒什麼聯繫呀?有的朋友或許會這樣疑問。是的。單片機當然不會看電視了,也不會接電話了 !     ^_^     但是,類比一下:比如單片機正在執行某個任務,突然要有更重要的事件,要求單片機響應,單片機就會應答響應,去執行更為重要的任務(中斷處理),原來的任務就繼續等待(現場的保護)。執行完更重要的任務後,回到中斷的入口處,繼續執行原來的任務(現場中斷的恢復)。51系列的單片機共有5個中斷源,分別為:外中斷0 、定時器T0中斷、外中斷1、定時器T1中斷、串口中斷。  
       或許,有些朋友已經大概領會了其中的意思,有些朋友還迷迷糊糊。不過不要緊,我們繼續往下看,下面我們來講講單片機的定時器是什麼?如何工作的?定時器,大家從字面上就可以看出其大概的意思吧?簡單的說:就是起定時作用!也就是讓單片機計數。定時器分為:方式0方式1、方式2和方式3等4種工作方式。有些朋友一定會問:定時器如何啟動?風扇的定時器,相信大家一定都用過吧!但是單片機的定時器,該如何啟動呢?總不該也用手一擰定時器吧!     ^_^ 當然不是,我們只要給單片機一些指令,就可以啟動定時器了!下面我們就定時器0,來說說怎麼啟動定時器0。

TMOD = 0X01;//設置定時器0 工作方式0
TH0 = (65536 - 5000) / 256;//載入高8位初值
TL0 = (65536 - 5000) % 256;//載入低8位初值
TR0 = 1;           //啟動定時器

       ^_^,簡單吧,這樣我們就可以把定時器啟動了。其中TMOD為T/C方式控制寄存器:

D7 D6 D5 D4 D3 D2 D1 D0
        _         _
          GATE C/T        M1        M0        GATE C/T      M1 M0

           |_________        __________| |_________        __________|
|            T/C1              | |            T/C0              |

       C/T就是counter(記數器)和timer(定時器)的選擇位,若值為1,則作計數器用。為0,則為定時期用!GATE為門控位。M1和M0工作方式的選擇:若M1=0;M0=0 則為方式0:13位定時/記數器。若M1=0;M0=1則為方式1,16定時/記數器。若M1=1;M0=0則為方式2,自動裝載8位定時/記數器。若M1=1;M0=1則為方式3,只適用於T/C0,2個8位定時/記數器。
       說了一大堆,感到有點困惑了吧。那我們還是來說說上面的。TMOD= 0X01;//至於為什麼是0X01,大家看:我們選擇的是定時器0方式0,所以T/C1全為0,而T/C0的M1為0。M0為1,所以D0-D7為0X01;0X01表示的是16進制數,這個大家應該都知道吧!還有D0-D7表示的是2進制數。
還需要轉換一下!
       TH0 = (65536 - 5000) / 256;//載入高8位初值。若在12M晶體下,定時5000微秒,即為5毫秒;但是如果不是在12M下,那又該怎麼計算了呢?如果是11.0592M呢?還記不記得,我們前面講過的機器週期和時鍾週期的概念? ^_^忘了,還是看看前面吧!呵呵!沒事,學習嘛,忘
了再翻翻書,看看就可以了!其實上訴的5000 = 1 * C     很顯然C=5000,但是如果是11.0592M那麼就不是1了,應該是1.085了,那麼5000 = 1.085 * C,則C就為5000 / 1.085 = ? 具體多少,大家自己去算算吧?同理TL0也是一樣的! 但是,細心的朋友會發現網上或者是資料上的TH0,TL0並不是和上面一樣的,而是直接TH0 = 0XEC;TL0 = 0X78 是不是和上面的一樣的,別忘了單片機也是計算機的一種哦。用C的話,直接寫上計算公式就行,計算就交給單片機完成。
       TR0 = 1;這句就是啟動定時器0,開始記數!哦,還有一點,有些朋友會問,你是65536是哪裡來的呢?呵呵你可別忘了:設置定時器0 工作方式0是16位的(2的16次方是多少,自己算算就知道了)簡單吧?但是如何和中斷一起使用呢?請繼續看下面的講解!

TMOD = 0X01;//設置定時器0 工作方式0
TH0 = (65536 - 5000) / 256;//載入高8位初值
TL0 = (65536 - 5000) % 256;//載入低8位初值
TR0 = 1;           //啟動定時器
EA = 1;//開總中斷
ET0 = 1;//開定時器中斷。若為0則表示關閉!
       這樣我們,就初始化定時器T0和中斷了,也就是定時器滿5毫秒後,產生一次中斷。產生中斷後,我們怎麼處理呢?嘿嘿!仔細想想?^_^每次中斷後,我們可以讓一個變量自加1,那麼200次中斷後,不就是1秒的時間了嗎?比起上面我們說的延時來出來是不是更加精確多了呢?那是肯定的!但是想想1秒種的時間就讓單片機產生那麼多次的中斷,單片機會不會累著呢?恩,那麼不好。如果在12M的晶體下,T0每次中斷不是可以產生最多65.336毫秒的時間嗎?那麼我們讓他每50毫秒中斷一次好了!這樣我們就20次搞定一秒的時間了!     ·爽·
       好了,講了那麼多,現在我們來寫個時間的程序吧!     ^_^

#include<at89x51.h>

#define HI       ((65536 - 50000) / 256)
#define LO       ((65536 - 50000) % 256)
#define _TH0_TL0_             (65536 - 50000)
#define M       20               //(1000/25)

/**********************************************************************************************/
unsigned hou = 12, min = 0, sec = 0;

unsigned char SEG_TAB_B[ ] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; //0-9數字
unsigned char SEG_TAB_A[ ] = {0x40,0x79,0x24,0x30,0x19,0x12,0x02,0x78,0x00,0x10};//0.-9.數字

/*********************************************************************************************/
void Delay(unsigned char a)//延時程序a*1MS
{
unsigned char j;
while(a-- != 0)
{
     for (j = 0; j < 125; j++);
}
}

/*********************************************************************************************/
void Disp(void)//數碼管顯示
{
     P2_0 = 1;
     P1 = SEG_TAB_B[ hou / 10 ];
     Delay(5);
     P2_0 = 0;

     P2_1 = 1;
     P1 = SEG_TAB_A[ hou % 10 ];
     Delay(5);
     P2_1 = 0;

     P2_2 = 1;
     P1 = SEG_TAB_B[ min / 10 ];
     Delay(5);
     P2_2 = 0;

     P2_3 = 1;
     P1 =S EG_TAB_A[ min % 10 ]; 
     Delay(5);
     P2_3 = 0;

     P2_4 = 1;
     P1 = SEG_TAB_B[ sec / 10 ];
     Delay(5);
     P2_4 = 0;

     P2_5 = 1;
     P1 = SEG_TAB_B[ sec % 10 ];
     Delay(5);
     P2_5 = 0;
}
/********************************************************************************************/
void IsrTimer0(void) interrupt 1 using 1       //定時50ms
{
static unsigned char count = 0; //定義靜態變量count

count++;
if(count == M)
{
     count = 0;
     sec++; 
     if(sec == 60)
     {
      min++;
      sec = 0;
      if(min == 60)
      {
       hou++;
       min = 0;
       if(hou == 24)
       {
        hou = 0;
       }
      }//if
     }//if
}//if
}

/******************************************************************************************/
void Timer0Init(void)     //定時器0
{
TMOD = 0x01;

TH0 = HI;
TL0 = LO;
TR0 = 1;

ET0 = 1;
EA = 1;
}

/******************************************************************************************/
void main(void)     //主函數
{
Timer0Init();

while(1)
{
     Disp();
}
}

       簡單吧,還是有點看不懂哦,那你自己慢慢體會吧,如果你自己能寫個時鍾程序來,那麼你的51單片機也就學了80 % 了。中斷和定時/記數器器,是個很重要的東西,幾乎用到單片機的地方都會涉及到中斷和定時!所以大家要好好掌握哦! ^_^
       哈哈,趕緊編譯HEX文件,搭好硬件,燒入單片機,上電看看效果先!呵呵,現在你應該有成就感了吧,想不到一個時鍾居然那麼簡單, 嘿嘿!但是問題來了!時鍾雖然做出來了,但是他的精度怎麼樣呢?一兩個小時,或許看不出什麼誤差,但是一天或者一年呢?暈,我的天呀,要是按年來算的話,那這個時鍾根本沒有實用價值!人家都說用C寫不出,精度高的時鍾程序來的!!!是不是有點後悔了,去學彙編吧!但是既然選擇了C,那麼就不要後悔!嘿嘿,想想C的高級語言,怎麼會輸給彙編呢 ^_^ 呵呵!看下面這段代碼:

     static unsigned char count = 0;

     TR0 = 0;
         TL0 += (_TH0_TL0_ + 9) % 256;
         TH0 += (_TH0_TL0_ + 9) / 256 + (char)CY;
         TR0 = 1;

     count++;
       在中斷處理服務程序中,我們加入上面的代碼。 TR0 = 0; 先關閉定時器T0,然後重新給TH0和TL0 賦值,再開啟 TR0 = 1;燒入單片機看看效果,怎麼樣,你第一次精確多了吧。但是還是有誤差!鬱悶!為什麼呢?那是硬件造成的誤差,我們可以用軟件來彌補!我們先把時鍾點亮,讓他走上幾個小時或者是幾天,看看到底誤差是多少!取個平均值。(這裡比如我們10小時快1秒)那麼可以通過以下語句
if(hour % 10 = 0)
{
     sec--;
}
來彌補!這樣可能會出現這樣的現象:秒直接跳變!我們可以再通過細分來實現,不要10小時那麼大,小些的就行!具體的操作還是留給朋友們吧!

(七)

       這回我們來講講鍵盤,大家肯定見過銀行櫃員機吧,取錢輸入密碼就要用到鍵盤,超市購物取回寄存物品要輸入密碼,還有你現在在用的PC機的鍵盤。但是鍵盤的是怎麼工作的呢?一般有2種方式:(1)掃瞄法,不斷掃瞄鍵盤的狀態,送CPU判斷並處理。如果鍵盤數目一大的話,顯然不適合(2)線反轉法,通過行列狀態的改變來判斷有無鍵被按下!
       現在我們在P1口接個4*4的鍵盤,P1.0--P1.3接行,P1.4---P1.7接列,再接4個4K7的上拉電阻至VCC。代碼如下:

//----鍵盤掃瞄法程序-------
//----用數碼管顯示相應的鍵值-----
//P1.0--P1.3接行-------
//P1.4---P1.7接列-------
#include<reg51.h>
unsigned char code tab[ ]={0x3F,0x06,0x5B,0x4F,
           0x66,0x6D,0x7D,0x07,
           0x7F,0x6F,0x77,0x7C,
          0x39,0x5E,0x79,0x71};//0到F的16個鍵植

/******************************************************************************/
void Delayt(unsigned char t)//延時函數
{
unsigned char i;
      for(t=0;i<=t;t++)
        for(i=0;i<255;i++);
}

/******************************************************************************/
bit pkey(void)//判斷鍵的否被按下,通過返回值確定
{
P1=0xf0;
     if(P1!=0xf0)
       {
        Delayt(25);
         if(P1!=0xf0)
          return 1;
         else
          return 0;
       }
     else
         return 0;
}

/******************************************************************************/
void main(void)//主函數
{
     unsigned char key,j,k,s;
     while(1)
      {
        if(pkey()==1)
        {  
          P1=0xfe;
       k=0xfe;
          for(j=0;j<4;j++)
              {
                      s=P1&0xf0;
            switch(s)
       {
        case 0xe0: key=4*j+0; break;
            case 0xd0: key=4*j+1; break;
         case 0xb0: key=4*j+2; break;
         case 0x70: key=4*j+3; break;
         default: break;
       }
       k=(k<<1)|0x01;
       P1=k;
                    }//for
        }//if
     //if((P1&0xf0)==0xf0)
           P0=tab[key];
     P2=1;
     Delayt(50);
      }//while
}

       還有一種就是線反轉法,實現如下:
1.和掃瞄法相同,把列線置低電平,行置高,讀行狀態
2.與1相反,把行置低,列置高,讀列狀態
3.若有鍵按下,則為2次所讀狀態的結果即為鍵所在的位置,這樣2次輸出和2次讀入可以完成鍵的識別!!!

       子函數如下:
unsigned char key_vscan(void)
{
       unsigned char row, col;
       P1     = 0xF0;
       row = P1&0xF0;
       row = row&0xF0;

       P1     = 0x0F;
       col = P1&0x0F;
       col = col&0x0F;

       return(key_val(row|col));
}

       下面我們再來介紹介紹一鍵多能的程序,即按下一個鍵,可以執行不同的命令!

void main (void)
{
unsigned char b = 0;
while( 1 )
{
     if(P1_0 == 0)
       {
      Delay(10);
         if(P1_0 == 0)
      {
       b++;
       if( b == N )//N為鍵的功能數目
       {
        b = 0;
       }
         while(P3_2 == 0);//等待鍵鬆開
      }
       }
     switch( b )
       {
         case 1: P2_0 = 0xFE;
                 break;
         case 2: P2_1 = 0xfd;
          //..............add your code here!
       }
}
}

(八)//以上的文字寫於2005年5月,由於時間關係,一直未能將此完成,最近閒著無聊又接著寫了些文字,以下寫於2006年6月5日!
       在這裡我想對上面一點,作個簡單的說明,如果你是剛學單片機,那麼你寫的代碼是VERY GOOD的,但是如果把上面的代碼應用於產品的話,那麼我可以告訴你,上面所寫的按鍵識別代碼全部是垃圾代碼,^_^,這下傻了吧,呵呵。為什麼?我的按鍵不是可以正常工作嗎?
       請看這裡:
if(P1_0 == 0)
      {
     Delay(10);//問題就在這裡,你讓CPU在這裡空轉?
        if(P1_0 == 0)
     {
      //...add your code here.
     }
}
進入第1個if判斷語句後,就進入了Delay(10);再看Delay函數,完全讓CPU執行(;空語句),所以在做大的產品或者代碼時,這個是非常耗費單片機內部資源的。有什麼辦法嗎?呵呵,那是肯定的。
       解決方法大致有如下2種:
1.將延時函數放在中斷中,在中斷裡查詢延時的標誌位。/*不僅僅用於鍵盤識別,亦可以用於其他的延時代碼,見EX1*/
2.直接在中斷中查詢按鍵的標誌位.//見EX2。
EX1:
unsigned char Delaytime;

void Delay(unsigned char Delaytime)//
{
while(Delaytime !=0 );//等在這裡,直到Delaytime為0。
}

void Timer0_interrupt(void) interrupt 1 using 2
{
if(Delaytime != )
     Delaytime--;
//...add your other code here
}

Delay函數具體延時多長時間,就要看你設定的T0定時器中斷和Delaytime的乘積,比如你的定時器中斷為50MS,Delaytime為20的話,那麼50MS*20=1S。

EX2:
#define Press_key = P2 ^ 7;//定義按鍵的I/O

void P_key(void)
{
char new_value,old_value;

new_value = Press_key;

if(new_value && !old_value)//識別按鍵。
{
     Turn_On_LEd( );
     //...add your other code here.
}
old_value = new_value;
}

void Timer0_interrupt(void) interrupt 1 using 2
{
P_key();

// ...add your other code
}

當然在實際過程當中,並不是如此簡單簡潔的,還希望大家能夠舉一反三哦... ^_^。

(九)

寫了這麼多了,大家也看了這麼多了,感覺怎麼樣?大家也覺得不難吧。其實51也就那麼簡單,真的很希望大家看完這篇文字以後,很自信的說,51單片機也已經入門。這是對我寫怎麼多文字最好的回答。時隔13個月之久再來繼續寫這些東西,沒有以前的激情和熱情,所以就草草了事結尾,希望大家不要在背地裡罵我哦,^_^。當然以上講的只是最簡單的一些東西,單片機的功能非常之強大,只要你能想得到,就一定可以用單片機來實現的。當然單片機和外部其他的芯片還有很多,比如數字溫度傳感器DS18B20,實時時鍾芯片DS1302,還有比如訪問AT24CXX的EEPROM存儲器等,更多的電路,還要靠大家在平時的學習過程當中,慢慢掌握。

arrow
arrow
    全站熱搜

    Aminzai 發表在 痞客邦 留言(1) 人氣()