[ SDCC For 8051 ] 04-Timer

Morgan Ting
閱益如美
Published in
17 min readJul 8, 2023
Photo by Agê Barros on Unsplash

Timer 計時器或 Counter 計數器是微處理器中必備功能可以用來精確計算時間來完成控制程序,本文章將介紹 8051 的 Timer / Counter 操作方式與四種運作模式,以 VScode 編輯器搭配 Embedded IDE 與 SDCC 編譯器建立一個實驗專案。

Timer 介紹

計時器 Timer 或計數器 Counter 用來計算時間以及計數外部輸入訊號。計時與計數兩者有何差別呢? 計時 / 計數器通常會搭配中斷功能一起運作當計時終了觸發中斷執行特定程序,在計時期間系統會依照設定時脈週期一步一步的計數直到計數到設定值表示完成一次計數循環,例如計時 1 秒鐘,每 10 ms 數一步那麼數到 100 次就完成了。

8051 內部有兩組 16 位元長度的計時 / 計數器分別是 Timer 0 簡稱 T0 ,與 Timer 1 簡稱 T1。16 位元的範圍是 0 ~ 65535 因此可以數 65535 步便會產生溢位觸發中斷。

回到目錄

相關暫存器

使用計時 / 計數器需要設定相關暫存器才能完成計數程序,以下介紹相關暫存器。

中斷致能暫存器
  • EA : 中斷總開關。EA=1 啟動中斷功能,EA=0 關閉中斷功能。
  • ES :串列埠中斷開關。ES=1 啟動串列埠中斷,ES=0關閉串列埠中斷。
  • ET1 :Timer 1 中斷開關。ET1 =1 啟動 Timer 1 中斷, ET1=0 關閉 Timer 1 中斷。
  • EX1 :INT 1外部中斷開關。EX1=1 啟動INT_1 外部中斷, EX1=0 關閉INT_1 外部中斷。
  • ET0 :Timer 0 中斷開關。ET0 =1 啟動 Timer 0 中斷, ET0=0 關閉 Timer 0 中斷。
  • EX0 :INT 0外部中斷開關。EX0=1 啟動INT_0 外部中斷, EX0=0 關閉INT_0 外部中斷。
中斷優先暫存器

中斷優先暫存器可以設定各個中斷的優先權,設置 1 表示高優先權,設置 0 表示低優先權。

  • PS : 串列埠中斷優先權。PS=1 高優先權,PS=0 低優先權。
  • PT1 : Timer 1 中斷優先權。PT1=1 高優先權,PT1=0 低優先權。
  • PX1 :INT 1 外部中斷優先權。PX1=1 高優先權,PX1=0 低優先權。
  • PT0 : Timer 0 中斷優先權。PT0=1 高優先權,PT0=0 低優先權。
  • PX0 :INT 0 外部中斷優先權。PX0=1 高優先權,PX0=0 低優先權。
計時/計數控制暫存器

TCON 暫存器高位四位元 TF1 ~ TR0 是計時 / 計數溢位旗標與啟動開關。低位四位元是外部中斷旗標與啟動開關。

  • TF1 :TF1 中斷旗標。Timer 1 觸發中斷時自動設置 1,執行完中斷函式時自動清零。
  • TR1 :TR1 啟動開關。TR1 設置 1 時啟動 Timer 1 , TR1 設置 0 時關閉 Timer 1。
  • TF0 :TF0 中斷旗標。Timer 0 觸發中斷時自動設置 1,執行完中斷函式時自動清零。
  • TR0 :TR0 啟動開關。TR0 設置 0 時啟動 Timer 0 , TR0設置 0 時關閉 Timer 0。
  • IE1 :INT1 中斷旗標。發生 INT 1 中斷時自動設置 1,執行完中斷函式時自動清零。
  • IT1 :INT1 中斷型態。INT1=1 負緣觸發,INT1=0 低準位觸發。
  • IE0 :INT0 中斷旗標。發生 INT 0 中斷時自動設置 1,執行完中斷函式時自動清零。
  • IT0 :INT1 中斷型態。INT0=1 負緣觸發,INT0=0 低準位觸發。
