3.11. 去彈跳

開關在圖上被畫成一個完美的開合接點,但真實開關的接點並不會在兩種狀態之間乾淨俐落地切換。它們會抖動(chatter)──在穩定下來之前的數毫秒內反覆接通與斷開電氣接觸許多次。讀取該接腳的 GPIO 輸入會把這看成一連串的邊緣訊號;不夠謹慎的輪詢迴圈會把一次真實的按壓算成好幾次「按壓」,而中斷處理常式則會在實際一次按壓中執行好幾次。

一段理想化的示波器波形,顯示開關輸入訊號。 訊號一開始為高電位(開關斷開),下降至 低電位,在數毫秒內來回彈跳數次,最後 穩定在低電位(開關閉合)。

彈跳的開關在穩定下來之前會產生一連串快速的轉態。

去彈跳(debouncing)是過濾抖動的做法,讓每一次實體按壓都只登錄為單一事件。有兩種方式可以解決這個問題──軟體(韌體中的計時規則)或硬體(線路上的小型濾波器)。兩者並非互斥。

3.11.1. 軟體去彈跳

其概念是記住輸入上次變化的時間,並拒絕在該時間戳記後一小段時間窗內的後續變化。接點彈跳通常持續不到 10 ms;一次真實的按壓需要 50 -- 100 ms;30 -- 50 ms 的時間窗能攔下所有彈跳,又不會擋掉真實的按壓。

在輪詢迴圈中,讀取接腳、與上一次的穩定值比較,並且只在去彈跳時間窗過去後才接受變化:

import time
from machine import Pin

button = Pin("P0", Pin.IN, Pin.PULL_UP)
last_state  = 1
last_change = 0
DEBOUNCE_MS = 50

while True:
    now = time.ticks_ms()
    state = button.value()
    if state != last_state and time.ticks_diff(now, last_change) > DEBOUNCE_MS:
        last_change = now
        last_state = state
        if state == 0:
            do_action()
    time.sleep_ms(10)

對於中斷驅動的讀取,在處理常式內套用相同的計時規則,然後透過 micropython.schedule() 把真實的按壓交回主要環境處理(請參閱 GPIO 輸入):

import time
import micropython
from machine import Pin

button = Pin("P0", Pin.IN, Pin.PULL_UP)
last_irq = 0
DEBOUNCE_MS = 50

def handle_press(pin):
    do_action()

def on_press(pin):
    global last_irq
    now = time.ticks_ms()
    if time.ticks_diff(now, last_irq) < DEBOUNCE_MS:
        return
    last_irq = now
    micropython.schedule(handle_press, pin)

button.irq(handler=on_press, trigger=Pin.IRQ_FALLING)

ISR 依時間戳記過濾彈跳並將回呼函式排入佇列;handle_press 回到主要環境執行,在那裡進行記憶體配置與緩慢的 I/O 都是安全的。

3.11.2. 硬體去彈跳

硬體去彈跳在抖動到達接腳之前就以電氣方式將其過濾掉。標準的工具是電容器。

電容器是一種兩端的元件,用來儲存電荷。在物理上,它是兩片相隔一小段距離的導電板,中間以絕緣體(介電質)隔開。

一個電容器被畫成兩片水平平行的導電板, 兩板之間夾著介電質(絕緣體)。各有一條 引線將每片板連接到外部端子──A 在上方, B 在下方。當在端子間施加電壓 V 時, 大小相等、極性相反的電荷 +Q 與 -Q 會 累積在兩片板上。

平行板電容器:兩個導體之間以一層絕緣體隔開。

在端子間施加電壓會將大小相等、極性相反的電荷驅入兩片板;其關係為

Q = C × V

其中 Q 是儲存的電荷(庫侖),V 是電容器兩端的電壓,C 是其電容(法拉)。電容由元件的構造決定;電容越大,代表在相同電壓下儲存的電荷越多。

其結果是:電容器無法瞬間改變其電壓。流入或流出的電荷必須通過路徑上的任何電阻,而該電阻決定了電壓能以多快的速度變化。

3.11.2.1. RC 時間常數

透過電阻為電容器充電,會產生朝向供應電壓的平滑指數上升,而非一步階躍。該上升的特徵時間就是 RC 時間常數:

τ = R × C

經過一個 τ 之後,電容器已達到供應電壓的約 63 %。經過 5 個 τ 之後,超過 99 %──實務上可視為「完全充電」。

一張圖顯示電容器的電壓沿著指數曲線 從 0 V 朝供應軌上升。時間 τ = RC 標示在 x 軸上,即曲線達到供應電壓 63 % 之處。

電容器沿著指數曲線充電。τ = RC 是達到最終電壓 63 % 所需的時間。

透過電阻放電則遵循鏡像的情形:電壓從初始值沿指數下降趨向零,在一個 τ 後降至起始電壓的 37 %,在 5 個 τ 後降至不到 1 %。

一張圖顯示電容器的電壓沿著指數曲線 從 Vmax 朝 0 V 下降。時間 τ = RC 標示在 x 軸上,即曲線降至起始電壓 37 % 之處。

電容器沿著指數衰減放電。τ = RC 是降至起始電壓 37 % 所需的時間。

3.11.2.2. 去彈跳電路

在輸入接腳與接地之間接一個電容器,並透過一個串聯電阻供電,就構成了一個低通濾波器。快速的尖峰來不及透過該電阻為電容器充放電;接腳維持在尖峰出現前的電壓附近。緩慢的變化──刻意的按壓──則會為電容器充放電,讀數也隨之跟上。

R1 將開關的高側上拉到 Vcc,產生一個會彈跳的原始開關訊號。R2C 接著將該訊號低通濾波後送入接腳:

一個帶有硬體去彈跳的開關輸入。Vcc 透過 一個 10 kΩ 的上拉電阻向下連到一個接點。 該接點一條分支經過開關連到接地,另一條 分支則透過一個 10 kΩ 的串聯電阻連到 Pin。一個 100 nF 的電容器接在 Pin 與 接地之間,完成這個低通濾波器。

硬體去彈跳:R2C 在原始開關訊號到達接腳之前先將其低通濾波。

典型值:R1 = 10 (上拉)、R2 = 10 (串聯)、C = 100 nF

當開關斷開時,電流流經 Vcc → R1R2 → 電容(串聯),以 τ_charge = (R1 + R2) × C = 2 ms 將電容充電至 Vcc。

當開關閉合時,開關節點被箝位到接地,電容僅透過 R2 向該接地放電,τ_discharge = R2 × C = 1 ms

兩個邊緣都經過 RC 濾波。由於電容位於自己的節點上,且在開關側的 R2 下游,因此它能在 Vcc(斷開)與 0 V(閉合)之間乾淨地擺動──兩種情況在穩態下都不需要有電流流經 R1

3.11.3. 在兩者之間做選擇

  • 軟體是預設做法。它在零件上毫無成本,閾值容易調校,而且適用於 CPU 所讀取的任何接腳。

  • 硬體在彈跳會到達 CPU 輪詢程式以外的某處時才值得付出零件成本──例如不能重複觸發的中斷、硬體計數器,或本身沒有濾波功能的周邊裝置。

軟體與硬體去彈跳也能和平共存:一個小型 RC 濾波器抑制最嚴重的尖峰,而軟體去彈跳時間窗則涵蓋剩下的部分。