3.11. ביטול ריטוט (Debouncing)

מתג מצויר כמגע מושלם של פתוח-או-סגור, אך המגעים של מתג אמיתי אינם עוברים באופן נקי בין שני המצבים. הם מרטטים – יוצרים ומנתקים מגע חשמלי פעמים רבות בתוך מספר מילישניות לפני שהם מתייצבים. כניסת GPIO שקוראת את הפין רואה זאת כצרור של קצוות; לולאת תשאול לא זהירה סופרת מספר ”לחיצות“ עבור לחיצה אמיתית אחת, ומטפל פסיקה רץ מספר פעמים עבור כל לחיצה ממשית.

An idealised scope trace showing a switch input signal. The signal starts high (open switch), drops low, bounces back and forth several times within a few milliseconds, then settles low (closed switch).

מתג מרטט מייצר צרור של מעברים מהירים לפני שהוא מתייצב.

ביטול ריטוט היא הפעולה של סינון הריטוט כך שכל לחיצה פיזית נרשמת כאירוע יחיד. שתי גישות פותרות זאת – תוכנה (כלל תזמון בקושחה) או חומרה (מסנן קטן על החוט). הן אינן סותרות זו את זו.

3.11.1. ביטול ריטוט בתוכנה

הרעיון הוא לזכור מתי הכניסה השתנתה לאחרונה ולדחות שינויים נוספים בתוך חלון קצר מאותה חותמת זמן. ריטוט מגע נמשך בדרך כלל פחות מ-10 מ“ש; לחיצה אמיתית אורכת 50 – 100 מ“ש; חלון של 30 – 50 מ“ש לוכד את כל הריטוטים מבלי לחסום לחיצות אמיתיות.

בלולאת תשאול, קרא את הפין, השווה לערך היציב האחרון, וקבל שינוי רק לאחר שחלון ביטול הריטוט חלף:

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 מסנן ריטוטים לפי חותמת זמן ומציב את פונקציית ה-callback בתור; handle_press רץ בחזרה בהקשר הראשי, שם הקצאה וקלט/פלט איטי בטוחים.

3.11.2. ביטול ריטוט בחומרה

ביטול ריטוט בחומרה מסנן את הריטוט בצורה חשמלית, לפני שהוא מגיע בכלל אל הפין. הכלי הסטנדרטי הוא קבל.

קבל הוא רכיב בעל שני הדקים המאחסן מטען חשמלי. פיזית, מדובר בשתי לוחיות מוליכות המוחזקות במרחק קצר זו מזו, מופרדות על ידי מבודד (הדיאלקטרי).

A capacitor drawn as two parallel horizontal plates with a dielectric (insulator) between them. A lead connects each plate to an external terminal -- A on top, B on bottom. Equal and opposite charges +Q and -Q accumulate on the two plates when a voltage V is applied across the terminals.

קבל לוחיות מקבילות: שני מוליכים מופרדים על ידי שכבה מבודדת.

הפעלת מתח על פני ההדקים שלו מניעה מטענים שווים ונגדיים אל שתי הלוחיות; היחס הוא

Q = C × V

כאשר Q הוא המטען המאוחסן (קולון), V הוא המתח על פני הקבל, ו-C היא הקיבול שלו (פאראד). הקיבול נקבע על ידי מבנה ההתקן; קיבול גבוה יותר פירושו אחסון מטען רב יותר באותו מתח.

התוצאה: קבל אינו יכול לשנות את מתחו באופן מיידי. מטען הזורם פנימה או החוצה חייב לעבור דרך כל התנגדות שנמצאת בנתיב, והתנגדות זו קובעת באיזו מהירות המתח יכול להשתנות.

3.11.2.1. קבוע הזמן RC

טעינת קבל דרך נגד מייצרת עלייה מעריכית חלקה לעבר מתח האספקה, ולא מדרגה. הזמן האופייני של עלייה זו הוא קבוע הזמן RC:

