《ESP32 入門》ESP32 輕觸按鈕(Tactile Switch)教學
帶領初學者以 ESP32 讀取輕觸開關狀態。內容涵蓋硬體接線原理、內部上拉電阻設定,並實作以按鍵控制 LED 亮滅的程式,幫助新手輕鬆掌握基礎的輸入控制。
輕觸按鈕(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),原因如下:
- 接線最簡單 — 按鈕一端接 GPIO、另一端接 GND,只需兩條線
- 業界標準 — Arduino、ESP32 社群的範例幾乎都用上拉
- ESP32 內建支援 — 不需要額外零件,
INPUT_PULLUP一行搞定 - 抗干擾較好 — 高電位做為預設狀態在實務上比較穩定

硬體準備
所需材料
- ESP32 NodeMCU-32S 開發板 × 1
- 12×12mm 輕觸按鈕 × 1
- 10KΩ 電阻 × 1(外部下拉用,選用)
- LED × 1(選用,用於輸出測試)
- 220Ω 電阻 × 1(選用,限流用)
- 麵包板 × 1
- 杜邦線若干



接線方式一:使用內建上拉電阻(推薦)
這是最簡單的接法,不需要額外電阻,只要兩條線:
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.println、delay),只設定旗標,在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); // 小延遲穩定讀值
}

常見問題排除
按鈕按下沒反應?
- 確認接腳是否接在對角位置,同側的兩支腳本來就是相連的
- 用三用電表的導通模式測試按鈕是否正常
- 確認 GPIO 腳位是否正確,NodeMCU-32S 的絲印編號對應實際 GPIO
- 檢查是否使用了僅供輸入的腳位(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 搭配輕觸按鈕的完整學習路徑:
- 基礎讀取 — 認識
digitalRead()和INPUT_PULLUP - 消彈跳 — 解決機械按鈕的訊號不穩問題
- Toggle 切換 — 最常用的按鈕應用模式
- 中斷處理 — 確保不漏接按鈕事件
- 長按/短按 — 單一按鈕實現多功能
掌握這些基礎後,按鈕就能成為你各種 ESP32 專題中可靠的輸入介面。下一步可以試著把按鈕和 OLED 顯示器、Wi-Fi 功能結合,做出更完整的互動專案!


