書名: 顯示器件應用分析精粹:從芯片架構到驅動程序設計作者名: 龍虎本章字數: 4624字更新時間: 2024-05-10 13:37:34
第2章 51單片機驅動開發
雖然搭建純硬件電路可以驅動LED,但使用單片機會帶來更大的靈活性,更何況,使用純硬件電路驅動比較復雜的顯示器件是不太現實的,所以提前介紹一些單片機開發知識很有必要。
我們以決定選擇51單片機,是因為雖然它看起來好像有些過時,但本書并不是專門介紹單片機開發的,所以主要考慮平臺應用的廣泛性以及學習資料的豐富性,這對于讀者學習與理解都非常有利,而51單片機在這兩方面絕對獨占鰲頭。另外,使用C51語言編寫的程序非常接近硬件底層,而且我們只會用到順序編程(盡管下一章會討論中斷編程,但主要還是為了介紹這種思路,實際編寫顯示驅動時并沒有使用到)。也就是說,即便在閱讀本書前從未使用過51單片機,也不妨礙理解顯示器件的驅動原理,只要對各類顯示器件的應用知識感興趣,后續內容一定是非常有價值的。
首先宏觀了解一下單片機。現階段的我們不需要想得很復雜,只需要認為它是一個包含CPU(Central Processing Unit,中央處理器)、ROM(Read-Only Memory,只讀存儲器)、RAM(Random Access Memory,隨機存取存儲器)三個部分的芯片(雖然細節上復雜得多,但我們無需理會,等入門后可進一步自行了解),如圖2.1所示。

圖2.1 單片機的基本單元
其中,CPU具有一定運算功能(例如加、減、乘、除等);ROM用來存儲數據(一般是指令程序,所以也稱為程序存儲器),并且CPU只能從ROM中讀取而不能寫入(修改)數據;RAM也是存儲數據的單元,通常被稱為數據存儲器,CPU不僅可以從中讀取數據,還可以把數據寫入進去。整個單片機的運行過程即:CPU按順序從ROM中提取并執行相應的指令,而RAM用來存儲執行指令期間可能需要的一些臨時數據。
那RAM到底存儲什么樣的臨時數據呢?例如,需要使用計算器得到5+(6×8)的計算結果,首先得計算出臨時值6×8=48,然后再計算出5+48=53。在這個過程中,可能會將臨時值先記在紙上(或心里),而RAM的作用就相當于那張紙,它允許將一些臨時的數據記錄在上面以供后續使用。這么一個簡單的運算就存在一個臨時數據,可想而知,運算越復雜則需要的臨時數據也會越多,也就需要更大空間的RAM。
單片機的運行過程與人的活動非常相似,人就相當于CPU,而事先做好的計劃就相當于ROM中存儲的指令,在計劃執行的過程中臨時需要處理的事情就相當于RAM存儲的數據,如圖2.2所示。

圖2.2 人的活動
現在的任務是:使用51單片機控制LED實現閃爍功能。Proteus軟件平臺搭建的硬件電路如圖2.3所示。

圖2.3 硬件電路
我們選擇Proteus軟件平臺中引腳相對較少的一款51單片機,其型號為AT89C1051(本書涉及的51單片機是指Intel公司推出的8位MCS-51單片機,通常稱為“51內核”。Atmel公司獲得51內核授權后,在其基礎上設計了一系列應用單片機,AT89C1051就是其中一款應用單片機的型號,如果后續提到的是“51單片機”,表示所述內容對于所有基于“51內核”的應用單片機都是適用的),它可供控制的輸入或輸出(Input/Output,IO)引腳有15個,51單片機將它們分為P1、P3兩組,每一組包含8個引腳,而每個引腳通過“組名+點+數字”的方式加以區分。例如,P1.7、P3.2(AT89C1051沒有P3.6引腳)。
XTAL1與XTAL2兩個引腳外圍連接了一顆12MHz的晶體與兩個30pF的匹配電容,它們與單片機內部電路配合可產生12MHz的時鐘(如果想接外部時鐘源,可以從XTAL1接入,XTAL2懸空即可),CPU就會以時鐘作為基準,依次從ROM中讀取指令并執行。另外,我們還增加了一個RC復位電路,這樣可以確保單片機上電后從ROM的首地址開始執行。
需要注意的是,使用Proteus軟件平臺進行仿真時,時鐘與復位相關的器件都不是必須的,沒有它們也可以運行出相同的結果,所以后續為了簡化電路,這些器件可能不會再添加。另外,Proteus軟件平臺中大多數芯片的原理圖符號沒有顯示電源與地引腳。
現在需要通過單片機循環點亮與熄滅LED(實現LED閃爍功能),也就是以一定的時間間隔讓P1.7引腳循環輸出低電平與高電平,應該怎么做呢?CPU與外界溝通時只做一件事:通過地址讀取或寫入數據。當它從ROM中讀取指令時,是通過把程序計數器(Program Counter,PC)給出的地址賦給ROM再順序讀取指令,當它往(從)RAM寫入(讀取)臨時數據時,也是先將地址賦給RAM再進行操作。對于IO引腳也是完全一樣,只不過51單片機為它們分配了一個特殊寄存器(Special Function Registers,SFR)。AT89C1051的特殊寄存器地址與復位值如圖2.4所示。

