本章內(nèi)容主要通過一些相關(guān)例程,來提高大家的編程技巧,并且?guī)椭蠹疫M行一些算法上的積累。同學(xué)們在做這部分內(nèi)容的時候,還是那句話,一定要能夠達到不看教程,獨立把程序做出來的效果,那樣才能基本上掌握相關(guān)知識點和內(nèi)容。 10.1 數(shù)字秒表實驗10.1.1 不同數(shù)據(jù)間的類型轉(zhuǎn)換在C語言中,不同數(shù)據(jù)類型之間是可以混合運算的。當表達式中的數(shù)據(jù)類型不一致時,首先轉(zhuǎn)換為同一種類型,然后再進行計算。C語言有兩種方法實現(xiàn)類型轉(zhuǎn)換,一是自動類型轉(zhuǎn)換,另外一種是強制類型轉(zhuǎn)換。這塊內(nèi)容是比較繁雜的,因此我們根據(jù)我們常用的編程應(yīng)用來講部分相關(guān)內(nèi)容。 當不同數(shù)據(jù)類型之間混合運算的時候,不同類型的數(shù)據(jù)首先會轉(zhuǎn)換為同一類型,轉(zhuǎn)換的主要原則是:短字節(jié)的數(shù)據(jù)向長字節(jié)數(shù)據(jù)轉(zhuǎn)換。 比如:unsigned char a ; unsigned int b; unsigned int c; c = a *b; 在運算的過程中,程序會自動全部按照unsigned int型來計算。比如a=10,b=200,c的結(jié)果就是2000。那當a=100,b=700,那c是70000嗎?新手最容易犯這種錯誤,大家要注意每個變量的數(shù)據(jù)類型,c的數(shù)據(jù)類型是unsigned int型,取值范圍是0~65535,70000超過65535溢出了,所以最終c的結(jié)果是(70000 - 65536) = 4464。 那要想讓c正常獲得70000這個結(jié)果,需要把c定義成一個unsigned long型。我們?nèi)绻麑懗桑?/font>unsigned char a=100; unsigned int b=700; unsigned long c=0; c = a *b;如果有做過實驗的同學(xué),會發(fā)現(xiàn)這個c的結(jié)果還是4464,這個是個什么情況呢? 大家注意,C語言不同類型運算的時候數(shù)值會轉(zhuǎn)換同一類型運算,但是每一步運算都會進行識別判斷,不會進行一個總的分析判斷。比如我們這個程序,a和b相乘的時候,是按照unsigned int類型運算的,運算的結(jié)果也是unsigned int類型的4464,只是最終把unsigned int類型4464賦值給了一個unsigned long型的變量而已。我們在運算的時候如何避免這類問題的產(chǎn)生呢?可以采用強制類型轉(zhuǎn)換的方法。 在一個變量前邊加上一個變量類型,并且這個變量類型用小括號括起來,表示把這個變量強制轉(zhuǎn)換成括號里的變量類型。如 c = (unsigned long)a * b;由于強制類型轉(zhuǎn)換運算優(yōu)先級高于*,所以這個地方的運算是先把a轉(zhuǎn)換成一個unsigned long型的變量,而后與b相乘,根據(jù)C語言的規(guī)則b會自動轉(zhuǎn)換成一個unsigned long型的變量,而后運算完畢結(jié)果也是一個unsigned long型的,最終賦值給了c。 當不同類型變量相互賦值時,短字節(jié)的數(shù)據(jù)向長字節(jié)的變量賦值時,值不變,比如unsigned char a=100; unsigned int b=700; b = a;那么最終b的值就是100了。但是如果我們的程序是unsigned char a=100; unsigned int b=700; a=b;那么a的值僅僅是取了b的低8位,我們首先要把700變成一個16位的二進制數(shù)據(jù),然后取它的低8位出來,也就是188,這就是長字節(jié)給短字節(jié)賦值的結(jié)果。 在51單片機里邊,有一種特殊情況,就是bit類型的變量,這個bit類型的強制類型轉(zhuǎn)換,是不符合上邊講的這個原則的,比如bit a = 0; unsigned char b; a = (bit)b;這個地方要特別注意,使用bit做強制類型轉(zhuǎn)換,不是取b的最低位,而是他會判斷b這個變量是0還是非0的值,如果b是0,那么a的結(jié)果就是0,如果b是任意非0的其他數(shù)字,那么a的結(jié)果都是1。 1.1.2 定時時間精準性調(diào)整我們的6.5.2章節(jié)有一個數(shù)碼管秒表顯示程序,那個程序是1秒數(shù)碼管加1,但是細心的同學(xué)做了實驗后,經(jīng)過長時間運行會發(fā)現(xiàn),和我們的實際的時間有了較大誤差了,那如何去調(diào)整這種誤差呢?要解決問題,先找到問題是什么原因造成的。 先補充介紹一下我們我們前邊講的中斷的內(nèi)容。其實單片機做的很智能的,當我們在看電視的時候,突然發(fā)生了水開的中斷,我們必須去提水的時候,第一,我們從電視跟前跑到廚房需要一定的時間,第二,因為我們看的電視是智能數(shù)字電視,因此在去提水之前我們可以使用遙控器將我們的電視進行暫停操作,方便回來后繼續(xù)從剛才的劇情往下進行。那暫停電視,跑到廚房提水,這一點點時間是很短的,在實際生活中可以忽略不計,但是在單片機秒表系統(tǒng),誤差會累計的,每1秒鐘都差了幾個微妙,時間一久,造成的累計誤差就不可小覷了。 單片機系統(tǒng)里,硬件進入中斷需要一定的時間,大概是幾個機器周期,還有要進行原始數(shù)據(jù)保護,就是把進中斷之前程序運行的一些變量先保存起來,這個專業(yè)詞匯叫做中斷壓棧,進入中斷后,重新給定時器TH和TL賦值,也需要幾個機器周期,這樣下來就會消耗一定的時間,我們得把這些時間補償回來。 方法一,使用軟件debug進行補償。 我們前邊教程講過使用debug來觀察程序運行時間,那我們可以把我們2次進入中斷的時間間隔觀察出來,看看和我們實際定時的時間相差了幾個機器周期,然后在進行定時器初值賦值的時候,進行一個調(diào)整。我們用的是11.0592M的晶振,發(fā)現(xiàn)差了幾個機器周期,就把定時器初值加上幾個機器周期,這樣相當于進行了一個補償。 方法二,使用累計誤差計算出來。 有的時候,除了程序本身存在的誤差外,硬件精度也可能會影響到時鐘的精度,比如晶振,會隨著溫度變化出現(xiàn)溫漂現(xiàn)象,就是精度和標稱值要差一點。那么我們還可以采取累計誤差的方法來提高精度。比如我們可以讓時鐘運行半個小時或者一個小時,看看最終時間差了幾秒,然后算算一共進了多少次定時器中斷,然后把這差的幾秒平均分配到每次的定時器中斷中,就可以實現(xiàn)時鐘的調(diào)整。 大家要明白,這個世界上本就沒有絕對的精度,我們只能提高精度,但是不可能消除誤差的,如果在這個基礎(chǔ)上還感覺精度不夠的話,不要著急,后邊我們會專門講時鐘芯片的,通常時鐘芯片計時的精度比單片機的精度要高一些。 10.1.3 使用字節(jié)操作修改位的技巧這里介紹個編程小技巧,在我們編程序的時候,有的情況下,想要操作一個字節(jié)中的某一位或者幾位的時候,但是又不想改變其他位原有的值,該如何操作呢? 比如我們學(xué)定時器的時候遇到一個寄存器TCON,這個寄存器是可以進行位操作的,比如我們可以直接寫TR0 =1;TR0是TCON的一個位,因為這個寄存器是允許位操作,這樣寫是沒有任何問題的。還有一個寄存器TMOD,這個寄存器是不支持位操作的,那如果我們要使用T0的模式1,我們希望達到的效果是TMOD的低4位是0001,如果我們直接寫成TMOD = 0x01的話,實際上已經(jīng)同時操作到了高4位,即屬于T1的部分,設(shè)置成了0000,如果T1定時器沒有用到的話,那我們隨便怎么樣都行,但是如果程序中既用到了T0,又用到了T1,那我們設(shè)置T0的同時已經(jīng)干擾到了T1的模式配置,這我們不希望看到的結(jié)果。 在這種情況下,就可以用我們前邊學(xué)過的"&"和"|"運算了。對于二進制位操作來說,不管該位原來的值是0還是1,它跟"0"進行"&&"運算,得到的結(jié)果都是0,而跟"1"進行"&&"運算,將保持原來的值不變;不管該位原來的值是0還是1,它跟"1"進行"||"運算,得到的結(jié)果都是1,而跟"0"進行"||"運算,將保持原來的值不變。 利用上述這個規(guī)律,我們就可以著手解決剛才的問題了。如果我們現(xiàn)在要設(shè)置TMOD的定時器0工作在模式1下,又不干擾定時器1的配置,我們可以進行這樣的操作:TMOD = TMOD & 0xF0; TMOD = TMOD | 0x01;第一步與0xF0后,TMOD的高4位不變,低4位清零,變成了xxxx0000;然后再進行第二步和0x01進行或運算,那高7位均不變,最低位變成1了,這樣就完成了只將低4位的值修改位0001,而高4位保持原不變的任務(wù),即只設(shè)置了T0而不影響T1。熟練掌握并靈活運用這個方法,會給你以后的編程帶來便利。 另外,在C語言中,a &= b;等價于a = a&b;同理,a |= b;等價于a = a|b;那么前邊那一段代碼就可以寫成TMOD &= 0xF0;TMOD |= 0x01這樣的簡寫形式。這種寫法可以一定程度上簡化代碼,是C語言的一種編程風格。 10.1.4 數(shù)碼管刷新函數(shù)算法改進在學(xué)習(xí)數(shù)碼管動態(tài)刷新的時候,為了方便大家理解,我們程序?qū)懙募氈乱恍,給大家引入了switch的用法,隨著我們編程能力的增強,對于74HC138這種非常有規(guī)律的數(shù)字器件,我們在編程上也可以改進一下邏輯算法,讓程序變的更簡潔。這種邏輯算法,通常不是靠學(xué)一下可以全部掌握的,而是通過不斷的編寫程序以及研究別人的程序一點點積累起來的,從今天開始,大家就要開始積累。 前邊動態(tài)刷新函數(shù)我們是這么寫的: switch(j) { case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]]; break; case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]]; break; case 2: ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]]; break; case 3: ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]]; break; case 4: ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]]; break; case 5: ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]]; break; default: break; } 首先我們進行第一步改進,寫成: switch(j) { case 0: ADDR0=0; ADDR1=0; ADDR2=0; break; case 1: ADDR0=1; ADDR1=0; ADDR2=0; break; case 2: ADDR0=0; ADDR1=1; ADDR2=0; break; case 3: ADDR0=1; ADDR1=1; ADDR2=0; break; case 4: ADDR0=0; ADDR1=0; ADDR2=1; break; case 5: ADDR0=1; ADDR1=0; ADDR2=1; break; default: break; } P0=LedChar[LedNumber[j++]]; if(6==j) j=0; 這種寫法已經(jīng)比上邊那種寫法簡單多了,我們還要繼續(xù)簡化。我們來看,ADDR0是P1的第0位,ADDR1是P1的第1位,ADDR2是P1的第2位,我們可以看出來,程序中的case 0到case 5的過程中,P1的這低3位的值分別是000,001,010,011,100,101。轉(zhuǎn)換成十進制,也就是從0到5。那我們程序就可以進一步改進,寫成以下函數(shù)形式: void LedScan() //LED顯示掃描函數(shù) { static unsigned char index = 0; P0 = 0xFF; //關(guān)閉所有段選位,顯示消隱 P1 = (P1 & 0xF8) | index; //位選索引值賦值到P1口低3位 P0 = LedNumber[index]; //相應(yīng)顯示緩沖區(qū)的值賦值到P0口 if (index < 5) //位選索引0-5循環(huán),因有6個數(shù)碼管 index++; else index = 0; } 大家看看,P1 = (P1 & 0xF8) | index;這行代碼就利用了上面講到的"&"和"|"運算來將index的低3位直接賦值到P1口的低3位上,這樣寫是不是要簡潔的多,也巧妙的多,同樣可以完美實現(xiàn)動態(tài)刷新的功能。 10.1.5 秒表程序做了一個秒表程序給同學(xué)們做參考,程序中涉及到的知識點我們幾乎都講過了,涉及到了定時器、數(shù)碼管、中斷、按鍵等多個知識點。此程序是多知識點同時應(yīng)用到一個程序中的小綜合,因此需要大家完全消化掉。這種小綜合也是將來做大項目程序的一個基礎(chǔ),因此還是老規(guī)矩,大家邊抄邊理解,理解透徹后獨立寫出來就算此關(guān)通過。 #include <reg52.h>
sbit KEY1 = P2^4;
sbit KEY2 = P2^5;
sbit KEY3 = P2^6;
sbit KEY4 = P2^7;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
unsigned char code LedChar[] = { //數(shù)碼管顯示字符轉(zhuǎn)換表
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[6] = { //數(shù)碼管顯示緩沖區(qū)
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
unsigned char KeySta[4] = { //按鍵狀態(tài)緩沖區(qū)
1, 1, 1, 1
};
bit StopwatchRunning = 0; //秒表運行標志
bit StopwatchRefresh = 1; //秒表計數(shù)刷新標志
unsigned char DecimalPart = 0; //秒表的小數(shù)部分
unsigned int IntegerPart = 0; //秒表的整數(shù)部分
unsigned char T0RH = 0; //T0重載值的高字節(jié)
unsigned char T0RL = 0; //T0重載值的低字節(jié)
void ConfigTimer0(unsigned int ms);
void StopwatchDisplay();
void KeyAction();
void main ()
{
P2 = 0xFE; //選擇第4行按鍵以進行掃描
P0 = 0xFF; //P0口初始化
ADDR3 = 1; //選擇數(shù)碼管
ENLED = 0; //LED總使能
EA = 1; //開總中斷
ConfigTimer0(2); //配置T0定時2ms
while(1)
{
KeyAction();
StopwatchDisplay();
}
}
void ConfigTimer0(unsigned int ms) //T0配置函數(shù)
{
unsigned long tmp;
tmp = 11059200 / 12; //定時器計數(shù)頻率
tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值
tmp = 65536 - tmp; //計算定時器重載值
tmp = tmp + 19; //修正中斷響應(yīng)延時造成的誤差,運行30分鐘修正值
T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié)
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0為模式1
TH0 = T0RH; //加載T0重載值
TL0 = T0RL;
ET0 = 1; //使能T0中斷
TR0 = 1; //啟動T0
}
void StopwatchDisplay() //秒表計數(shù)顯示函數(shù)
{
unsigned char i;
unsigned char buff[6];
if (StopwatchRefresh)
{
StopwatchRefresh = 0;
i = DecimalPart % 10; //小數(shù)部分轉(zhuǎn)換到低2位
buff[0] = LedChar[ I];
i = DecimalPart / 10;
buff[1] = LedChar[ I];
buff[2] = IntegerPart % 10; //整數(shù)部分轉(zhuǎn)換到高4位
buff[3] = (IntegerPart / 10) % 10;
buff[4] = (IntegerPart / 100) % 10;
buff[5] = (IntegerPart / 1000) % 10;
for (i=5; i>=3; i--) //高位的0轉(zhuǎn)換為空字符
{
if (buff[ I] == 0)
buff[ I] = 0xFF;
else
break;
}
for ( ; i>=2; i--) //有效數(shù)字位轉(zhuǎn)換顯示字符
{
buff[ I] = LedChar[buff[ I]];
}
buff[2] &= 0x7F; //點亮小數(shù)點
for (i=0; i<=5; i++) //一次性拷貝到顯示緩沖區(qū),消除可能存在的顯示抖動
{
LedBuff[ I] = buff[ I];
}
}
}
void StopwatchAction() //秒表啟停函數(shù)
{
if (StopwatchRunning) //已啟動則停止
StopwatchRunning = 0;
else //未啟動則啟動
StopwatchRunning = 1;
}
void StopwatchReset() //秒表復(fù)位函數(shù)
{
StopwatchRunning = 0; //停止秒表
DecimalPart = 0; //清零計數(shù)值
IntegerPart = 0;
StopwatchRefresh = 1; //置刷新標志
}
void KeyAction() //按鍵動作函數(shù)
{
unsigned char i;
static unsigned char backup[4] = {1,1,1,1};
for (i=0; i<4; i++)
{
if (backup[ I] != KeySta[ I])
{
if (backup[ I] != 0) //按鍵按下時執(zhí)行動作
{
switch (i)
{
case 1: StopwatchReset(); break; //Esc鍵復(fù)位秒表
case 2: StopwatchAction(); break; //回車鍵啟停秒表
default: break;
}
}
backup[ I] = KeySta[ I];
}
}
}
void LedScan() //LED顯示掃描函數(shù)
{
static unsigned char index = 0;
P0 = 0xFF; //關(guān)閉所有段選位,顯示消隱
P1 = (P1 & 0xF8) | index; //位選索引值賦值到P1口低3位
P0 = LedBuff[index]; //相應(yīng)顯示緩沖區(qū)的值賦值到P0口
if (index < 5) //位選索引0-5循環(huán),因有6個數(shù)碼管
index++;
else
index = 0;
}
void KeyScan() //按鍵掃描函數(shù)
{
unsigned char i;
static unsigned char keybuf[4] = { //按鍵掃描緩沖區(qū),保存一段時間內(nèi)的掃描值
0xFF, 0xFF, 0xFF, 0xFF
};
//按鍵值移入緩沖區(qū)
keybuf[0] = (keybuf[0] << 1) | KEY1;
keybuf[1] = (keybuf[1] << 1) | KEY2;
keybuf[2] = (keybuf[2] << 1) | KEY3;
keybuf[3] = (keybuf[3] << 1) | KEY4;
//消抖后更新按鍵狀態(tài)
for (i=0; i<4; i++)
{
if (keybuf[ I] == 0x00)
{
KeySta[ I] = 0;
}
else if (keybuf[ I] == 0xFF)
{
KeySta[ I] = 1;
}
}
}
void StopwatchCount() //秒表計數(shù)函數(shù)
{
if (StopwatchRunning)
{
DecimalPart++; //小數(shù)部分+1
if (DecimalPart >= 100) //小數(shù)部分計到100時進位到整數(shù)部分
{
DecimalPart = 0;
IntegerPart++;
if (IntegerPart >= 10000) //整數(shù)部分計到10000時歸零
{
IntegerPart = 0;
}
}
StopwatchRefresh = 1;
}
}
void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù)
{
static unsigned char tmr10ms = 0;
TH0 = T0RH; //將重載值賦值到計數(shù)器
TL0 = T0RL;
KeyScan(); //按鍵掃描
LedScan(); //數(shù)碼管掃描顯示
//定時10ms進行一次秒表計數(shù)
tmr10ms++;
if (tmr10ms >= 5)
{
tmr10ms = 0;
StopwatchCount();
}
}10.2 PWM的學(xué)習(xí)PWM在我們今后的單片機應(yīng)用中非常非常多,應(yīng)用的方向也很多,它的原理很簡單,但是往往應(yīng)用于不同場合上意義不完全一樣,這里我先把基本概念和基本原理給大家介紹一下,后邊遇到用的時候起碼知道是個什么東西。 PWM是Pulse Width Modulation的縮寫,它的中文名字是脈沖寬度調(diào)制,一種說法是它利用微處理器的數(shù)字輸出來對模擬電路進行控制的一種有效的技術(shù),其實就是使用數(shù)字信號達到一個模擬信號的效果。這是個什么概念呢?我們一步步來介紹。 首先從它的名字來看,脈沖寬度調(diào)制,就是改變脈沖寬度來實現(xiàn)不同的效果。我們先來看三組不同的脈沖信號,如圖10-1所示。
1.JPG (34.26 KB, 下載次數(shù): 195)
下載附件
2013-9-28 01:42 上傳
圖10-1 PWM波形 這是一個周期是10ms,即頻率是100Hz的波形,但是每個周期內(nèi),高低電平脈沖各不相同,這就是PWM的本質(zhì)。在這里大家要記住一個概念,叫做“占空比”。占空比是指高電平的時間占整個周期的比例。比如第一部分波形的占空比是40%,第二部分波形占空比是60%,第三部分波形占空比是80%,這就是PWM的解釋。 那為何它會對模擬電路進行控制呢?大家想一想,我們數(shù)字電路里,只有0和1兩種狀態(tài),比如我們教程第二課學(xué)會的點亮LED小燈那個程序,當我們寫一個LED = 0;小燈就會長亮,當我們寫一個LED = 1;小燈就會滅掉。當我們讓小燈亮和滅間隔運行的時候,小燈是閃爍。如果我們把這個間隔不斷的減小,減小到我們的肉眼分辨不出來,也就是100Hz以上的頻率,這個時候小燈表現(xiàn)出來的現(xiàn)象就是既保持亮的狀態(tài),但是亮度沒有LED = 0;的時候亮度高。那我們不斷改變時間參數(shù),讓LED = 0;的時間大于或者小于LED = 1;的時間,會發(fā)現(xiàn)亮度都不一樣,這就是模擬電路的感覺了,不再是純粹的0和1,還有亮度不斷變化。大家會發(fā)現(xiàn),如果我們是100Hz的信號,如圖10-1所示,假如高電平熄滅小燈,低電平點亮小燈的話,第一部分波形熄滅4ms,點亮6ms,亮度最高,第二部分熄滅6ms,點亮4ms,亮度次之,第三部分熄滅8ms,點亮2ms,亮度最低。我們用程序驗證一下。 #include <reg52.h> sbit PWMOUT = P0^0; sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; unsigned char HReloadH = 0; //高電平重載值的高字節(jié) unsigned char HReloadL = 0; //高電平重載值的低字節(jié) unsigned char LReloadH = 0; //低電平重載值的高字節(jié) unsigned char LReloadL = 0; //低電平重載值的低字節(jié) void ConfigPWM(unsigned int fr, unsigned char dc); void ClosePWM(); void main () { unsigned int i; P0 = 0xFF; //P0口初始化 ADDR0 = 0; //選擇獨立LED ADDR1 = 1; ADDR2 = 1; ADDR3 = 1; ENLED = 0; //LED總使能 EA = 1; //開總中斷 while(1) { ConfigPWM(100, 10); //頻率100Hz,占空比10% for (i=0; i<40000; i++); ClosePWM(); ConfigPWM(100, 40); //頻率100Hz,占空比40% for (i=0; i<40000; i++); ClosePWM(); ConfigPWM(100, 90); //頻率100Hz,占空比90% for (i=0; i<40000; i++); ClosePWM(); for (i=0; i<40000; i++); } } void ConfigPWM(unsigned int fr, unsigned char dc) //PWM配置函數(shù),fr-頻率,dc-占空比 { unsigned int high, low; unsigned long tmp; tmp = (11059200 / 12) / fr; //計算一個周期所需的計數(shù)值 high = (tmp * dc) / 100; //計算高電平所需的計數(shù)值 low = tmp - high; //計算低電平所需的計數(shù)值 high = 65536 - high + 13; //計算高電平的定時器重載值并修正 low = 65536 - low + 13; //計算低電平的定時器重載值并修正 HReloadH = (unsigned char)(high >> 8); //高電平重載值拆分為高低字節(jié) HReloadL = (unsigned char)high; LReloadH = (unsigned char)(low >> 8); //低電平重載值拆分為高低字節(jié) LReloadL = (unsigned char)low; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0為模式1 TH0 = HReloadH; //加載T0重載值 TL0 = HReloadL; ET0 = 1; //使能T0中斷 TR0 = 1; //啟動T0 PWMOUT = 1; //輸出高電平 } void ClosePWM() //關(guān)閉PWM { TR0 = 0; //停止定時器 ET0 = 0; PWMOUT = 1; //輸出高電平 } void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù) { if (PWMOUT == 1) //當前輸出為高電平時,裝載低電平值并輸出低電平 { TH0 = LReloadH; TL0 = LReloadL; PWMOUT = 0; } else //當前輸出為低電平時,裝載高電平值并輸出高電平 { TH0 = HReloadH; TL0 = HReloadL; PWMOUT = 1; } } 大家下載了這個程序,會發(fā)現(xiàn)小燈從最亮到滅一共4個亮度等級。如果我們讓亮度等級更多,并且讓亮度等級連續(xù)起來,會產(chǎn)生一個小燈漸變的效果,和人呼吸有點類似,所以我們習(xí)慣上稱之為呼吸燈,程序代碼如下,這個程序用了2個定時器2個中斷,這是我們第一次這樣用,大家可以學(xué)習(xí)一下。我們來試試這個程序,試完了大家一定要自己關(guān)閉教程把程序?qū)懗鰜恚杏洝?/font> #include <reg52.h> sbit PWMOUT = P0^0; sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; unsigned long PeriodCnt = 0; //PWM周期計數(shù)值 unsigned char HReloadH = 0; //高電平重載值的高字節(jié) unsigned char HReloadL = 0; //高電平重載值的低字節(jié) unsigned char LReloadH = 0; //低電平重載值的高字節(jié) unsigned char LReloadL = 0; //低電平重載值的低字節(jié) unsigned char T1RH = 0; //T1重載值的高字節(jié) unsigned char T1RL = 0; //T1重載值的低字節(jié) void ConfigTimer1(unsigned int ms); void ConfigPWM(unsigned int fr, unsigned char dc); void main () { P0 = 0xFF; //P0口初始化 ADDR0 = 0; //選擇獨立LED ADDR1 = 1; ADDR2 = 1; ADDR3 = 1; ENLED = 0; //LED總使能 EA = 1; //開總中斷 ConfigPWM(100, 10); //配置并啟動PWM ConfigTimer1(50); //T1定時調(diào)整占空比 while(1); } void ConfigTimer1(unsigned int ms) //T1配置函數(shù) { unsigned long tmp; tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 11; //修正中斷響應(yīng)延時造成的誤差 T1RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) T1RL = (unsigned char)tmp; TMOD &= 0x0F; //清零T1的控制位 TMOD |= 0x10; //配置T1為模式1 TH1 = T1RH; //加載T1重載值 TL1 = T1RL; ET1 = 1; //使能T1中斷 TR1 = 1; //啟動T1 } void ConfigPWM(unsigned int fr, unsigned char dc) //PWM配置函數(shù),fr-頻率,dc-占空比 { unsigned int high, low; PeriodCnt = (11059200 / 12) / fr; //計算一個周期所需的計數(shù)值 high = (PeriodCnt * dc) / 100; //計算高電平所需的計數(shù)值 low = PeriodCnt - high; //計算低電平所需的計數(shù)值 high = 65536 - high + 13; //計算高電平的定時器重載值并修正 low = 65536 - low + 13; //計算低電平的定時器重載值并修正 HReloadH = (unsigned char)(high >> 8); //高電平重載值拆分為高低字節(jié) HReloadL = (unsigned char)high; LReloadH = (unsigned char)(low >> 8); //低電平重載值拆分為高低字節(jié) LReloadL = (unsigned char)low; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0為模式1 TH0 = HReloadH; //加載T0重載值 TL0 = HReloadL; ET0 = 1; //使能T0中斷 TR0 = 1; //啟動T0 PWMOUT = 1; //輸出高電平 } void AdjustDutyCycle(unsigned char dc) //占空比調(diào)整函數(shù),頻率不變只調(diào)整占空比 { unsigned int high, low; high = (PeriodCnt * dc) / 100; //計算高電平所需的計數(shù)值 low = PeriodCnt - high; //計算低電平所需的計數(shù)值 high = 65536 - high + 13; //計算高電平的定時器重載值并修正 low = 65536 - low + 13; //計算低電平的定時器重載值并修正 HReloadH = (unsigned char)(high >> 8); //高電平重載值拆分為高低字節(jié) HReloadL = (unsigned char)high; LReloadH = (unsigned char)(low >> 8); //低電平重載值拆分為高低字節(jié) LReloadL = (unsigned char)low; } void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù),產(chǎn)生PWM { if (PWMOUT == 1) //當前輸出為高電平時,裝載低電平值并輸出低電平 { TH0 = LReloadH; TL0 = LReloadL; PWMOUT = 0; } else //當前輸出為低電平時,裝載高電平值并輸出高電平 { TH0 = HReloadH; TL0 = HReloadL; PWMOUT = 1; } } void InterruptTimer1() interrupt 3 //T1中斷服務(wù)函數(shù),定時動態(tài)調(diào)整占空比 { static bit br = 0; static unsigned char index = 0; unsigned char code table[13] = { //占空比調(diào)整表 5, 18, 30, 41, 51, 60, 68, 75, 81, 86, 90, 93, 95 }; TH1 = T1RH; //重新加載T1重載值 TL1 = T1RL; AdjustDutyCycle(table[index]); //調(diào)整PWM的占空比 if (br == 0) //逐步增大占空比 { index++; if (index >= 12) { br = 1; } } else //逐步減小占空比 { index--; if (index == 0) { br = 0; } } } 呼吸燈寫出來后,其他各種效果的燈光閃爍都應(yīng)該可以做出來,大家看到的KTV里邊那絢麗的燈光閃爍,其實就是采用的PWM技術(shù)控制的。 10.3 交通燈實驗同學(xué)們在學(xué)習(xí)技術(shù)的時候,一定要多動腦筋,遇到問題后,三思而后問。有些時候你考慮的和真理就差一點點了,沒有堅持下去,別人告訴你后你才恍然大悟。這樣得到的結(jié)論,可以讓你學(xué)到知識,但是卻培養(yǎng)不了你的邏輯思維能力。不是不能問,而是要在認真思考的基礎(chǔ)上提問。 有同學(xué)有疑問,板子上只有8個流水燈,那如果我要做很多個流水燈一起花樣顯示怎么辦呢?那我們在講課的時候其實都提到過了,板子上是有8個流水燈,還有6個數(shù)碼管,還有1個點陣LED,一個數(shù)碼管相當于8個小燈,一個點陣LED相當于64個小燈,那如果全部算上的話,我們板子上實際共接了8+6*8+64=120個小燈,你如果單獨只接小燈,花樣燈就做出來了。 還有同學(xué)問,板子上流水燈和數(shù)碼管可以一起工作嗎?如何一起工作呢?我們剛說了,一個數(shù)碼管是8個小燈,但是大家反過來想一想,8個流水燈是不是相當于一個數(shù)碼管嗎。那板子上6個數(shù)碼管我們可以讓他們同時亮,7個數(shù)碼管就不會了嗎?當然了,思考的習(xí)慣是要慢慢培養(yǎng)的,想不到的同學(xué)繼續(xù)努力,每天前進一小步,堅持一段時間后回頭看看,就會發(fā)現(xiàn)你學(xué)會了很多。 發(fā)一個交通燈的程序給大家做學(xué)習(xí)參考。因為板子資源有限,所以我把左邊LED8和LED9一起亮作為綠燈,把LED5和LED6一起亮作為黃燈,把LED2和LED3一起亮作為紅燈,用數(shù)碼管做倒計時,做了一個簡易的交通燈程序給大家做參考學(xué)習(xí),讓LED和數(shù)碼管同時參與工作。 #include <reg52.h> sbit KEY1 = P2^4; sbit KEY2 = P2^5; sbit KEY3 = P2^6; sbit KEY4 = P2^7; sbit ADDR3 = P1^3; sbit ENLED = P1^4; unsigned char code LedChar[] = { //數(shù)碼管顯示字符轉(zhuǎn)換表 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; unsigned char LedBuff[7] = { //數(shù)碼管+獨立LED顯示緩沖區(qū) 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; bit flag1s = 1; //1秒定時標志 unsigned char T0RH = 0; //T0重載值的高字節(jié) unsigned char T0RL = 0; //T0重載值的低字節(jié) void ConfigTimer0(unsigned int ms); void TrafficLight(); void main () { P2 = 0xFE; //選擇第4行按鍵以進行掃描 P0 = 0xFF; //P0口初始化 ADDR3 = 1; //選擇數(shù)碼管 ENLED = 0; //LED總使能 EA = 1; //開總中斷 ConfigTimer0(1); //配置T0定時1ms while(1) { if (flag1s) { //每秒執(zhí)行一次 flag1s = 0; TrafficLight(); } } } void ConfigTimer0(unsigned int ms) //T0配置函數(shù) { unsigned long tmp; tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 17; //修正中斷響應(yīng)延時造成的誤差 T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0為模式1 TH0 = T0RH; //加載T0重載值 TL0 = T0RL; ET0 = 1; //使能T0中斷 TR0 = 1; //啟動T0 } void TrafficLight() { static unsigned char color = 2; //交通燈顏色索引,0-綠色,1-黃色,2-紅色 static unsigned char timer = 0; //交通燈倒計時定時器 if (timer == 0) //倒計時到0時,切換交通燈 { switch (color) { //左端兩個LED代表綠燈,中間兩個LED代表黃燈,右端兩個LED代表紅燈, case 0: color=1; LedBuff[6]=0xE7; timer=2; break; //切換到黃色,亮3秒 case 1: color=2; LedBuff[6]=0xFC; timer=29; break; //切換到紅色,亮30秒 case 2: color=0; LedBuff[6]=0x3F; timer=39; break; //切換到綠色,亮40秒 default: break; } } else //倒計時未到0時,遞減其計數(shù)值 { timer--; } LedBuff[0] = LedChar[timer%10]; //倒計時數(shù)值個位顯示 LedBuff[1] = LedChar[timer/10]; //倒計時數(shù)值十位顯示 } void LedScan() //LED顯示掃描函數(shù) { static unsigned char index = 0; //LED位選索引 P0 = 0xFF; //關(guān)閉所有段選位,顯示消隱 P1 = (P1 & 0xF8) | index; //位選索引值賦值到P1口低3位 P0 = LedBuff[index]; //相應(yīng)顯示緩沖區(qū)的值賦值到P0口 if (index < 6) //位選索引0-6循環(huán),因有6個數(shù)碼管+一組獨立LED index++; else index = 0; } void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù) { static unsigned int tmr1s = 0; //1秒定時器 TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; LedScan(); //LED掃描顯示 tmr1s++; //1秒定時的處理 if (tmr1s >= 1000) { tmr1s = 0; flag1s = 1; } } 10.4 長短按鍵的應(yīng)用10.4.1 51單片機RAM區(qū)域劃分前邊介紹單片機資源的時候,我們提到過我們的STC89C52RC共有512字節(jié)的RAM,就是用來保存數(shù)據(jù)的,如我們定義的變量都是直接存在RAM里邊。51單片機的這512字節(jié)的RAM數(shù)據(jù)是分塊的,因此我們在訪問的時候,也要注意一些問題。 51單片機的RAM分為兩個部分,一塊是片內(nèi)RAM,一塊是片外RAM。標準的51的片內(nèi)RAM地址從0x00H~0x7F共128個字節(jié),而現(xiàn)在我們用的51系列的單片機都是帶擴展片內(nèi)RAM的,RAM是從0x00~0xFF共256個字節(jié)。片外RAM最大可以擴展到0x0000~0xFFFF共64K字節(jié)。這里有一點大家要明白,片內(nèi)RAM和片外RAM的地址不是連起來的,片內(nèi)是從0x00開始,片外也是從0x0000開始的。以下是幾個Keil C51語言中的關(guān)鍵字,代表了RAM不同區(qū)域的劃分,大家先記一下。 data:片內(nèi)RAM從0x00~0x7F idata:片內(nèi)RAM從0x00~0xFF pdata:片外RAM從0x0000~0x00FF xdata:片外RAM從0x0000~0xFFFF 大家可以看出來,data是idata的一部分,pdata是xdata的一部分。為什么還這樣去區(qū)分呢?因為RAM分塊的訪問方式主要和匯編語言有關(guān),因此這塊內(nèi)容大家了解一下即可,只需要記住如何訪問速度更快即可。 我們定義一個變量a,可以這樣:unsigned char data a=0,而我們前邊定義變量時都沒有加data這個關(guān)鍵字,是因為我們在Keil默認設(shè)置下,data是可以省略的,即什么都不加的時候變量就是定義到data區(qū)域中的。data區(qū)域RAM的訪問在匯編語言中用的是直接尋址,訪問運行速度是最快的。如果你定義成idata,不僅僅可以訪問data區(qū)域,還可以訪問0x80H~0xFF的范圍,但加了idata關(guān)鍵字后,訪問的時候是利用了51單片機的通用寄存器進行間接尋址,速度較data速度慢一些,而且我們平時大多數(shù)情況下不太希望訪問到0x80H~0xFF,因為這塊通常用于中斷和函數(shù)調(diào)用的堆棧,所以在絕大多數(shù)情況下,我們使用內(nèi)部RAM的時候,只用data就可以了。 對于外部RAM來說,使用pdata定義的變量存到了外部RAM的0x00~0xFF的地址范圍里,這塊地址的訪問和idata類似,都是用8位的通用寄存器進行間接尋址,而如果你定義成xdata,可以訪問的范圍更廣泛,從0到64k的地址都可以訪問到,但是它需要使用2個8位的寄存器DPTRH和DPTRL來進行間接尋址,速度是最慢的。 我們的STC89C52RC共有512字節(jié)的RAM,256字節(jié)的片內(nèi)RAM和256字節(jié)的片外RAM。一般情況下,我們是使用data區(qū)域,data不夠用了,我們就用xdata,如果希望程序執(zhí)行效率點,可以使用pdata關(guān)鍵字來定義。其他型號的,有更大的RAM的51系列單片機,如果要使用更大的RAM,就必須得用xdata來訪問了。 10.4.2 長短按鍵在我們的單片機系統(tǒng)中,如果我們按下一次按鍵加1,那我們第八章學(xué)到的技術(shù)就可以完成,但是我們想連續(xù)加很多數(shù)字的時候,要一次次按下這個按鍵確實不方便,我們希望我們按住按鍵的時候,數(shù)字會持續(xù)增加,這就是這節(jié)課的長短按鍵實例。 當按下一個按鍵持續(xù)時間低于1秒的時候,運行一次按鍵動作,當按下按鍵持續(xù)時間超過1秒后,每經(jīng)過200ms則自動再執(zhí)行一次按鍵動作,形成一個長按鍵效果。這個程序做的是一個定時炸彈的效果,打開開關(guān)后,數(shù)碼管顯示數(shù)字0,按下向上的按鍵數(shù)字加1,按下向下的按鍵數(shù)字減1,長按向上按鍵1秒后,數(shù)字會持續(xù)增加,長按向下按鍵1秒后,數(shù)字會持續(xù)減小。設(shè)定好數(shù)字后,按下回車按鍵,時間就會進行倒計時,當?shù)褂嫊r到0的時候,用蜂鳴器和板子上的8個LED小燈做炸彈效果,蜂鳴器持續(xù)響,LED小燈全亮。 #include <reg52.h> sbit BUZZ = P1^6; //蜂鳴器控制引腳 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 ADDR3 = P1^3; //LED選擇地址線3 sbit ENLED = P1^4; //LED總使能引腳 unsigned char code LedChar[] = { //數(shù)碼管顯示字符轉(zhuǎn)換表 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; unsigned char LedBuff[7] = { //數(shù)碼管+獨立LED顯示緩沖區(qū) 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; unsigned char 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鍵、 回車鍵、 向右鍵 }; unsigned char KeySta[4][4] = { //全部矩陣按鍵的當前狀態(tài) {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; unsigned long pdata KeyDownTime[4][4] = { //每個按鍵按下的持續(xù)時間,單位ms {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0} }; bit enBuzz = 0; //蜂鳴器使能標志 unsigned char T0RH = 0; //T0重載值的高字節(jié) unsigned char T0RL = 0; //T0重載值的低字節(jié) bit flag1s = 0; //1秒定時標志 bit flagStart = 0; //倒計時啟動標志 unsigned int CountDown = 0; //倒計時計數(shù)器 void ConfigTimer0(unsigned int ms); void DisplayNumber(unsigned int dat); void KeyDrive(); void main(void) { P0 = 0xFF; //P0口初始化 ADDR3 = 1; //選擇數(shù)碼管 ENLED = 0; //LED總使能 EA = 1; //開總中斷 ConfigTimer0(1); //配置T0定時1ms DisplayNumber(CountDown); while(1) { KeyDrive(); if (flagStart && flag1s) //倒計時啟動且1秒定時到達時,處理倒計時 { flag1s = 0; if (CountDown > 0) //倒計時未到0時,計數(shù)器遞減 { CountDown--; DisplayNumber(CountDown); if (CountDown == 0) //減到0時,執(zhí)行聲光報警 { enBuzz = 1; //啟動蜂鳴器發(fā)聲 LedBuff[6] = 0x00; //點亮獨立LED } } } } } void ConfigTimer0(unsigned int ms) //T0配置函數(shù) { unsigned long tmp; tmp = 11059200 / 12; //定時器計數(shù)頻率 tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 31; //修正中斷響應(yīng)延時造成的誤差 T0RH = (unsigned char)(tmp >> 8); //定時器重載值拆分為高低字節(jié) T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0為模式1 TH0 = T0RH; //加載T0重載值 TL0 = T0RL; ET0 = 1; //使能T0中斷 TR0 = 1; //啟動T0 } void DisplayNumber(unsigned int dat) //將一個無符號整型數(shù)轉(zhuǎn)到數(shù)碼管顯示緩沖區(qū)以供顯示 { signed char i; unsigned char buf[6]; for (i=0; i<6; i++) //拆分為十進制的位 { buf[ i] = dat % 10; dat /= 10; } for (i=5; i>=1; i--) //高位的0不予顯示 { if (buf [ i] == 0) LedBuff[ i] = 0xFF; else break; } for ( ; i>=0; i--) //有效數(shù)據(jù)位轉(zhuǎn)換為顯示字符 { LedBuff[ i] = LedChar[buf[ i]]; } } void KeyAction(unsigned char keycode) //按鍵動作函數(shù),根據(jù)鍵碼執(zhí)行相應(yīng)動作 { if ((keycode>='0') && (keycode<='9')) //本程序中對0-9的數(shù)字按鍵不做響應(yīng) {} else if (keycode == 0x26) //向上鍵,倒計時設(shè)定值遞增 { if (CountDown < 9999) //最大計時9999秒 { CountDown++; DisplayNumber(CountDown); } } else if (keycode == 0x28) //向下鍵,倒計時設(shè)定值遞減 { if (CountDown > 1) //最小計時1秒 { CountDown--; DisplayNumber(CountDown); } } else if (keycode == 0x0D) //回車鍵,啟動倒計時 { flagStart = 1; //啟動倒計時 } else if (keycode == 0x1B) //Esc鍵,取消倒計時 { enBuzz = 0; //關(guān)閉蜂鳴器 LedBuff[6] = 0xFF; //關(guān)閉獨立LED flagStart = 0; //停止倒計時 CountDown = 0; //倒計時數(shù)歸零 DisplayNumber(CountDown); } } void KeyDrive() //按鍵動作驅(qū)動函數(shù) { unsigned char i, j; static unsigned char backup[4][4] = { //按鍵值備份,保存前一次的值 {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1} }; static unsigned long pdata TimeThr[4][4] = { //保持按下時啟動快速輸入的時間閾值 {1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000} }; 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]; } if (KeyDownTime[ i][j] > 0) //檢測執(zhí)行快速輸入 { if (KeyDownTime[ i][j] >= TimeThr[ i][j]) //按下時間達到閾值時執(zhí)行一次動作 { KeyAction(KeyCodeMap[ i][j]); //調(diào)用按鍵動作函數(shù) TimeThr[ i][j] += 200; //間隔200ms執(zhí)行下一次動作 } } else //按鍵彈起時復(fù)位閾值時間 { TimeThr[ i][j] = 1000; //啟動快速輸入的條件為持續(xù)按下超過1000ms } } } } void LedScan() //LED顯示掃描函數(shù) { static unsigned char index = 0; P0 = 0xFF; //關(guān)閉所有段選位,顯示消隱 P1 = (P1 & 0xF8) | index; //位選索引值賦值到P1口低3位 P0 = LedBuff[index]; //相應(yīng)顯示緩沖區(qū)的值賦值到P0口 if (index < 6) //位選索引0-6循環(huán),因有6個數(shù)碼管+一組獨立LED index++; else index = 0; } void KeyScan() //按鍵掃描函數(shù) { unsigned char i; static unsigned char keyout = 0; //矩陣按鍵掃描輸出計數(shù)器 static unsigned char keybuf[4][4] = { //按鍵掃描緩沖區(qū),保存一段時間內(nèi)的掃描值 {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,即16ms(4*4ms)內(nèi)都只檢測到按下狀態(tài)時,可認為按鍵已按下 KeySta[keyout][ i] = 0; KeyDownTime[keyout][ i] += 4; //按下持續(xù)時間累加 } else if ((keybuf[keyout] [ i] & 0x0F) == 0x0F) { //連續(xù)4次掃描值為1,即16ms(4*4ms)內(nèi)都只檢測到彈起狀態(tài)時,可認為按鍵已彈起 KeySta[keyout][ i] = 1; KeyDownTime[keyout][ i] = 0; //按下的持續(xù)時間清零 } } //執(zhí)行下一次的掃描輸出 keyout++; keyout &= 0x03; 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; } } void InterruptTimer0() interrupt 1 //T0中斷服務(wù)函數(shù) { static unsigned int tmr1s = 0; //1秒定時器 TH0 = T0RH; //定時器重新加載重載值 TL0 = T0RL; if (enBuzz) //蜂鳴器發(fā)聲處理 BUZZ = ~BUZZ; //驅(qū)動蜂鳴器發(fā)聲 else BUZZ = 1; //關(guān)閉蜂鳴器 KeyScan(); //按鍵掃描 LedScan(); //LED掃描顯示 if (flagStart) //倒計時啟動時處理1秒定時 { tmr1s++; if (tmr1s >= 1000) { tmr1s = 0; flag1s = 1; } } else //倒計時為啟動時1秒定時器始終歸零 { tmr1s = 0; } } 長按鍵功能實現(xiàn)的重點有兩個:第一,是在原來的矩陣按鍵掃描函數(shù)KeyScan內(nèi),當檢測到按鍵按下后,持續(xù)的對一個時間變量進行累加,其目的是用這個時間變量來記錄按鍵按下的時間;第二,是在按鍵驅(qū)動函數(shù)KeyDrive里,除了原來的檢測到按鍵按下這個動作時執(zhí)行按鍵動作函數(shù)KeyAction外,還監(jiān)測表示按鍵按下時間的變量,根據(jù)它的值來完成長按時的連續(xù)快速按鍵動作功能。 1.5 作業(yè)1、將第一個例程進行倒計時處理,從9999.99開始進行倒計時,并且只顯示有效位。 2、理解PWM的實質(zhì),在點陣上實現(xiàn)不同亮度的小燈的花樣排列。 3、實現(xiàn)數(shù)碼管計時和流水燈同時運行的效果。 4、學(xué)會長短按鍵的用法,獨立把本章程序全部寫出來。 |