一個非常詳細的單片機編程教程分享給大家,從LED和KEY入手提高單片機編程能力
前言 我們用學單片機不要停在演示的基礎上。只能讓單片機完成局部事。這樣我們永遠不會走出流水燈地獄!!! 學習單片機也已經(jīng)有幾年了,藉此機會和大家聊一下我學習過程中的一些經(jīng)歷和想法吧。也感謝一線工人提供了這個機會。希望大家有什么好的想法和建議都直接跟帖說出來。畢竟只有交流才能夠碰撞出火花來 ^_^。
“賣弄”也好,“吹噓”也罷,我只是想認真的寫寫我這一路走來歷經(jīng)的總總,把其中值得注意,以及經(jīng)驗的地方寫出來,權當是我對自己的一個總結吧。而作為看官的你,如果看到了我的錯誤,還請一定指正,這樣對我以及其它讀者都有幫助,而至于你如果從中能夠收獲到些許,那便是我最大的欣慰了。姑妄言之,姑妄聽之。如果有啥好的想法和建議一定要說出來。幾年前,和眾多初學者一樣,我接觸到了單片機,立刻被其神奇的功能所吸引,從此不能自拔。很多個日夜就這樣陪伴著它度過了。期間也遇到過非常多的問題,也一度被這些問題所困惑……等到回過頭來,看到自己曾經(jīng)走過的路,唏噓不已。經(jīng)常混跡于論壇里,也看到了很多初學者發(fā)的求助帖子,看到他們走在自己曾走過的彎路上,忽然想到了自己的那段日子,心里竟然莫名的沖動,凡此總總,我總是盡自己所能去回帖。很多時候,都想寫一點什么東西出來,希望對廣大的初學者有一點點幫助。但總是不知從何處寫起。今天借一線工人的臺,唱一唱我的戲。
一路學習過來的過程中,幫助最大之一無疑來自于網(wǎng)絡了。很多時候,通過網(wǎng)絡,我們都可以獲取到所需要的學習資料。但是,隨著我們學習的深入,我們會慢慢發(fā)現(xiàn),網(wǎng)絡提供的東西是有限度的,好像大部分的資料都差不多,或者說是適合大部分的初學者所需,而當我們想更進一步提高時,卻發(fā)現(xiàn)能夠獲取到的資料越來越少,相信各位也會有同感,鋪天蓋地的單片機資料中大部分不是流水燈就是LED,液晶,而且也只是僅僅作功能性的演示。于是有些人選擇了放棄,或者是轉移到其他興趣上面去了,而只有少部分人選擇了繼續(xù)摸索下去,結合市面上的書籍,然后在網(wǎng)絡上鍥而不舍的搜集資料,再從牛人的只言片語中去體會,不斷動手實踐,慢慢的,也摸索出來了自己的一條路子。當然這個過程必然是艱辛的,而他學會了之后也不會在網(wǎng)絡上輕易分享自己的學習成果。如此惡性循環(huán)下去,也就不難理解為什么初級的學習資料滿天飛,而深入一點的學習資料卻很少的原因了。相較于其他領域,單片機技術的封鎖更加容易。盡管已經(jīng)問世了很多年了,有價值的資料還是相當?shù)那啡保蟛糠值馁Y料都是止于入門階段或者是簡單的演示實驗。但是在實際工程應用中卻是另外一回事。有能力的高手無暇或者是不愿公開自己的學習經(jīng)驗。
很多時候,我也很困惑,看到國外愛好者毫不保留的在網(wǎng)絡上發(fā)布自己的作品,我忽然感覺到一絲絲的悲哀。也許,我們真的該轉變一下思路了,幫助別人,其實也是在幫助自己。啰啰嗦嗦的說了這么多,相信大家能夠明白說的是什么意思。在接下來的一段日子里,我將會結合電子工程師之家舉辦的主題周活動寫一點自己的想法。盡可能從實用的角度去講述。希望能夠幫助更多的初學者更上一層樓。而關于這個主題周的最大主題我想了這樣的一個名字“從單片機初學者邁向單片機工程師”。名字挺大挺響亮,給我的壓力也挺大的,但我會努力,爭取使這樣的一系列文章能夠帶給大家一點幫助,而不是看后大跌眼鏡。這樣的一系列文章主要的對象是初學者,以及想從初學者更進一步提高的讀者。而至于老手,以及那些牛XX的人,希望能夠給我們這些初學者更多的一些指點哈~@_@ 。
第一章 流水燈
從這一章開始,我們開始邁入單片機的世界。在我們開始這一章具體的學習之前,有必要給大家先說明一下。在以后的系列文章中,我們將以51內核的單片機為載體,C語言為編程語言,開發(fā)環(huán)境為KEIL uv3。至于為什么選用C語言開發(fā),好處不言而喻,開發(fā)速度快,效率高,代碼可復用率高,結構清晰,尤其是在大型的程序中,而且隨著編譯器的不斷升級,其編譯后的代碼大小與匯編語言的差距越來越小。而關于C語言和匯編之爭,就像那個啥,每隔一段時間總會有人挑起這個話題,如果你感興趣,可以到網(wǎng)上搜索相關的帖子自行閱讀。不是說匯編不重要,在很多對時序要求非常高的場合,需要利用匯編語言和C語言混合編程才能夠滿足系統(tǒng)的需求。在我們學習掌握C語言的同時,也還需要利用閑余的時間去學習了解匯編語言。
- 從點亮LED(發(fā)光二極管)開始
- 在市面上眾多的單片機學習資料中,最基礎的實驗無疑于點亮LED了,即控制單片機的I/O的電平的變化。
如同如下實例代碼一般
void main(void)
{
LedInit() ;
While(1)
{
LED = ON ;
DelayMs(500) ;
LED = OFF ;
DelayMs(500) ;
}
}
程序很簡單,從它的結構可以看出,LED先點亮500MS,然后熄滅500MS,如此循環(huán)下去,形成的效果就是LED以1HZ的頻率進行閃爍。下面讓我們分析上面的程序有沒有什么問題。
看來看出,好像很正常的啊,能有什么問題呢?這個時候我們應該換一個思路去想了。試想,整個程序除了控制LED = ON ; LED = OFF; 這兩條語句外,其余的時間,全消耗在了DelayMs(500)這兩個函數(shù)上。而在實際應用系統(tǒng)中是沒有哪個系統(tǒng)只閃爍一只LED就其它什么事情都不做了的。因此,在這里我們要想辦法,把CPU解放出來,讓它不要白白浪費500MS的延時等待時間。寧可讓它一遍又一遍的掃描看有哪些任務需要執(zhí)行,也不要讓它停留在某個地方空轉消耗CPU時間。
從上面我們可以總結出:
(1)無論什么時候我們都要以實際應用的角度去考慮程序的編寫。
(2)無論什么時候都不要讓CPU白白浪費等待,尤其是延時(超過1MS)這樣的地方。
下面讓我們從另外一個角度來考慮如何點亮一顆LED。 先看看我們的硬件結構是什么樣子的。 我手上的單片機板子是電子工程師之家的開發(fā)的學習板。就以它的實際硬件連接圖來分析吧。如下圖所示 一般的LED的正常發(fā)光電流為10~20MA而低電流LED的工作電流在2mA以下(亮度與普通發(fā)光管相同)。在上圖中我們可知,當Q1~Q8引腳上面的電平為低電平時,LED發(fā)光。通過LED的電流約為(VCC - Vd)/ RA2 。其中Vd為LED導通后的壓降,約為1.7V左右。這個導通壓降根據(jù)LED顏色的不同,以及工作電流的大小的不同,會有一定的差別。下面一些參數(shù)是網(wǎng)上有人測出來的,供大家參考。 紅色的壓降為1.82-1.88V,電流5-8mA, 綠色的壓降為1.75-1.82V,電流3-5mA, 橙色的壓降為1.7-1.8V,電流3-5mA , 蘭色的壓降為3.1-3.3V,電流8-10mA, 白色的壓降為3-3.2V,電流10-15mA, (供電電壓5V,LED直徑為5mm)
74HC573真值表如下: 通過這個真值表我們可以看出。當OutputEnable引腳接低電平的時候,并且LatchEnable引腳為高電平的時候,Q端電平與D端電平相同。結合我們的LED硬件連接圖可以知道LED_CS端為高電平時候,P0口電平的變化即Q端的電平的變化,進而引起LED的亮滅變化。由于單片機的驅動能力有限,在此,74HC573的主要作用就是起一個輸出驅動的作用。需要注意的是,通過74HC573的最大電流是有限制的,否則可能會燒壞74HC573這個芯片。 上面這個圖是從74HC573的DATASHEET中截取出來的,從上可以看出,每個引腳允許通過的最大電流為35mA 整個芯片允許通過的最大電流為75mA。在我們設計相應的驅動電路時候,這些參數(shù)是相當重要的,而且是最容易被初學者所忽略的地方。同時在設計的時候,要留出一定量的余量出來,不能說單個引腳允許通過的電流為35mA,你就設計為35mA,這個時候你應該把設計的上限值定在20mA左右才能保證能夠穩(wěn)定的工作。(設計相應驅動電路時候,應該仔細閱讀芯片的數(shù)據(jù)手冊,了解每個引腳的驅動能力,以及整個芯片的驅動能力) 了解了相應的硬件后,我們再來編寫驅動程序。 首先定義LED的接口
#define LED P0
然后為亮滅常數(shù)定義一個宏,由硬件連接圖可以,當P0輸出為低電平時候LED亮,P0輸出為高電平時,LED熄滅。
#define LED_ON() LED = 0x00 //所有LED亮
#define LED_OFF() LED = 0xff //所有LED熄滅
下面到了重點了,究竟該如何釋放CPU,避免其做延時空等待這樣的事情呢。很簡單,我們?yōu)橄到y(tǒng)產生一個1MS的時標。假定LED需要亮500MS,熄滅500MS,那么我們可以對這個1MS的時標進行計數(shù),當這個計數(shù)值達到500時候,清零該計數(shù)值,同時把LED的狀態(tài)改變。
unsigned int g_u16LedTimeCount = 0 ; //LED計數(shù)器
unsigned char g_u8LedState = 0 ; //LED狀態(tài)標志, 0表示亮,1表示熄滅
void LedProcess(void)
{
if(0 == g_u8LedState) //如果LED的狀態(tài)為亮,則點亮LED
{
LED_ON() ;
}
else //否則熄滅LED
{
LED_OFF() ;
}
}
void LedStateChange(void)
{
if(g_bSystemTime1Ms) //系統(tǒng)1MS時標到
{
g_bSystemTime1Ms = 0 ;
g_u16LedTimeCount++ ; //LED計數(shù)器加一
if(g_u16LedTimeCount >= 500) //計數(shù)達到500,即500MS到了,改變LED的狀態(tài)。
{
g_u16LedTimeCount = 0 ;
g_u8LedState = ! g_u8LedState ;
}
}
}
上面有一個變量沒有提到,就是g_bSystemTime1Ms 。這個變量可以定義為位變量或者是其它變量,在我們的定時器中斷函數(shù)中對其置位,其它函數(shù)使用該變量后,應該對其復位(清0) 。 我們的主函數(shù)就可以寫成如下形式(示意代碼)
void main(void)
{
while(1)
{
LedProcess() ;
LedStateChange() ;
}
}
因為LED的亮或者滅依賴于LED狀態(tài)變量(g_u8LedState)的改變,而狀態(tài)變量的改變,又依賴于LED計數(shù)器的計數(shù)值(g_u16LedTimeCount ,只有計數(shù)值達到一定后,狀態(tài)變量才改變)所以,兩個函數(shù)都沒有堵塞CPU的地方。讓我們來從頭到尾分析一遍整個程序的流程。 程序首先執(zhí)行LedProcess() ;函數(shù) 因為g_u8LedState 的初始值為0 (見定義,對于全局變量,在定義的時候最好給其一個確定的值)所以LED被點亮,然后退出LedStateChange()函數(shù),執(zhí)行下一個函數(shù)LedStateChange() ,在函數(shù)LedStateChange()內部首先判斷1MS的系統(tǒng)時標是否到了,如果沒有到就直接退出函數(shù),如果到了,就把時標清0以便下一個時標消息的到來,同時對LED計數(shù)器加一,然后再判斷LED計數(shù)器是否到達我們預先想要的值500,如果沒有,則退出函數(shù),如果有,對計數(shù)器清0,以便下次重新計數(shù),同時把LED狀態(tài)變量取反,然后退出函數(shù)。 由上面整個流程可以知道,CPU所做的事情,就是對一些計數(shù)器加一,然后根據(jù)條件改變狀態(tài),再根據(jù)這個狀態(tài)來決定是否點亮LED。這些函數(shù)執(zhí)行所花的時間都是相當短的,如果主程序中還有其它函數(shù),則CPU會順次往下執(zhí)行下去。對于其它的函數(shù)(如果有的話)也要采取同樣的措施,保證其不堵塞CPU,如果全部基于這種方法設計,那么對于不是非常龐大的系統(tǒng),我們的系統(tǒng)依舊可以保證多個任務(多個函數(shù))同時執(zhí)行。系統(tǒng)的實時性得到了一定的保證,從宏觀上看來,就是多個任務并發(fā)執(zhí)行。 好了,這一章就到此為止,讓我們總結一下,究竟有哪些需要注意的吧。 (1) 無論什么時候我們都要以實際應用的角度去考慮程序的編寫。
(2) 無論什么時候都不要讓CPU白白浪費等待,尤其是延時(超過1MS)這樣的地方。
(3) 設計相應驅動電路時候,應該仔細閱讀芯片的數(shù)據(jù)手冊,了解每個引腳的驅動能力,以及整個芯片的驅動能力。
(4) 最重要的是,如何去釋放CPU(參考本章的例子),這是寫出合格程序的基礎。
附完整程序代碼(基于電子工程師之家的單片機開發(fā)板)
- #include<reg52.h>
- sbit LED_SEG = P1^4; //數(shù)碼管段選
- sbit LED_DIG = P1^5; //數(shù)碼管位選
- sbit LED_CS11 = P1^6; //led控制位
- sbit ir=P1^7;
- #define LED P0 //定義LED接口
- bit g_bSystemTime1Ms = 0 ; // 1MS系統(tǒng)時標
- unsigned int g_u16LedTimeCount = 0 ; //LED計數(shù)器
- unsigned char g_u8LedState = 0 ; //LED狀態(tài)標志, 0表示亮,1表示熄滅
- #define LED_ON() LED = 0x00 ; //所有LED亮
- #define LED_OFF() LED = 0xff ; //所有LED熄滅
- void Timer0Init(void)
- {
- TMOD &= 0xf0 ;
- TMOD |= 0x01 ; //定時器0工作方式1
- TH0 = 0xfc ; //定時器初始值
- TL0 = 0x66 ;
- TR0 = 1 ;
- ET0 = 1 ;
- }
- void LedProcess(void)
- {
- if(0 == g_u8LedState) //如果LED的狀態(tài)為亮,則點亮LED
- {
- LED_ON() ;
- }
- else //否則熄滅LED
- {
- LED_OFF() ;
- }
- }
- void LedStateChange(void)
- {
- if(g_bSystemTime1Ms) //系統(tǒng)1MS時標到
- {
- g_bSystemTime1Ms = 0 ;
- g_u16LedTimeCount++ ; //LED計數(shù)器加一
- if(g_u16LedTimeCount >= 500) //計數(shù)達到500,即500MS到了,改變LED的狀態(tài)。
- {
- g_u16LedTimeCount = 0 ;
- g_u8LedState = ! g_u8LedState ;
- }
- }
- }
- void main(void)
- {
- Timer0Init() ;
- EA = 1 ;
- LED_CS11 = 1 ; //74HC595輸出允許
- LED_SEG = 0 ; //數(shù)碼管段選和位選禁止(因為它們和LED共用P0口)
- LED_DIG = 0 ;
- while(1)
- {
- LedProcess() ;
- LedStateChange() ;
- }
- }
- void Time0Isr(void) interrupt 1
- {
- TH0 = 0xfc ; //定時器重新賦初值
- TL0 = 0x66 ;
- g_bSystemTime1Ms = 1 ; //1MS時標標志位置位
- }
復制代碼
第二章----模塊化編程初識 好的開始是成功的一半 通過上一章的學習,我想你已經(jīng)掌握了如何在程序中釋放CPU了。希望能夠繼續(xù)堅持下去。一個良好的開始是成功的一半。我們今天所做的一切都是為了在單片機編程上做的更好。 在談論今天的主題之前,先說下我以前的一些經(jīng)歷。在剛開始接觸到C語言程序的時候,由于學習內容所限,寫的程序都不是很大,一般也就幾百行而矣。所以所有的程序都完成在一個源文件里面。記得那時候大一參加學校里的一個電子設計大賽,調試了一個多星期,所有程序加起來大概將近1000行,長長的一個文件,從上瀏覽下來都要好半天。出了錯誤簡單的語法錯誤還好定位,其它一些錯誤,往往找半天才找的到。那個時候開始知道了模塊化編程這個東西,也嘗試著開始把程序分模塊編寫。最開始是把相同功能的一些函數(shù)(譬如1602液晶的驅動)全部寫在一個頭文件(.h)文件里面,然后需要調用的地方包含進去,但是很快發(fā)現(xiàn)這種方法有其局限性,很容易犯重復包含的錯誤。 而且調用起來也很不方便。很快暑假的電子設計大賽來臨了,學校對我們的單片機軟件編程進行了一些培訓。由于學校歷年來參加國賽和省賽,因此積累了一定數(shù)量的驅動模塊,那些日子,老師每天都會布置一定量的任務,讓我們用這些模塊組合起來,完成一定功能。而正是那些日子模塊化編程的培訓,使我對于模塊化編程有了更進一步的認識。并且程序規(guī)范也開始慢慢注意起來。此后的日子,無論程序的大小,均采用模塊化編程的方式去編寫。很長一段時間以來,一直有單片機愛好者在QQ上和我一起交流。有時候,他們會發(fā)過來一些有問題的程序源文件,讓我?guī)兔π薷囊幌隆M瑯邮情L長的一個文件,而且命名極不規(guī)范,從頭看下來,著實是痛苦,說實話,還真不如我重新給他們寫一個更快一些,此話到不假,因為手頭積累了一定量的模塊,在完成一個新的系統(tǒng)時候,只需要根據(jù)上層功能需求,在底層模塊的支持下,可以很快方便的完成。而不需要從頭到尾再一磚一瓦的重新編寫。藉此,也可以看出模塊化編程的一個好處,就是可重復利用率高。下面讓我們揭開模塊化神秘面紗,一窺其真面目。 C語言源文件 *.c ,
提到C語言源文件,大家都不會陌生。因為我們平常寫的程序代碼幾乎都在這個XX.C文件里面。編譯器也是以此文件來進行編譯并生成相應的目標文件。作為模塊化編程的組成基礎,我們所要實現(xiàn)的所有功能的源代碼均在這個文件里。理想的模塊化應該可以看成是一個黑盒子。即我們只關心模塊提供的功能,而不管模塊內部的實現(xiàn)細節(jié)。好比我們買了一部手機,我們只需要會用手機提供的功能即可,不需要知曉它是如何把短信發(fā)出去的,如何響應我們按鍵的輸入,這些過程對我們用戶而言,就是是一個黑盒子。
在大規(guī)模程序開發(fā)中,一個程序由很多個模塊組成,很可能,這些模塊的編寫任務被分配到不同的人。而你在編寫這個模塊的時候很可能就需要利用到別人寫好的模塊的借口,這個時候我們關心的是,它的模塊實現(xiàn)了什么樣的接口,我該如何去調用,至于模塊內部是如何組織的,對于我而言,無需過多關注。而追求接口的單一性,把不需要的細節(jié)盡可能對外部屏蔽起來,正是我們所需要注意的地方。 C語言頭文件 *.h
談及到模塊化編程,必然會涉及到多文件編譯,也就是工程編譯。在這樣的一個系統(tǒng)中,往往會有多個C文件,而且每個C文件的作用不盡相同。在我們的C文件中,由于需要對外提供接口,因此必須有一些函數(shù)或者是變量提供給外部其它文件進行調用。 假設我們有一個LCD.C文件,其提供最基本的LCD的驅動函數(shù)
LcdPutChar(char cNewValue) ; //在當前位置輸出一個字符
而在我們的另外一個文件中需要調用此函數(shù),那么我們該如何做呢?
頭文件的作用正是在此。可以稱其為一份接口描述文件。其文件內部不應該包含任何實質性的函數(shù)代碼。我們可以把這個頭文件理解成為一份說明書,說明的內容就是我們的模塊對外提供的接口函數(shù)或者是接口變量。同時該文件也包含了一些很重要的宏定義以及一些結構體的信息,離開了這些信息,很可能就無法正常使用接口函數(shù)或者是接口變量。但是總的原則是:不該讓外界知道的信息就不應該出現(xiàn)在頭文件里,而外界調用模塊內接口函數(shù)或者是接口變量所必須的信息就一定要出現(xiàn)在頭文件里,否則,外界就無法正確的調用我們提供的接口功能。因而為了讓外部函數(shù)或者文件調用我們提供的接口功能,就必須包含我們提供的這個接口描述文件----即頭文件。同時,我們自身模塊也需要包含這份模塊頭文件(因為其包含了模塊源文件中所需要的宏定義或者是結構體),好比我們平常所用的文件都是一式三份一樣,模塊本身也需要包含這個頭文件。 下面我們來定義這個頭文件,一般來說,頭文件的名字應該與源文件的名字保持一致,這樣我們便可以清晰的知道哪個頭文件是哪個源文件的描述。
于是便得到了LCD.C的頭文件LCD.h 其內容如下。
#ifndef _LCD_H_
#define _LCD_H_
extern LcdPutChar(char cNewValue) ;
#endif
這與我們在源文件中定義函數(shù)時有點類似。不同的是,在其前面添加了extern 修飾符表明其是一個外部函數(shù),可以被外部其它模塊進行調用。
#ifndef _LCD_H_
#define _LCD_H_
#endif
這個幾條條件編譯和宏定義是為了防止重復包含。假如有兩個不同源文件需要調用LcdPutChar(char cNewValue)這個函數(shù),他們分別都通過#include “Lcd.h”把這個頭文件包含了進去。在第一個源文件進行編譯時候,由于沒有定義過 _LCD_H_ 因此 #ifndef _LCD_H_ 條件成立,于是定義_LCD_H_ 并將下面的聲明包含進去。在第二個文件編譯時候,由于第一個文件包含時候,已經(jīng)將_LCD_H_定義過了。因此#ifndef _LCD_H_ 不成立,整個頭文件內容就沒有被包含。假設沒有這樣的條件編譯語句,那么兩個文件都包含了extern LcdPutChar(char cNewValue) ; 就會引起重復包含的錯誤。
不得不說的typedef
很多朋友似乎了習慣程序中利用如下語句來對數(shù)據(jù)類型進行定義
#define uint unsigned int
#define uchar unsigned char
然后在定義變量的時候 直接這樣使用
uint g_nTimeCounter = 0 ;
不可否認,這樣確實很方便,而且對于移植起來也有一定的方便性。但是考慮下面這種情況你還會 這么認為嗎?
#define PINT unsigned int * //定義unsigned int 指針類型
PINT g_npTimeCounter, g_npTimeState ;
那么你到底是定義了兩個unsigned int 型的指針變量,還是一個指針變量,一個整形變量呢?而你的初衷又是什么呢,想定義兩個unsigned int 型的指針變量嗎?如果是這樣,那么估計過不久就會到處抓狂找錯誤了。
慶幸的是C語言已經(jīng)為我們考慮到了這一點。typedef 正是為此而生。為了給變量起一個別名我們可以用如下的語句
typedef unsigned int uint16 ; //給指向無符號整形變量起一個別名 uint16
typedef unsigned int * puint16 ; //給指向無符號整形變量指針起一個別名 puint16
在我們定義變量時候便可以這樣定義了:
uint16 g_nTimeCounter = 0 ; //定義一個無符號的整形變量
puint16 g_npTimeCounter ; //定義一個無符號的整形變量的指針
在我們使用51單片機的C語言編程的時候,整形變量的范圍是16位,而在基于32的微處理下的整形變量是32位。倘若我們在8位單片機下編寫的一些代碼想要移植到32位的處理器上,那么很可能我們就需要在源文件中到處修改變量的類型定義。這是一件龐大的工作,為了考慮程序的可移植性,在一開始,我們就應該養(yǎng)成良好的習慣,用變量的別名進行定義。
如在8位單片機的平臺下,有如下一個變量定義
uint16 g_nTimeCounter = 0 ;
如果移植32單片機的平臺下,想要其的范圍依舊為16位。
可以直接修改uint16 的定義,即
typedef unsigned short int uint16 ;
這樣就可以了,而不需要到源文件處處尋找并修改。
將常用的數(shù)據(jù)類型全部采用此種方法定義,形成一個頭文件,便于我們以后編程直接調用。
文件名 MacroAndConst.h
其內容如下:
#ifndef _MACRO_AND_CONST_H_
#define _MACRO_AND_CONST_H_
typedef unsigned int uint16;
typedef unsigned int UINT;
typedef unsigned int uint;
typedef unsigned int UINT16;
typedef unsigned int WORD;
typedef unsigned int word;
typedef int int16;
typedef int INT16;
typedef unsigned long uint32;
typedef unsigned long UINT32;
typedef unsigned long DWORD;
typedef unsigned long dword;
typedef long int32;
typedef long INT32;
typedef signed char int8;
typedef signed char INT8;
typedef unsigned char byte;
typedef unsigned char BYTE;
typedef unsigned char uchar;
typedef unsigned char UINT8;
typedef unsigned char uint8;
typedef unsigned char BOOL;
#endif 至此,似乎我們對于源文件和頭文件的分工以及模塊化編程有那么一點概念了。那么讓我們趁熱打鐵,將上一章的我們編寫的LED閃爍函數(shù)進行模塊劃分并重新組織進行編譯。 在上一章中我們主要完成的功能是P0口所驅動的LED以1Hz的頻率閃爍。其中用到了定時器,以及LED驅動模塊。因而我們可以簡單的將整個工程分成三個模塊,定時器模塊,LED模塊,以及主函數(shù)
對應的文件關系如下 main.c
Timer.h?Timer.c --
Led.h?Led.c -- 在開始重新編寫我們的程序之前,先給大家講一下如何在KEIL中建立工程模板吧,這個模板是我一直沿用至今。希望能夠給大家一點啟發(fā)。 下面的內容就主要以圖片為主了。同時輔以少量文字說明。我們以芯片AT89S52為例。
OK,到此一個簡單的工程模板就建立起來了,以后我們再新建源文件和頭文件的時候,就可以直接保存到src文件目錄下面了。 下面我們開始編寫各個模塊文件。 首先編寫Timer.c 這個文件主要內容就是定時器初始化,以及定時器中斷服務函數(shù)。其內容如下。 #include <reg52.h>
bit g_bSystemTime1Ms = 0 ; // 1MS系統(tǒng)時標
void Timer0Init(void)
{
TMOD &= 0xf0 ;
TMOD |= 0x01 ; //定時器0工作方式1
TH0 = 0xfc ; //定時器初始值
TL0 = 0x66 ;
TR0 = 1 ;
ET0 = 1 ;
}
void Time0Isr(void) interrupt 1
{
TH0 = 0xfc ; //定時器重新賦初值
TL0 = 0x66 ;
g_bSystemTime1Ms = 1 ; //1MS時標標志位置位
} 由于在Led.c文件中需要調用我們的g_bSystemTime1Ms變量。同時主函數(shù)需要調用Timer0Init()初始化函數(shù),所以應該對這個變量和函數(shù)在頭文件里作外部聲明。以方便其它函數(shù)調用。 Timer.h 內容如下。
#ifndef _TIMER_H_
#define _TIMER_H_
extern void Timer0Init(void) ;
extern bit g_bSystemTime1Ms ;
#endif
完成了定時器模塊后,我們開始編寫LED驅動模塊。
Led.c 內容如下:
#include <reg52.h>
#include "MacroAndConst.h"
#include "Led.h"
#include "Timer.h"
static uint16 g_u16LedTimeCount = 0 ; //LED計數(shù)器
static uint8 g_u8LedState = 0 ; //LED狀態(tài)標志, 0表示亮,1表示熄滅
#define LED P0 //定義LED接口
#define LED_ON() LED = 0x00 ; //所有LED亮
#define LED_OFF() LED = 0xff ; //所有LED熄滅
void LedProcess(void)
{
if(0 == g_u8LedState) //如果LED的狀態(tài)為亮,則點亮LED
{
LED_ON() ;
}
else //否則熄滅LED
{
LED_OFF() ;
}
}
void LedStateChange(void)
{
if(g_bSystemTime1Ms) //系統(tǒng)1MS時標到
{
g_bSystemTime1Ms = 0 ;
g_u16LedTimeCount++ ; //LED計數(shù)器加一
if(g_u16LedTimeCount >= 500) //計數(shù)達到500,即500MS到了,改變LED的狀態(tài)。
{
g_u16LedTimeCount = 0 ;
g_u8LedState = ! g_u8LedState ;
}
}
}
這個模塊對外的借口只有兩個函數(shù),因此在相應的Led.h 中需要作相應的聲明。
Led.h 內容:
#ifndef _LED_H_
#define _LED_H_
extern void LedProcess(void) ;
extern void LedStateChange(void) ;
#endif
這兩個模塊完成后,我們將其C文件添加到工程中。然后開始編寫主函數(shù)里的代碼。
如下所示:
#include <reg52.h>
#include "MacroAndConst.h"
#include "Timer.h"
#include "Led.h"
sbit LED_SEG = P1^4; //數(shù)碼管段選
sbit LED_DIG = P1^5; //數(shù)碼管位選
sbit LED_CS11 = P1^6; //led控制位
void main(void)
{
LED_CS11 = 1 ; //74HC595輸出允許
LED_SEG = 0 ; //數(shù)碼管段選和位選禁止(因為它們和LED共用P0口)
LED_DIG = 0 ;
Timer0Init() ;
EA = 1 ;
while(1)
{
LedProcess() ;
LedStateChange() ;
}
} 至此,第二章到此結束。
一起來總結一下我們需要注意的地方吧
1. C語言源文件(*.c)的作用是什么
2. C語言頭文件(*.h)的作用是什么
3. typedef 的作用
4. 工程模板如何組織
5. 如何創(chuàng)建一個多模塊(多文件)的工程
第三章 按鍵程序編寫的基礎
從這一章開始,我們步入按鍵程序設計的殿堂。在基于單片機為核心構成的應用系統(tǒng)中,用戶輸入是必不可少的一部分。輸入可以分很多種情況,譬如有的系統(tǒng)支持PS2鍵盤的接口,有的系統(tǒng)輸入是基于編碼器,有的系統(tǒng)輸入是基于串口或者USB或者其它輸入通道等等。在各種輸入途徑中,更常見的是,基于單個按鍵或者由單個鍵盤按照一定排列構成的矩陣鍵盤(行列鍵盤)。我們這一篇章主要討論的對象就是基于單個按鍵的程序設計,以及矩陣鍵盤的程序編寫。 ◎按鍵檢測的原理 常見的獨立按鍵的外觀如下,相信大家并不陌生,各種常見的開發(fā)板學習板上隨處可以看到他們的身影。 總共有四個引腳,一般情況下,處于同一邊的兩個引腳內部是連接在一起的,如何分辨兩個引腳是否處在同一邊呢?可以將按鍵翻轉過來,處于同一邊的兩個引腳,有一條突起的線將他們連接一起,以標示它們倆是相連的。如果無法觀察得到,用數(shù)字萬用表的二極管擋位檢測一下即可。搞清楚這點非常重要,對于我們畫PCB的時候的封裝很有益。 它們和我們的單片機系統(tǒng)的I/O口連接一般如下: 對于單片機I/O內部有上拉電阻的微控制器而言,還可以省掉外部的那個上拉電阻。簡單分析一下按鍵檢測的原理。當按鍵沒有按下的時候,單片機I/O通過上拉電阻R接到VCC,我們在程序中讀取該I/O的電平的時候,其值為1(高電平); 當按鍵S按下的時候,該I/O被短接到GND,在程序中讀取該I/O的電平的時候,其值為0(低電平) 。這樣,按鍵的按下與否,就和與該按鍵相連的I/O的電平的變化相對應起來。結論:我們在程序中通過檢測到該I/O口電平的變化與否,即可以知道按鍵是否被按下,從而做出相應的響應。一切看起來很美好,是這樣的嗎? 現(xiàn)實并非理想 在我們通過上面的按鍵檢測原理得出上述的結論的時候,其實忽略了一個重要的問題,那就是現(xiàn)實中按鍵按下時候的電平變化狀態(tài)。我們的結論是基于理想的情況得出來的,就如同下面這幅按鍵按下時候對應電平變化的波形圖一樣: 而實際中,由于按鍵的彈片接觸的時候,并不是一接觸就緊緊的閉合,它還存在一定的抖動,盡管這個時間非常的短暫,但是對于我們執(zhí)行時間以us為計算單位的微控制器來說,它太漫長了。因而,實際的波形圖應該如下面這幅示意圖一樣。 這樣便存在這樣一個問題。假設我們的系統(tǒng)有這樣功能需求:在檢測到按鍵按下的時候,將某個I/O的狀態(tài)取反。由于這種抖動的存在,使得我們的微控制器誤以為是多次按鍵的按下,從而將某個I/O的狀態(tài)不斷取反,這并不是我們想要的效果,假如該I/O控制著系統(tǒng)中某個重要的執(zhí)行的部件,那結果更不是我們所期待的。于是乎有人便提出了軟件消除抖動的思想,道理很簡單:抖動的時間長度是一定的,只要我們避開這段抖動時期,檢測穩(wěn)定的時候的電平不久可以了嗎?聽起來確實不錯,而且實際應用起來效果也還可以。于是,各種各樣的書籍中,在提到按鍵檢測的時候,總也不忘說道軟件消抖。就像下面的偽代碼所描述的一樣。(假設按鍵按下時候,低電平有效)
If(0 == io_KeyEnter) //如果有鍵按下了
{
Delayms(20) ; //先延時20ms避開抖動時期
If(0 == io_KeyEnter) //然后再檢測,如果還是檢測到有鍵按下
{
return KeyValue ; //是真的按下了,返回鍵值
}
else
{
return KEY_NULL //是抖動,返回空的鍵值
}
while(0 == io_KeyEnter) ; //等待按鍵釋放
}
所以合理的分配好微控制的處理時間,是編寫按鍵程序的基礎。?乍看上去,確實挺不錯,實際中呢?在實際的系統(tǒng)中,一般是不允許這么樣做的。為什么呢?首先,這里的Delayms(20) , 讓微控制器在這里白白等待了20 ms 的時間,啥也沒干,考慮我在《學會釋放CPU》一章中所提及的幾點,這是不可取的。其次while(0 == io_KeyEnter) ;更是程序設計中的大忌(極少的特殊情況例外)。任何非極端情況下,都不要使用這樣語句來堵塞微控制器的執(zhí)行進程。原本是等待按鍵釋放,結果CPU就一直死死的盯住該按鍵,其它事情都不管了,那其它事情不干了嗎?你同意別人可不會同意 消除抖動有必要嗎?的確,軟件上的消抖確實可以保證按鍵的有效檢測。但是,這種消抖確實有必要嗎?有人提出了這樣的疑問。抖動是按鍵按下的過程中產生的,如果按鍵沒有按下,抖動會產生嗎?如果沒有按鍵按下,抖動也會在I/O上出現(xiàn),我會立刻把這個微控制器錘了,永遠不用這樣一款微控制器。所以抖動的出現(xiàn)即意味著按鍵已經(jīng)按下,盡管這個電平還沒有穩(wěn)定。所以只要我們檢測到按鍵按下,即可以返回鍵值,問題的關鍵是,在你執(zhí)行完其它任務的時候,再次執(zhí)行我們的按鍵任務的時候,抖動過程還沒有結束,這樣便有可能造成重復檢測。所以,如何在返回鍵值后,避免重復檢測,或者在按鍵一按下就執(zhí)行功能函數(shù),當功能函數(shù)的執(zhí)行時間小于抖動時間時候,如何避免再次執(zhí)行功能函數(shù),就成為我們要考慮的問題了。這是一個仁者見仁,智者見智的問題,就留給大家去思考吧。所以消除抖動的目的是:防止按鍵一次按下,多次響應。
第四章 基于狀態(tài)轉移的獨立按鍵程序設計
有一個小液晶屏,還有四個按鍵,功能是時鐘,鬧鐘以及秒表。在調整時間的時候,短按+鍵每次調整值加一,長按的時候調整值連續(xù)增加。小的時候很好奇,這樣的功能到底是如何實現(xiàn)的呢,今天就讓我們來剖析它的原理吧。
本章所描述的按鍵程序要達到的目的:檢測按鍵按下,短按,長按,釋放。即通過按鍵的返回值我們可以獲取到如下的信息:按鍵按下(短按),按鍵長按,按鍵連_發(fā),按鍵釋放。不知道大家還記得小時候玩過的電子鐘沒有,就是外形類似于CALL 機(CALL 機,好像是很古老的東西了。 狀態(tài)在生活中隨處可見。譬如早上的時候,鬧鐘把你叫醒了,這個時候,你便處于清醒的狀態(tài),馬上你就穿衣起床洗漱吃早餐,這一系列事情就是你在這個狀態(tài)做的事情。做完這些后你會去等車或者開車去上班,這個時候你就處在上班途中的狀態(tài)…..中午下班時間到了,你就處于中午下班的狀態(tài),諸如此類等等,在每一個狀態(tài)我們都會做一些不同的事情,而總會有外界條件促使我們轉換到另外一種狀態(tài),譬如鬧鐘叫醒我們了,下班時間到了等等。對于狀態(tài)的定義出發(fā)點不同,考慮的方向不同,或者會有些許細節(jié)上面的差異,但是大的狀態(tài)總是相同的。生活中的事物同樣遵循同樣的規(guī)律,譬如,用一個智能充電器給你的手機電池充電,剛開始,它是處于快速充電狀態(tài),隨著電量的增加,電壓的升高,當達到規(guī)定的電壓時候,它會轉換到恒壓充電。總而言之,細心觀察,你會發(fā)現(xiàn)生活中的總總都可以歸結為一個個的狀態(tài),而狀態(tài)的變換或者轉移總是由某些條件引起同時伴隨著一些動作的發(fā)生。我們的按鍵亦遵循同樣的規(guī)律,下面讓我們來簡單的描繪一下它的狀態(tài)流程轉移圖。 
下面對上面的流程圖進行簡要的分析。 首先按鍵程序進入初始狀態(tài)S1,在這個狀態(tài)下,檢測按鍵是否按下,如果有按下,則進入按鍵消抖狀態(tài)2,在下一次執(zhí)行按鍵程序時候,直接由按鍵消抖狀態(tài)進入按鍵按下狀態(tài)3,在此狀態(tài)下檢測按鍵是否按下,如果沒有按鍵按下,則返回初始狀態(tài)S1,如果有則可以返回鍵值,同時進入長按狀態(tài)S4,在長按狀態(tài)下每次進入按鍵程序時候對按鍵時間計數(shù),當計數(shù)值超過設定閾值時候,則表明長按事件發(fā)生,同時進入按鍵連_發(fā)狀態(tài)S5。如果按鍵鍵值為空鍵,則返回按鍵釋放狀態(tài)S6,否則繼續(xù)停留在本狀態(tài)。在按鍵連_發(fā)狀態(tài)下,如果按鍵鍵值為空鍵則返回按鍵釋放狀態(tài)S6,如果按鍵時間計數(shù)超過連_發(fā)閾值,則返回連_發(fā)按鍵值,清零時間計數(shù)后繼續(xù)停留在本狀態(tài)。 看了這么多,也許你已經(jīng)有一個模糊的概念了,下面讓我們趁熱打鐵,一起來動手編寫按鍵驅動程序吧。 下面是我使用的硬件的連接圖。
硬件連接很簡單,四個獨立按鍵分別接在P3^0------P3^3四個I/O上面。 因為51單片機I/O口內部結構的限制,在讀取外部引腳狀態(tài)的時候,需要向端口寫1.在51單片機復位后,不需要進行此操作也可以進行讀取外部引腳的操作。因此,在按鍵的端口沒有復用的情況下,可以省略此步驟。而對于其它一些真正雙向I/O口的單片機來說,將引腳設置成輸入狀態(tài),是必不可少的一個步驟。 下面的程序代碼初始化引腳為輸入。
void KeyInit(void)
{
io_key_1 = 1 ;
io_key_2 = 1 ;
io_key_3 = 1 ;
io_key_4 = 1 ;
}
根據(jù)按鍵硬件連接定義按鍵鍵值
#define KEY_VALUE_1 0x0e
#define KEY_VALUE_2 0x0d
#define KEY_VALUE_3 0x0b
#define KEY_VALUE_4 0x07
#define KEY_NULL 0x0f
下面我們來編寫按鍵的硬件驅動程序。
根據(jù)第一章所描述的按鍵檢測原理,我們可以很容易的得出如下的代碼:
static uint8 KeyScan(void)
{
if(io_key_1 == 0)return KEY_VALUE_1 ;
if(io_key_2 == 0)return KEY_VALUE_2 ;
if(io_key_3 == 0)return KEY_VALUE_3 ;
if(io_key_4 == 0)return KEY_VALUE_4 ;
return KEY_NULL ;
}
其中io_key_1等是我們按鍵端口的定義,如下所示:
sbit io_key_1 = P3^0 ;
sbit io_key_2 = P3^1 ;
sbit io_key_3 = P3^2 ;
sbit io_key_4 = P3^3 ;
KeyScan()作為底層按鍵的驅動程序,為上層按鍵掃描提供一個接口,這樣我們編寫的上層按鍵掃描函數(shù)可以幾乎不用修改就可以拿到我們的其它程序中去使用,使得程序復用性大大提高。同時,通過有意識的將與底層硬件連接緊密的程序和與硬件無關的代碼分開寫,使得程序結構層次清晰,可移植性也更好。對于單片機類的程序而言,能夠做到函數(shù)級別的代碼重用已經(jīng)足夠了。 在編寫我們的上層按鍵掃描函數(shù)之前,需要先完成一些宏定義。
//定義長按鍵的TICK數(shù),以及連_發(fā)間隔的TICK數(shù)
#define KEY_LONG_PERIOD 100
#define KEY_CONTINUE_PERIOD 25
//定義按鍵返回值狀態(tài)(按下,長按,連_發(fā),釋放)
#define KEY_DOWN 0x80
#define KEY_LONG 0x40
#define KEY_CONTINUE 0x20
#define KEY_UP 0x10
//定義按鍵狀態(tài)
#define KEY_STATE_INIT 0
#define KEY_STATE_WOBBLE 1
#define KEY_STATE_PRESS 2
#define KEY_STATE_LONG 3
#define KEY_STATE_CONTINUE 4
#define KEY_STATE_RELEASE 5
接著我們開始編寫完整的上層按鍵掃描函數(shù),按鍵的短按,長按,連按,釋放等等狀態(tài)的判斷均是在此函數(shù)中完成。對照狀態(tài)流程轉移圖,然后再看下面的函數(shù)代碼,可以更容易的去理解函數(shù)的執(zhí)行流程。完整的函數(shù)代碼如下:
void GetKey(uint8 *pKeyValue)
{
static uint8 s_u8KeyState = KEY_STATE_INIT ;
static uint8 s_u8KeyTimeCount = 0 ;
static uint8 s_u8LastKey = KEY_NULL ; //保存按鍵釋放時候的鍵值
uint8 KeyTemp = KEY_NULL ;
KeyTemp = KeyScan() ; //獲取鍵值
switch(s_u8KeyState)
{
case KEY_STATE_INIT :
{
if(KEY_NULL != (KeyTemp))
{
s_u8KeyState = KEY_STATE_WOBBLE ;
}
}
break ;
case KEY_STATE_WOBBLE : //消抖
{
s_u8KeyState = KEY_STATE_PRESS ;
}
break ;
case KEY_STATE_PRESS :
{
if(KEY_NULL != (KeyTemp))
{
s_u8LastKey = KeyTemp ; //保存鍵值,以便在釋放按鍵狀態(tài)返回鍵值
KeyTemp |= KEY_DOWN ; //按鍵按下
s_u8KeyState = KEY_STATE_LONG ;
}
else
{
s_u8KeyState = KEY_STATE_INIT ;
}
}
break ;
case KEY_STATE_LONG :
{
if(KEY_NULL != (KeyTemp))
{
if(++s_u8KeyTimeCount > KEY_LONG_PERIOD)
{
s_u8KeyTimeCount = 0 ;
KeyTemp |= KEY_LONG ; //長按鍵事件發(fā)生
s_u8KeyState = KEY_STATE_CONTINUE ;
}
}
else
{
s_u8KeyState = KEY_STATE_RELEASE ;
}
}
break ;
case KEY_STATE_CONTINUE :
{
if(KEY_NULL != (KeyTemp))
{
if(++s_u8KeyTimeCount > KEY_CONTINUE_PERIOD)
{
s_u8KeyTimeCount = 0 ;
KeyTemp |= KEY_CONTINUE ;
}
}
else
{
s_u8KeyState = KEY_STATE_RELEASE ;
}
}
break ;
case KEY_STATE_RELEASE :
{
s_u8LastKey |= KEY_UP ;
KeyTemp = s_u8LastKey ;
s_u8KeyState = KEY_STATE_INIT ;
}
break ;
default : break ;
}
*pKeyValue = KeyTemp ; //返回鍵值
}
關于這個函數(shù)內部的細節(jié)我并不打算花過多筆墨去講解。對照著按鍵狀態(tài)流程轉移圖,然后去看程序代碼,你會發(fā)現(xiàn)其實思路非常清晰。最能讓人理解透徹的,莫非就是將整個程序自己看懂,然后想象為什么這個地方要這樣寫,抱著思考的態(tài)度去閱讀程序,你會發(fā)現(xiàn)自己的程序水平會慢慢的提高。所以我更希望的是你能夠認認真真的看完,然后思考。也許你會收獲更多。 不管怎么樣,這樣的一個程序已經(jīng)完成了本章開始時候要求的功能:按下,長按,連按,釋放。事實上,如果掌握了這種基于狀態(tài)轉移的思想,你會發(fā)現(xiàn)要求實現(xiàn)其它按鍵功能,譬如,多鍵按下,功能鍵等等,亦相當簡單,在下一章,我們就去實現(xiàn)它。 在主程序中我編寫了這樣的一段代碼,來演示我實現(xiàn)的按鍵功能。
void main(void)
{
uint8 KeyValue = KEY_NULL;
uint8 temp = 0 ;
LED_CS11 = 1 ; //流水燈輸出允許
LED_SEG = 0 ;
LED_DIG = 0 ;
Timer0Init() ;
KeyInit() ;
EA = 1 ;
while(1)
{
Timer0MainLoop() ;
KeyMainLoop(&KeyValue) ;
if(KeyValue == (KEY_VALUE_1 | KEY_DOWN)) P0 = ~1 ;
if(KeyValue == (KEY_VALUE_1 | KEY_LONG)) P0 = ~2 ;
if(KeyValue == (KEY_VALUE_1 | KEY_CONTINUE)) { P0 ^= 0xf0;}
if(KeyValue == (KEY_VALUE_1 | KEY_UP)) P0 = 0xa5 ;
}
}
按住第一個鍵,可以清晰的看到P0口所接的LED的狀態(tài)的變化。當按鍵按下時候,第一個LED燈亮,等待2 S后第二個LED亮,第一個熄滅,表示長按事件發(fā)生。再過500 ms 第5~8個LED閃爍,表示連按事件發(fā)生。當釋放按鍵時候,P0口所接的LED的狀態(tài)為:
滅亮滅亮亮滅亮滅,這也正是P0 = 0xa5這條語句的功能。
第五章--多任務環(huán)境下的數(shù)碼管編程設計 數(shù)碼管在實際應用中非常廣泛,尤其是在某些對成本有限制的場合。編寫一個好用的LED程序并不是那么的簡單。曾經(jīng)有人這樣說過,如果用數(shù)碼管和按鍵,做一個簡易的可以調整的時鐘出來,那么你的單片機就算入門了60%了。此話我深信不疑。我遇到過很多單片機的愛好者,他們問我說單片機我已經(jīng)掌握了,該如何進一步的學習下去呢?我并不急于回答他們的問題,而是問他們:會編寫數(shù)碼管的驅動程序了吧?“嗯”。會編寫按鍵程序了吧?“嗯”。好,我給你出一個小題目,你做一下。用按鍵和數(shù)碼管以及單片機定時器實現(xiàn)一個簡易的可以調整的時鐘,要求如下: 8位數(shù)碼管顯示,顯示格式如下 時-分-秒 XX-XX-XX 要求:系統(tǒng)有四個按鍵,功能分別是 調整,加,減,確定。在按下調整鍵時候,顯示時的兩位數(shù)碼管以1 Hz 頻率閃爍。如果再次按下調整鍵,則分開始閃爍,時恢復正常顯示,依次循環(huán),直到按下確定鍵,恢復正常的顯示。在數(shù)碼管閃爍的時候,按下加或者減鍵可以調整相應的顯示內容。按鍵支持短按,和長按,即短按時,修改的內容每次增加一或者減小一,長按時候以一定速率連續(xù)增加或者減少。 結果很多人,很多愛好者一下子都理不清楚思路。其實問題的根源在于沒有以工程化的角度去思考程序的編寫。很多人在學習數(shù)碼管編程的時候,都是照著書上或者網(wǎng)上的例子來進行試驗。殊不知,這些例子代碼僅僅只是具有一個演示性的作用,拿到實際中是很難用的。舉一個簡單的例子。 下面這段程序是在網(wǎng)上隨便搜索到的:
while(1)
{
for(num=0;num<9;num++)
{
P0=table[num];
P2=code[num] ;
delayms(2) ;
}
} 看出什么問題來了沒有,如果沒有看出來請仔細想一下,如果還沒有想出來,請回過頭去,認真再看一遍“學會釋放CPU”這一章的內容。這個程序作為演示程序是沒有什么問題的,但是實際應用的時候,數(shù)碼管顯示的內容經(jīng)常變化,而且還有很多其它任務需要執(zhí)行,因此這樣的程序在實際中是根本就無法用的,更何況,它這里也調用了delayms(2)這個函數(shù)來延時2 ms這更是令我們深惡痛絕 。 本章的內容正是探討如何解決多任務環(huán)境下(不帶OS)的數(shù)碼管程序設計的編寫問題。理解了其中的思想,無論要求我們顯示的形式怎么變化(如數(shù)碼管閃爍,移位等),我們都可以很方便的解決問題。 數(shù)碼管的顯示分為動態(tài)顯示和靜態(tài)顯示兩種。靜態(tài)顯示是每一位數(shù)碼管都用一片獨立的驅動芯片進行驅動。比較常見的有74LS164,74HC595等。利用這類芯片的好處就是可以級聯(lián),留給單片機的接口只需要時鐘線,數(shù)據(jù)線,因此比較節(jié)省I/O口。如下圖所示: 利用74LS164級聯(lián)驅動8個單獨的數(shù)碼管 靜態(tài)顯示的優(yōu)點是程序編寫簡單。但是由于涉及到的驅動芯片數(shù)量比較多,同時考慮到PCB的布線等等因素,在低成本要求的開發(fā)環(huán)境下,單純的靜態(tài)驅動并不合適。這個時候就可以考慮到動態(tài)驅動了。動態(tài)驅動的圖如下所示(以EE21開發(fā)板為例) 由上圖可以看出。8個數(shù)碼管的段碼由一個單獨的74HC573驅動。同時每一個數(shù)碼管的公共端連接在另外一個74HC573的輸出上。當送出第一位數(shù)碼管的段碼內容時候,同時選通第一位數(shù)碼管的位選,此時,第一位數(shù)碼管就顯示出相應的內容了。一段時間之后,送出第二位數(shù)碼管段碼的內容,選通第二位數(shù)碼管的位選,這時顯示的內容就變成第二位數(shù)碼管的內容了……依次循環(huán)下去,就可以看到了所有數(shù)碼管同時顯示了。事實上,任意時刻,只有一位數(shù)碼管是被點亮的。由于人眼的視覺暫留效應以及數(shù)碼管的余輝效應,當數(shù)碼管掃描的頻率非常快的時候,人眼已經(jīng)無法分辨出數(shù)碼管的變化了,看起來就是同時點亮的。我們假設數(shù)碼管的掃描頻率為50 Hz, 則完成一輪掃描的時間就是1 / 50 = 20 ms 。我們的系統(tǒng)共有8位數(shù)碼管,則每一位數(shù)碼管在一輪掃描周期中點亮的時間為20 / 8 = 2.5 ms 。動態(tài)掃描對時間要求有一點點嚴格,否則,就會有明顯的閃爍。 假設我們程序 中所有任務如下:
while(1)
{
LedDisplay() ; //數(shù)碼管動態(tài)掃描
ADProcess() ; //AD采集處理
TimerProcess() ; //時間相關處理
DataProcess() ; //數(shù)據(jù)處理
}
LedDisplay() 這個任務的執(zhí)行時間,如同我們剛才計算的那樣,50 Hz頻率掃描,則該函數(shù)執(zhí)行的時間為20 ms 。 假設ADProcess()這個任務執(zhí)行的的時間為2 ms ,TimerProcess()這個函數(shù)執(zhí)行的時間為 1 ms ,DataProcess() 這個函數(shù)執(zhí)行的時間為10 ms 。 那么整個主函數(shù)執(zhí)行一遍的總時間為 20 + 2 + 1 + 10 = 33 ms 。即LedDisplay() 這個函數(shù)的掃描頻率已經(jīng)不為50 Hz 了,而是 1 / 33 = 30.3 Hz 。這個頻率數(shù)碼管已經(jīng)可以感覺到閃爍了,因此不符合我們的要求。為什么會出現(xiàn)這種情況呢? 我們剛才計算的50 Hz 是系統(tǒng)只有LedDisplay()這一個任務的時候得出來的結果。當系統(tǒng)添加了其它任務后,當然系統(tǒng)循環(huán)執(zhí)行一次的總時間就增加了。如何解決這種現(xiàn)象了,還是離不開我們第二章所講的那個思想。 系統(tǒng)產生一個2.5 ms 的時標消息。LedDisplay() , 每次接收到這個消息的時候, 掃描一位數(shù)碼管。這樣8個時標消息過后,所有的數(shù)碼管就都被掃描一遍了。可能有朋友會有這樣的疑問:ADProcess() 以及 DataProcess() 等函數(shù)執(zhí)行的時間還是需要十幾ms 啊,在這十幾ms 的時間里,已經(jīng)產生好幾個2.5 ms的時標消息了,這樣豈不是漏掉了掃描,顯示起來還是會閃爍。能夠想到這一點,很不錯,這也就是為什么我們要學會釋放CPU的原因。對于ADProcess(),TimerProcess(),DataProcess(),等任務我們依舊要采取此方法對CPU進行釋放,使其執(zhí)行的時間盡可能短暫,關于如何做到這一點,在以后的講解如何設計多任務程序設計的時候會講解到。 下面我們基于此思路開始編寫具體的程序。 首先編寫Timer.c文件。該文件中主要為系統(tǒng)提供時間相關的服務。必要的頭文件包含。 #include <reg52.h> #include "MacroAndConst.h" 為了方便計算,我們取數(shù)碼管掃描一位的時間為2 ms。設置定時器0為2 ms中斷一次。同時聲明一個位變量,作為2 ms時標消息的標志。 bit g_bSystemTime2Ms = 0 ; // 2msLED動態(tài)掃描時標消息
初始化定時器0
void Timer0Init(void)
{
TMOD &= 0xf0 ;
TMOD |= 0x01 ; //定時器0工作方式1
TH0 = 0xf8 ; //定時器初始值
TL0 = 0xcc ;
TR0 = 1 ;
ET0 = 1 ;
}
在定時器0中斷處理程序中,設置時標消息。
void Time0Isr(void) interrupt 1
{
TH0 = 0xf8 ; //定時器重新賦初值
TL0 = 0xcc ;
g_bSystemTime2Ms = 1 ; //2MS時標標志位置位
} 然后我們開始編寫數(shù)碼管的動態(tài)掃描函數(shù)。 新建一個C源文件,并包含相應的頭文件。 #include <reg52.h>
#include "MacroAndConst.h"
#include "Timer.h" 先開辟一個數(shù)碼管顯示的緩沖區(qū)。動態(tài)掃描函數(shù)負責從這個緩沖區(qū)中取出數(shù)據(jù),并掃描顯示。而其它函數(shù)則可以修改該緩沖區(qū),從而改變顯示的內容。
uint8 g_u8LedDisplayBuffer[8] = {0} ; //顯示緩沖區(qū)
然后定義共陽數(shù)碼管的段碼表以及相應的硬件端口連接。
code uint8 g_u8LedDisplayCode[]=
{
0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E,
0xbf, //'-'號代碼
} ;
sbit io_led_seg_cs = P1^4 ;
sbit io_led_bit_cs = P1^5 ;
#define LED_PORT P0
再分別編寫送數(shù)碼管段碼函數(shù),以及位選通函數(shù)。
static void SendLedSegData(uint8 dat)
{
LED_PORT = dat ;
io_led_seg_cs = 1 ; //開段碼鎖存,送段碼數(shù)據(jù)
io_led_seg_cs = 0 ;
}
static void SendLedBitData(uint8 dat)
{
uint8 temp ;
temp = (0x01 << dat ) ; //根據(jù)要選通的位計算出位碼
LED_PORT = temp ;
io_led_bit_cs = 1 ; //開位碼鎖存,送位碼數(shù)據(jù)
io_led_bit_cs = 0 ;
}
下面的核心就是如何編寫動態(tài)掃描函數(shù)了。
如下所示:
void LedDisplay(uint8 * pBuffer)
{
static uint8 s_LedDisPos = 0 ;
if(g_bSystemTime2Ms)
{
g_bSystemTime2Ms = 0 ;
SendLedBitData(8) ; //消隱,只需要設置位選不為0~7即可
if(pBuffer[s_LedDisPos] == '-') //顯示'-'號
{
SendLedSegData(g_u8LedDisplayCode[16]) ;
}
else
{
SendLedSegData(g_u8LedDisplayCode[pBuffer[s_LedDisPos]]) ;
}
SendLedBitData(s_LedDisPos);
if(++s_LedDisPos > 7)
{
s_LedDisPos = 0 ;
}
}
}
函數(shù)內部定義一個靜態(tài)的變量s_LedDisPos,用來表示掃描數(shù)碼管的位置。每當我們執(zhí)行該函數(shù)一次的時候,s_LedDisPos的值會自加1,表示下次掃描下一個數(shù)碼管。然后判斷g_bSystemTime2Ms時標消息是否到了。如果到了,就開始執(zhí)行相關掃描,否則就直接跳出函數(shù)。SendLedBitData(8) ;的作用是消隱。因為我們的系統(tǒng)的段選和位選是共用P0口的。在送段碼之前,必須先關掉位選,否則,因為上次位選是選通的,在送段碼的時候會造成相應數(shù)碼管的點亮,盡管這個時間很短暫。但是因為我們的數(shù)碼管是不斷掃描的,所以看起來還是會有些微微亮。為了消除這種影響,就有必要再送段碼數(shù)據(jù)之前關掉位選。 if(pBuffer[s_LedDisPos] == '-') //顯示'-'號這行語句是為了顯示’-’符號特意加上去的,大家可以看到在定義數(shù)碼管的段碼表的時候,我多加了一個字節(jié)的代碼0xbf: code uint8 g_u8LedDisplayCode[]=
{
0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E,
0xbf, //'-'號代碼
} ; 通過SendLedSegData(g_u8LedDisplayCode[pBuffer[s_LedDisPos]]) ;送出相應的段碼數(shù)據(jù)后,然后通過SendLedBitData(s_LedDisPos);打開相應的位選。這樣對應的數(shù)碼管就被點亮了。
if(++s_LedDisPos > 7)
{
s_LedDisPos = 0 ;
} 然后s_LedDisPos自加1,以便下次執(zhí)行本函數(shù)時,掃描下一個數(shù)碼管。因為我們的系統(tǒng)共有8個數(shù)碼管,所以當s_LedDisPos > 7后,要對其進行清0 。否則,沒有任何一個數(shù)碼管被選中。這也是為什么我們可以用。 SendLedBitData(8) ; //消隱,只需要設置位選不為0~7即可
對數(shù)碼管進行消隱操作的原因。 下面我們來編寫相應的主函數(shù),并實現(xiàn)數(shù)碼管上面類似時鐘的效果,如顯示10-20-30 ,即10點20分30秒。 Main.c
#include <reg52.h>
#include "MacroAndConst.h"
#include "Timer.h"
#include "Led7Seg.h"
sbit io_led = P1^6 ;
void main(void)
{
io_led = 0 ; //發(fā)光二極管與數(shù)碼管共用P0口,這里禁止掉發(fā)光二極管的鎖存輸出
Timer0Init() ;
g_u8LedDisplayBuffer[0] = 1 ;
g_u8LedDisplayBuffer[1] = 0 ;
g_u8LedDisplayBuffer[2] = '-' ;
g_u8LedDisplayBuffer[3] = 2 ;
g_u8LedDisplayBuffer[4] = 0 ;
g_u8LedDisplayBuffer[5] = '-' ;
g_u8LedDisplayBuffer[6] = 3 ;
g_u8LedDisplayBuffer[7] = 0 ;
EA = 1 ;
while(1)
{
LedDisplay(g_u8LedDisplayBuffer) ;
}
} 將整個工程進行編譯,看看效果如何 動起來,既然我們想要模擬一個時鐘,那么時鐘肯定是要走動的,不然還稱為什么時鐘撒。下面我們在前面的基礎之上,添加一點相應的代碼,讓我們這個時鐘走動起來。 我們知道,之前我們以及設置了一個掃描數(shù)碼管用到的2 ms時標。 如果我們再對這個時標進行計數(shù),當計數(shù)值達到500,即500 * 2 = 1000 ms 時候,即表示已經(jīng)逝去了1 S的時間。我們再根據(jù)這個1 S的時間更新顯示緩沖區(qū)即可。聽起來很簡單,讓我們實現(xiàn)它吧。 首先在Timer.c中聲明如下兩個變量:
bit g_bTime1S = 0 ; //時鐘1S時標消息
static uint16 s_u16ClockTickCount = 0 ; //對2 ms 時標進行計數(shù)
再在定時器中斷函數(shù)中添加如下代碼:
if(++s_u16ClockTickCount == 500)
{
s_u16ClockTickCount = 0 ;
g_bTime1S = 1 ;
} 從上面可以看出,s_u16ClockTickCount計數(shù)值達到500的時候,g_bTime1S時標消息產生。然后我們根據(jù)這個時標消息刷新數(shù)碼管顯示緩沖區(qū): void RunClock(void)
{
if(g_bTime1S )
{
g_bTime1S = 0 ;
if(++g_u8LedDisplayBuffer[7] == 10)
{
g_u8LedDisplayBuffer[7] = 0 ;
if(++g_u8LedDisplayBuffer[6] == 6)
{
g_u8LedDisplayBuffer[6] = 0 ;
if(++g_u8LedDisplayBuffer[4] == 10)
{
g_u8LedDisplayBuffer[4] = 0 ;
if(++g_u8LedDisplayBuffer[3] == 6)
{
g_u8LedDisplayBuffer[3] = 0 ;
if( g_u8LedDisplayBuffer[0]<2)
{
if(++g_u8LedDisplayBuffer[1]==10)
{
g_u8LedDisplayBuffer[1] = 0 ;
g_u8LedDisplayBuffer[0]++;
}
}
else
{
if(++g_u8LedDisplayBuffer[1]==4)
{
g_u8LedDisplayBuffer[1] = 0 ;
g_u8LedDisplayBuffer[0] = 0 ;
}
}
}
}
}
}
}
} 這個函數(shù)的作用就是對每個數(shù)碼管緩沖位的值進行判斷,判斷的標準就是我們熟知的24小時制。如秒的個位到了10 就清0,同時秒的十位加1….諸如此類,我就不一一詳述了。同時,我們再編寫一個時鐘初始值設置函數(shù),這樣,可以很方便的在主程序開始的時候修改時鐘初始值。 void SetClock(uint8 nHour, uint8 nMinute, uint8 nSecond)
{
g_u8LedDisplayBuffer[0] = nHour / 10 ;
g_u8LedDisplayBuffer[1] = nHour % 10 ;
g_u8LedDisplayBuffer[2] = '-' ;
g_u8LedDisplayBuffer[3] = nMinute / 10 ;
g_u8LedDisplayBuffer[4] = nMinute % 10 ;
g_u8LedDisplayBuffer[5] = '-' ;
g_u8LedDisplayBuffer[6] = nSecond / 10 ;
g_u8LedDisplayBuffer[7] = nSecond % 10 ;
} 然后修改下我們的主函數(shù)如下:
void main(void)
{
io_led = 0 ; //發(fā)光二極管與數(shù)碼管共用P0口,這里禁止掉發(fā)光二極管的鎖存輸出
Timer0Init() ;
SetClock(10,20,30) ; //設置初始時間為10點20分30秒
EA = 1 ;
while(1)
{
LedDisplay(g_u8LedDisplayBuffer) ;
RunClock();
}
} 編譯好之后,下載到我們的實驗板上,怎么樣,一個簡單的時鐘就這樣誕生了。 至此,本章所訴就告一段落了。至于如何完成數(shù)碼管的閃爍顯示,就像本章開頭所說的那個數(shù)碼管時鐘的功能,就作為一個思考的問題留給大家思考吧。 同時整個LED篇就到此結束了,在以后的文章中,我們將開始學習如何編寫實用的按鍵掃描程序。
第六章----漸明漸暗的燈 看著學習板上的LED按照我們的意愿開始閃爍起來,你心里是否高興了,我相信你會的。但是很快你就會感覺到太單調,總是同一個頻率在閃爍,總是同一個亮度在閃爍。如果要是能夠由暗逐漸變亮,然后再由亮變暗該多漂亮啊。嗯,想法不錯,可以該從什么地方入手呢。 在開始我們的工程之前,首先來了解一個概念:PWM。 PWM(Pulse Width Modulation)是脈沖寬度調制的英文單詞的縮寫。下面這段話是通信百科中對其的定義: 脈沖寬度調制(PWM)是利用微處理器的數(shù)字輸出來對模擬電路進行控制的一種非常有效的技術,廣泛應用在從測量、通信到功率控制與變換的許多領域中。脈寬調制是開關型穩(wěn)壓電源中的術語。這是按穩(wěn)壓的控制方式分類的,除了PWM型,還有PFM型和PWM、PFM混合型。脈寬調制式開關型穩(wěn)壓電路是在控制電路輸出頻率不變的情況下,通過電壓反饋調整其占空比,從而達到穩(wěn)定輸出電壓的目的。 讀起來有點晦澀難懂。其實簡單的說來,PWM技術就是通過調整一個周期固定的方波的占空比,來調節(jié)輸出電壓的平均當電壓,電流或者功率等被控量。我們可以用一個水龍頭來類比,把1S時間分成50等份,即每一個等份20MS。在這20MS時間里如果我們把水龍頭水閥一直打開,那么在這20MS里流過的水肯定是最多的,如果我們把水閥打開15MS,剩下的5MS關閉水閥,那么流出的水相比剛才20MS全開肯定要小的多。同樣的道理,我們可以通過控制20MS時間里水閥開啟的時間的長短來控制流過的水的多少。那么在1S內平均流出的水流量也就可以被控制了。 當我們調整PWM的占空比時,就會引起電壓或者電流的改變,LED的明暗狀態(tài)就會隨之發(fā)生相應的變化,聽起來好像可以通過這種方法來實現(xiàn)我們想要的漸明漸暗的效果。讓我們來試一下吧。 大家都知道人眼有一個臨界頻率,當LED的閃爍頻率達到一定的時候,人眼就分辨不出LED是否在閃爍了。就像我們平常看電視一樣,看起來畫面是連續(xù)的,實質不是這個樣子,所有連續(xù)動作都是一幀幀靜止的畫面在1S的時間里快速播放出來,譬如每秒24幀的速度播放,由于人眼的視覺暫留效應,看起來畫面就是連續(xù)的了。同樣的道理,為了讓我們的LED在變化的過程中,我們感覺不到其在閃爍,可以將其閃爍的頻率定在50Hz以上。同時為了看起來明暗過渡的效果更加明顯,我們在這里定義其變化范圍為0~99(100等分).即最亮的時候其灰度等級為99,為0的時候最暗,也就是熄滅了。 于是乎我們定義PWM的占空比上限為99, 下限定義為0
#define LED_PWM_LIMIT_MAX 99
#define LED_PWM_LIMIT_MIN 0
假定我們LED的閃爍頻率為50HZ,而亮度變化的范圍為0~99共100等分。則每一等分所占用的時間為 1/(50*100) = 200us 即我們在改變LED的亮滅狀態(tài)時,應該是在200us整數(shù)倍時刻時。在這里我們用單片機的定時器產生200us的中斷,同時每20MS調整一次LED的占空比。這樣在20MS * 100 = 2S的時間內LED可以從暗逐漸變亮,在下一個2S內可以從亮逐漸變暗,然后不斷循環(huán)。 由于大部分的內容都可以在中斷中完成,因此,我們的大部分代碼都在Timer.c這個文件中編寫,主函數(shù)中除了初始化之外,就是一個空的死循環(huán)。
Timer.c內容如下。
#include <reg52.h>
#include "MacroAndConst.h"
#define LED P0 //定義LED接口
#define LED_ON() LED = 0x00 ; //所有LED亮
#define LED_OFF() LED = 0xff ; //所有LED熄滅
#define LED_PWM_LIMIT_MAX 99
#define LED_PWM_LIMIT_MIN 0
static uint8 s_u8TimeCounter = 0 ; //中斷計數(shù)
static uint8 s_u8LedDirection = 0 ; //LED方向控制 0 :漸亮 1 :漸滅
static int8 s_s8LedPWMCounter = 0 ; //LED占空比
void Timer0Init(void)
{
TMOD &= 0xf0 ;
TMOD |= 0x01 ; //定時器0工作方式1
TH0 = 0xff ; //定時器初始值(200us中斷一次)
TL0 = 0x47 ;
TR0 = 1 ;
ET0 = 1 ;
}
void Time0Isr(void) interrupt 1
{
static int8 s_s8PWMCounter = 0 ;
TH0 = 0xff ; //定時器重新賦初值
TL0 = 0x47 ;
if(++s_u8TimeCounter >= 100) //每20MS調整一下LED的占空比
{
s_u8TimeCounter = 0 ;
//如果是漸亮方向變化,則占空比遞增
if((s_s8LedPWMCounter <= LED_PWM_LIMIT_MAX) &&(0 == s_u8LedDirection))
{
s_s8LedPWMCounter++ ;
if(s_s8LedPWMCounter > LED_PWM_LIMIT_MAX)
{
s_u8LedDirection = 1 ;
s_s8LedPWMCounter = LED_PWM_LIMIT_MAX ;
}
}
//如果是漸暗方向變化,則占空比遞漸
if((s_s8LedPWMCounter >= LED_PWM_LIMIT_MIN) &&(1 == s_u8LedDirection))
{
s_s8LedPWMCounter-- ;
if(s_s8LedPWMCounter < LED_PWM_LIMIT_MIN)
{
s_u8LedDirection = 0 ;
s_s8LedPWMCounter = LED_PWM_LIMIT_MIN ;
}
}
s_s8PWMCounter = s_s8LedPWMCounter ; //獲取LED的占空比
}
if(s_s8PWMCounter > 0) //占空比大于0,則點亮LED,否則熄滅LED
{
LED_ON() ;
s_s8PWMCounter-- ;
}
else
{
LED_OFF();
}
}
其實PWM技術在我們實際生活中應用的非常多。比較典型的應用就是控制電機的轉速,控制充電電流的大小,等等。而隨著技術的發(fā)展,也出現(xiàn)了其他類型的PWM技術,如相電壓PWM,線電壓PWM,SPWM等等,如果有興趣可以到網(wǎng)上去獲取相應資料學習。關于漸明漸暗的燈就簡單的講到這里。
第七章 一個有關0.0625℃的運算想到的問題
碰到一哥們號稱挺NB的嵌入軟件工程師,看了他的代碼后就歐拉,事情是在一個只有4K代碼的單片機接2個DS18B20測溫傳感器,都知道DS18B20輸出數(shù)據(jù)只要乘以0.0625就是測量的溫度值,這哥們說程序空間怎么也不夠,實際上程序只有簡單的采集兩個DS18B20的數(shù)據(jù)轉換成溫度值,之后在1602液晶上顯示,挺簡單個程序,怎么也想不通為什么程序空間不夠。只讀了一下代碼發(fā)現(xiàn)程序就沒動腦子,真的用浮點庫把DS18B20數(shù)據(jù)直接乘以0.0625了,那程序不超才怪呢,稍微動動腦子也會知道0.0625不就是1/16嗎,把DS18B20的數(shù)據(jù)直接右移4位不就是了(當然要注意符號),這右移程序可十分簡單還省空間,問題很好解決,空間自然也就夠了。 現(xiàn)在想來嵌入處理器確實是進步了,程序空間是越來越大,數(shù)據(jù)RAM空間也越來越大,導致很多人在寫程序的時候真的是什么都不顧,借著C語言的靈活性真是縱橫馳騁,壓根也不講個程序效率和可靠性。正如前些日子見到一孩子用ARM cortex-m3處理器給人接活寫個便攜表的1024點FFT算法,本身12位的AD系統(tǒng),這小家伙直接到網(wǎng)上下載了浮點的FFT算法代碼就給人加上了,結果整個程序死慢死慢的,人家用戶可不買單啊,這時要動動腦子把數(shù)據(jù)直接變成乘以某個數(shù)變成整數(shù)后用定點FFT處理,之后再把數(shù)據(jù)除一下不就行了。速度自然也快了,而且也能省下空間。實際當中我們做嵌入軟件很多時候犯懶都忽視程序執(zhí)行效率問題,是都能實現(xiàn)功能,但有時候就是沒法談性能。我?guī)状闻龅竭@樣的工程師,直接把傳感器的信號放大后進嵌入處理器的AD,也不看看AD數(shù)據(jù)是否穩(wěn)定有效,直接就進行FFT運算,那FFT結果真是熱鬧,不難看出混疊很嚴重,于是又機械地在FFT基礎上再去衍生算法,系統(tǒng)程序越做越大,速度越做越慢。實際上也很簡單的事,在傳感器放大信號進AD之前來一級抗混疊濾波基本也就解決了,大有所謂嵌入軟件高手的概念是程序幾乎是萬能,實在解決不了就換大程序空間更高速的處理器,整個惡性循環(huán)。 經(jīng)常聽說現(xiàn)在流行低碳族,我想出色的嵌入軟件工程師最容易成為低碳一族,只要讓代碼高效那處理器頻率自然可以靈活降下來,自然耗電也就少了,二氧化碳排放也就少了。想想目前到處都是嵌入處理器,代碼條數(shù)看來也別有效果。
第八章 如何設計復雜的多任務程序 我們在入門階段,一般面對的設計都是單一的簡單的任務,流程圖可以如圖 1 所示,通常會用踏步循環(huán)延時來滿足任務需要。面對多任務,稍微復雜的程序設計,沿用圖 1 的思想,我們會做出如圖 2 所示的程序,在大循環(huán)體中不斷增加任務,通常還要用延時來滿足特定任務節(jié)拍,這種程序設計思想它有明顯的不足,主要是各個任務之間相互影響,增加新的任何之后,以前很好的運行的任務有可能不正常,例如數(shù)碼管動態(tài)掃描,本來顯示效果很好的驅動函數(shù),在增加新的任務后出現(xiàn)閃爍,顯示效果變差了。 很明顯,初學者在設計程序時,需要從程序構架思想上下功夫,在做了大量基本模塊練習之后,需要總結提煉自己的程序設計思路(程序架構思想)。首先我們來理解“任務”,所謂任務,就是需要 CPU 周期“關照”的事件,絕大多數(shù)任務不需要 CPU 一直“關照” ,例如啟動 ADC 的啟動讀取。甚至有些任務“害怕”CPU 一直“關照”例如 LCD 的刷新,因為 LCD 是顯示給人看的,并不需要高速刷新,即便是顯示的內容在高速變化,也不需要高速刷新,道理是一樣的。這樣看來,讓CPU做簡單任務一定很浪費,事實也是如此,絕大多數(shù)簡單任務,CPU都是在“空轉” (循環(huán)踏步延時) 。對任務總結還可以知道,很多任務需要 CPU 不斷“關照” ,其實這種“不斷”也是有極限的,比如數(shù)碼管動態(tài)掃描,能夠做到40Hz 就可以了,又如鍵盤掃描,能夠做到20Hz(經(jīng)驗值),基本上也就不會丟有效按鍵鍵值了,再如LCD刷新,我覺得做到 10Hz 就可以了,等等。看來,絕大多數(shù)任務都是工作在低速頻度。而我們的CPU一旦運行起來,速度又很快,CPU本身就是靠很快的速度執(zhí)行很簡單的指令來勝任復雜的任務(邏輯)的。如果有辦法把“快”的 CPU分成多個慢的CPU,然后給不同的任務分配不同速度的CPU,這種設想是不是很好呢! 確實很好,下面就看如何將“快”的CPU劃分成多個“慢”的 CPU。根據(jù)這種想法,我們需要合理分配CPU資源來“關照”不同的任務,最好能夠根據(jù)任務本身合理占用CPU資源,首先看如圖 3 所示的流程圖,各個任務流程獨立,各任務通過全局變量來交互信息,在流程中有一個重要的模塊“任務切換”,就是任務切換模塊實現(xiàn) CPU 合理分配,這個任務切換模塊是怎么實現(xiàn)的呢? 首先需要理解,CPU 一旦運行起來,就無法停止(硬件支持時鐘停止的不在這里討論),誰能夠控制一批脫韁的馬呢?對了,有中斷,中斷能夠讓CPU回到特定的位置,設想,能不能用一個定時中斷,周期性的將 CPU這匹運行著的脫韁的馬召喚回來,重新給它安排特定的任務,事實上,任務切換就是這樣實現(xiàn)的。 如圖 4A 所示,CPU 在空閑任務循環(huán)等待,定時中斷將 CPU 周期性喚回,根據(jù)任務設計了不同的響應頻度,滿足條件的任務將獲得CPU資源,CPU為不同任務“關照”完成后,再次返回空閑任務,如此周而復始,對于各個任務而言,好像各自擁有一個獨立的CPU,各自獨立運行。用這種思想構建的程序框架,最大的好處是任務很容易裁剪,系統(tǒng)能夠做得很復雜。在充分考慮單片機中斷特性(在哪里中斷就返回到哪里)后,實際可行的任務切換如圖4B所示,定時中斷可能發(fā)生在任務調度,隨機任務執(zhí)行的任何時候,圖中最大的框框所示,不管中斷在何時發(fā)生,它都會正常返回,定時中斷所產生的影響只在任務調度模塊起作用,即依次讓不同的任務按不同的節(jié)拍就緒。任務調度會按一定的優(yōu)先級執(zhí)行就緒任務。總結不同的任務需要CPU關照的頻度,選擇最快的那個頻度來設定定時器中斷的節(jié)拍,一般選擇 200Hz,或者 100Hz 都可以。另外再給每個任務設定一個節(jié)拍控制計數(shù)器 C,也就是定時器每中斷多少次后執(zhí)行任務一次。例如取定時中斷節(jié)拍為 200Hz,給任務設定的 C=10,則任務執(zhí)行頻度為 200/10=20Hz,如果是數(shù)碼管掃描,按 40Hz 不閃爍規(guī)律,則任務節(jié)拍控制計數(shù)器 C=5 即可。在程序設計中,C 代表著任務運行的節(jié)拍控制參數(shù),我們習慣用 delay 來描述,不同的任務用task0,task1……來描述。 下面我們來用代碼實現(xiàn)以上多任務程序設計思想。首先是任務切換
while(1)
{
if(task_delay[0]==0) task0(); //task0就緒,
if(task_delay[1]==0) task1(); //task1就緒,
……
} 很顯然,執(zhí)行任務的條件是任務延時量task_delay=0,那么任務延時量誰來控制呢?定時器啊!定時器中斷對任務延時量減一直到歸零,標志任務就緒。當沒有任務就緒時,任務切換本身就是一個Idle 任務。
void timer0(void) interrupt 1
{
if(task_delay[0]) task_delay[0]--;
if(task_delay[1]) task_delay[1]--;
……
} 例如 timer0 的中斷節(jié)拍為 200Hz,task0_delay 初值為 10,則 task0()執(zhí)行頻度為200/10=20Hz。有了以上基礎,我們來設計一個簡單多任務程序,進一步深入理解這種程序設計思想。任務要求:用單片機不同 IO 腳輸出 1Hz,5Hz,10Hz,20Hz 方波信號,這個程序很短,將直接給出。
#include "reg51.h"
#define TIME_PER_SEC 200 //定義任務時鐘頻率,200Hz
#define CLOCK 22118400 //定義時鐘晶振,單位Hz
#define MAX_TASK 4 //定義任務數(shù)量
extern void task0(void); //任務聲明
extern void task1(void);
extern void task2(void);
extern void task3(void);
sbit f1Hz = P1^0; //端口定義
sbit f5Hz = P1^1;
sbit f10Hz = P1^2;
sbit f20Hz = P1^3;
unsigned char task_delay[4]; //任務延時變量定義
//定時器0初始化
void timer0_init(void)
{
unsigned char i;
for(i=0;i<MAX_TASK;i++) task_delay[ i]=0; //任務延時量清零
TMOD = (TMOD & 0XF0) | 0X01; //定時器 0工作在模式 1, 16Bit定時器模式
TH0 = 255-CLOCK/TIME_PER_SEC/12/256;
TL0 = 255-CLOCK/TIME_PER_SEC/12%256;
TR0 =1;
ET0 =1; //開啟定時器和中斷
}
// 系統(tǒng) OS定時中斷服務
void timer0(void) interrupt 1
{
unsigned char i;
TH0 = 255-CLOCK/TIME_PER_SEC/12/256;
TL0 = 255-CLOCK/TIME_PER_SEC/12%256;
for(i=0;i<MAX_TASK;i++) if(task_delay[ i]) task_delay[ i]--;
//每節(jié)拍對任務延時變量減1 ,減至 0 后,任務就緒。
}
/*main主函數(shù)*/
void main(void)
{
timer0_init();
EA=1;//開總中斷
while(1)
{
if(task_delay[0]==0) {task0(); task_delay[0] = TIME_PER_SEC/ 2;}
//要產生 1hz 信號,翻轉周期就是 2Hz,以下同
if(task_delay[1]==0) {task1(); task_delay[1] = TIME_PER_SEC/10;}
//要產生 5hz 信號,翻轉周期就是 10Hz,以下同
if(task_delay[2]==0) {task2(); task_delay[2] = TIME_PER_SEC/20;}
if(task_delay[3]==0) {task3(); task_delay[3] = TIME_PER_SEC/40;}
}
}
void task0(void)
{
f1Hz = !f1Hz;
}
void task1(void)
{
f5Hz = !f5Hz;
}
void task2(void)
{
f10Hz = !f10Hz;
}
void task3(void)
{
f20Hz = !f20Hz;
}
仿真效果如圖5 所示。  圖 5 仿真波形圖 同樣的程序,同學們可以考慮用圖 2 所示的思想設計,看看容易不容易,如果你的程序實現(xiàn)了相同的功能,如果我改變要求,改變信號的頻率,你的程序容易修改嗎? 要進一步完善這種程序設計思想,有幾個問題還需要考慮: 對任務本身有什么要求? 不同任務之間有沒有優(yōu)先級?(不同的事情總有個輕重緩急吧!) 任務間如何延時? …… 為了回答這些問題,下面我們來分析 CPU的運行情況。
CPU運行情況如圖 6 所示,黑色區(qū)域表示 CPU進程,系統(tǒng)啟動后, CPU將無休止的運行,CPU資源將如何分配呢?程序首先進入“任務切換”進程,如果當前沒有任務就緒,就在任務切換進程循環(huán)(也可以理解為空閑進程),定時中斷將 CPU 當前進程打斷,在定時中斷進程可能讓某些任務就緒,中斷返回任務切換進程,很快會進入就緒任務 0,CPU“關照”完任務 0,再次回到任務切換進程,如果還有其它任務就緒,還會再次進入其它任務,沒有任務就循環(huán)等待,定時中斷會不斷讓新的任務就緒,CPU 也會不斷進入任務“關照” 。這樣不同的任務就會獲得不同的CPU資源,每一個任務都像是擁有一個獨立的CPU 為之服務。從這種進程切換我們可以看出,在定時中斷和任務切換過程中,額外的占用了一些 CPU資源, 這就是定時中斷頻度不宜太快, 否則將大大降低CPU的有效資源率, 當然太慢也不行。另外就是 CPU每次關照任務的時間不能太長,如果超過一個中斷周期,就會影響到其它任務的實時性。所謂的實時性就是按定時中斷設定的節(jié)拍,準時得到CPU關照。這樣,每一個子任務就必須簡單,每次“關照”時間最好不要超過定時中斷節(jié)拍周期(5ms 或 10ms,初學者要對 ms 有一個概念,機器周期為 us 級的單片機,1ms 可以執(zhí)行上千條指令,對于像數(shù)碼管掃描,鍵盤掃描,LCD顯示等常規(guī)任務都是綽綽有余的,只是遇到大型計算,數(shù)據(jù)排序就顯得短了) 關于任務優(yōu)先級的問題:一個復雜系統(tǒng),多個任務之間總有“輕重緩急”之區(qū)別,那些需要嚴格實時的任務通常用中斷實現(xiàn),中斷能夠保證第一時間相應,我們這里討論的不是那種實時概念,是指在最大允許時差內能夠得到 CPU“關照” ,例如鍵盤掃描,為了保證較好的操作效果,快的/慢的/長的/短的(不同人按鍵不一樣)都能夠正確識別,這就要保證足夠的掃描速度,這種掃描速度對不同的按鍵最好均等,如果我們按 50Hz 來設計,那么就要保證鍵盤掃描速度在任何情況下都能夠做到 50Hz 掃描頻度,不會因為某個新任務的開啟而被破壞,如果確實有新的任務有可能破壞這個 50Hz 掃描頻度,我們就應該在優(yōu)先級安排上讓鍵盤掃描優(yōu)先級高于那個可能影響鍵盤掃描的任務。這里體現(xiàn)的就是當同時多個任務就緒時, 最先執(zhí)行哪個的問題,任務調度時要優(yōu)先執(zhí)行級別高的任務。關于“長”任務的問題:有些任務雖然很獨立,但完成一次任務執(zhí)行需要很長時間,例如 DS18B20,從復位初始化到讀回溫度值,最長接近 1s,這主要是 DS18B20 溫度傳感器完成一次溫度轉換需要500 到 750ms,這個時間對 CPU 而言,簡直是太長了,就像一件事情需要我們人等待 10 年一樣,顯然這樣的任務是其它任務所耽擱不起的。像類似 DS18B20 這樣的器件(不少 ADC 也是這樣) ,怎么設計任務體解決“長”的問題。進一步研究這些器件發(fā)現(xiàn),真正需要CPU“關照”它們的時間并不長,關鍵是等待結果要很長時間。解決的辦法就是把類似的器件驅動分成多個段:初始化段、啟動段、讀結果段,而在需要花長時間等待時間段,不要 CPU關照,允許 CPU去關照其它任務。 將一個任務分成若干段,確保每段需要CPU 關照時長小于定時器中斷節(jié)拍長,這樣CPU在處理這些長任務時,就不會影響到其它任務的執(zhí)行。 正是基于以上程序設計思想,總結完善后提出一種耗費資源特別少并且不使用堆棧的多線程操作系統(tǒng),這個操作系統(tǒng)以純C語言實現(xiàn),無硬件依賴性,需要單片機的資源極少。起名為 Easy51RTOS,特別適合初學者學習使用。有任務優(yōu)先級,通過技巧可以任務間延時,缺點是高優(yōu)先級任務不具有搶占功能,一個具有搶占功能的操作系統(tǒng),一定要涉及到現(xiàn)場保護與恢復,需要更多的 RAM 資源,涉及到堆棧知識,文件系統(tǒng)將很復雜,初學者學習難度大。 為了便于初學者學習,將代碼文件壓縮至 4 個文件。
Easy51RTOS.Uv2 Keil工程文件,KEIL用戶很熟悉的
main.c main函數(shù)和用戶任務 task 函數(shù)文件
os_c.c Easy51RTOS相關函數(shù)文件
os_cfg.h Easy51RTOS相關配置參數(shù)頭文件
文件解讀如下:
仿真圖如圖8 所示
 主程序巧妙實現(xiàn)優(yōu)先級設定:
