第4章 ESP32 數位輸入教學:按鍵、Pull-up、Pull-down 與 Debounce

發布日期: |更新日期:
文章主圖:第4章 ESP32 數位輸入教學:按鍵、Pull-up、Pull-down 與 Debounce

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 邏輯是:

ESP32 INPUT_PULLUP 按鍵狀態
按鍵 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 邏輯

ESP32 INPUT_PULLDOWN 按鍵狀態
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 比較

ESP32 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 資源限制。

下一步學習

不要停在這一篇,接著把內容做成自己的實作

目前主題:ESP32 / IoT。從完整學習路線、免費資源、會員下載,再接到實作所需材料。

同系列:ESP32 |  已是會員?直接前往 Getfile
相關資源與會員文章下載
文章可免費閱讀;下載完整 DOCX 或 PDF 需登入免費基本會員。
查看相關資源 會員免費下載 DOCX 會員免費 PDF/列印

← 回文章列表