תכנות מיקרו-בקרים בשפת MicroPython · פרק 5 — פסיקות⌂📚🏠
פרק 5

פסיקות

Interrupts
2 שעות לימוד

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

בפרק הזה:
  • נסביר למה סקר מפספס אירועים קצרים, ואיך פסיקה פותרת את זה.
  • נקשור פונקציה להדק בעזרת 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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
presses counted: 0

5.2 Pin.irq וכללי המטפל

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

BTN3V3PULL_UP14 (ESP32) · GP14 (Pico W)
חיבור הלחצן. השם board.BTN בוחר את ההדק הנכון.
3.3tVIRQ_FALLINGIRQ_RISINGמשוחררלחוץ
מתח ההדק בלחיצה אחת, והקבוע המתאים לכל קצה.

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

פסיקה קשיחה רצה מיד, בהשהיה מינימלית וללא הקצאת זיכרון. פסיקה רכה מתוזמנת לביצוע בקרוב, ולכן מותר לה להקצות זיכרון, וההשהיה שלה גדולה יותר. כדי לבקש פסיקה קשיחה מעבירים ל-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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
press number 1  led = 1
press number 2  led = 0
press number 3  led = 1
...
▶ניסוי חי — פסיקה מהלחצןלחצו על הלחצן במעבדה, וצפו בדגל שמגיע ללולאה.
⚠
המעבדה אינה זמינה כעת. החומר בפרק שלם גם בלעדיה: ממשיכים לקרוא, וחוזרים לניסוי מאוחר יותר, או פותחים אותו ישירות מן הקישור שמתחת למסגרת.
https://fahed-lab.com/books/micropython/lab/mpy_lab?prog=irq_flag
פותחים את המעבדה בדף נפרד ↗ fahed-lab.com/books/micropython/lab/mpy_lab?prog=irq_flag

5.3 קפיצות מגע (Contact Bounce)

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

3.3tVDEBOUNCE_MSקוראים את ההדק כאן
לחיצה אחת נותנת כמה קצוות יורדים, והשחרור נותן קצוות נוספים. לכן ממתינים לשקט ורק אז קוראים את ההדק.

הכלל: כל קצה מחדש את חותמת הזמן. כשעובר 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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
edges: 10  presses: 2

5.4 פסיקת טיימר (Timer Interrupt)

גם הטיימר מייצר פסיקה, בכל פרק זמן קבוע או פעם אחת. מספר הטיימר שונה בין שני הלוחות: 0 ב-ESP32, ואילו ב-Pico W הוא -1. board.timer() מסתירה את ההבדל.

הטיימר מחליף מצב כל חצי שנייה, והלולאה חופשית לעשות מה שהיא רוצה. נסו אותו במעבדה: prog=irq_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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
ticks = 2
ticks = 4
ticks = 6
הערה: לפי התיעוד של ESP32, מטפלי טיימר הם תמיד רכים, והארגומנט hard=True גורם ל-ValueError. ובכלל, ברירת המחדל משתנה מפורט לפורט. לכן הכלל שלנו נשאר: כותבים את המטפל כאילו הוא קשיח.

5.5 מד תדר פשוט (Frequency Meter)

סופרים N קצוות עולים בחלון זמן באורך T, ולכן התדר הוא f = NT.

דוגמה 5.1
ספרנו 250 קצוות עולים בחלון באורך 0.5 s. מהו התדר? ומהו השינוי הקטן ביותר שהמדידה הזאת מגלה?
פתרון:
f = NT = 2500.5 = 500 Hz
Δf = 1T = 10.5 = 2 Hz
f = 500 Hz

קריאת המונה ואיפוסו הם שני צעדים שחייבים להתבצע יחד. אילו הגיעה פסיקה ביניהם, קצה היה הולך לאיבוד, ולכן מגנים עליהם בקטע קריטי: disable_irq() עוצר את כל הפסיקות, ו-enable_irq(state) מחזיר אותן. משאירים ביניהם כמה שפחות קוד.

