第15章 Arduino Interrupt(中斷)與即時事件處理:attachInterrupt、ISR、volatile 與脈衝計數完整教學

發布日期: |更新日期:
文章主圖:第15章 Arduino Interrupt(中斷)與即時事件處理:attachInterrupt、ISR、volatile 與脈衝計數完整教學

本章承接第 14 章的非阻塞程式設計,進一步說明 Arduino 如何使用 Interrupt(中斷)即時回應按鍵、脈衝與快速事件。

內容以 Arduino Uno R3 為主要示範環境,涵蓋外部中斷腳位、attachInterrupt()、ISR、volatile、按鍵去彈跳、脈衝計數、轉速計算與共享變數的安全存取。

中斷適合處理短暫且不能漏掉的事件,但 ISR 必須保持簡短,不可在其中執行 delay()、大量 Serial 輸出或耗時工作。

本章學習目標

  • 了解 Interrupt(中斷)的基本概念。
  • 分辨輪詢(Polling)與中斷的差異。
  • 認識 Arduino Uno 的外部中斷腳位。
  • 正確使用 attachInterrupt() 與 detachInterrupt()。
  • 了解 ISR(Interrupt Service Routine,中斷服務常式)的設計原則。
  • 正確使用 volatile 關鍵字。
  • 使用中斷偵測按鍵、旋轉編碼器與脈衝訊號。
  • 避免在中斷程式中使用不適合的函式。
  • 理解按鍵彈跳與中斷誤觸發問題。

什麼是 Interrupt 中斷

Interrupt 中文稱為中斷。當 Arduino 正在執行主程式時,如果外部發生重要事件,中斷機制會暫停目前工作,立即執行指定的中斷服務常式,處理完成後再回到原本位置繼續執行。

常見中斷事件包括按鍵按下、感測器脈衝、旋轉編碼器轉動、流量計輸出、馬達轉速感測器脈衝與計時器事件。

  1. Arduino 正在執行主程式。
  2. 外部事件發生。
  3. 主程式暫時停止。
  4. 執行 ISR。
  5. 中斷處理完成。
  6. 返回主程式原本位置繼續執行。
知識卡:Interrupt
項目 說明
英文 Interrupt
中文 中斷
主要用途 即時處理外部事件
設定函式 attachInterrupt()
停用函式 detachInterrupt()
中斷處理函式 ISR
Uno 外部中斷腳位 D2、D3
常用觸發方式 RISING、FALLING、CHANGE、LOW

為什麼需要中斷

一般 Arduino 程式常在 loop() 中不斷讀取輸入腳位,這種方式稱為 Polling(輪詢)。程式簡單時,輪詢容易理解且容易除錯。

當主程式同時更新 LCD、讀取多個感測器、處理 Serial、控制馬達、執行長時間運算或使用 delay() 時,短暫脈衝可能在兩次檢查之間消失,中斷便能提高事件反應速度。

void loop()
{
  if (digitalRead(2) == LOW)
  {
    Serial.println("Button pressed");
  }
}

輪詢與中斷的差異

輪詢與中斷比較
比較項目 輪詢 Polling 中斷 Interrupt
事件來源 主程式主動檢查輸入狀態 硬體事件主動通知微控制器
反應速度 受 loop() 執行時間影響 事件發生時立即進入 ISR
適用情境 一般按鍵、慢速感測器、簡單選單 短脈衝、轉速、流量、即時事件
優點 流程容易理解、追蹤與除錯 反應快、不必持續檢查腳位
缺點 主程式忙碌時可能漏掉事件 設計較複雜,ISR 有許多限制

適合使用中斷的情況

  • 計算旋轉編碼器脈衝。
  • 計算馬達轉速。
  • 接收流量計或霍爾感測器脈衝。
  • 偵測短時間數位訊號。
  • 精準記錄事件發生次數。
  • 偵測緊急停止訊號。
  • 處理定時器事件。

