51微控制器定時器的原理與使用
定時器是微控制器的重要功能模組之一,在檢測、控制領域有廣泛應用。定時器常用作定時時鐘,以實現定時檢測,定時響應、定時控制,並且可以產生ms寬的脈衝訊號,驅動步進電機。定時和計數的最終功能都是通過計數實現,若計數的事件源是週期固定的脈衝則可實現定時功能,否則只能實現計數功能。因此可以將定時和計數功能全由一個部件實現。通過下圖可以簡單分析定時器的結構與工作原理。
一、定時器
1、51微控制器計數器的脈衝輸入腳。主要的脈衝輸入腳有Px,y, 也指對應T0的P3.4和對應T1的P3.5,主要用來檢測片外來的脈衝。而引腳18和19則對應著晶振的輸入脈衝,脈衝的頻率和週期為
F = f/12 = 11.0592M/12 = 0.9216MHZ T = 1/F = 1.085us
2、定時器有兩種工作模式,分別為計數模式和定時模式。對Px,y的輸入脈衝進行計數為計數模式。定時模式,則是對MCU的主時鐘經過12分頻後計數。因為主時鐘是相對穩定的,所以可以通過計數值推算出計數所經過的時間。
3、51計數器的計數值存放於特殊功能暫存器中。T0(TL0-0x8A, TH0-0x8C), T1(TL1-0x8B, TH1-0x8D)
4、TLx與THx之間的搭配關係
1)、TLx與THx之間32進位制。即當TLx計到32個脈衝時,TLx歸0同時THx進1。這也稱為方式0。
2)、TLx與THx之間256進位制。即當TLx計到256個脈衝時,TLx歸0同時THx進1。這也稱為方式1。在方式1時,最多計65536個脈衝產生溢位。在主頻為11.0592M時,每計一個脈衝為1.085us,所以溢位一次的時間為1.085usx65536=71.1ms。
3)、THx用於存放TLx溢位後,TLx下次計數的起點。這也稱為方式2。
4)、THx與TLx分別獨立對自己的輸入脈衝計數。這也稱為方式3。
5、定時器初始化
1)、確定定時器的計數模式。
2)、確定TLx與THx之間的搭配關係。
3)、確定計數起點值。即TLx與THx的初值。
4)、是否開始計數。TRx
(1)和(2)可以由工作方式暫存器TMOD來設定,TMOD用於設定定時/計數器的工作方式,低四位用於T0,高四位用於T1。其格式如下:
GATE:門控位,用於設定計數器計數與否,是否受P3.2或P3.3電壓狀態的影響。GATE=0時,表示計數器計數與否與兩埠電壓狀態無關;GATA=1時,計數器是否計數要參考引腳的狀態,即P3.2為高時T0才計數,P3.3為高時T1才計數。
C/T:定時/計數模式選擇位。 =0為定時模式; =1為計數模式。
M1M0:工作方式設定位。定時/計數器有四種工作方式,由M1M0進行設定。
6、計數器的溢位
計數器溢位後,THx與TLx都歸0。並將特殊功能區中對應的溢位標誌位TFx寫為1。
好了,理論就講述到這。現在我們通過一些實驗來看看怎麼使用定時器。
實驗一、P1口連線的8個LED燈以1秒鐘的頻率閃爍。
首先上程式碼:
#include "reg51.h"
char c;
void Timer0_Init() //初始化定時器
{
TMOD = 0x01; //
TH0 = 0;
TL0 = 0; //定時器的計數起點為0
TR0 = 1; //啟動定時器0
}
void main()
{
Timer0_Init();
while(1)
{
if(TF0 == 1) //檢測定時器0是否溢位,每到65535次
{
TF0=0;
c++;
if(c==14) //71ms乘以14為1s
{
c=0;
P1=~P1;
}
}
}
}
上述程式碼的思路是每計算14個溢位,則翻轉P1口狀態。產生一個溢位的時間是71.1ms,14個則約為1s。
實驗二、讓一個LED燈每1秒鐘閃爍。
#include "reg51.h"
sbit LD1 = P1^0;
void Timer0_Init() //初始化定時器
{
TMOD = 0x01; //
TH0 = 0;
TL0 = 0; //定時器的計數起點為0
TR0 = 1; //啟動定時器0
}
void Timer0_Overflow() //處理定時器0的溢位事件
{
static char c;
if(TF0 == 1) //檢測定時器0是否溢位,每到65535次
{
TF0=0;
c++;
if(c==14) //71ms乘以14為1s
{
c=0;
LD1=!LD1;
}
}
}
void main()
{
Timer0_Init(); //初始化定時器0
while(1)
{
Timer0_Overflow();
}
}
相比於上個例子,這裡有兩個區別,首先是將timer0的溢位事件作為子函式單獨出來,其次是注意翻轉一個led燈時候用的是“!”。
例子三、讓連線到P1口的LED1和LED8燈每1秒鐘閃爍。
#include "reg51.h"
void Timer0_Init() //初始化定時器
{
TMOD |= 0x01; //定時器0方式1,計數與否不受P3.2的影響
TH0 = 0;
TL0 = 0; //定時器的計數起點為0
TR0 = 1; //啟動定時器0
}
void Timer0_Overflow() //´處理定時器0的溢位事件
{
static char c;
if(TF0 == 1) //檢測定時器0是否溢位,每到65535次
{
TF0=0;
c++;
if(c==14) //71ms乘以14為1s
{
c=0;
P1 ^= (1<<0);//LD1=!LD1;
}
}
}
void Timer1_Init()
{
TMOD|=0x10; //定時器1方式1,計數與否不受P3.3的影響
TH1=0;
TL1=0; //定時器1的計數起點為0
TR1=1; //啟動定時器1
}
void Timer1_Overflow() //處理定時器1的溢位事件
{
static char c;
if(TF1==1) //軟體查詢,主迴圈每跑完一圈才會到這裡。
{
TF1=0;
c++;
if(c==14)
{
c=0;
P1 ^= (1<<7);//LD8=!LD8;
}
}
}
void main()
{
Timer0_Init(); //初始化定時器0
Timer1_Init(); //初始化定時器1
while(1)
{
Timer0_Overflow();
Timer1_Overflow();
}
}
相較於例二,例子三有幾個點值得注意:
1、TMOD初始化為什麼採用TMOD |= 0x01或0x10的形式?
首先如果在定時器初始化函式中採用TMOD = 0x01和TMOD = 0x10,那麼將造成LD1閃爍比LD8閃爍快8倍。分析一下,從main函式開始執行,先是初始化timer0,這時候定時器1設定為工作方式1。接著程式執行到Timer1_Init(),這時候TMOD=00010000,即選定了timer1在工作方式1,但同時timer0重新配置為工作方式0, 也就是32進位制,所以產生快8倍現象。
那為什麼用|這個符號就可以做到互不影響呢?|是或運算子,即有1出1,全0出0。什麼意思呢?舉個例子,a是11110000,b是10101010,那麼a|b就是11111010。通過引入這個符號,可以實現tmod對兩個定時器的獨立操作。
2、為什麼使用P1 ^= (1<<0)可以實現LD1的控制呢?
首先解釋下^這個符號。^稱為異或運算子,相同出0,不同出1。舉個例子,a是11110000,b是10101010,那麼a^b就是01011010。
然後再來分析 x ^= (1<<i), 假設x為10101010,當i為1時, (1<<i)為00000010,那麼x^ (1<<i)=10101010^00000010=10101000。當i為2時,(1<<i)為00000100,那麼x^ (1<<i)=10101010^00000100=10101110,以此類推。我們發現,x ^= (1<<i)是在將x的第i位翻轉而同時不影響其他位。
因此P1 ^= (1<<0)實際是在翻轉P0口第一位的值,因此也就是在閃爍LD1燈。
上面三個例子實際都是採用了軟體查詢法。即main函式會每次進入到溢位事件函式裡去判斷TF0或1是否等於1,這樣就浪費了大量CPU時間。同時,實時性差,假如在執行Timer0_Overflow()的時候timer1也溢位了,這時候timer1的溢位事件就沒有及時處理。因此下面我們要引入中斷系統。
二、中斷系統
中斷系統是一套硬體電路,它可以在每個機器週期對所有的外設的標誌位作查詢。相比於前面的軟體查詢(if(xx==1)),中斷系統也可以叫做硬體查詢。51的中斷系統可查詢以下6個標誌位。
IE0(TCON.1),外部中斷0中斷請求標誌位。
IT1(TCON.2),外部中斷1觸發方式控制位。
IE1(TCON.3),外部中斷1中斷請求標誌位。
TF0(TCON.5),定時/計數器T0溢位中斷請求標誌位。
TF1(TCON.7),定時/計數器T1溢位中斷請求標誌位。
RI(SCON.0)或TI(SCON.1),序列口中斷請求標誌。當序列口接收完一幀序列資料時置位RI或當序列口傳送完一幀序列資料時置位TI,向CPU申請中斷。
當中斷系統查詢到外設的標誌位變為1時,中斷系統可暫停當前的主迴圈,並且將程式跳轉到使用者預先指定的函式中執行。要啟動中斷系統,必須先進行中斷初始化,其流程如下:
所以在使用定時器中斷時,我們只需要首先初始化中斷系統,開啟總中斷(相當於總開關),開啟定時器對應的控制位(相當於支路開關),再初始化定時器即可。中斷系統作為微控制器的外設,只有在某個中斷產生時才會打斷主迴圈,並由相應的中斷號引入到相應的中斷服務子函式。下圖是6箇中斷標誌位的資訊。a、是否要查詢外設標誌(EA=0或EA=1,EA 也叫 CPU中斷允許(總允許)位)
b、查詢到標誌1,是否要跳程式
c、跳轉的目標函式,即中斷服務子函式
實驗四、使用中斷系統實現LD1燈每1秒鐘閃爍。
#include "reg51.h"
void Timer0_Init()
{
TMOD|=0x01;
TH0=56320/256; //計數起點為56320 ==10ms溢位一次
TL0=56320%256;
TR0=1;
}
void Timer1_Init()
{
}
void ISR_Init() //初始化中斷系統
{
EA=1; //啟動中斷系統
EX0=0; //-->IE0
ET0=1; //-->TF0 控制位置1,表明當TF0置1時,中斷系統將介入
EX1=0; //-->IE1
ET1=0; //-->TF1
ES=0; //-->RI,TI
}
//以下中斷服務子程式,我們希望中斷系統來呼叫,而不是我們在main函式裡面呼叫,因此使用interrupt. */
void IE0_isr() interrupt 0
{
}
/*void TF0_isr() interrupt 1 //71.1ms 進入一次,但如果要求10MS進來一次呢?
{
static char c;
c++;
if(c==14)
{
P1^=(1<<0);
c=0;
}
}
*/
void TF0_isr() interrupt 1 //10ms 進入一次
{
static char c;
TH0=56320/256; //重灌初值
TL0=56320%256;
c++;
if(c==100)
{
P1^=(1<<0);
c=0;
}
}
void IE1_isr() interrupt 2
{
}
void TF1_isr() interrupt 3
{
}
void RI_TI_isr() interrupt 4
{
}
void main()
{
Timer0_Init();
Timer1_Init();
ISR_Init();
while(1)
{
//...
//發現溢位後,中斷系統根據中斷號尋找中斷子服務函式,並強行暫停主迴圈並進入子函式
//...
}
}
顯然使用中斷系統查詢得到的1s更為精確。因為中斷系統獨立於main函式執行。另外本程式還預裝了timer0的初值,這樣的話就可以實現比71ms更小的時間片,比如要求10ms就進入中斷。關於初值的設定,請參考下圖。實驗五、用定時器實現數碼管顯示1234。
//數碼管的定時掃描,每5ms顯示一個數碼管,也就是說相同的數碼管,每20ms會被重新裝入同樣的數值,根據人眼的延遲效應,人眼觀測到的數碼管上的數值是靜態的。
#include "reg51.h"
unsigned int count;
extern void load_smg();
void Timer0_Init()
{
TMOD|=0X01;
TH0=60928/256;
TL0=60928%256;//每5ms進入一次中斷
TR0=1;
}
void isr_Init()
{
EA=1;
ET0=1; //TF0 如果這個標誌為1,進入中斷子函式
}
void TF0_isr() interrupt 1
{
TH0=60928/256;
TL0=60928%256;//重灌初值
load_smg();
}
void main()
{
Timer0_Init();
isr_Init();
while(1)
{
}
}
#include "reg51.h"
//char seg[10]={0xC0,0XF9,0XA4,0XB0,0X99,0X92,0X82,0XF8,0X80,0X90};
code char seg[10]={0xC0,0XF9,0XA4,0XB0,0X99,0X92,0X82,0XF8,0X80,0X90};
char smgbuf[4]={1,2,3,4}; //從RAM的smgbuf這個地址開始連續存放4個數,並且每個數佔一個單元。
extern unsigned int count; //外部申明,表示並不在這裡申明
void fill_smgbuf() //向LED緩衝區填充資料
{
smgbuf[0]=count/1000; //千位
smgbuf[1]=(count%1000)/100; //百位
smgbuf[2]=((count%1000)%100)/10; //十位
smgbuf[3]=((count%1000)%100)%10; //個位
}
void load_smg() //將數碼管顯示緩衝區的資料,顯示到數碼管上
{
static char i;
fill_smgbuf();
i++;
if(i>=4)
{
i=0;
}
P0=0xFF; //消除上一個迴圈的影子
P2 = ~(1<<i);
P0 = seg[smgbuf[i]];
}
實驗六、實現按鈕控制數碼管上的數值加1或減1,並且當按住按鈕不放時,能實現快速的增減。
這裡的關鍵點在於如何實現快速增減,具體請詳細分析程式碼。程式碼連結點選開啟連結