第17章 Arduino Timer(計時器)與精準時間控制:Timer1、CTC、中斷與週期任務完整教學
本章完整介紹 Arduino Timer 硬體計時器的工作原理,說明 Arduino Uno 的 Timer0、Timer1、Timer2、系統時脈、Prescaler、Overflow、Compare Match 與 CTC 模式。透過 Timer1 實作 1 毫秒系統節拍、LED 定時切換、感測器固定週期取樣與多週期任務排程,並說明 ISR 設計原則、事件旗標、事件計數器、PWM、Servo、tone() 與 millis() 的 Timer 資源衝突,協助讀者建立穩定且精準的時間控制程式。
Timer(硬體計時器)是 Arduino Uno 進行精準時間控制、固定週期取樣、PWM 與脈衝輸出的核心硬體。本章將從系統時脈、Prescaler、Overflow 與 Compare Match 開始,逐步說明 Timer0、Timer1、Timer2 的用途與差異。
實作將以 Arduino Uno R3 的 ATmega328P 為主,使用 Timer1 CTC 模式建立 1 毫秒系統節拍、週期事件與多任務排程,並示範如何讓 ISR 只負責計數與設定旗標。
直接修改 Timer 暫存器可能影響 millis()、micros()、delay()、analogWrite()、Servo 與 tone()。進行設定前,必須先確認 Timer 資源是否已被 Arduino 核心或其他函式庫占用。
學習目標
完成本章後,你將能夠:
- 了解 Timer(硬體計時器)的基本工作原理。
- 認識 Arduino Uno 的 Timer0、Timer1 與 Timer2。
- 理解系統時脈、Prescaler 與計數器之間的關係。
- 分辨 Overflow 與 Compare Match 兩種計時事件。
- 了解 millis()、micros()、delay() 與 Timer0 的關係。
- 了解 PWM 與 Timer 的關係。
- 使用 Timer1 建立固定週期中斷。
- 正確設計 Timer ISR。
- 避免修改 Timer 後影響 PWM、Servo 與時間函式。
- 使用 Timer 建立精準週期任務與脈衝輸出。
為什麼需要 Timer?
在前面的章節中,我們已經使用過:
delay();
millis();
micros();
這些函式可以協助 Arduino 處理時間。
例如:
delay(1000);
表示等待 1 秒。
或者:
if(millis() - previousMillis >= 1000)
{
previousMillis = millis();
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
表示每隔 1 秒切換一次 LED。
但是,當專案需要更精準或更穩定的時間控制時,只依靠主程式檢查時間可能不夠。
例如:
- 每 1 毫秒執行一次控制計算。
- 固定頻率讀取感測器。
- 產生穩定方波。
- 控制馬達速度迴路。
- 建立精準取樣週期。
- 播放特定頻率聲音。
- 產生 PWM。
- 計算脈衝時間。
- 執行即時控制系統。
這些功能通常需要使用微控制器內部的:
Hardware Timer(硬體計時器)。
什麼是 Timer?
Timer 是微控制器內部的一種硬體計數器。
它會根據系統時脈持續增加數值。
基本流程如下:
系統時脈
↓
Prescaler 分頻
↓
Timer 計數器遞增
↓
到達指定數值
↓
產生事件或中斷
Timer 不需要依靠 loop() 主動增加數值。
即使 Arduino 正在執行其他程式,Timer 硬體仍會在背景中持續計數。
因此,Timer 可以提供比一般軟體等待更穩定的時間基準。
知識卡:Timer
Arduino Uno 的系統時脈
Arduino Uno R3 通常使用:
16 MHz
系統時脈。
MHz 是 Megahertz,代表每秒幾百萬次振盪。
16 MHz = 每秒 16,000,000 次時脈
每一個時脈週期的時間約為:
1 ÷ 16,000,000 秒
也就是:
0.0000000625 秒
換算為:
62.5 奈秒
如果 Timer 每一個系統時脈都加一,速度會非常快。
因此,需要使用 Prescaler 降低 Timer 的計數速度。
Prescaler 是什麼?
Prescaler 中文常稱為:
- 預分頻器。
- 時脈分頻器。
它可以先將系統時脈除以一個固定數值,再提供給 Timer。
例如 Arduino Uno 的系統時脈為:
16 MHz
如果 Prescaler 設定為:
64
Timer 實際接收到的時脈為:
16,000,000 ÷ 64
= 250,000 Hz
也就是 Timer 每秒增加:
250,000 次
每一次計數所需時間為:
1 ÷ 250,000
= 0.000004 秒
= 4 微秒
Timer 計算公式
Timer 計數頻率:
Timer 頻率 = CPU 時脈 ÷ Prescaler
每次計數時間:
每次計數時間 = Prescaler ÷ CPU 時脈
若 CPU 為 16 MHz,Prescaler 為 64:
每次計數時間
= 64 ÷ 16,000,000
= 4 微秒
8 位元與 16 位元 Timer
Timer 本質上是一個持續增加的二進位計數器。
不同 Timer 有不同位元數。
8 位元 Timer
8 位元可表示:
0~255
總共有:
256 個計數值
當計數從 255 再增加一次,就會回到 0。
16 位元 Timer
16 位元可表示:
0~65535
總共有:
65536 個計數值
16 位元 Timer 可以計算更長的時間,也能設定更細緻的週期。
Arduino Uno 的 Timer1 就是 16 位元 Timer。
Arduino Uno 的三個 Timer
ATmega328P 內部具有三個主要 Timer:
Timer0
Timer0 是 Arduino 核心系統非常重要的計時器。
它通常負責支援:
millis();
micros();
delay();
Timer0 也與部分 PWM 腳位有關。
Arduino Uno 上常見對應為:
D5
D6
因此,如果直接修改 Timer0 的設定,可能造成:
- millis() 計時不準。
- micros() 計時不準。
- delay() 時間錯誤。
- D5、D6 PWM 頻率改變。
初學者通常不應隨意修改 Timer0。
Timer1
Timer1 是 Arduino Uno 唯一的 16 位元 Timer。
它具備較大的計數範圍,因此非常適合:
- 精準週期中斷。
- 低頻 PWM。
- 馬達控制。
- Servo 控制。
- 輸入捕捉。
- 頻率測量。
- 精準脈衝輸出。
Timer1 通常與 PWM 腳位:
D9
D10
有關。
Arduino Servo 函式庫在 Uno 上通常也會使用 Timer1。
因此,如果程式已使用:
#include <Servo.h>
再修改 Timer1 設定,可能造成 Servo 功能異常。
Timer2
Timer2 是另一個 8 位元 Timer。
通常與 PWM 腳位:
D3
D11
有關。
Arduino 的:
tone();
功能通常會使用 Timer2。
因此,若修改 Timer2,可能影響:
- tone()。
- noTone()。
- D3、D11 的 PWM 頻率。
Timer 與 PWM 腳位對應
Arduino Uno 常見 Timer 與 PWM 腳位關係如下:
這也是為什麼不同 PWM 腳位的預設頻率不一定完全相同。
PWM 並不是由 analogWrite() 函式本身憑空產生,而是由 Timer 硬體協助輸出。
Timer 的基本運作流程
假設一個 8 位元 Timer:
0
1
2
3
...
253
254
255
0
1
2
...
Timer 會持續增加。
當發生特定條件時,可以:
- 設定旗標。
- 改變輸出腳位。
- 觸發中斷。
- 清除計數器。
- 重新開始計數。
常見事件包括:
- Overflow。
- Compare Match。
Overflow(溢位)
當 Timer 計數到最大值後,再增加一次,就會回到 0。
例如 8 位元 Timer:
254
255
0
1
從 255 回到 0 的事件稱為:
Overflow(溢位)。
Timer 可以在溢位時產生中斷。
Overflow 時間計算
假設:
- CPU 時脈:16 MHz
- Prescaler:64
- Timer:8 位元
- 計數範圍:0~255
每次計數需要:
64 ÷ 16,000,000
= 4 微秒
完整 256 次計數需要:
256 × 4 微秒
= 1024 微秒
也就是:
1.024 毫秒
Timer0 大致就是利用這類計時機制,建立 Arduino 的毫秒時間基準。
Compare Match(比較符合)
Timer 除了等待計數到最大值之外,也可以設定一個比較值。
例如:
比較值 = 249
Timer 從 0 開始計數:
0
1
2
...
248
249
當 Timer 值等於比較值時,就發生:
Compare Match。
此時可以:
- 觸發中斷。
- 清除 Timer。
- 切換輸出腳位。
- 執行週期事件。
CTC 模式
CTC 是:
Clear Timer on Compare Match
意思是:
當 Timer 計數值等於比較值時,自動將計數器清零。
流程如下:
Timer 從 0 開始
↓
持續計數
↓
到達 OCR 比較值
↓
產生 Compare Match
↓
Timer 清零
↓
重新計數
CTC 非常適合建立固定週期中斷。
Timer 週期公式
在 CTC 模式中,週期通常可用以下概念計算:
中斷頻率
=
CPU 時脈
÷
Prescaler
÷
(比較值 + 1)
也可以寫成:
比較值
=
CPU 時脈
÷
Prescaler
÷
目標頻率
- 1
範例:建立每秒 1000 次中斷
目標頻率:
1000 Hz
也就是每:
1 毫秒
觸發一次。
假設:
CPU 時脈 = 16,000,000 Hz
Prescaler = 64
比較值:
16,000,000 ÷ 64 ÷ 1000 - 1
計算結果:
250 - 1
= 249
因此:
OCR = 249
Timer 從 0 數到 249,一共 250 個計數。
每次計數 4 微秒:
250 × 4 微秒
= 1000 微秒
= 1 毫秒
Timer 暫存器是什麼?
微控制器內部功能通常由暫存器控制。
Timer 相關暫存器可以設定:
- 計時模式。
- Prescaler。
- 比較值。
- 計數器目前值。
- 中斷開關。
- 輸出行為。
Arduino 一般函式會幫我們隱藏許多暫存器細節。
但是,如果要直接控制 Timer,就需要操作 ATmega328P 的暫存器。
Timer1 常見暫存器
初學者不必一次背下所有位元,只要先理解每個暫存器負責的功能。
第一個 Timer1 實驗:每秒切換 LED
實驗目標
使用 Timer1 CTC 模式:
- 每 1 毫秒產生一次中斷。
- ISR 累積 1000 次。
- 每 1000 毫秒設定一次事件。
- loop() 負責切換 LED。
這個設計會延續第 15 章的原則:
ISR 只做最少工作,主要控制交給 loop()。
接線
可以直接使用 Arduino Uno 內建 LED:
LED_BUILTIN
通常位於 D13。
不需額外接線。
完整程式
const int LED_PIN = LED_BUILTIN;
volatile unsigned int millisecondCount = 0;
volatile bool oneSecondEvent = false;
bool ledState = false;
void setup()
{
pinMode(LED_PIN, OUTPUT);
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 249;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS11);
TCCR1B |= (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
interrupts();
}
void loop()
{
bool eventCopy = false;
noInterrupts();
if (oneSecondEvent)
{
oneSecondEvent = false;
eventCopy = true;
}
interrupts();
if (eventCopy)
{
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
}
ISR(TIMER1_COMPA_vect)
{
millisecondCount++;
if (millisecondCount >= 1000)
{
millisecondCount = 0;
oneSecondEvent = true;
}
}
程式設定分析
清除 Timer1 設定
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
用途:
- 清除原本控制設定。
- 將 Timer1 計數值歸零。
- 避免舊設定影響新模式。
設定比較值
OCR1A = 249;
代表 Timer1 從 0 計數到 249。
總共有:
250 次計數
在 16 MHz、Prescaler 64 的條件下,剛好約為 1 毫秒。
設定 CTC 模式
TCCR1B |= (1 << WGM12);
WGM12 是 Waveform Generation Mode 的其中一個控制位元。
設定後,Timer1 會使用 CTC 模式。
當計數值到達 OCR1A 時,自動清零。
設定 Prescaler 64
TCCR1B |= (1 << CS11);
TCCR1B |= (1 << CS10);
Timer1 的 Clock Select 位元為:
CS12
CS11
CS10
設定組合:
0 1 1
代表:
Prescaler = 64
啟用 Compare Match A 中斷
TIMSK1 |= (1 << OCIE1A);
這行會啟用:
Timer1 Compare Match A Interrupt
當 Timer1 值等於 OCR1A 時,就會執行:
ISR(TIMER1_COMPA_vect)
Timer ISR
ISR(TIMER1_COMPA_vect)
{
millisecondCount++;
if(millisecondCount >= 1000)
{
millisecondCount = 0;
oneSecondEvent = true;
}
}
這個 ISR 每 1 毫秒執行一次。
它只做:
- 計數加一。
- 檢查是否達到 1000。
- 設定事件旗標。
不在 ISR 中執行:
- Serial.print()。
- LCD 更新。
- 長時間計算。
- delay()。
- I²C 通訊。
為什麼不直接在 ISR 切換 LED?
下面寫法雖然通常可以運作:
ISR(TIMER1_COMPA_vect)
{
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
但不建議將所有功能都放入 ISR。
更好的方式是:
ISR 設定事件
↓
loop() 處理事件
這樣未來若要加入:
- Serial 輸出。
- LCD 更新。
- 多個控制動作。
- 感測器讀取。
會更加安全且容易維護。
第二個實驗:每 10 毫秒讀取一次感測器
實驗目標
使用 Timer1:
- 每 1 毫秒產生中斷。
- 每 10 毫秒設定感測器取樣事件。
- 主程式讀取 A0。
- 每 500 毫秒輸出一次結果。
程式
const int SENSOR_PIN = A0;
volatile unsigned int timerMilliseconds = 0;
volatile bool sampleEvent = false;
int latestSensorValue = 0;
unsigned long previousReportMillis = 0;
const unsigned long REPORT_INTERVAL = 500;
void setup()
{
Serial.begin(9600);
setupTimer1();
}
void loop()
{
bool sampleCopy = false;
noInterrupts();
if (sampleEvent)
{
sampleEvent = false;
sampleCopy = true;
}
interrupts();
if (sampleCopy)
{
latestSensorValue = analogRead(SENSOR_PIN);
}
unsigned long currentMillis = millis();
if (currentMillis - previousReportMillis >= REPORT_INTERVAL)
{
previousReportMillis = currentMillis;
Serial.print("Sensor: ");
Serial.println(latestSensorValue);
}
}
void setupTimer1()
{
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 249;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS11);
TCCR1B |= (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
interrupts();
}
ISR(TIMER1_COMPA_vect)
{
timerMilliseconds++;
if (timerMilliseconds >= 10)
{
timerMilliseconds = 0;
sampleEvent = true;
}
}
這個範例的時間架構
Timer1 每 1 ms 中斷
↓
累積 10 次
↓
設定 sampleEvent
↓
loop() 執行 analogRead()
↓
每 500 ms 使用 Serial 顯示
這種設計將時間觸發與實際工作分開。
事件旗標會遺失事件嗎?
假設 Timer 每 10 毫秒設定:
sampleEvent = true;
但主程式因為其他工作花了 30 毫秒才處理。
這段期間 ISR 可能設定旗標多次,但 bool 仍只有:
true
因此多次事件可能合併成一次。
如果每一次事件都不能遺失,應使用計數器:
volatile byte pendingSamples = 0;
ISR:
if(pendingSamples < 255)
{
pendingSamples++;
}
主程式每次取出一個事件。
使用事件計數器
volatile byte pendingSamples = 0;
ISR(TIMER1_COMPA_vect)
{
timerMilliseconds++;
if (timerMilliseconds >= 10)
{
timerMilliseconds = 0;
if (pendingSamples < 255)
{
pendingSamples++;
}
}
}
主程式:
bool hasSample = false;
noInterrupts();
if (pendingSamples > 0)
{
pendingSamples--;
hasSample = true;
}
interrupts();
if (hasSample)
{
latestSensorValue = analogRead(SENSOR_PIN);
}
這能保留多個尚未處理的事件。
不過,如果主程式長期跟不上 Timer 產生事件的速度,事件佇列仍會累積或溢位。
因此還需要確保:
主程式的處理速度必須快於事件產生速度。
Timer 與即時系統
Timer 常用於建立固定週期工作,例如:
大型程式可以建立一個系統節拍:
1 ms Tick
再由它產生多種週期事件:
1 ms
10 ms
100 ms
1000 ms
第三個實驗:多週期任務排程
實驗功能
使用同一個 1 毫秒 Timer,建立:
- 每 10 毫秒執行一次快速任務。
- 每 100 毫秒執行一次中速任務。
- 每 1000 毫秒執行一次慢速任務。
程式
volatile unsigned int tick10ms = 0;
volatile unsigned int tick100ms = 0;
volatile unsigned int tick1000ms = 0;
volatile bool task10msEvent = false;
volatile bool task100msEvent = false;
volatile bool task1000msEvent = false;
void setup()
{
Serial.begin(9600);
setupTimer1();
}
void loop()
{
bool run10ms = false;
bool run100ms = false;
bool run1000ms = false;
noInterrupts();
if (task10msEvent)
{
task10msEvent = false;
run10ms = true;
}
if (task100msEvent)
{
task100msEvent = false;
run100ms = true;
}
if (task1000msEvent)
{
task1000msEvent = false;
run1000ms = true;
}
interrupts();
if (run10ms) task10ms();
if (run100ms) task100ms();
if (run1000ms) task1000ms();
}
void setupTimer1()
{
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 249;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS11);
TCCR1B |= (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
interrupts();
}
ISR(TIMER1_COMPA_vect)
{
tick10ms++;
tick100ms++;
tick1000ms++;
if (tick10ms >= 10)
{
tick10ms = 0;
task10msEvent = true;
}
if (tick100ms >= 100)
{
tick100ms = 0;
task100msEvent = true;
}
if (tick1000ms >= 1000)
{
tick1000ms = 0;
task1000msEvent = true;
}
}
void task10ms()
{
// 快速工作,例如按鍵掃描
}
void task100ms()
{
// 中速工作,例如讀取感測器
}
void task1000ms()
{
Serial.println("System heartbeat");
}
這是作業系統嗎?
不是。
這種方式只是簡單的:
Cooperative Task Scheduling(協作式任務排程)。
它不像電腦作業系統或 RTOS 那樣:
- 自動切換任務。
- 管理任務優先順序。
- 配置獨立堆疊。
- 暫停與恢復任務。
所有任務仍由 loop() 依序執行。
因此,每個任務仍應:
- 快速完成。
- 不使用長時間 delay()。
- 不進入無限迴圈。
- 避免長時間阻塞。
Timer 中斷頻率不能無限提高
假設 Timer 每:
1 微秒
中斷一次。
也就是:
1,000,000 次/秒
微控制器幾乎所有時間都會用來:
- 進入 ISR。
- 保存暫存器。
- 執行 ISR。
- 返回主程式。
主程式將沒有足夠時間執行。
因此,中斷頻率越高:
- CPU 負擔越大。
- 其他中斷越容易延遲。
- 主程式可用時間越少。
- 系統越容易不穩定。
設計 Timer 時必須考慮:
ISR 執行時間必須遠小於中斷週期。
ISR 使用率概念
假設:
- Timer 每 100 微秒中斷一次。
- ISR 每次執行 20 微秒。
那麼 CPU 用於 ISR 的比例約為:
20 ÷ 100 = 20%
如果 ISR 執行 80 微秒:
80 ÷ 100 = 80%
主程式只剩約 20% 時間。
如果 ISR 執行時間超過中斷週期,系統就可能無法正常處理事件。
Timer 中斷的設計原則
Timer ISR 建議只做:
- 增加計數器。
- 設定旗標。
- 更新簡單狀態。
- 寫入單一硬體暫存器。
- 進行極短的數值操作。
不建議在 Timer ISR 中:
- Serial.print()。
- delay()。
- LCD 或 OLED 更新。
- I²C 通訊。
- SPI 大量傳輸。
- EEPROM 寫入。
- 浮點數複雜計算。
- 動態記憶體操作。
- 長迴圈。
Timer 與 analogWrite()
在 Arduino Uno 中,analogWrite() 依賴 Timer 產生 PWM。
例如 D9 與 D10 使用 Timer1。
如果你重新設定 Timer1 為 CTC 模式:
TCCR1A = 0;
TCCR1B = 0;
原本 D9、D10 的 PWM 設定會被改變。
此時:
analogWrite(9, 128);
可能不再正常輸出預期的 PWM。
因此:
使用 Timer1 自訂計時功能時,應避免再依賴 D9、D10 的標準 analogWrite()。
Timer 與 Servo 函式庫
Arduino Uno 的 Servo 函式庫通常會使用 Timer1 產生 Servo 控制脈衝。
如果程式同時使用:
#include <Servo.h>
以及自訂 Timer1 暫存器:
TCCR1A
TCCR1B
OCR1A
兩者可能互相覆蓋設定。
結果可能包括:
- Servo 抖動。
- Servo 無法轉動。
- Timer 中斷週期錯誤。
- PWM 失效。
- 程式行為不穩定。
因此,在使用 Timer 前必須先確認:
是否已有其他函式庫占用同一個 Timer。
Timer 與 tone()
Arduino Uno 的 tone() 通常會使用 Timer2。
例如:
tone(8, 1000);
如果同時修改 Timer2:
TCCR2A
TCCR2B
OCR2A
則 tone() 可能無法正常工作。
Timer 資源衝突表
實際使用情況可能因開發板核心與函式庫實作而不同,但 Arduino Uno 通常可依此原則判斷。
Timer 設定前的檢查清單
在修改 Timer 前,先確認:
- 是否使用 millis()、micros() 或 delay()?
- 是否使用 Servo?
- 是否使用 tone()?
- 是否使用對應 Timer 的 PWM 腳位?
- 是否有其他函式庫使用該 Timer?
- 需要的中斷頻率是多少?
- ISR 執行時間是否足夠短?
- 是否真的需要直接操作 Timer?
使用 Timer 函式庫
對初學者而言,直接操作暫存器較容易出錯。
也可以使用 Timer 函式庫,例如某些 TimerOne 類型的函式庫。
概念範例:
#include <TimerOne.h>
volatile bool timerEvent = false;
void setup()
{
Serial.begin(9600);
Timer1.initialize(1000000);
Timer1.attachInterrupt(handleTimer);
}
void loop()
{
if (timerEvent)
{
timerEvent = false;
Serial.println("One second");
}
}
void handleTimer()
{
timerEvent = true;
}
其中:
Timer1.initialize(1000000);
通常代表設定週期為:
1,000,000 微秒
= 1 秒
使用函式庫的優點與限制
優點
- 程式較容易閱讀。
- 不必直接設定大量暫存器。
- 降低位元設定錯誤。
- 適合快速建立原型。
限制
- 函式庫仍會占用某個 Timer。
- 可能與 Servo、PWM 等功能衝突。
- 不同函式庫的 API 不一定相同。
- 底層設定彈性可能較少。
- 必須確認是否支援目前開發板。
使用函式庫並不代表不需要理解 Timer。
只有理解 Timer 資源,才能避免函式庫之間互相衝突。
Timer 計算實例一:每 10 毫秒一次
目標週期:
10 ms
目標頻率:
100 Hz
假設:
CPU = 16 MHz
Prescaler = 64
比較值:
16,000,000 ÷ 64 ÷ 100 - 1
結果:
2499
因此可設定:
OCR1A = 2499;
Timer1 是 16 位元,可容納 2499。
Timer 計算實例二:每 1 秒一次
目標頻率:
1 Hz
若使用 Prescaler 64:
16,000,000 ÷ 64 ÷ 1 - 1
= 249999
但 Timer1 最大值只有:
65535
因此 249999 太大,無法直接使用。
可以改用較大的 Prescaler,例如:
1024
重新計算:
16,000,000 ÷ 1024 ÷ 1 - 1
= 15624
此數值小於 65535,因此可使用:
OCR1A = 15624;
第四個實驗:Timer1 每秒直接產生事件
程式
const int LED_PIN = LED_BUILTIN;
volatile bool timerEvent = false;
bool ledState = false;
void setup()
{
pinMode(LED_PIN, OUTPUT);
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 15624;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS12);
TCCR1B |= (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
interrupts();
}
void loop()
{
bool eventCopy = false;
noInterrupts();
if (timerEvent)
{
timerEvent = false;
eventCopy = true;
}
interrupts();
if (eventCopy)
{
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
}
ISR(TIMER1_COMPA_vect)
{
timerEvent = true;
}
Prescaler 1024 的設定
Timer1 Prescaler 由:
CS12
CS11
CS10
控制。
設定:
1 0 1
代表:
Prescaler = 1024
程式:
TCCR1B |= (1 << CS12);
TCCR1B |= (1 << CS10);
常見 Prescaler
Timer1 常見 Prescaler 包括:
其他組合可能代表:
- 停止 Timer。
- 使用外部時脈輸入。
如何選擇 Prescaler?
Prescaler 越小:
- Timer 計數越快。
- 時間解析度越高。
- 可直接計算的最長週期越短。
Prescaler 越大:
- Timer 計數越慢。
- 可計算較長週期。
- 時間解析度較低。
選擇原則:
- 先計算目標比較值。
- 確認比較值沒有超過 Timer 最大值。
- 在可行範圍內,選擇較小的 Prescaler,以保留較高解析度。
Timer 誤差來源
Timer 計算結果不一定完全等於真實時間。
誤差來源包括:
- 開發板晶振誤差。
- 陶瓷振盪器精度。
- 電壓與溫度變化。
- 比較值只能使用整數。
- ISR 延遲。
- 其他中斷造成的延後。
- 程式中斷被暫時停用。
因此,Arduino Timer 適合一般嵌入式控制,但若需要極高精度時間,可能需要:
- 精密晶振。
- RTC。
- 外部時鐘源。
- GPS PPS。
- 專業計時晶片。
中斷延遲與抖動
即使 Timer 在固定時間產生中斷,ISR 實際開始執行的時間仍可能稍微延後。
原因可能是:
- 其他 ISR 正在執行。
- 程式暫時關閉中斷。
- CPU 正在完成目前指令。
- 多個中斷同時發生。
這種時間上的微小變化稱為:
Jitter(抖動)。
對一般 LED、按鍵掃描與低速感測器而言,少量 Jitter 通常沒有問題。
但對:
- 高速通訊。
- 音訊取樣。
- 精密馬達控制。
- 波形產生。
則需要仔細評估。
不要長時間使用 noInterrupts()
當程式執行:
noInterrupts();
Timer 中斷也會被暫停。
若關閉中斷時間過長,可能造成:
- Timer 事件延遲。
- Timer 事件遺失。
- millis() 時間偏差。
- Serial 資料遺失。
- 外部中斷漏接。
因此:
noInterrupts();
只應保護非常短的共享資料操作。
使用硬體輸出比較產生方波
Timer 不一定需要進入 ISR 才能控制輸出。
某些 Timer 可以在 Compare Match 時,由硬體自動:
- 切換腳位。
- 設為 HIGH。
- 設為 LOW。
這稱為:
Output Compare。
優點是:
- 不需要 ISR。
- 頻率更穩定。
- Jitter 更低。
- CPU 負擔更小。
這類設定通常需要操作:
COM1A
COM1B
等輸出比較控制位元。
Timer1 硬體方波概念
例如讓 OC1A 腳位在每次 Compare Match 時切換狀態:
Compare Match
↓
D9 自動切換
↓
HIGH → LOW
↓
下一次 Compare Match
↓
LOW → HIGH
因為完整方波需要兩次切換,所以輸出頻率通常為:
輸出頻率
=
CPU 時脈
÷
2
÷
Prescaler
÷
(比較值 + 1)
硬體方波範例
void setup()
{
pinMode(9, OUTPUT);
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 15624;
TCCR1A |= (1 << COM1A0);
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS12);
TCCR1B |= (1 << CS10);
}
void loop()
{
}
這個程式會讓 Timer1 透過硬體切換 OC1A,也就是 Arduino Uno 的 D9。
為什麼程式沒有 ISR?
因為腳位切換由 Timer 硬體直接完成。
主程式不需要介入。
這種方式比在 ISR 裡呼叫:
digitalWrite();
更快、更穩定。
硬體方波頻率計算
在前面的設定中:
CPU = 16 MHz
Prescaler = 1024
OCR1A = 15624
Compare Match 頻率約為:
1 Hz
但每次 Compare Match 只切換一次狀態。
完整方波需要:
HIGH → LOW → HIGH
因此實際完整方波頻率約為:
0.5 Hz
也就是:
每 2 秒一個完整週期
D9 每 1 秒改變一次狀態。
Timer 作為外部事件計數器
Timer 除了使用 CPU 時脈,也可以使用外部腳位的脈衝作為計數來源。
此時 Timer 不再計算時間,而是計算:
- 外部脈衝數。
- 馬達旋轉脈衝。
- 流量計訊號。
- 頻率輸入。
- 編碼器訊號。
這種功能稱為:
Counter Mode(計數器模式)。
Arduino Uno 的某些 Timer 有專用外部時脈腳位。
這屬於較進階的暫存器應用,但概念仍然相同:
外部脈衝
↓
Timer 計數器增加
↓
達到目標值
↓
觸發事件
Timer 與 Input Capture
Timer1 還具有:
Input Capture(輸入捕捉)。
當特定輸入腳位出現上升緣或下降緣時,硬體會立即把 Timer 當前計數值保存到專用暫存器。
用途包括:
- 測量脈衝寬度。
- 測量頻率。
- 測量週期。
- 記錄事件精確時間。
- 分析超音波回波訊號。
Input Capture 比在一般 ISR 中自行讀取 micros() 更精準,因為捕捉動作由硬體完成。
Timer 與 Watchdog 的差異
Timer 與 Watchdog 都具有時間功能,但目的不同。
Watchdog 不是一般 Timer 的替代品。
它主要用於:
當程式卡住時,自動重啟系統。
綜合實驗:多週期環境監測控制器
專案功能
使用 Timer1 建立 1 毫秒系統節拍,並執行:
- 每 10 毫秒掃描按鍵。
- 每 100 毫秒讀取 A0 感測器。
- 每 500 毫秒更新 LED 狀態。
- 每 1000 毫秒輸出系統資訊。
- 所有耗時工作都放在 loop()。
- ISR 只設定事件旗標。
完整程式
const int SENSOR_PIN = A0;
const int BUTTON_PIN = 2;
const int LED_PIN = LED_BUILTIN;
volatile unsigned int counter10ms = 0;
volatile unsigned int counter100ms = 0;
volatile unsigned int counter500ms = 0;
volatile unsigned int counter1000ms = 0;
volatile bool event10ms = false;
volatile bool event100ms = false;
volatile bool event500ms = false;
volatile bool event1000ms = false;
int sensorValue = 0;
bool systemEnabled = true;
bool ledState = false;
int lastButtonState = HIGH;
void setup()
{
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
setupTimer1();
}
void loop()
{
bool run10ms = false;
bool run100ms = false;
bool run500ms = false;
bool run1000ms = false;
noInterrupts();
if (event10ms)
{
event10ms = false;
run10ms = true;
}
if (event100ms)
{
event100ms = false;
run100ms = true;
}
if (event500ms)
{
event500ms = false;
run500ms = true;
}
if (event1000ms)
{
event1000ms = false;
run1000ms = true;
}
interrupts();
if (run10ms) scanButton();
if (run100ms) readSensor();
if (run500ms) updateLed();
if (run1000ms) printSystemStatus();
}
void setupTimer1()
{
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 249;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS11);
TCCR1B |= (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
interrupts();
}
ISR(TIMER1_COMPA_vect)
{
counter10ms++;
counter100ms++;
counter500ms++;
counter1000ms++;
if (counter10ms >= 10)
{
counter10ms = 0;
event10ms = true;
}
if (counter100ms >= 100)
{
counter100ms = 0;
event100ms = true;
}
if (counter500ms >= 500)
{
counter500ms = 0;
event500ms = true;
}
if (counter1000ms >= 1000)
{
counter1000ms = 0;
event1000ms = true;
}
}
void scanButton()
{
int currentButtonState = digitalRead(BUTTON_PIN);
if (lastButtonState == HIGH && currentButtonState == LOW)
{
systemEnabled = !systemEnabled;
}
lastButtonState = currentButtonState;
}
void readSensor()
{
if (systemEnabled)
{
sensorValue = analogRead(SENSOR_PIN);
}
}
void updateLed()
{
if (systemEnabled)
{
ledState = !ledState;
}
else
{
ledState = false;
}
digitalWrite(LED_PIN, ledState);
}
void printSystemStatus()
{
Serial.print("Enabled: ");
Serial.print(systemEnabled ? "YES" : "NO");
Serial.print(" Sensor: ");
Serial.print(sensorValue);
Serial.print(" LED: ");
Serial.println(ledState ? "ON" : "OFF");
}
綜合程式架構
Timer1 每 1 ms 中斷
↓
累積不同週期
↓
設定事件旗標
↓
loop() 取得事件
↓
執行不同任務
任務分配如下:
此程式的按鍵問題
範例每 10 毫秒掃描一次按鍵,可降低部分彈跳影響,但仍不是完整的軟體去彈跳。
若要更可靠,應加入:
- 穩定狀態變數。
- 連續多次相同讀值。
- 30~50 毫秒確認時間。
- 狀態改變後才產生事件。
Timer 只負責提供固定掃描週期,不會自動消除按鍵彈跳。
Timer 除錯技巧
Timer 程式沒有正常工作時,可以依序檢查。
1. 確認 CPU 時脈
Arduino Uno 通常是:
16 MHz
若開發板時脈不同,計算結果也會不同。
2. 確認 Timer 位元數
Timer0、Timer2:
8 位元
比較值不可超過:
255
Timer1:
16 位元
比較值不可超過:
65535
3. 確認 Prescaler
重新計算:
比較值
=
CPU 時脈
÷ Prescaler
÷ 目標頻率
- 1
4. 確認 CTC 模式
Timer1 CTC 常見設定:
TCCR1B |= (1 << WGM12);
若模式錯誤,Timer 可能不會依預期清零。
5. 確認中斷已啟用
TIMSK1 |= (1 << OCIE1A);
6. 確認全域中斷已開啟
設定完成後需要:
interrupts();
7. 確認 ISR 名稱正確
Timer1 Compare Match A:
ISR(TIMER1_COMPA_vect)
{
}
名稱必須完全正確。
8. 移除 ISR 中的複雜工作
先將 ISR 縮減為:
ISR(TIMER1_COMPA_vect)
{
eventFlag = true;
}
再檢查主程式是否能收到事件。
9. 檢查函式庫衝突
確認是否同時使用:
- Servo。
- TimerOne。
- tone()。
- 馬達控制函式庫。
- 紅外線函式庫。
- 其他占用 Timer 的函式庫。
10. 檢查 PWM 腳位
如果修改 Timer1,D9、D10 的 analogWrite() 可能受到影響。
初學者常見錯誤
錯誤一:修改 Timer0
初學者直接重設 Timer0:
TCCR0A = 0;
TCCR0B = 0;
可能造成:
- millis() 失效。
- delay() 錯誤。
- micros() 不準。
除非非常清楚 Arduino 核心實作,否則應避免修改 Timer0。
錯誤二:比較值超出範圍
例如在 8 位元 Timer 設定:
OCR2A = 1000;
Timer2 最大只能容納 255。
錯誤三:忘記減一
錯誤計算:
OCR = CPU ÷ Prescaler ÷ Frequency
CTC 模式通常應使用:
OCR = CPU ÷ Prescaler ÷ Frequency - 1
因為 Timer 從 0 開始計數。
錯誤四:在 ISR 中使用 Serial
錯誤:
ISR(TIMER1_COMPA_vect)
{
Serial.println("Timer");
}
應設定旗標,再由 loop() 輸出。
錯誤五:ISR 執行時間過長
如果 Timer 每 1 毫秒中斷一次,但 ISR 執行超過 1 毫秒,系統就無法正常工作。
錯誤六:Timer 與 Servo 同時使用
Servo 函式庫通常占用 Timer1。
重新設定 Timer1 可能使 Servo 失效。
錯誤七:Timer 與 PWM 腳位衝突
修改 Timer1 後仍使用:
analogWrite(9, value);
可能無法得到標準 PWM。
錯誤八:事件旗標遺失多次事件
若事件頻率高於主程式處理速度,單一 bool 旗標可能合併多個事件。
需要時改用事件計數器。
錯誤九:長時間關閉中斷
noInterrupts();
使用時間過長會使 Timer 事件與其他中斷延遲。
錯誤十:把 Timer 當成真正的多工
Timer 只提供週期事件。
若某個任務執行時間過長,其他任務仍然會被延遲。
Timer 使用原則
在實際專案中,可遵循以下原則:
- 簡單時間控制優先使用 millis()。
- 需要固定週期或較高精度時,再使用硬體 Timer。
- 避免隨意修改 Timer0。
- 使用 Timer 前先確認 PWM 與函式庫衝突。
- ISR 越短越好。
- ISR 只設定旗標或增加計數。
- 不在 ISR 中使用 delay()、Serial 或複雜通訊。
- 中斷頻率不可高到占用大量 CPU。
- 比較值必須符合 Timer 位元範圍。
- 使用公式計算 Prescaler 與 OCR。
- 需要穩定波形時,優先考慮硬體 Output Compare。
- 主程式必須能及時處理 Timer 產生的任務。
本章實作練習
練習一:Timer 計算
已知:
CPU 時脈 = 16 MHz
Prescaler = 64
目標頻率 = 1000 Hz
計算 OCR1A。
答案應為:
249
練習二:500 毫秒 LED 切換
使用 Timer1 CTC 模式。
需求:
- 每 500 毫秒設定事件旗標。
- loop() 切換 LED。
- ISR 不可使用 digitalWrite()。
- 不可使用 delay()。
練習三:10 毫秒感測器取樣
使用 Timer1:
- 每 10 毫秒讀取一次 A0。
- 每 1 秒在 Serial Monitor 顯示平均值。
- ISR 只設定取樣事件。
練習四:多週期任務
建立:
- 10 ms 按鍵掃描。
- 100 ms 感測器讀取。
- 500 ms LED 更新。
- 1000 ms Serial 輸出。
練習五:事件計數器
將單一:
volatile bool eventFlag;
改為:
volatile byte pendingEvents;
確保主程式忙碌時,可以保留多個尚未處理的事件。
練習六:Timer 資源分析
說明下列功能可能使用哪一個 Timer:
- millis()
- Servo
- tone()
- D9 PWM
- D3 PWM
Challenge:精準資料取樣系統
建立一個使用 Timer1 的感測資料取樣系統。
功能需求
- 每 5 毫秒產生一次取樣事件。
- 讀取 A0 類比訊號。
- 累積 200 筆資料。
- 每累積 200 筆,計算: 最小值。最大值。平均值。
- 在 Serial Monitor 顯示結果。
- ISR 只增加事件計數。
- 不可在 ISR 中呼叫 analogRead()。
- 不可使用 delay()。
- 若主程式來不及處理,必須能偵測事件積壓。
建議變數
volatile byte pendingSamples = 0;
volatile bool sampleOverflow = false;
unsigned long sampleSum = 0;
int sampleMinimum = 1023;
int sampleMaximum = 0;
unsigned int sampleCount = 0;
建議函式
setupTimer1();
processPendingSample();
updateStatistics();
printStatistics();
resetStatistics();
參考輸出
Samples: 200
Minimum: 214
Maximum: 806
Average: 511
Overflow: NO
若事件處理不及:
Sample event overflow detected
本章重點整理
完成本章後,你應該已經了解:
- Timer 是微控制器內部的硬體計數器。
- Arduino Uno 使用 16 MHz 系統時脈。
- Prescaler 可降低 Timer 計數速度。
- Timer0 與 millis()、micros()、delay() 密切相關。
- Timer1 是 16 位元 Timer,適合精準週期控制。
- Timer2 常與 tone() 及部分 PWM 腳位有關。
- Overflow 是計數器超過最大值後回到 0。
- Compare Match 是計數值到達指定值時產生事件。
- CTC 模式會在 Compare Match 後清除計數器。
- Timer 中斷頻率可透過 CPU 時脈、Prescaler 與比較值計算。
- ISR 應保持短小,只設定旗標或增加計數器。
- 修改 Timer 可能影響 PWM、Servo、tone() 與 Arduino 時間函式。
- 硬體 Output Compare 可以在不使用 ISR 的情況下產生穩定波形。
- Timer 可以用來建立固定週期取樣與簡易任務排程。
下一章預告
第18章 Arduino Library(函式庫)安裝、使用與自製函式庫
你將學會:
- Arduino Library 是什麼。
- 函式庫如何封裝硬體控制與程式功能。
- 使用 Library Manager 安裝函式庫。
- 使用 ZIP 檔匯入函式庫。
- 分辨同名函式庫與相容性問題。
- 閱讀函式庫範例與 API。
- 理解 .h 與 .cpp 檔案。
- 建立自己的 Arduino 函式庫。
- 撰寫 library.properties。
- 將重複程式整理成可重複使用的模組。