前兩篇教程中咱們學習了LED、按鍵、開關的基本原理,數字輸入輸出的使用以及二者之間的關係。咱們用到了 pin_mode 、 pin_read 和 pin_write 這三個函數,實際上它們離最底層(至少是單片機制造商容許咱們接觸到的最底層)就只有一步之遙了。而學單片機要是不瞭解一點底層,那跟Arduino玩家還有什麼區別?(爲防止有忠實的Arduino粉絲罵我,我得認可仍是有一小部分Arduino玩家是知道本篇教程所介紹內容的。)根本很差意思說本身學過單片機好吧。這所謂的最底層,就是數字IO寄存器了。html
在開始以前,你須要下載兩份文檔:編程
單片機的數據手冊。官網連接極慢,我在國內平臺上傳了一份,在本篇教程寫成之時是最新的。網絡
開發板的原理圖。本應在教程之初就放出來,但事實證實沒有原理圖也不影響使用。如今是確定須要的。架構
等等,你可能還不知道寄存器是什麼。那咱們就從寄存器開始吧。函數
寄存器是一類CPU內部的存儲器,分爲通用寄存器與特殊功能寄存器(8086對特殊功能寄存器還有細分)。通用寄存器,顧名思義是通用的,能夠存儲操做數、運算結果、內存地址等數據,在使用C語言編程時,通常不直接接觸通用寄存器,而由編譯器負責安排其使用。特殊功能寄存器有特定的功能,一些做用於CPU內部,如PC存放下一條指令的地址,SP記錄內存中棧頂的位置(如今無需瞭解這些);另外一些與IO模組相鏈接,單片機程序經過這類寄存器來控制各類外設,咱們今天要學習的數字信號IO寄存器就屬於這一類,而且應該是其中最簡單的了。學習
咱們使用的單片機的型號是ATmega324PA,它有多種封裝,引腳(pin)數不盡相同,但都有32個通用輸入輸出(GPIO)引腳。因爲AVR架構是8位字長的,CPU一次處理1位數據和8位數據所需的時間是同樣的,這32個引腳被組織爲4個端口(port),分別是PA、PB、PC和PD。ui
在AVR架構tiny與mega系列的單片機中,每一個端口都有3個寄存器控制數字信號IO,分別是PORTx、DDRx和PINx。這裏的x是A、B、C或D,因爲這4個端口在數字IO方面徹底相同,就把它們合併起來說。相應地,對於每一個引腳Pxn,有PORTxn、DDxn(沒有R)和PINxn三個bit控制其數字IO。spa
DDxn控制引腳方向:當DDxn爲1時,Pxn爲輸出;當DDxn爲0時,Pxn爲輸入。code
當Pxn爲輸入時,若是PORTxn爲1,則該引腳經過一個上拉電阻鏈接到VCC;不然引腳懸空。htm
當Pxn爲輸出時,若是PORTxn爲1,引腳輸出高電平;不然輸出低電平。
PINxn的值爲Pxn引腳的電平。若是給PINxn寫入1,PORTxn的值會翻轉。
還有不少細節問題,如MCUCR寄存器中PUD位的功能、復位後寄存器的值、輸入輸出切換的方法、讀取引腳電平的延遲、未鏈接引腳的處理方法等,留做今天的做業,閱讀數據手冊I/O-Ports一章中除Alternate Port Functions一節之外的內容(一共8頁不到,很少吧),找出這些問題的答案,並以此爲基礎回答上一篇教程最後的問題。
講了這麼多,相信你也沒記住多少,並且你也不知道去哪裏用這些寄存器。
要使用寄存器,你須要在C語言程序中寫 #include <avr/io.h> (在建立項目時自動生成的代碼中就有),而後就可使用 PORTA 、 DDRB 、 PINC 等寄存器了。它們是宏定義,你沒必要去探究它們展開後是怎樣的,只需知道這些宏能夠讀取,能夠賦值,能夠位操做,就像 uint8_t 類型變量同樣。
可是諸如 PORTA0 和 DDB7 等宏定義卻不表明寄存器上的那一位,它們實際上就是字面值常量,如 PORTAx 的意義是寄存器 PORTA 的第x位(第0位爲最低位,第7位爲最高位),它的值就是x。所以,直接對這些宏複製是不正確的(不只意義不正確,編譯也不會經過)。
在開發板的庫函數中的 <ee1/bit.h> 提供了包含幾個用於位操做的宏函數。咱們先按照手冊來用,稍後來看它們是如何實現的。
咱們先返璞歸真一下,回到最初的例子,點亮一個LED,不過此次咱們再也不使用 <ee1/led.h> 提供的函數,而是直接操做寄存器。
先點亮紅色LED吧。在原理圖的第2頁左上角,紅色LED經過一個電阻鏈接到網絡LED0,而在第1頁中LED0鏈接的是單片機PC4引腳,所以咱們須要讓PC4引腳輸出高電平。回到上面看一下三個寄存器的功能,輸出高電平須要DDxn和PORTxn同時爲1。這裏把x和n分別用C和4帶入,即咱們要讓DDC4和PORTC4爲1。
將一個寄存器的一位置爲1能夠由 set_bit 實現。它須要兩個參數,要操做的整型變量與表示第幾位的整數。把DDC4置爲1應該寫 set_bit(DDRC, 4); ,4 能夠用 DDC4 替換,這個定義就是這麼用的。相似地也能夠將PORTC4置爲1。點亮紅色LED的整個程序以下:
1 #include <avr/io.h> 2 #include <ee1/bit.h> 3 4 int main(void) 5 { 6 set_bit(DDRC , 4); 7 set_bit(PORTC, 4); 8 }
相信聰明的你已經知道閃爍和流水燈怎麼寫了。翻轉輸出電平可使用 flip_bit(PORTC, 4); ,也可使用 set_bit(PINC, 4); 。
下面來看數字輸入。仍是用第一個與按鍵相關的例子,讓LED狀態與按鍵保持一致,即按下亮起。
讀取一個寄存器中的一位可使用 read_bit。若是引腳上電平爲高,read_bit 的運算結果非0(但不必定是布爾值1)。若是你沒有忘記的話,按鍵按下時引腳電平爲低,所以對讀取引腳電平的結果取非才是按鍵是否按下。
在原理圖中,按鍵一端鏈接在BTN0網絡上,進而鏈接到單片機的PA4引腳。所以按鍵是否按下應該寫爲:!read_bit(PINA, 4) 。
在讀取以前應該先把引腳配置爲輸入。儘管復位後默認爲輸入,在這個例子中沒有必要向DDA4寫0,但明確寫出來可讓看這段代碼的人(可能別人也多是你本身)明白PA4是做輸入的,這樣作是一種良好的習慣。至於PORTA4,因爲這一引腳在外部有鏈接上拉電阻,就沒有必要啓用內部上拉電阻了。
1 #include <avr/io.h> 2 #include <ee1/bit.h> 3 4 int main(void) 5 { 6 reset_bit(DDRA, 4); 7 set_bit(DDRC, 4); 8 while (1) 9 { 10 cond_bit(!read_bit(PINA, 4), PORTC, 4); 11 } 12 }
再結合按鍵動做的知識,你應該知道怎樣直接經過寄存器操做來判斷按鍵動做了吧。
順便說一句,以上兩個程序都沒必要在項目屬性中給linker加上libee1庫。雖然代碼中使用了 <ee1/bit.h> ,但其中都是宏定義,與linker無關。
以前留了一個問題,就是位操做是如何實現的。如下爲 <ee1/bit.h> 中部分代碼:
1 #define set_bit (r, b) ((r) |= (1u << (b))) 2 #define reset_bit(r ,b) ((r) &= ~(1u << (b))) 3 #define read_bit (r, b) ((r) & (1u << (b))) 4 #define flip_bit (r, b) ((r) ^= (1u << (b)))
寫那麼多括號是爲了防止出現運算符優先級的問題。假設r就是一個寄存器,好比PORTC,b就是一個數字,好比4,也能夠是一個變量,那麼 (r) |= (1u << (b)) 就至關於 r = r | 1u << b (後綴u表示無符號數,位操做的運算數通常都是無符號數)。對於二進制表示下的每一位,若是不是第b位,那麼位或運算符右邊此位爲0,運算結果等於左邊,即r的這些爲保持不變;對於第b位,右邊此位爲1,不管左邊此位的值是多少,結果必定是1,即這一位被置1;這樣就實現了將一位置爲1的功能。
reset_bit 的實現還要繞一個彎。1u << b 是一個第b位爲1,其他位爲0的數,那麼 ~(1u << b) ,即位與賦值號右邊,是一個第b位爲0而其他爲都是1的數。仿照上面的分析可得,運算結果的第b位必定是0而其他位與r中原來的值相同。相似的分析也可應用於 flip_bit :兩個bit進行異或運算的結果,若相同則爲1,不一樣則爲0;當一個運算數是1時,結果就是另外一個運算數取反;當一個運算數是0時,結果與另外一個運算數相同;所以 flip_bit 就使r的第b爲取反而其餘爲不變。
以上是向寄存器中的位寫入的操做。用於讀取位的 read_bit 的原理也大體相同,用寄存器的值與 1u << b 相與,僅當第b位爲1時結果是 1u << b ,這是個非零數,不然結果爲0。read_bit 語句能夠直接放在 if 語句的條件部分,但若是是根據其結果決定一個變量是否加1,不能直接加上其運算結果,能夠轉換成 bool 類型或用 if 語句判斷。
這篇教程有點長。好好消化一下,而後把之前寫過的程序用寄存器從新寫一遍,以此鞏固所學的知識。
從本教程開始至今,咱們先了解了LED燈、按鍵、撥動開關、數字輸入輸出的使用方法,而後學習C語言位操做與數字IO寄存器,終於打通了一條從底層到應用的路。而網絡上不少教程都是反過來說的,即先介紹寄存器,而後直接經過寄存器來驅動LED、檢測按鍵等,甚至有直接寫諸如 DDRB |= 0x0C; 或 if (PINB & 0x40) 這樣的代碼的,初學者怎麼看得明白?站在個人角度,我以爲以上都是常識,都不用講,儘管我學習的時候也頗費周折(正是由於那些反過來的教程)。如今我站在初學者的角度,認爲本教程的講解順序是更容易理解的。
我學習計算機以前,總對計算機抱有特殊的幻想,以爲它什麼都能幹,很神奇。如今這些想法都沒有了,尤爲是在學習單片機的過程當中。學習計算機教會咱們分析問題、解決問題,而學習單片機讓咱們更好地理解計算機是如何按照咱們的想法來解決問題的。這篇教程帶你瞭解了寄存器,在你學習單片機的全過程當中,它都會伴隨着你。寄存器是硬件和軟件之間的一個重要紐帶,計算機的任何功能都離不開寄存器。CPU?有寄存器。總線通訊?經過寄存器。內存分頁?須要寄存器。萬物基於寄存器。又有更多像寄存器同樣的紐帶,在電子空穴與豐富多彩的計算機世界之間創建起聯繫。它們看起來如此複雜,卻又清晰明瞭,就算一晚上之間全部計算機都忽然消失,人類也能從電子管和打孔紙帶開始,一層一層地構建起計算機的世界。而咱們瞭解的只不過是這個巨大致系中的滄海一粟。
初入計算機世界,你想着計算機能幹什麼,學完計算機我能幹什麼。而計算機世界是如此高深,在逐漸深刻後,你會明白計算機不能幹什麼,我不能幹什麼。數碼管、蜂鳴器,它們一直在你的開發板上,你殊不知道如何使用它們;更不用說那些高級到你沒據說過的東西。學得越多,你會發現雖然本來不會的減小了,但腦海中萌生出的「不切實際」的想法更多——學習的速度永遠趕不上認知的速度。本系列教程不能幫你消除全部「不會」,而是要在帶你一步步消除一些「不會」的過程當中讓你學會發現更多「不會」並消除的方法。