計時/計數模式控制暫存器

TMOD 暫存器分成兩組分別控制 Timer 1 與 Timer 0 。

  • GATE :以 Timer 1為例,當 GATE = 0 且 TCON 的 TR1 = 1,啟動 Timer 1 此為內部啟動。當 GATE = 1 且 TCON 的 TR1 = 1 以及 INT1 為高電位時啟動 Timer 1 此為外部啟動。
  • C/T : 內部計時 / 外部計數開關。當 C/T = 0 時使用系統時脈。當 C/T = 1 時計數器的時脈由 T1 ( Timer 1 )或 T0 ( Timer 0 ) 輸入,此時稱為外部計數。
  • M1 與 M0 組成四種計數模式。
計時計數器 4模式
回到目錄

四種模式

8051 的 Timer 0 與 Timer 1 各有有四種運作模式,說明如下。

一、模式 0

模式 0 ,M1=0 M0=0。此模式下計時 / 計數器為 13 位元計數模式。其中高 8 位元存放於

TH0 暫存器,低 5 位元存放於 TL0 暫存器。

模式 0

上圖以 Timer 0 為示例,把 T0 、TR0 、INT0 替換成 T1、TR1、INT1 即可套用在 Timer 1。

  • 當 C/T = 0 執行計時功能此時外部震盪器會先通過除頻器先除以 12 後再進入計時器內部,若外部震盪器為 12MHz 則進入計時器的時脈為 12 M / 12 = 1 MHz,時脈週期為 1 us ,也就是說每隔 1 us 計時器便會上數一步。
  • 當C/T = 1 此時計數時脈由 T0 腳位輸入,形成外部計數。

TH0 與 TL0 形成 13 位元暫存器,最大儲存範圍為 0 ~ 8191 ,數到超過範圍就會溢位並觸發中斷。

二、模式 1

模式 1 ,M1=0 M0=1。此模式下計時 / 計數器為 16 位元計數模式。其中高 8 位元存放於

TH0 暫存器,低 8 位元存放於 TL0 暫存器。

模式 1

運作方式與模式 0 雷同,唯計數範圍比較大 0 ~ 65535。通常使用計時功能習慣使用模式 1。

三、模式 2

模式 2 ,M1=1 M0=0。此模式下 8位元 TL0 暫存器溢位時會將 8 位元暫存器 TH0 內容自動載入到 TL0,計數範圍 0 ~ 255 。

模式 2

模式 2 真正擔任計數器的是 TL0 暫存器,TH0 存放設定值。當 TL0 溢位時會自動將 TH0 內容載入到 TL0 不需要使用者在程式中特意設定。

四、模式 3

模式 2 ,M1=1 M0=1。此模式提供兩組獨立 8 位元計時 / 計數器。

Timer 1 在此模式下無計時 / 計數作用,其 TR1 與 TF1 被用來做為內部計時器的啟動與中斷。

模式 3
回到目錄

電路圖

實驗電路圖
回到目錄

新建專案

這一次 Timer 實驗設計成 Timer 0 與 Timer 1 分別控制 Port 1 與 Port 2 上的 LED 燈,其中 Timer 0 為模式 1 使用 16 位元計數長度,Timer 1 為模式 2 使用 8 位元計數長度並有自動加載功能,兩個 Timer 分別使用不同模式藉此了解兩種模式在使用上的差異。

Timer 0 設計成每秒鐘觸發一次中斷LED 燈移動一次,Timer 1 設計成每 0.5 秒觸發一次中斷 LED燈移動一次。

首先在 Embedded IDE 中點擊 New Project,選擇 Internal Template 內建模板,接著選擇 89C52 SDCC Quickstart 。

新建專案
選用內建模板
89C52 SDCC 模板

最後輸入專案名稱,例如本專案命名為 TIMER_LED,輸入完成後按下 Enter 鍵並選擇存放資料夾即完成專案的建立。

