第9章 Raspberry Pi GPIO Input 實作:按鍵輸入、Pull-up、Pull-down 與 Debounce

發布日期: |更新日期:
文章主圖:第9章 Raspberry Pi GPIO Input 實作:按鍵輸入、Pull-up、Pull-down 與 Debounce

上一章我們完成了第一個真正的 Raspberry Pi GPIO Output 專案:

Python
   ↓
GPIO Output
   ↓
LED

我們已經可以控制:

  • LED ON
  • LED OFF
  • LED Blink
  • 4 顆 LED 流水燈
  • 8 顆 LED 跑馬燈
  • 往返流水燈

但是整個系統目前仍然是:

Raspberry Pi 主動告訴硬體該做什麼。

本章要加入相反方向的資料流:

讓外部硬體把狀態送進 Raspberry Pi。

也就是:

Push Button
     ↓
GPIO Input
     ↓
Python

接著就可以建立:

Button
   ↓
Python
   ↓
LED ON / OFF

甚至:

Button +
    ↓
跑馬燈加速

Button -
    ↓
跑馬燈減速

從這一章開始,Raspberry Pi 不再只是單向控制設備,而開始具有真正的:

Human-Machine Interaction

也就是人機互動能力。

1. GPIO Input 是什麼?

上一章使用:

GPIO Output

由 Raspberry Pi 主動輸出:

HIGH
LOW

本章則使用:

GPIO Input

由 Raspberry Pi 讀取外部訊號。

Raspberry Pi 官方說明,設為 Input 的 GPIO 可以讀取 High(約 3.3V)或 Low(約 0V),而內部 Pull-up / Pull-down 可以協助避免輸入處於不確定狀態。

概念:

External Circuit
       │
       ▼
GPIO Input
       │
       ▼
Python

2. GPIO Input 可以讀什麼?

常見 Input Device 包括:

  • Push Button
  • Toggle Switch
  • Limit Switch
  • Reed Switch
  • PIR Sensor
  • Digital Sensor
  • Encoder
  • Alarm Contact
  • Float Switch

本章先從最簡單的:

Push Button

開始。

3. Push Button 是什麼?

我們使用的是一般:

Momentary Push Button

也就是按住時接通、放開時恢復原狀的按鍵。

常見狀態:

沒有按
=
Open Circuit

按下
=
Closed Circuit

這種按鍵通常稱為:

Normally Open,NO

4. 什麼是 Normally Open?

Normally Open:

NO

表示正常未按時:

Circuit Open

按下:

Circuit Closed

例如:

GPIO ── Button ── GND

沒按:

GPIO   X   GND

按下:

GPIO ───── GND

這就是本章主要使用的 Push Button 接法。

5. 四腳按鍵為什麼有四支 Pin?

Breadboard 常見小型 Push Button 具有四支腳。

通常內部其實是:

A ───── A

B ───── B

左右兩組平常不相通。

按下:

A ───── B

才互相導通。

因此最容易出錯的情況是:

把兩條線接在本來就永久相通的同一側。

這樣按不按都沒有變化。

6. 如何確認四腳 Button?

如果不確定,可以使用 Multimeter:

Continuity Mode

先量:

  • 未按下
  • 按下

找出:

哪兩腳平常相通

以及:

哪兩組按下後才相通

這比單純猜按鍵腳位可靠。

7. 第一個重要問題:GPIO Input 沒接東西時是多少?

可能不是固定的:

HIGH

也不是固定:

LOW

而可能變成:

Floating Input

8. Floating Input 是什麼?

假設:

GPIO17
  │
  └── 沒接任何明確 HIGH / LOW

那麼這個 Input 沒有固定電位參考。

它可能受到:

  • 電磁干擾
  • 人體
  • 鄰近導線
  • 電源 Noise

影響。

結果:

HIGH
LOW
HIGH
LOW
...

看起來像亂跳。

這就是:

Floating Input

9. Floating Input 為什麼不好?

假設你的程式:

if button_pressed:
    relay_on()

如果 GPIO Floating:

Noise
 ↓
False Trigger
 ↓
Relay ON

這在真正控制系統中會非常危險。

因此 Input 必須有:

Defined Default State

10. Pull-up / Pull-down 的目的

Pull Resistor 的目的就是:

在外部沒有主動拉動 GPIO 時,替 Input 建立一個明確預設狀態。

兩種主要方式:

Pull-up

以及:

Pull-down

Raspberry Pi 官方文件也明確指出 GPIO Input 可透過內部 Pull-up 或 Pull-down 讓輸入更容易保持明確 High / Low 狀態。

11. Pull-up 是什麼?

Pull-up:

3.3V
 │
Pull-up Resistor
 │
GPIO
 │
Button
 │
GND

平常 Button 沒有按:

GPIO = HIGH

因為 Pull-up 將 Input 弱拉向:

3.3V

12. Pull-up 按下會發生什麼?

按下 Button:

GPIO
 │
Button
 │
GND

所以:

GPIO = LOW

因此實際電氣狀態:

Pull-up 按鍵電氣狀態
Button GPIO Electrical Level
Released HIGH
Pressed LOW

這叫:

Active Low

13. 為什麼 Pressed 是 LOW 不會很奇怪嗎?

如果只看電壓:

Pressed
=
LOW

好像有點反直覺。

但 GPIO Zero 會把這些底層細節封裝起來。

例如:

button = Button(27)

