本教材現(xiàn)以連載的方式由網(wǎng)絡(luò)發(fā)布,并將于2014年由清華大學(xué)出版社出版最終完整版,版權(quán)歸作者和清華大學(xué)出版社所有。本著開(kāi)源、分享的理念,本教材可以自由傳播及學(xué)習(xí)使用,但是務(wù)必請(qǐng)注明出處來(lái)自金沙灘工作室 .
我們課程到了這里,基本知識(shí)介紹完畢。如果同學(xué)們能夠認(rèn)真把前邊的“降龍十八章”領(lǐng)悟透徹,那剩下的主要工作就是不斷反復(fù)練習(xí)鞏固了。本章我們首先介紹實(shí)際項(xiàng)目開(kāi)發(fā)中的一些技巧和規(guī)范性的東西,然后帶領(lǐng)大家一起來(lái)做一個(gè)真正的項(xiàng)目,把項(xiàng)目開(kāi)發(fā)的整個(gè)流程都走一遍。
19.1 類型說(shuō)明
C語(yǔ)言不僅提供了豐富的數(shù)據(jù)類型給我們使用,而且還允許用戶自己定義類型說(shuō)明符,也就是說(shuō)為了方便,給已經(jīng)存在的數(shù)據(jù)類型起個(gè)“代號(hào)”,比如“9527就是你的終身代號(hào)”,就用9527來(lái)代表某個(gè)人。在C語(yǔ)言中,使用typedef即可完成這項(xiàng)功能,定義格式如下:typedef 原類型名 新類型名 typedef語(yǔ)句并未定義一種新的數(shù)據(jù)類型,他僅僅是給已經(jīng)有的數(shù)據(jù)類型取了一個(gè)更加簡(jiǎn)潔直觀的名字,可以用這個(gè)新的類型名字來(lái)定義變量。在實(shí)際開(kāi)發(fā)中,很多公司都會(huì)使用這個(gè)關(guān)鍵字來(lái)給變量類型取新名字,一是為了方便代碼的移植,還有就是為了代碼更加的簡(jiǎn)潔一些,比如以下的這幾種類型定義方式。 typedef signed char int8; // 8位有符號(hào)整型數(shù) typedef signed int int16; //16位有符號(hào)整型數(shù) typedef signed long int32; //32位有符號(hào)整型數(shù) typedef unsigned char uint8; // 8位無(wú)符號(hào)整型數(shù) typedef unsigned int uint16; //16位無(wú)符號(hào)整型數(shù) typedef unsigned long uint32; //32位無(wú)符號(hào)整型數(shù) 經(jīng)過(guò)以上的這種類型說(shuō)明后,今后我們?cè)诔绦蛑芯涂梢灾苯邮褂胾int8來(lái)替代unsigned char來(lái)定義變量。聰明的你,是否發(fā)現(xiàn)我們起的這個(gè)代號(hào),無(wú)符號(hào)型的前邊帶一個(gè)u,有符號(hào)的不帶u,int表示整數(shù)的意思,后邊的數(shù)字代表的是這個(gè)變量類型占的位數(shù),這種命名方式很多公司都采用,大家也可以學(xué)著采用這種方式。 有的時(shí)候也有用宏定義代替typedef的功能,但是宏定義是由預(yù)處理完成的,而typedef則是在編譯時(shí)完成的,后者更加靈活。我發(fā)現(xiàn)有的同學(xué)用這種定義方式: #define uchar unsigned char 這種方式不建議大家使用,在這種應(yīng)用下是沒(méi)問(wèn)題,但是當(dāng)用到指針的時(shí)候,就有可能出錯(cuò),在一些比較正規(guī)的公司如果寫(xiě)出這種形式可能會(huì)感覺(jué)寫(xiě)代碼的人比較初級(jí)。下面我們就介紹一下typedef和#define 之間的區(qū)別。 #define是預(yù)編譯處理命令,在編譯處理時(shí)進(jìn)行簡(jiǎn)單的替換,不做任何正確性檢查,不管含義是否正確都會(huì)被代入,比如: #define PI 3.1415926 有了這個(gè)宏,我們今后可以直接用PI來(lái)替代3.1415926了,比如我們寫(xiě)area = PI*r*r求圓的面積就會(huì)直接替換成3.1415926*r*r。如果我們不小心寫(xiě)成了3.1415g26,編譯的時(shí)候還是會(huì)代入。 typedef是在編譯時(shí)進(jìn)行處理的,它是在自己的作用域內(nèi)給一個(gè)已經(jīng)存在的類型起一個(gè)代號(hào),如果我們把前邊的類型說(shuō)明錯(cuò)誤的寫(xiě)成: typedef unsinged char uint8; 編譯器會(huì)直接報(bào)錯(cuò)。 對(duì)于#define來(lái)說(shuō),更多的應(yīng)用是進(jìn)行一些程序可讀性、易維護(hù)的替換。比如: #define LCD1602_DB P0 #define SYS_MCLK (11059200/12) 在寫(xiě)1602程序的過(guò)程中,我們可以直接用LCD1602_DB表示1602的通信總線,我們也可以直接用SYS_MCLK來(lái)作為我們單片機(jī)的機(jī)器周期,這樣如果改動(dòng)一些硬件,比如出于特定需要而換了其它頻率的晶振,那么我們可以直接在程序最開(kāi)始部分改一下即可,不用到處去修改數(shù)字了。 而對(duì)于類型說(shuō)明,有的情況下typedef和#define用法一樣,有的情況就不一樣了。 typedef unsigned char uint8; uint8 i, j; #define uchar unsigned char uchar i, j; 這兩種用法是完全相同的,等價(jià)的,沒(méi)有區(qū)別,不過(guò)大家要注意typedef后邊有分號(hào),而#define后邊是沒(méi)有分號(hào)的。 typedef int* int_p; int_p i, j; #define int_p int* int_p i, j; 這兩種用法得到的結(jié)果是不一樣的,其中第一種無(wú)疑是定義了i和j這兩個(gè)int指針變量。而第二種呢?因?yàn)?/font>define是直接替換,實(shí)際上就是int* i, j; 所以i是一個(gè)int指針變量,而j卻是一個(gè)普通的int變量。 總之,typedef是專門(mén)給類型重新起名的,而#define是純粹替換的,大家記住其用法。 19.2 頭文件
在前邊的章節(jié)中,我們多次使用過(guò)文件包含命令#include,這條指令的功能是將指定的被包含文件的全部?jī)?nèi)容插到該命令行的位置處,從而把指定文件和當(dāng)前的源程序文件連成一個(gè)源文件參與編譯,通常的寫(xiě)法如下:#include <文件名> 或者 #include ”文件名” 使用尖括號(hào)表示預(yù)處理程序直接到系統(tǒng)指定的“包含文件目錄”去查找,使用雙引號(hào)則表示預(yù)處理程序首先在當(dāng)前文件所在的文件目錄中查找被包含的文件,如果沒(méi)有找到才會(huì)再到系統(tǒng)的“包含文件目錄”去查找。一般情況下,我們的習(xí)慣是系統(tǒng)提供的頭文件用尖括號(hào)方式,我們用戶自己編寫(xiě)的頭文件用雙引號(hào)方式。 我們?cè)谇斑呌眠^(guò)很多次#include <reg52.h>,這個(gè)文件所在的位置是keil軟件安裝目錄的\C51\INC這個(gè)路徑內(nèi),大家可以去看看,在這個(gè)文件夾內(nèi),有很多系統(tǒng)自帶的頭文件,當(dāng)然也包含了<intrins.h>這個(gè)頭文件。當(dāng)我們一旦寫(xiě)了#include <reg52.h>這條指令后,那么相當(dāng)于在我們當(dāng)前的.C文件中,寫(xiě)下了以下的代碼。 #ifndef __REG52_H__ #define __REG52_H__
/* BYTE Registers */ sfr P0 = 0x80; sfr P1 = 0x90; sfr P2 = 0xA0; sfr P3 = 0xB0; ... ...
/* BIT Registers */ /* PSW */ sbit CY = PSW^7; sbit AC = PSW^6; sbit F0 = PSW^5; sbit RS1 = PSW^4; sbit RS0 = PSW^3; sbit OV = PSW^2; sbit P = PSW^0; //8052 only
/* TCON */ sbit TF1 = TCON^7; sbit TR1 = TCON^6; sbit TF0 = TCON^5; sbit TR0 = TCON^4; sbit IE1 = TCON^3; sbit IT1 = TCON^2; sbit IE0 = TCON^1; sbit IT0 = TCON^0; ... ...
#endif 我們之前在程序中,只要寫(xiě)了#include <reg52.h>這條指令,我們就可以隨便使用P0、TCON、TMOD這些寄存器和TR0、TR1、TI、RI等這些寄存器的位,都是因?yàn)樗鼈円呀?jīng)在這個(gè)頭文件中定義或聲明過(guò)了。 前邊我們講過(guò),要調(diào)用某個(gè)函數(shù),必須提前進(jìn)行聲明。而Keil自己做了很多函數(shù),生成了庫(kù)文件,我們?nèi)绻褂眠@些函數(shù)的時(shí)候,不需要寫(xiě)這些函數(shù)的代碼,而直接調(diào)用這些函數(shù)即可,調(diào)用之前首先要進(jìn)行聲明一下,而這些聲明也放在頭文件當(dāng)中。比如我們所用的_nop_();函數(shù),就是在<intrins.h>這個(gè)頭文件中。 在我們前邊應(yīng)用的實(shí)例中,很多文件中的所要用到的函數(shù),都是在其他文件中定義的,我們?cè)诋?dāng)前文件要調(diào)用它的時(shí)候,提前聲明一下即可。為了讓我們程序的易維護(hù)性和可移植性提高,我們自己就可以編寫(xiě)我們所需要的頭文件。我們自己編寫(xiě)的頭文件中不僅僅可以進(jìn)行函數(shù)的聲明,變量的外部聲明,一些宏定義也可以放在其中。 舉個(gè)例子,比如我們?cè)?font face="Times New Roman">main.c這個(gè)文件中,配套寫(xiě)了一個(gè)main.h文件。新建頭文件的方式也很簡(jiǎn)單,和.c是類似的,首先點(diǎn)擊新建文件的那個(gè)圖標(biāo),或者點(diǎn)擊菜單File->New,然后點(diǎn)擊保存文件,保存的時(shí)候命名為main.h即可。為了方便我們編寫(xiě)、修改維護(hù),我們?cè)?/font>Keil編程環(huán)境中新建一個(gè)頭文件組,把所有的源文件放在一個(gè)組內(nèi),把所有的頭文件放在一個(gè)組內(nèi),如圖19-1所示。
psb(36).jpeg (22.98 KB, 下載次數(shù): 167)
下載附件
2013-11-28 19:58 上傳
圖19-1 工程文件分組管理 大家注意,main.h里除了要包含main.c所要使用的一些宏之外,還要在里邊對(duì)main.c文件中所定義的全局變量,進(jìn)行extern聲明,提供給其他的.c文件使用,還要把main.c內(nèi)的自定義類型進(jìn)行聲明,還要把main.c內(nèi)所使用的全局函數(shù)進(jìn)行聲明,方便給其他文件調(diào)用。比如我們把main.h文件寫(xiě)成下邊這樣。 enum eStaSystem { //系統(tǒng)運(yùn)行狀態(tài)枚舉 E_NORMAL, E_SET_TIME, E_SET_ALARM };
extern enum eStaSystem staSystem;
void RefreshTemp(uint8 ops); void ConfigTimer0(uint16 ms); 首先大家注意,對(duì)于函數(shù)的外部聲明,extern是可以省略的,但是對(duì)于外部變量的聲明是不能省略的。其次enum是一個(gè)枚舉體,前邊我們已經(jīng)提到過(guò)了,大家可以再把書(shū)翻回去了解一下枚舉體的作用和結(jié)構(gòu)。我們?cè)?/font>main.c當(dāng)中定義的staSystem其他文件中要調(diào)用,在這里就要用extern聲明一下。 頭文件這樣編寫(xiě)看似沒(méi)問(wèn)題,實(shí)際上則不然。首先第一個(gè)比較明顯的問(wèn)題,由于所有的源文件都有可能要包含這個(gè)main.h,同樣main.c也會(huì)包含它,而staSystem這個(gè)枚舉變量是在main.c中定義的,所以當(dāng)main.h被main.c包含時(shí)就不需要進(jìn)行外部聲明,而被其它文件包含時(shí)則應(yīng)進(jìn)行這個(gè)聲明。此外,在我們的程序編寫(xiě)過(guò)程中,經(jīng)常會(huì)遇到頭文件包含頭文件的用法,假設(shè)a.h包含了main.h文件,b.h文件同樣也包含了main.h文件,如果現(xiàn)在有一個(gè)c文件1602.c文件既包含了a.h又包含了b.h,這樣就會(huì)出現(xiàn)頭文件的重復(fù)包含,從而會(huì)發(fā)生變量函數(shù)等的重復(fù)聲明,因此我們C語(yǔ)言還有一個(gè)知識(shí)點(diǎn)叫做條件編譯。
19.3 條件編譯
條件編譯屬于預(yù)處理程序,包括我們之前講的宏,都是程序在編譯之前的一些必要的處理過(guò)程,這些都不是實(shí)際程序功能代碼,而僅僅是告訴編譯器需要進(jìn)行的特定操作等。 條件編譯通常有三種用法,第一種表達(dá)式: #if 表達(dá)式 程序段 1 #else 程序段 2 #endif 作用:如果表達(dá)式的值為“真”(非0),則編譯程序段1,否則,編譯程序段2。在使用中,表達(dá)式通常是一個(gè)常量,我們通常事先用宏來(lái)進(jìn)行聲明,通過(guò)宏聲明的值來(lái)確定到底執(zhí)行哪段程序。 比如我們公司開(kāi)發(fā)了同類的兩款產(chǎn)品,這兩款產(chǎn)品的功能有一部分是相同的,有一部分是不同的,同樣所編寫(xiě)的程序代碼大部分的代碼是一樣的,只有一少部分有區(qū)別。這個(gè)時(shí)候?yàn)榱朔奖愠绦虻木S護(hù),可以把兩款產(chǎn)品的代碼寫(xiě)到同一個(gè)工程程序中,然后把其中有區(qū)別的功能利用條件編譯。 #define PLAN 0 #if (PLAN == 0) 程序段1 #else 程序段2 #endif 這樣寫(xiě)之后,當(dāng)我們要編譯款式1的時(shí)候,把PLAN宏聲明成0即可,當(dāng)我們要編譯款式2的時(shí)候,把宏聲明的值改為1或其它值即可。 第二種表達(dá)式和第三種表達(dá)式是類似的,使用哪一種完全看個(gè)人喜好,但是所有的程序最好統(tǒng)一。 表達(dá)式二: #ifdef 標(biāo)識(shí)符 程序段1 #else 程序段2 #endif 表達(dá)式三: #ifndef 標(biāo)識(shí)符 程序段1 #else 程序段2 #endif 在本章的示例中我們使用到了表達(dá)式三,表達(dá)式三的作用是:如果標(biāo)識(shí)符沒(méi)有被#define命令所聲明過(guò),則編譯程序段1,否則則編譯程序段2。此外,命令中的#else部分是可以省略的。表達(dá)式二和表達(dá)式三正好相反,大家自己看一下吧。其實(shí)#ifndef就是if no define的縮寫(xiě)。 在頭文件的編寫(xiě)過(guò)程中,為了防止命名的錯(cuò)亂,我們每個(gè).c文件對(duì)應(yīng)的.h文件,除名字一致外,進(jìn)行宏聲明的時(shí)候,也用這個(gè)頭文件的名字,并且大寫(xiě),在中間加上下劃線,比如我們這個(gè)main.h的結(jié)構(gòu),我們首先要這樣寫(xiě): #ifndef _MAIN_H #define _MAIN_H
程序段1
#endif 這樣說(shuō)明的意思是,如果這個(gè)_MAIN_H沒(méi)有聲明,那么我們就聲明_MAIN_H,并且我們的程序段1是有效的,最終結(jié)束;那么如果_MAIN_H已經(jīng)聲明過(guò)了,那么我們也就不用在聲明了,同時(shí)程序段1也就不必要再有效了。這樣就可以有效的解決了a.h包含了main.h后,b.h中既包含main.h,而1602.c既包含a.h又包含b.h所帶來(lái)的尷尬。 第二個(gè)問(wèn)題是,main.c文件中定義的外部變量,在main.c中不需要進(jìn)行外部聲明。那么我們可以在我們的main.c程序中最開(kāi)始的位置加上一句: #define _MAIN_C 然后在main.h內(nèi)對(duì)這類變量進(jìn)行聲明的時(shí)候,再加上這樣的條件編譯語(yǔ)句: #ifndef _MAIN_C 程序段2 #endif 這樣處理之后,大家看一下,由于我們?cè)?font face="Times New Roman">main.c的程序中首先對(duì)_MAIN_C進(jìn)行宏聲明了,因此程序段2中的內(nèi)容不會(huì)參與到main.c的編譯中去,而其他所有的包含main.h的源文件則會(huì)把程序段2參與到編譯中,因此前邊我們的main.h文件的整體代碼如下所示。 #ifndef _MAIN_H #define _MAIN_H
enum eStaSystem { //系統(tǒng)運(yùn)行狀態(tài)枚舉 E_NORMAL, E_SET_TIME, E_SET_ALARM };
#ifndef _MAIN_C extern enum eStaSystem staSystem; #endif
void RefreshTemp(uint8 ops); void ConfigTimer0(uint16 ms);
#endif
19.4 多功能電子鐘
本章的重頭戲就是我們要做的這個(gè)項(xiàng)目實(shí)踐開(kāi)發(fā)——多功能電子鐘。當(dāng)接到一個(gè)具體項(xiàng)目開(kāi)發(fā)任務(wù)后,要根據(jù)項(xiàng)目做出框架規(guī)劃,整理出邏輯思路,并且寫(xiě)出規(guī)范的程序,調(diào)試代碼最終完成功能。[size=14.0000pt]19.4.1 硬件布局規(guī)劃 作為電子鐘,或者說(shuō)萬(wàn)年歷,提供日期、時(shí)間的顯示是一個(gè)基本的功能,但是我們的設(shè)計(jì)要求并不滿足于基本功能,而是要提供更多的信息,并且兼容人性化設(shè)計(jì)。在我們的設(shè)計(jì)中,除了基本的走時(shí)(包括時(shí)間、日期、星期)、板載按鍵校時(shí)功能外,還提供鬧鐘、溫度測(cè)量、紅外遙控校時(shí)這幾項(xiàng)實(shí)用功能,所以稱之為多功能。 如果一個(gè)產(chǎn)品只是所需功能的雜亂堆積,而不考慮怎樣讓人用起來(lái)更舒服、更愉悅,那么這就非常的不人性化,也絕對(duì)不是一個(gè)優(yōu)秀的設(shè)計(jì)或者說(shuō)產(chǎn)品。比如電子鐘把日期和時(shí)間都顯示到液晶上,這樣看起來(lái)主次就不是很分明,顯得雜亂。人性化設(shè)計(jì)考慮的是大多數(shù)人的行為習(xí)慣,當(dāng)然最終的產(chǎn)品依靠了設(shè)計(jì)人員的經(jīng)驗(yàn)和審美等因素。比如我們KST-51開(kāi)發(fā)板的器件布局,右上方向是顯示器件,右下是按鍵輸入,有一些外圍器件比如上下拉電阻,三極管等我們可以隱藏到液晶底下,這就是大多數(shù)人的習(xí)慣。而在我們的多功能電子鐘項(xiàng)目中,如何去體現(xiàn)人性化設(shè)計(jì)呢? 我們先來(lái)觀察一下各種顯示器件,數(shù)字顯示如果采用LED點(diǎn)陣或者數(shù)碼管就會(huì)比較醒目,但是點(diǎn)陣無(wú)法同時(shí)顯示這么多數(shù)字,于是我們就把最常用的時(shí)間用數(shù)碼管來(lái)顯示,日期、鬧鐘設(shè)置、溫度等輔助信息我們顯示到液晶上。那么點(diǎn)陣呢?我們可以用它來(lái)顯示星期,這對(duì)于盼望著周末的人們來(lái)說(shuō)是不是很醒目很人性化呢?對(duì)了,還有獨(dú)立的LED,我們就用它來(lái)給電子鐘做裝飾吧,用個(gè)來(lái)回跑的流水燈增加點(diǎn)活潑氣氛。最后再來(lái)個(gè)遙控器功能,如果電子鐘掛的太高了或者放在不方便觸碰的位置,我們就可以使用遙控器來(lái)校時(shí)。大家再來(lái)想想看,整個(gè)過(guò)程是不是挺人性化的。 當(dāng)然了,我們所用的是KST-51單片機(jī)開(kāi)發(fā)板來(lái)作為我們的硬件平臺(tái),如果這個(gè)是個(gè)實(shí)際項(xiàng)目,就不需要那么多外圍器件了,首先做好單片機(jī)最小系統(tǒng),而后配備我們多功能電子鐘所需要的硬件外設(shè)就可以了。也就是說(shuō),我們?cè)谶M(jìn)行項(xiàng)目開(kāi)發(fā)時(shí),設(shè)計(jì)的硬件電路是根據(jù)我們的實(shí)際項(xiàng)目需求來(lái)設(shè)計(jì)的。
19.4.2 程序結(jié)構(gòu)組織
項(xiàng)目需求和硬件規(guī)劃已經(jīng)確定了,我們就得研究如何實(shí)現(xiàn)它們,程序結(jié)構(gòu)如何組織。一個(gè)項(xiàng)目,如果需要的部件很多,同時(shí)實(shí)現(xiàn)的功能也很多,為了方便編寫(xiě)和程序維護(hù),整個(gè)程序必須采用模塊化編程,也就是每個(gè)模塊對(duì)應(yīng)一個(gè)c文件來(lái)實(shí)現(xiàn),這種用法實(shí)際上在前面的章節(jié)已經(jīng)開(kāi)始使用了。一方面,如果所有的代碼堆到一起會(huì)顯得雜亂無(wú)章,更重要的是容易造成意外錯(cuò)誤,程序一旦有邏輯上的問(wèn)題或者更新需求,這種維護(hù)將變成一種災(zāi)難。此外,當(dāng)一個(gè)項(xiàng)目程序量很大的時(shí)候,可以由多個(gè)程序員共同參與編程,多模塊的方式也可以讓每個(gè)程序員之間的代碼最終很方便的融合到一起。 模塊的劃分并沒(méi)有什么教條可以遵循,而是根據(jù)具體需要靈活處理。那么我們就以這個(gè)多功能電子鐘項(xiàng)目為例,來(lái)給大家介紹說(shuō)明如何合理的劃分模塊。我們要實(shí)現(xiàn)的功能有:走時(shí)、校時(shí)、鬧鐘、溫度、遙控這幾個(gè)功能。要想實(shí)現(xiàn)這幾個(gè)功能,其中走時(shí)所需要的就是時(shí)鐘芯片,即DS1302;時(shí)間需要顯示給人看,就需要顯示器件,我們用到了點(diǎn)陣、數(shù)碼管、獨(dú)立LED、液晶;再來(lái)看校時(shí),校時(shí)需要輸入器件,本例中我們可以用板載按鍵和遙控器,他們各自的驅(qū)動(dòng)代碼不同,但是實(shí)現(xiàn)的功能是一樣的,都是校時(shí);還有鬧鐘設(shè)置,在校時(shí)的輸入器件的支持下,鬧鐘也就不需要額外的硬件輸入了,只需要用程序代碼讓蜂鳴器響就行了。 功能上大概列舉出來(lái)了,那么我們就可以把程序源代碼劃分為這樣幾個(gè)模塊:DS1302作為走時(shí)的核心自成一個(gè)模塊;點(diǎn)陣、數(shù)碼管、獨(dú)立LED都屬于LED的范疇,控制方式都類似,也都需要?jiǎng)討B(tài)掃描,所以把他們整體作為一個(gè)模塊;液晶是另一個(gè)顯示模塊;按鍵和遙控器的驅(qū)動(dòng)各自成為一個(gè)模塊。 模塊劃分到這里,大家就要特別注意,隨著我們程序量變大,功能變強(qiáng),對(duì)程序的劃分要分層了。前邊我們劃分的這些模塊,都屬于是底層驅(qū)動(dòng)范疇的,他們要共同為上層應(yīng)用服務(wù),那么上層應(yīng)用是什么呢?就是根據(jù)最終需要顯示的效果來(lái)調(diào)度各種顯示驅(qū)動(dòng)函數(shù),決定把時(shí)間的哪一部分顯示到哪個(gè)器件上,然后還要根據(jù)按鍵或者遙控器的輸入來(lái)具體實(shí)現(xiàn)時(shí)間的調(diào)整,還要不停的對(duì)比當(dāng)前時(shí)間和設(shè)定的鬧鐘時(shí)間來(lái)完成鬧鐘功能,那么這些功能函數(shù)自然就成為一個(gè)應(yīng)用層模塊了(當(dāng)然你也可以把它們都放在main.c文件內(nèi)實(shí)現(xiàn),但我們不推薦這樣做,如果程序還有其他應(yīng)用層代碼模塊的話,main.c仍然會(huì)變得復(fù)雜而不易維護(hù))。這個(gè)應(yīng)用層模塊在本例中我們?nèi)∶麨?/font>Time.c,即完成時(shí)間相關(guān)的應(yīng)用層功能。最后,還有一個(gè)溫度功能,除了要加入溫度傳感器的DS18B20底層驅(qū)動(dòng)模塊外,它的上層顯示功能非常簡(jiǎn)單,不值得再單獨(dú)占一個(gè)c文件,所以我們直接把它放到main.c中實(shí)現(xiàn)。 模塊劃分完畢,我們就要進(jìn)行整體程序流程的規(guī)劃。我們剛剛對(duì)程序進(jìn)行了分層,一層是硬件底層驅(qū)動(dòng),再就是上層應(yīng)用功能。底層驅(qū)動(dòng)這些是不需要什么流程圖的,流程圖的主要結(jié)構(gòu)都是上層應(yīng)用程序的流程。當(dāng)我們把上層應(yīng)用程序的流程劃分出來(lái)之后,每個(gè)上層應(yīng)用功能都會(huì)有對(duì)底層硬件操作的需求,比如按鍵實(shí)現(xiàn)的功能,必然要寫(xiě)按鍵的底層驅(qū)動(dòng)程序,這些在前邊的學(xué)習(xí)過(guò)程中我們都會(huì)寫(xiě)了。根據(jù)我們所需要的上層應(yīng)用功能,我們畫(huà)出了我們的流程圖,如圖19-2所示。
psb(37).jpeg (51.56 KB, 下載次數(shù): 167)
下載附件
2013-11-28 19:58 上傳
圖19-2 多功能電子鐘流程圖 [size=14.0000pt]19.4.3 [size=14.0000pt]程序代碼編寫(xiě)在實(shí)際項(xiàng)目開(kāi)發(fā)中,我們不僅僅希望我們的源程序、頭文件等文件結(jié)構(gòu)規(guī)范、代碼編寫(xiě)規(guī)范,更希望我們的工程文件規(guī)整規(guī)范,方便維護(hù)。因此我們首先新建一個(gè)lesson19_1的文件夾,用來(lái)存放我們本章的工程文件。而后我們新建工程保存的時(shí)候,在lesson19-1文件夾內(nèi)再建立一個(gè)文件夾,取名為project,專門(mén)用于存放工程文件的,如圖19-3所示。
psb(38).jpeg (57.25 KB, 下載次數(shù): 151)
下載附件
2013-11-28 19:58 上傳
圖19-3 工程文件夾 然后我們新建文件,保存的時(shí)候,在lesson19_1目錄再建立一個(gè)文件夾,取名為source文件夾,專門(mén)用來(lái)存放我們的源代碼,如圖19-4所示。
psb(39).jpeg (85.76 KB, 下載次數(shù): 154)
下載附件
2013-11-28 19:58 上傳
圖19-4 文件文件夾 最后,隨便看一個(gè)之前的例子都能看到,工程編譯后會(huì)生成很多額外的文件,這些文件可以統(tǒng)稱為編譯輸出文件,輸出文件的路徑配置,進(jìn)入Options for Target->Output,點(diǎn)擊Select Folder for Objects,在lesson19_1建立一個(gè)文件夾,取名為output,專門(mén)用來(lái)存放這些輸出文件,如圖19-5所示。
psb(40).jpeg (46.83 KB, 下載次數(shù): 164)
下載附件
2013-11-28 19:58 上傳
圖19-5 輸出文件夾 進(jìn)行了這樣三個(gè)步驟,當(dāng)今后我們要對(duì)這個(gè)工程進(jìn)行整理編寫(xiě)的時(shí)候,文件就不再凌亂了,而是非常規(guī)整的排列在我們的文件夾內(nèi)。尤其是今后大家還可能學(xué)到編寫(xiě)程序的另外的方式,就是編譯的時(shí)候使用Keil軟件,而編寫(xiě)代碼的時(shí)候在其他更好的編輯器中進(jìn)行,那么編輯器的工程文件也可以放到project下,而不會(huì)對(duì)其它部分產(chǎn)生任何影響?傊,這是一套規(guī)范而又實(shí)用的工程文件組織方案。 工程建立完畢,文件夾也整理妥當(dāng),下面就開(kāi)始正式編寫(xiě)代碼。當(dāng)我們要進(jìn)行一個(gè)實(shí)際產(chǎn)品或者項(xiàng)目開(kāi)發(fā)的時(shí)候,首先電路原理圖是確定的,所使用的單片機(jī)的引腳也是明確的,還有一些比如類型說(shuō)明,一些特殊的全局參數(shù)及宏聲明,我們會(huì)放到一個(gè)專門(mén)的頭文件中,在這里我們命名為config.h文件。 /***********************config.h文件程序源代碼*************************/ #ifndef _CONFIG_H #define _CONFIG_H
/* 通用頭文件 */ #include <reg52.h> #include <intrins.h>
/* 數(shù)據(jù)類型定義 */ typedef signed char int8; // 8位有符號(hào)整型數(shù) typedef signed int int16; //16位有符號(hào)整型數(shù) typedef signed long int32; //32位有符號(hào)整型數(shù) typedef unsigned char uint8; // 8位無(wú)符號(hào)整型數(shù) typedef unsigned int uint16; //16位無(wú)符號(hào)整型數(shù) typedef unsigned long uint32; //32位無(wú)符號(hào)整型數(shù)
/* 全局運(yùn)行參數(shù)定義 */ #define SYS_MCLK (11059200/12) //系統(tǒng)主時(shí)鐘頻率,即振蕩器頻率÷12
/* IO引腳分配定義 */ sbit KEY_IN_1 = P2^4; //矩陣按鍵的掃描輸入引腳1 sbit KEY_IN_2 = P2^5; //矩陣按鍵的掃描輸入引腳2 sbit KEY_IN_3 = P2^6; //矩陣按鍵的掃描輸入引腳3 sbit KEY_IN_4 = P2^7; //矩陣按鍵的掃描輸入引腳4 sbit KEY_OUT_1 = P2^3; //矩陣按鍵的掃描輸出引腳1 sbit KEY_OUT_2 = P2^2; //矩陣按鍵的掃描輸出引腳2 sbit KEY_OUT_3 = P2^1; //矩陣按鍵的掃描輸出引腳3 sbit KEY_OUT_4 = P2^0; //矩陣按鍵的掃描輸出引腳4
sbit ADDR0 = P1^0; //LED位選譯碼地址引腳0 sbit ADDR1 = P1^1; //LED位選譯碼地址引腳1 sbit ADDR2 = P1^2; //LED位選譯碼地址引腳2 sbit ADDR3 = P1^3; //LED位選譯碼地址引腳3 sbit ENLED = P1^4; //LED顯示部件的總使能引腳
#define LCD1602_DB P0 //1602液晶數(shù)據(jù)端口 sbit LCD1602_RS = P1^0; //1602液晶指令/數(shù)據(jù)選擇引腳 sbit LCD1602_RW = P1^1; //1602液晶讀寫(xiě)引腳 sbit LCD1602_E = P1^5; //1602液晶使能引腳
sbit DS1302_CE = P1^7; //DS1302片選引腳 sbit DS1302_CK = P3^5; //DS1302通信時(shí)鐘引腳 sbit DS1302_IO = P3^4; //DS1302通信數(shù)據(jù)引腳
sbit I2C_SCL = P3^7; //I2C總線時(shí)鐘引腳 sbit I2C_SDA = P3^6; //I2C總線數(shù)據(jù)引腳
sbit BUZZER = P1^6; //蜂鳴器控制引腳
sbit IO_18B20 = P3^2; //DS18B20通信引腳
sbit IR_INPUT = P3^3; //紅外接收引腳
#endif 這個(gè)config.h聲明包含了系統(tǒng)所共同使用的類型說(shuō)明以及宏聲明,方便使用。下邊的編程步驟,就是從main.c文件開(kāi)始,以流程圖作為主線來(lái)進(jìn)行代碼編寫(xiě)。 作為研發(fā)工程師來(lái)講,調(diào)試這樣一個(gè)程序,也得幾個(gè)小時(shí)的時(shí)間,不可能寫(xiě)出來(lái)就好用,所以我這里是無(wú)法全部把整個(gè)過(guò)程給大家還原出來(lái)。但是主要的編寫(xiě)代碼的過(guò)程我會(huì)盡可能的給大家介紹一下。 我們程序的流程雖然是從main.c開(kāi)始的,但是那是整體程序框架,而編寫(xiě)代碼,往往用流程圖來(lái)做主線,卻不是嚴(yán)格按照流程圖的順序來(lái)。比如我們這個(gè)程序,首先我們要進(jìn)行功能性調(diào)試驗(yàn)證。 習(xí)慣上,我首先要調(diào)試顯示程序,因?yàn)轱@示程序可以直觀的看到,而且調(diào)試好顯示后,如果要調(diào)試其他的模塊,可以用顯示模塊來(lái)驗(yàn)證其他模塊運(yùn)行結(jié)果正確與否。顯示設(shè)備就是1602液晶和LED,由于蜂鳴器比較簡(jiǎn)單,所以我們將蜂鳴器和LED放到一起。調(diào)試的時(shí)候,可以在main.c文件中,添加臨時(shí)的調(diào)試函數(shù),比如給1602液晶發(fā)送數(shù)據(jù),讓1602液晶顯示個(gè)字符串,保證1602液晶的底層程序是沒(méi)問(wèn)題的;調(diào)用相應(yīng)的函數(shù)讓LED進(jìn)行顯示以及刷新,保證LED部分的程序也是沒(méi)問(wèn)題的。通過(guò)這種方式,如果發(fā)現(xiàn)哪部分還有問(wèn)題就繼續(xù)調(diào)整,如果發(fā)現(xiàn)顯示部分OK了,那就可以繼續(xù)往下編寫(xiě)了。 1602液晶的底層驅(qū)動(dòng)我們之前都已經(jīng)寫(xiě)過(guò)了,直接拿過(guò)來(lái)用就行了。而對(duì)于LED的動(dòng)態(tài)刷新問(wèn)題,在講紅外的時(shí)候已經(jīng)闡述過(guò),用于LED刷新的定時(shí)器應(yīng)該采用高優(yōu)先級(jí)以避免紅外接收中斷動(dòng)輒上百ms的執(zhí)行時(shí)間影響視覺(jué)效果,我們選擇T1用來(lái)作為紅外接收的計(jì)時(shí),按理說(shuō)再用T0設(shè)置成高優(yōu)先級(jí)來(lái)處理LED刷新即可,但是,本例中我們還啟用了矩陣按鍵,而矩陣按鍵的掃描也采用T0而對(duì)紅外中斷實(shí)現(xiàn)嵌套的話,由于按鍵掃描的時(shí)間會(huì)達(dá)到幾百us,這幾百us的延時(shí)則足以使紅外對(duì)碼位的解析產(chǎn)生誤判了。怎么辦呢?是不是會(huì)很自然的想到:再增加一個(gè)定時(shí)器用來(lái)做LED掃描并實(shí)現(xiàn)對(duì)紅外中斷的嵌套,而按鍵掃描和紅外處于相同的低優(yōu)先級(jí)而不能彼此嵌套,按鍵遲后上百ms再響應(yīng)不會(huì)感覺(jué)到問(wèn)題,同樣幾百us的延時(shí)對(duì)紅外起始引導(dǎo)碼的9ms來(lái)說(shuō)也完全可以容忍。那么還有沒(méi)有定時(shí)器可用了呢,好在我們的STC89C52還有一個(gè)定時(shí)器T2(標(biāo)準(zhǔn)的8051是沒(méi)有T2的,它是8052的擴(kuò)充外設(shè),現(xiàn)在絕大多數(shù)的51系列單片機(jī)都是有這個(gè)T2的),于是問(wèn)題解決。此外還有一個(gè)問(wèn)題,就是由于操作液晶的時(shí)候要對(duì)P1.0和P1.1進(jìn)行操作,而刷新LED是中斷,優(yōu)先級(jí)是高于液晶的。如果我們當(dāng)前正在操作液晶,對(duì)P1.0和P1.1操作了,數(shù)碼管刷新的中斷又來(lái)了,也要對(duì)P1.0和P1.1進(jìn)行操作,就會(huì)導(dǎo)致邏輯錯(cuò)誤。雖然這種錯(cuò)誤出現(xiàn)機(jī)率極小,但是邏輯必須要嚴(yán)謹(jǐn),必需避免它。那么當(dāng)我們進(jìn)行液晶操作的時(shí)候,如果數(shù)碼管的定時(shí)中斷來(lái)了,我們?cè)诒敬沃袛嘀芯头艞墝?duì)數(shù)碼管的刷新,不對(duì)那幾個(gè)口線進(jìn)行操作,因?yàn)橐壕У淖x寫(xiě)操作都很快,所以對(duì)實(shí)際顯示效果并沒(méi)有太大的影響。 這部分代碼除了定時(shí)器2的寄存器配置外,其他的內(nèi)容我們之前幾乎都用到過(guò),大家可以通過(guò)分析程序?qū)W明白。而定時(shí)器2的寄存器配置,相信學(xué)到這里的同學(xué)也可以通過(guò)查閱數(shù)據(jù)手冊(cè)自己看明白,這里要求同學(xué)們自學(xué)一下。我直接把代碼貼出來(lái),大家研究一下。 /***********************Lcd1602.h文件程序源代碼*************************/ #ifndef _LCD1602_H #define _LCD1602_H
#ifndef _LCD1602_C
#endif
void InitLcd1602(); void LcdClearScreen(); void LcdOpenCursor(); void LcdCloseCursor(); void LcdSetCursor(uint8 x, uint8 y); void LcdShowStr(uint8 x, uint8 y, uint8 *str); void LcdShowChar(uint8 x, uint8 y, uint8 chr);
#endif /***********************Lcd1602.c文件程序源代碼*************************/ #define _LCD1602_C #include "config.h" #include "Lcd1602.h"
bit tmpADDR0; //暫存LED位選譯碼地址0的值 bit tmpADDR1; //暫存LED位選譯碼地址1的值
/* 暫停LED動(dòng)態(tài)掃描,暫存相關(guān)引腳的值 */ void LedScanPause() { ENLED = 1; tmpADDR0 = ADDR0; tmpADDR1 = ADDR1; } /* 恢復(fù)LED動(dòng)態(tài)掃描,恢復(fù)相關(guān)引腳的值 */ void LedScanContinue() { ADDR0 = tmpADDR0; ADDR1 = tmpADDR1; ENLED = 0; } /* 等待液晶準(zhǔn)備好 */ void LcdWaitReady() { uint8 sta;
LCD1602_DB = 0xFF; LCD1602_RS = 0; LCD1602_RW = 1; do { LCD1602_E = 1; sta = LCD1602_DB; //讀取狀態(tài)字 LCD1602_E = 0; } while (sta & 0x80); //bit7等于1表示液晶正忙,重復(fù)檢測(cè)直到其等于0為止 } /* 向LCD1602液晶寫(xiě)入一字節(jié)命令,cmd-待寫(xiě)入命令值 */ void LcdWriteCmd(uint8 cmd) { LedScanPause(); LcdWaitReady(); LCD1602_RS = 0; LCD1602_RW = 0; LCD1602_DB = cmd; LCD1602_E = 1; LCD1602_E = 0; LedScanContinue(); } /* 向LCD1602液晶寫(xiě)入一字節(jié)數(shù)據(jù),dat-待寫(xiě)入數(shù)據(jù)值 */ void LcdWriteDat(uint8 dat) { LedScanPause(); LcdWaitReady(); LCD1602_RS = 1; LCD1602_RW = 0; LCD1602_DB = dat; LCD1602_E = 1; LCD1602_E = 0; LedScanContinue(); } /* 清屏 */ void LcdClearScreen() { LcdWriteCmd(0x01); } /* 打開(kāi)光標(biāo)的閃爍效果 */ void LcdOpenCursor() { LcdWriteCmd(0x0F); } /* 關(guān)閉光標(biāo)顯示 */ void LcdCloseCursor() { LcdWriteCmd(0x0C); } /* 設(shè)置顯示RAM起始地址,亦即光標(biāo)位置,(x,y)-對(duì)應(yīng)屏幕上的字符坐標(biāo) */ void LcdSetCursor(uint8 x, uint8 y) { uint8 addr;
if (y == 0) //由輸入的屏幕坐標(biāo)計(jì)算顯示RAM的地址 addr = 0x00 + x; //第一行字符地址從0x00起始 else addr = 0x40 + x; //第二行字符地址從0x40起始 LcdWriteCmd(addr | 0x80); //設(shè)置RAM地址 } /* 在液晶上顯示字符串,(x,y)-對(duì)應(yīng)屏幕上的起始坐標(biāo),str-字符串指針 */ void LcdShowStr(uint8 x, uint8 y, uint8 *str) { LcdSetCursor(x, y); //設(shè)置起始地址 while (*str != '\0') //連續(xù)寫(xiě)入字符串?dāng)?shù)據(jù),直到檢測(cè)到結(jié)束符 { LcdWriteDat(*str++); } } /* 在液晶上顯示一個(gè)字符,(x,y)-對(duì)應(yīng)屏幕上的起始坐標(biāo),chr-字符ASCII碼 */ void LcdShowChar(uint8 x, uint8 y, uint8 chr) { LcdSetCursor(x, y); //設(shè)置起始地址 LcdWriteDat(chr); //寫(xiě)入ASCII字符 } /* 初始化1602液晶 */ void InitLcd1602() { LcdWriteCmd(0x38); //16*2顯示,5*7點(diǎn)陣,8位數(shù)據(jù)接口 LcdWriteCmd(0x0C); //顯示器開(kāi),光標(biāo)關(guān)閉 LcdWriteCmd(0x06); //文字不動(dòng),地址自動(dòng)+1 LcdWriteCmd(0x01); //清屏 } /***********************LedBuzzer.h文件程序源代碼*************************/ #ifndef _LED_BUZZER_H #define _LED_BUZZER_H
struct sLedBuff { //LED顯示緩沖區(qū)結(jié)構(gòu) uint8 array[8]; //點(diǎn)陣緩沖區(qū) uint8 number[6]; //數(shù)碼管緩沖區(qū) uint8 alone; //獨(dú)立LED緩沖區(qū) };
#ifndef _LED_BUZZER_C extern bit staBuzzer; extern struct sLedBuff ledBuff; #endif
void InitLed(); void FlowingLight(); void ShowLedNumber(uint8 index, uint8 num, uint8 point); void ShowLedArray(uint8 *ptr);
#endif /***********************LedBuzzer.c文件程序源代碼*************************/ #define _LED_BUZZER_C #include "config.h" #include "LedBuzzer.h"
uint8 code LedChar[] = { //數(shù)碼管顯示字符轉(zhuǎn)換表 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E };
bit staBuzzer = 0; //蜂鳴器狀態(tài)控制位,1-鳴叫、0-關(guān)閉 struct sLedBuff ledBuff; //LED顯示緩沖區(qū),默認(rèn)初值全0,正好達(dá)到上電全亮的效果
/* LED初始化函數(shù),初始化IO、配置定時(shí)器 */ void InitLed() { //初始化IO口 P0 = 0xFF; ENLED = 0; //配置T2作為動(dòng)態(tài)掃描定時(shí) T2CON = 0x00; //配置T2工作在16位自動(dòng)重載定時(shí)器模式 RCAP2H = ((65536-SYS_MCLK/1500)>>8); //配置重載值,每秒產(chǎn)生1500次中斷, RCAP2L = (65536-SYS_MCLK/1500); //以使刷新率達(dá)到100Hz無(wú)閃爍的效果 TH2 = RCAP2H; //設(shè)置初值等于重載值 TL2 = RCAP2L; ET2 = 1; //使能T2中斷 PT2 = 1; //設(shè)置T2中斷為高優(yōu)先級(jí) TR2 = 1; //啟動(dòng)T2 } /* 流水燈實(shí)現(xiàn)函數(shù),間隔調(diào)用實(shí)現(xiàn)流動(dòng)效果 */ void FlowingLight() { static uint8 i = 0; const uint8 code tab[] = { //流動(dòng)表 0x7F, 0x3F, 0x1F, 0x0F, 0x87, 0xC3, 0xE1, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF };
ledBuff.alone = tab[ i]; //表中對(duì)應(yīng)值送到獨(dú)立LED的顯示緩沖區(qū) if (i < (sizeof(tab)-1)) //索引遞增循環(huán),遍歷整個(gè)流動(dòng)表 i++; else i = 0; } /* 數(shù)碼管上顯示一位數(shù)字,index-數(shù)碼管位索引(從右到左對(duì)應(yīng)0~5), ** num-待顯示的數(shù)字,point-代表是否顯示此位上的小數(shù)點(diǎn) */ void ShowLedNumber(uint8 index, uint8 num, uint8 point) { ledBuff.number[ index] = LedChar[num]; //輸入數(shù)字轉(zhuǎn)換為數(shù)碼管字符0~F if (point != 0) { ledBuff.number[ index] &= 0x7F; //point不為0時(shí)點(diǎn)亮當(dāng)前位的小數(shù)點(diǎn) } } /* 點(diǎn)陣上顯示一幀圖片,ptr-待顯示圖片指針 */ void ShowLedArray(uint8 *ptr) { uint8 i;
for (i=0; i<sizeof(ledBuff.array); i++) { ledBuff.array[ i] = *ptr++; } } /* T2中斷服務(wù)函數(shù),LED動(dòng)態(tài)掃描、蜂鳴器控制 */ void InterruptTimer2() interrupt 5 { static uint8 i = 0; //LED位選索引
TF2 = 0; //清零T2中斷標(biāo)志 //全部LED動(dòng)態(tài)掃描顯示 if (ENLED == 0) //LED使能時(shí)才進(jìn)行動(dòng)態(tài)掃描 { P0 = 0xFF; //關(guān)閉所有段選位,顯示消隱 P1 = (P1 & 0xF0) | i; //位選索引值賦值到P1口低4位 P0 = *((uint8 data*)&ledBuff+i); //緩沖區(qū)中索引位置的數(shù)據(jù)送到P0口 if (i < (sizeof(ledBuff)-1)) //索引遞增循環(huán),遍歷整個(gè)緩沖區(qū) i++; else i = 0; } //由蜂鳴器狀態(tài)位控制蜂鳴器 if (staBuzzer == 1) BUZZER = ~BUZZER; //蜂鳴器鳴叫 else BUZZER = 1; //蜂鳴器靜音 } 第二個(gè)部分,我們就要調(diào)試時(shí)鐘DS1302的程序代碼了,這部分代碼,我們首先可以把前邊在1602液晶上顯示時(shí)間的代碼拿過(guò)來(lái)當(dāng)作調(diào)試手段,當(dāng)可以成功顯示到1602液晶上后,我們就可以寫(xiě)進(jìn)去一個(gè)初始時(shí)間,再讀出來(lái),把星期顯示在LED點(diǎn)陣上,時(shí)間顯示到數(shù)碼管上,日期顯示到液晶上,并且讓流水燈流動(dòng)起來(lái)。這塊功能調(diào)試好以后,就是一個(gè)簡(jiǎn)單的電子鐘了。 /***********************DS1302.h文件程序源代碼*************************/ #ifndef _DS1302_H #define _DS1302_H
struct sTime { //日期時(shí)間結(jié)構(gòu) uint16 year; //年 uint8 mon; //月 uint8 day; //日 uint8 hour; //時(shí) uint8 min; //分 uint8 sec; //秒 uint8 week; //星期 };
#ifndef _DS1302_C
#endif
void InitDS1302(); void GetRealTime(struct sTime *time); void SetRealTime(struct sTime *time);
#endif /***********************Ds1302.c文件程序源代碼*************************/ #define _DS1302_C #include "config.h" #include "DS1302.h"
/* 發(fā)送一個(gè)字節(jié)到DS1302通信總線上 */ void DS1302ByteWrite(uint8 dat) { uint8 mask;
for (mask=0x01; mask!=0; mask<<=1) //低位在前,逐位移出 { if ((mask&dat) != 0) //首先輸出該位數(shù)據(jù) DS1302_IO = 1; else DS1302_IO = 0; DS1302_CK = 1; //然后拉高時(shí)鐘 DS1302_CK = 0; //再拉低時(shí)鐘,完成一個(gè)位的操作 } DS1302_IO = 1; //最后確保釋放IO引腳 } /* 由DS1302通信總線上讀取一個(gè)字節(jié) */ uint8 DS1302ByteRead() { uint8 mask; uint8 dat = 0;
for (mask=0x01; mask!=0; mask<<=1) //低位在前,逐位讀取 { if (DS1302_IO != 0) //首先讀取此時(shí)的IO引腳,并設(shè)置dat中的對(duì)應(yīng)位 { dat |= mask; } DS1302_CK = 1; //然后拉高時(shí)鐘 DS1302_CK = 0; //再拉低時(shí)鐘,完成一個(gè)位的操作 } return dat; //最后返回讀到的字節(jié)數(shù)據(jù) } /* 用單次寫(xiě)操作向某一寄存器寫(xiě)入一個(gè)字節(jié),reg-寄存器地址,dat-待寫(xiě)入字節(jié) */ void DS1302SingleWrite(uint8 reg, uint8 dat) { DS1302_CE = 1; //使能片選信號(hào) DS1302ByteWrite((reg<<1)|0x80); //發(fā)送寫(xiě)寄存器指令 DS1302ByteWrite(dat); //寫(xiě)入字節(jié)數(shù)據(jù) DS1302_CE = 0; //除能片選信號(hào) } /* 用單次讀操作從某一寄存器讀取一個(gè)字節(jié),reg-寄存器地址,返回值-讀到的字節(jié) */ uint8 DS1302SingleRead(uint8 reg) { uint8 dat;
DS1302_CE = 1; //使能片選信號(hào) DS1302ByteWrite((reg<<1)|0x81); //發(fā)送讀寄存器指令 dat = DS1302ByteRead(); //讀取字節(jié)數(shù)據(jù) DS1302_CE = 0; //除能片選信號(hào)
return dat; } /* 用突發(fā)模式連續(xù)寫(xiě)入8個(gè)寄存器數(shù)據(jù),dat-待寫(xiě)入數(shù)據(jù)指針 */ void DS1302BurstWrite(uint8 *dat) { uint8 i;
DS1302_CE = 1; DS1302ByteWrite(0xBE); //發(fā)送突發(fā)寫(xiě)寄存器指令 for (i=0; i<8; i++) //連續(xù)寫(xiě)入8字節(jié)數(shù)據(jù) { DS1302ByteWrite(dat[ i]); } DS1302_CE = 0; } /* 用突發(fā)模式連續(xù)讀取8個(gè)寄存器的數(shù)據(jù),dat-讀取數(shù)據(jù)的接收指針 */ void DS1302BurstRead(uint8 *dat) { uint8 i;
DS1302_CE = 1; DS1302ByteWrite(0xBF); //發(fā)送突發(fā)讀寄存器指令 for (i=0; i<8; i++) //連續(xù)讀取8個(gè)字節(jié) { dat[ i] = DS1302ByteRead(); } DS1302_CE = 0; } /* 獲取實(shí)時(shí)時(shí)間,即讀取DS1302當(dāng)前時(shí)間并轉(zhuǎn)換為時(shí)間結(jié)構(gòu)體格式 */ void GetRealTime(struct sTime *time) { uint8 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]; } /* 設(shè)定實(shí)時(shí)時(shí)間,時(shí)間結(jié)構(gòu)體格式的設(shè)定時(shí)間轉(zhuǎn)換為數(shù)組并寫(xiě)入DS1302 */ void SetRealTime(struct sTime *time) { uint8 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); } /* DS1302初始化,如發(fā)生掉電則重新設(shè)置初始時(shí)間 */ void InitDS1302() { uint8 dat; struct sTime code InitTime[] = { //默認(rèn)初始值:2014-01-01 12:30:00 星期3 0x2014,0x01,0x01, 0x12,0x30,0x00, 0x03 };
DS1302_CE = 0; //初始化DS1302通信引腳 DS1302_CK = 0; dat = DS1302SingleRead(0); //讀取秒寄存器 if ((dat & 0x80) != 0) //由秒寄存器最高位CH的值判斷DS1302是否已停止 { DS1302SingleWrite(7, 0x00); //撤銷寫(xiě)保護(hù)以允許寫(xiě)入數(shù)據(jù) SetRealTime(&InitTime); //設(shè)置DS1302為默認(rèn)的初始時(shí)間 } } 時(shí)鐘顯示調(diào)試完畢后,下一步就可以開(kāi)始編寫(xiě)按鍵代碼,使用按鍵可以調(diào)整時(shí)鐘,調(diào)整鬧鐘的時(shí)間。當(dāng)然,我們?cè)谡{(diào)試按鍵底層驅(qū)動(dòng)的時(shí)候,不一定要把所有想要的功能都羅列出來(lái),可以先進(jìn)行按鍵底層功能程序的調(diào)試,按下按鍵讓蜂鳴器響一下,或者閃爍個(gè)小燈等都可以用來(lái)檢驗(yàn)按鍵底層代碼工作的正確性。隨著我們程序量的加大,有些功能也可以進(jìn)行綜合了,可以在Time.c文件中和main.c文件中添加程序了,一邊添加一邊調(diào)試,而不是把所有的程序代碼都寫(xiě)完后,像無(wú)頭蒼蠅一樣到處找漏洞。
/***********************keyboard.h文件程序源代碼*************************/ #ifndef _KEY_BOARD_H #define _KEY_BOARD_H
#ifndef _KEY_BOARD_C
#endif
void KeyScan(); void KeyDriver(); void KeyAction(uint8 keycode);
#endif /***********************keyboard.c文件程序源代碼*************************/ #define _KEY_BOARD_C #include "config.h" #include "keyboard.h" #include "Time.h" #include "main.h"
const uint8 code KeyCodeMap[4][4] = { //矩陣按鍵到PC標(biāo)準(zhǔn)鍵碼的映射表 { '1', '2', '3', 0x26 }, //數(shù)字鍵1、數(shù)字鍵2、數(shù)字鍵3、向上鍵 { '4', '5', '6', 0x25 }, //數(shù)字鍵4、數(shù)字鍵5、數(shù)字鍵6、向左鍵 { '7', '8', '9', 0x28 }, //數(shù)字鍵7、數(shù)字鍵8、數(shù)字鍵9、向下鍵 { '0', 0x1B, 0x0D, 0x27 } //數(shù)字鍵0、ESC鍵、 回車(chē)鍵、 向右鍵 }; uint8 pdata KeySta[4][4] = { //全部矩陣按鍵的當(dāng)前狀態(tài) {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} };
/* 按鍵動(dòng)作函數(shù),根據(jù)鍵碼執(zhí)行相應(yīng)的操作 */ void KeyAction(uint8 keycode) { if ((keycode>='0') && (keycode<='9')) //數(shù)字鍵輸入當(dāng)前位設(shè)定值 { InputSetNumber(keycode); } else if (keycode == 0x25) //向左鍵,向左切換設(shè)置位 { SetLeftShift(); } else if (keycode == 0x27) //向右鍵,向右切換設(shè)置位 { SetRightShift(); } else if (keycode == 0x0D) //回車(chē)鍵,切換運(yùn)行狀態(tài)/保存設(shè)置 { SwitchSystemSta(); } else if (keycode == 0x1B) //Esc鍵,靜音/取消當(dāng)前設(shè)置 { if (staSystem == E_NORMAL) //處于正常運(yùn)行狀態(tài)時(shí)鬧鈴靜音 { staMute = 1; } else //處于設(shè)置狀態(tài)時(shí)退出設(shè)置 { CancelCurSet(); } } } /* 按鍵驅(qū)動(dòng)函數(shù),檢測(cè)按鍵動(dòng)作,調(diào)度相應(yīng)動(dòng)作函數(shù),需在主循環(huán)中調(diào)用 */ void KeyDriver() { uint8 i, j; static uint8 pdata backup[4][4] = { //按鍵值備份,保存前一次的值 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; for (i=0; i<4; i++) //循環(huán)掃描4*4的矩陣按鍵 { for (j=0; j<4; j++) { if (backup[ i][j] != KeySta[ i][j]) //檢測(cè)按鍵動(dòng)作 { if (backup[ i][j] != 0) //按鍵按下時(shí)執(zhí)行動(dòng)作 { KeyAction(KeyCodeMap[ i][j]); //調(diào)用按鍵動(dòng)作函數(shù) } backup[ i][j] = KeySta[ i][j]; //刷新前一次的備份值 } } } } /* 按鍵掃描函數(shù),需在定時(shí)中斷中調(diào)用,推薦調(diào)用間隔1ms */ void KeyScan() { uint8 i; static uint8 keyout = 0; //矩陣按鍵掃描輸出索引 static uint8 keybuf[4][4] = { //矩陣按鍵掃描緩沖區(qū) {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF} };
//將一行的4個(gè)按鍵值移入緩沖區(qū) keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1; keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2; keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3; keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4; //消抖后更新按鍵狀態(tài) for (i=0; i<4; i++) //每行4個(gè)按鍵,所以循環(huán)4次 { if ((keybuf[keyout][ i] & 0x0F) == 0x00) { //連續(xù)4次掃描值為0,即4*4ms內(nèi)都是按下?tīng)顟B(tài)時(shí),可認(rèn)為按鍵已穩(wěn)定的按下 KeySta[keyout][ i] = 0; } else if ((keybuf[keyout][ i] & 0x0F) == 0x0F) { //連續(xù)4次掃描值為1,即4*4ms內(nèi)都是彈起狀態(tài)時(shí),可認(rèn)為按鍵已穩(wěn)定的彈起 KeySta[keyout][ i] = 1; } } //執(zhí)行下一次的掃描輸出 keyout++; //輸出索引遞增 keyout &= 0x03; //索引值加到4即歸零 switch (keyout) //根據(jù)索引值,釋放當(dāng)前輸出引腳,拉低下次的輸出引腳 { case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break; case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break; case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break; case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break; default: break; } } 按鍵程序調(diào)試完畢后,下一步毫無(wú)疑問(wèn)就是紅外的代碼了。紅外所要實(shí)現(xiàn)的功能是和按鍵完全一樣的,但是如果說(shuō)我們把紅外按鍵的代碼解析出來(lái)后,再去做相應(yīng)的操作顯得有點(diǎn)多余了。我們的處理方式是,把紅外的按鍵代碼解析出來(lái),和我們板載按鍵進(jìn)行映射對(duì)應(yīng)關(guān)系,不同的紅外按鍵映射為板子上的不同的板載按鍵值就可以了,這樣只需要寫(xiě)一套按鍵驅(qū)動(dòng)程序,紅外的代碼只做解析和映射功能即可。 /***********************Infrared.h文件程序源代碼*************************/ #ifndef _INFRARED_H #define _INFRARED_H
#ifndef _INFRARED_C
#endif
void InitInfrared(); void InfraredDriver();
#endif /***********************Infrared.c文件程序源代碼*************************/ #define _INFRARED_C #include "config.h" #include "Infrared.h" #include "keyboard.h"
const uint8 code IrCodeMap[][2] = { //紅外鍵碼到標(biāo)準(zhǔn)PC鍵碼的映射表 {0x45, 0x00}, {0x46, 0x00}, {0x47, 0x1B}, //開(kāi)關(guān)->無(wú) Mode->無(wú) 靜音->ESC {0x44, 0x00}, {0x40, 0x25}, {0x43, 0x27}, //播放->無(wú) 后退->向左 前進(jìn)->向右 {0x07, 0x00}, {0x15, 0x28}, {0x09, 0x26}, // EQ->無(wú) 減號(hào)->向下 加號(hào)->向上 {0x16, 0x30}, {0x19, 0x1B}, {0x0D, 0x0D}, //'0'->'0' 箭頭->ESC U/SD->回車(chē) {0x0C, 0x31}, {0x18, 0x32}, {0x5E, 0x33}, //'1'->'1' '2'->'2' '3'->'3' {0x08, 0x34}, {0x1C, 0x35}, {0x5A, 0x36}, //'4'->'4' '5'->'5' '6'->'6' {0x42, 0x37}, {0x52, 0x38}, {0x4A, 0x39}, //'7'->'7' '6'->'8' '9'->'9' };
bit irflag = 0; //紅外接收標(biāo)志,收到一幀正確數(shù)據(jù)后置1 uint8 ircode[4]; //紅外代碼接收緩沖區(qū)
/* 紅外接收驅(qū)動(dòng),檢測(cè)接收到的鍵碼,調(diào)度相應(yīng)動(dòng)作函數(shù) */ void InfraredDriver() { uint8 i; if (irflag) { irflag = 0; for (i=0; i<sizeof(IrCodeMap)/sizeof(IrCodeMap[0]); i++) //遍歷映射表 { if (ircode[2] == IrCodeMap[ i][0]) //在表中找到當(dāng)前接收的鍵碼后, { //用對(duì)應(yīng)的映射碼執(zhí)行函數(shù)調(diào)度, KeyAction(IrCodeMap[ i][1]); //直接調(diào)用按鍵動(dòng)作函數(shù)即可。 break; } } } } /* 初始化紅外接收功能 */ void InitInfrared() { IR_INPUT = 1; //確保紅外接收引腳被釋放 TMOD &= 0x0F; //清零T1的控制位 TMOD |= 0x10; //配置T1為模式1 TR1 = 0; //停止T1計(jì)數(shù) ET1 = 0; //禁止T1中斷 IT1 = 1; //設(shè)置INT1為負(fù)邊沿觸發(fā) EX1 = 1; //使能INT1中斷 } /* 獲取當(dāng)前高電平的持續(xù)時(shí)間 */ uint16 GetHighTime() { TH1 = 0; //清零T1計(jì)數(shù)初值 TL1 = 0; TR1 = 1; //啟動(dòng)T1計(jì)數(shù) while (IR_INPUT) //紅外輸入引腳為1時(shí)循環(huán)檢測(cè)等待,變?yōu)?時(shí)則結(jié)束本循環(huán) { if (TH1 >= 0x40) { //當(dāng)T1計(jì)數(shù)值大于0x4000,即高電平持續(xù)時(shí)間超過(guò)約18ms時(shí), break; //強(qiáng)制退出循環(huán),是為了避免信號(hào)異常時(shí),程序假死在這里。 } } TR1 = 0; //停止T1計(jì)數(shù)
return (TH1*256 + TL1); //T1計(jì)數(shù)值合成為16bit整型數(shù),并返回該數(shù) } /* 獲取當(dāng)前低電平的持續(xù)時(shí)間 */ uint16 GetLowTime() { TH1 = 0; //清零T1計(jì)數(shù)初值 TL1 = 0; TR1 = 1; //啟動(dòng)T1計(jì)數(shù) while (!IR_INPUT) //紅外輸入引腳為0時(shí)循環(huán)檢測(cè)等待,變?yōu)?時(shí)則結(jié)束本循環(huán) { if (TH1 >= 0x40) { //當(dāng)T1計(jì)數(shù)值大于0x4000,即低電平持續(xù)時(shí)間超過(guò)約18ms時(shí), break; //強(qiáng)制退出循環(huán),是為了避免信號(hào)異常時(shí),程序假死在這里。 } } TR1 = 0; //停止T1計(jì)數(shù)
return (TH1*256 + TL1); //T1計(jì)數(shù)值合成為16bit整型數(shù),并返回該數(shù) } /* INT1中斷服務(wù)函數(shù),執(zhí)行紅外接收及解碼 */ void EXINT1_ISR() interrupt 2 { uint8 i, j; uint8 byt; uint16 time; //接收并判定引導(dǎo)碼的9ms低電平 time = GetLowTime(); if ((time<7833) || (time>8755)) //時(shí)間判定范圍為8.5~9.5ms, { //超過(guò)此范圍則說(shuō)明為誤碼,直接退出 IE1 = 0; //退出前清零INT1中斷標(biāo)志 return; } //接收并判定引導(dǎo)碼的4.5ms高電平 time = GetHighTime(); if ((time<3686) || (time>4608)) //時(shí)間判定范圍為4.0~5.0ms, { //超過(guò)此范圍則說(shuō)明為誤碼,直接退出 IE1 = 0; return; } //接收并判定后續(xù)的4字節(jié)數(shù)據(jù) for (i=0; i<4; i++) //循環(huán)接收4個(gè)字節(jié) { for (j=0; j<8; j++) //循環(huán)接收判定每字節(jié)的8個(gè)bit { //接收判定每bit的560us低電平 time = GetLowTime(); if ((time<313) || (time>718)) //時(shí)間判定范圍為340~780us, { //超過(guò)此范圍則說(shuō)明為誤碼,直接退出 IE1 = 0; return; } //接收每bit高電平時(shí)間,判定該bit的值 time = GetHighTime(); if ((time>313) && (time<718)) //時(shí)間判定范圍為340~780us, { //在此范圍內(nèi)說(shuō)明該bit值為0 byt >>= 1; //因低位在先,所以數(shù)據(jù)右移,高位為0 } else if ((time>1345) && (time<1751)) //時(shí)間范圍1460~1900us, { //在此范圍內(nèi)說(shuō)明該bit值為1 byt >>= 1; //因低位在先,所以數(shù)據(jù)右移, byt |= 0x80; //高位置1 } else //不在上述范圍內(nèi)則說(shuō)明為誤碼,直接退出 { IE1 = 0; return; } } ircode[ i] = byt; //接收完一個(gè)字節(jié)后保存到緩沖區(qū) } irflag = 1; //接收完畢后設(shè)置標(biāo)志 IE1 = 0; //退出前清零INT1中斷標(biāo)志 } 這一切底層的驅(qū)動(dòng)完成之后,我們就可以整理調(diào)試main.c和Time.c內(nèi)的功能代碼了。一邊添加功能一邊調(diào)試,把最終的功能代碼調(diào)試出來(lái),在KST-51開(kāi)發(fā)板上做驗(yàn)證。這一切都完事之后,我們可以添加一項(xiàng)新功能,就是DS18B20溫度傳感器顯示,這個(gè)是個(gè)獨(dú)立功能,直接寫(xiě)好代碼,添加進(jìn)去就可以了。 /***********************DS18B20.h文件程序源代碼*************************/ #ifndef _DS18B20_H #define _DS18B20_H
#ifndef _DS18B20_C
#endif
bit Start18B20(); bit Get18B20Temp(int16 *temp);
#endif /***********************DS18B20.c文件程序源代碼*************************/ #define _DS18B20_C #include "config.h" #include "DS18B20.h"
/* 軟件延時(shí)函數(shù),延時(shí)時(shí)間(t*10)us */ void DelayX10us(uint8 t) { do { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); } while (--t); } /* 復(fù)位總線,獲取存在脈沖,以啟動(dòng)一次讀寫(xiě)操作 */ bit Get18B20Ack() { bit ack; EA = 0; //禁止總中斷 IO_18B20 = 0; //產(chǎn)生500us復(fù)位脈沖 DelayX10us(50); IO_18B20 = 1; DelayX10us(6); //延時(shí)60us ack = IO_18B20; //讀取存在脈沖 while(!IO_18B20); //等待存在脈沖結(jié)束 EA = 1; //重新使能總中斷 return ack; } /* 向DS18B20寫(xiě)入一個(gè)字節(jié),dat-待寫(xiě)入字節(jié) */ void Write18B20(uint8 dat) { uint8 mask; EA = 0; //禁止總中斷 for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次移出8個(gè)bit { IO_18B20 = 0; //產(chǎn)生2us低電平脈沖 _nop_(); _nop_(); if ((mask&dat) == 0) //輸出該bit值 IO_18B20 = 0; else IO_18B20 = 1; DelayX10us(6); //延時(shí)60us IO_18B20 = 1; //拉高通信引腳 } EA = 1; //重新使能總中斷 } /* 從DS18B20讀取一個(gè)字節(jié),返回值-讀到的字節(jié) */ uint8 Read18B20() { uint8 dat; uint8 mask; EA = 0; //禁止總中斷 for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次采集8個(gè)bit { IO_18B20 = 0; //產(chǎn)生2us低電平脈沖 _nop_(); _nop_(); IO_18B20 = 1; //結(jié)束低電平脈沖,等待18B20輸出數(shù)據(jù) _nop_(); //延時(shí)2us _nop_(); if (!IO_18B20) //讀取通信引腳上的值 dat &= ~mask; else dat |= mask; DelayX10us(6); //再延時(shí)60us } EA = 1; //重新使能總中斷
return dat; } /* 啟動(dòng)一次18B20溫度轉(zhuǎn)換,返回值-表示是否啟動(dòng)成功 */ bit Start18B20() { bit ack; ack = Get18B20Ack(); //執(zhí)行總線復(fù)位,并獲取18B20應(yīng)答 if (ack == 0) //如18B20正確應(yīng)答,則啟動(dòng)一次轉(zhuǎn)換 { Write18B20(0xCC); //跳過(guò)ROM操作 Write18B20(0x44); //啟動(dòng)一次溫度轉(zhuǎn)換 } return ~ack; //ack==0表示操作成功,所以返回值對(duì)其取反 } /* 讀取DS18B20轉(zhuǎn)換的溫度值,返回值-表示是否讀取成功 */ bit Get18B20Temp(int16 *temp) { bit ack; uint8 LSB, MSB; //16bit溫度值的低字節(jié)和高字節(jié) ack = Get18B20Ack(); //執(zhí)行總線復(fù)位,并獲取18B20應(yīng)答 if (ack == 0) //如18B20正確應(yīng)答,則讀取溫度值 { Write18B20(0xCC); //跳過(guò)ROM操作 Write18B20(0xBE); //發(fā)送讀命令 LSB = Read18B20(); //讀溫度值的低字節(jié) MSB = Read18B20(); //讀溫度值的高字節(jié) *temp = ((int16)MSB << 8) + LSB; //合成為16bit整型數(shù) } return ~ack; //ack==0表示操作應(yīng)答,所以返回值為其取反值 } /***********************Time.h文件程序源代碼*************************/ #ifndef _TIME_H #define _TIME_H
#ifndef _TIME_C extern bit staMute; #endif
void RefreshTime(); void RefreshDate(uint8 ops); void RefreshAlarm(); void AlarmMonitor(); void SwitchSystemSta(); void CancelCurSet(); void SetRightShift(); void SetLeftShift(); void InputSetNumber(uint8 ascii);
#endif /***********************Time.c文件程序源代碼*************************/ #define _TIME_C #include "config.h" #include "DS1302.h" #include "LedBuzzer.h" #include "Lcd1602.h" #include "Time.h" #include "main.h"
uint8 code WeekMod[] = { //星期X字符圖片表 0xFF, 0x99, 0x00, 0x00, 0x00, 0x81, 0xC3, 0xE7, //星期日(紅心) 0xEF, 0xE7, 0xE3, 0xE7, 0xE7, 0xE7, 0xE7, 0xC3, //星期1 0xC3, 0x81, 0x9D, 0x87, 0xC3, 0xF9, 0xC1, 0x81, //星期2 0xC3, 0x81, 0x9D, 0xC7, 0xC7, 0x9D, 0x81, 0xC3, //星期3 0xCF, 0xC7, 0xC3, 0xC9, 0xC9, 0x81, 0xCF, 0xCF, //星期4 0x81, 0xC1, 0xF9, 0xC3, 0x87, 0x9D, 0x81, 0xC3, //星期5 0xC3, 0x81, 0xF9, 0xC3, 0x81, 0x99, 0x81, 0xC3, //星期6 };
bit staMute = 0; //靜音標(biāo)志位 uint8 AlarmHour = 0x07; //鬧鐘時(shí)間的小時(shí)數(shù) uint8 AlarmMin = 0x30; //鬧鐘時(shí)間的分鐘數(shù) struct sTime CurTime; //當(dāng)前日期時(shí)間
uint8 SetIndex = 0; //設(shè)置位索引 uint8 pdata SetAlarmHour; //鬧鐘小時(shí)數(shù)設(shè)置緩沖 uint8 pdata SetAlarmMin; //鬧鐘分鐘數(shù)設(shè)置緩沖 struct sTime pdata SetTime; //日期時(shí)間設(shè)置緩沖區(qū)
/* 獲取當(dāng)前日期時(shí)間,并刷新時(shí)間和星期的顯示 */ void RefreshTime() { GetRealTime(&CurTime); //獲取當(dāng)前日期時(shí)間 ShowLedNumber(5, CurTime.hour>>4, 0); //時(shí) ShowLedNumber(4, CurTime.hour&0xF,1); ShowLedNumber(3, CurTime.min>>4, 0); //分 ShowLedNumber(2, CurTime.min&0xF, 1); ShowLedNumber(1, CurTime.sec>>4, 0); //秒 ShowLedNumber(0, CurTime.sec&0xF, 0); ShowLedArray(WeekMod + CurTime.week*8); //星期 } /* 日期刷新函數(shù),ops-刷新選項(xiàng):為0時(shí)只當(dāng)日期變化才刷新,非0則立即刷新 */ void RefreshDate(uint8 ops) { uint8 pdata str[12]; static uint8 backup = 0; if ((backup!=CurTime.day) || (ops!=0)) { str[0] = ((CurTime.year>>12) & 0xF) + '0'; //4位數(shù)年份 str[1] = ((CurTime.year>>8) & 0xF) + '0'; str[2] = ((CurTime.year>>4) & 0xF) + '0'; str[3] = (CurTime.year & 0xF) + '0'; str[4] = '-'; //分隔符 str[5] = (CurTime.mon >> 4) + '0'; //月份 str[6] = (CurTime.mon & 0xF) + '0'; str[7] = '-'; //分隔符 str[8] = (CurTime.day >> 4) + '0'; //日期 str[9] = (CurTime.day & 0xF) + '0'; str[10] = '\0'; //字符串結(jié)束符 LcdShowStr(0, 0, str); //顯示到液晶上 backup = CurTime.day; //刷新上次日期值 } } /* 刷新鬧鐘時(shí)間的顯示 */ void RefreshAlarm() { uint8 pdata str[8]; LcdShowStr(0, 1, "Alarm at "); //顯示提示標(biāo)題 str[0] = (AlarmHour >> 4) + '0'; //鬧鐘小時(shí)數(shù) str[1] = (AlarmHour & 0xF) + '0'; str[2] = ':'; //分隔符 str[3] = (AlarmMin >> 4) + '0'; //鬧鐘分鐘數(shù) str[4] = (AlarmMin & 0xF) + '0'; str[5] = '\0'; //字符串結(jié)束符 LcdShowStr(9, 1, str); //顯示到液晶上 } /* 鬧鐘監(jiān)控函數(shù),抵達(dá)設(shè)定的鬧鐘時(shí)間時(shí)執(zhí)行鬧鈴 */ void AlarmMonitor() { if ((CurTime.hour==AlarmHour) && (CurTime.min==AlarmMin)) //檢查時(shí)間匹配 { if (!staMute) //檢查是否靜音 staBuzzer = ~staBuzzer; //實(shí)現(xiàn)蜂鳴器斷續(xù)鳴叫 else staBuzzer = 0; } else { staMute = 0; staBuzzer = 0; } } /* 將設(shè)置時(shí)間及標(biāo)題提示顯示到液晶上 */ void ShowSetTime() { uint8 pdata str[18]; str[0] = ((SetTime.year>>4) & 0xF) + '0'; //2位數(shù)年份 str[1] = (SetTime.year & 0xF) + '0'; str[2] = '-'; str[3] = (SetTime.mon >> 4) + '0'; //月份 str[4] = (SetTime.mon & 0xF) + '0'; str[5] = '-'; str[6] = (SetTime.day >> 4) + '0'; //日期 str[7] = (SetTime.day & 0xF) + '0'; str[8] = '-'; str[9] = (SetTime.week & 0xF) + '0'; //星期 str[10] = ' '; str[11] = (SetTime.hour >> 4) + '0'; //小時(shí) str[12] = (SetTime.hour & 0xF) + '0'; str[13] = ':'; str[14] = (SetTime.min >> 4) + '0'; //分鐘 str[15] = (SetTime.min & 0xF) + '0'; str[16] = '\0'; LcdShowStr(0, 0, "Set Date Time"); //顯示提示標(biāo)題 LcdShowStr(0, 1, str); //顯示設(shè)置時(shí)間值 } /* 將設(shè)置鬧鐘及標(biāo)題提示顯示到液晶上 */ void ShowSetAlarm() { uint8 pdata str[8]; str[0] = (SetAlarmHour >> 4) + '0'; //小時(shí) str[1] = (SetAlarmHour & 0xF) + '0'; str[2] = ':'; str[3] = (SetAlarmMin >> 4) + '0'; //分鐘 str[4] = (SetAlarmMin & 0xF) + '0'; str[5] = '\0'; LcdShowStr(0, 0, "Set Alarm"); //顯示提示標(biāo)題 LcdShowStr(0, 1, str); //顯示設(shè)定鬧鐘值 } /* 切換系統(tǒng)運(yùn)行狀態(tài) */ void SwitchSystemSta() { if (staSystem == E_NORMAL) //正常運(yùn)行切換到時(shí)間設(shè)置 { staSystem = E_SET_TIME; SetTime.year = CurTime.year; //當(dāng)前時(shí)間拷貝到時(shí)間設(shè)置緩沖區(qū)中 SetTime.mon = CurTime.mon; SetTime.day = CurTime.day; SetTime.hour = CurTime.hour; SetTime.min = CurTime.min; SetTime.sec = CurTime.sec; SetTime.week = CurTime.week; LcdClearScreen(); //液晶清屏 ShowSetTime(); //顯示設(shè)置時(shí)間 SetIndex = 255; //與接下來(lái)的右移一起將光標(biāo)設(shè)在最左邊的位置上 SetRightShift(); LcdOpenCursor(); //開(kāi)啟光標(biāo) } else if (staSystem == E_SET_TIME) //時(shí)間設(shè)置切換到鬧鐘設(shè)置 { staSystem = E_SET_ALARM; SetTime.sec = 0; //秒清零,即當(dāng)設(shè)置時(shí)間后從0秒開(kāi)始走時(shí) SetRealTime(&SetTime); //設(shè)定時(shí)間寫(xiě)入實(shí)時(shí)時(shí)鐘 SetAlarmHour = AlarmHour; //當(dāng)前鬧鐘值拷貝到設(shè)置緩沖區(qū) SetAlarmMin = AlarmMin; LcdClearScreen(); //液晶清屏 ShowSetAlarm(); //顯示設(shè)置鬧鐘 SetIndex = 255; //與接下來(lái)的右移一起將光標(biāo)設(shè)在最左邊的位置上 SetRightShift(); } else //鬧鐘設(shè)置切換會(huì)正常運(yùn)行 { staSystem = E_NORMAL; AlarmHour = SetAlarmHour; //設(shè)定的鬧鐘值寫(xiě)入鬧鐘時(shí)間 AlarmMin = SetAlarmMin; LcdCloseCursor(); //關(guān)閉光標(biāo) LcdClearScreen(); //液晶清屏 RefreshTime(); //刷新當(dāng)前時(shí)間 RefreshDate(1); //立即刷新日期顯示 RefreshTemp(1); //立即刷新溫度顯示 RefreshAlarm(); //鬧鐘設(shè)定值顯示 } } /* 取消當(dāng)前設(shè)置,返回正常運(yùn)行狀態(tài) */ void CancelCurSet() { staSystem = E_NORMAL; LcdCloseCursor(); //關(guān)閉光標(biāo) LcdClearScreen(); //液晶清屏 RefreshTime(); //刷新當(dāng)前時(shí)間 RefreshDate(1); //立即刷新日期顯示 RefreshTemp(1); //立即刷新溫度顯示 RefreshAlarm(); //鬧鐘設(shè)定值顯示 } /* 時(shí)間或鬧鐘設(shè)置時(shí),設(shè)置位右移一位,到頭后折回 */ void SetRightShift() { if (staSystem == E_SET_TIME) { switch (SetIndex) { case 0: SetIndex=1; LcdSetCursor(1, 1); break; case 1: SetIndex=2; LcdSetCursor(3, 1); break; case 2: SetIndex=3; LcdSetCursor(4, 1); break; case 3: SetIndex=4; LcdSetCursor(6, 1); break; case 4: SetIndex=5; LcdSetCursor(7, 1); break; case 5: SetIndex=6; LcdSetCursor(9, 1); break; case 6: SetIndex=7; LcdSetCursor(11,1); break; case 7: SetIndex=8; LcdSetCursor(12,1); break; case 8: SetIndex=9; LcdSetCursor(14,1); break; case 9: SetIndex=10; LcdSetCursor(15,1); break; default: SetIndex=0; LcdSetCursor(0, 1); break; } } else if (staSystem == E_SET_ALARM) { switch (SetIndex) { case 0: SetIndex=1; LcdSetCursor(1,1); break; case 1: SetIndex=2; LcdSetCursor(3,1); break; case 2: SetIndex=3; LcdSetCursor(4,1); break; default: SetIndex=0; LcdSetCursor(0,1); break; } } } /* 時(shí)間或鬧鐘設(shè)置時(shí),設(shè)置位左移一位,到頭后折回 */ void SetLeftShift() { if (staSystem == E_SET_TIME) { switch (SetIndex) { case 0: SetIndex=10; LcdSetCursor(15,1); break; case 1: SetIndex=0; LcdSetCursor(0, 1); break; case 2: SetIndex=1; LcdSetCursor(1, 1); break; case 3: SetIndex=2; LcdSetCursor(3, 1); break; case 4: SetIndex=3; LcdSetCursor(4, 1); break; case 5: SetIndex=4; LcdSetCursor(6, 1); break; case 6: SetIndex=5; LcdSetCursor(7, 1); break; case 7: SetIndex=6; LcdSetCursor(9, 1); break; case 8: SetIndex=7; LcdSetCursor(11,1); break; case 9: SetIndex=8; LcdSetCursor(12,1); break; default: SetIndex=9; LcdSetCursor(14,1); break; } } else if (staSystem == E_SET_ALARM) { switch (SetIndex) { case 0: SetIndex=3; LcdSetCursor(4,1); break; case 1: SetIndex=0; LcdSetCursor(0,1); break; case 2: SetIndex=1; LcdSetCursor(1,1); break; default: SetIndex=2; LcdSetCursor(3,1); break; } } } /* 輸入設(shè)置數(shù)字,修改對(duì)應(yīng)的設(shè)置位,并顯示該數(shù)字,ascii-輸入數(shù)字的ASCII碼 */ void InputSetNumber(uint8 ascii) { uint8 num; num = ascii - '0'; if (num <= 9) //只響應(yīng)0~9的數(shù)字 { if (staSystem == E_SET_TIME) { switch (SetIndex) { case 0: SetTime.year = (SetTime.year&0xFF0F)|(num<<4); LcdShowChar(0, 1, ascii); break; //年份高位數(shù)字 case 1: SetTime.year = (SetTime.year&0xFFF0)|(num); LcdShowChar(1, 1, ascii); break; //年份低位數(shù)字 case 2: SetTime.mon = (SetTime.mon&0x0F)|(num<<4); LcdShowChar(3, 1, ascii); break; //月份高位數(shù)字 case 3: SetTime.mon = (SetTime.mon&0xF0)|(num); LcdShowChar(4, 1, ascii); break; //月份低位數(shù)字 case 4: SetTime.day = (SetTime.day&0x0F)|(num<<4); LcdShowChar(6, 1, ascii); break; //日期高位數(shù)字 case 5: SetTime.day = (SetTime.day&0xF0)|(num); LcdShowChar(7, 1, ascii); break; //日期低位數(shù)字 case 6: SetTime.week = (SetTime.week&0xF0)|(num); LcdShowChar(9, 1, ascii); break; //星期數(shù)字 case 7: SetTime.hour = (SetTime.hour&0x0F)|(num<<4); LcdShowChar(11,1, ascii); break; //小時(shí)高位數(shù)字 case 8: SetTime.hour = (SetTime.hour&0xF0)|(num); LcdShowChar(12,1, ascii); break; //小時(shí)低位數(shù)字 case 9: SetTime.min = (SetTime.min&0x0F)|(num<<4); LcdShowChar(14,1, ascii); break; //分鐘高位數(shù)字 default:SetTime.min = (SetTime.min&0xF0)|(num); LcdShowChar(15,1, ascii); break; //分鐘低位數(shù)字 } SetRightShift(); //完成該位設(shè)置后自動(dòng)右移 } else if (staSystem == E_SET_ALARM) { switch (SetIndex) { case 0: SetAlarmHour = (SetAlarmHour&0x0F) | (num<<4); LcdShowChar(0,1, ascii); break; //小時(shí)高位數(shù)字 case 1: SetAlarmHour = (SetAlarmHour&0xF0) | (num); LcdShowChar(1,1, ascii); break; //小時(shí)低位數(shù)字 case 2: SetAlarmMin = (SetAlarmMin&0x0F) | (num<<4); LcdShowChar(3,1, ascii); break; //分鐘高位數(shù)字 default:SetAlarmMin = (SetAlarmMin&0xF0) | (num); LcdShowChar(4,1, ascii); break; //分鐘低位數(shù)字 } SetRightShift(); //完成該位設(shè)置后自動(dòng)右移 } } } /***********************main.h文件程序源代碼*************************/ #ifndef _MAIN_H #define _MAIN_H
enum eStaSystem { //系統(tǒng)運(yùn)行狀態(tài)枚舉 E_NORMAL, E_SET_TIME, E_SET_ALARM };
#ifndef _MAIN_C extern enum eStaSystem staSystem; #endif
void RefreshTemp(uint8 ops); void ConfigTimer0(uint16 ms);
#endif /***********************main.c文件程序源代碼*************************/ #define _MAIN_C #include "config.h" #include "Lcd1602.h" #include "LedBuzzer.h" #include "keyboard.h" #include "DS1302.h" #include "DS18B20.h" #include "Infrared.h" #include "Time.h" #include "main.h"
bit flag2s = 0; //2s定時(shí)標(biāo)志位 bit flag200ms = 0; //200ms定時(shí)標(biāo)志 uint8 T0RH = 0; //T0重載值的高字節(jié) uint8 T0RL = 0; //T0重載值的低字節(jié) enum eStaSystem staSystem = E_NORMAL; //系統(tǒng)運(yùn)行狀態(tài)
void main() { EA = 1; //開(kāi)總中斷 ConfigTimer0(1); //配置T0定時(shí)1ms InitLed(); //初始化LED模塊 InitDS1302(); //初始化實(shí)時(shí)時(shí)鐘模塊 InitInfrared(); //初始化紅外接收模塊 InitLcd1602(); //初始化液晶模塊 Start18B20(); //啟動(dòng)首次溫度轉(zhuǎn)換 while (!flag2s); //上電后延時(shí)2秒 flag2s = 0; RefreshTime(); //刷新當(dāng)前時(shí)間 RefreshDate(1); //立即刷新日期顯示 RefreshTemp(1); //立即刷新溫度顯示 RefreshAlarm(); //鬧鐘設(shè)定值顯示 while (1) //進(jìn)入主循環(huán) { KeyDriver(); //執(zhí)行按鍵驅(qū)動(dòng) InfraredDriver(); //執(zhí)行紅外接收驅(qū)動(dòng) if (flag200ms) //每隔200ms執(zhí)行以下分支 { flag200ms = 0; FlowingLight(); //流水燈效果實(shí)現(xiàn) RefreshTime(); //刷新當(dāng)前時(shí)間 AlarmMonitor(); //監(jiān)控鬧鐘 if (staSystem == E_NORMAL) //正常運(yùn)行時(shí)刷新日期顯示 { RefreshDate(0); } } if (flag2s) //每隔2s執(zhí)行以下分支 { flag2s = 0; if (staSystem == E_NORMAL) //正常運(yùn)行時(shí)刷新溫度顯示 { RefreshTemp(0); } } } } /* 溫度刷新函數(shù),讀取當(dāng)前溫度并根據(jù)需要刷新液晶顯示, ** ops-刷新選項(xiàng):為0時(shí)只當(dāng)溫度變化才刷新,非0則立即刷新 */ void RefreshTemp(uint8 ops) { int16 temp; uint8 pdata str[8]; static int16 backup = 0; Get18B20Temp(&temp); //獲取當(dāng)前溫度值 Start18B20(); //啟動(dòng)下一次轉(zhuǎn)換 temp >>= 4; //舍棄4bit小數(shù)位 if ((backup!=temp) || (ops!=0)) //按需要刷新液晶顯示 { str[0] = (temp/10) + '0'; //十位轉(zhuǎn)為ASCII碼 str[1] = (temp%10) + '0'; //個(gè)位轉(zhuǎn)為ASCII碼 str[2] = '\''; //用'C代替℃ str[3] = 'C'; str[4] = '\0'; //字符串結(jié)束符 LcdShowStr(12, 0, str); //顯示到液晶上 backup = temp; //刷新上次溫度值 } } /* 配置并啟動(dòng)T0,ms-T0定時(shí)時(shí)間 */ void ConfigTimer0(uint16 ms) { uint32 tmp; tmp = (SYS_MCLK*ms)/1000; //計(jì)算所需的計(jì)數(shù)值 tmp = 65536 - tmp; //計(jì)算定時(shí)器重載值 tmp = tmp + 34; //補(bǔ)償中斷響應(yīng)延時(shí)造成的誤差 T0RH = (uint8)(tmp>>8); //定時(shí)器重載值拆分為高低字節(jié) T0RL = (uint8)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0為模式1 TH0 = T0RH; //加載T0重載值 TL0 = T0RL; ET0 = 1; //使能T0中斷 TR0 = 1; //啟動(dòng)T0 } /* T0中斷服務(wù)函數(shù),實(shí)現(xiàn)系統(tǒng)定時(shí)和按鍵掃描 */ void InterruptTimer0() interrupt 1 { static uint8 tmr2s = 0; static uint8 tmr200ms = 0; TH0 = T0RH; //定時(shí)器重新加載重載值 TL0 = T0RL; tmr200ms++; //定時(shí)200ms if (tmr200ms >= 200) { tmr200ms = 0; flag200ms = 1; tmr2s++; //定時(shí)2s if (tmr2s >= 10) { tmr2s = 0; flag2s = 1; } } KeyScan(); //執(zhí)行按鍵掃描 } 程序代碼已經(jīng)完成了,但是大家的學(xué)習(xí)還得繼續(xù),把思路學(xué)差不多之后,要自己能夠不看源代碼,獨(dú)立把這個(gè)程序編寫(xiě)出來(lái),那么我就可以很高興的告訴你,你的單片機(jī)已經(jīng)合格了,你可以動(dòng)手開(kāi)發(fā)一些小產(chǎn)品,進(jìn)入下一個(gè)層次的歷練了。 當(dāng)然了,同學(xué)們不要指望這樣的代碼一下子寫(xiě)出來(lái)就好用,包括我們研發(fā)工程師,調(diào)試這種代碼也是一步步來(lái)的,在調(diào)試的過(guò)程中,可能還要穿插修改很多之前寫(xiě)好的代碼,協(xié)調(diào)功能工作等等。同學(xué)們?nèi)绻?dú)立寫(xiě)這種代碼,3天到一個(gè)周調(diào)試出來(lái)還是比較正常的。學(xué)到這里,相信同學(xué)們對(duì)于做技術(shù)的基本耐性已經(jīng)具備了。做技術(shù),耐心、細(xì)心、恒心,缺一不可。不要像初學(xué)那樣遇到一個(gè)問(wèn)題動(dòng)不動(dòng)就浮躁了,慢慢來(lái),最終把這個(gè)功能實(shí)現(xiàn)出來(lái),完成你單片機(jī)之路的第一個(gè)項(xiàng)目。 作業(yè) 1、學(xué)會(huì)使用類型說(shuō)明定義新類型,能夠區(qū)別typedef和#define。 2、學(xué)會(huì)建立編寫(xiě)頭文件,并且掌握頭文件的格式。 3、掌握條件編譯的用法 4、獨(dú)立將多功能電子鐘項(xiàng)目開(kāi)發(fā)的代碼完成。 上一課: 第18章 RS485通信和Modbus協(xié)議
下一課: 第20章 單片機(jī)開(kāi)發(fā)常用工具的使用 |