輸入專案名稱
回到目錄

流程圖

流程圖
回到目錄

程式撰寫

首先引入標頭檔案 < lint.h > 與 < 8051.h >。

#include <lint.h>
#include <8051.h>

接著宣告變數分別給 Timer_0 與 Time_1 中斷服務常式內使用。其中 cnt0 與 cnt1 拿來做為延時用途,LED0 與 LED1 則是輸出燈號的狀態。

unsigned char cnt0=0;
unsigned char LED0=0x01;
unsigned int cnt1=0;
unsigned char LED1=0x01;

設定 Timer_0 暫存器初始化

void timer0_init(void)
{
TH0 = (65536–50000)/256; // 50000 表示計數 50000 步後觸發中斷,即 50000 us
TL0 = (65536–50000)%256; // 外接震盪器為 12 MHz 經過除頻 12 後時脈頻率為 1MHz
TMOD = (TMOD&0xF0) | 0x01; // 設定 Timer_0 為 Mode 1
ET0 = 1; // 啟動 Timer 0 中斷
TR0 = 1; // 啟動 Timer_0
}

目標值為何要寫成 ( 65536–50000 ) ?

因為 16 位元計數器的計數範圍為 0 ~ 65535,這裡我們希望數 50000 步就觸發中斷因此用 65536 減去 50000 等於 15536 ,計數器會以 15536 為初值往上數到溢位進而觸發中斷,15536 數到 65536 就是 50000 步,使用 ( 65536–50000 ) 這樣的寫法可以直觀的看出我們要的計數步數。

模式 1 是 16 位元長度計數分別由 TH0 與 TL0 兩個 8 位元暫存器一同組成,因此需要把目標值分別填入兩個暫存器。TH0 代表 16 位元中的高 8 位元 Bit_15 ~ Bit_8 ,因此可以利用除法求商將目標值填入 TH0,也因為 TH0 是 8 位元所以把目標值除以 2 的 8 次方256。同樣的,把目標值除以 256 取餘數可以得到低 8 位元填入 TL0 中。

以此為範例,65536–50000 = 15536

15536 = 0x 0011 1100 1011 0000

15536/256 = 0x 0011 1100

15536%256 = 0x 1011 0000

TMOD 的 Bit_1 與 Bit_0 是 Timer_0 的 M1 與 M0 暫存器,TMOD 的初始值為 0 因此將位於 Bit_0 的 M0 設為 1 表示將 Timer_0 的模式設定為模式 1,也就是 16 位元長度計數。

TMOD = (TMOD&0xF0) | 0x01; 表示保留 TMOD 的高 4 位元設定值並只更動 Bit_0 位元 ET0 與 TR0 分別是開啟 Timer_0 的中斷與啟動Timer_0 計數功能。

初始化 Timer_0 後來完成 Timer_0 的中斷服務常式,也就是觸發中斷要做的事情。

void Timer0_ISR(void) __interrupt(1)
{
TH0 = (65536-50000)/256;
TL0 = (65536-50000)%256;
cnt0++; // counter + 1
if(cnt0 >= 20) // 50000*20*1us = 1 second
{
cnt0 = 0;
P1 = ~LED0; // P1 輸出
LED0 = LED0 << 1; // 將 LED0 內容左移
if(LED0 > 0x08) LED0 = 0x01; // 大於 8 時回到 1
}
}

我們將中斷服務常式命名為 Timer0_ISR ,函式的命名以能識別為主沒有固定寫法, __interrupt(1) 是 SDCC 的格式,括號內的 1 表示中斷編號,Timer_0 的中斷編號是 1 這點不能寫錯否則觸發中斷會跳去別的位址就不能正常執行了。