圖2.4 特殊寄存器地址與復位值
51單片機中的特殊寄存器地址的范圍為80H~FFH(后綴“H”或“h”表示十六進制,“D”或“d”表示十進制,“B”或“b”表示二進制),每個地址對應一個8位寄存器,并且對使用到的寄存器均賦給了一個名稱,同時還給出了復位時的初始值。例如,地址為E0H對應的寄存器名稱為ACC,復位值為全0。但是正如圖2.4中展示的,AT89C1051單片機中很多特殊寄存器地址都是空白的(表示該單片機型號沒有使用到),它們可以預留給其他型號的單片機,現階段的你無需理會。
現在需要對LED進行控制,而LED與單片機相連接的引腳為P1.7,所以必須找到P1的地址才能對其進行控制。從圖2.4可以看到,P1的地址為90H,其復位值為全1。P1每一個引腳與90H地址所在的8位寄存器P1的對應關系如圖2.5所示。

圖2.5 P1特殊寄存器與引腳的對應關系
從圖中可以看到,P1寄存器中的每一位都對應單片機一個控制引腳(它們分別為P1.7、P1.6、P1.5、P1.4、P1.3、P1.2、P1.1、P1.0),寄存器最高位對應P1.7,最低位對應P1.0。單片機上電后會進行復位,其值為全1。也就是說,對于圖2.3所示電路,由于P1.7為高電平,所以LED是不亮的。那么為了將LED其點亮,我們應該將P1.7設置為低電平,也就是給P1寄存器所在地址寫入7FH(二進制01111111B)。
我們來看看相應的C51驅動源代碼,如清單2.1所示。

清單2.1 LED閃爍功能
在源代碼的最開始,首先使用關鍵字sfr定義了一個標識符P1(也可以取其他的名字),它的值就是前面從特殊寄存器區域找到的P1地址:0x90(前綴0x表示十六進制,不加前綴且以1~9開頭的數字表示十進制,這是C語言約定的數字進制表達方式,與前述后綴H、D是對應的,但是在進行C語言編程時只能使用前綴方式表達數字進制。另外,我們進一步約定前綴0b表示二進制以方便后續行文,因為C語言并不支持二進制整數的形式)。
有人可能會問:為什么不使用unsigned char來定義P1呢?問得好!在C51編程語言中,使用關鍵字sfr就相當于告訴編譯軟件:我現在定義的是一個特殊功能寄存器,而不是一個變量。就相當于給地址為0x90的特殊功能寄存器取了一個別名,這樣以后訪問它就不用總是記著0x90這個數字地址,使用起來會很方便,這同時也是一個很好的習慣。如果使用unsigned char定義一個P1,只能表示定義的是一個變量,后續對該變量的操作也不能控制相應的特殊寄存器P1。簡單地說,變量的定義并沒有建立它與特殊功能寄存器之間的映射關系。
main主函數是整個代碼的執行入口,我們首先聲明了兩個無符號整形變量(i,j)用于后續需要的延時操作。接下來while語句判斷小括號內的表達式,如果為0,則不執行大括號{}中的語句,如果為非0,則執行花括號中的語句。為了使代碼更為緊湊,本書采用傳統的K&R風格來書寫大括號,這種風格把左大括號留在前一行的末尾,而不是另起并占據一行。
while語句后小括號內的表達式設置為1,它是非0的,表示無限循環執行大括號中語句。具體操作是這樣的:首先給P1賦給0x7F,也就能夠讓P1.7為低電平而點亮LED。然后再用兩個嵌套的for語句延時約1s(不需要很精確),這是非常必要的,因為在不做延時的情況下,單片機的運行速度非常快,肉眼將無法觀察到LED閃爍狀態。緊接著再將P1賦為0xFF,也就能夠讓P1.7為高電平而使LED熄滅,最后延時約1s后回到循環的開始,又將P1賦為0x7F……如此循環運行下去,LED就會一直不停地閃爍起來。
終于可以正常工作了,這實在是一件美好的事情。接下來我們對剛剛編寫好的源代碼進行一些優化,相應的源代碼如清單2.2所示。

