Learning Objectives
- 用 Pin.IN 與 Pin.PULL_UP 讀取按鈕,說明為什麼按下時讀到 0
- 說明浮接腳位的問題,以及上拉、下拉電阻的作用
- 分辨輪詢與中斷兩種讀取方式,用 Pin.irq 在按下的瞬間執行函式
- 說明按鈕彈跳的現象,用 ticks_ms 寫出軟體去彈跳
讓程式知道外面發生了什麼事
上一課把腳位設成輸出,程式控制 LED。這一課反過來:把腳位設成輸入,讀取外面的電壓, 程式就知道按鈕有沒有被按下。按鈕是最簡單的輸入裝置,之後的磁簧開關、傾斜開關、人體感測器,讀法都一樣。
這一課要回答:
- 按鈕怎麼接?為什麼按下時讀到的是 0,不是 1?
- 程式要「一直去看」按鈕,還是讓按鈕「通知」程式?
- 明明只按一下,為什麼程式算到好幾下?
輸入腳位只認兩種電壓
設成輸入的腳位會把外面的電壓判斷成 1(接近 3.3V)或 0(接近 0V):
from machine import Pin
button = Pin(4, Pin.IN) # GPIO4 設成輸入
print(button.value()) # 讀到 1 或 0
什麼都沒接的腳位:浮接
按鈕只是一個開關:按下時兩端接通,放開時斷開。如果按鈕一端接 GPIO4、另一端接 GND, 放開時 GPIO4 什麼都沒接,這叫浮接(floating)。浮接的腳位會受到雜訊、手靠近、旁邊的導線影響, 讀到的值忽 0 忽 1,程式會以為有人一直在按。
解決方法是加一顆電阻,讓腳位在按鈕放開時有確定的電位:
| 接法 | 電阻接到 | 放開時讀到 | 按下時讀到 |
|---|---|---|---|
| 上拉電阻(pull-up),按鈕接 GND | 3.3V | 1 | 0 |
| 下拉電阻(pull-down),按鈕接 3.3V | GND | 0 | 1 |
ESP32 的腳位內建上拉與下拉電阻,用程式開啟就好,不必另外接:
button = Pin(4, Pin.IN, Pin.PULL_UP) # 開啟內建上拉電阻
這一課都用「按鈕接 GPIO 與 GND,開啟內建上拉」的接法,所以按下=0、放開=1,邏輯是反過來的,寫程式時要記得。 上拉與下拉的電路原理,「自造者入門」軌第 06 課有完整說明。
ESP32 的 GPIO34~39 只能當輸入,而且沒有內建上拉、下拉電阻;接按鈕到這幾支腳位時,要自己接一顆外部電阻(常用 10kΩ)。
逐步看:輪詢按鈕,按下就亮
按鈕在 0.65 秒按下、1.35 秒放開(上方電路的按鈕會變色)。程式每 0.1 秒讀一次按鈕, 注意程式在下一次讀取時才發現按鈕的變化,所以印出的時間會比真正按下的時間晚一點。
輪詢與中斷
輪詢很直覺,但有兩個問題:
- 可能漏掉:每 0.1 秒才看一次,按下又放開只花 0.05 秒的話,兩次讀取之間就錯過了;程式還在
sleep裡做別的事,就更容易漏掉(第 07 課會詳細討論)。 - 反應有延遲:最差的情況要等一整個間隔才發現。
另一種方式是中斷(interrupt):由硬體監看腳位,電位一改變就暫停主程式,去執行你指定的函式, 執行完再回到主程式剛才的地方。電位改變的那一瞬間叫邊緣(edge):
| 名稱 | 電位變化 | 在上拉接法中代表 |
|---|---|---|
下降邊緣 Pin.IRQ_FALLING |
1 → 0 | 按下 |
上升邊緣 Pin.IRQ_RISING |
0 → 1 | 放開 |
def on_press(pin): # 中斷處理函式:參數是觸發的 Pin 物件
...
button.irq(trigger=Pin.IRQ_FALLING, handler=on_press)
兩種都要偵測時寫 trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING。
中斷處理函式(ISR)要短:主程式被暫停了,處理函式應該只做最少的事,例如把計數加一、設一個旗標,
複雜的工作(印很多資料、連網路、長時間 sleep)留給主程式看到旗標再做。
要修改函式外面的變數,記得寫 global。
真的 ESP32 上,中斷可能發生在主程式任意兩個指令之間;本站的模擬器簡化成在
sleep或呼叫硬體函式時才執行處理函式,觀念相同。
逐步看:用中斷計算按了幾次
主程式只是每秒印一次計數,從來沒有讀按鈕;按鈕按下的那一刻(0.3、1.4、2.2 秒),
程式會「跳」到 on_press 執行,再回到 sleep 繼續等。注意左上角的時間。
按一下,算成好幾下:彈跳
按鈕裡面是兩片金屬接點。按下的瞬間,接點會碰到、彈開、再碰到,來回幾次才穩定, 這叫彈跳(bounce)。人的手感覺只按了一下,但電位其實在很短的時間內 1、0、1、0 跳了好幾次:
按鈕電位 1 ───┐ ┌┐ ┌──────────┐
└─┘└─┘ (按住)
↑ ↑ ↑
三次下降邊緣,只有第一次是真的
輪詢每 0.1 秒才讀一次,通常不會受影響;中斷反應很快,每一次彈跳都會觸發,計數就多了。
軟體去彈跳:記下上一次「算數」的時間,距離上一次太近(例如 50 ms 以內)的邊緣當成彈跳,直接忽略。 彈跳時間依按鈕而不同,50 ms 是常見的起點,實際要看你的按鈕調整。 也可以在硬體上加電容濾掉彈跳,「自造者入門」軌第 07 課會介紹。
下一個範例的按鈕故意設成會彈跳:第一次按下時電位跳了三次,第二次跳了兩次。
逐步看:軟體去彈跳
time.ticks_ms() 讀開機後經過的毫秒數,time.ticks_diff(a, b) 算 a 比 b 晚多少毫秒(第 07 課會說明為什麼不直接相減)。
看每一次中斷時 now 與 last_ms 差多少。
小測驗:上拉電阻與接線
按鈕一端接 GPIO4、另一端接 GND,程式寫 Pin(4, Pin.IN, Pin.PULL_UP)。按住按鈕時 value() 讀到什麼?
同樣的接法,但程式寫成 Pin(4, Pin.IN),沒有開啟上拉。放開按鈕時會怎樣?
想把按鈕接到 GPIO35,下面哪個說法正確?
小測驗:中斷與去彈跳
下面模擬去彈跳的程式:edges 是幾個下降邊緣發生的時間(毫秒),會印出什麼?
edges = [100, 104, 109, 400, 403, 900]
last = -1000
presses = 0
for now in edges:
if now - last > 50:
presses += 1
last = now
print(presses)
按鈕用上拉接法,想在放開按鈕的那一刻執行函式,trigger 要寫什麼?
下面哪一件事不適合放在中斷處理函式裡?
重點整理
Pin(編號, Pin.IN, Pin.PULL_UP):設成輸入並開啟內建上拉電阻;按鈕接 GPIO 與 GND 時,按下=0、放開=1。- 什麼都沒接的輸入腳位會浮接,讀值不穩定;上拉、下拉電阻讓它有確定的電位。GPIO34~39 沒有內建上拉、下拉。
- 輪詢:在迴圈裡每隔一段時間讀一次,簡單,但有延遲、可能漏掉很短的按壓。
- 中斷:
pin.irq(trigger=…, handler=函式),在下降(1→0)或上升(0→1)邊緣立刻執行處理函式;處理函式要短,修改外面的變數要寫global。 - 機械按鈕會彈跳:一次按下產生好幾個邊緣。軟體去彈跳的做法是記下上次算數的時間,太近的邊緣忽略。
下一課用 PWM 調整 LED 亮度、讓蜂鳴器發出不同音高、控制伺服馬達的角度。