פסיקות
אם התוכנית שלנו עסוקה בעבודה ארוכה, מתי היא תבחין שהלחצן נלחץ? עד עכשיו התוכנית היא שהלכה לבדוק. בפרק הזה נהפוך את התפקידים: ההדק עצמו עוצר את התוכנית לרגע, מריץ פונקציה קצרה, ואז התוכנית חוזרת לעבודתה.
- נסביר למה סקר מפספס אירועים קצרים, ואיך פסיקה פותרת את זה.
- נקשור פונקציה להדק בעזרת
Pin.irq, ונכתוב מטפל קצר שמרים דגל. - נטפל בקפיצות מגע בעזרת
ticks_diff. - נשתמש בפסיקת טיימר ובכלב שמירה, ונבנה מד תדר פשוט.
5.1 למה סקר אינו מספיק (Polling)
החלופה היא סקר, כלומר התוכנית קוראת את ההדק שוב ושוב בלולאה. אבל היא אינה רואה מה שקורה בין שתי קריאות. בניסוי הבא יש שלוש לחיצות, וכל אחת נמשכת 0.2 s. הפקודה time.sleep(1) מייצגת כל עבודה שתופסת את התוכנית שנייה שלמה:
import board
import time
from machine import Pin
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
count = 0
last = 1
for i in range(5):
now = btn.value()
if last == 1 and now == 0:
count += 1
last = now
time.sleep(1)
print("presses counted:", count)presses counted: 0
5.2 Pin.irq וכללי המטפל
מחברים את הלחצן בין ההדק לבין GND, עם נגד המשיכה הפנימי. כך יש לשינוי שני קצוות: קצה יורד מ-1 ל-0 בלחיצה, וקצה עולה מ-0 ל-1 בשחרור.
board.BTN בוחר את ההדק הנכון.המטפל קוטע את התוכנית בכל שורה שהיא, ולכן אנחנו מקפידים על הכללים שהתיעוד קובע:
- קצר ומהיר: משנים משתנה ויוצאים. בלי
sleepובלי לולאות המתנה, ובליprintארוך, כי ההדפסה איטית וזמנה אינו קבוע. - בלי הקצאת זיכרון בפסיקה קשיחה: לא רשימה חדשה, לא מחרוזת, לא מספר עשרוני ולא
append. - משתנה משותף: יוצרים אותו לפני שמפעילים את הפסיקה, והוא מספר שלם או ערך בוליאני.
פסיקה קשיחה רצה מיד, בהשהיה מינימלית וללא הקצאת זיכרון. פסיקה רכה מתוזמנת לביצוע בקרוב, ולכן מותר לה להקצות זיכרון, וההשהיה שלה גדולה יותר. כדי לבקש פסיקה קשיחה מעבירים ל-Pin.irq את הארגומנט hard=True, והתיעוד אומר שחלק מהפורטים אינם תומכים בו. לכן כותבים כל מטפל כאילו הוא קשיח, וכך הוא עובד בשני המקרים.
המטפל מקבל ארגומנט אחד, אובייקט ההדק שגרם לפסיקה. את המשתנים שלו מציינים ב-global, אחרת הם נחשבים משתנים מקומיים ושום דבר בתוכנית לא משתנה. ובלי trigger פועלים שני הקצוות יחד, לפי התיעוד.
את העבודה הארוכה משאירים ללולאה: המטפל מרים דגל, שהוא משתנה בוליאני, והלולאה רואה אותו ופועלת. הנה מונה לחיצות, עם אותן לחיצות ואותה עבודה עמוסה:
import board
import time
from machine import Pin
pressed = False
count = 0
def on_press(pin):
global pressed, count
pressed = True
count += 1
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
btn.irq(handler=on_press, trigger=Pin.IRQ_FALLING)
while True:
if pressed:
pressed = False
led.value(not led.value())
print("press number", count, " led =", led.value())
time.sleep_ms(50)press number 1 led = 1 press number 2 led = 0 press number 3 led = 1 ...
5.3 קפיצות מגע (Contact Bounce)
כשסוגרים את הלחצן, החלקים שלו נוגעים ומתנתקים כמה פעמים בזמן קצר לפני שהמגע מתייצב, וכך גם בשחרור. אלה קפיצות מגע, והפסיקה רואה כל קצה מהן.
הכלל: כל קצה מחדש את חותמת הזמן. כשעובר DEBOUNCE_MS בלי קצה חדש, התוכנית הראשית קוראת את ההדק: אם הוא 0 זו לחיצה, ואם הוא 1 הקצוות באו מקפיצות השחרור. את הזמן מחשבים בפונקציה time.ticks_diff, כי מונה המילישניות חוזר לאפס כשהוא מגיע לסופו, וחיסור רגיל טועה בנקודת החזרה. התעלמות מקצוות במשך זמן קבוע אחרי כל קבלה אינה מספיקה: קפיצות השחרור בלחיצה ארוכה מגיעות אחריה, וייספרו כלחיצה שנייה.
import board
import time
from machine import Pin
DEBOUNCE_MS = 30
edges = presses = stamp = 0
pending = False
def on_edge(pin):
global edges, stamp, pending
edges += 1
stamp = time.ticks_ms()
pending = True
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
btn.irq(handler=on_edge, trigger=Pin.IRQ_FALLING)
for i in range(240):
quiet = time.ticks_diff(time.ticks_ms(), stamp)
if pending and quiet >= DEBOUNCE_MS:
pending = False
if btn.value() == 0:
presses += 1
time.sleep_ms(5)
print("edges:", edges, " presses:", presses)edges: 10 presses: 2
5.4 פסיקת טיימר (Timer Interrupt)
גם הטיימר מייצר פסיקה, בכל פרק זמן קבוע או פעם אחת. מספר הטיימר שונה בין שני הלוחות: 0 ב-ESP32, ואילו ב-Pico W הוא -1. board.timer() מסתירה את ההבדל.
import board
import time
from machine import Pin, Timer
ticks = 0
led = Pin(board.LED, Pin.OUT)
def on_tick(t):
global ticks
ticks += 1
led.value(ticks % 2)
tim = board.timer()
tim.init(period=500, mode=Timer.PERIODIC, callback=on_tick)
for i in range(3):
time.sleep(1)
print("ticks =", ticks)ticks = 2 ticks = 4 ticks = 6
hard=True גורם ל-ValueError. ובכלל, ברירת המחדל משתנה מפורט לפורט. לכן הכלל שלנו נשאר: כותבים את המטפל כאילו הוא קשיח.5.5 מד תדר פשוט (Frequency Meter)
סופרים N קצוות עולים בחלון זמן באורך T, ולכן התדר הוא f = NT.
קריאת המונה ואיפוסו הם שני צעדים שחייבים להתבצע יחד. אילו הגיעה פסיקה ביניהם, קצה היה הולך לאיבוד, ולכן מגנים עליהם בקטע קריטי: disable_irq() עוצר את כל הפסיקות, ו-enable_irq(state) מחזיר אותן. משאירים ביניהם כמה שפחות קוד.
import board
import time
from machine import Pin, disable_irq, enable_irq
pulses = 0
def on_edge(pin):
global pulses
pulses += 1
sig = Pin(board.BTN, Pin.IN)
sig.irq(handler=on_edge, trigger=Pin.IRQ_RISING)
for i in range(4):
time.sleep(1)
state = disable_irq()
n = pulses
pulses = 0
enable_irq(state)
print("f = %d Hz" % n)f = 100 Hz f = 100 Hz f = 50 Hz f = 50 Hz
5.6 כלב שמירה (Watchdog)
מה אם התוכנית נתקעה ואיש אינו ליד הלוח? כלב שמירה הוא מונה עצמאי שמפעיל מחדש את הלוח אם לא האכלנו אותו בפקודה feed() לפני שהזמן שהוקצב נגמר.
import time
from machine import WDT
wdt = WDT(timeout=2000)
for i in range(2):
time.sleep(1)
wdt.feed()
print("fed", i)
print("the program hangs here")
while True:
time.sleep(1)fed 0 fed 1 the program hangs here [watchdog: the board resets] ...
wdt.feed() בתוך מטפל של טיימר: הטיימר ממשיך לרוץ גם כשהתוכנית תקועה, וכלב השמירה לא מגלה דבר. ולפי התיעוד אי אפשר לעצור אותו אחרי שהופעל, והזמן המרבי ב-Pico W הוא 8388 ms.machine. קושרים את הפונקציה לתכונה Button.when_pressed בספרייה gpiozero, ולה הארגומנט bounce_time לקפיצות המגע. היא רצה בתהליכון רקע ולא בפסיקה, ולכן מותר להדפיס בה.עיקרי הפרק:
- סקר מפספס את מה שקורה בין שתי קריאות, ופסיקה לא מפספסת.
- המטפל קצר: משנה מספר או דגל ויוצא, והעבודה הארוכה בלולאה הראשית.
- צעדים שחייבים להתבצע יחד על משתנה משותף מוגנים בקטע קריטי.
- עם קפיצות מגע ממתינים לשקט בעזרת
ticks_diff, ואז קוראים את מצב ההדק. - כלב שמירה מאכילים מן הלולאה הראשית, ולא מפסיקה.
פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| pin.irq(handler=f, trigger=t) | קושרת פונקציה לקצה: Pin.IRQ_FALLING או Pin.IRQ_RISING. |
| time.ticks_diff(a, b) | ההפרש הנכון בין שני זמנים, גם כשהמונה חוזר לאפס. |
| tim.init(period=ms, mode=m, callback=f) | פסיקת טיימר: Timer.PERIODIC או Timer.ONE_SHOT. |
| disable_irq() / enable_irq(state) | גבולות הקטע הקריטי. |
| WDT(timeout=ms) / wdt.feed() | מפעילה כלב שמירה / מאכילה אותו. |
מילון מונחים:
| עברית | English | ערבית |
|---|---|---|
| פסיקה | Interrupt | المقاطعة |
| מטפל בפסיקה | Interrupt Handler | معالج المقاطعة |
| סקר | Polling | الاستطلاع |
| קצה עולה / יורד | Rising / Falling Edge | الحافة الصاعدة / الهابطة |
| דגל | Flag | الراية |
| קטע קריטי | Critical Section | المقطع الحرج |
| קפיצות מגע | Contact Bounce | ارتداد التلامس |
| כלב שמירה | Watchdog Timer | مؤقّت الحراسة |
שאלות לתרגול:
btn.irq(handler=on_press()), ולא קורה דבר בלחיצה. למה?irq מקבלת את התוצאה שלה, None. כותבים את השם בלי סוגריים.(א)
count += 1(ב)
t = 3.3 * n(ג)
led.value(1)?Pin.IRQ_RISING, והיא מדפיסה: press number 3 led = 1ONE_SHOT מכבה אותה אחרי שתי שניות. למה מפעילים את הטיימר מן הלולאה ולא מן המטפל?n = pulses ל-pulses = 0, הוא יאבד. זה תלוי ברגע שבו הקצה מגיע, ולכן זו שגיאה נדירה שאינה חוזרת באופן קבוע, ובגלל זה קשה לגלות אותה.