定時/計數器從電路上來講是一個脈沖計數器,當計數脈沖來自于單片機內部機器周期時,我們習慣上稱其為定時器,而當計數脈沖來自于單片機外部的輸入信號時,則稱其為計數器。8051系列單片機在片內集成了兩個可編程的定時/計數器,分別稱其為定時/計數器0(T0)和定時/計數器1(T1),二者都具有定時和計數的功能。
這兩個定時計數器可以獨立配置為定時器或計數器。當被配置為定時器時,將按照預先設置好的長度運行一段時間后產生一個溢出中斷;當被配置為計數器時,如果單片機的外部中斷引腳上檢測到一個脈沖信號,該計數器加1,當達到預先設置好事件數目時,產生一個中斷事件。
51單片機的定時計數器由內部寄存器和外部引腳組成,T0(P3.4)引腳和T1(P3.5)引腳用于接收外部的脈沖信號。
51單片機的52子系列還有一個和這兩個計數器功能差別較大的16位定時計數器T2,T2的最常用功能是用作串行口的波特率發生器,這部分內容我們之后再進行講解。
51單片機通過對相應寄存器的操作來實現對定時計數器的控制,這些寄存器包括工作方式控制寄存器TMOD(Timer MODe register)和控制寄存器TCON(Timer CONtrol register),此外T0和T1還分別擁有兩個8位數據寄存器TH0、TL0和TH1、TL1。
TMOD是定時計數器的工作方式控制寄存器,通過對該寄存器的操作可以改變T0和T1的工作方式。該寄存器的內部結構和說明如下圖所示,該寄存器不支持位尋址,單片機復位后被清零。
TCON是定時計數器控制寄存器,上一節講解外部中斷時中我們已經介紹過了,其內部結構如表下圖所示,在51單片機復位后初始化值為所有位都清零。
TH0、TL0/TH1、TL1分別是T0/T1的數據高位/低位寄存器,均為8位。當定時計數器收到一個驅動事件(定時、計數)后,對應的數據寄存器的內容加1,當數據寄存器的值到達最大時,將產生一個溢出中斷,在單片機復位后所有寄存器的值都被初始化為0x00,這些寄存器都不能位尋址。
51單片機定時器工作原理
定時功能:定時功能是通過計數器的計數來實現的。計數脈沖來自單片機內部,每個機器周期產生1個計數脈沖,即每個機器周期使計數器加1。由于1個機器周期等于12個振蕩脈沖周期,所以計數器的計數頻率為振蕩器頻率的1/12。假如晶振的頻率fosc =12MHz,則計數器的計數頻率 fcont =fosc ×1/12 為1MHz,即每微秒計數器加1。這樣,單片機的定時功能就是對單片機的機器周期數進行計數。由此可知,計數器的計數脈沖周期為:
T=1/fcont =1/(fosc ×1/12)=12/fosc
式中,fosc 為單片機振蕩器的頻率;fcont 為計數脈沖的頻率,fcont =fosc /12。在實際應用中,可以根據計數值計算出定時時間,也可以反過來按定時時間的要求計算出計數器的初值。
單片機的定時器用于定時,其定時的時間由計數初值和選擇的計數器的長度(如8位、13位或16位)來確定。
計數功能:計數功能就是對外部事件進行計數。外部事件的發生以輸入脈沖表示,因此計數功能實質上就是對外部輸入脈沖進行計數。STC89 系列單片機的T0(P3.4)、T1(P3.5)或T2(P1.0)信號引腳作為計數器的外部計數輸入端,當外部輸入脈沖信號產生由1至0的負跳變時,計數器的值加1。
在計數方式下,計數器在每個機器周期的S5P2期間,對外部脈沖輸入進行1次采樣。如果在第1個機器周期中采樣到高電平“1”,而在第2個機器周期中采樣到1個有效負跳變脈沖,即低電平“0”,則在第3個機器周期的S3P1期間計數器加1。由此可見,采樣1次由“1”至“0”的負跳變計數脈沖需要花費2個機器周期,即24個振蕩器周期,故計數器的最高計數頻率為fcont =fosc ×1/24。例如,單片機的工作頻率fosc 為12MHz,則最高的采樣頻率為0.5MHz。
對外部脈沖的占空比并沒有什么限制,但外部計數脈沖的高電平和低電平保持時間均必須在1個機器周期以上,方可確保某一給定的電平在變化前至少采樣1次。
51單片機定時器模式設置
51單片機T0和T1定時器都有4種模式,由TMOD寄存器中間的M1、M0這兩位的設置來控制。寄存器配置工作模式如下圖所示:
接下來我們一起看看這些模式的設置與特點。
工作方式0:當M1、M0設定為“00”時,定時器工作于工作方式0,此時定時計數器的內部結構如圖所示。
在工作方式0下,定時器內部計數器計數值為13位,由TH0/TH1的8位和TL0/TL1的低5位組成;當TL0/TL1溢出時將向TH0/TH1進位,當TH0/TH1溢出后則產生相應的溢出中斷。工作方式下的驅動事件來源則由GATE位、C/T#位來控制。
工作方式1:當M1、M0設定為“01”時,定時器工作于工作方式1,此時定時計數器的內部結構如圖所示。
和工作方式0相比,工作方式1的唯一區別在于此時的內部計數器寬度為16位,分別由TH0/TH1的8位和TL0/TL1的8位組成,其溢出方式和驅動事件的來源和工作方式相同。51系列單片機的定時計數器采用加1計數的方式,即當接收到一個驅動事件時計數器加1,當計數器溢出時則產生相應的中斷請求,第一個驅動事件到來時刻和中斷請求產生。
51單片機在接收到一個驅動事件之后計數器加1,當計數器溢出時則產生相應的中斷請求。在定時的模式下,定時計數器的驅動事件為單片機的機器周期,也就是外部時鐘頻率的1/12,可以根據定時器的工作原理計算出工作方式0和工作方式1下的最長定時長度T為:
通過對定時計數器的數據寄存器賦一個初始化值的方法可以讓定時計數器得到0到最大定時長度中任意選擇的定時長度,初始化值N的計算公式如下:
注意:定時計數器的工作方式0和工作方式1,不具備自動重新裝入初始化值的功能,所以如果要想循環得到確定的定時長度就必須在每次啟動定時器之前重新初始化數據寄存器,通常是在中斷服務程序里完成這樣的工作。
工作方式2:當M1、M0設定為“10”時,定時器工作于工作方式2,此時定時計數器的內部結構如圖所示。
定時計數器的工作方式2和前兩種工作方式有很大的不同,工作方式2下的8位計數器的初始化數值可以被自動重新裝入。在工作方式2下,TL0/TL1為一個獨立的8位計數器,而TH0/TH1用于存放時間常數,當T0/T1產生溢出中斷時,TH0/TH1中的初始化數值被自動裝入TL0/TL1中。這種方式可以大大減少程序的工作量,但是其定時長度也大大減少,應用較多的場合是較短的重復定時或用作串行口的波特率發生器。
工作方式3:當M1、M0設定為“11”時,定時器工作于工作方式3,此時定時計數器的內部結構如圖所示。
在這種工作方式下T0被拆分成了兩個獨立的8位計數器TH0和TL0,TL0使用T0本身的控制和中斷資源,而TH0則占用了T1的TR1和TF1作為啟動控制位和溢出標志。在這種情況下,T1將停止運行并且其數據寄存器將保持當前數值,所以設置T0為工作方式3也可以代替復位TR1來關閉T1定時計數器。
接下來
51單片機定時器中斷
51系列單片機內部集成了兩個定時器/計數器,分別提供了兩個定時中斷源:TF0和TF1。
TF0中斷,定時器/計數器0中斷請求,其中斷請求號為1。
TF1中斷,定時器/計數器1中斷請求,其中斷請求號為3。
51單片機的斷控制寄存器IE中的EA位和ET0/ET1都被置“1”時,定時計數器0/1的中斷被使能,在這種狀態下,如果定時計數器0/1出現一個計數溢出事件,則會觸發定時計數器中斷事件。可以通過修改中斷優先級寄存器IP中的PT0/PT1位來提高定時計數器的中斷優先級,MCS-51單片機的定時計數器的中斷處理函數的結構如下:
void 函數名(void) interrupt 中斷向量號 (using 工作組寄存器)
{
//中斷代碼
}
51單片機定時器計數功能應用
現在我們設計一個簡單的電路來實踐一下定時器的使用方法。
如圖所示的電路中我們將T0對應的外部脈沖引腳連接一個按鍵K5,用來測試定時器的計數功能。
普通計數器:P3.4電平變化后計數器累加,如果將計數初值設置為0xff,則每按鍵一次后計數器都會溢出,標志位就會置位,程序中通過掃描計數器標志位狀態來切換數字。
/*
*這是一個定時器計數應用程序
*目的是運用定時器計數功能進行捕獲計數
*/
#include
#include
typedef unsigned char u8;
typedef unsigned int u16;
u8 data_L,data_H;
u8 num = 0;
u8 code num_codelist[10] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void delay(u8 ms);
void count_func(void);
void data_init(void);
void T0_init(void);
void display(void);
void main(void)
{
T0_init();
while(1)
{
count_func();
data_init();
display();
}
}
void delay(u8 ms)
{
u8 i,j;
for(i=0; i
}
void data_init(void)
{
data_L = num%10;
data_H = num/10;
}
void display(void)
{
P2 = 0xfe;
P0 = num_codelist[data_H];
delay(1);
P2 = 0xfd;
P0 = num_codelist[data_L];
delay(1);
}
void count_func(void)
{
//查詢標志位
if(1 == TF0)
{
TH0 = 0xff;
TL0 = 0xff;
if(20 == num)
{
num = 0;
}
else
{
num++;
}
TF0 = 0;
}
}
void T0_init(void)
{
//設置定時器0為模式2
TMOD = 0x05;
//設置計數初值(模式2具備自動重裝)
TH0 = 0xff;
TL0 = 0xff;
//中斷使用設置,這里只啟用定時器不使用中斷功能
ET0=0;
TR0 = 1;
EA = 0;
}
中斷計數器:這里的操作原理與以上普通程序相似,只是啟用了中斷,就不用進行循環掃描操作了,定時器中斷進行計數,捕獲P3.4電平變化后計數器加1,當數據寄存器數值溢出后產生中斷,在中斷中處理程序功能即可。
/*
*這是一個定時器中斷計數應用程序
*目的是運用定時器中斷功能進行計數
*/
#include
#include
typedef unsigned char u8;
typedef unsigned int u16;
u8 data_L,data_H;
u8 num = 0;
u8 code num_codelist[10] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void delay(u8 ms);
//void count_func(void);
void data_init(void);
void T0_init(void);
void display(void);
void main(void)
{
T0_init();
while(1)
{
data_init();
display();
}
}
void delay(u8 ms)
{
u8 i,j;
for(i=0; i
}
void data_init(void)
{
data_L = num%10;
data_H = num/10;
}
void display(void)
{
P2 = 0xfe;
P0 = num_codelist[data_H];
delay(1);
P2 = 0xfd;
P0 = num_codelist[data_L];
delay(1);
}
void T0_init(void)
{
//設置定時器0為模式2
TMOD = 0x05;
//設置計數初值
TH0 = 0xff;
TL0 = 0xff;
//中斷使用設置,啟用中斷
ET0 = 1;
TR0 = 1;
EA = 1;
}
//void count_func(void)
//{
// if(1 == TF0) //查詢是否溢出
// {
// TH0 = 0xff; //重新賦值
// TL0 = 0xff;
// if(99 == num)
// {
// num = 0;
// }
// else
// {
// num++;
// }
// TF0 = 0;
// }
//}
void count_func(void) interrupt 1
{
//重新賦值
TH0 = 0xff;
TL0 = 0xff;
// //中斷中此位會硬件清零,這句可以不用寫
// TF0 = 0;
// //這里相當于對按鍵進行消抖,實際使用時酌情使用
// delay(15);
//查詢外部輸入脈沖變化
if(0 == T0)
{
if(20 == num)
{
num = 0;
}
else
{
num++;
}
}
}
這兩個程序很大一部分都是相同的,不同之處在于計數處理部分,第一個程序沒有使用中斷,我們定義了一個void count_func(void),第二個程序中我們使用了定時器中斷,中斷函數就是void count_func(void) interrupt 1。程序的主要區別就在于這兩個函數void count_func(void)是普通函數需要定義,說明,調用,而void count_func(void) interrupt 1是中斷函數它只需要定義就行。另外兩函數功能差異就是我們剛所講到的,一個是在主函數循環中不斷掃描寄存器標志位,一個是中斷中查詢引腳狀態。
其他程序段都是比較簡單易懂的吧,這里簡單說明一下:void data_init(void)這個函數是對顯示數據進行個位與十位處理的。void display(void)這個函數是數碼管顯示處理函數,前面講數碼管時是介紹過了,這里只是把它打包成一個函數了。void T0_init(void)這個函數是定時器T0的初始化函數,里面包括定時器的模式,計數初值,中斷等設置內容。以上程序不理解的可以留言或參考資料分析一下。最后再來看一下電路仿真情況。
兩個程序實現的功能都是一樣的,現在想想定時器使用計數功能時中斷作用通過一些特殊處理(比如我們這里將計數初值設置為最大值0xff)是不是和前面講的外部引腳P3.3和P3.4的中斷就很相似了,按鍵按一下就會產生中斷。所以如果某些程序需要多個外部中斷而單片機沒有那么多中斷引腳時不妨可以使用這種方式來增加單片機功能。當然這都是題外話,等你真正做開發時很大概率是不會用這款單片機的,現在的單片機功能強大著呢!但這是一種開發者應該具有的思維,也不是說你想到了某個法子就能派上用場,但平時積累一些“奇巧淫技”是有必要的,萬一哪天就用上了呢!或許你的一個軟件優化就幫公司產品省去了一筆費用,如果是一個月產n千或n萬,甚至更高產量的產品,那老板不給你加薪還給誰加薪呢。對于有這樣思維的,像對待自己孩子一樣對待工作的人,無論什么崗位,我認為如果遇到了都值得成為合伙人,可以直接給股份,共創未來。這就是你的個人,從某種意義上來說,工作能力是職場的信譽值,信譽是市場硬通貨,信譽也是真正的財富密碼。打工時拿到好的工資是因為老板認可你所做工作,產品找明星代言,當我們選擇它時很大可能是因為信任那個明星。對于信譽值達到一定程度的產品,它本身就成了一種信譽,就像在股市你之所以買某只股票,很大的原因就是因為你研究過了這家公司后得出你的信任這家公司這個結論。以上為我個人分析,當然并不一定所有人(老板)都會這么想,但我認為若真正信任員工,那雇傭關系轉換為具有規則契約的合作關系能取得更好成就,這是一種互信機制。這都是本人回顧曾經“滄海”的產生的真實理念,但往事“不堪回首”,故事就不說了,以后有機會再交流,5年或許10年后我還會再來看這段話,或許到時我也早已不是“光桿司令”了。
51單片機定時器定時功能應用
通過以上兩個例程各位對定時器計數功能應該都能掌握了吧,想驗證是否掌握最快的方法就是自己親自敲一遍代碼運行一下,看結果是否相符,如果自己敲代碼后仿真運行或在實驗板上沒結果時記得對照程序好好檢查一下,看是哪里出了問題。
接下來我們繼續介紹定時器的定時功能。我們想來看一段代碼,可以嘗試一下在不看后面解析的情況下試試自己能否讀出程序的結果。
/*
*這是一個定時器定時應用程序
*目的是運用定時器定時模式進行控制數碼管和LED顯示
*/
#include
#include
typedef unsigned char u8;
typedef unsigned int u16;
u8 data_L,data_H;
u8 T0_cnt = 0;
u8 T0_s = 0;
u8 T1_cnt = 0;
u8 crol = 0xfe;
u8 code num_codelist[10] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void delay(u8 ms);
void data_init(void);
void Timer_init(void);
void display(void);
void main(void)
{
Timer_init();
P1 = crol;
while(1)
{
data_init();
display();
}
}
void delay(u8 ms)
{
u8 i,j;
for(i=0; i
}
void data_init(void)
{
data_L = T0_s%10;
data_H = T0_s/10;
}
void display(void)
{
P2 = 0xfe;
P0 = num_codelist[data_H];
delay(1);
P2 = 0xfd;
P0 = num_codelist[data_L];
delay(1);
}
void Timer_init(void)
{
//使能中斷總開關
EA = 1;
// 使能定時器中斷
ET0 = 1;
ET1 = 1;
// 設置工作方式1
TMOD = 0x11;
// 設置定時器0定時時間50ms
TH0 = (65536-50000)/256;
TL0 = (65536-50000)%256;
// 設置定時器1定時時間50ms
TH1 = (65536-50000)/256;
TH1 = (65536-50000)%256;
// 設置控制寄存器:啟動定時器
TR0 = 1;
TR1 = 1;
}
void T0_func(void) interrupt 1
{
//重新賦值
TH0 = (65536-50000)/256;
TL0 = (65536-50000)%256;
//計時1s
T0_cnt++;
if(20 == T0_cnt)
{
T0_cnt = 0;
T0_s++;
if(60 == T0_s)
{
T0_s = 0;
}
}
}
void T1_func(void) interrupt 3
{
//重新賦值
TH1 = (65536-50000)/256;
TL1 = (65536-50000)%256;
//計時0.5s
T1_cnt++;
if(5 == T1_cnt)
{
T1_cnt = 0;
//燈移位
crol = _crol_(crol,1);
P1 = crol;
}
}
這個程序的功能是使用定時T0定時控制數碼管每秒變化數字,利用定時器T1控制LED每秒移位點亮4個燈。
先看程序運行的結果。
現在來分析一下程序,程序整體來說是非常基礎的,很多都是出現過多次的程序段了,我們主要介紹一下幾個新的函數。
void Timer_init(void)這個函數是定時器T0和T1的初始化函數,里面包含中斷控制位配置,數據寄存器初始化賦值以及定時器開關設置,這里將定時器T0和T1都設置為模式1,即16位定時器功能,這種模式下定時器數據寄存器沒有自動重裝功能,所以在每次中斷之后要進行賦初值操作,在定時器中斷中有這段代碼,計數通過前面內容中的公式得出,這里是設置每50ms一次中斷。
計算初值這點肯定很多初學者不太理解,需要多看一下資料說明,原理其實很簡單,51單片機的計算器都是向上計數的,即從0~設置的定時器數據寄存器最大值,這里是16位所以就是0xffff(十進制就是65535),當計數到0xffff時再記一次這個數就溢出了,就產生了溢出中斷,這時在中斷函數中我們將數據寄存器程序重新賦值它又重頭開始進行計數,所以最大定時時間就是0xffff個機器周期,51單片機是一個機器周期為12個時鐘周期,如果使用12M晶振則剛好一個機器周期對應為1us,如果我們只需定時1us數據寄存器中就賦值0xffff,即65536-1,,經過一個機器周期就會產生中斷了,如果定時2us,則設置為65536-2,其他的數值以此類推。
void T0_func(void) interrupt 1這個函數是定時器0的中斷函數,首先對定時器數據寄存器進行賦初值然后設置一個1秒的累加技術操作,使得變量T0_s在每一秒計時之后加一,加到60s時進行清零,之后再循環計數,數碼管將顯示當前秒數值。
void T1_func(void) interrupt 3這個函數是定時器T1中斷函數,這里設置一個0.25s的時間進行LED點亮移位操作。
評論
查看更多