1. 程式人生 > >51微控制器定時器的原理與使用

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時,中斷系統可暫停當前的主迴圈,並且將程式跳轉到使用者預先指定的函式中執行。要啟動中斷系統,必須先進行中斷初始化,其流程如下:

a、是否要查詢外設標誌(EA=0或EA=1,EA 也叫 CPU中斷允許(總允許)位)

b、查詢到標誌1,是否要跳程式

c、跳轉的目標函式,即中斷服務子函式

所以在使用定時器中斷時,我們只需要首先初始化中斷系統,開啟總中斷(相當於總開關),開啟定時器對應的控制位(相當於支路開關),再初始化定時器即可。中斷系統作為微控制器的外設,只有在某個中斷產生時才會打斷主迴圈,並由相應的中斷號引入到相應的中斷服務子函式。下圖是6箇中斷標誌位的資訊。



實驗四、使用中斷系統實現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,並且當按住按鈕不放時,能實現快速的增減。


這裡的關鍵點在於如何實現快速增減,具體請詳細分析程式碼。程式碼連結點選開啟連結