GPIO Zero 的 Button 預設 pull_up=True,也就是內部 Pull-up;按鍵另一端應接 GND。雖然電氣上按下變 LOW,但 button.is_pressed 仍會回傳 True。

所以程式可以保持直觀:

if button.is_pressed:
    print("Pressed")

14. Pull-down 是什麼?

Pull-down:

3.3V
 │
Button
 │
GPIO
 │
Pull-down Resistor
 │
GND

沒按:

GPIO = LOW

按下:

GPIO = HIGH

15. Pull-down 的電氣狀態

Pull-down 按鍵電氣狀態
Button GPIO Electrical Level
Released LOW
Pressed HIGH

這叫:

Active High

16. Pull-up 和 Pull-down 哪個比較好?

兩種都可以。

但對 Raspberry Pi + GPIO Zero 的一般 Push Button 實驗,本系列優先使用:

Internal Pull-up

原因很簡單:

GPIO Zero 的:

Button(pin)

預設就是:

pull_up=True

而官方文件指定這種接法:

GPIO
↕
Button
↕
GND

17. 本章主要 Button 接法

使用:

BCM GPIO27
=
Physical Pin 13

另一側:

GND
=
Physical Pin 14

接法:

GPIO27 / Pin 13
       │
       ▼
     Button
       │
       ▼
GND / Pin 14

程式:

button = Button(27)

GPIO Zero 預設開啟內部 Pull-up。

18. 為什麼不需要外接 Pull-up Resistor?

因為 Raspberry Pi GPIO 支援內部 Pull-up / Pull-down,而 GPIO Zero 可以透過 Button 設定控制它。Raspberry Pi 官方說明除了 GPIO2 / GPIO3 有固定 Pull-up 外,其他一般 GPIO 的 Pull 狀態可由軟體配置。

因此簡單 Button Lab 可以只接:

GPIO
+
Button
+
GND

19. 什麼情況還是會用外部 Pull Resistor?

正式硬體設計中,如果涉及:

  • 長線
  • Noise
  • EMI
  • Boot State
  • 特定 Resistance
  • Fail-safe
  • Industrial Signal

仍可能需要 External Pull Resistor。

Internal Pull 主要適合一般數位輸入與實驗。

20. 本章材料

基本實驗需要:

  • Raspberry Pi 5
  • Push Button × 1
  • LED × 1
  • 330Ω Resistor × 1
  • Breadboard
  • Female-to-Male Dupont Wire

進階雙按鍵跑馬燈需要:

  • Push Button × 2
  • LED × 8
  • 330Ω Resistor × 8
  • Breadboard

21. 接線前一樣先關機

執行:

sudo poweroff

然後:

拔 USB-C
   ↓
接 Button
   ↓
檢查
   ↓
接 LED
   ↓
再檢查
   ↓
重新 Power On

GPIO Input 雖然沒有直接帶負載,但接錯:

5V

進 GPIO 仍然可能損壞 Raspberry Pi。

22. 第一個 Button 接線

使用:

GPIO27
Physical Pin 13

接:

Button

再:

GND
Physical Pin 14

完整:

GPIO27 / Pin 13
       │
       │
     Button
       │
       │
GND / Pin 14

沒有外接 Resistor。

因為:

Internal Pull-up

由 GPIO Zero 處理。

23. 建立第 9 章目錄

cd ~

建立:

mkdir -p raspberrypi-course/chapter09

進入:

cd raspberrypi-course/chapter09

確認:

pwd

24. 第一支 GPIO Input 程式

建立:

nano button_state.py

內容:

from gpiozero import Button
from time import sleep

button = Button(27)

try:
    while True:
        print(button.is_pressed)
        sleep(0.2)

except KeyboardInterrupt:
    print("\nProgram stopped.")

finally:
    button.close()

執行:

python3 button_state.py

25. button.is_pressed 是什麼?

GPIO Zero 的 Button.is_pressed:

Pressed
→ True

Released
→ False

官方 API 明確將 is_pressed 定義為 Button Active 時回傳 True,否則 False。

所以你不需要自己反轉:

LOW = Pressed

的電氣邏輯。

26. 觀察結果

沒按:

False
False
False

按下:

True
True
True

放開:

False

這就是第一個:

GPIO Input

成功實作。

27. GPIO Zero 的 button.value

也可以:

print(button.value)

GPIO Zero Button 的 value:

Pressed
=
1

Released
=
0

例如:

from gpiozero import Button
from time import sleep

button = Button(27)

while True:
    print(button.value)
    sleep(0.2)

28. value 和 is_pressed 哪個比較好?

如果語意是:

按鍵有沒有按?

推薦:

button.is_pressed

因為程式很容易讀懂。

如果後面需要把 Input 統一成數值:

0
1

則:

button.value

也很方便。

29. 第一個 Pressed 判斷

from gpiozero import Button
from time import sleep

button = Button(27)

try:
    while True:
        if button.is_pressed:
            print("Button pressed")
        else:
            print("Button released")

        sleep(0.1)

except KeyboardInterrupt:
    print("\nProgram stopped.")

finally:
    button.close()

30. 這叫 Polling

剛才程式:

while True:
    if button.is_pressed:
        ...

一直反覆詢問:

Button 現在什麼狀態?

這種方式叫:

Polling

概念:

Check Button
    ↓
Wait
    ↓
Check Button
    ↓
Wait

31. Polling 有什麼問題?

Polling 本身沒有錯。

簡單程式很好理解。

但是如果系統還要同時處理:

  • Sensor
  • MQTT
  • Database
  • Camera
  • Web

你不一定想把程式全部寫成:

一直檢查 Button

因此 GPIO Zero 還提供:

Event-driven Programming

32. wait_for_press()

最簡單事件等待方式:

from gpiozero import Button

button = Button(27)

print("Waiting for button...")

button.wait_for_press()

print("Button pressed")

button.close()

GPIO Zero 官方 wait_for_press() 會暫停目前程式,直到 Button 進入 Active 狀態或 Timeout。

33. wait_for_press() 的流程

Program Start
      ↓
Waiting
      ↓
使用者按 Button
      ↓
Program Continue
      ↓
Print

這比:

while True 一直 Poll

更適合某些順序型操作。

34. wait_for_release()

同樣也有:

button.wait_for_release()

官方 API 定義為等待 Button 回到 Inactive 狀態。

例如:

from gpiozero import Button

button = Button(27)

button.wait_for_press()
print("Pressed")

button.wait_for_release()
print("Released")

button.close()

35. wait_for_press 可以設定 timeout

例如:

pressed = button.wait_for_press(
    timeout=5
)

如果 5 秒內有按:

繼續

如果沒有:

Timeout

官方 wait_for_press(timeout=None) 支援指定最大等待秒數。

36. when_pressed 是什麼?

GPIO Zero 提供:

button.when_pressed

可以指定:

當按鍵從 Released 變成 Pressed 時,執行哪個 Function。

官方定義 when_pressed 為 Input 從 Inactive 變為 Active 時觸發的 Handler。

37. 第一個 Event 程式

from gpiozero import Button
from signal import pause

button = Button(27)


def button_pressed():
    print("Button pressed")


button.when_pressed = button_pressed

pause()

按一下:

Button pressed

每次 Press Event 都會呼叫:

button_pressed()

38. 為什麼需要 pause()?

如果沒有:

pause()

主程式可能馬上執行到結尾並退出。

因此:

設定 Event Handler
       ↓
pause()
       ↓
等待事件

這種結構很常用在 GPIO Zero Event-driven Project。

39. when_released

同樣:

button.when_released

可以在 Button 放開時執行 Function。

例如:

from gpiozero import Button
from signal import pause

button = Button(27)


def pressed():
    print("Pressed")


def released():
    print("Released")


button.when_pressed = pressed
button.when_released = released

pause()

GPIO Zero Input API 支援 Press 與 Release 狀態事件。

40. Event-driven 和 Polling 差在哪?

Polling

while True:
    if button.is_pressed:
        ...

程式自己反覆檢查。

Event-driven

button.when_pressed = function

等狀態改變後執行 Function。

概念:

Polling
=
你一直問門鈴有沒有響


Event
=
門鈴響了才通知你

41. Button 控制 LED:按住亮、放開滅

現在加入上一章:

LED

LED:

GPIO17 / Physical Pin 11

Button:

GPIO27 / Physical Pin 13

42. 接線

LED:

GPIO17
  │
330Ω
  │
LED
  │
GND

Button:

GPIO27
  │
Button
  │
GND

兩者可以共用 Ground Rail。

43. Polling 版:按住 LED 亮

from gpiozero import LED, Button
from time import sleep

led = LED(17)
button = Button(27)

try:
    while True:
        if button.is_pressed:
            led.on()
        else:
            led.off()

        sleep(0.01)

except KeyboardInterrupt:
    print("\nProgram stopped.")

finally:
    led.off()
    button.close()
    led.close()

44. Event 版更簡單

from gpiozero import LED, Button
from signal import pause

led = LED(17)
button = Button(27)

button.when_pressed = led.on
button.when_released = led.off

pause()

這非常直觀:

Button Press
→ LED ON

Button Release
→ LED OFF

45. 這就是 Event-driven GPIO

程式不需要:

while True

而是把關係直接寫成:

button.when_pressed = led.on

也就是:

Button 被按時執行 LED 的 on()。

46. 按一下切換 LED 狀態

現在要做:

第一次按
→ LED ON

第二次按
→ LED OFF

第三次按
→ LED ON

這需要:

led.toggle()

47. Toggle LED 程式

from gpiozero import LED, Button
from signal import pause

led = LED(17)

button = Button(
    27,
    bounce_time=0.05
)


def toggle_led():
    led.toggle()

    print(
        "LED:",
        "ON" if led.is_lit else "OFF"
    )


button.when_pressed = toggle_led

pause()

48. 為什麼這裡加入 bounce_time?

因為真實機械按鍵按下一次時:

未必只產生一次乾淨的電氣變化。

可能實際出現:

ON
OFF
ON
OFF
ON

非常短暫地跳動。

這就是:

Contact Bounce

49. Contact Bounce 是什麼?

機械 Button 內部是金屬接點。

兩個接點碰撞時,不會像理想數學模型:

Open
↓
Closed

一次完成。

而可能:

Open
Closed
Open
Closed
Open
Closed

在很短時間內反覆變化。

對人眼看起來:

只按一次

但 Raspberry Pi 非常快,可能讀到:

很多次狀態改變

50. Bounce 會造成什麼問題?

例如:

button.when_pressed = led.toggle

你只按:

1 次

但機械 Bounce 造成:

Pressed
Released
Pressed
Released
Pressed

結果 LED 可能:

ON
OFF
ON

最後狀態看起來不可靠。

51. Debounce 是什麼?

Debounce:

去彈跳

意思是:

按鍵第一次發生狀態改變後,在一小段時間內忽略後續快速變化。

例如:

第一次 Press
     ↓
接受
     ↓
接下來 50ms
忽略快速變化

52. GPIO Zero bounce_time

GPIO Zero Button 提供:

bounce_time

官方定義:

若為 None,不做 Software Bounce Compensation;若指定秒數,初次狀態變化後會在這段時間內忽略其他變化。

例如:

button = Button(
    27,
    bounce_time=0.05
)

表示:

50 ms

的 Debounce Window。

53. bounce_time 單位是秒

例如:

bounce_time=0.05

代表:

0.05 sec
=
50 ms

而:

bounce_time=0.1

代表:

100 ms

GPIO Zero API 的 bounce_time 以秒為單位。

54. bounce_time 設多少?

沒有所有按鍵都適用的唯一數值。

一般 Lab 可以從:

0.03
0.05
0.1

附近開始測試。

如果太短:

仍可能 Bounce

如果太長:

快速連按可能被忽略

所以要依:

  • Button
  • Wiring
  • 實際操作

測試。

55. bounce_time 預設不是自動開啟

GPIO Zero 官方目前的:

bounce_time=None

代表預設:

不做 Software Debounce。

因此:

Button(27)

有 Internal Pull-up,但並不代表自動開啟 Debounce。

這兩個功能要分清楚。

56. Pull-up 與 Debounce 是兩件不同的事

Pull-up 解決:

Input Floating

Debounce 解決:

Mechanical Contact Bounce

所以:

Pull-up
≠
Debounce

兩個問題完全不同。

57. Pull-up 解決的是穩定預設電位

Button Released
   ↓
GPIO 不 Floating
   ↓
保持 HIGH

58. Debounce 解決的是短時間多次變化

Button Press
    ↓
機械 Bounce
    ↓
軟體忽略短暫變化
    ↓
只認一次有效 Press

59. Pull-down 如何使用 GPIO Zero?

假設電路改成:

3.3V
  │
Button
  │
GPIO27

希望未按為 LOW:

button = Button(
    27,
    pull_up=False
)

GPIO Zero 官方說明 pull_up=False 會把 GPIO 預設拉 Low,此時 Button 另一端應接 3V3。

60. Pull-down 接線

3.3V
Physical Pin 1
      │
      ▼
    Button
      │
      ▼
GPIO27
Physical Pin 13

程式:

button = Button(
    27,
    pull_up=False
)

61. Pull-up 和 Pull-down 程式比較

Pull-up

接:

GPIO ↔ Button ↔ GND

程式:

button = Button(27)

或明確:

button = Button(
    27,
    pull_up=True
)

Pull-down

接:

3.3V ↔ Button ↔ GPIO

程式:

button = Button(
    27,
    pull_up=False
)

這與 GPIO Zero 官方 API 定義一致。

62. pull_up=None 是什麼?

如果:

pull_up=None

GPIO Zero 不使用內部 Pull-up / Pull-down。

此時 Input 會:

Floating

除非你自己設計了:

External Pull Resistor

GPIO Zero 官方指出 pull_up=None 時,必須使用外部 Pull Circuit,並且要明確指定 active_state。

63. 初學者不要無意間使用 pull_up=None

例如:

button = Button(
    27,
    pull_up=None
)

但硬體又沒有 External Pull。

結果:

Input Floating

就可能出現亂跳。

因此本系列基礎按鍵:

使用預設 Internal Pull-up。

64. External Pull-down 範例

如果真的需要 External Pull-down:

3.3V
 │
Button
 │
GPIO
 │
10kΩ
 │
GND

則要依實際電路設定 GPIO Zero。

不過這屬於進階硬體設計。

本章核心仍使用 Internal Pull。

65. GPIO2 / GPIO3 要特別注意

Raspberry Pi 官方說明:

GPIO2
GPIO3

本身具有固定 Pull-up,常用於 I²C SDA/SCL。

因此一般 Button Project 我們優先選:

GPIO27
GPIO22

避免佔用主要 I²C Pin。

66. Button 長按偵測

GPIO Zero 還提供:

hold_time
when_held
hold_repeat

官方目前 Button 預設:

hold_time = 1 second

而 when_held 會在按住達到指定時間後觸發。

67. 長按 2 秒程式

from gpiozero import Button
from signal import pause

button = Button(
    27,
    hold_time=2
)


def held():
    print("Button held for 2 seconds")


button.when_held = held

pause()

如果按住超過:

2 sec

就觸發。

68. hold_repeat

如果:

hold_repeat=True

則按鍵持續按住時,可以每隔 hold_time 再觸發一次 Handler。GPIO Zero API 目前就是這樣定義 hold_repeat。

這非常適合:

按住 +
→ 持續加速

或:

按住 -
→ 持續減速

69. 本章進階專題:兩顆 Button

接下來把上一章的 8 顆 LED 跑馬燈加入:

Speed Up Button
+
Speed Down Button

使用:

跑馬燈速度控制按鍵 GPIO 配置
功能 BCM Physical
Speed Up GPIO20 Pin 38
Speed Down GPIO21 Pin 40

按鍵另一端都接:

GND

70. 為什麼選 GPIO20 / GPIO21?

上一章 8 顆 LED 已經使用:

17
27
22
23
24
25
5
6

因此本章選:

GPIO20
GPIO21