清單2.2 優化后的源代碼
優化后的源代碼中并沒有再使用關鍵字sfr定義標識符P1,但是main函數內部仍然使用了P1標識符,這是為什么呢?注意到我們在源代碼開頭包含了一個名為reg51.h的頭文件,打開看一下里面是什么內容,如清單2.3所示(部分)。

清單2.3 頭文件reg51.h(部分)
可以看到,頭文件reg51.h里面使用關鍵字sfr定義了很多標識符,我們之前定義的P1就在里面。reg51.h是廠家已經定義好的通用頭文件,51單片機中所有特殊功能寄存器的定義都包含在里面,這意味著如果需要對某個特殊功能寄存器進行訪問,只需要從中查看相應的標識符并對其進行操作即可。例如,標識符P3定義的地址為0xB0,這與圖2.4所示的地址B0H是完全對應的。
然后我們進一步使用while語句重寫了延時代碼并包裝成了delay_us與delay_ms兩個函數,它們分別表示以微秒與毫秒為單位進行延時。為什么要重新包裝函數呢?因為從清單2.1可以看到,兩處延時約1s的代碼是完全一樣的,包裝成函數就可以方便我們在代碼中進行多次調用。需要特別注意的是,此處包裝的delay_us函數的延時并不是精確的微秒數,因為C51代碼中while語句編譯成匯編語言后并不僅僅只是一條語句,從Keil軟件平臺獲取的匯編指令如圖2.6所示。
圖2.6中第一行是C51語句,下面是對應的匯編指令。最左側第1列以“C:”開頭的十六進制數字為程序存儲器中存儲指令的地址,第2列為指令對應的二進制數字(也稱為“機器碼”),程序存儲器中存儲的就是它,第3、4列為相應的匯編指令。
可以看到,delay_us函數被分解成為11條匯編指令,在單片機運行在12MHz頻率時鐘的前提下,除JNC、JNZ、RET指令需耗時2μs(微秒)外,其他指令均耗時1μs,再加上調用該函數也需要用到LCALL指令(耗時2μs),所以總共消耗的時間約為2μs(LCALL指令)+12μs(地址范圍0x0036~0x0043之間的指令)+2μs(RET指令)=16μs。換句話說,即便給delay_us函數傳遞的延時參數為0,它也將消耗不少時間。也正因為如此,我們在delay_ms函數中調用delay_us函數時傳遞的延時參數值并不是1000,而是60。因為1ms/16μs=62.5,再考慮到delay_ms函數本身也會消耗一些時間,所以取了個小一點的時間值。當然,我們這里對延時要求并不高,只要大約為1s就可以了,但這種延時計算的方法在一些對延時精度要求很高的場合將會非常有用,不熟悉匯編語言的讀者了解一下即可,并不影響后續的程序設計。對于需要精確延時幾微秒(例如1μs、2μs)的場合,我們會在合適的場合討論具體實現方法。

圖2.6 毫秒匯編指令
另外,在點亮LED的語句中,我們將P1與0x7F進行“與”運算,由于“有0為0,全1為1”的運算特性,所以P1的最高位P1.7將被置0,而其他位對應的引腳狀態將不變,這樣可以避免之前給P1直接賦值0x7F的“野蠻”方式對其他位帶來影響(把低7位都置1了)。在這個簡單例子中,P1口的其他引腳并沒有使用到,然而一旦其他引腳也被用來作為控制使用,這種考慮是必須的。在熄滅LED代碼中,將P1與0x80進行“或”運算,由于“有1為1,全0為0”的運算特性,所以P1的最高位P1.7將被置1,而其他位將不變,同樣可以避免影響P1口其他位的現有狀態。
代碼總是會有可以優化的空間,再一次修改的代碼如清單2.4所示。

清單2.4 再次優化的代碼
首先使用define預編譯指令把前面代碼中的數字定義為有意義的宏標識符,這樣能使源代碼的可讀性更強,而且一旦外部電路的行為進行了更改(例如,修改后LED要求的驅動電平是反的,或者閃爍的速度更快),只需要修改幾個宏定義即可,而沒有必要花費心思去源代碼內部對應位置進行修改。是不是很方便?
接下來使用關鍵字typedef將unsigned int類型定義了個別名uint,后續在需要定義unsigned int類型變量時可以直接使用uint,這樣代碼會更加簡潔。然后使用關鍵字sbit(與關鍵字sfr所起的作用是完全一樣的,只不過sbit是給特殊功能寄存器的某一位定義別名)將控制發光二極管的引腳P1.7也定義了一個別名LED,這樣在代碼閱讀時就很容易理解賦值的具體含義。另外,語句“LED ^=1”相當于“LED=LED ^ 1”,其中符號“^”表示“異或”運算,由于它有“相同為0,不同為1”的運算特性,也就可以將LED驅動電平取反。