本教材現(xiàn)以連載的方式由網(wǎng)絡(luò)發(fā)布,并將于2014年由清華大學(xué)出版社出版最終完整版,版權(quán)歸作者和清華大學(xué)出版社所有。本著開源、分享的理念,本教材可以自由傳播及學(xué)習(xí)使用,但是務(wù)必請注明出處來自金沙灘工作室
在前面的課程中我們已經(jīng)了解到了不少關(guān)于時鐘的概念,比如我們用的單片機(jī)的主時鐘是11.0592M、I2C總線有一條時鐘信號線SCL等,這些時鐘本質(zhì)上都是一個某一頻率的方波信號。那么除了這些在前面新學(xué)到的時鐘概念外,還有一個我們早已熟悉的不能再熟悉的時鐘概念——年-月-日 時:分:秒,就是我們的鐘表和日歷給出的時間,它的重要程度我想就不需要多說了吧,在單片機(jī)系統(tǒng)里我們把它稱作實時時鐘,以區(qū)別于前面提到的幾種方波時鐘信號。實時時鐘,有時也被稱作墻上時鐘,很形象的一個名詞,對吧,大家知道他們講的一回事就行了。本章,我們將學(xué)習(xí)實時時鐘的應(yīng)用,有了它,你的單片機(jī)系統(tǒng)就能在漫漫歷史長河中找到自己的時間定位啦,可以在指定時間干某件事,或者記錄下某事發(fā)生的具體時間,等等。除此之外,本章還會學(xué)習(xí)到C語言的結(jié)構(gòu)體,它也是C語言的精華部分,我們通過本章先來了解它的基礎(chǔ),后面再逐漸達(dá)到熟練、靈活運用它,你的編程水平會提高一個檔次哦。15.1 BCD碼的學(xué)習(xí) 在我們?nèi)粘Ia(chǎn)生活中用的最多的數(shù)字是十進(jìn)制數(shù)字,而單片機(jī)系統(tǒng)的所有數(shù)據(jù)本質(zhì)上都是二進(jìn)制的,所以聰明的前輩們就給我們創(chuàng)造了BCD碼。
BCD碼(Binary-Coded Decimal)亦稱二進(jìn)碼十進(jìn)制數(shù)或二-十進(jìn)制代碼。用4位二進(jìn)制數(shù)來表示1位十進(jìn)制數(shù)中的0~9這10個數(shù)字。是一種二進(jìn)制的數(shù)字編碼形式,用二進(jìn)制編碼的十進(jìn)制代碼。BCD碼這種編碼形式利用了四個位元來儲存一個十進(jìn)制的數(shù)碼,使二進(jìn)制和十進(jìn)制之間的轉(zhuǎn)換得以快捷的進(jìn)行。我們前邊講過十六進(jìn)制和二進(jìn)制本質(zhì)上是一回事,十六進(jìn)制僅僅是二進(jìn)制的一種縮寫形式而已。而十進(jìn)制的一位數(shù)字,從0到9,最大的數(shù)字就是9,再加1就要進(jìn)位,所以用4位二進(jìn)制表示十進(jìn)制,就是從0000到1001,不存在1010、1011、1100、1101、1110、1111這6個數(shù)字。BCD碼如果到了1001,再加1的話,數(shù)字就變成了0001 0000這樣的數(shù)字了,相當(dāng)于用了8位的二進(jìn)制數(shù)字表示了2位的十進(jìn)制數(shù)字。關(guān)于BCD碼更詳細(xì)的介紹請點擊www.torrancerestoration.com的基礎(chǔ)教程欄目里面有很多相關(guān)文章.
BCD碼的應(yīng)用還是非常廣泛的,比如我們這節(jié)課要學(xué)的實時時鐘,日期時間在時鐘芯片中的存儲格式就是BCD碼,當(dāng)我們需要把它記錄的時間轉(zhuǎn)換成可以直觀顯示的ASCII碼時(比如在液晶上顯示),就可以省去一步由二進(jìn)制的整型數(shù)到ASCII的轉(zhuǎn)換過程,而直接取出表示十進(jìn)制1位數(shù)字的4個二進(jìn)制位然后再加上0x30就可組成一個ASCII碼字節(jié)了,這樣就會方便的多,在后面的實際例程中將看到這個簡單的轉(zhuǎn)換。
15.2 SPI時序初步認(rèn)識 UART、I2C和SPI是單片機(jī)通信中最常用的三種通信協(xié)議。前邊我們已經(jīng)學(xué)了UART和I2C通信協(xié)議,這節(jié)課我們來學(xué)習(xí)剩下的SPI通信協(xié)議。SPI是英語Serial Peripheral Interface的縮寫,顧名思義就是串行外圍設(shè)備接口。SPI是一種高速的、全雙工、同步通信總線,標(biāo)準(zhǔn)的SPI也僅僅使用4個引腳,常用于單片機(jī)和EEPROM、FLASH、實時時鐘、數(shù)字信號處理器等器件的通信。SPI通信原理比I2C要簡單,它主要是主從方式通信,這種模式通常只有一個主機(jī)和一個或者多個從機(jī),標(biāo)準(zhǔn)的SPI是4根線,分別是SSEL(片選,也寫作SCS)、SCLK(時鐘,也寫作SCK)、MOSI(主機(jī)輸出從機(jī)輸入Master Output/Slave Input)和MISO(主機(jī)輸入從機(jī)輸出Master Input/Slave Output)。
SSEL:從設(shè)備片選使能信號。如果從設(shè)備是低電平使能的話,當(dāng)拉低這個引腳后,從設(shè)備就會被選中,主機(jī)和這個被選中的從機(jī)進(jìn)行通信。
SCLK:時鐘信號,由主機(jī)產(chǎn)生,和I2C通信的SCL有點類似。
MOSI:主機(jī)給從機(jī)發(fā)送指令或者數(shù)據(jù)的通道。
MISO:主機(jī)讀取從機(jī)的狀態(tài)或者數(shù)據(jù)的通道。
在某些情況下,我們也可以用3根線的SPI或者2根線的SPI進(jìn)行通信。比如主機(jī)只給從機(jī)發(fā)送命令,從機(jī)不需要回復(fù)數(shù)據(jù)的時候,那MISO就可以不要;而在主機(jī)只讀取從機(jī)的數(shù)據(jù),不需要給從機(jī)發(fā)送指令的時候,那MOSI可以不要;當(dāng)一個主機(jī)一個從機(jī)的時候,從機(jī)的片選有時可以固定為有效電平而一直處于使能狀態(tài),那么SSEL可以不要;此時如果再加上主機(jī)只給從機(jī)發(fā)送數(shù)據(jù),那么SSEL和MISO都可以不要;如果主機(jī)只讀取從機(jī)送來的數(shù)據(jù),SSEL和MOSI都可以不要。 3線和2線的SPI大家要知道怎么回事,實際使用也是有應(yīng)用的,但是當(dāng)我們提及SPI的時候,一般都是指標(biāo)準(zhǔn)SPI,都是指4根線的這種形式。
SPI通信的主機(jī)也是我們的單片機(jī),在讀寫數(shù)據(jù)時序的過程中,有四種模式,要了解這四種模式,首先我們得學(xué)習(xí)一下2個名詞。
CPOL:Clock Polarity,就是時鐘的極性。
時鐘的極性是什么概念呢?通信的整個過程分為空閑時刻和通信時刻,SCLK在數(shù)據(jù)發(fā)送之前和之后的空閑狀態(tài)是高電平那么CPOL=1,如果空閑狀態(tài)SCLK是低電平,那么CPOL=0。
CPHA:Clock Phase,就是時鐘的相位。
主機(jī)和從機(jī)要交換數(shù)據(jù),就牽涉到一個問題,即主機(jī)在什么時刻輸出數(shù)據(jù)到MOSI上而從機(jī)在什么時刻采樣這個數(shù)據(jù),或者從機(jī)在什么時刻輸出數(shù)據(jù)到MISO上而主機(jī)什么時刻采樣這個數(shù)據(jù)。同步通信的一個特點就是所有數(shù)據(jù)的變化和采樣都是伴隨著時鐘沿進(jìn)行的,也就是說數(shù)據(jù)總是在時鐘的邊沿附近變化或被采樣。而一個時鐘周期必定包含了一個上升沿和一個下降沿,這是周期的定義所決定的,只是這兩個沿的先后并無規(guī)定。又因為數(shù)據(jù)從產(chǎn)生的時刻到它的穩(wěn)定是需要一定時間的,那么,如果主機(jī)在上升沿輸出數(shù)據(jù)到MOSI上,從機(jī)就只能在下降沿去采樣這個數(shù)據(jù)了。反之如果一方在下降沿輸出數(shù)據(jù),那么另一方就必須在上升沿采樣這個數(shù)據(jù)。
CPHA=1,就表示數(shù)據(jù)的輸出是在一個時鐘周期的第一個沿上,至于這個沿是上升沿還是下降沿,這要是CPOL的值而定,CPOL=1那就是下降沿,反之就是上升沿。那么數(shù)據(jù)的采樣自然就是在第二個沿上了。
CPHA=0,就表示數(shù)據(jù)的采樣是在一個時鐘周期的第一個沿上,同樣它是什么沿由CPOL決定。那么數(shù)據(jù)的輸出自然就在第二個沿上了。仔細(xì)想一下,這里會有一個問題:就是當(dāng)一幀數(shù)據(jù)開始傳輸?shù)谝籦it時,在第一個時鐘沿上就采樣該數(shù)據(jù)了,那么它是在什么時候輸出來的呢?有兩種情況:一是SSEL使能的邊沿,二是上一幀數(shù)據(jù)的最后一個時鐘沿,有時兩種情況還會同時生效。
我們以CPOL=1/CPHA=1為例,把時序圖畫出來給大家看一下,如圖15-1所示,。
1.JPG (47.69 KB, 下載次數(shù): 284)
下載附件
2013-10-11 01:07 上傳
15-1 SPI通信時序圖(1)
大家看圖15-1所示,當(dāng)數(shù)據(jù)未發(fā)送時以及發(fā)送完畢后,SCK都是高電平,因此CPOL=1。可以看出,在SCK第一個沿的時候,MOSI和MISO會發(fā)生變化,同時SCK第二個沿的時候,數(shù)據(jù)是穩(wěn)定的,此刻采樣數(shù)據(jù)是合適的,也就是上升沿即一個時鐘周期的后沿鎖存讀取數(shù)據(jù),即CPHA=1。注意最后最隱蔽的SSEL片選,一般情況下,這個引腳通常用來決定是哪個從機(jī)和主機(jī)進(jìn)行通信。剩余的三種模式,我把圖畫出來,簡化起見把MOSI和MISO合在一起了,大家仔細(xì)對照看看研究一下,把所有的理論過程都弄清楚,有利于你對SPI通信的深刻理解,如圖15-2所示。
2.JPG (103.48 KB, 下載次數(shù): 276)
下載附件
2013-10-11 01:08 上傳
15-2 SPI通信時序圖(2)
在時序上,SPI是不是比I2C要簡單的多?沒有了起始、停止和應(yīng)答,UART和SPI在通信的時候,只負(fù)責(zé)通信,不管是否通信成功,而I2C卻要通過應(yīng)答信息來獲取通信成功失敗的信息,所以相對來說,UART和SPI的時序都要比I2C簡單一些。
15.3 實時時鐘芯片DS1302 本節(jié)課的DS1302是個實時時鐘芯片,我們可以用單片機(jī)寫入時間或者讀取當(dāng)前的時間數(shù)據(jù),我也會帶著大家通過閱讀這個芯片的數(shù)據(jù)手冊來學(xué)習(xí)和掌握這個器件。
由于IT技術(shù)國際化比較強(qiáng),因此數(shù)據(jù)手冊絕大多數(shù)都是英文的,導(dǎo)致很多英語基礎(chǔ)不好的同學(xué)看到英文手冊頭就大了。這里我要告訴大家的是,只要精神不退縮,方法總比困難多,很多英語水平不高的,看數(shù)據(jù)手冊照樣完全沒問題,因為我們的專業(yè)詞匯也就那么幾個,多看幾次就認(rèn)識了。我們現(xiàn)在不是考試,因此大家可以充分利用一些英文翻譯軟件,翻譯過來的中文意思有時候可能不是那么準(zhǔn)確,那你就把翻譯的內(nèi)容和英文手冊里的一些圖表比較參考學(xué)習(xí)。此外數(shù)據(jù)手冊除了介紹性的說明外,一般還會配相關(guān)的圖形或者表格,結(jié)合起來看也有利于理解手冊所表達(dá)的意思。這節(jié)課我會把DS1302的英文資料盡可能的用比較便于理解的方式給大家表達(dá)出來,同學(xué)們可以把我的表達(dá)和英文手冊多做一下對比,盡可能快的慢慢開始學(xué)會了解英文手冊。
15.3.1 DS1302的特點 DS1302是DALLAS(達(dá)拉斯)公司出的一款涓流充電時鐘芯片,2001年DALLAS被MAXIM(美信)收購,因此我們看到的DS1302的數(shù)據(jù)手冊既有DALLAS的標(biāo)志,又有MAXIM的標(biāo)志,大家了解即可。
DS1302實時時鐘芯片廣泛應(yīng)用于電話、傳真、便攜式儀器等產(chǎn)品領(lǐng)域,他的主要性能指標(biāo)如下:
1、DS1302是一個實時時鐘芯片,可以提供秒、分、小時、日期、月、年等信息,并且還有軟年自動調(diào)整的能力,可以通過配置AM/PM來決定采用24小時格式還是12小時格式。
2、擁有31字節(jié)數(shù)據(jù)存儲RAM。
3、串行I/O通信方式,相對并行來說比較節(jié)省IO口的使用。
4、DS1302的工作電壓比較寬,大概是2.0V~5.5V都可以正常工作。
5、DS1302這種時鐘芯片功耗一般都很低,它在工作電壓2.0V的時候,工作電流小于300nA。
6、DS1302共有8個引腳,有兩種封裝形式,一種是DIP-8封裝,芯片寬度(不含引腳)是300mil,一種是SOP-8封裝,有兩種寬度,一種是150mil,一種是208mil。我們看一下DS1302的引腳封裝圖,如圖15-3所示。
1.JPG (35.1 KB, 下載次數(shù): 272)
下載附件
2013-10-11 00:56 上傳
圖15-3 DS1302封裝圖
所謂的DIP封裝Dual In-line Package,也叫做雙列直插式封裝技術(shù),就如同我們開發(fā)板上的STC89C52RC單片機(jī),就是個典型的DIP封裝,當(dāng)然這個STC89C52RC還有其他的封裝,為了方便學(xué)習(xí)使用,我們采用的是DIP封裝。而74HC245、74HC138、24C02、DS1302我們用的都是SOP封裝Small Out-Line Package,是一種芯片兩側(cè)引出L形引腳的封裝技術(shù),大家可以看看開發(fā)板上的芯片,了解一下這些常識性知識。
7、當(dāng)供電電壓是5V的時候,兼容標(biāo)準(zhǔn)的TTL電平標(biāo)準(zhǔn),這里的意思是,可以完美的和單片機(jī)進(jìn)行通信。
8、由于DS1302是DS1202的升級版本,所以所有的功能都兼容DS1202。此外DS1302有兩個電源輸入,一個是主電源,另外一個是備用電源,比如可以用電池或者大電容,這樣是為了保證系統(tǒng)掉電的情況下,我們的時鐘還會繼續(xù)走。如果使用的是充電電池,還可以在正常工作時,設(shè)置充電功能,給我們的備用電池進(jìn)行充電。
DS1302的特點第二條“擁有31字節(jié)數(shù)據(jù)存儲RAM”,這是DS1302額外存在的資源。這31字節(jié)的RAM相當(dāng)于一個存儲器一樣,我們編寫單片機(jī)程序的時候,可以把我們想存儲的數(shù)據(jù)存儲在DS1302里邊,需要的時候讀出來,這塊功能和EEPROM有點類似,相當(dāng)于一個掉電丟失數(shù)據(jù)的“EEPROM”,如果我們的時鐘電路加上備用電池,那么這31個字節(jié)的RAM就可以替代EEPROM的功能了。這31字節(jié)的RAM功能使用很少,所以在這里我不講了,大家了解即可。
15.3.2 DS1302的硬件信息 我們平時所用的不管是單片機(jī),還是其他一些電子器件,根據(jù)使用條件的約束,可以分為商業(yè)級和工業(yè)級,DS1302的購買信息如下圖15-4所示。
2.JPG (60.18 KB, 下載次數(shù): 281)
下載附件
2013-10-11 00:56 上傳
圖15-4 DS1302訂購信息
我們在訂購DS1302的時候,就可以根據(jù)圖15-4所標(biāo)識的來跟銷售廠家溝通,商業(yè)級的工作電壓略窄,是0到70度,而工業(yè)級可以工作在零下40度到85度。TOP MARK就是指在芯片上印的字。
DS1302一共有8個引腳,下邊要根據(jù)引腳分布圖和典型電路圖來介紹一下每個引腳的功能,如圖15-5和圖15-6所示。
3.JPG (28.05 KB, 下載次數(shù): 285)
下載附件
2013-10-11 00:56 上傳
4.JPG (17.59 KB, 下載次數(shù): 279)
下載附件
2013-10-11 00:56 上傳
圖15-5 DS1302引腳圖 圖15-6 DS1302典型電路 1腳VCC2是主電源正極的引腳,2腳X1和3腳X2是晶振輸入和輸出引腳,4腳GND是負(fù)極,5腳CE是使能引腳,接單片機(jī)的IO口,6腳I/O是數(shù)據(jù)傳輸引腳,接單片機(jī)的IO口,7腳SCLK是通信時鐘引腳,接單片機(jī)的IO口,8腳VCC1是備用電源引腳?紤]到KST-51開發(fā)板是一套以學(xué)習(xí)為目的的板子,加上備用電池對航空運輸和攜帶不方便,所以8腳可以直接懸空,斷電后不需要DS1302再運行了,或者是在8腳接一個10uF的電容,經(jīng)過試驗可以運行1分鐘左右的時間,如果大家想運行時間再長,可以加大電容的容量,如圖15-7和圖15-8所示。
5.JPG (34.69 KB, 下載次數(shù): 286)
下載附件
2013-10-11 00:56 上傳
6.JPG (40.25 KB, 下載次數(shù): 11250)
下載附件
2013-10-11 00:56 上傳
圖15-7 DS1302無備用電源 圖15-8 DS1302電容作備用電源
涓流充電功能,課程也不講了,大家也作為選學(xué)即可,我們使用的時候直接用5V電源接一個二極管,在有主電源的情況下給電容充電,在主電源掉電的情況下,這個電容可以給DS1302大約供電1分鐘左右,這種電路的最大用處是在電池供電系統(tǒng)中更換主電池的時候保持實時時鐘的運行不中斷,1分鐘的時間對于更換電池足夠了。此外,通過我們的使用經(jīng)驗,在DS1302的主電源引腳串聯(lián)一個1K電阻可以有效的防止電源對DS1302的沖擊,R6就是,而R9,R26,R32都是上拉電阻。
我們把8個引腳功能分別介紹,如表15-1所示。
表15-1 DS1302引腳功能圖
引腳編號
| 引腳名稱
| 引腳功能
| 1
| Vcc2
| 主電源引腳,當(dāng)Vcc2比Vcc1高0.2V以上時,DS1302由VCC2供電,當(dāng)Vcc2低于Vcc1時,由Vcc1供電。
| 2
| X1
| 這兩個引腳需要接一個32.768K的晶振,給DS1302提供一個基準(zhǔn)。特別注意,要求這個晶振的引腳負(fù)載電容必須是6pF,而不是要加6pF的電容。如果使用有源晶振的話,接到X1上即可,X2懸空。
| 3
| X2
| 4
| GND
| 接地。
| 5
| CE
| DS1302的輸入引腳。當(dāng)讀寫DS1302的時候,這個引腳必須是高電平,DS1302這個引腳內(nèi)部有一個40k的下拉電阻。
| 6
| I/O
| 這個引腳是一個雙向通信引腳,讀寫數(shù)據(jù)都是通過這個引腳完成。DS1302這個引腳的內(nèi)部含有一個40k的下拉電阻。
| 7
| SCLK
| 輸入引腳。SCLK是用來作為通信的時鐘信號。DS1302這個引腳的內(nèi)部含有一個40k的下拉電阻。
| 8
| Vcc1
| 備用電源引腳。
| DS1302的電路一個重點就是時鐘電路,它所使用的晶振是一個32.768k的晶振,晶振外部也不需要額外添加其他的電容或者電阻電路了。時鐘的精度,首先取決于晶振的精度以及晶振的引腳負(fù)載電容。如果晶振不準(zhǔn)或者負(fù)載電容過大過小,都會導(dǎo)致時鐘誤差過大。在這一切都搞定后,最終一個考慮因素是晶振的溫漂。隨著溫度的變化,晶振往往精度會發(fā)生變化,因此,在實際的系統(tǒng)中,其中一種方法就是經(jīng)常校對。比如我們所用的電腦的時鐘,通常我們會設(shè)置一個選項“將計算機(jī)設(shè)置于internet時間同步”。選中這個選項后,一般可以過一段時間,我們的計算機(jī)就會和internet時間校準(zhǔn)同步一次。
15.3.3 DS1302寄存器介紹 DS1302的一條指令一個字節(jié)8位,其中第七位(即最高位)是固定1,這一位如果是0的話,那寫進(jìn)去是無效的。第六位是選擇RAM還是CLOCK的,我前邊說過,我們這里主要講CLOCK時鐘的使用,它的RAM功能我們不用,所以如果選擇CLOCK功能,第六位是0,如果要用RAM,那第六位就是1。從第五到第一位,決定了寄存器的5位地址,而第零位是讀寫位,如果要寫,這一位就是0,如果要讀,這一位就是1,如圖15-9所示。
7.JPG (14.25 KB, 下載次數(shù): 291)
下載附件
2013-10-11 00:56 上傳
圖15-9 DS1302命令字節(jié)
DS1302時鐘的寄存器,其中8個和時鐘有關(guān)的,5位地址分別是00000一直到00111這8個地址,還有一個寄存器的地址是01000,這是涓流充電所用的寄存器,我們這里不講。在DS1302的數(shù)據(jù)手冊里的地址,直接把第七位、第六位和第零位值給出來了,所以指令就成了80H、81H那些了,最低位是1,那么表示讀,最低位是0表示寫,如圖15-10所示。
1.JPG (77.73 KB, 下載次數(shù): 322)
下載附件
2013-10-11 00:59 上傳
圖15-10 DS1302的時鐘寄存器
寄存器一:最高位CH是一個時鐘停止標(biāo)志位。如果我們的時鐘電路有備用電源部分,上電后,我們要先檢測一下這一位,如果這一位是0,那說明我們的時鐘在系統(tǒng)掉電后,由于備用電源的供給,時鐘是持續(xù)正常運行的;如果這一位是1,那么說明我們的時鐘在系統(tǒng)掉電后,時鐘部分不工作了。若我們的Vcc1懸空或者是電池沒電了,當(dāng)我們下次重新上電時,讀取這一位,那這一位就是1,我們可以通過這一位判斷時鐘在單片機(jī)系統(tǒng)掉電后是否持續(xù)運行。剩下的7位高3位是秒的十位,低4位是秒的個位,這里注意再提一次,DS1302內(nèi)部是BCD碼,而秒的十位最大是5,所以3個二進(jìn)制位就夠了。
寄存器二:bit7沒意義,剩下的7位高3位是分鐘的十位,低4位是分鐘的個位。
寄存器三:bit7是1的話代表是12小時制,是0的話代表是24小時制,bit6固定是0,bit5在12小時制下0代表的是上午,1代表的是下午,在24小時制下和bit4一起代表了小時的十位,低4位代表的是小時的個位。
寄存器四:高2位固定是0,bit5和bit4是日期的十位,低4位是日期的個位。
寄存器五:高3位固定是0,bit4是月的十位,低4位是月的個位。
寄存器六:高5位固定是0,低3位代表了星期。
寄存器七:高4位代表了年的十位,低4位代表了年的個位。這里特別注意,這里的00到99年指的是2000年到2099年。
寄存器八:bit7是一個保護(hù)位,如果這一位是1,那么是禁止給任何其他的寄存器或者那31個字節(jié)的RAM寫數(shù)據(jù)的。因此在寫數(shù)據(jù)之前,這一位必須先寫成0。
15.3.4 DS1302通信時序介紹DS1302我們前邊也有提起過,是三根線,分別是CE、I/O和SCLK,其中CE是使能線,SCLK是時鐘線,I/O是數(shù)據(jù)線。前邊我們學(xué)過SPI通信,同學(xué)們發(fā)現(xiàn)沒發(fā)現(xiàn),這個DS1302的通信線定義和SPI怎么這么像呢?
事實上,DS1302的通信是SPI的變異種類,它用了SPI的通信時序,但是通信的時候沒有完全按照SPI的規(guī)則來,下面我們一點點解剖一下DS1302的變異SPI通信方式。
先看一下單字節(jié)寫入操作,如圖15-11所示。
2.JPG (26.49 KB, 下載次數(shù): 272)
下載附件
2013-10-11 00:59 上傳
圖15-11 DS1302單字節(jié)寫操作
然后我們在對比一下再對比一下CPOL=0并且CPHA=0的情況下的SPI的操作時序,如圖15-12所示。
3.JPG (26.77 KB, 下載次數(shù): 246)
下載附件
2013-10-11 00:59 上傳
圖15-12 CPOL=0/CPHA=0通信時序
圖15-11和圖15-12的通信時序,其中CE和SSEL的使能控制是反的,對于通信寫數(shù)據(jù),都是在SCK的上升沿,從機(jī)進(jìn)行采樣,下降沿的時候,主機(jī)發(fā)送數(shù)據(jù)。DS1302的時序里,單片機(jī)要預(yù)先寫一個字節(jié)指令,指明要寫入的寄存器的地址以及后續(xù)的操作是寫操作,然后再寫入一個字節(jié)的數(shù)據(jù)。
對于單字節(jié)讀操作,我就不做對比了,把DS1302的時序圖貼出來給大家看一下,如圖15-13所示。
4.JPG (24.81 KB, 下載次數(shù): 265)
下載附件
2013-10-11 00:59 上傳
圖15-13 DS1302單字節(jié)讀操作
讀操作有兩處特別注意的地方。第一,DS1302的時序圖上的箭頭都是針對DS1302來說的,因此讀操作的時候,先寫第一個字節(jié)指令,上升沿的時候DS1302來鎖存數(shù)據(jù),下降沿我們用單片機(jī)發(fā)送數(shù)據(jù)。到了第二個字?jǐn)?shù)據(jù),由于我們這個時序過程相當(dāng)于CPOL=0/CPHA=0,前沿發(fā)送數(shù)據(jù),后沿讀取數(shù)據(jù),第二個字節(jié)是DS1302下降沿輸出數(shù)據(jù),我們的單片機(jī)上升沿來讀取,因此箭頭從DS1302角度來說,出現(xiàn)在了下降沿。
第二個需要注意的地方就是,我們的單片機(jī)沒有標(biāo)準(zhǔn)的SPI接口,和I2C一樣需要用IO口來模擬通信過程。在讀DS1302的時候,理論上SPI是上升沿讀取,但是我們的程序是用IO口模擬的,所以數(shù)據(jù)的讀取和時鐘沿的變化不可能同時了,必然就有一個先后順序。通過實驗發(fā)現(xiàn),如果先讀取IO線上的數(shù)據(jù),再拉高SCLK產(chǎn)生上升沿,那么讀到的數(shù)據(jù)一定是正確的,而顛倒順序后數(shù)據(jù)就有可能出錯。這個問題產(chǎn)生的原因還是在于DS1302的通信協(xié)議與標(biāo)準(zhǔn)SPI協(xié)議存在的差異造成的,如果是標(biāo)準(zhǔn)SPI的數(shù)據(jù)線,數(shù)據(jù)會一直保持到下一個周期的下降沿才會變化,所以讀取數(shù)據(jù)和上升沿的先后順序就無所謂了;但DS1302的IO線會在時鐘上升沿后被DS1302釋放,也就是撤銷強(qiáng)推挽輸出變?yōu)槿跸吕瓲顟B(tài),而此時在51單片機(jī)引腳內(nèi)部上拉的作用下,IO線上的實際電平會慢慢上升,從而導(dǎo)致在上升沿產(chǎn)生后再讀取IO數(shù)據(jù)的話就可能出錯。因此這里的程序我們按照先讀取IO數(shù)據(jù),再拉高SCLK產(chǎn)生上升沿的順序。
下面我們就寫一個程序,先將2013年10月8號星期二12點30分00秒這個時間寫到DS1302內(nèi)部,讓DS1302正常運行,然后在不停的讀取DS1302的當(dāng)前時間,并顯示在我們的液晶屏上
/***********************lcd1602.c文件程序源代碼*************************/ 略 /***********************main.c文件程序源代碼*************************/
#include <reg52.h>
sbit DS1302_CE = P1^7; //DS1302片選引腳 sbit DS1302_CK = P3^5; //DS1302通信時鐘引腳 sbit DS1302_IO = P3^4; //DS1302通信數(shù)據(jù)引腳
bit flag200ms = 0; //200ms定時標(biāo)志 unsigned char T0RH = 0; //T0重載值的高字節(jié) unsigned char T0RL = 0; //T0重載值的低字節(jié)
void ConfigTimer0(unsigned int ms); void DS1302Init(void); unsigned char DS1302SingleRead(unsignedchar reg); extern void LcdInit(); extern void LcdShowStr(unsigned char x,unsigned char y, const unsigned char *str);
void main () { unsigned char i; unsigned char psec = 0xAA; //保存上一次讀取的秒數(shù),初值AA可以保證首次讀取時間后必定刷新顯示 unsigned char time[8]; //當(dāng)前時間數(shù)組 unsigned char str[12]; //字符串轉(zhuǎn)換緩沖區(qū)
LcdInit(); //初始化液晶 DS1302Init(); //初始化實時時鐘 ConfigTimer0(1); //T0定時1ms EA = 1; //開總中斷
while(1) { if (flag200ms) //每200ms讀取依次時間 { flag200ms = 0; for (i=0; i<7; i++) //讀取DS1302當(dāng)前時間 { time[ i] = DS1302SingleRead(i); } if (psec != time[0]) //檢測到時間有變化時刷新顯示 { str[0] = '2'; //添加年份的高2位:20 str[1] = '0'; str[2] = (time[6] >> 4) +'0';//“年”高位數(shù)字轉(zhuǎn)換為ASCII碼 str[3] = (time[6]&0x0F) +'0';//“年”低位數(shù)字轉(zhuǎn)換為ASCII碼 str[4] = '-'; //添加日期分隔符 str[5] = (time[4] >> 4) +'0'; //“月” str[6] = (time[4]&0x0F) +'0'; str[7] = '-'; str[8] = (time[3] >> 4) +'0'; //“日” str[9] = (time[3]&0x0F) +'0'; str[10] = '\0'; LcdShowStr(0, 0, str); //顯示到液晶的第一行
str[0] = (time[5]&0x0F) +'0'; //“星期” str[1] = '\0'; LcdShowStr(11, 0,"week"); LcdShowStr(15, 0, str); //顯示到液晶的第一行
str[0] = (time[2] >> 4) +'0'; //“時” str[1] = (time[2]&0x0F) +'0'; str[2] = ':'; //添加時間分隔符 str[3] = (time[1] >> 4) +'0'; //“分” str[4] = (time[1]&0x0F) +'0'; str[5] = ':'; str[6] = (time[0] >> 4) +'0'; //“秒” str[7] = (time[0]&0x0F) +'0'; str[8] = '\0'; LcdShowStr(4, 1, str); //顯示到液晶的第二行
psec = time[0]; //用當(dāng)前值更新上次秒數(shù) } } } }
void DS1302ByteWrite(unsigned chardat) //發(fā)送一個字節(jié)到DS1302通信總線上 { unsigned char mask;
for (mask=0x01; mask!=0; mask<<=1) //低位在前,逐位移出 { if ((mask&dat) != 0) //首先輸出該位數(shù)據(jù) { DS1302_IO = 1; } else { DS1302_IO = 0; } DS1302_CK = 1; //然后拉高時鐘 DS1302_CK = 0; //再拉低時鐘,完成一個位的操作 } DS1302_IO = 1; //最后確保釋放IO引腳 } unsigned char DS1302ByteRead(void) //由DS1302通信總線上讀取一個字節(jié) { unsigned char mask; unsigned char dat = 0;
for (mask=0x01; mask!=0; mask<<=1) //低位在前,逐位讀取 { if (DS1302_IO != 0) //首先讀取此時的IO引腳,并設(shè)置dat中的對應(yīng)位 { dat |= mask; } DS1302_CK = 1; //然后拉高時鐘 DS1302_CK = 0; //再拉低時鐘,完成一個位的操作 } return dat; //最后返回讀到的字節(jié)數(shù)據(jù) } void DS1302SingleWrite(unsigned char reg,unsigned char dat) //用單次模式向DS1302的某一寄存器寫入一字節(jié)數(shù)據(jù),寄存器地址reg,待寫入字節(jié)dat { DS1302_CE = 1; //使能片選信號 DS1302ByteWrite((reg<<1) | 0x80); //發(fā)送寫寄存器指令,左移空出來最低位讀寫位 DS1302ByteWrite(dat); //寫入字節(jié)數(shù)據(jù) DS1302_CE = 0; //除能片選信號 } unsigned char DS1302SingleRead(unsignedchar reg) //用單次模式從DS1302的某一寄存器讀取一字節(jié)數(shù)據(jù),寄存器地址reg,返回值為讀取到的字節(jié)數(shù)據(jù) { unsigned char dat;
DS1302_CE = 1; //使能片選信號 DS1302ByteWrite((reg<<1) | 0x81); //發(fā)送讀寄存器指令 dat = DS1302ByteRead(); //讀取字節(jié)數(shù)據(jù) DS1302_CE = 0; //除能片選信號
return dat; } void DS1302Init(void) //DS1302初始化 { unsigned char i; unsigned char code InitTime[] = {0x00,0x30,0x12, 0x08, 0x10, 0x02,0x13}; //2013年10月8日星期二 12:30:00
DS1302_CE = 0; //初始化DS1302通信引腳 DS1302_CK = 0; i= DS1302SingleRead(0); //讀取秒寄存器 if ((i & 0x80) != 0) //由秒寄存器最高位CH的值判斷DS1302是否已停止 { DS1302SingleWrite(7, 0x00); //撤銷寫保護(hù)以允許寫入數(shù)據(jù) for (i=0; i<7; i++) //設(shè)置DS1302為默認(rèn)的初始時間 { DS1302SingleWrite(i, InitTime[ i]); } } }
void ConfigTimer0(unsigned int ms) //T0配置函數(shù) { unsigned long tmp;
tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 12; //修正中斷響應(yīng)延時造成的誤差
T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0為模式1 TH0 = T0RH; //加載T0重載值 TL0 = T0RL; ET0 = 1; //使能T0中斷 TR0 = 1; //啟動T0 } void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù) { static unsigned char tmr200ms = 0;
TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; tmr200ms++; if (tmr200ms >= 200) //定時200ms { tmr200ms = 0; flag200ms = 1; } }
前邊學(xué)習(xí)了EEPROM的讀寫,因此DS1302的讀寫底層時序的程序應(yīng)該沒有什么問題,我就不過多解釋了,大家自己認(rèn)真揣摩一下。
15.3.5 DS1302的BURST模式
進(jìn)行產(chǎn)品開發(fā)的時候,邏輯的嚴(yán)謹(jǐn)性非常重要,如果一個產(chǎn)品或者程序邏輯上不嚴(yán)謹(jǐn),就有可能出現(xiàn)功能上的錯誤。比如我們15.3.4節(jié)里的這個程序,我們再回顧一下。當(dāng)單片機(jī)定時器時間到了200ms后,我們連續(xù)把DS1302的時間參數(shù)的7個字節(jié)讀了出來。但是不管怎么讀,都會有一個時間差,在極端的情況下就會出現(xiàn)這樣一種情況:假如我們當(dāng)前的時間是00:00:59,我們先讀秒,讀到的秒是59,然后再去讀分鐘,而就在讀完秒到還未開始讀分鐘的這段時間內(nèi),剛好時間進(jìn)位了,變成了00:01:00這個時間,我們讀到的分鐘就是01,顯示在液晶上就會出現(xiàn)一個00:01:59,這個時間很明顯是錯誤的。出現(xiàn)這個問題的概率極小,但確實實實在在可能存在的。
為了解決這個問題,芯片廠家肯定要給我們提供一種解決方案,這就是DS1302的突發(fā)模式。突發(fā)模式也分為RAM突發(fā)模式和時鐘突發(fā)模式,RAM部分我們不講,我們只看和時鐘相關(guān)的clock burst mode。
當(dāng)我們寫指令到DS1302的時候,只要我們將要寫的5位地址全部寫1,即讀操作用0xBF,寫操作用0xBE,這樣的指令送給DS1302之后,它就會自動識別出來是burst模式,馬上把所有的8個字節(jié)同時鎖存到另外的8個字節(jié)的寄存器緩沖區(qū)內(nèi),這樣時鐘繼續(xù)走,而我們讀數(shù)據(jù)是從另外一個緩沖區(qū)內(nèi)讀取的。同樣的道理,如果我們用burst模式寫數(shù)據(jù),那么我們也是先寫到這個緩沖區(qū)內(nèi),最終DS1302會把這個緩沖區(qū)內(nèi)的數(shù)據(jù)一次性送到他的時鐘寄存器內(nèi)。
要注意的是,不管讀寫,只要使用時鐘的burst模式,則必須一次性讀寫8個寄存器,要把時鐘的寄存器完全讀出來或者完全寫進(jìn)去。
下邊就提供一個burst模式的例程給大家學(xué)習(xí)一下。
/***********************lcd1602.c文件程序源代碼*************************/ 略 /***********************main.c文件程序源代碼*************************/
#include <reg52.h>
sbit DS1302_CE = P1^7; //DS1302片選引腳 sbit DS1302_CK = P3^5; //DS1302通信時鐘引腳 sbit DS1302_IO = P3^4; //DS1302通信數(shù)據(jù)引腳
bit flag200ms = 0; //200ms定時標(biāo)志 unsigned char T0RH = 0; //T0重載值的高字節(jié) unsigned char T0RL = 0; //T0重載值的低字節(jié)
void ConfigTimer0(unsigned int ms); void DS1302Init(void); void DS1302BurstRead(unsigned char *dat); extern void LcdInit(); extern void LcdShowStr(unsigned char x,unsigned char y, const unsigned char *str);
void main () { unsigned char psec = 0xAA; //保存上一次讀取的秒數(shù),初值AA可以保證首次讀取時間后必定刷新顯示 unsigned char time[8]; //當(dāng)前時間數(shù)組 unsigned char str[12]; //字符串轉(zhuǎn)換緩沖區(qū)
LcdInit(); //初始化液晶 DS1302Init(); //初始化實時時鐘 ConfigTimer0(1); //T0定時1ms EA = 1; //開總中斷
while(1) { if (flag200ms) //每200ms讀取依次時間 { flag200ms = 0; DS1302BurstRead(time); //讀取DS1302當(dāng)前時間 if (psec != time[0]) //檢測到時間有變化時刷新顯示 { str[0] = '2'; //添加年份的高2位:20 str[1] = '0'; str[2] = (time[6] >> 4) +'0';//“年”高位數(shù)字轉(zhuǎn)換為ASCII碼 str[3] = (time[6]&0x0F) +'0';//“年”低位數(shù)字轉(zhuǎn)換為ASCII碼 str[4] = '-'; //添加日期分隔符 str[5] = (time[4] >> 4) +'0'; //“月” str[6] = (time[4]&0x0F)+ '0'; str[7] = '-'; str[8] = (time[3] >> 4) +'0'; //“日” str[9] = (time[3]&0x0F) +'0'; str[10] = '\0'; LcdShowStr(0, 0, str); //顯示到液晶的第一行
str[0] = (time[5]&0x0F) +'0'; //“星期” str[1] = '\0'; LcdShowStr(11, 0,"week"); LcdShowStr(15, 0, str); //顯示到液晶的第一行
str[0] = (time[2] >> 4) +'0'; //“時” str[1] = (time[2]&0x0F) +'0'; str[2] = ':'; //添加時間分隔符 str[3] = (time[1] >> 4) +'0'; //“分” str[4] = (time[1]&0x0F) +'0'; str[5] = ':'; str[6] = (time[0] >> 4) +'0'; //“秒” str[7] = (time[0]&0x0F) +'0'; str[8] = '\0'; LcdShowStr(4, 1, str); //顯示到液晶的第二行
psec = time[0]; //用當(dāng)前值更新上次秒數(shù) } } } }
void DS1302ByteWrite(unsigned chardat) //發(fā)送一個字節(jié)到DS1302通信總線上 { unsigned char mask;
for (mask=0x01; mask!=0; mask<<=1) //低位在前,逐位移出 { if ((mask&dat) != 0) //首先輸出該位數(shù)據(jù) { DS1302_IO = 1; } else { DS1302_IO = 0; } DS1302_CK = 1; //然后拉高時鐘 DS1302_CK = 0; //再拉低時鐘,完成一個位的操作 } DS1302_IO = 1; //最后確保釋放IO引腳 } unsigned char DS1302ByteRead(void) //由DS1302通信總線上讀取一個字節(jié) { unsigned char mask; unsigned char dat = 0;
for (mask=0x01; mask!=0; mask<<=1) //低位在前,逐位讀取 { if (DS1302_IO != 0) //首先讀取此時的IO引腳,并設(shè)置dat中的對應(yīng)位 { dat |= mask; } DS1302_CK = 1; //然后拉高時鐘 DS1302_CK = 0; //再拉低時鐘,完成一個位的操作 } return dat; //最后返回讀到的字節(jié)數(shù)據(jù) } void DS1302SingleWrite(unsigned char reg,unsigned char dat) //用單次模式向DS1302的某一寄存器寫入一字節(jié)數(shù)據(jù),寄存器地址reg,待寫入字節(jié)dat { DS1302_CE = 1; //使能片選信號 DS1302ByteWrite((reg<<1) | 0x80); //發(fā)送寫寄存器指令 DS1302ByteWrite(dat); //寫入字節(jié)數(shù)據(jù) DS1302_CE = 0; //除能片選信號 } unsigned char DS1302SingleRead(unsignedchar reg) //用單次模式從DS1302的某一寄存器讀取一字節(jié)數(shù)據(jù),寄存器地址reg,返回值為讀取到的字節(jié)數(shù)據(jù) { unsigned char dat;
DS1302_CE = 1; //使能片選信號 DS1302ByteWrite((reg<<1) | 0x81); //發(fā)送讀寄存器指令 dat = DS1302ByteRead(); //讀取字節(jié)數(shù)據(jù) DS1302_CE = 0; //除能片選信號
return dat; } void DS1302BurstWrite(unsigned char*dat) //用突發(fā)模式向DS1302連續(xù)寫入8個寄存器數(shù)據(jù),待寫入數(shù)據(jù)指針dat { unsigned char i;
DS1302_CE = 1; DS1302ByteWrite(0xBE); //發(fā)送突發(fā)寫寄存器指令 for (i=0; i<8; i++) //連續(xù)寫入8字節(jié)數(shù)據(jù) { DS1302ByteWrite(dat[ i]); } DS1302_CE = 0; } void DS1302BurstRead(unsigned char*dat) //用突發(fā)模式從DS1302連續(xù)讀取8個寄存器的數(shù)據(jù),數(shù)據(jù)接收指針dat { unsigned char i;
DS1302_CE = 1; DS1302ByteWrite(0xBF); //發(fā)送突發(fā)讀寄存器指令 for (i=0; i<8; i++) //連續(xù)讀取8個字節(jié) { dat[ i] = DS1302ByteRead(); } DS1302_CE = 0; } void DS1302Init(void) //DS1302初始化 { unsigned char dat; unsigned char code InitTime[] = {0x00,0x30,0x12, 0x08, 0x10, 0x02, 0x13,0x00}; //2013年10月8日星期二 12:30:00
DS1302_CE = 0; //初始化DS1302通信引腳 DS1302_CK = 0; dat = DS1302SingleRead(0); //讀取秒寄存器 if ((dat & 0x80) != 0) //由秒寄存器最高位CH的值判斷DS1302是否已停止 { DS1302SingleWrite(7, 0x00); //撤銷寫保護(hù)以允許寫入數(shù)據(jù) DS1302BurstWrite(InitTime); //設(shè)置DS1302為默認(rèn)的初始時間 } }
void ConfigTimer0(unsigned int ms) //T0配置函數(shù) { unsigned long tmp;
tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 12; //修正中斷響應(yīng)延時造成的誤差
T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0為模式1 TH0 = T0RH; //加載T0重載值 TL0 = T0RL; ET0 = 1; //使能T0中斷 TR0 = 1; //啟動T0 } void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù) { static unsigned char tmr200ms = 0;
TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; tmr200ms++; if (tmr200ms >= 200) //定時200ms { tmr200ms = 0; flag200ms = 1; } }
15.4 結(jié)構(gòu)體數(shù)據(jù)類型
我們在前邊學(xué)數(shù)據(jù)類型的時候,主要是字符型、整型、浮點型等基本類型,而學(xué)數(shù)組的時候,數(shù)組的定義要求數(shù)組元素必須是想同的數(shù)據(jù)類型。在實際應(yīng)用中,有時候還需要把不同類型的數(shù)據(jù)組成一個有機(jī)的整體來處理,這些組合在一個整體中的數(shù)據(jù)之間還有一定的聯(lián)系,比如一個學(xué)生的姓名、性別、年齡、考試成績等,這就引入了復(fù)合數(shù)據(jù)類型。復(fù)合數(shù)據(jù)類型主要包含結(jié)構(gòu)體數(shù)據(jù)類型、共用體數(shù)據(jù)類型和枚舉體數(shù)據(jù)類型,我們本節(jié)主要要學(xué)習(xí)一下結(jié)構(gòu)體數(shù)據(jù)類型。
首先我們回顧一下上面的例程,我們把DS1302的7個字節(jié)的時間放到一個緩沖數(shù)組中,然后把數(shù)組中的值稍作轉(zhuǎn)換顯示到液晶上,這里就存在一個小問題,DS1302時間寄存器的定義并不是我們常用的“年月日時分秒”的順序,而是在中間加了一個字節(jié)的“星期幾”,而且每當(dāng)我要用這個時間的時候都要清楚的記得數(shù)組的第幾個元素表示的是什么,這樣一來,一是很容易出錯,而是程序的可讀性不強(qiáng)。當(dāng)然你可以把每一個元素都定一個明確的變量名字,這樣就不容易出錯也易讀了,但結(jié)構(gòu)上卻顯得很零散了。于是,我們就可以用結(jié)構(gòu)體來將這一組彼此相關(guān)的數(shù)據(jù)做一個封裝,他們既組成了一個整體,易讀不易錯。而且可以單獨定義其中每一個成員的數(shù)據(jù)類型,比如說把年份用unsigned int類型,即4個十進(jìn)制位來表示顯然比2位更符合日常習(xí)慣,而其它的類型還是可以用2位來表示。結(jié)構(gòu)體本身不是一個基本的數(shù)據(jù)類型,而是構(gòu)造的,它每個成員可以是一個基本的數(shù)據(jù)類型或者是一個構(gòu)造類型。結(jié)構(gòu)體既然是一種構(gòu)造而成的數(shù)據(jù)類型,那么在使用之前必須先定義它。
聲明結(jié)構(gòu)體變量的一般格式如下:
struct 結(jié)構(gòu)體名
{
類型1 變量名1;
類型2 變量名2;
...
類型n 變量名n;
} 結(jié)構(gòu)體變量名;
這種聲明方式僅僅是一個結(jié)構(gòu)體變量的聲明方式,但是在實際應(yīng)用中,可能需要多個具有相同形式的結(jié)構(gòu)體變量,這種定義方式就不是很方便了,因此我們推薦以下方式:
struct 結(jié)構(gòu)體名
{
類型1 變量名1;
類型2 變量名2;
...
類型n 變量名n;
} ;
struct 結(jié)構(gòu)體名 結(jié)構(gòu)體變量名1,結(jié)構(gòu)體變量名2,...結(jié)構(gòu)體變量名n;
為了方便大家理解,我來構(gòu)造一個實際的表示日期時間的結(jié)構(gòu)體。
struct sTime { //日期時間結(jié)構(gòu)體定義
unsigned int year; //年
unsigned char mon; //月
unsigned char day; //日
unsigned char hour; //時
unsigned char min; //分
unsigned char sec; //秒
unsigned char week; //星期
};
struct sTime bufTime;
Struct是結(jié)構(gòu)體類型的關(guān)鍵字,sTime是這個結(jié)構(gòu)體的名字,bufTime就是定義了一個具體的結(jié)構(gòu)體變量。那如果要給結(jié)構(gòu)體變量的成員賦值的話,寫法是
bufTime.year = 2013;
bufTime.mon = 10;
數(shù)組的元素也可以是結(jié)構(gòu)體類型,因此可以構(gòu)成結(jié)構(gòu)體數(shù)組,結(jié)構(gòu)體數(shù)組的每一個元素都是具有想同結(jié)構(gòu)類型的結(jié)構(gòu)體變量。例如我們前邊構(gòu)造的這個結(jié)構(gòu)類型,直接定義成struct sTime bufTime[3];就表示定義了一個結(jié)構(gòu)體數(shù)組,這個數(shù)組的3個元素,每一個都是一個結(jié)構(gòu)體變量。同樣的道理,結(jié)構(gòu)體數(shù)組中的元素的成員如果需要賦值,就可以寫成
bufTime[0].year = 2013;
bufTime[0].mon = 10;
一個指針變量如果指向了一個結(jié)構(gòu)體變量的時候,稱之為結(jié)構(gòu)指針變量。結(jié)構(gòu)指針變量中的值是指向的結(jié)構(gòu)體變量的首地址,通過結(jié)構(gòu)體指針也可以訪問到這個結(jié)構(gòu)變量。
結(jié)構(gòu)指針變量聲明的一般形式如下:
struct sTime *pbufTime;
這里要特別注意的是,使用結(jié)構(gòu)體指針對結(jié)構(gòu)體成員的訪問,和使用結(jié)構(gòu)體變量名對結(jié)構(gòu)體成員的訪問,其表達(dá)式有所不同。結(jié)構(gòu)體指針對結(jié)構(gòu)體成員的訪問表達(dá)式為
pbufTime->year = 2013; 或者是
(*pbufTime).year = 2013;
很明顯前者更簡潔,所以大都使用前者。
介紹結(jié)構(gòu)體數(shù)據(jù)類型要干嘛,毫無疑問要應(yīng)用在我們的程序中。下邊這個程序的功能相當(dāng)于一個萬年歷了,并且加入了按鍵調(diào)時功能。學(xué)有余力的同學(xué)看到這里,不妨先不看我提供的代碼,自己寫寫試試。如果能夠獨立寫一個按鍵可調(diào)的萬年歷程序,單片機(jī)可以說基本入門了。如果自己還不能夠獨立完成這個程序,那么還是老規(guī)矩,先抄并且理解,而后自己獨立默寫出來,并且要邊默寫邊理解。
本例直接忽略了星期這項內(nèi)容,通過上、下、左、右、回車、ESC這6個按鍵可以調(diào)整時間。簡單說一下這個程序的幾個要點,方便大家閱讀理解程序。
1、定義一個結(jié)構(gòu)體類型sTime用來封裝日期時間的各個元素,又用該結(jié)構(gòu)體定義了一個結(jié)構(gòu)體時間緩沖區(qū)變量bufTime來暫存從DS1302讀出的時間和設(shè)置時間時的設(shè)定值。需要注意的是在其它文件中要使用這個結(jié)構(gòu)體變量時,必須首先再聲明一次sTime類型;
2、定義一個變量setIndex來控制當(dāng)前是否處于設(shè)置時間的狀態(tài),以及設(shè)置時間的哪一位,該值為0就表示正常運行,1-12分別代表可以修改日期時間的12個位;
3、由于這節(jié)課的程序功能要進(jìn)行時間調(diào)整,用到了1602液晶的光標(biāo)功能,添加了設(shè)置光標(biāo)的函數(shù),我們要改變哪一位的數(shù)字,就在1602對應(yīng)位置上進(jìn)行光標(biāo)閃爍,所以Lcd1602.c程序和之前的有所不同;
4、時間的顯示、增減、設(shè)置移位等上層功能函數(shù)都放在main.c中來實現(xiàn),當(dāng)按鍵需要這些函數(shù)時則在按鍵文件中做外部聲明,這樣做是為了避免一組功能函數(shù)分散在不同的文件內(nèi)而使程序顯得凌亂。
/***********************lcd1602.c文件程序源代碼*************************/
#include <reg52.h>
sbit DS1302_CE = P1^7; //DS1302片選引腳 sbit DS1302_CK = P3^5; //DS1302通信時鐘引腳 sbit DS1302_IO = P3^4; //DS1302通信數(shù)據(jù)引腳
struct sTime { //日期時間結(jié)構(gòu)體定義 unsigned int year; //年 unsigned char mon; //月 unsigned char day; //日 unsigned char hour; //時 unsigned char min; //分 unsigned char sec; //秒 unsigned char week; //星期 };
void DS1302ByteWrite(unsigned chardat) //發(fā)送一個字節(jié)到DS1302通信總線上 { unsigned char mask;
for (mask=0x01; mask!=0; mask<<=1) //低位在前,逐位移出 { if ((mask&dat) != 0) //首先輸出該位數(shù)據(jù) { DS1302_IO = 1; } else { DS1302_IO = 0; } DS1302_CK = 1; //然后拉高時鐘 DS1302_CK = 0; //再拉低時鐘,完成一個位的操作 } DS1302_IO = 1; //最后確保釋放IO引腳 } unsigned char DS1302ByteRead(void) //由DS1302通信總線上讀取一個字節(jié) { unsigned char mask; unsigned char dat = 0;
for (mask=0x01; mask!=0; mask<<=1) //低位在前,逐位讀取 { if (DS1302_IO != 0) //首先讀取此時的IO引腳,并設(shè)置dat中的對應(yīng)位 { dat |= mask; } DS1302_CK = 1; //然后拉高時鐘 DS1302_CK = 0; //再拉低時鐘,完成一個位的操作 } return dat; //最后返回讀到的字節(jié)數(shù)據(jù) } void DS1302SingleWrite(unsigned char reg,unsigned char dat) //用單次模式向DS1302的某一寄存器寫入一字節(jié)數(shù)據(jù),寄存器地址reg,待寫入字節(jié)dat { DS1302_CE = 1; //使能片選信號 DS1302ByteWrite((reg<<1) | 0x80); //發(fā)送寫寄存器指令 DS1302ByteWrite(dat); //寫入字節(jié)數(shù)據(jù) DS1302_CE = 0; //除能片選信號 } unsigned char DS1302SingleRead(unsignedchar reg) //用單次模式從DS1302的某一寄存器讀取一字節(jié)數(shù)據(jù),寄存器地址reg,返回值為讀取到的字節(jié)數(shù)據(jù) { unsigned char dat;
DS1302_CE = 1; //使能片選信號 DS1302ByteWrite((reg<<1) | 0x81); //發(fā)送讀寄存器指令 dat = DS1302ByteRead(); //讀取字節(jié)數(shù)據(jù) DS1302_CE = 0; //除能片選信號
return dat; } void DS1302BurstWrite(unsigned char*dat) //用突發(fā)模式向DS1302連續(xù)寫入8個寄存器數(shù)據(jù),待寫入數(shù)據(jù)指針dat { unsigned char i;
DS1302_CE = 1; DS1302ByteWrite(0xBE); //發(fā)送突發(fā)寫寄存器指令 for (i=0; i<8; i++) //連續(xù)寫入8字節(jié)數(shù)據(jù) { DS1302ByteWrite(dat[ i]); } DS1302_CE = 0; } void DS1302BurstRead(unsigned char*dat) //用突發(fā)模式從DS1302連續(xù)讀取8個寄存器的數(shù)據(jù),數(shù)據(jù)接收指針dat { unsigned char i;
DS1302_CE = 1; DS1302ByteWrite(0xBF); //發(fā)送突發(fā)讀寄存器指令 for (i=0; i<8; i++) //連續(xù)讀取8個字節(jié) { dat[ i] = DS1302ByteRead(); } DS1302_CE = 0; } void GetRealTime(struct sTime *time) //獲取實時時間,即讀取DS1302的當(dāng)前時間 { unsigned char buf[8];
DS1302BurstRead(buf); time->year = buf[6] + 0x2000; time->mon = buf[4]; time->day = buf[3]; time->hour = buf[2]; time->min = buf[1]; time->sec = buf[0]; time->week = buf[5]; } void SetRealTime(struct sTime *time) //設(shè)定實時時間,即將當(dāng)前時間寫入DS1302 { unsigned char buf[8];
buf[7] = 0; buf[6] = time->year; buf[5] = time->week; buf[4] = time->mon; buf[3] = time->day; buf[2] = time->hour; buf[1] = time->min; buf[0] = time->sec; DS1302BurstWrite(buf); } void DS1302Init(void) //DS1302初始化 { unsigned char dat; struct sTime code InitTime[] = {0x2013,0x10,0x08, 0x12,0x30,0x00, 0x02};//2013年10月8日 12:30:00 星期二
DS1302_CE = 0; //初始化DS1302通信引腳 DS1302_CK = 0; dat = DS1302SingleRead(0); //讀取秒寄存器 if ((dat & 0x80) != 0) //由秒寄存器最高位CH的值判斷DS1302是否已停止 { DS1302SingleWrite(7, 0x00); //撤銷寫保護(hù)以允許寫入數(shù)據(jù) SetRealTime(&InitTime); //設(shè)置DS1302為默認(rèn)的初始時間 } }
/***********************main.c文件程序源代碼*************************/ #include <reg52.h>
struct sTime { //日期時間結(jié)構(gòu)體定義 unsigned int year; unsigned char mon; unsigned char day; unsigned char hour; unsigned char min; unsigned char sec; unsigned char week; };
struct sTime bufTime; //日期時間緩沖區(qū) unsigned char setIndex = 0; //時間設(shè)置索引
bit flag200ms = 1; //200ms定時標(biāo)志 unsigned char T0RH = 0; //T0重載值的高字節(jié) unsigned char T0RL = 0; //T0重載值的低字節(jié)
void ConfigTimer0(unsigned int ms); void RefreshTimeShow(); extern void DS1302Init(void); extern void GetRealTime(struct sTime*time); extern void SetRealTime(struct sTime*time); extern void KeyDrive(); extern void KeyScan(); extern void LcdInit(); extern void LcdShowStr(unsigned char x,unsigned char y, const unsigned char *str); extern void LcdSetCursor(unsigned char x,unsigned char y);
void main () { unsigned char psec = 0xAA; //保存上一次讀取的秒數(shù),初值AA可以保證首次讀取時間后必定刷新顯示
LcdInit(); //初始化液晶 DS1302Init(); //初始化實時時鐘 ConfigTimer0(1); //T0定時1ms EA = 1; //開總中斷
//初始化屏幕上固定不變的內(nèi)容 LcdShowStr(3, 0, "20 - - "); LcdShowStr(4, 1, " : : ");
while(1) { KeyDrive(); if (flag200ms && (setIndex == 0)) //每隔200ms且未處于設(shè)置狀態(tài)時, { flag200ms = 0; GetRealTime(&bufTime); //獲取當(dāng)前時間 if (psec != bufTime.sec) //檢測到時間有變化時刷新顯示 { RefreshTimeShow(); psec = bufTime.sec; //用當(dāng)前值更新上次秒數(shù) } } } }
void ShowBcdByte(unsigned char x, unsignedchar y, unsigned char bcd) //將一個BCD碼字節(jié)顯示到屏幕上 { unsigned char str[4];
str[0] = (bcd >> 4) + '0'; str[1] = (bcd&0x0F) + '0'; str[2] = '\0'; LcdShowStr(x, y, str); } void RefreshTimeShow() //刷新日期時間的顯示 { ShowBcdByte(5, 0, bufTime.year); ShowBcdByte(8, 0, bufTime.mon); ShowBcdByte(11, 0, bufTime.day); ShowBcdByte(4, 1, bufTime.hour); ShowBcdByte(7, 1, bufTime.min); ShowBcdByte(10, 1, bufTime.sec); } void RefreshSetShow() //刷新當(dāng)前設(shè)置位的光標(biāo)指示 { switch (setIndex) { case 1: LcdSetCursor(5, 0); break; case 2: LcdSetCursor(6, 0); break; case 3: LcdSetCursor(8, 0); break; case 4: LcdSetCursor(9, 0); break; case 5: LcdSetCursor(11, 0);break; case 6: LcdSetCursor(12, 0);break; case 7: LcdSetCursor(4, 1); break; case 8: LcdSetCursor(5, 1); break; case 9: LcdSetCursor(7, 1); break; case 10: LcdSetCursor(8, 1);break; case 11: LcdSetCursor(10, 1); break; case 12: LcdSetCursor(11, 1); break; default: break; } } unsigned char IncBcdHigh(unsigned charbcd) //遞增一個BCD碼的高位 { if ((bcd&0xF0) < 0x90) bcd += 0x10; //高位小于9,就在高位加1 else bcd &= 0x0F; //否則就把高位清零
return bcd; } unsigned char IncBcdLow(unsigned charbcd) //遞增一個BCD碼的低位 { if ((bcd&0x0F) < 0x09) bcd += 0x01; //小于9則加1, else bcd &= 0xF0; //否則直接清零
return bcd; } unsigned char DecBcdHigh(unsigned charbcd) //遞減一個BCD碼的高位 { if ((bcd&0xF0) > 0x00) bcd -= 0x10; else bcd |= 0x90;
return bcd; } unsigned char DecBcdLow(unsigned charbcd) //遞減一個BCD碼的低位 { if ((bcd&0x0F) > 0x00) bcd -= 0x01; else bcd |= 0x09;
return bcd; } void IncSetTime() //遞增時間當(dāng)前設(shè)置位的值 { switch (setIndex) { case 1: bufTime.year =IncBcdHigh(bufTime.year); break; case 2: bufTime.year =IncBcdLow(bufTime.year); break; case 3: bufTime.mon = IncBcdHigh(bufTime.mon); break; case 4: bufTime.mon = IncBcdLow(bufTime.mon); break; case 5: bufTime.day = IncBcdHigh(bufTime.day); break; case 6: bufTime.day = IncBcdLow(bufTime.day); break; case 7: bufTime.hour =IncBcdHigh(bufTime.hour); break; case 8: bufTime.hour =IncBcdLow(bufTime.hour); break; case 9: bufTime.min = IncBcdHigh(bufTime.min); break; case 10: bufTime.min =IncBcdLow(bufTime.min); break; case 11: bufTime.sec =IncBcdHigh(bufTime.sec); break; case 12: bufTime.sec =IncBcdLow(bufTime.sec); break; default: break; } RefreshTimeShow(); RefreshSetShow(); } void DecSetTime() //遞減時間當(dāng)前設(shè)置位的值 { switch (setIndex) { case 1: bufTime.year =DecBcdHigh(bufTime.year); break; case 2: bufTime.year =DecBcdLow(bufTime.year); break; case 3: bufTime.mon = DecBcdHigh(bufTime.mon); break; case 4: bufTime.mon = DecBcdLow(bufTime.mon); break; case 5: bufTime.day = DecBcdHigh(bufTime.day); break; case 6: bufTime.day = DecBcdLow(bufTime.day); break; case 7: bufTime.hour =DecBcdHigh(bufTime.hour); break; case 8: bufTime.hour =DecBcdLow(bufTime.hour); break; case 9: bufTime.min = DecBcdHigh(bufTime.min); break; case 10: bufTime.min =DecBcdLow(bufTime.min); break; case 11: bufTime.sec =DecBcdHigh(bufTime.sec); break; case 12: bufTime.sec =DecBcdLow(bufTime.sec); break; default: break; } RefreshTimeShow(); RefreshSetShow(); } void RightShiftTimeSet() //右移時間設(shè)置位 { if (setIndex != 0) { if (setIndex < 12) setIndex++; else setIndex = 1; RefreshSetShow(); } } void LeftShiftTimeSet() //左移時間設(shè)置位 { if (setIndex != 0) { if (setIndex > 1) setIndex--; else setIndex = 12; RefreshSetShow(); } }
void ConfigTimer0(unsigned int ms) //T0配置函數(shù) { unsigned long tmp;
tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 12; //修正中斷響應(yīng)延時造成的誤差
T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0為模式1 TH0 = T0RH; //加載T0重載值 TL0 = T0RL; ET0 = 1; //使能T0中斷 TR0 = 1; //啟動T0 } void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù) { static unsigned char tmr200ms = 0;
TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; KeyScan(); //按鍵掃描 tmr200ms++; if (tmr200ms >= 200) //定時200ms { tmr200ms = 0; flag200ms = 1; } }
15.5 作業(yè)
1、理解BCD碼的原理。
2、理解SPI的通信原理,SPI通信過程的四種模式配置。
3、能夠結(jié)合教程閱讀DS1302的英文數(shù)據(jù)手冊,學(xué)會DS1302的讀寫操作。
4、能夠獨立完成帶按鍵功能的萬年歷程序,并且將課程帶的上、下、左、右、回車、ESC這幾個按鍵的調(diào)時修改成為數(shù)字鍵、回車、ESC調(diào)時的功能。 |