不一定需要中斷的情況

  • 一般 LED 開關。
  • 緩慢變化的溫度感測器。
  • 每秒讀取一次的資料。
  • 簡單按鍵選單。
  • LCD 畫面切換。

中斷不是越多越好。只有在需要快速反應,或輪詢可能漏掉事件時,才應優先考慮使用。

Arduino Uno 的外部中斷腳位

Arduino Uno R3 外部中斷腳位
Arduino 腳位 外部中斷編號
D2 INT0
D3 INT1

不同 Arduino 開發板的外部中斷腳位可能不同,因此不建議直接在程式中寫死中斷編號。

建議使用 digitalPinToInterrupt(pin) 將 Arduino 腳位轉換成中斷編號,讓程式更容易移植。

attachInterrupt(
  digitalPinToInterrupt(2),
  handleInterrupt,
  FALLING
);

attachInterrupt() 基本語法

attachInterrupt(
  digitalPinToInterrupt(pin),
  ISR,
  mode
);
attachInterrupt() 參數
參數 說明
pin 外部中斷輸入腳位,建議透過 digitalPinToInterrupt(pin) 轉換
ISR 中斷發生時執行的函式
mode LOW、CHANGE、RISING 或 FALLING 觸發方式

中斷觸發模式

LOW 低電位觸發

當腳位維持 LOW 時觸發。只要訊號持續為低電位,就可能反覆進入 ISR,因此初學階段較少使用。

attachInterrupt(
  digitalPinToInterrupt(2),
  handleInterrupt,
  LOW
);

CHANGE 狀態改變觸發

當訊號由 LOW 變 HIGH 或由 HIGH 變 LOW 時觸發,適合旋轉編碼器、脈衝寬度量測及同時偵測上升緣與下降緣。

attachInterrupt(
  digitalPinToInterrupt(2),
  handleInterrupt,
  CHANGE
);

RISING 上升緣觸發

當訊號由 LOW 變成 HIGH 時觸發,適合偵測正向脈衝的上升緣。

attachInterrupt(
  digitalPinToInterrupt(2),
  handleInterrupt,
  RISING
);

FALLING 下降緣觸發

當訊號由 HIGH 變成 LOW 時觸發。按鍵搭配 INPUT_PULLUP 時,放開為 HIGH、按下為 LOW,因此按下瞬間通常使用 FALLING。

attachInterrupt(
  digitalPinToInterrupt(2),
  handleInterrupt,
  FALLING
);

第一個中斷實驗:按鍵計數器

實驗材料
元件 數量
Arduino Uno R3 1
按鍵 1
Breadboard 1
杜邦線 若干

使用 Arduino 內部上拉電阻,D2 經按鍵接到 GND,不需要額外外接上拉電阻。

const int BUTTON_PIN = 2;
volatile unsigned long pressCount = 0;

void setup()
{
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(BUTTON_PIN),
    handleButtonPress,
    FALLING
  );
}

void loop()
{
  Serial.print("Press count: ");
  Serial.println(pressCount);
  delay(500);
}

void handleButtonPress()
{
  pressCount++;
}

每當按鍵訊號由 HIGH 變成 LOW,ISR 就會把 pressCount 加一。此範例主要用來理解中斷流程,後續會改善共享變數讀取與按鍵彈跳問題。

什麼是 ISR

ISR 是 Interrupt Service Routine 的縮寫,中文稱為中斷服務常式。

當中斷發生時,Arduino 暫停 loop(),執行 ISR,完成後再回到主程式。ISR 的核心原則是越短越好。

ISR 適合執行的工作

  • 設定旗標。
  • 增加計數值。
  • 記錄簡單狀態。
  • 儲存事件發生時間。

ISR 不適合執行的工作

  • 複雜計算。
  • 長迴圈。
  • 更新 LCD。
  • 控制大量硬體。
  • 大量字串處理。
  • 長時間等待。

ISR 執行太久會暫停主程式並延遲其他中斷,可能造成程式反應遲鈍、資料遺失、計時不準、通訊異常或系統卡住。

ISR 中應避免的函式

不要在 ISR 中使用 delay()

