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