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