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