- 硬件:STC89C52RC
- 開發工具:Keil uVision4
對於剛接觸單片機的同學來說可能會對定時器/計數器的應用很蒙圈,特別是初值的計算和各種定時方式的選擇。下面希望能給你帶來一個清晰的思路。
定時器:一般用於軟件計時,給定時器設置一個時間,時間到了系統停止當前的工作跳轉到事先定義好的定時器中斷函數裏,函數裏可以做一些週期性的事情。
計數器:一般用於檢測外來脈衝信號,給計數器設置一個次數,次數到了系統停止當前的工作跳轉到事先定義好的計數器中斷函數裏,函數裏做相應的事情。
先說一下相關的寄存器,也可以直接跳過,看後面的實例分析。
配置定時器或者計數器就是對相應的寄存器進行賦值,下面是相關的寄存器描述:
第一部分寄存器:
對照着上面這一字節的每一位,進一步解析:
位(符號) |
功能 |
TMOD.7 (GATE) |
置1時,只有在腳爲高、TR1=1時纔可打開定時器/計數器1 置0時,TR1=1即可打開定時器/計數器1 |
TMOD.3 (GATE) |
置1時,只有在腳爲高、TR1=1時纔可打開定時器/計數器0 置0時,TR1=1即可打開定時器/計數器0。 |
TMOD.6 ( /) |
置1時,用作計數器1(從T1/P3.5腳輸入) 置0時,用作定時器1 |
TMOD.2 (/ ) |
置1時,用作計數器0(從T0/P3.4腳輸入) 置0時,用作定時器0 |
TMOD.5/TMOD.4 (M1、M0) 定時器/計數器1 選擇工作方式 |
方式0:M1=0,M0=0 ,13位定時器/計數器 |
方式1:M1=0,M0=1 ,16位定時器/計數器 |
|
方式2:M1=1,M0=0 ,8位自動重載定時器 |
|
方式3:M1=1,M0=1 ,定時器/計數器1 此時無效 |
|
TMOD.1/TMOD.0 (M1、M0) 定時器/計數器0 選擇工作方式 |
方式0:M1=0,M0=0 ,13位定時器/計數器 |
方式1:M1=0,M0=1 ,16位定時器/計數器 |
|
方式2:M1=1,M0=0 ,8位自動重載定時器 |
|
方式3:M1=0,M0=0 ,雙8位定時器/計數器 |
/*1*/ TMOD|=0x00; //選擇定時器0,工作方式爲0,
/*2*/ TMOD|=0x30; //選擇定時器1,工作方式爲1
/*3*/ TMOD|=0x40; //選擇計數器1,工作方式爲0
用或運算是爲了在給相應位賦值時不會影響無關位。可以試着解讀TMOD=0xDA
第二部分寄存器:
主要看T開頭的,I開頭是外部中斷,先不管。
位(符號) |
功能 |
TCON.7 (TF1) |
定時器/計數器1溢出標誌位。當 T1 被允許計數後T1從初值開始加1計數,最高位產生溢出時,置“1 ”TF1 ,並向 CPU請求中斷,當CPU響應時,由硬件清“0 ”TF1 ,TF1也可以由程序查詢或清“0 ”。 |
TCON.5 (TR1) |
定時器 T1 的運行控制位。該位由軟件置位和清零。當 GATE(TMOD.7)=0,TR1=1 時就允許T1開始計數,TR1=0 時禁止 T1 計數。當 GATE(TMOD.7)=1,TR1=1 且 INT1 輸入高電平時,才允許 T1 計數。 |
TCON.4 (TF0) |
定時器/計數器 0 溢出標誌位。當T0被允許計數後T0 從初值開始加 1 計數,最高位產生溢出時,置“1”TF0,並向CPU請求中斷,當 CPU 響應時,由硬件清“0”TF0,TF0也可以由程序查詢或清“0”。
|
TCON.3 (TR0) |
定時器 T0 的運行控制位。該位由軟件置位和清零。當 GATE(TMOD.3)=0,TR0=1 時就允許T0開始計數,TR1=0 時禁止 T0 計數。當 GATE(TMOD.3)=1,TR0=1 且 INT0 輸入高電平時,才允許 T0 計數 |
除了TCON、TMOD還有TL0、TH0和TL1、TH1,它們分別是定時器0的Timer寄存器和定時器1的Timer寄存器。這個參數沒有單位,不是毫秒或是其他,所以設置定時器的時間要通過一定的計算得來,也就是後面要說的重點部分。
定時器的應用:
編寫單片機定時器程序的步驟:
- 對TMOD賦值,以確定T0和T1的工作方式。
- 計算初值,並將初值寫入TH0,TL0或TH1,TL1。
- 中斷方式時,對IE賦值,開放中斷。
- 使TR0或TR1置位,啓動定時器/計數器定時或計數。
下面以定時器0爲例,闡述不同的方式的編程過程。
方式0:
#include<reg52.h>
#define uchar unsigned char
#define uint unsigned int
sbit led1=P1^0;
uchar num;
void TIM0init(void)
{
TMOD=0x00; //設置定時器0爲工作方式0
TH0=(8192-5000)/32; //裝入初值,怎麼計算,下面分析
TL0=(8192-5000)%32;
EA=1; //開總中斷
ET0=1; //開定時器中斷
TR0=1; //啓動定時器0
}
/*
interrupt 0 指明是外部中斷0;
interrupt 1 指明是定時器中斷0;
interrupt 2 指明是外部中斷1;
interrupt 3 指明是定時器中斷1;
interrupt 4 指明是串行口中斷;
函數名字可以隨便起,但定時器0的中斷號是固定爲1的
*/
void T0_time() interrupt 1
{
TH0=(8192-5000)/32; //重裝初值,如果不重裝,中斷只觸發一次
TL0=(8192-5000)%32;
num++;
}
void main()
{
TIM0init();
while(1)
{
if(num==200) //如果到了200,說明一秒時間到
{
num=0;
led1=~led1; //讓發光管狀態取反
}
}
}
假設單片機用的晶振是12MHz,上面的中斷函數每過5ms會被調用一次,也就是發光管每一秒狀態取反一次。那麼怎麼計算初值以確定TL0和TH0的值呢?
定時器方式0是指13位定時器,=8192;也就是說,當設置好初值後,系統會在這個初值的隔一個機器週期就會自增1,當累加到8192的時候溢出,然後觸發中斷。所以(8192-初值)*機器週期=定時器產生一次中斷的時間。
如果我們要設定的定時器產生一次中斷的時間爲5ms,那麼:
機器週期=12*(1/12MHz)=1μs
初值=(8192-5ms/1μs)=3192
13位定時器中,TH0整個 8 位全用,TL0只用低 5 位參與分頻。
TH0 |
bit7 |
bit6 |
bit5 |
bit4 |
bit3 |
bit2 |
bit1 |
bit0 |
TL0 |
|
|
|
bit4 |
bit3 |
bit2 |
bit1 |
bit0 |
因:3192=“110001111000”
所以TH0=“1100011”,TL0=“11000”
即TH0=(8192-5000)/32,TL0=(8192-5000)%32
如果用的是11.0592MHz的晶振,機器週期就不是整數了,12*(1/11059200)≈1.0851μs.
關於機器週期:
方式0跟方式1差不多的,不同的是方式1中TH0、TL0所有位全用。兩個字節,=65536.
方式2:
在定時器的方式0和方式1中,當計數溢出後,計數器變爲0,因此在循環定時或循環計數時必須用軟件反覆設置計數初值,這必然會影響到定時的精度,同時也給程序設計帶來很多麻煩。
方式2被稱爲8位初值自動重裝的8位定時器/計數器,TL(0/1)從初值開始計數,當溢出時,在溢出標誌TF(0/1)置1的同時,自動將TH(0/1)中的常數重新裝入TL(0/1)中,使TL(0/1)從初值開始重新計數,這樣避免了認爲軟件重新裝初值所帶來的時間誤差,從而提高了定時的精度。
#include<reg52.h>
#define uchar unsigned char
#define uint unsigned int
sbit led1=P1^0;
uint num;
void TIM0init(void)
{
TMOD=0x02; //設置定時器0爲工作方式2
TH0=6; //裝入初值
TL0=6;
EA=1; //開總中斷
ET0=1; //開定時器中斷
TR0=1; //啓動定時器0
}
void T0_time() interrupt 1
{
//相比上面的方式0,這裏不需要認爲加入重裝初值的代碼
num++;
}
void main()
{
TIM0init();
while(1)
{
if(num==4000) //如果到了4000,說明1秒時間到
{
num=0;
led1=~led1; //讓發光管狀態取反
}
}
}
這個也是基於12MHz的振盪頻率,TL0跟TL1必然是相同的,計算初值的方法跟上面一樣。方式2爲8位定時器/計數器,最多能裝載=256個,相對方式0的13位和方式1的16位的少。方式2經歷256個機器週期該計數器就會溢出。
還有一個值得注意的是num變量的類型變了,因爲4000已經超出了uchar的方位,所以改爲uint。
方式3:
當選擇方式3時,定時器T0就會被分成兩個獨立的計數器或者定時器。此時,TL0爲8位計數器,計數溢出好置位TF0,並向CPU申請中斷,之後需要軟件重裝初值; TH0也被固定爲8位計數器,不過TL0已經佔用了TF0和TR0,因此TH0將佔T1的中斷請求標誌TF1和定時器啓動控制爲TR1。
爲了防止中斷衝突,定時器T0在方式3時,T1不能產生中斷,但可以正常工作在方式0、1、2下。通常這種情況下T1將用作串行口的波特率發生器。
下面的例子是利用定時器方式3,TL0計數器對應的8位定時器實現第一個發光管以1s亮滅閃爍,用TH0計數器對應的8位定時器實現第二個發光管以0.5s亮滅閃爍。
#include<reg52.h>
#define uchar unsigned char
#define uint unsigned int
sbit led1=P1^0;
sbit led2=P1^1;
uint num1,num2;
void TIMEinit(void)
{
TMOD=0x03; //設置定時器0爲工作方式3
TH0=6; //裝初值
TL0=6;
EA=1; //開總中斷
ET0=1; //開定時器0中斷
ET1=1; //開定時器1中斷
TR0=1; //啓動定時器0
TR1=1; //啓動定時器0的高8位計數器
}
void TL0_time() interrupt 1
{
TL0=6; //重裝初值
num1++;
}
void TH0_time() interrupt 3 //佔用T1定時器的中斷號
{
TH0=6; //重裝初值
num2++;
}
void main()
{
TIMEinit();
while(1)
{
if(num1>=4000) //12*(1/12MHz)*(256-6)*4000=1s
{
num1=0;
led1=~led1;
}
if(num2>=2000) //12*(1/12MHz)*(256-6)*2000=0.5s
{
num2=0;
led2=~led2 ;
}
}
}
這裏的num1>=4000而不是num1==4000,是爲了穩妥起見,萬一定時器計數超過了4000,而主循環還沒來得及判斷,則會錯過4000.那led1就不能實現取反了。
僅供參考,錯誤之處以及不足之處還望多多指教。