在中斷服務常式內TH0 與 TL0 又重新設定一次數值這是因為觸發中斷後若不重設數值便會停止,所以要在中斷期間再賦予新值。我們預設每隔 1 秒觸發一次中斷如果 16 位元計數到溢位 65536 ,每隔 1 us 數一步最多也只能數 65536 us = 65.536 ms 遠遠不足需求因此需要另外設定一個變數來計數中斷發生次數,Timer_0 我們設定為計數 50000 步發生中斷,50000 * 1us = 50 ms,若觸發中斷 20 次即 20 * 50 ms = 1000 ms = 1 s 剛好一秒,所以當中斷觸發 20 次時表示 1 秒鐘到了,因此加個判別式 if ( cnt0 >= 20 ) ,達到 1 秒後將變數 cnt0 歸零繼續下一輪計數,P1 輸出變數 LED0 初值為 0x01 ,取反 ~LED0 是因為 P1 上的 LED 燈是共陽極形式連接,P1 輸出低電位才會亮燈,P1 輸出一次後 LED0 內容往左移動 1 次,從 0x01 變成 0x02 依此類推直到 0x08 輸出後回到 0x01 ,這一段中斷服務常式會將 P1_0 ~ P1_3 上的 LED 燈依序每隔一秒點亮持續重複。

接著是 Timer_1 的部分,其設定方式與 Timer_0 雷同。

void timer1_init(void)
{
TH1 = (256-200)/256;
TL1 = (256-200)%256;
TMOD = (TMOD&0x0F) | 0x20;
ET1 = 1;
TR1 = 1;
}

因為 Timer_1 工作在模式 2 ,8 位元長度計數因此最多數到 256 步即產生溢位觸發中斷。

這裡設定成數 200 步觸發中斷,TMOD 的 Bit_5與 Bit_4 是 Timer_1 的 M1 與 M0 ,為了設定工作在模式 2 需要將 M1 設定為 1 ,TMOD = (TMOD&0x0F) | 0x20 保留低 4 位元並將 Bit_5 設為 1。ET1 與 TR1 分別啟動 Timer_1 中斷與計時功能。

Timer_1 觸發中斷後要該什麼 ?

void Timer1_ISR(void) __interrupt(3)
{
cnt1++;
if(cnt1 >= 2500) // 200*2500*1us= 0.5 second
{
cnt1 = 0;
P2 = ~LED1; // P2 輸出
LED1 = LED1 << 1; // 將 LED0 內容左移
if(LED1 > 0x08) LED1 = 0x01; // 大於 8 時回到 1
}
}

Timer_1 的中斷服務常式其中斷編號為 3 ,由於設定成 200 步觸發一次中斷因此每隔 200 * 1us = 200 us 就會觸發一次,想要每隔 0.5 秒點亮 LED 燈需要設一個變數來計數中斷發生次數,發生 2500 次時,2500 * 200 * 1us = 500000 us = 500 ms = 0.5 s ,因此發生 2500 次中斷後便將變數 LED1 內容輸出到 P2 藉此點亮 LED 燈,每點亮一次就將變數 LED1 內容左移一次,直到大於 0x08 設回 0x01 從頭來過。

讀者是否有發現 Timer_0 與 Timer_1 的中斷服務常式有哪裡不一樣 ?

Timer_1 發生中斷不須重新設定 TH 與 TL 暫存器,這是模式 2 的特性。在初始化時已經設定過一次,TL 存放目標值而TH 執行計數,觸發中斷後會自動將 TL 內容放到 TH 裡面而整個計數週期又重新來過,因此在 Timer_1 的中斷服務常式內不需要再設定一次 TH 與 TL 內容。

主程式 main 內只需要呼叫初始化函數並且開啟中斷功能。

void main(void)
{
timer0_init();
timer1_init();
EA = 1; // 啟動中斷總開關
P1 = 0x0F; // P1 關閉全部 LED 燈
P2 = 0x0F; // P2 關閉全部 LED 燈
while (1); // 空循環
}
回到目錄

編譯與成果

完成程式撰寫後回到 Embedded IDE 環境中,滑鼠點擊上方的專案確認編譯器是否為 SDCC,之後按下編譯鍵 ( Build ) 進行編譯。

編譯

編譯過程螢幕下方會開啟終端機顯示訊息,編譯完成後提示按下任意鍵離開終端機。

編譯完成