void handleButtonPress()
{
  delay(100);
  pressCount++;
}

delay() 依賴計時中斷運作,而 ISR 執行期間其他中斷通常受到限制,因此 delay() 可能無法正常工作。ISR 內不應等待。

不要在 ISR 中大量使用 Serial

void handleButtonPress()
{
  Serial.println("Button pressed");
}

Serial.print() 可能依賴緩衝區與中斷傳送資料。在 ISR 中大量輸出可能造成緩衝區塞滿、程式停住、輸出不完整或其他中斷延遲。

volatile bool buttonEvent = false;

void handleButtonPress()
{
  buttonEvent = true;
}

void loop()
{
  if (buttonEvent)
  {
    buttonEvent = false;
    Serial.println("Button pressed");
  }
}

volatile 關鍵字

若變數會同時由 ISR 修改,並由主程式讀取或修改,就應宣告為 volatile。

volatile 告訴編譯器,該變數可能隨時被另一個執行流程修改,每次存取都必須以記憶體中的實際值為準。

volatile bool buttonEvent = false;
volatile unsigned long pulseCount = 0;

使用旗標處理中斷事件

較安全的架構是讓 ISR 只設定旗標,再由 loop() 執行 LED 控制、Serial 輸出與其他完整工作。

const int BUTTON_PIN = 2;
const int LED_PIN = 9;

volatile bool buttonEvent = false;
bool ledState = false;

void setup()
{
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(BUTTON_PIN),
    handleButtonPress,
    FALLING
  );
}

void loop()
{
  if (buttonEvent)
  {
    buttonEvent = false;
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);

    Serial.print("LED: ");
    Serial.println(ledState ? "ON" : "OFF");
  }
}

void handleButtonPress()
{
  buttonEvent = true;
}

按鍵彈跳與誤觸發

機械按鍵在接點接觸瞬間,訊號可能在 HIGH 與 LOW 之間快速反覆變化。使用者只按一次,Arduino 卻可能收到多次中斷,這種現象稱為 Contact Bounce(接點彈跳)。

按鍵彈跳可能使 pressCount 一次增加多次,因此必須加入軟體或硬體去彈跳。

在 ISR 中進行基本時間判斷

const int BUTTON_PIN = 2;

volatile unsigned long pressCount = 0;
volatile unsigned long lastInterruptTime = 0;

const unsigned long DEBOUNCE_TIME = 50;

void setup()
{
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(BUTTON_PIN),
    handleButtonPress,
    FALLING
  );
}

void loop()
{
  noInterrupts();
  unsigned long countCopy = pressCount;
  interrupts();

  Serial.print("Press count: ");
  Serial.println(countCopy);
  delay(500);
}

void handleButtonPress()
{
  unsigned long currentTime = millis();

  if (currentTime - lastInterruptTime >= DEBOUNCE_TIME)
  {
    pressCount++;
    lastInterruptTime = currentTime;
  }
}

millis() 可在 ISR 中讀取目前值,但 ISR 執行期間通常不會持續正常更新。可用於短時間事件判斷,不可在 ISR 內等待 millis() 改變。

較安全的旗標式去彈跳

const int BUTTON_PIN = 2;
const int LED_PIN = 9;

volatile bool interruptTriggered = false;

unsigned long lastAcceptedTime = 0;
const unsigned long DEBOUNCE_TIME = 50;
bool ledState = false;

void setup()
{
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(BUTTON_PIN),
    handleButtonInterrupt,
    FALLING
  );
}

void loop()
{
  if (interruptTriggered)
  {
    noInterrupts();
    interruptTriggered = false;
    interrupts();

    unsigned long currentTime = millis();

    if (currentTime - lastAcceptedTime >= DEBOUNCE_TIME)
    {
      lastAcceptedTime = currentTime;
      ledState = !ledState;
      digitalWrite(LED_PIN, ledState);

      Serial.print("LED: ");
      Serial.println(ledState ? "ON" : "OFF");
    }
  }
}

void handleButtonInterrupt()
{
  interruptTriggered = true;
}

這種設計讓 ISR 只設定旗標,時間判斷、LED 控制與 Serial 輸出都由 loop() 完成。

noInterrupts() 與 interrupts()

中斷控制函式
函式 用途
noInterrupts() 暫時停用全域中斷
interrupts() 重新啟用全域中斷

Arduino Uno 使用 8 位元微控制器。unsigned long 需要 4 Bytes,主程式讀取到一半時若 ISR 修改變數,可能得到混合且不完整的數值。

讀取多位元共享變數時,可短暫停用中斷,複製到一般變數後立即重新啟用。停用區段必須越短越好。

noInterrupts();
unsigned long countCopy = pulseCount;
interrupts();

不要在 noInterrupts() 與 interrupts() 之間執行 delay()、Serial 輸出、顯示更新或長時間運算,否則可能漏掉脈衝、影響通訊與時間計算。

detachInterrupt() 停用外部中斷

不再需要某個外部中斷時,可使用 detachInterrupt() 停用該腳位的中斷處理。

void stopButtonInterrupt()
{
  detachInterrupt(
    digitalPinToInterrupt(BUTTON_PIN)
  );
}

第二個實驗:脈衝計數器

流量感測器、霍爾感測器、馬達轉速感測器、光電編碼器、風速計與電能計量裝置常以脈衝代表事件。

const int PULSE_PIN = 2;
volatile unsigned long pulseCount = 0;

void setup()
{
  pinMode(PULSE_PIN, INPUT_PULLUP);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(PULSE_PIN),
    countPulse,
    FALLING
  );
}

void loop()
{
  noInterrupts();
  unsigned long countCopy = pulseCount;
  interrupts();

  Serial.print("Pulse count: ");
  Serial.println(countCopy);
  delay(1000);
}

void countPulse()
{
  pulseCount++;
}

每收到一個下降緣脈衝,pulseCount 就增加一次。主程式以短暫原子區段複製計數值,避免讀取不完整資料。

第三個實驗:測量每秒脈衝數

const int PULSE_PIN = 2;

volatile unsigned long pulseCount = 0;

unsigned long previousMillis = 0;
const unsigned long REPORT_INTERVAL = 1000;

void setup()
{
  pinMode(PULSE_PIN, INPUT_PULLUP);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(PULSE_PIN),
    countPulse,
    FALLING
  );
}

void loop()
{
  unsigned long currentMillis = millis();
  unsigned long elapsedTime =
    currentMillis - previousMillis;

  if (elapsedTime >= REPORT_INTERVAL)
  {
    previousMillis = currentMillis;

    noInterrupts();
    unsigned long pulseCopy = pulseCount;
    pulseCount = 0;
    interrupts();

    float pulsesPerSecond =
      pulseCopy * 1000.0 / elapsedTime;

    Serial.print("Pulses per second: ");
    Serial.println(pulsesPerSecond, 2);
  }
}

void countPulse()
{
  pulseCount++;
}
  1. 記錄本次報告與上次報告之間的實際經過時間。
  2. 安全複製 pulseCount,並將共享計數值歸零。
  3. 使用實際經過毫秒數換算每秒脈衝數,避免主程式延遲造成誤差。

從脈衝計算轉速

感測器每轉一圈輸出一個脈衝時,RPM 等於每秒脈衝數乘以 60。pulsesPerSecond 應使用實際經過時間換算,而不是假設每次量測都剛好等於一秒。

float rpm = pulsesPerSecond * 60.0;

若每轉一圈產生多個脈衝,則必須再除以每圈脈衝數。

const int PULSES_PER_REVOLUTION = 20;

float rpm =
  pulsesPerSecond * 60.0 /
  PULSES_PER_REVOLUTION;

第四個實驗:中斷控制 LED 狀態

const int BUTTON_PIN = 2;
const int LED_PIN = 9;

volatile bool buttonEvent = false;
bool ledState = false;

void setup()
{
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(BUTTON_PIN),
    handleButtonInterrupt,
    FALLING
  );
}

void loop()
{
  if (buttonEvent)
  {
    noInterrupts();
    buttonEvent = false;
    interrupts();

    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);

    Serial.print("LED state: ");
    Serial.println(ledState ? "ON" : "OFF");
  }
}

void handleButtonInterrupt()
{
  buttonEvent = true;
}
  1. 中斷發生。
  2. ISR 設定事件旗標。
  3. loop() 安全取得並清除旗標。
  4. 主程式執行 LED 控制與 Serial 輸出。

中斷與旋轉編碼器

旋轉編碼器常輸出 A、B 兩組方波,透過兩組訊號的先後順序,可判斷順時針、逆時針與旋轉步數。簡化接線為 CLK 接 D2、DT 接 D4、GND 接 GND。

const int ENCODER_CLK = 2;
const int ENCODER_DT = 4;

volatile long encoderPosition = 0;

void setup()
{
  pinMode(ENCODER_CLK, INPUT_PULLUP);
  pinMode(ENCODER_DT, INPUT_PULLUP);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(ENCODER_CLK),
    handleEncoder,
    FALLING
  );
}

void loop()
{
  noInterrupts();
  long positionCopy = encoderPosition;
  interrupts();

  Serial.print("Position: ");
  Serial.println(positionCopy);
  delay(100);
}

void handleEncoder()
{
  if (digitalRead(ENCODER_DT) == HIGH)
  {
    encoderPosition++;
  }
  else
  {
    encoderPosition--;
  }
}

此為入門版範例。實際旋轉編碼器可能有接點彈跳、多次邊緣、方向誤判與快速旋轉漏計數,進階專案可採完整狀態解碼或專用函式庫。

外部中斷與 Pin Change Interrupt

Arduino Uno 除了 D2、D3 的標準外部中斷外,ATmega328P 還支援 Pin Change Interrupt(腳位變化中斷),可讓更多數位腳位偵測狀態改變。

Pin Change Interrupt 設定較複雜,通常需要操作暫存器或使用函式庫,而且多個腳位可能共用同一個 ISR,程式必須自行判斷是哪個腳位發生變化。初學階段建議先熟悉 D2 與 D3。

中斷優先順序與嵌套

微控制器可能同時具有外部中斷、Timer、UART、ADC 與 SPI 等中斷來源。一般 Arduino 程式不需要手動設定優先順序,但 ISR 仍應保持簡短,避免長時間阻擋其他中斷。

Arduino Uno 一般在執行 ISR 時不會立即進入其他 ISR。初學者不應在 ISR 內呼叫 interrupts() 允許中斷嵌套,否則會大幅增加程式複雜度與風險。

中斷與 millis()、micros()

  • millis() 與 micros() 本身依賴 Timer 中斷更新。
  • ISR 執行期間,millis() 不會持續正常增加。
  • delay() 在 ISR 中無法正常工作。
  • micros() 在很短時間內可能仍可提供讀值,但不應用於長時間等待。
  • ISR 應只記錄事件,不在其中等待時間經過。

共享變數與不可分割存取

若 ISR 修改 volatile unsigned long pulseCount,而 loop() 同時讀取,主程式可能先讀取部分 Byte,接著 ISR 修改變數,再讀取剩餘 Byte,最後得到混合結果。

使用 noInterrupts() 與 interrupts() 保護必要的讀寫區段,可形成 Atomic Access(不可分割存取)。

noInterrupts();
unsigned long countCopy = pulseCount;
interrupts();

bool 或 byte 是否需要停用中斷

在 Arduino Uno 上,單一 byte 或 bool 通常可以一次完成讀寫,因此較不容易讀到一半。

若操作包含讀取後立刻清除,例如讀取 eventFlag 後設為 false,仍可能在兩個動作之間再次發生中斷。要求嚴謹時,可把複合操作放在短暫原子區段內。

noInterrupts();
bool eventCopy = eventFlag;
eventFlag = false;
interrupts();

推薦的中斷程式架構

  1. ISR 只記錄事件,例如設定旗標或增加計數。
  2. 主程式使用短暫原子區段取得共享資料。
  3. 主程式在中斷區段外完成顯示、通訊、運算與硬體控制。
void handleInterrupt()
{
  eventFlag = true;
}

void loop()
{
  noInterrupts();
  bool eventCopy = eventFlag;
  eventFlag = false;
  interrupts();

  if (eventCopy)
  {
    updateLed();
    updateDisplay();
    sendSerialReport();
  }
}

綜合實驗:即時脈衝監測系統

  • 使用 D2 接收脈衝。
  • 使用中斷計算脈衝數。
  • 每秒計算一次脈衝頻率。
  • 在 Serial Monitor 顯示頻率。
  • 頻率超過門檻時點亮 D9 LED。
  • 全程不使用 delay()。
綜合實驗接線
裝置 接線
脈衝訊號輸出 D2
脈衝裝置 GND Arduino GND
警示 LED D9 → 220Ω 電阻 → LED → GND
const int PULSE_PIN = 2;
const int LED_PIN = 9;

volatile unsigned long pulseCount = 0;

unsigned long previousMillis = 0;
const unsigned long SAMPLE_INTERVAL = 1000;
const float ALERT_THRESHOLD = 20.0;

void setup()
{
  pinMode(PULSE_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);

  attachInterrupt(
    digitalPinToInterrupt(PULSE_PIN),
    countPulse,
    FALLING
  );
}

void loop()
{
  unsigned long currentMillis = millis();
  unsigned long elapsedTime =
    currentMillis - previousMillis;

  if (elapsedTime >= SAMPLE_INTERVAL)
  {
    previousMillis = currentMillis;

    noInterrupts();
    unsigned long pulseCopy = pulseCount;
    pulseCount = 0;
    interrupts();

    float frequency =
      pulseCopy * 1000.0 / elapsedTime;

    bool alertState =
      frequency > ALERT_THRESHOLD;

    digitalWrite(
      LED_PIN,
      alertState ? HIGH : LOW
    );

    Serial.print("Frequency: ");
    Serial.print(frequency, 2);
    Serial.print(" Hz");
    Serial.print(" Alert: ");
    Serial.println(
      alertState ? "ON" : "OFF"
    );
  }
}

void countPulse()
{
  pulseCount++;
}

預期輸出

Frequency: 8 Hz Alert: OFF
Frequency: 16 Hz Alert: OFF
Frequency: 25 Hz Alert: ON
Frequency: 31 Hz Alert: ON

常見錯誤與故障排除

Arduino 中斷常見錯誤
問題 原因或改善方式
ISR 中使用 delay() ISR 不應等待,將延遲或計時工作移到 loop()。
ISR 中大量輸出 Serial 改為 ISR 設定旗標,再由 loop() 輸出。
共享變數未使用 volatile 被 ISR 修改的共享變數應宣告為 volatile。
ISR 寫得太長 不要在 ISR 更新 LCD、讀 I²C、寫 EEPROM、控制 Servo、處理 String 或執行長迴圈。
按鍵一次觸發多次 加入軟體去彈跳、RC 去彈跳、Schmitt Trigger 或專用去彈跳電路。
把所有腳位當成外部中斷腳位 Arduino Uno 標準外部中斷主要為 D2 與 D3,其他腳位需使用 Pin Change Interrupt。
長時間停用中斷 只在共享變數複製或更新時短暫停用。
共享變數使用 String ISR 應使用 bool、byte、整數等簡單固定大小型別。

中斷除錯檢查順序

  1. 確認 Arduino Uno 使用 D2 或 D3。
  2. 確認 pinMode() 與實際接線一致。
  3. 按鍵接地並使用 INPUT_PULLUP 時,通常選擇 FALLING。
  4. 確認 attachInterrupt() 指定的 ISR 名稱與函式定義完全一致。
  5. 確認 ISR 修改的共享變數已使用 volatile。
  6. 先把 ISR 縮減為只設定 eventFlag,確認系統是否恢復正常。
  7. 若計數增加過快,檢查按鍵彈跳。
  8. 可在 loop() 收到事件旗標後切換 LED,確認中斷是否發生。

中斷使用原則

  1. 能以簡單輪詢完成,就不一定需要中斷。
  2. 可能漏掉短脈衝時,再考慮使用中斷。
  3. ISR 越短越好。
  4. ISR 只記錄事件,不執行完整業務邏輯。
  5. 共享變數使用 volatile。
  6. 多位元共享變數使用原子方式存取。
  7. 不在 ISR 中使用 delay()。
  8. 避免在 ISR 中使用 Serial、I²C 與複雜函式庫。
  9. 機械按鍵必須考慮接點彈跳。
  10. 不要長時間停用所有中斷。

本章實作練習

練習一:按鍵事件旗標

  • D2 連接按鍵並使用 INPUT_PULLUP。
  • 使用 FALLING 觸發中斷。
  • ISR 只設定 buttonEvent。
  • loop() 收到事件後輸出 Button event received。

練習二:中斷計數器

  • 每按一次按鍵,計數值加一。
  • Serial Monitor 依序顯示 Count: 1、Count: 2、Count: 3。
  • 加入 50 毫秒軟體去彈跳。

練習三:不阻塞脈衝計數器

  • 使用中斷接收脈衝。
  • 每秒顯示一次脈衝數。
  • 不可使用 delay()。
  • 每次顯示後將脈衝計數歸零。

練習四:轉速計

假設感測器每圈輸出兩個脈衝,使用公式 RPM = 每秒脈衝數 × 60 ÷ 2 計算轉速。

練習五:緊急停止事件

  • 緊急停止按鍵接 D2。
  • 按下後 ISR 立即設定 emergencyStop。
  • loop() 停止 PWM 輸出。
  • Serial Monitor 顯示 Emergency stop activated。
  • ISR 不可直接輸出 Serial。

Challenge:即時轉速監測器

  • 使用中斷接收轉速感測器脈衝。
  • 每轉一圈輸出 20 個脈衝。
  • 每秒計算一次 RPM。
  • Serial Monitor 顯示脈衝數與 RPM。
  • RPM 超過 3000 時點亮警示 LED。
  • 不使用 delay()。
  • ISR 只增加脈衝計數。
  • 使用 calculateRpm()、updateAlertLed() 與 printSystemStatus() 整理程式。

參考輸出

Pulses: 800 RPM: 2400 Alert: OFF
Pulses: 1050 RPM: 3150 Alert: ON

本章重點整理

  • Interrupt 可讓 Arduino 即時處理外部事件。
  • 輪詢由主程式主動檢查,中斷則由硬體事件主動通知。
  • Arduino Uno 的標準外部中斷主要位於 D2 與 D3。
  • attachInterrupt() 用於建立中斷事件。
  • RISING、FALLING、CHANGE 與 LOW 代表不同觸發條件。
  • ISR 應保持簡短,只設定旗標或增加計數。
  • ISR 與主程式共享的變數應使用 volatile。
  • 多位元共享變數應使用短暫原子存取。
  • ISR 中不應使用 delay() 或大量 Serial.print()。
  • 機械按鍵需要處理接點彈跳。
  • 中斷適合脈衝計數、轉速量測與即時事件偵測。

下一章預告:Arduino EEPROM 非揮發性記憶體

  • EEPROM 是什麼。
  • SRAM、Flash 與 EEPROM 的差異。
  • Arduino 重新開機後變數內容消失的原因。
  • 使用 EEPROM.read() 與 EEPROM.write()。
  • 使用 EEPROM.update() 延長 EEPROM 壽命。
  • 儲存設定值、校正值與使用者選項。
  • 避免過度寫入造成 EEPROM 損耗。
  • 建立可保留設定的 Arduino 專案。
相關資源與文章匯出
下載 Arduino 入門基礎教材 下載 DOCX PDF/列印

← 回文章列表