المقاطعات
إذا كان برنامجنا مشغولا بعمل طويل، فمتى يلاحظ أن الزر ضُغط؟ حتى الآن كان البرنامج هو الذي يذهب ليسأل. في هذا الفصل نقلب الأدوار: الطرف نفسه يوقف البرنامج لحظة، ينفّذ دالة قصيرة، ثم يعود البرنامج إلى عمله.
- نشرح لماذا يفوّت الاستطلاع أحداثا قصيرة، وكيف تحلّ المقاطعة ذلك.
- نربط دالة بطرف بواسطة
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، و-1 على Pico W، فيخفيه 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 ضاعت. وهذا يتوقف على لحظة الحافة، فهو خطأ نادر لا يتكرر بانتظام، ولذلك يصعب اكتشافه.