避免和 LED Pin 重疊。

而 GPIO20 / GPIO21 在目前這個專案中沒有被其他功能占用。

71. 8 顆 LED Pin Table

沿用上一章:

8 顆 LED GPIO 配置
LED BCM GPIO Physical
LED1 GPIO17 Pin 11
LED2 GPIO27 Pin 13
LED3 GPIO22 Pin 15
LED4 GPIO23 Pin 16
LED5 GPIO24 Pin 18
LED6 GPIO25 Pin 22
LED7 GPIO5 Pin 29
LED8 GPIO6 Pin 31

Button:

Speed Up / Speed Down 按鍵配置
Button BCM GPIO Physical
Speed + GPIO20 Pin 38
Speed - GPIO21 Pin 40

GND 可共用。

72. 完整硬體架構

Raspberry Pi 5

GPIO17 ─330Ω─ LED1 ─┐
GPIO27 ─330Ω─ LED2 ─┤
GPIO22 ─330Ω─ LED3 ─┤
GPIO23 ─330Ω─ LED4 ─┤
GPIO24 ─330Ω─ LED5 ─┤
GPIO25 ─330Ω─ LED6 ─┤── GND
GPIO5  ─330Ω─ LED7 ─┤
GPIO6  ─330Ω─ LED8 ─┘


GPIO20 ── Button + ── GND

GPIO21 ── Button - ── GND

Button 使用 Internal Pull-up。

73. 跑馬燈速度應該用什麼 Variable?

例如:

run_delay = 0.2

代表每顆 LED:

0.2 sec

切換。

如果按:

Speed +

應:

delay 變小

因為 Delay 越短:

跑越快

74. Speed Down 則相反

按:

Speed -

應:

delay 變大

例如:

0.1
→ 快

0.2
→ 正常

0.5
→ 慢

75. 要設定速度上下限

如果一直按加速:

delay
↓
0
↓
負數

會產生問題。

所以設定:

MIN_DELAY = 0.05
MAX_DELAY = 1.00

例如:

最快
=
0.05 sec

最慢
=
1.00 sec

76. 兩按鍵控制速度完整版

from gpiozero import LED, Button
from time import sleep

LED_PINS = [
    17,
    27,
    22,
    23,
    24,
    25,
    5,
    6
]

BUTTON_UP_PIN = 20
BUTTON_DOWN_PIN = 21

MIN_DELAY = 0.05
MAX_DELAY = 1.00
STEP = 0.05

run_delay = 0.20


def speed_up():
    global run_delay

    run_delay = max(
        MIN_DELAY,
        run_delay - STEP
    )

    print(
        f"Speed up: "
        f"{run_delay:.2f} s"
    )


def speed_down():
    global run_delay

    run_delay = min(
        MAX_DELAY,
        run_delay + STEP
    )

    print(
        f"Speed down: "
        f"{run_delay:.2f} s"
    )


def show_led(led):
    led.on()
    sleep(run_delay)
    led.off()


def all_off(leds):
    for led in leds:
        led.off()


def close_all(leds):
    for led in leds:
        led.close()


def main():
    leds = [
        LED(pin)
        for pin in LED_PINS
    ]

    button_up = Button(
        BUTTON_UP_PIN,
        bounce_time=0.05
    )

    button_down = Button(
        BUTTON_DOWN_PIN,
        bounce_time=0.05
    )

    button_up.when_pressed = (
        speed_up
    )

    button_down.when_pressed = (
        speed_down
    )

    print(
        "Running light started."
    )

    print(
        "Button + : speed up"
    )

    print(
        "Button - : speed down"
    )

    try:
        while True:
            for led in leds:
                show_led(led)

            for led in leds[-2:0:-1]:
                show_led(led)

    except KeyboardInterrupt:
        print(
            "\nProgram stopped."
        )

    finally:
        all_off(leds)

        button_up.close()
        button_down.close()

        close_all(leds)


if __name__ == "__main__":
    main()

77. 這支程式如何運作?

主 Loop:

LED1
 ↓
LED2
 ↓
LED3
 ↓
...
 ↓
LED8
 ↓
LED7
 ↓
...
 ↓
LED2

同時 GPIO Zero 監控:

Button +
Button -

當 Press Event 發生:

Button +
   ↓
speed_up()

或:

Button -
   ↓
speed_down()

78. Event Handler 可以和主 Loop 同時運作

這正是 Event-driven Programming 的價值。

程式不需要在主 Loop 每一步都寫:

if button_up.is_pressed:

而是:

button_up.when_pressed = speed_up

GPIO Zero 會在事件發生時呼叫 Handler。

79. 為什麼使用 bounce_time=0.05?

跑馬燈速度控制非常適合觀察 Bounce 問題。

如果沒有 Debounce,一按可能:

Speed up
Speed up
Speed up

連續觸發。

加入:

bounce_time=0.05

可以讓一次正常機械按壓比較穩定地被視為一次事件。GPIO Zero 的 bounce_time 就是用來忽略初次狀態變化後一小段時間內的快速變化。

80. global run_delay 是什麼?

Function 裡:

global run_delay

表示:

我要修改外面那個全域 run_delay Variable。

例如:

run_delay = 0.20

如果沒有:

global

Python 會把 Function 內的 Assignment 視為區域變數操作。

81. global 適合大型 Project 嗎?

小型教學程式可以使用。

大型 Project 更推薦:

  • Class
  • Shared State Object
  • Queue
  • Config Object

避免大量 Global Variable。

但本章重點是 GPIO Input 與 Event,所以先使用容易理解的方式。

82. max() 防止速度太快

run_delay = max(
    MIN_DELAY,
    run_delay - STEP
)

例如:

目前 0.05
再按 +

計算可能:

0.00

但:

max(0.05, 0.00)

結果仍:

0.05

83. min() 防止速度太慢

run_delay = min(
    MAX_DELAY,
    run_delay + STEP
)

如果:

目前 1.00

再按減速:

仍維持:

1.00

84. 如果想按住持續加速

可以:

button_up = Button(
    BUTTON_UP_PIN,
    bounce_time=0.05,
    hold_time=0.3,
    hold_repeat=True
)

然後:

button_up.when_pressed = speed_up
button_up.when_held = speed_up

GPIO Zero 支援 when_held 與 hold_repeat,可在按住達到 hold_time 後持續觸發。

85. 按住持續減速

同樣:

button_down = Button(
    BUTTON_DOWN_PIN,
    bounce_time=0.05,
    hold_time=0.3,
    hold_repeat=True
)

然後:

button_down.when_pressed = (
    speed_down
)

button_down.when_held = (
    speed_down
)

效果就像:

音量鍵

短按一次調一格,

長按則持續調整。

86. Button 完全沒有反應:先檢查 GPIO Number

例如:

Button(20)

代表:

BCM GPIO20

對應:

Physical Pin 38

不是:

Physical Pin 20

GPIO Zero 使用 BCM Numbering。

87. Physical Pin 20 是什麼?

Physical Pin 20 是:

GND

所以如果看到:

Button(20)

卻把線接:

Physical Pin 20

就完全搞錯。

因此仍然遵守:

Code
→ BCM Number

Wiring
→ BCM + Physical Number

88. Button 一直顯示 Pressed

可能原因:

  • Button 接錯腳
  • GPIO 一直短接 GND
  • 四腳 Button 同側接法錯
  • pull_up 設定和電路不匹配

例如 Internal Pull-up:

Button(27)

預期:

Released → HIGH
Pressed  → LOW

如果 GPIO 永遠接 GND:

永遠 Pressed

89. Button 永遠不會 Pressed

可能:

  • Button 沒有真正接 GND
  • Breadboard 插錯 Row
  • Button 方向放錯
  • GPIO Number 錯
  • Push Button 腳位判斷錯
  • Wire 接觸不良

先使用:

print(button.is_pressed)

建立最小測試。

90. Button 一直 True / False 亂跳

最常見:

Floating Input

例如使用:

Button(
    27,
    pull_up=None
)

卻沒有 External Pull Resistor。

所以要確認:

Internal Pull

真的存在。

91. Button 按一下卻觸發兩三次

這通常就是:

Bounce

先試:

button = Button(
    27,
    bounce_time=0.05
)

如果仍發生,可以依硬體情況適度調整。

GPIO Zero 的 bounce_time 就是針對這類短時間狀態變化提供 Software Debounce。

92. bounce_time 太大會怎樣?

例如:

bounce_time=1

表示第一次狀態變化後:

1 秒

內的其他變化都可能被忽略。

因此快速:

按一下
放開
再按

可能有部分事件被過濾。

所以 Debounce:

不是越大越好。

93. Debounce 也可以做 Hardware

除了:

Software Debounce

還可以使用:

  • RC Circuit
  • Schmitt Trigger
  • Hardware Debouncer

正式工業系統會依:

  • Noise
  • Response Time
  • Reliability

設計。

本章先使用:

bounce_time

了解概念。

94. Event Handler 不要做太久的 Blocking 工作

例如:

def pressed():
    sleep(10)

雖然簡單測試可以,但長時間 Blocking Handler 可能讓 Event Architecture 變得不易維護。

比較好的方式是:

Event Handler
→ 快速更新狀態

真正長時間工作由:

  • Main Loop
  • Worker
  • Thread
  • Queue

處理。

這會在更進階 IoT 程式中變得重要。

95. 本章按鍵安全原則

  1. GPIO Input 仍然只接受 3.3V 邏輯。
  2. 不要把 5V Button Signal 直接送進 GPIO。
  3. Internal Pull-up Button 直接接 GND。
  4. Pull-down Button 接 3.3V。
  5. 不使用 Internal Pull 時,要有 External Pull。
  6. 不讓 GPIO Input Floating。
  7. BCM / Physical Number 要分清楚。
  8. 接線前關機。
  9. 長線按鍵要考慮 Noise。
  10. 真正安全系統不能只依賴 Software。

96. 本章 GPIO Zero Button API 速查

GPIO Zero Button API 速查表
API 用途
Button(pin) 建立按鍵
is_pressed 是否按下
value Press=1 / Release=0
wait_for_press() 等待按下
wait_for_release() 等待放開
when_pressed Press Event
when_released Release Event
when_held Long Press Event
pull_up=True Internal Pull-up
pull_up=False Internal Pull-down
pull_up=None 不使用 Internal Pull
bounce_time Software Debounce
hold_time 長按判定時間
hold_repeat 長按重複事件
close() 釋放 Device

以上 Button API 與參數皆由目前 GPIO Zero Button 官方 API 提供。

97. Pull-up / Pull-down 快速對照

Pull-up / Pull-down 快速對照
模式 未按 按下 Button 另一側
Pull-up HIGH LOW GND
Pull-down LOW HIGH 3.3V

GPIO Zero:

Button(27)

預設 Pull-up。

Pull-down:

Button(
    27,
    pull_up=False
)

98. Polling / Event 快速對照

Polling

while True:
    if button.is_pressed:
        ...

適合:

  • 初學
  • 主迴圈控制
  • 需要每次循環一起處理

Event

button.when_pressed = handler

適合:

  • 人機輸入
  • 非同步觸發
  • 狀態改變才處理

99. Debounce 快速對照

沒有:

Button(27)

預設:

bounce_time=None

不做 Software Debounce。

有:

Button(
    27,
    bounce_time=0.05
)

在第一次狀態變化後短暫忽略快速變化。

100. 本章完整學習流程

GPIO Input
   ↓
Push Button
   ↓
Floating Input
   ↓
Pull-up
   ↓
Pull-down
   ↓
Button()
   ↓
is_pressed
   ↓
wait_for_press
   ↓
when_pressed
   ↓
when_released
   ↓
LED Control
   ↓
toggle()
   ↓
Contact Bounce
   ↓
Debounce
   ↓
bounce_time
   ↓
Two Buttons
   ↓
Speed Up / Down
   ↓
8 LED Running Light

101. 本章重點整理

完成本章後,應掌握:

  1. GPIO Input 是 Raspberry Pi 讀取外部數位狀態的方法。
  2. GPIO Input 可以讀 HIGH 與 LOW。
  3. Push Button 是最簡單的 Digital Input Device。
  4. Normally Open Button 未按時為 Open Circuit。
  5. Floating Input 沒有固定電位,容易受 Noise 影響。
  6. Pull-up / Pull-down 用來建立 Input 預設狀態。
  7. Raspberry Pi GPIO 支援內部 Pull-up / Pull-down。
  8. GPIO2 / GPIO3 有固定 Pull-up。
  9. Pull-up 未按為 HIGH,按下接 GND 後為 LOW。
  10. Pull-down 未按為 LOW,按下接 3.3V 後為 HIGH。
  11. GPIO Zero Button 預設 pull_up=True。
  12. 預設 Pull-up 時 Button 另一端接 GND。
  13. pull_up=False 時使用 Pull-down,Button 另一端接 3.3V。
  14. pull_up=None 不使用內部 Pull。
  15. GPIO Zero 使用 BCM Numbering。
  16. Button(27) 表示 BCM GPIO27。
  17. button.is_pressed Pressed 時回傳 True。
  18. button.value Pressed 時為 1。
  19. Polling 是主程式反覆讀取 Button 狀態。
  20. wait_for_press() 會等待按鍵 Press。
  21. wait_for_release() 會等待 Release。
  22. when_pressed 可以設定 Press Event Handler。
  23. when_released 可以設定 Release Event Handler。
  24. Event-driven Programming 不需要所有輸入都一直 Poll。
  25. Button Press 可以直接呼叫 led.on()。
  26. Button Release 可以直接呼叫 led.off()。
  27. led.toggle() 適合製作按一下切換狀態。
  28. 機械 Button 會發生 Contact Bounce。
  29. Bounce 可能讓一次 Press 被判定成多次事件。
  30. Debounce 用來降低機械 Bounce 的影響。
  31. GPIO Zero 提供 bounce_time。
  32. bounce_time=None 代表預設不做 Software Debounce。
  33. bounce_time 單位為秒。
  34. bounce_time=0.05 等於 50ms。
  35. Debounce 不宜設定過度長。
  36. Pull-up 與 Debounce 解決的是不同問題。
  37. hold_time 可以設定 Long Press 時間。
  38. when_held 可以處理長按事件。
  39. hold_repeat=True 可以持續產生 Hold Event。
  40. 兩顆 Button 可以做 Speed Up / Speed Down。
  41. Delay 越小,跑馬燈越快。
  42. Speed Control 應設定 Min / Max。
  43. Event Handler 可以修改主程式的控制狀態。
  44. 8 LED 跑馬燈可以同時接受 Button Input。
  45. Input 與 Output 整合後開始形成完整控制系統。
  46. Button 沒反應時先檢查 BCM / Physical Pin。
  47. Button 永遠 Pressed 可能是一直接 GND。
  48. Button 亂跳可能是 Floating。
  49. Button 一次觸發多次通常要檢查 Bounce。
  50. GPIO Input 一樣不能直接輸入 5V。

FAQ:Raspberry Pi GPIO Button 常見問題

Q1:Raspberry Pi Button 一定需要外接電阻嗎?

不一定。

Raspberry Pi GPIO 支援 Internal Pull-up / Pull-down,GPIO Zero Button 預設會啟用 Pull-up,因此簡單 Button 可直接接在 GPIO 與 GND 之間。

Q2:GPIO Zero Button 預設是 Pull-up 還是 Pull-down?

預設:

Pull-up

也就是:

Button(27)

等同使用內部 Pull-up。

Q3:Pull-up Button 要怎麼接?

GPIO
 │
Button
 │
GND

GPIO Zero 官方就是以這種方式定義 pull_up=True。

Q4:Pull-down Button 要怎麼接?

3.3V
 │
Button
 │
GPIO

程式:

button = Button(
    27,
    pull_up=False
)

Q5:為什麼 Pull-up 按下時 GPIO 是 LOW?

因為按下後 GPIO 被 Button 直接接到:

GND

所以電氣狀態變成 LOW。

