關閉

第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?

在前面的章節中,我們已經使用過:

delay();
millis();
micros();

這些函式可以協助 Arduino 處理時間。

例如:

delay(1000);

表示等待 1 秒。

或者:

if(millis() - previousMillis >= 1000)
{
previousMillis = millis();

digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

表示每隔 1 秒切換一次 LED。

但是,當專案需要更精準或更穩定的時間控制時,只依靠主程式檢查時間可能不夠。

例如:

這些功能通常需要使用微控制器內部的:

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 的設定,可能造成:

初學者通常不應隨意修改 Timer0。

Timer1

Timer1 是 Arduino Uno 唯一的 16 位元 Timer。

它具備較大的計數範圍,因此非常適合:

Timer1 通常與 PWM 腳位:

D9

D10

有關。

Arduino Servo 函式庫在 Uno 上通常也會使用 Timer1。

因此,如果程式已使用:

#include <Servo.h>

再修改 Timer1 設定,可能造成 Servo 功能異常。

Timer2

Timer2 是另一個 8 位元 Timer。

通常與 PWM 腳位:

D3

D11

有關。

Arduino 的:

tone();

功能通常會使用 Timer2。

因此,若修改 Timer2,可能影響:

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(溢位)

當 Timer 計數到最大值後,再增加一次,就會回到 0。

例如 8 位元 Timer:

254

255

0

1

從 255 回到 0 的事件稱為:

Overflow(溢位)。

Timer 可以在溢位時產生中斷。

Overflow 時間計算

假設:

每次計數需要:

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。

此時可以:

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 相關暫存器可以設定:

Arduino 一般函式會幫我們隱藏許多暫存器細節。

但是,如果要直接控制 Timer,就需要操作 ATmega328P 的暫存器。

Timer1 常見暫存器

初學者不必一次背下所有位元,只要先理解每個暫存器負責的功能。

第一個 Timer1 實驗:每秒切換 LED

實驗目標

使用 Timer1 CTC 模式:

這個設計會延續第 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;

用途:

設定比較值

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 毫秒執行一次。

它只做:

不在 ISR 中執行:

為什麼不直接在 ISR 切換 LED?

下面寫法雖然通常可以運作:

ISR(TIMER1_COMPA_vect)
{
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

但不建議將所有功能都放入 ISR。

更好的方式是:

ISR 設定事件

loop() 處理事件

這樣未來若要加入:

會更加安全且容易維護。

第二個實驗:每 10 毫秒讀取一次感測器

實驗目標

使用 Timer1:

程式

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,建立:

程式

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() 依序執行。

因此,每個任務仍應:

Timer 中斷頻率不能無限提高

假設 Timer 每:

1 微秒

中斷一次。

也就是:

1,000,000 次/秒

微控制器幾乎所有時間都會用來:

主程式將沒有足夠時間執行。

因此,中斷頻率越高:

設計 Timer 時必須考慮:

ISR 執行時間必須遠小於中斷週期。

ISR 使用率概念

假設:

那麼 CPU 用於 ISR 的比例約為:

20 ÷ 100 = 20%

如果 ISR 執行 80 微秒:

80 ÷ 100 = 80%

主程式只剩約 20% 時間。

如果 ISR 執行時間超過中斷週期,系統就可能無法正常處理事件。

Timer 中斷的設計原則

Timer ISR 建議只做:

不建議在 Timer ISR 中:

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

兩者可能互相覆蓋設定。

結果可能包括:

因此,在使用 Timer 前必須先確認:

是否已有其他函式庫占用同一個 Timer。

Timer 與 tone()

Arduino Uno 的 tone() 通常會使用 Timer2。

例如:

tone(8, 1000);

如果同時修改 Timer2:

TCCR2A
TCCR2B
OCR2A

則 tone() 可能無法正常工作。

Timer 資源衝突表

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

Timer 設定前的檢查清單

在修改 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。

只有理解 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?

Prescaler 越小:

Prescaler 越大:

選擇原則:

Timer 誤差來源

Timer 計算結果不一定完全等於真實時間。

誤差來源包括:

因此,Arduino Timer 適合一般嵌入式控制,但若需要極高精度時間,可能需要:

中斷延遲與抖動

即使 Timer 在固定時間產生中斷,ISR 實際開始執行的時間仍可能稍微延後。

原因可能是:

這種時間上的微小變化稱為:

Jitter(抖動)。

對一般 LED、按鍵掃描與低速感測器而言,少量 Jitter 通常沒有問題。

但對:

則需要仔細評估。

不要長時間使用 noInterrupts()

當程式執行:

noInterrupts();

Timer 中斷也會被暫停。

若關閉中斷時間過長,可能造成:

因此:

noInterrupts();

只應保護非常短的共享資料操作。

使用硬體輸出比較產生方波

Timer 不一定需要進入 ISR 才能控制輸出。

某些 Timer 可以在 Compare Match 時,由硬體自動:

這稱為:

Output Compare。

優點是:

這類設定通常需要操作:

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 毫秒系統節拍,並執行:

完整程式

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 毫秒掃描一次按鍵,可降低部分彈跳影響,但仍不是完整的軟體去彈跳。

若要更可靠,應加入:

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. 檢查函式庫衝突

確認是否同時使用:

10. 檢查 PWM 腳位

如果修改 Timer1,D9、D10 的 analogWrite() 可能受到影響。

初學者常見錯誤

錯誤一:修改 Timer0

初學者直接重設 Timer0:

TCCR0A = 0;
TCCR0B = 0;

可能造成:

除非非常清楚 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 使用原則

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

本章實作練習

練習一:Timer 計算

已知:

CPU 時脈 = 16 MHz

Prescaler = 64

目標頻率 = 1000 Hz

計算 OCR1A。

答案應為:

249

練習二:500 毫秒 LED 切換

使用 Timer1 CTC 模式。

需求:

練習三:10 毫秒感測器取樣

使用 Timer1:

練習四:多週期任務

建立:

練習五:事件計數器

將單一:

volatile bool eventFlag;

改為:

volatile byte pendingEvents;

確保主程式忙碌時,可以保留多個尚未處理的事件。

練習六:Timer 資源分析

說明下列功能可能使用哪一個 Timer:

Challenge:精準資料取樣系統

建立一個使用 Timer1 的感測資料取樣系統。

功能需求

建議變數

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

本章重點整理

完成本章後,你應該已經了解:

下一章預告

第18章 Arduino Library(函式庫)安裝、使用與自製函式庫

你將學會: