《ESP32 入門》ESP32 輕觸按鈕(Tactile Switch)教學

帶領初學者以 ESP32 讀取輕觸開關狀態。內容涵蓋硬體接線原理、內部上拉電阻設定,並實作以按鍵控制 LED 亮滅的程式,幫助新手輕鬆掌握基礎的輸入控制。

《ESP32 入門》ESP32 輕觸按鈕(Tactile Switch)教學

輕觸按鈕(Tactile Switch)是電子專題中最常見的輸入元件之一,不管是控制 LED 開關、切換模式,還是觸發特定功能,都少不了它。這篇教學會帶你從零開始,了解輕觸按鈕的原理,並搭配 ESP32 NodeMCU-32S 完成從基礎讀取到進階應用的完整實作。


認識輕觸按鈕

外觀與結構

輕觸按鈕通常是一個 6mm × 6mm 或 12mm × 12mm 的正方形元件,底部有 4 支金屬接腳。按鈕頂部有一個圓形的按壓帽,按下時內部的金屬片會接觸導通,放開後彈簧會將金屬片彈回,恢復斷路狀態。

接腳說明

4 支腳其實只分成 2 組,同一側的 2 支腳是直接相連的:

  腳1 ──┐     ┌── 腳2
        │按鈕│
  腳3 ──┘     └── 腳4
  • 腳1 和 腳3 永遠導通(同一側)
  • 腳2 和 腳4 永遠導通(同一側)
  • 按下按鈕時,腳1/腳3 才會和 腳2/腳4 導通

實際接線時只需要使用對角的 2 支腳即可(例如腳1 和 腳4)。

工作特性

項目 規格
工作電壓 12V DC(最大)
額定電流 50mA
接觸電阻 ≤ 100mΩ
使用壽命 約 10 萬次
按壓力道 約 160~260gf

為什麼按鈕需要上拉或下拉電阻?

在開始接線之前,先搞懂一個初學者最常卡關的觀念:浮接(Floating)問題

浮接狀態 — 不接電阻會怎樣?

假設我們把按鈕一端接 3.3V、另一端接 GPIO,中間不加任何電阻:

         ┌─── 3.3V
         │
       [按鈕]
         │
  GPIO ──┘
  • 按鈕按下時:GPIO 被拉到 3.3V → 讀到 HIGH ✔ 沒問題
  • 按鈕沒按下時:GPIO 哪裡都沒接 → 浮接狀態 ✘ 大問題!

浮接時 GPIO 就像一根天線,會受到周圍電磁干擾的影響,讀值會在 HIGH 和 LOW 之間隨機跳動。序列埠的輸出可能長這樣:

1 0 1 1 0 0 1 0 1 1 1 0 0 1 0 ...  ← 完全不可預測!

這就是為什麼按鈕電路一定要搭配上拉電阻(Pull-up)或下拉電阻(Pull-down),在按鈕未按下時給 GPIO 一個明確的電位。

下拉電阻(Pull-down)— 預設 LOW

下拉電阻把 GPIO 「拉」向 GND,讓按鈕未按下時維持在 LOW:

         ┌─── 3.3V
         │
       [按鈕]
         │
  GPIO ──┤
         │
       [10KΩ]  ← 下拉電阻
         │
         └─── GND

工作原理:

  • 按鈕未按下時: GPIO 透過 10KΩ 電阻連接到 GND。沒有電流流過(因為上方斷路),GPIO 電壓 = 0V → 讀到 LOW
  • 按鈕按下時: GPIO 同時連到 3.3V(經過按鈕)和 GND(經過 10KΩ)。因為按鈕的電阻趨近 0Ω,遠小於 10KΩ,根據分壓原理,GPIO 電壓 ≈ 3.3V → 讀到 HIGH
按鈕未按下:                    按鈕按下:
3.3V ─ ✘(斷路)─ GPIO          3.3V ─ [按鈕≈0Ω] ─ GPIO
                    │                                │
                  [10KΩ]                           [10KΩ]
                    │                                │
                   GND                              GND

GPIO = 0V (LOW) ✔              GPIO ≈ 3.3V (HIGH) ✔

邏輯很直覺:按下 = HIGH,沒按 = LOW

上拉電阻(Pull-up)— 預設 HIGH

上拉電阻把 GPIO 「拉」向 3.3V,讓按鈕未按下時維持在 HIGH:

         ┌─── 3.3V
         │
       [10KΩ]  ← 上拉電阻
         │
  GPIO ──┤
         │
       [按鈕]
         │
         └─── GND

工作原理:

  • 按鈕未按下時: GPIO 透過 10KΩ 電阻連接到 3.3V。沒有電流流過(因為下方斷路),GPIO 電壓 = 3.3V → 讀到 HIGH
  • 按鈕按下時: GPIO 同時連到 3.3V(經過 10KΩ)和 GND(經過按鈕)。按鈕電阻趨近 0Ω,遠小於 10KΩ,GPIO 電壓 ≈ 0V → 讀到 LOW
按鈕未按下:                    按鈕按下:
3.3V                            3.3V
 │                               │
[10KΩ]                         [10KΩ]
 │                               │
GPIO                            GPIO
 │                               │
 ✘(斷路)                     [按鈕≈0Ω]
                                 │
                                GND

GPIO = 3.3V (HIGH) ✔           GPIO ≈ 0V (LOW) ✔

邏輯和下拉相反:按下 = LOW,沒按 = HIGH。初學者可能覺得「按下卻是 LOW」有點反直覺,但習慣就好,而且上拉電阻在實務上使用更廣泛。

為什麼用 10KΩ?

電阻值的選擇是一個權衡:

  • 太小(例如 1KΩ):按鈕按下時從 3.3V 到 GND 的電流較大(3.3mA),浪費電力,對電池供電的專案不友好
  • 太大(例如 1MΩ):拉力太弱,容易受到電磁干擾,訊號不穩定
  • 10KΩ 是業界慣用值:按下時電流僅 0.33mA(3.3V ÷ 10KΩ),既省電又穩定

ESP32 內建上拉/下拉電阻

好消息是 ESP32 內部已經幫每個 GPIO 都準備了上拉和下拉電阻(約 45KΩ),只要在程式裡設定就能啟用,不需要外接電阻:

pinMode(pin, INPUT_PULLUP);    // 啟用內建上拉電阻(約 45KΩ)
pinMode(pin, INPUT_PULLDOWN);  // 啟用內建下拉電阻(約 45KΩ)
pinMode(pin, INPUT);           // 不啟用 → 浮接,需自行外接電阻
模式 預設電位 按下讀值 適用場景
INPUT_PULLUP HIGH LOW 最常用,接線最簡單(按鈕另一端接 GND)
INPUT_PULLDOWN LOW HIGH 邏輯較直覺,按鈕另一端接 3.3V
INPUT + 外接電阻 視電阻而定 視電阻而定 需要精確控制電阻值,或使用僅輸入腳位時
注意: GPIO 34、35、36、39 是僅輸入腳位(Input Only),沒有內建上拉/下拉電阻,使用這些腳位時必須外接電阻。

上拉 vs 下拉,該選哪個?

比較項目 上拉電阻(Pull-up) 下拉電阻(Pull-down)
預設狀態 HIGH LOW
按下讀值 LOW HIGH
接線方式 按鈕接 GND(簡單) 按鈕接 3.3V
ESP32 內建 INPUT_PULLUP INPUT_PULLDOWN
業界慣例 ★ 最常見,Arduino 預設 較少用
抗干擾能力 較好(HIGH 為預設較不易被干擾拉低) 略差

結論:大多數情況建議使用上拉電阻(INPUT_PULLUP),原因如下:

  1. 接線最簡單 — 按鈕一端接 GPIO、另一端接 GND,只需兩條線
  2. 業界標準 — Arduino、ESP32 社群的範例幾乎都用上拉
  3. ESP32 內建支援 — 不需要額外零件,INPUT_PULLUP 一行搞定
  4. 抗干擾較好 — 高電位做為預設狀態在實務上比較穩定

硬體準備

所需材料

  • ESP32 NodeMCU-32S 開發板 × 1
  • 12×12mm 輕觸按鈕 × 1
  • 10KΩ 電阻 × 1(外部下拉用,選用)
  • LED × 1(選用,用於輸出測試)
  • 220Ω 電阻 × 1(選用,限流用)
  • 麵包板 × 1
  • 杜邦線若干
NodeMCU-32S 相容版本 ESP32開發板 WiFi 藍牙 可用Arduino IDE
全腳位引出,還保持迷你的身型,插上麵包後還能插杜邦線,真的是太棒了! 和NodeMCU V2幾乎一樣尺寸! 有5V供電輸出,非常方便! 有了ESP32開發板,真的可以忘記原來的那些Arduino板子了! 可以用Arduino IDE開發,但效能更強大,還內建WiFi 傑森實測記錄,大家可以到 F 粉 絲 團 B 看貼文哦! ESP32-D0WDQ6 內置兩個低功耗 Xtensa® 32-bit LX6 MCU。片上存儲包括: • 448 KB 的 ROM,用於程序啟動和內核功能
12*12*5mm 輕觸開關 4腳 微動按鈕 按鍵 4個一組
尺寸:12*12*5mm 4個為1組,1組10元。 非常適合插在麵包板上,比8*8的還容易插
400孔 麵包板 紅藍線 85x55mm Arduino 迷你麵包板 小麵包板
高品質麵包板 400孔,紅藍線 多次拔插也不會鬆動

接線方式一:使用內建上拉電阻(推薦)

這是最簡單的接法,不需要額外電阻,只要兩條線:

ESP32 NodeMCU-32S          輕觸按鈕
─────────────────          ────────
GPIO 4  ──────────────────  腳1(一端)
GND     ──────────────────  腳4(對角另一端)

程式裡設定 INPUT_PULLUP,ESP32 內部的 45KΩ 上拉電阻就會幫你把 GPIO 拉到 HIGH:

  • 按鈕未按下 → GPIO 讀到 HIGH(1)
  • 按鈕按下時 → GPIO 透過按鈕接到 GND → 讀到 LOW(0)

接線方式二:外接下拉電阻

如果偏好「按下 = HIGH」的直覺邏輯,可以外接下拉電阻:

3.3V ── 按鈕 ── GPIO 4 ── 10KΩ電阻 ── GND

程式裡設定 INPUT(不啟用內建電阻),由外部 10KΩ 負責下拉:

  • 按鈕未按下 → GPIO 被 10KΩ 拉到 GND → 讀到 LOW(0)
  • 按鈕按下時 → GPIO 被按鈕拉到 3.3V → 讀到 HIGH(1)

接線方式三:使用內建下拉電阻

ESP32 獨有的優勢(Arduino UNO 沒有內建下拉),不需要外接電阻:

ESP32 NodeMCU-32S          輕觸按鈕
─────────────────          ────────
GPIO 4  ──────────────────  腳1(一端)
3.3V    ──────────────────  腳4(對角另一端)

程式裡設定 INPUT_PULLDOWN

  • 按鈕未按下 → GPIO 被內建電阻拉到 GND → 讀到 LOW(0)
  • 按鈕按下時 → GPIO 被按鈕拉到 3.3V → 讀到 HIGH(1)
注意: ESP32 的 GPIO 工作電壓為 3.3V,請勿直接接 5V 訊號。

範例一:基礎按鈕讀取

我們選擇用最簡單接線,也就是不接電阻,使用內建上拉電阻,按下按鈕時在序列埠顯示訊息。

// 基礎按鈕讀取
// 使用 ESP32 內建上拉電阻

const int buttonPin = 4;  // 按鈕接在 GPIO 4

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);  // 啟用內建上拉電阻
  Serial.println("按鈕讀取測試 - 準備就緒");
}

void loop() {
  int buttonState = digitalRead(buttonPin);

  if (buttonState == LOW) {  // INPUT_PULLUP 時,按下為 LOW
    Serial.println("按鈕被按下了!");
  }

  delay(100);  // 簡單延遲避免過度輸出
}

上傳程式後打開序列埠監控視窗(鮑率 115200),按下按鈕就能看到訊息。

但是你也會發現,雖然只是輕輕按一下,卻跳出一大串的觸發提示,下一節就要教大家解決這個問題!


範例二:軟體消彈跳(Debounce)