GPIO Zero 會把 Active Low 邏輯封裝,因此:

button.is_pressed

仍然正常回傳 True。

Q6:什麼是 Floating Input?

Input 沒有 Pull-up、Pull-down 或其他明確外部訊號時,電位可能受到 Noise 影響而隨機變化。

因此 Digital Input 應建立穩定的 Default State。

Q7:如何讀取 Button 狀態?

例如:

if button.is_pressed:
    print("Pressed")

is_pressed 是 GPIO Zero Button 官方提供的布林狀態屬性。

Q8:如何等使用者按一下?

button.wait_for_press()

程式會等待直到 Press 或 Timeout。

Q9:如何在按鍵按下時自動執行 Function?

button.when_pressed = my_function

when_pressed 是 GPIO Zero Button 的 Event Handler。

Q10:按住 Button 亮 LED、放開關閉怎麼做?

button.when_pressed = led.on
button.when_released = led.off

就可以完成。

Q11:如何按一下切換 LED?

button.when_pressed = led.toggle

建議 Button 加上適當 bounce_time,避免 Bounce 造成一次按壓被判定多次。

Q12:什麼是 Button Bounce?

機械接點在按下或放開瞬間可能快速反覆接通/斷開。

Raspberry Pi 速度足以把這些短暫變化讀成多個事件。

Q13:GPIO Zero 如何 Debounce?

例如:

button = Button(
    27,
    bounce_time=0.05
)

bounce_time 代表初次狀態變化後忽略其他變化的秒數。

Q14:bounce_time 預設是多少?

預設:

None

也就是不進行 Software Bounce Compensation。

Q15:bounce_time=0.05 是多少時間?

0.05 秒
=
50 ms

Q16:bounce_time 是不是越大越好?

不是。

太大可能把使用者真正的快速連按也忽略掉。

應依實際 Button 與操作測試。

Q17:如何偵測長按?

例如:

button = Button(
    27,
    hold_time=2
)

button.when_held = held_function

GPIO Zero 支援 hold_time 與 when_held。

Q18:可以按住 Button 不斷觸發嗎?

可以設定:

hold_repeat=True

GPIO Zero 會依 hold_time 間隔重複執行 when_held Handler。

Q19:為什麼我的 Button 一直顯示 Pressed?

可能:

  • GPIO 永遠接 GND
  • Button 四腳方向接錯
  • Pull-up/Pull-down 和 Wiring 不一致
  • BCM Number 接錯

先使用最小 is_pressed 測試。

Q20:Button 可以接 5V 嗎?

Button 本身只是機械 Switch,但 Raspberry Pi GPIO Input 是 3.3V 邏輯。

不要建立:

5V
→ Button
→ GPIO

這種接法。

Pull-down 實驗應使用:

3.3V

而不是 5V。

結語

完成這一章之後,我們已經真正把 Raspberry Pi GPIO 的:

Input

與上一章的:

Output

組合在一起。

現在整個系統已經可以:

Human
  │
  ▼
Button
  │
  ▼
GPIO Input
  │
  ▼
Python
  │
  ▼
Decision
  │
  ▼
GPIO Output
  │
  ▼
LED

這個架構其實就是大量嵌入式系統、機器人與自動控制設備的基本模式:

Sense → Decide → Act

也就是:

感知
 ↓
判斷
 ↓
動作

本章甚至更進一步完成:

Button +
   │
   ▼
Speed Up

Button -
   │
   ▼
Speed Down

        ↓

8 LED Running Light

代表 Raspberry Pi 已經開始具有真正的:

人機介面能力。

到目前為止,我們控制的 LED 仍然只有:

ON
或
OFF

也就是純 Digital Output。

下一章要進入另一個非常重要的概念:

PWM

我們會讓 GPIO 不再只是:

HIGH
LOW

而是透過快速脈衝控制:

Brightness
Frequency
Duty Cycle

接著完成:

  • LED 調光
  • 呼吸燈
  • Buzzer
  • 不同音調
  • PWM 基本控制

正式開始理解 Raspberry Pi 的:

時間比例型輸出控制。

下一章

第 10 章 Raspberry Pi PWM 控制:LED 調光、呼吸燈與蜂鳴器實作

下一章將完整學習:

  • PWM 是什麼
  • Frequency
  • Period
  • Duty Cycle
  • 0%
  • 25%
  • 50%
  • 75%
  • 100%
  • GPIO Zero PWMLED
  • PWMLED(18)
  • value
  • LED Brightness
  • for Loop 漸亮
  • for Loop 漸暗
  • pulse()
  • 呼吸燈
  • Hardware PWM 與 Software PWM 基本差異
  • Raspberry Pi 5 PWM 注意事項
  • Active Buzzer
  • Passive Buzzer
  • TonalBuzzer
  • Frequency 與音調
  • 蜂鳴器警報
  • 溫度警報模擬
  • Button 調整 LED Brightness
  • Button 調整 PWM Duty Cycle
  • GPIO PWM 不直接驅動大功率負載
  • Transistor / MOSFET Driver
  • PWM 控制風扇/馬達 Driver 的正確架構

完成第 10 章後,我們的 Raspberry Pi GPIO 就會從:

Input + Digital Output

進一步擴充成:

Input
+
Digital Output
+
PWM Output

為後面的感測器、Servo、Relay、Motor 與完整 IoT 控制系統建立更完整的硬體基礎。

下一步學習

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

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

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

← 回文章列表