for(i=0;i<MAX_TASK;i++)
if (task_delay[ i]==0) {run(task[ i]); break;} //就緒任務調度 這里的 break 將跳出 for 循環(huán),使得每次重新任務調度總是從 task0 開始,就意味著優(yōu)先級高的任務就緒會先執(zhí)行。這樣task0具有最高優(yōu)先級,task1、task2、task3優(yōu)先級依次降低。特別是 void task3(void)用 switch(state)狀態(tài)機實現(xiàn)了任務分段,這也是任務內系統(tǒng)延時的一種方法。 今天我把咱們常用的傳感器DS1320 DS18B20給大家介紹下。 對于市面上的大多數(shù)51單片機開發(fā)板來說。ds1302和ds18b20應該是比較常見的兩種外圍芯片。ds1302是具有SPI總線接口的時鐘芯片。ds18b20則是具有單總線接口的數(shù)字溫度傳感器。下面讓我們分別來認識并學會應用這兩種芯片。 首先依舊是看DS1302的datasheet中的相關介紹。 上面是它的一些基本的應用介紹。
下面是它的引腳的描述。 下面是DS1302的時鐘寄存器。我們要讀取的時間數(shù)據(jù)就是從下面這些數(shù)據(jù)寄存器中讀取出來的。當我們要想調整時間時,可以把時間數(shù)據(jù)寫入到相應的寄存器中就可以了。 這是DS1302內部的31個RAM寄存器。在某些應用場合我們可以應用到。如我們想要做一個帶定時功能的鬧鐘。則可以把鬧鐘的時間寫入到31個RAM寄存器中的任意幾個。當單片機掉電時,只要我們的DS1302的備用電池還能工作,那么保存在其中的鬧鐘數(shù)據(jù)就不會丟失~~ 由于對于這些器件的操作基本上按照數(shù)據(jù)手冊上面提供的時序圖和相關命令字來進行操作就可以了。因此在我們應用這些器件的時候一定要對照著手冊上面的要求來進行操作。如果覺得還不夠放心的話。可以到網(wǎng)上下載一些參考程序。對著手冊看別人的程序,看別人的思路是怎么樣的。 DS1302和單片機的連接很簡單。只需一根復位線,一根時鐘線,一根數(shù)據(jù)線即可。同時它本身還需要接一個32.768KHz的晶振來提供時鐘源。對于晶振的兩端可以分別接一個6PF左右的電容以提高晶振的精確度。同時可以在第8腳接上一個3.6V的可充電的電池。當系統(tǒng)正常工作時可以對電池進行涓流充電。當系統(tǒng)掉電時,DS1302由這個電池提供的能量繼續(xù)工作。 下面讓我們來驅動它。
- sbit io_DS1302_RST = P2^0 ;
- sbit io_DS1302_IO = P2^1 ;
- sbit io_DS1302_SCLK = P2^2 ;
-
- //-------------------------------------常數(shù)宏---------------------------------//
- #define DS1302_SECOND_WRITE 0x80 //寫時鐘芯片的寄存器位置
- #define DS1302_MINUTE_WRITE 0x82
- #define DS1302_HOUR_WRITE 0x84
- #define DS1302_WEEK_WRITE 0x8A
- #define DS1302_DAY_WRITE 0x86
- #define DS1302_MONTH_WRITE 0x88
- #define DS1302_YEAR_WRITE 0x8C
- #define DS1302_SECOND_READ 0x81 //讀時鐘芯片的寄存器位置
- #define DS1302_MINUTE_READ 0x83
- #define DS1302_HOUR_READ 0x85
- #define DS1302_WEEK_READ 0x8B
- #define DS1302_DAY_READ 0x87
- #define DS1302_MONTH_READ 0x89
- #define DS1302_YEAR_READ 0x8D
- //-----------------------------------操作宏----------------------------------//
- #define DS1302_SCLK_HIGH io_DS1302_SCLK = 1 ;
- #define DS1302_SCLK_LOW io_DS1302_SCLK = 0 ;
-
- #define DS1302_IO_HIGH io_DS1302_IO = 1 ;
- #define DS1302_IO_LOW io_DS1302_IO = 0 ;
- #define DS1302_IO_READ io_DS1302_IO
- #define DS1302_RST_HIGH io_DS1302_RST = 1 ;
- #define DS1302_RST_LOW io_DS1302_RST = 0 ;
- /******************************************************
- * 保存時間數(shù)據(jù)的結構體 *
- ******************************************************/
- struct
- {
- uint8 Second ;
- uint8 Minute ;
- uint8 Hour ;
- uint8 Day ;
- uint8 Week ;
- uint8 Month ;
- uint8 Year ;
- }CurrentTime ;
- /******************************************************************************
- * Function: static void v_DS1302Write_f( uint8 Content ) *
- * Description:向DS1302寫一個字節(jié)的內容 *
- * Parameter:uint8 Content : 要寫的字節(jié) *
- * *
- ******************************************************************************/
- static void v_DS1302Write_f( uint8 Content )
- {
- uint8 i ;
- for( i = 8 ; i > 0 ; i-- )
- {
- if( Content & 0x01 )
- {
- DS1302_IO_HIGH
- }
- else
- {
- DS1302_IO_LOW
- }
- Content >>= 1 ;
- DS1302_SCLK_HIGH
- DS1302_SCLK_LOW
- }
- }
- /******************************************************************************
- * Function: static uint8 v_DS1302Read_f( void ) *
- * Description: 從DS1302當前設定的地址讀取一個字節(jié)的內容 *
- * Parameter: *
- * Return: 返回讀出來的值(uint8) *
- ******************************************************************************/
- static uint8 v_DS1302Read_f( void )
- {
- uint8 i, ReadValue ;
- DS1302_IO_HIGH
- for( i = 8 ; i > 0 ; i-- )
- {
- ReadValue >>= 1 ;
- if( DS1302_IO_READ )
- {
- ReadValue |= 0x80 ;
- }
- else
- {
- ReadValue &= 0x7f ;
- }
- DS1302_SCLK_HIGH
- DS1302_SCLK_LOW
-
- }
- return ReadValue ;
- }
- /******************************************************************************
- * Function: void v_DS1302WriteByte_f( uint8 Address, uint8 Content ) *
- * Description: 從DS1302指定的地址寫入一個字節(jié)的內容 *
- * Parameter: Address: 要寫入數(shù)據(jù)的地址 *
- * Content: 寫入數(shù)據(jù)的具體值 *
- * Return: *
- ******************************************************************************/
- void v_DS1302WriteByte_f( uint8 Address, uint8 Content )
- {
- DS1302_RST_LOW
- DS1302_SCLK_LOW
- DS1302_RST_HIGH
- v_DS1302Write_f( Address ) ;
- v_DS1302Write_f( Content ) ;
- DS1302_RST_LOW
- DS1302_SCLK_HIGH
- }
- /******************************************************************************
- * Function: uint8 v_DS1302ReadByte_f( uint8 Address ) *
- * Description:從DS1302指定的地址讀出一個字節(jié)的內容 *
- * Parameter:Address: 要讀出數(shù)據(jù)的地址 *
- * *
- * Return: 指定地址讀出的值(uint8) *
- ******************************************************************************/
- uint8 v_DS1302ReadByte_f( uint8 Address )
- {
- uint8 ReadValue ;
- DS1302_RST_LOW
- DS1302_SCLK_LOW
- DS1302_RST_HIGH
- v_DS1302Write_f( Address ) ;
- ReadValue = v_DS1302Read_f() ;
- DS1302_RST_LOW
- DS1302_SCLK_HIGH
- return ReadValue ;
- }
- /******************************************************************************
- * Function: void v_ClockInit_f( void ) *
- * Description:初始化寫入DS1302時鐘寄存器的值(主程序中只需調用一次即可) *
- * Parameter: *
- * *
- * Return: *
- ******************************************************************************/
- void v_ClockInit_f( void )
- {
- if( v_DS1302ReadByte_f( 0xc1) != 0xf0 )
- {
- v_DS1302WriteByte_f( 0x8e, 0x00 ) ; //允許寫操作
- v_DS1302WriteByte_f( DS1302_YEAR_WRITE, 0x08 ) ; //年
- v_DS1302WriteByte_f( DS1302_WEEK_WRITE, 0x04 ) ; //星期
- v_DS1302WriteByte_f( DS1302_MONTH_WRITE, 0x12 ) ; //月
- v_DS1302WriteByte_f( DS1302_DAY_WRITE, 0x11 ) ; //日
- v_DS1302WriteByte_f( DS1302_HOUR_WRITE, 0x13 ) ; //小時
- v_DS1302WriteByte_f( DS1302_MINUTE_WRITE, 0x06 ) ; //分鐘
- v_DS1302WriteByte_f( DS1302_SECOND_WRITE, 0x40 ) ; //秒
- v_DS1302WriteByte_f( 0x90, 0xa5 ) ; //充電
- v_DS1302WriteByte_f( 0xc0, 0xf0 ) ; //判斷是否初始化一次標識寫入
- v_DS1302WriteByte_f( 0x8e, 0x80 ) ; //禁止寫操作
- }
- }
- /******************************************************************************
- * Function: void v_ClockUpdata_f( void ) *
- * Description:讀取時間數(shù)據(jù),并保存在結構體CurrentTime中 *
- * Parameter: *
- * *
- * Return: *
- ******************************************************************************/
- void v_ClockUpdata_f( void )
- {
- CurrentTime.Second = v_DS1302ReadByte_f( DS1302_SECOND_READ ) ;
- CurrentTime.Minute = v_DS1302ReadByte_f( DS1302_MINUTE_READ ) ;
- CurrentTime.Hour = v_DS1302ReadByte_f( DS1302_HOUR_READ ) ;
- CurrentTime.Day = v_DS1302ReadByte_f( DS1302_DAY_READ ) ;
- CurrentTime.Month = v_DS1302ReadByte_f( DS1302_MONTH_READ ) ;
- CurrentTime.Week = v_DS1302ReadByte_f( DS1302_WEEK_READ ) ;
- CurrentTime.Year = v_DS1302ReadByte_f( DS1302_YEAR_READ ) ;
- }
復制代碼
有了上面的這些函數(shù)我們就可以對DS1302進行操作了。當我們想要獲取當前時間時,只需要調用v_ClockUpdata_f( void )這個函數(shù)即可。讀取到的時間數(shù)據(jù)保存在CurrentTime這個結構體中。至于如何把時間數(shù)據(jù)在數(shù)碼管或者是液晶屏上顯示出來我相信大家應該都會了吧^_^. 看看顯示效果如何~~ 下面再讓我們看看DS18B20吧。 DS18B20是單總線的數(shù)字溫度傳感器。其與單片機的接口只需要一根數(shù)據(jù)線即可。當然連線簡單意味著軟件處理上可能要麻煩一點。下面來看看它的優(yōu)點:  看看它的靚照。外形和我們常用的三極管沒有什么兩樣哦。 DS18B20的內部存儲器分為以下幾部分 ROM:存放該器件的編碼。前8位為單線系列的編碼(DS18B20的編碼是19H)后面48位為芯片的唯一序列號。在出場的時候就已經(jīng)設置好,用戶無法更改。最后8位是以上56位的CRC碼。 RAM:DS18B20的內部暫存器共9個字節(jié)。其中第一個和第二個字節(jié)存放轉換后的溫度值。第二個和第三個字節(jié)分別存放高溫和低溫告警值。(可以用RAM指令將其拷貝到EEPROM中)第四個字節(jié)為配置寄存器。第5~7個字節(jié)保留。第9個字節(jié)為前8個字節(jié)的CRC碼。 DS18B20的溫度存放如上圖所示。其中S位符號位。當溫度值為負值時,S = 1 ,反之則S = 0 。我們把得到的溫度數(shù)據(jù)乘上對應的分辨率即可以得到轉換后的溫度值。 DS18B20的通訊協(xié)議: 在對DS18B20進行讀寫編程時,必須嚴格保證讀寫的時序。否則將無法讀取測溫結果。根據(jù)DS18B20的通訊協(xié)議,主機控制DS18B20完成溫度轉換必須經(jīng)過3個步驟:每一次讀寫之前都要對DS18B20進行復位,復位成功后發(fā)送一條ROM指令,最后發(fā)送RAM指令。這樣才能對DS18B20進行預定的操作。復位要求主機將數(shù)據(jù)線下拉500us,然后釋放,DS18B20收到信號后等待16~160us然后發(fā)出60~240us的存在低脈沖,主機收到此信號表示復位成功。 上圖即DS18B20的復位時序圖。 下面是讀操作的時序圖  這是寫操作的時序圖  下面讓我們來看看它的驅動程序如何寫吧。
- sbit io_DS18B20_DQ = P2^3 ;
- #define DS18B20_DQ_HIGH io_DS18B20_DQ = 1 ;
- #define DS18B20_DQ_LOW io_DS18B20_DQ = 0 ;
- #define DS18B20_DQ_READ io_DS18B20_DQ
- /*******************************************************************
- * 保存溫度值的數(shù)組.依次存放正負標志,溫度值十位,個位,和小數(shù)位 *
- *******************************************************************/
- uint8 Temperature[ 4 ] ;
- void v_Delay10Us_f( uint16 Count )
- {
- while( --Count )
- {
- _nop_();
- }
- }
- /**************************************************************************
- * Function: uint8 v_Ds18b20Init_f( void ) *
- * Description: 初始化DS18B20 *
- * Parameter: *
- * *
- * Return: 返回初始化的結果(0:復位成功 1:復位失敗) *
- **************************************************************************/
- uint8 v_Ds18b20Init_f( void )
- {
- uint8 Flag ;
- DS18B20_DQ_HIGH //稍作延時
- v_Delay10Us_f( 3 ) ;
- DS18B20_DQ_LOW //總線拉低
- v_Delay10Us_f( 80 ) ; //延時大于480us
- DS18B20_DQ_HIGH //總線釋放
- v_Delay10Us_f( 15 ) ;
- Flag = DS18B20_DQ_READ ; //如果Flag為0,則復位成功,否則復位失敗
- return Flag ;
- }
- /******************************************************************************
- * Function: void v_Ds18b20Write_f( uint8 Cmd ) *
- * Description: 向DS18B20寫命令 *
- * Parameter: Cmd: 所要寫的命令 *
- * *
- * Return: *
- ******************************************************************************/
- void v_Ds18b20Write_f( uint8 Cmd )
- {
- uint8 i ;
- for( i = 8 ; i > 0 ; i-- )
- {
- DS18B20_DQ_LOW //拉低總線,開始寫時序
- DS18B20_DQ_READ = Cmd & 0x01 ; //控制字的最低位先送到總線
- v_Delay10Us_f( 5 ) ; //稍作延時,讓DS18B20讀取總線上的數(shù)據(jù)
- DS18B20_DQ_HIGH //拉高總線,1bit寫周期結束
- Cmd >>= 1 ;
- }
- }
- /******************************************************************************
- * Function: uint8 v_Ds18b20Read_f( void ) *
- * Description: 向DS18B20讀取一個字節(jié)的內容 *
- * Parameter: *
- * *
- * Return: 讀取到的數(shù)據(jù) *
- ******************************************************************************/
- uint8 v_Ds18b20Read_f( void )
- {
- uint8 ReadValue, i ;
- for( i = 8 ; i > 0 ; i-- )
- {
- DS18B20_DQ_LOW
- ReadValue >>= 1 ;
- DS18B20_DQ_HIGH
- if( DS18B20_DQ_READ == 1 )
- ReadValue |= 0x80 ;
- v_Delay10Us_f( 3 ) ;
- }
- return ReadValue ;
- }
- /******************************************************************************
- * Function: uint16 v_Ds18b20ReadTemp_f( void ) *
- * Description: 讀取當前的溫度數(shù)據(jù)(只保留了一位小數(shù)) *
- * Parameter: *
- * *
- * Return: 讀取到的溫度值 *
- ******************************************************************************/
- uint16 v_Ds18b20ReadTemp_f( void )
- {
- uint8 TempH, TempL ;
- uint16 ReturnTemp ;
-
- /* if( v_Ds18b20Init_() ) return ; //復位失敗,在這里添加錯誤處理的代碼 */
- v_Ds18b20Init_f() ; /復位DS18B20
- v_Ds18b20Write_f( 0xcc ) ; //跳過ROM
- v_Ds18b20Write_f( 0x44 ) ; //啟動溫度轉換
- v_Ds18b20Init_f() ;
- v_Ds18b20Write_f( 0xcc ) ; //跳過ROM
- v_Ds18b20Write_f( 0xbe ) ; //讀取DS18B20內部的寄存器內容
- TempL = v_Ds18b20Read_f() ; //讀溫度值低位(內部RAM的第0個字節(jié))
- TempH = v_Ds18b20Read_f() ; //讀溫度值高位(內部RAM的第1個字節(jié))
- ReturnTemp = TempH ;
- ReturnTemp <<= 8 ;
- ReturnTemp |= TempL ; //溫度值放在變量ReturnTemp中
- return ReturnTemp ;
- }
- /******************************************************************************
- * Function: void v_TemperatureUpdate_f( void ) *
- * Description:讀取當前的溫度數(shù)據(jù)并轉化存放在數(shù)組Temperature(只保留了一位小數(shù)) *
- * Parameter: *
- * *
- * Return: *
- ******************************************************************************/
- void v_TemperatureUpdate_f( void )
- {
- uint8 Tflag = 0 ;
- uint16 TempDat ;
- float Temp ;
- TempDat = v_Ds18b20ReadTemp_f() ;
- if( TempDat & 0xf000 )
- {
- Tflag = 1 ;
- TempDat = ~TempDat + 1 ;
- }
- Temp = TempDat >> 4; (TempDat * 0.0625 ) 請大家不要用乘以,不知道為什么可以看我上面的內容
- TempDat = Temp * 10 ; ;小數(shù)部用可以用查表法,大家有什么好辦法來討論下,呵呵
- Temperature[ 0 ] = Tflag ; //溫度正負標志
- Temperature[ 1 ] = TempDat / 100 + '0' ; //溫度十位值
- Temperature[ 2 ] = TempDat % 100 / 10 + '0' ; //溫度個位值
- Temperature[ 3 ] = TempDat % 10 + '0' ;//溫度小數(shù)位
- }
復制代碼
如果想獲取當前的溫度數(shù)據(jù),在主函數(shù)中調用v_TemperatureUpdate_f( void )就可以了。溫度數(shù)據(jù)就保存到Temperature中去了。至于如何顯示,就不用多說了吧~@_@~ 。 時間和溫度一起顯示出來看看 OK,至此ds18b20和ds1302的應用告一段落。如果有不懂的,記得多看datasheet,多交流。
全部資料51hei下載地址:
pdf格式:
從單片機初學者邁向單片機工程師.pdf
(4.79 MB, 下載次數(shù): 163)
2018-11-25 15:16 上傳
點擊文件名下載附件
word格式:
從單片機初學者邁向單片機工程師.doc
(2.05 MB, 下載次數(shù): 75)
2018-11-25 18:16 上傳
點擊文件名下載附件
|