機械按鈕在按下和放開的瞬間,金屬接點會產生多次快速的通斷,稱為「彈跳」(Bounce)。如果不處理,一次按壓可能會被判讀為多次觸發。

什麼是彈跳?

按鈕按下的瞬間,訊號可能長這樣:

HIGH ─────┐ ┌┐ ┌┐ ┌──────────────────── HIGH
          └─┘└─┘└─┘
LOW                                      LOW
          ← 彈跳區間 →
          (約 5~20ms)

消彈跳程式

// 軟體消彈跳(Debounce)範例

const int buttonPin = 4;

int buttonState = HIGH;          // 目前按鈕狀態
int lastButtonState = HIGH;      // 上一次按鈕狀態
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;  // 消彈跳延遲(毫秒)

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  Serial.println("消彈跳測試 - 準備就緒");
}

void loop() {
  int reading = digitalRead(buttonPin);

  // 如果讀值有變化,重置計時器
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }

  // 超過消彈跳延遲才更新狀態
  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != buttonState) {
      buttonState = reading;

      // 只在按下的瞬間觸發(HIGH → LOW)
      if (buttonState == LOW) {
        Serial.println("按鈕觸發!(已消彈跳)");
      }
    }
  }

  lastButtonState = reading;
}

這個方法的核心邏輯是:讀值必須穩定超過 50 毫秒,才算有效的狀態變化。

現在按一下就只會觸發一次囉!


範例三:按鈕切換 LED(Toggle)

每按一次按鈕,LED 在亮和滅之間切換,這是最實用的按鈕應用之一。我們直接用ESP32內建的LED就好,就不用另外接線了!

程式碼

// 按鈕切換 LED(Toggle)
// 結合消彈跳處理

const int buttonPin = 4;
const int ledPin = 2;  // GPIO 2(板載 LED)

bool ledState = false;
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);
  Serial.println("LED 切換測試 - 準備就緒");
}

void loop() {
  int reading = digitalRead(buttonPin);

  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    static int buttonState = HIGH;
    if (reading != buttonState) {
      buttonState = reading;

      if (buttonState == LOW) {
        ledState = !ledState;  // 切換狀態
        digitalWrite(ledPin, ledState ? HIGH : LOW);
        Serial.print("LED 狀態:");
        Serial.println(ledState ? "ON" : "OFF");
      }
    }
  }

  lastButtonState = reading;
}

範例四:使用中斷(Interrupt)處理按鈕

前面的範例都使用 loop() 輪詢的方式讀取按鈕,但如果主程式有其他耗時的任務,可能會漏掉按鈕事件。使用中斷(Interrupt)可以確保按鈕事件即時被捕捉。

// 使用中斷(Interrupt)處理按鈕

const int buttonPin = 4;
const int ledPin = 2;

volatile bool buttonPressed = false;  // volatile 確保在中斷中正確更新
bool ledState = false;
unsigned long lastInterruptTime = 0;

// 中斷服務程式(ISR)
void IRAM_ATTR handleButtonPress() {
  unsigned long interruptTime = millis();
  // 在 ISR 中做簡易消彈跳
  if (interruptTime - lastInterruptTime > 200) {
    buttonPressed = true;
    lastInterruptTime = interruptTime;
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);

  // 設定中斷:FALLING = HIGH→LOW(按下的瞬間)
  attachInterrupt(digitalPinToInterrupt(buttonPin), handleButtonPress, FALLING);

  Serial.println("中斷模式測試 - 準備就緒");
}

void loop() {
  if (buttonPressed) {
    buttonPressed = false;
    ledState = !ledState;
    digitalWrite(ledPin, ledState ? HIGH : LOW);
    Serial.print("LED 狀態(中斷觸發):");
    Serial.println(ledState ? "ON" : "OFF");
  }

  // 主程式可以做其他事情,不怕漏掉按鈕事件
  // 例如:讀取感測器、處理網路通訊等
}

中斷重點說明

項目 說明
IRAM_ATTR 將 ISR 放入 IRAM,ESP32 必須加上此屬性
volatile 告訴編譯器這個變數可能被中斷修改,避免最佳化錯誤
FALLING 偵測 HIGH → LOW 的瞬間觸發,適合 INPUT_PULLUP
RISING 偵測 LOW → HIGH 的瞬間觸發
CHANGE HIGH → LOW 或 LOW → HIGH 都會觸發
注意: ISR 裡不要做耗時操作(如 Serial.printlndelay),只設定旗標,在 loop() 中處理。

範例五:偵測長按與短按

實務上常需要區分「短按」和「長按」來執行不同功能,例如短按切換 LED、長按啟動 Wi-Fi 設定模式。

// 偵測長按與短按

const int buttonPin = 4;
const int ledPin = 2;

bool ledState = false;
int lastButtonState = HIGH;
unsigned long pressStartTime = 0;
bool isPressing = false;
const unsigned long longPressTime = 1000;  // 長按門檻:1 秒

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
  Serial.println("長按/短按偵測 - 準備就緒");
}

void loop() {
  int reading = digitalRead(buttonPin);

  // 偵測按下(HIGH → LOW)
  if (reading == LOW && lastButtonState == HIGH) {
    pressStartTime = millis();
    isPressing = true;
  }

  // 偵測放開(LOW → HIGH)
  if (reading == HIGH && lastButtonState == LOW) {
    if (isPressing) {
      unsigned long pressDuration = millis() - pressStartTime;

      if (pressDuration >= longPressTime) {
        // 長按動作
        Serial.println("【長按】觸發!可用於特殊功能");
        Serial.println("例如:啟動 Wi-Fi AP 設定模式...");
      } else if (pressDuration > 50) {  // 大於 50ms 排除彈跳
        // 短按動作
        ledState = !ledState;
        digitalWrite(ledPin, ledState ? HIGH : LOW);
        Serial.print("【短按】LED 切換:");
        Serial.println(ledState ? "ON" : "OFF");
      }
      isPressing = false;
    }
  }

  lastButtonState = reading;
  delay(10);  // 小延遲穩定讀值
}

常見問題排除

按鈕按下沒反應?

  1. 確認接腳是否接在對角位置,同側的兩支腳本來就是相連的
  2. 用三用電表的導通模式測試按鈕是否正常
  3. 確認 GPIO 腳位是否正確,NodeMCU-32S 的絲印編號對應實際 GPIO
  4. 檢查是否使用了僅供輸入的腳位(GPIO 34、35、36、39 沒有內建上拉電阻)

按一次觸發好幾次?

這就是彈跳問題,請參考範例二的消彈跳處理。50ms 的延遲通常足夠,如果仍有問題可以嘗試增加到 80~100ms。

ESP32 哪些 GPIO 可以用?

可安全使用 GPIO 4, 5, 13, 14, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33
僅輸入(無上拉) GPIO 34, 35, 36, 39
開機時避免使用 GPIO 0, 2, 12, 15(影響開機模式)

延伸應用

學會按鈕操作後,可以嘗試以下進階應用:

  • 多按鈕選單系統:用 3 個按鈕(上、下、確認)搭配 OLED 做出選單介面
  • 按鈕 + Wi-Fi 控制:短按切換本地 LED,長按啟動 AP 模式進行 Wi-Fi 設定
  • 按鈕計數器:搭配 LCD 顯示按壓次數,可做簡易計數器或碼表
  • 按鈕組合鍵:同時偵測多個按鈕的組合,實現密碼鎖功能
  • 按鈕 + MQTT:按下按鈕透過 MQTT 發送訊息,實現 IoT 遠端控制

總結

這篇教學涵蓋了 ESP32 NodeMCU-32S 搭配輕觸按鈕的完整學習路徑:

  1. 基礎讀取 — 認識 digitalRead()INPUT_PULLUP
  2. 消彈跳 — 解決機械按鈕的訊號不穩問題
  3. Toggle 切換 — 最常用的按鈕應用模式
  4. 中斷處理 — 確保不漏接按鈕事件
  5. 長按/短按 — 單一按鈕實現多功能

掌握這些基礎後,按鈕就能成為你各種 ESP32 專題中可靠的輸入介面。下一步可以試著把按鈕和 OLED 顯示器、Wi-Fi 功能結合,做出更完整的互動專案!