האות על ההדק של הלחצן עצמו: שתי שניות בתדר 100 Hz, ואחר כך שתי שניות בתדר 50 Hz.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
f = 100 Hz
f = 100 Hz
f = 50 Hz
f = 50 Hz
אזהרה: מתח האות אינו יכול לעלות על מתח ההזנה של הבקר. ובלוח אמיתי המטפל לוקח זמן, ולכן כשהקצוות קרובים מדי חלקם הולכים לאיבוד. הסימולציה האידיאלית אינה מראה זאת.
שאלה 5.1: ספרנו 330 קצוות בחלון באורך 0.5 s. מהו התדר?

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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
fed 0
fed 1
the program hangs here
[watchdog: the board resets]
...
אזהרה: לא שמים wdt.feed() בתוך מטפל של טיימר: הטיימר ממשיך לרוץ גם כשהתוכנית תקועה, וכלב השמירה לא מגלה דבר. ולפי התיעוד אי אפשר לעצור אותו אחרי שהופעל, והזמן המרבי ב-Pico W הוא 8388 ms.
הערה: ב-Raspberry Pi 4/5: אין machine. קושרים את הפונקציה לתכונה Button.when_pressed בספרייה gpiozero, ולה הארגומנט bounce_time לקפיצות המגע. היא רצה בתהליכון רקע ולא בפסיקה, ולכן מותר להדפיס בה.

עיקרי הפרק:

פקודות הפרק:

פקודהמה היא עושה
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مؤقّت الحراسة
התשובות הסופיות לשאלות הפרק
5.1 f = 660 Hz.

שאלות לתרגול:

רמה ראשונה — בסיס
1.
לחצן עם נגד משיכה פנימי. איזה קצה קורה בלחיצה, ואיזה בשחרור?
תשובות: היורד בלחיצה, והעולה בשחרור.
2.
כתבנו btn.irq(handler=on_press()), ולא קורה דבר בלחיצה. למה?
תשובות: הסוגריים מפעילים את הפונקציה מיד, ו-irq מקבלת את התוצאה שלה, None. כותבים את השם בלי סוגריים.
3.
מה מותר במטפל קשיח:
(א) count += 1
(ב) t = 3.3 * n
(ג) led.value(1)?
תשובות: (א) ו-(ג). (ב) יוצר מספר עשרוני, וזו הקצאת זיכרון.
רמה שנייה — יישום
4.
שנו את תוכנית ספירת הלחיצות כך שתספור שחרורים. מהי השורה האחרונה שהיא מדפיסה, עם אותן לחיצות?
תשובות: משנים את הקצה ל-Pin.IRQ_RISING, והיא מדפיסה: press number 3 led = 1
5.
אור מדרגות: לחיצה מדליקה את הנורית, וטיימר מסוג ONE_SHOT מכבה אותה אחרי שתי שניות. למה מפעילים את הטיימר מן הלולאה ולא מן המטפל?
תשובות: כי הכנת הטיימר עלולה להקצות זיכרון ולקחת זמן, והמטפל צריך להישאר קצר: הוא מרים דגל בלבד.
רמה שלישית — העמקה
6.
הפכנו את חלון מד התדר ל-0.25 s. מה דיוק המדידה? ומה המחיר לעומת חלון של שנייה שלמה?
תשובות: Δf = 4 Hz: הקריאה מהירה יותר, אבל הדיוק נמוך פי ארבעה.
7.
מחקו את הקטע הקריטי ממד התדר. מתי תופיע השגיאה? והאם היא חוזרת בכל הרצה?
תשובות: אם יגיע קצה בין n = pulses ל-pulses = 0, הוא יאבד. זה תלוי ברגע שבו הקצה מגיע, ולכן זו שגיאה נדירה שאינה חוזרת באופן קבוע, ובגלל זה קשה לגלות אותה.
נוסחאון הספר: כל נוסחאות הפרק הזה מרוכזות, עם סרטוטיהן וטבלאות הסמלים שלהן, בפרק 25 — נוסחאון הספר.