ESP32 除了可以透過 GPIO 控制 LED、Relay、Buzzer 等輸出設備,也必須能夠讀取外部世界的狀態。
最基本的例子就是:
按鍵(Push Button)
看似簡單的一顆按鍵,實際上包含了微控制器數位輸入非常重要的幾個觀念:
- HIGH
- LOW
- GPIO Input
- Floating
- Pull-up
- Pull-down
- INPUT_PULLUP
- INPUT_PULLDOWN
- Contact Bounce
- Debounce
- Edge Detection
- millis()
- 短按與單次觸發
如果沒有理解這些觀念,很容易發生:
按一次按鍵
↓
ESP32 卻判斷成很多次
或者:
完全沒有按按鍵
↓
GPIO 卻不停 HIGH / LOW 跳動
本章將從最基本的 Digital Input 開始,逐步完成:
Push Button
↓
ESP32 GPIO
↓
digitalRead()
↓
Pull-up / Pull-down
↓
Debounce
↓
Edge Detection
↓
LED Control
完成之後,這套程式就可以進一步應用在:
- Relay 開關
- 電子密碼鎖
- 選單控制
- 計數器
- 模式切換
- 啟動/停止鍵
- IoT 控制器
- 工業控制面板
一、什麼是 Digital Input?
Digital Input 就是讓 ESP32 GPIO 判斷外部訊號目前屬於:
HIGH
或:
LOW
Arduino-ESP32 官方 GPIO API 中,GPIO 設為 Input 後,可以透過 digitalRead() 取得 HIGH 或 LOW 狀態。
例如:
Button
│
↓
GPIO27
│
↓
ESP32
ESP32 可以判斷:
Button Released
或:
Button Pressed
二、ESP32 數位輸入最基本的三個函式
Arduino ESP32 GPIO Input 最常使用:
pinMode();
digitalRead();
以及內部電阻設定:
INPUT_PULLUP
INPUT_PULLDOWN
Arduino-ESP32 官方文件目前支援基本模式:
INPUT
OUTPUT
INPUT_PULLUP
INPUT_PULLDOWN
其中 INPUT 為高阻抗輸入,INPUT_PULLUP 啟用內部上拉,INPUT_PULLDOWN 啟用內部下拉。
三、第一個 ESP32 按鍵讀取程式
本章建議使用:
GPIO27
作為按鍵輸入。
GPIO27 不屬於經典 ESP32 的 GPIO34~39 Input-only 特殊群組,也不是 GPIO0、2、5、12、15 這幾個 Strapping Pins,因此很適合做基礎按鍵實驗。經典 ESP32 的特殊 GPIO 分類可由 Espressif 官方 GPIO 文件確認。
先建立最基本程式:
const int BUTTON_PIN = 27;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT);
}
void loop()
{
int buttonState = digitalRead(BUTTON_PIN);
Serial.println(buttonState);
delay(100);
}
看起來很簡單。
但是如果 GPIO27 沒有任何外部 Pull-up 或 Pull-down:
GPIO27
可能處於:
Floating
也就是浮動狀態。
四、什麼是 Floating?
如果 GPIO 設成:
pinMode(BUTTON_PIN, INPUT);
而且外部沒有任何電路把它固定在 High 或 Low,輸入屬於高阻抗狀態。Arduino-ESP32 官方 GPIO 文件也說明,沒有啟用 Pull-up 或 Pull-down 時,Input 會維持高阻抗。
可以想像:
?
│
GPIO27
│
ESP32
它沒有被可靠地接到:
3.3V
也沒有可靠地接到:
GND
因此可能受到:
- 人體接近
- 電源雜訊
- 周圍線路
- 電磁干擾
- 長導線
影響。
Serial Monitor 可能看到:
1
1
0
1
0
0
1
...
即使完全沒有按按鍵。
這就是:
Floating Input
五、為什麼 Digital Input 一定需要預設狀態?
理想的按鍵應該只有兩種明確狀態:
Released
以及:
Pressed
不能有:
不知道
因此需要讓 GPIO 在「按鍵沒有按下」時,也一定具有明確電位。
最常見方法:
Pull-up
或:
Pull-down
六、什麼是 Pull-up?
Pull-up 中文通常稱為:
上拉電阻
概念:
3.3V
│
R
│
├──── GPIO
│
Button
│
GND
當按鍵沒有按:
3.3V
│
R
│
GPIO
因此:
GPIO = HIGH
當按鍵按下:
GPIO
│
Button
│
GND
因此:
GPIO = LOW
所以 Pull-up 邏輯是:
| 按鍵 | GPIO |
|---|---|
| Released | HIGH |
| Pressed | LOW |
這一點非常重要。
七、Pull-up 為什麼需要電阻?
不能直接做:
3.3V
│
GPIO
│
Button
│
GND
因為按鍵按下之後就可能形成:
3.3V
↓
Button
↓
GND
也就是直接短接電源。
Pull-up 電阻則形成:
3.3V
│
Resistor
│
GPIO
│
Button
│
GND
按鍵按下時,電流受到電阻限制。
八、ESP32 內建 Pull-up
好消息是:
ESP32 的許多 GPIO 已經具有內部 Pull-up / Pull-down 功能。
Arduino-ESP32 官方文件指出,支援內部 Pull-up / Pull-down 的 GPIO 可以透過 pinMode() 啟用;Arduino API 中提供 INPUT_PULLUP 與 INPUT_PULLDOWN。
因此通常不用額外接:
10kΩ Pull-up
只需要:
pinMode(BUTTON_PIN, INPUT_PULLUP);
九、INPUT_PULLUP 按鍵接線
使用:
GPIO27
接按鍵:
ESP32 GPIO27
│
│
Button
│
│
GND
程式:
pinMode(27, INPUT_PULLUP);
ESP32 內部概念:
3.3V
│
Internal Pull-up
│
GPIO27
│
Button
│
GND
十、INPUT_PULLUP 完整程式
const int BUTTON_PIN = 27;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.println();
Serial.println("ESP32 Button Test");
}
void loop()
{
int buttonState = digitalRead(BUTTON_PIN);
if (buttonState == LOW)
{
Serial.println("Button Pressed");
}
else
{
Serial.println("Button Released");
}
delay(200);
}
十一、為什麼按下是 LOW?
因為使用:
INPUT_PULLUP
未按:
Internal Pull-up
↓
GPIO27
↓
HIGH
按下:
GPIO27
│
Button
│
GND
所以:
LOW
因此程式:
if (digitalRead(BUTTON_PIN) == LOW)
{
// Button Pressed
}
非常常見。
十二、什麼是 Pull-down?
Pull-down 中文:
下拉電阻
概念:
3.3V
│
Button
│
├──── GPIO
│
R
│
GND
沒有按:
GPIO
│
R
│
GND
所以:
GPIO = LOW
按下:
3.3V
│
Button
│
GPIO
所以:
GPIO = HIGH
十三、Pull-down 邏輯
| Button | GPIO |
|---|---|
| Released | LOW |
| Pressed | HIGH |
也就是正好和:
INPUT_PULLUP
相反。
十四、ESP32 INPUT_PULLDOWN
Arduino-ESP32 官方 API 提供:
INPUT_PULLDOWN
可讓支援內部下拉的 GPIO 使用內建 Pulldown。
例如:
const int BUTTON_PIN = 27;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLDOWN);
}
void loop()
{
int state = digitalRead(BUTTON_PIN);
if (state == HIGH)
{
Serial.println("Button Pressed");
}
else
{
Serial.println("Button Released");
}
delay(200);
}
接線變成:
3.3V
│
Button
│
GPIO27
內部:
GPIO27
│
Internal Pull-down
│
GND
十五、Pull-up 與 Pull-down 比較
| 項目 | Pull-up | Pull-down |
|---|---|---|
| Released | HIGH | LOW |
| Pressed | LOW | HIGH |
| 按鍵另一端 | GND | 3.3V |
| Arduino ESP32 | INPUT_PULLUP | INPUT_PULLDOWN |
| 程式判斷 Pressed | LOW | HIGH |
兩種方式都可以。
本套 ESP32 教材後續按鍵範例會優先使用:
INPUT_PULLUP
也就是:
Button
↓
GND
方便統一程式邏輯。
十六、經典 ESP32 有哪些 GPIO 不能使用內部 Pull-up / Pull-down?
這裡一定要再次提醒。
經典 ESP32:
GPIO34
GPIO35
GPIO36
GPIO37
GPIO38
GPIO39
屬於 Input-only GPIO,而且沒有軟體可啟用的內部 Pull-up / Pull-down。
因此不要假設:
pinMode(34, INPUT_PULLUP);
能像 GPIO27 一樣工作。
如果真的使用 GPIO34:
3.3V
│
10kΩ
│
GPIO34
│
Button
│
GND
需要:
External Pull-up
十七、GPIO34 按鍵正確概念
例如:
3.3V
│
10kΩ
│
├──── GPIO34
│
Button
│
GND
程式:
const int BUTTON_PIN = 34;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT);
}
void loop()
{
int state = digitalRead(BUTTON_PIN);
Serial.println(state);
delay(100);
}
因為 Pull-up 是:
External
而不是:
Internal
十八、GPIO34~39 適合當按鍵嗎?
可以。
因為它們本來就是:
Input Only
但是缺點是必須自行處理:
External Pull-up
或:
External Pull-down
Espressif 官方 Button 元件文件也特別提醒,Input-only IO 不具相同的內部 Pull-up / Pull-down 能力,需要外部電阻。
初學者為了簡單,可以先使用:
GPIO27
GPIO32
GPIO33
這類一般 GPIO。
十九、按鍵真正的問題:Contact Bounce
現在來到這一章最重要的部分。
我們理想中認為:
Button Released
↓
Button Pressed
應該是:
HIGH
↓
LOW
只有一次變化。
但真正的機械按鍵接點,在按下瞬間可能產生:
HIGH
LOW
HIGH
LOW
HIGH
LOW
LOW
LOW
最後才真正穩定。
這種現象叫做:
Contact Bounce
中文常稱:
按鍵彈跳
二十、按鍵彈跳會造成什麼問題?
假設程式:
if (digitalRead(BUTTON_PIN) == LOW)
{
counter++;
}
我們希望:
按一次
↓
counter +1
結果卻可能:
按一次
↓
counter +5
甚至更多。
因為 ESP32 執行程式的速度遠快於人類按鍵動作。
人類認為:
只按了一次
CPU 卻可能在這段時間內執行:
loop()
loop()
loop()
loop()
loop()
...
很多次。
二十一、這裡其實有兩個不同問題
初學者經常把兩件事情都叫:
Debounce
但最好分開理解。
第一個:
Contact Bounce(補充說明)
機械接點本身抖動。
第二個:
Level Repeat
只要按鍵一直保持 LOW:
if (digitalRead(BUTTON_PIN) == LOW)
{
counter++;
}
每一次 loop() 都會再加一次。
因此真正好用的按鍵程式通常需要同時處理:
Debounce
+
Edge Detection
二十二、什麼是 Edge Detection?
假設使用 INPUT_PULLUP。
按鍵:
Released = HIGH
Pressed = LOW
真正希望偵測的是:
HIGH
↓
LOW
這個變化瞬間。
也就是:
Falling Edge
而不是一直判斷:
現在是不是 LOW?
二十三、Level Detection 與 Edge Detection
Level Detection
if (digitalRead(BUTTON_PIN) == LOW)
{
counter++;
}
如果按住一秒:
LOW
LOW
LOW
LOW
LOW
...
就會一直執行。
Edge Detection
只判斷:
HIGH → LOW
那個瞬間:
Released
↓
Pressed
所以:
按一次
=
觸發一次
這才適合:
- 計數器
- LED Toggle
- Relay Toggle
- Menu
- Mode Switch
二十四、最簡單的 Edge Detection 程式
const int BUTTON_PIN = 27;
int lastButtonState = HIGH;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop()
{
int currentButtonState = digitalRead(BUTTON_PIN);
if (lastButtonState == HIGH &&
currentButtonState == LOW)
{
Serial.println("Button Pressed");
}
lastButtonState = currentButtonState;
}
概念:
Previous = HIGH
Current = LOW
才判斷:
Pressed Event
二十五、Edge Detection 還是不能完全解決 Bounce
因為實際按鍵可能:
HIGH
LOW ← Press
HIGH
LOW
HIGH
LOW
程式可能看到很多個:
HIGH → LOW
所以仍然可能判斷:
Pressed
Pressed
Pressed
因此還需要:
Debounce
二十六、什麼是 Debounce?
Debounce 中文常翻譯:
防彈跳
核心概念:
GPIO 電位剛變化時,不要立刻把每一次短暫跳動都視為真正的新按鍵事件,而是等待或持續確認一段時間,確定輸入已經穩定。
Espressif 官方 FAQ 也提出利用軟體對 Digital Input 進行多次 Sampling / Filtering,降低短暫電位變化造成誤判的方式。
二十七、Debounce 有哪些方法?
主要可以分:
Debounce
│
├── Hardware Debounce
│
└── Software Debounce
Software 又可以:
Software
│
├── delay()
│
├── millis()
│
├── Sampling
│
└── State Machine
本章主要使用:
delay()
以及:
millis()
二十八、方法一:delay() Debounce
最簡單的方法:
if (buttonState == LOW)
{
delay(30);
if (digitalRead(BUTTON_PIN) == LOW)
{
// Confirm Pressed
}
}
這裡使用:
30 ms
只是本教材的起始實驗值,不是所有按鍵都必須使用 30 ms。
不同:
- 按鍵
- 開關
- 電路
- 線長
- 環境
都可能需要調整。
二十九、delay() Debounce 完整範例
const int BUTTON_PIN = 27;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop()
{
if (digitalRead(BUTTON_PIN) == LOW)
{
delay(30);
if (digitalRead(BUTTON_PIN) == LOW)
{
Serial.println("Button Pressed");
while (digitalRead(BUTTON_PIN) == LOW)
{
delay(10);
}
delay(30);
}
}
}
這個版本可以做到:
按一次
↓
輸出一次
三十、這個程式為什麼有效?
流程:
Button LOW
↓
wait 30 ms
↓
still LOW?
↓
YES
↓
Pressed
↓
等待 Release
所以不會在按住期間一直重複:
Pressed
Pressed
Pressed
三十一、但是 delay() 有一個很大的缺點
這一段:
delay(30);
會暫停程式。
更明顯的是:
while (digitalRead(BUTTON_PIN) == LOW)
{
delay(10);
}
使用者如果按住按鍵:
3 秒
程式可能就在這裡等待:
3 秒
期間其他工作可能無法按原本 loop 流程正常執行。
如果 ESP32 同時需要:
Read Sensor
Update LCD
Control Relay
MQTT
Web Server
LED Animation
這種 Blocking 寫法就不理想。
三十二、什麼是 Blocking?
Blocking 可以理解成:
目前工作沒有完成
↓
其他 loop 工作先等
例如:
delay(5000);
會讓目前執行流程等待:
5 秒
對非常簡單的 LED / Button 教學沒有問題。
但是進入 IoT 專題後,我們更希望:
Button
Sensor
Display
Wi-Fi
MQTT
Relay
可以一起被程式持續處理。
所以要進一步學:
millis()
三十三、什麼是 millis()?
Arduino Framework 的:
millis()
可以取得程式執行後經過的毫秒計數。
概念:
0
1
2
3
...
1000
1001
...
我們可以記錄:
按鍵最後一次變化時間
然後判斷:
現在時間 - 最後變化時間
是否已經超過 Debounce Interval。
三十四、millis() Debounce 核心概念
假設:
const unsigned long DEBOUNCE_DELAY = 30;
當讀到 Button State 改變:
HIGH → LOW
先記錄:
lastDebounceTime = millis();
然後等待訊號持續穩定超過:
30 ms
才正式承認:
Button Pressed
三十五、完整 millis() Debounce 程式
以下是一個很重要的基礎版本:
const int BUTTON_PIN = 27;
int stableButtonState = HIGH;
int lastReading = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 30;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.println();
Serial.println("ESP32 Debounce Test");
}
void loop()
{
int reading = digitalRead(BUTTON_PIN);
if (reading != lastReading)
{
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) >= DEBOUNCE_DELAY)
{
if (reading != stableButtonState)
{
stableButtonState = reading;
if (stableButtonState == LOW)
{
Serial.println("Button Pressed");
}
else
{
Serial.println("Button Released");
}
}
}
lastReading = reading;
}
三十六、這個程式的工作方式
第一步:
int reading = digitalRead(BUTTON_PIN);
讀取目前 GPIO。
第二步:
if (reading != lastReading)
{
lastDebounceTime = millis();
}
如果發現輸入改變:
重新開始 Debounce Timer
第三步:
if ((millis() - lastDebounceTime) >= DEBOUNCE_DELAY)
表示這個狀態已經穩定至少:
30 ms
第四步:
if (reading != stableButtonState)
只有真正穩定狀態改變時才處理。
第五步:
if (stableButtonState == LOW)
代表:
Button Pressed
這樣就不需要:
delay(30);
也不用:
while(...)
阻塞整個程式。
三十七、為什麼用 millis() 比 delay() 更適合 IoT?
假設 ESP32 同時處理:
Button
│
├── DHT22
├── OLED
├── Relay
├── Wi-Fi
└── MQTT
使用 millis() 時:
Button Debounce
不需要把 CPU 卡在等待流程。
所以 loop 還可以繼續處理:
Sensor
Network
Display
Control
這是後面 ESP32 專題非常重要的程式設計觀念。
三十八、實作:按一次切換 LED ON / OFF
現在把 Debounce 真正變成有用的控制器:
Button
↓
ESP32
↓
LED
要求:
按第 1 次
→ LED ON
按第 2 次
→ LED OFF
按第 3 次
→ LED ON
也就是:
Toggle
三十九、材料
- ESP32 × 1
- Push Button × 1
- LED × 1
- 220Ω~330Ω 電阻 × 1
- Breadboard
- Jumper Wire
GPIO:
Button
→ GPIO27
LED
→ GPIO23
四十、接線
Button
GPIO27
│
Button
│
GND
使用:
INPUT_PULLUP
LED
GPIO23
│
220Ω
│
LED
│
GND
四十一、完整 Button Toggle LED 程式
const int BUTTON_PIN = 27;
const int LED_PIN = 23;
int stableButtonState = HIGH;
int lastReading = HIGH;
bool ledState = false;
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 30;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
Serial.println();
Serial.println("===========================");
Serial.println(" ESP32 Button Toggle LED");
Serial.println("===========================");
}
void loop()
{
int reading = digitalRead(BUTTON_PIN);
if (reading != lastReading)
{
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) >= DEBOUNCE_DELAY)
{
if (reading != stableButtonState)
{
stableButtonState = reading;
if (stableButtonState == LOW)
{
ledState = !ledState;
digitalWrite(
LED_PIN,
ledState ? HIGH : LOW
);
if (ledState)
{
Serial.println("Button Pressed -> LED ON");
}
else
{
Serial.println("Button Pressed -> LED OFF");
}
}
}
}
lastReading = reading;
}
四十二、程式執行流程
Read Button
↓
State changed?
│
YES
↓
Start Debounce Timer
↓
Stable >= 30 ms?
│
YES
↓
New stable state?
│
YES
↓
Pressed?
│
YES
↓
Toggle LED
四十三、什麼是這一行?
ledState = !ledState;
假設:
ledState = false
執行:
!ledState
就變成:
true
下一次:
true
↓
false
所以:
OFF → ON → OFF → ON
這就是 Toggle 最簡單的寫法。
四十四、為什麼 Toggle 必須搭配 Edge Detection?
如果直接:
if (digitalRead(BUTTON_PIN) == LOW)
{
ledState = !ledState;
}
當你按住按鍵時:
loop 1 → Toggle
loop 2 → Toggle
loop 3 → Toggle
loop 4 → Toggle
...
LED 可能高速:
ON
OFF
ON
OFF
...
所以真正需要的是:
Button Press Event
而不是:
Button Is Low
這是數位輸入程式設計非常重要的差別。
四十五、實作:按鍵計數器
將 Toggle 改成計數器也很簡單。
新增:
unsigned long pressCount = 0;
在穩定 Pressed Event:
pressCount++;
Serial.print("Press Count = ");
Serial.println(pressCount);
四十六、完整 Debounce Counter
const int BUTTON_PIN = 27;
int stableButtonState = HIGH;
int lastReading = HIGH;
unsigned long lastDebounceTime = 0;
unsigned long pressCount = 0;
const unsigned long DEBOUNCE_DELAY = 30;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.println();
Serial.println("ESP32 Button Counter");
}
void loop()
{
int reading = digitalRead(BUTTON_PIN);
if (reading != lastReading)
{
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) >= DEBOUNCE_DELAY)
{
if (reading != stableButtonState)
{
stableButtonState = reading;
if (stableButtonState == LOW)
{
pressCount++;
Serial.print("Press Count = ");
Serial.println(pressCount);
}
}
}
lastReading = reading;
}
按:
1 次
輸出:
Press Count = 1
再按:
Press Count = 2
而不是:
1
2
3
4
5
6
一次跳很多筆。
四十七、按鍵狀態可以分成哪些事件?
未來完整的人機介面,不只有:
Pressed
還可以區分:
Press
Release
Single Click
Double Click
Long Press
Repeat Press
Espressif 官方 IoT Button Component 本身也提供 Press、Release、Single Click、Double Click、Multiple Click、Long Press 等事件模型。
因此本章學到的:
Stable State
+
Edge
+
Timing
正是後續建立:
Short Press
Long Press
Double Click
的基礎。
四十八、如何判斷長按?
概念:
Button Pressed
↓
記錄時間
↓
Button Released
↓
Released Time - Press Time
例如自行定義:
< 1000 ms
→ Short Press
>= 1000 ms
→ Long Press
這裡的 1000 ms 是應用程式自行定義的人機介面規則,不是 ESP32 的硬體限制。
四十九、簡單 Long Press 概念程式
unsigned long pressStartTime = 0;
if (buttonPressed)
{
pressStartTime = millis();
}
if (buttonReleased)
{
unsigned long pressTime =
millis() - pressStartTime;
if (pressTime >= 1000)
{
Serial.println("Long Press");
}
else
{
Serial.println("Short Press");
}
}
後面可以用:
短按
→ 切換功能
長按
→ 進入設定模式
很適合:
- LCD Menu
- IoT Controller
- Relay Timer
- 電子時鐘
- 密碼鎖
五十、Software Debounce 與 Hardware Debounce
除了程式,也可以從硬體處理按鍵。
Software Debounce
優點:
- 不增加零件。
- 容易調整。
- 適合一般 Arduino / ESP32 專題。
- 可以結合 Edge、Long Press、Double Click。
例如:
millis()
+
State Machine
Hardware Debounce
可以使用:
Resistor
+
Capacitor
甚至:
Schmitt Trigger
等方式先處理訊號。
概念:
Mechanical Button
↓
RC Filter
↓
Cleaner Signal
↓
ESP32
對抗雜訊要求較高或具有特定即時性要求的設計,可能會使用硬體與軟體一起處理。
五十一、簡單 RC Debounce 概念
例如:
3.3V
│
10kΩ
│
├──── GPIO
│ │
Button C
│ │
GND GND
RC 可以讓非常短暫的電位變化被平滑。
但實際:
R
C
數值需要根據:
- 開關特性
- 需要的反應速度
- GPIO Threshold
- 系統需求
設計,而不是看到網路範例就固定照抄。
一般入門 ESP32 專題先使用:
Software Debounce
已經足夠學習大部分按鍵控制。
五十二、為什麼不建議用很長的 delay 做 Debounce?
例如:
delay(500);
雖然可能讓:
重複觸發
變少。
但代價是:
按鍵反應慢
+
整個程式被阻塞
這不是好的 Debounce 方法。
Debounce 的目標不是:
讓 CPU 等很久
而是:
確認 Input 已經穩定
五十三、ESP32 Button + Relay 可以怎麼做?
完成本章程式後,把:
LED
換成:
Relay Module
就可以做到:
Button
↓
ESP32
↓
Relay
↓
Load
例如:
第一次按
→ Relay ON
第二次按
→ Relay OFF
程式核心仍然:
relayState = !relayState;
但是要注意 Relay Module 可能是:
Active HIGH
或:
Active LOW
必須依實際模組規格確認。
五十四、按鍵為什麼很適合使用 Serial Monitor 除錯?
測試時先不要急著控制 Relay。
先:
Serial.println("Button Pressed");
確認:
一次按下
=
一次 Pressed
再接:
LED
最後才接:
Relay
推薦順序:
Button
↓
Serial
↓
LED
↓
Relay
比一次全部接上更容易找到問題。
五十五、ESP32 Button Debug 流程
按鍵沒有反應時:
Step 1
確認 GPIO Number
↓
Step 2
確認 GND
↓
Step 3
確認 pinMode
↓
Step 4
確認 Pull-up / Pull-down
↓
Step 5
Serial.print digitalRead()
↓
Step 6
確認 Pressed 邏輯 HIGH / LOW
↓
Step 7
加入 Debounce
↓
Step 8
最後才控制 Relay / Menu
五十六、常見錯誤 1:INPUT_PULLUP 邏輯寫反
錯誤:
if (digitalRead(BUTTON_PIN) == HIGH)
{
Serial.println("Pressed");
}
如果按鍵:
GPIO → Button → GND
並使用:
INPUT_PULLUP
通常應該是:
LOW
代表 Pressed。
五十七、常見錯誤 2:INPUT 模式沒有任何電阻
例如:
pinMode(27, INPUT);
接一顆開關,但 Released 時 GPIO 懸空。
結果:
HIGH
LOW
HIGH
LOW
亂跳。
解決:
INPUT_PULLUP
或者:
INPUT_PULLDOWN
或者:
External Pull Resistor
五十八、常見錯誤 3:GPIO34 使用 INPUT_PULLUP
經典 ESP32 GPIO34~39 不具有軟體可啟用的內部 Pull-up / Pull-down。
改成:
External Resistor
或者換:
GPIO27
GPIO32
GPIO33
五十九、常見錯誤 4:按一次計數很多次
如果程式:
if (digitalRead(BUTTON_PIN) == LOW)
{
count++;
}
問題可能包含:
Level Repeat
+
Mechanical Bounce
應該加入:
Edge Detection
+
Debounce
六十、常見錯誤 5:只用 delay(500) 解決所有問題
雖然看起來:
可以用
但會讓程式反應遲鈍,而且不適合後面的:
Wi-Fi
MQTT
LCD
Sensor
Web Server
專題。
更推薦:
millis()
非阻塞 Debounce。
六十一、常見錯誤 6:按鍵四腳接錯
常見 6×6 mm Tact Switch 有:
4 Pins
但實際常是:
A ─── A
B ─── B
同一側兩腳內部可能已相通。
按下後才:
A
│
B
導通。
如果把導線全部接在同一組已經相通的腳,按不按可能都沒有差別。
所以最好使用:
Multimeter Continuity
先確認按鍵內部接點。
六十二、常見錯誤 7:按鍵沒有共地
例如 ESP32 和外部電路分開供電時:
ESP32 GND
與外部訊號來源通常必須具有正確共同參考電位。
如果 GND 沒有正確建立:
GPIO High / Low
就可能無法可靠判斷。
六十三、按鍵接線速查
INPUT_PULLUP(補充說明)
GPIO
│
Button
│
GND
程式:
pinMode(BUTTON_PIN, INPUT_PULLUP);
邏輯:
Released = HIGH
Pressed = LOW
INPUT_PULLDOWN
3.3V
│
Button
│
GPIO
程式:
pinMode(BUTTON_PIN, INPUT_PULLDOWN);
邏輯:
Released = LOW
Pressed = HIGH
六十四、Pull-up / Pull-down 快速記憶方式
可以記:
Pull-UP
↓
沒按時拉向 UP
↓
HIGH
所以:
INPUT_PULLUP
Released = HIGH
Pull-DOWN
↓
沒按時拉向 DOWN
↓
LOW
所以:
INPUT_PULLDOWN
Released = LOW
六十五、ESP32 官方 GPIO API 重點
Arduino-ESP32 官方目前將:
pinMode()
用於設定 GPIO Mode,
digitalRead()
用於讀取 Input 狀態,而:
INPUT_PULLUP
INPUT_PULLDOWN
則可啟用內部 Pull-up / Pull-down。
因此本章最核心 API 就是:
pinMode(BUTTON_PIN, INPUT_PULLUP);
int state = digitalRead(BUTTON_PIN);
六十六、本章推薦的正式 Button 架構
後續所有專題,建議不要再使用:
if (digitalRead(BUTTON_PIN) == LOW)
{
delay(500);
}
而逐步統一成:
Read Input
↓
Detect Change
↓
Debounce Timer
↓
Stable State
↓
Edge Event
↓
Application Action
也就是:
GPIO
↓
Debounce
↓
Event
↓
Function
這樣未來可以很容易從:
LED
換成:
Relay
Menu
Motor
Wi-Fi Command
MQTT Publish
六十七、專題範例:ESP32 人機按鍵控制器
例如未來建立:
Button 1
│
├── GPIO32
│
Button 2
│
├── GPIO33
│
Button 3
│
└── GPIO27
↓
ESP32
│
┌────┼────┐
│ │ │
LED Relay OLED
可以定義:
Button 1
→ 上一頁
Button 2
→ 下一頁
Button 3
→ Enter
這就是 LCD / OLED 人機介面的基本架構。
六十八、專題範例:啟動/停止控制
例如:
START Button
→ GPIO32
STOP Button
→ GPIO33
ESP32:
START
↓
System Running
STOP
↓
System Stop
進一步可以控制:
Relay
Fan
Pump
Motor
但每一顆機械按鍵都應該考慮:
Debounce
而不是直接讀取 GPIO 就執行控制。
六十九、本章最重要的程式版本
如果只保存一個範例,建議保存:
const int BUTTON_PIN = 27;
int stableButtonState = HIGH;
int lastReading = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 30;
void setup()
{
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop()
{
int reading = digitalRead(BUTTON_PIN);
if (reading != lastReading)
{
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) >= DEBOUNCE_DELAY)
{
if (reading != stableButtonState)
{
stableButtonState = reading;
if (stableButtonState == LOW)
{
Serial.println("PRESSED");
}
else
{
Serial.println("RELEASED");
}
}
}
lastReading = reading;
}
因為這個架構後面可以直接改成:
PRESSED
↓
LED Toggle
Relay Toggle
Counter
Menu Select
Start
Stop
Mode Change
七十、本章實作成果
完成本章後,你應該已經能夠解釋:
Digital Input
HIGH
LOW
Floating(補充說明)
沒有固定電位
Pull-up
Released = HIGH
Pressed = LOW
Pull-down
Released = LOW
Pressed = HIGH
Debounce(補充說明)
過濾短暫不穩定輸入
Edge Detection(補充說明)
偵測狀態變化事件
並完成:
Button
↓
Debounce
↓
Toggle LED
這已經是一個真正可以用在後續專題的數位輸入架構。
七十一、本章總結
ESP32 按鍵程式看似只需要:
digitalRead();
但真正可靠的數位輸入至少包含四個核心觀念:
1. 不讓 Input Floating
使用:
Pull-up
或:
Pull-down
2. 理解 Active LOW
使用:
INPUT_PULLUP
時:
Released = HIGH
Pressed = LOW
3. 處理 Contact Bounce
機械按鍵在切換瞬間可能產生多次短暫電位變化,因此必須進行:
Debounce
Espressif 官方文件也建議可透過軟體 Sampling / Filtering 對 Digital Input 進行去抖處理。
4. 使用 Edge Event,而不是只看 Level
不要只問:
按鍵現在是不是 LOW?
更應該判斷:
剛剛是不是從 HIGH 變 LOW?
因此:
Digital Input
↓
Pull-up
↓
Debounce
↓
Edge Detection
↓
Button Event
才是後續 ESP32 人機控制程式真正需要的架構。
七十二、下一章
接下來將從:
Digital
進入:
Analog
下一章:
第 5 章 ESP32 ADC 類比輸入完整教學:AnalogRead、ADC1 / ADC2、電位器與感測器
將進一步介紹:
Analog Voltage
↓
ADC
↓
ESP32
↓
analogRead()
↓
0~4095
↓
Voltage
並進一步理解:
ADC1
ADC2
Resolution
Attenuation
Wi-Fi
Potentiometer
LDR
Analog Sensor
特別是經典 ESP32 的 ADC2 與 Wi-Fi 使用限制,是後面 IoT Sensor 專題非常重要的硬體觀念;Espressif 官方 GPIO 文件目前也特別提醒,經典 ESP32 使用 Wi-Fi 時應注意 ADC2 資源限制。