第17章 Arduino Timer(計時器)與精準時間控制:Timer1、CTC、中斷與週期任務完整教學

發布日期: |更新日期:
文章主圖:第17章 Arduino Timer(計時器)與精準時間控制:Timer1、CTC、中斷與週期任務完整教學

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

知識卡:Timer
項目 說明
中文 硬體計時器/計數器
主要用途 計時、PWM、週期事件、脈衝產生
計數來源 系統時脈或外部訊號
常見事件 Overflow、Compare Match
Uno Timer 數量 3 個
Timer0 8 位元
Timer1 16 位元
Timer2 8 位元
相關功能 millis()、micros()、PWM、Servo、tone()

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:

Arduino Uno 的三個 Timer
Timer 位元數 常見用途
Timer0 8 位元 millis()、micros()、delay()、PWM
Timer1 16 位元 Servo、精準計時、PWM
Timer2 8 位元 tone()、PWM、獨立計時應用

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 腳位關係如下:

Timer 與 PWM 腳位對應
Timer PWM 腳位
Timer0 D5、D6
Timer1 D9、D10
Timer2 D3、D11

這也是為什麼不同 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 常見暫存器
暫存器 主要用途
TCCR1A Timer1 控制設定 A
TCCR1B Timer1 控制設定 B
TCNT1 Timer1 目前計數值
OCR1A Timer1 比較值 A
OCR1B Timer1 比較值 B
TIMSK1 Timer1 中斷啟用設定
TIFR1 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 常用於建立固定週期工作,例如:

Timer 與即時系統
週期 可能用途
1 ms 按鍵掃描、控制迴路
10 ms 感測器取樣、介面更新
20 ms Servo 控制週期
100 ms 系統狀態更新
500 ms 畫面更新
1000 ms 報告、統計、心跳訊號

大型程式可以建立一個系統節拍:

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 資源衝突表

Timer 資源衝突表
功能 常使用的 Timer
millis()、micros()、delay() Timer0
D5、D6 PWM Timer0
D9、D10 PWM Timer1
Servo 函式庫 Timer1
D3、D11 PWM Timer2
tone() Timer2

實際使用情況可能因開發板核心與函式庫實作而不同,但 Arduino Uno 通常可依此原則判斷。

Timer 設定前的檢查清單

在修改 Timer 前,先確認:

  1. 是否使用 millis()、micros() 或 delay()?
  2. 是否使用 Servo?
  3. 是否使用 tone()?
  4. 是否使用對應 Timer 的 PWM 腳位?
  5. 是否有其他函式庫使用該 Timer?
  6. 需要的中斷頻率是多少?
  7. ISR 執行時間是否足夠短?
  8. 是否真的需要直接操作 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 包括:

常見 Prescaler
CS12 CS11 CS10 Prescaler
0 0 1 1
0 1 0 8
0 1 1 64
1 0 0 256
1 0 1 1024

其他組合可能代表:

  • 停止 Timer。
  • 使用外部時脈輸入。

如何選擇 Prescaler?

Prescaler 越小:

  • Timer 計數越快。
  • 時間解析度越高。
  • 可直接計算的最長週期越短。

Prescaler 越大:

  • Timer 計數越慢。
  • 可計算較長週期。
  • 時間解析度較低。

選擇原則:

  1. 先計算目標比較值。
  2. 確認比較值沒有超過 Timer 最大值。
  3. 在可行範圍內,選擇較小的 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 都具有時間功能,但目的不同。

Timer 與 Watchdog 的差異
項目 Timer Watchdog
主要用途 計時、PWM、週期事件 系統異常時重新啟動
正常程式使用 經常 安全監控
是否可產生週期中斷 可以 部分模式可以
是否可重置系統 一般不會 可以
是否用於 PWM 可以 不可以

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 ms
感測器讀取 100 ms
LED 更新 500 ms
系統報告 1000 ms

此程式的按鍵問題

範例每 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 使用原則

在實際專案中,可遵循以下原則:

  1. 簡單時間控制優先使用 millis()。
  2. 需要固定週期或較高精度時,再使用硬體 Timer。
  3. 避免隨意修改 Timer0。
  4. 使用 Timer 前先確認 PWM 與函式庫衝突。
  5. ISR 越短越好。
  6. ISR 只設定旗標或增加計數。
  7. 不在 ISR 中使用 delay()、Serial 或複雜通訊。
  8. 中斷頻率不可高到占用大量 CPU。
  9. 比較值必須符合 Timer 位元範圍。
  10. 使用公式計算 Prescaler 與 OCR。
  11. 需要穩定波形時,優先考慮硬體 Output Compare。
  12. 主程式必須能及時處理 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。
  • 將重複程式整理成可重複使用的模組。
相關資源與文章匯出
下載 Arduino 入門基礎教材 下載 DOCX PDF/列印

← 回文章列表