回到專案資料夾,在 build \ Debug 資料夾內部有一個副檔名為 .hex 的檔案便可以使用燒錄軟體進行燒錄。

16 進位燒錄檔
回到目錄

完整程式碼

#include <lint.h>
#include <8051.h>

unsigned char cnt0=0;
unsigned char LED0=0x01;
unsigned int cnt1=0;
unsigned char LED1=0x01;

void timer0_init(void)
{
TH0 = (65536-50000)/256; // 50000 表示計數 50000 步後觸發中斷,即 50000 us
TL0 = (65536-50000)%256; // 外接震盪器為 12 MHz 經過除頻 12 後時脈頻率為 1MHz
TMOD = (TMOD&0xF0) | 0x01; // 設定 Timer_0 為 Mode 1
ET0 = 1; // 啟動 Timer 0 中斷
TR0 = 1; // 啟動 Timer_0
}

void timer1_init(void)
{
TH1 = (256-200)/256;
TL1 = (256-200)%256;
TMOD = (TMOD&0x0F) | 0x20;
ET1 = 1;
TR1 = 1;
}

void Timer0_ISR(void) __interrupt(1)
{
TH0 = (65536-50000)/256;
TL0 = (65536-50000)%256;
cnt0++; // counter + 1
if(cnt0 >= 20) // 50000*20*1us = 1 second
{
cnt0 = 0;
P1 = ~LED0; // P1 輸出
LED0 = LED0 << 1; // 將 LED0 內容左移
if(LED0 > 0x08) LED0 = 0x01; // 大於 8 時回到 1
}
}

void Timer1_ISR(void) __interrupt(3)
{
cnt1++;
if(cnt1 >= 2500) // 200*2500*1us= 0.5 second
{
cnt1 = 0;
P2 = ~LED1; // P2 輸出
LED1 = LED1 << 1; // 將 LED0 內容左移
if(LED1 > 0x08) LED1 = 0x01; // 大於 8 時回到 1
}
}

void main(void)
{
timer0_init();
timer1_init();
EA = 1; // 啟動中斷總開關
P1 = 0x0F; // P1 關閉全部 LED 燈
P2 = 0x0F; // P2 關閉全部 LED 燈
while (1); // 空循環

}
回到目錄

總結

8051 具有兩組計時 / 計數器 Timer_0 與 Timer_1,其計數長度與運作模式讓使用者可彈性運用。計時 / 計數器總結如下:

  • 8051 擁有兩組獨立計時 / 計數器。
  • 暫存器 TCON 控制兩組計時 / 計數器的啟動與中斷旗標。
  • 暫存器 TMOD 控制兩組計時 / 計數器的運作模式與設定內部 / 外部啟動。
  • 模式 0 為 13 位元計數長度,計數範圍 0 ~ 8191 ,計數目標值高 8 位元存放於 TH 低 5 位元存放於 TL 暫存器。
  • 模式 1 為 16 位元計數長度,計數範圍 0 ~ 65535 ,計數目標值高 8 位元存放於 TH 低 8 位元存放於 TL 暫存器。
  • 模式 2 為 8 位元計數長度,計數範圍 0 ~ 255,計數目標值存放於 TL 暫存器,TH 暫存器為實際計數用途,計數終了觸發中斷後會自動將 TL 內容加載到 TH 繼續下一輪計數。
  • 模式 3 為各自獨立 8 位元計時 / 計數器,此模式只運作於 Timer_0 而 Timer_1 的啟動開關與中斷旗標被用於計時功能因此 Timer_1 無法運作。
  • Timer_0 的中斷編號為 1 ,Timer_1 的中斷編號為 3 。
回到目錄

參考資源

  • ElectricWins 網站 [連結]
  • Delta MOOCx 課程-微算機原理及應用 [連結]
回到目錄

感謝讀者

感謝讀者閱讀文章, 若您喜歡本篇文章可拍手鼓勵,謝謝您。

--

--

Morgan Ting
閱益如美

用好奇心探索世界。喜愛學習樂於分享。