Learning Objectives
- 說明輸入腳位為什麼需要上拉或下拉電阻,以及 gpiozero 的 Button 預設怎麼接
- 用 is_pressed 輪詢按鈕狀態,讓 LED 跟著按鈕亮暗
- 用 when_pressed、when_released 指定事件處理函式,並說明為什麼函式名稱後面不加括號
- 說明按鈕彈跳造成的問題,並用時間間隔去除彈跳
讓程式知道外面發生了什麼事
上一課的 LED 是輸出:程式決定腳位的電壓。這一課反過來,把腳位設成輸入:程式讀取腳位上的電壓, 判斷是高電位(約 3.3V,讀到 1)還是低電位(約 0V,讀到 0)。按鈕、開關、人體感測器都是這樣接上來的。
這一課要回答:
- 按鈕只有兩支腳,為什麼還要談「上拉電阻」?
- 程式要一直問「按了沒?」,還是讓按鈕按下時通知程式?
- 明明只按一次,程式為什麼可能算成好幾次?
浮接與上拉電阻
按鈕的作用只是「接通」或「斷開」。如果把按鈕接在 GPIO 與 GND 之間:
- 按下:GPIO 接到 GND,讀到 0。
- 放開:GPIO 什麼都沒接,叫做浮接(floating)。這時讀到的值不固定,手靠近、旁邊的電線有訊號都可能讓它亂跳。
解決方法是加一顆上拉電阻(pull-up resistor):用一顆電阻把 GPIO 接到 3.3V。放開時,電阻把電壓「拉」到 3.3V,穩定讀到 1; 按下時,GPIO 直接接到 GND,讀到 0(電阻限制了從 3.3V 流到 GND 的電流)。反過來接到 GND 的叫下拉電阻(pull-down)。
樹莓派的 GPIO 晶片裡就有可以用程式開啟的上拉、下拉電阻,所以不用另外接。gpiozero 的 Button 預設 pull_up=True:
| 接法 | 程式 | 放開 | 按下 | is_pressed |
|---|---|---|---|---|
| 按鈕接在 GPIO 與 GND 之間 | Button(27)(預設 pull_up=True) |
高電位 | 低電位 | 按下時 True |
| 按鈕接在 GPIO 與 3.3V 之間 | Button(27, pull_up=False) |
低電位 | 高電位 | 按下時 True |
不管哪一種接法,is_pressed 都是「按下時為 True」:gpiozero 依你的接法幫你換算好,不用自己記「按下是 0 還是 1」。
GPIO27(實體第 13 腳)──[ 按鈕 ]── GND(實體第 14 腳) 內部上拉到 3.3V
GPIO17(實體第 11 腳)──[ 330Ω ]──▶|── GND 上一課的 LED
按鈕不要接在 GPIO 與 5V 之間:GPIO 不耐 5V。
逐步看:輪詢,LED 跟著按鈕
按鈕的時間表設定成在 0.6~1.3 秒、1.9~2.4 秒被按下(電路畫面上的按鈕會變色)。 程式每 0.1 秒問一次「按了沒?」,這叫輪詢(polling)。注意 LED 亮起來的時間:按鈕在 0.6 秒按下, 程式要到下一次檢查才發現,所以 LED 稍微晚一點才亮。
事件:按下時通知我
輪詢的程式整天都在問「按了沒?」。另一種寫法是事件(event):先告訴 gpiozero「按下的時候,請呼叫這個函式」,
之後程式就可以去做別的事(或用 pause() 等著)。
button.when_pressed = pressed # 指定函式:不加括號
button.when_pressed = pressed() # 錯誤:這是「現在就呼叫」,把回傳值 None 指定給 when_pressed
pressed 是函式物件本身;pressed() 是呼叫它並得到回傳值。被交給別人、等事件發生時才呼叫的函式,叫做回呼函式(callback)。
也可以直接交出物件的方法,例如 button.when_pressed = led.on,按下時就會執行 led.on()。
在模擬器裡,事件處理函式會在
sleep()或pause()等待的期間被呼叫;真的 gpiozero 用另一個執行緒處理事件,概念相同。
逐步看:when_pressed 與 when_released
程式設定好兩個事件就停在 pause()。看步驟的跳躍:每次按下或放開,執行就從 pause() 跳進對應的函式,做完再回去等。按鈕在 0.5、1.5、2.3 秒被按下。
按鈕彈跳
按鈕裡是兩片金屬接點。按下的瞬間,接點會碰到、彈開、再碰到,在很短的時間內(通常是毫秒等級)來回好幾次, 叫做彈跳(bounce)。人感覺不到,但程式反應很快,會把一次按下當成好幾次。
理想的按下: ‾‾‾‾‾‾‾‾\________________/‾‾‾‾‾
實際的按下: ‾‾‾‾‾‾‾‾\_/‾\_/‾\_________/‾‾‾‾‾
↑ 彈跳:程式可能看到 3 次按下
去彈跳(debounce)的常見做法是:偵測到一次之後,一小段時間內的變化都不算。gpiozero 的 Button 有 bounce_time 參數
(單位是秒)做這件事,例如 Button(27, bounce_time=0.05)。下面的範例自己用時間間隔實作,讓你看到原理
(這個模擬器目前沒有模擬 bounce_time 參數)。也可以在硬體上加電容濾掉彈跳,「自造者入門」軌第 07 課會介紹。
逐步看:一次按下被算成三次
按鈕的時間表模擬了彈跳:0.4 秒按下時接點彈了三次(400、405、413 毫秒),1.5 秒按下時彈了兩次。
raw 照單全收,real 只算和上一次相隔 50 毫秒以上的那一次。看最後的輸出:raw 是 5,real 是 2。
小測驗:上拉電阻與讀值
按鈕接在 GPIO27 與 GND 之間,程式寫 button = Button(27)。按鈕放開時,GPIO27 的電位是?
同樣的接法,按鈕按下時 button.is_pressed 是什麼?
按鈕接在 GPIO27 與 3.3V 之間,程式應該怎麼寫?
小測驗:事件與回呼函式
下面的程式(不需要硬體)模擬「設定回呼函式」,會印出什麼?
def hello():
print("hi")
return 5
handler = hello
result = hello()
print(handler is hello, result)
寫成 button.when_pressed = pressed()(多了括號)會發生什麼事?
重點整理
- 輸入腳位不能浮接:用上拉電阻(接 3.3V)或下拉電阻(接 GND)給它一個確定的電位;樹莓派晶片內建可開關的上拉、下拉電阻。
Button(27)預設pull_up=True:按鈕接 GPIO 與 GND;is_pressed不論接法都是「按下時為 True」。- 輪詢:迴圈裡反覆讀
is_pressed;事件:when_pressed = 函式(不加括號),用pause()等待。 - 彈跳會讓一次按下被算成好幾次;用
bounce_time、時間間隔或硬體濾波去除。
下一課用 PWM 調整 LED 的亮度,讓蜂鳴器發出不同的音高。