AVR單片機教程——數字IO寄存器

来源:https://www.cnblogs.com/jerry-fuyi/archive/2019/10/06/11620890.html
-Advertisement-
Play Games

前兩篇教程中我們學習了LED、按鍵、開關的基本原理,數字輸入輸出的使用以及兩者之間的關係。我們用到了 pin_mode 、 pin_read 和 pin_write 這三個函數,實際上它們離最底層(至少是單片機製造商允許我們接觸到的最底層)就只有一步之遙了。而學單片機要是不瞭解一點底層,那跟Ardu ...


前兩篇教程中我們學習了LED、按鍵、開關的基本原理,數字輸入輸出的使用以及兩者之間的關係。我們用到了 pin_mode 、 pin_read 和 pin_write 這三個函數,實際上它們離最底層(至少是單片機製造商允許我們接觸到的最底層)就只有一步之遙了。而學單片機要是不瞭解一點底層,那跟Arduino玩家還有什麼區別?(為防止有忠實的Arduino粉絲罵我,我得承認還是有一小部分Arduino玩家是知道本篇教程所介紹內容的。)根本不好意思說自己學過單片機好吧。這所謂的最底層,就是數字IO寄存器了。

在開始之前,你需要下載兩份文檔:

單片機的數據手冊。官網鏈接極慢,我在國內平臺上傳了一份,在本篇教程寫成之時是最新的。

開發板的原理圖。本應在教程之初就放出來,但事實證明沒有原理圖也不影響使用。現在是肯定需要的。

 

等等,你可能還不知道寄存器是什麼。那我們就從寄存器開始吧。

寄存器是一類CPU內部的存儲器,分為通用寄存器與特殊功能寄存器(8086對特殊功能寄存器還有細分)。通用寄存器,顧名思義是通用的,可以存儲操作數、運算結果、記憶體地址等數據,在使用C語言編程時,一般不直接接觸通用寄存器,而由編譯器負責安排其使用。特殊功能寄存器有特定的功能,一些作用於CPU內部,如PC存放下一條指令的地址,SP記錄記憶體中棧頂的位置(現在無需瞭解這些);另一些與IO模組相連接,單片機程式通過這類寄存器來控制各種外設,我們今天要學習的數字信號IO寄存器就屬於這一類,並且應該是其中最簡單的了。

我們使用的單片機的型號是ATmega324PA,它有多種封裝,引腳(pin)數不盡相同,但都有32個通用輸入輸出(GPIO)引腳。由於AVR架構是8位字長的,CPU一次處理1位數據和8位數據所需的時間是一樣的,這32個引腳被組織為4個埠(port),分別是PA、PB、PC和PD。

在AVR架構tiny與mega系列的單片機中,每個埠都有3個寄存器控制數字信號IO,分別是PORTx、DDRx和PINx。這裡的x是A、B、C或D,由於這4個埠在數字IO方面完全相同,就把它們合併起來講。相應地,對於每個引腳Pxn,有PORTxn、DDxn(沒有R)和PINxn三個bit控制其數字IO。

DDxn控制引腳方向:當DDxn為1時,Pxn為輸出;當DDxn為0時,Pxn為輸入。

當Pxn為輸入時,如果PORTxn為1,則該引腳通過一個上拉電阻連接到VCC;否則引腳懸空。

當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 }

再結合按鍵動作的知識,你應該知道怎樣直接通過寄存器操作來判斷按鍵動作了吧。

 

之前留了一個問題,就是位操作是如何實現的。以下為 <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?有寄存器。匯流排通信?通過寄存器。記憶體分頁?需要寄存器。萬物基於寄存器。又有更多像寄存器一樣的紐帶,在電子空穴與豐富多彩的電腦世界之間建立起聯繫。它們看起來如此複雜,卻又清晰明瞭,就算一夜之間所有電腦都突然消失,人類也能從電子管和打孔紙帶開始,一層一層地構建起電腦的世界。而我們瞭解的只不過是這個巨大體系中的滄海一粟。

初入電腦世界,你想著電腦能幹什麼,學完電腦我能幹什麼。而電腦世界是如此高深,在逐漸深入後,你會明白電腦不能幹什麼,我不能幹什麼。數位管、蜂鳴器,它們一直在你的開發板上,你卻不知道如何使用它們;更不用說那些高級到你沒聽說過的東西。學得越多,你會發現雖然原本不會的減少了,但腦海中萌生出的“不切實際”的想法更多——學習的速度永遠趕不上認知的速度。本系列教程不能幫你消除所有“不會”,而是要在帶你一步步消除一些“不會”的過程中讓你學會發現更多“不會”並消除的方法。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 今天我們來安裝和測試一下php的多併發高性能網路通信擴展,這個擴展是使用C語音開發的,載入到PHP以後,在PHP的層面上實現了多併發非同步通信,模擬了go語音的很多特性,極大的拓寬了PHP的應用場景。 直接使用官網上的那句命令就可以,安裝swoole時可能會出現錯誤和卡住不動,多試幾次就能成功。pec ...
  • 假定有下麵這樣的列表: 編寫一個函數,它以一個列表值作為參數,返回一個字元串。該字元串包含所有表項,表項之間以逗號和空格分隔,併在最後一個表項之前插入 and。例如,將前面的 spam 列表傳遞給函數,將返回'apples, bananas, tofu, and cats'。但你的函數應該能夠處理傳 ...
  • 今日主要內容 正則表達式 logging模塊 一、正則表達式 (一)什麼是正則表達式 1. 正則表達式的定義: 是對字元串操作的一種邏輯公式,就是用事先定義好的一些特定字元、及這些特定字元的組合,組成一個“規則字元串”,這個“規則字元串”用來表達對字元串的一種過濾邏輯。 簡單來說,我們使用正則表達式 ...
  • 由於JDK中提供的ByteBuffer無法動態擴容,並且API使用複雜等原因,Netty中提供了ByteBuf。Bytebuf的API操作更加便捷,可以動態擴容,提供了多種ByteBuf的實現,以及高效的零拷貝機制。 ByteBuf的操作 ByteBuf有三個重要的屬性:capacity容量,rea ...
  • 構建環境 macOS 10.13.6 JDK1.8 IntelliJ IDEA 2018.3.6 (Ultimate Edition) "Spring v5.1.9.RELEASE" Gradle 5.5.1。直接使用brew安裝Gradle 源碼構建 1.源碼導入 2.閱讀Spring源碼下的 i ...
  • 1 import random 2 3 def v_code(): 4 for i in range(5): 5 code1 = random.randrange(10) #生成一個隨機0-9隨機數字 6 code2 = random.choice(chr(random.randrange(65,9 ...
  • [TOC] numpy模塊 numpy簡介 numpy官方文檔: numpy是Python的一種開源的數值計算擴展庫。這種庫可用來存儲和處理大型numpy數組,比Python自身的嵌套列表結構要高效的多(該結構也可以用來表示numpy數組)。 numpy庫有兩個作用: 1. 區別於list列表,提供 ...
  • 關於虛擬機模板 想用vagrant搭建hadoop集群,要完成以下準備工作: 1. 三個虛擬機實例操作系統都是CentOS7的server版; 2. 每個實例都要安裝同樣的應用、關閉防火牆、關閉swap等; 今天就來做個模板,用此模板創建好的虛擬機都已經完成了上述操作; 關於vagrant的安裝和基 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...