τ = R × C

לאחר τ אחד, הקבל הגיע לכ-63 % ממתח האספקה. לאחר 5 τ, הוא מעל 99 % – ”טעון במלואו“ לצרכים מעשיים.

A graph showing a capacitor's voltage rising along an exponential curve from 0 V toward the supply rail. The time τ = RC is marked on the x-axis where the curve reaches 63 % of the supply voltage.

קבל נטען לאורך עקומה מעריכית. τ = RC הוא הזמן להגיע ל-63 % מהמתח הסופי.

פריקה דרך נגד עוקבת אחר התמונה ההפוכה: המתח יורד מעריכית מערכו ההתחלתי לעבר אפס, צונח ל-37 % ממתח ההתחלה לאחר τ אחד, ולמתחת ל-1 % לאחר 5 τ.

A graph showing a capacitor's voltage falling along an exponential curve from Vmax toward 0 V. The time τ = RC is marked on the x-axis where the curve drops to 37 % of the starting voltage.

קבל נפרק לאורך דעיכה מעריכית. τ = RC הוא הזמן לרדת ל-37 % ממתח ההתחלה.

3.11.2.2. מעגל ביטול הריטוט

קבל בין פין כניסה לבין אדמה, המוזן דרך נגד טורי, יוצר מסנן מעביר נמוכים. קוצים מהירים אינם מספיקים לטעון או לפרוק את הקבל דרך אותו נגד; הפין נשאר קרוב לכל מתח שהיה בו לפני הקוץ. שינויים איטיים – לחיצה מכוונת – טוענים או פורקים את הקבל והקריאה עוקבת.

R1 מושך את הצד הגבוה של המתג מעלה אל Vcc, ומייצר אות מתג גולמי שמרטט. R2 ו-C לאחר מכן מסננים-מעביר-נמוכים את האות הזה אל הפין:

A switch input with hardware debouncing. Vcc connects through a 10 kΩ pull-up resistor down to a junction. That junction connects to ground through the switch on one branch, and through a 10 kΩ series resistor to the Pin on the other branch. A 100 nF capacitor between Pin and ground completes the low-pass filter.

ביטול ריטוט בחומרה: R2 ו-C מסננים-מעביר-נמוכים את אות המתג הגולמי לפני שהוא מגיע אל הפין.

ערכים טיפוסיים: R1 = 10 (משיכה-מעלה), R2 = 10 (טורי), C = 100 nF.

כאשר המתג פתוח, זרם זורם Vcc → R1R2 → קבל (בטור), וטוען את הקבל ל-Vcc עם τ_charge = (R1 + R2) × C = 2 ms.

כאשר המתג נסגר, צומת המתג נצמד לאדמה והקבל מתנקז דרך R2 בלבד אל אותה אדמה עם τ_discharge = R2 × C = 1 ms.

שני הקצוות מסוננים-RC. מכיוון שהקבל יושב על הצומת שלו עצמו, במורד הזרם מ-R2 מהמתג, הוא מתנדנד בצורה נקייה בין Vcc (פתוח) ל-0 V (סגור) – שום זרם אינו צריך לזרום דרך R1 במצב יציב באף אחד מהמקרים.

3.11.3. בחירה ביניהם

  • תוכנה היא ברירת המחדל. היא אינה עולה דבר ברכיבים, הסף קל לכוונון, והיא עובדת על כל פין שהמעבד קורא.

  • חומרה שווה את הרכיבים כאשר הריטוט מגיע למשהו אחר מלבד קוד התשאול של המעבד – פסיקה שאסור לה להיפלט פעמיים, מונה חומרה, התקן היקפי ללא מסנן משלו.

ביטול ריטוט בתוכנה ובחומרה גם דרים יחד בשלום: מסנן RC קטן מדכא את הקוצים החמורים ביותר, וחלון ביטול ריטוט בתוכנה מכסה את מה שנותר.