برمجة المتحكّمات بلغة مايكروبايثون · الفصل 5 — المقاطعاتפסיקות · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 5

المقاطعات

פסיקות – Interrupts
ساعتان تعليميتان

إذا كان برنامجنا مشغولا بعمل طويل، فمتى يلاحظ أن الزر ضُغط؟ حتى الآن كان البرنامج هو الذي يذهب ليسأل. في هذا الفصل نقلب الأدوار: الطرف نفسه يوقف البرنامج لحظة، ينفّذ دالة قصيرة، ثم يعود البرنامج إلى عمله.

في هذا الفصل:
  • نشرح لماذا يفوّت الاستطلاع أحداثا قصيرة، وكيف تحلّ المقاطعة ذلك.
  • نربط دالة بطرف بواسطة 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، و-1 على Pico W، فيخفيه 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 — لوحة القوانين الشاملة. (נוסחאון הספר)