كتابة معالجات المقاطعات¶
على العتاد المناسب توفر MicroPython القدرة على كتابة معالجات المقاطعات بلغة Python. تُعرَّف معالجات المقاطعات - المعروفة أيضاً بإجراءات خدمة المقاطعة (ISR's) - كدوال رد نداء. وتُنفَّذ استجابةً لحدث مثل تشغيل مؤقت أو تغير في الجهد على دبوس. ويمكن أن تقع مثل هذه الأحداث في أي نقطة أثناء تنفيذ كود البرنامج. وينطوي هذا على عواقب كبيرة، بعضها خاص بلغة MicroPython. والبعض الآخر مشترك بين جميع الأنظمة القادرة على الاستجابة للأحداث في الزمن الحقيقي. تتناول هذه الوثيقة القضايا الخاصة باللغة أولاً، ثم تتبعها مقدمة موجزة في برمجة الزمن الحقيقي لمن هم جدد عليها.
تستخدم هذه المقدمة مصطلحات غامضة مثل "بطيء" أو "بأسرع ما يمكن". وهذا متعمد، إذ تعتمد السرعات على التطبيق. فالمدد المقبولة لإجراء ISR تعتمد على المعدل الذي تحدث به المقاطعات، وطبيعة البرنامج الرئيسي، ووجود أحداث متزامنة أخرى.
نصائح وممارسات موصى بها¶
يلخص هذا النقاط المفصّلة أدناه ويسرد التوصيات الرئيسية لكود معالج المقاطعة.
اجعل الكود قصيراً وبسيطاً قدر الإمكان.
تجنب تخصيص الذاكرة: لا إلحاق بالقوائم أو إدراج في القواميس، ولا فاصلة عائمة.
ضع في اعتبارك استخدام
micropython.scheduleللالتفاف حول القيد أعلاه.حيث يُرجع إجراء ISR عدة بايتات استخدم
bytearrayمخصصاً مسبقاً. وإذا كان من المقرر مشاركة عدة أعداد صحيحة بين إجراء ISR والبرنامج الرئيسي فضع في اعتبارك استخدام مصفوفة (array.array).حيث تُشارَك البيانات بين البرنامج الرئيسي وإجراء ISR، ضع في اعتبارك تعطيل المقاطعات قبل الوصول إلى البيانات في البرنامج الرئيسي وإعادة تمكينها مباشرة بعد ذلك (انظر الأقسام الحرجة).
خصّص مخزناً مؤقتاً للاستثناءات الطارئة (انظر أدناه).
قضايا MicroPython¶
مخزن الاستثناءات الطارئة المؤقت¶
إذا حدث خطأ في إجراء ISR، فلن تتمكن MicroPython من إنتاج تقرير خطأ ما لم يُنشأ مخزن مؤقت خاص لهذا الغرض. ويصبح التنقيح أبسط إذا أُدرج الكود التالي في أي برنامج يستخدم المقاطعات.
import micropython
micropython.alloc_emergency_exception_buf(100)
يمكن لمخزن الاستثناءات الطارئة المؤقت أن يحمل تتبّع مكدّس استثناء واحد فقط. وهذا يعني أنه إذا طُرح استثناء ثانٍ أثناء معالجة استثناء بينما تكون الكومة مقفلة، فإن تتبّع مكدّس ذلك الاستثناء الثاني سيحل محل الأصلي - حتى لو عولج الاستثناء الثاني بنظافة. ويمكن أن يؤدي هذا إلى رسائل استثناء مربكة إذا طُبع المخزن المؤقت لاحقاً.
البساطة¶
لأسباب متنوعة من المهم إبقاء كود ISR قصيراً وبسيطاً قدر الإمكان. وينبغي أن يقوم فقط بما يجب القيام به فوراً بعد الحدث الذي سبّبه: أما العمليات التي يمكن تأجيلها فينبغي تفويضها إلى حلقة البرنامج الرئيسي. ونمطياً سيتعامل إجراء ISR مع جهاز العتاد الذي سبّب المقاطعة، مهيئاً إياه لوقوع المقاطعة التالية. وسيتواصل مع الحلقة الرئيسية عبر تحديث البيانات المشتركة للإشارة إلى أن المقاطعة قد وقعت، ثم سيعود. وينبغي أن يعيد إجراء ISR التحكم إلى الحلقة الرئيسية بأسرع ما يمكن. وهذه ليست قضية خاصة بـ MicroPython لذا تُغطى بمزيد من التفصيل أدناه.
التواصل بين إجراء ISR والبرنامج الرئيسي¶
عادةً يحتاج إجراء ISR إلى التواصل مع البرنامج الرئيسي. وأبسط وسيلة للقيام بذلك هي عبر كائن أو أكثر من كائنات البيانات المشتركة، إما المُعلنة كعمومية أو المُشاركة عبر صنف (انظر أدناه). وهناك قيود ومخاطر متنوعة حول القيام بهذا، تُغطى بمزيد من التفصيل أدناه. وتُستخدم الأعداد الصحيحة وكائنات bytes و bytearray شائعاً لهذا الغرض إلى جانب المصفوفات (من وحدة array) التي يمكنها تخزين أنواع بيانات متنوعة.
استخدام دوال الكائنات كدوال رد نداء¶
تدعم MicroPython هذه التقنية القوية التي تمكّن إجراء ISR من مشاركة متغيرات النسخة مع الكود الأساسي. كما تمكّن صنفاً يطبّق مشغّل جهاز من دعم نسخ أجهزة متعددة. ويتسبب المثال التالي في وميض مصباحي LED بمعدلات مختلفة.
import machine
import micropython
micropython.alloc_emergency_exception_buf(100)
class Foo(object):
def __init__(self, freq, led):
self.led = led
self.timer = machine.Timer(-1, freq=freq, callback=self.cb, hard=True)
def cb(self, tim):
self.led.toggle()
red = Foo(1, machine.LED("LED_RED"))
green = Foo(0.8, machine.LED("LED_GREEN"))
في هذا المثال تقود النسخة red مصباح LED الأحمر من مؤقت افتراضي بتردد 1 هرتز: ففي كل مرة يُطلق المؤقت يُستدعى red.cb()، فيبدّل حالة مصباح LED الأحمر. وتعمل النسخة green بشكل مماثل بمؤقت 0.8 هرتز يبدّل حالة مصباح LED الأخضر. ويمنح استخدام دوال النسخة فائدتين. أولاً، صنف واحد يمكّن من مشاركة الكود بين نسخ عتاد متعددة. ثانياً، بصفتها دالة مرتبطة فإن الوسيط الأول لدالة رد النداء هو self. وهذا يمكّن دالة رد النداء من الوصول إلى بيانات النسخة وحفظ الحالة بين الاستدعاءات المتتالية. على سبيل المثال، لو كان للصنف أعلاه متغير self.count معيّن إلى صفر في الباني، لأمكن لـ cb() زيادة العدّاد. وعندئذٍ ستحتفظ النسختان red و green بعدّات مستقلة لعدد المرات التي غيّر فيها كل مصباح LED حالته.
إنشاء كائنات Python¶
لا يمكن لإجراءات ISR إنشاء نسخ من كائنات Python. وذلك لأن MicroPython تحتاج إلى تخصيص ذاكرة للكائن من مخزن من كتل الذاكرة الحرة يُسمى heap. وهذا غير مسموح به في معالج المقاطعة لأن تخصيص الكومة ليس قابلاً لإعادة الدخول. وبعبارة أخرى قد تقع المقاطعة بينما يكون البرنامج الرئيسي في منتصف تنفيذ عملية تخصيص - وللحفاظ على سلامة الكومة يمنع المفسّر تخصيصات الذاكرة في كود ISR.
ومن نتائج هذا أن إجراءات ISR لا يمكنها استخدام الحساب ذي الفاصلة العائمة؛ وذلك لأن الفواصل العائمة كائنات Python. وبالمثل لا يمكن لإجراء ISR أن يلحق عنصراً بقائمة. وعملياً قد يكون من الصعب تحديد أي بنى الكود بالضبط ستحاول إجراء تخصيص للذاكرة وتثير رسالة خطأ: وهذا سبب آخر لإبقاء كود ISR قصيراً وبسيطاً.
إحدى طرق تجنب هذه المشكلة هي أن يستخدم إجراء ISR مخازن مؤقتة مخصصة مسبقاً. على سبيل المثال يُنشئ باني الصنف نسخة bytearray وعلَماً منطقياً. وتُسند دالة ISR البيانات إلى مواقع في المخزن المؤقت وتضبط العلَم. ويحدث تخصيص الذاكرة في كود البرنامج الرئيسي عند إنشاء نسخة الكائن بدلاً من حدوثه في إجراء ISR.
عادةً توفر دوال الإدخال/الإخراج في مكتبة MicroPython خياراً لاستخدام مخزن مؤقت مخصص مسبقاً. على سبيل المثال يقرأ machine.I2C.readfrom_into() في مخزن مؤقت قابل للتغيير يوفره المستدعي: وهذا يمكّن من استخدامه في إجراء ISR.
إحدى وسائل إنشاء كائن دون استخدام صنف أو متغيرات عمومية هي كما يلي:
def set_volume(t, buf=bytearray(3)):
buf[0] = 0xa5
buf[1] = t >> 4
buf[2] = 0x5a
return buf
ينشئ المترجم الوسيط الافتراضي buf عندما تُحمَّل الدالة لأول مرة (عادةً عندما تُستورد الوحدة التي تحتويها).
تحدث حالة من إنشاء الكائن عند إنشاء مرجع لدالة مرتبطة. وهذا يعني أن إجراء ISR لا يمكنه تمرير دالة مرتبطة إلى دالة. وأحد الحلول هو إنشاء مرجع للدالة المرتبطة في باني الصنف وتمرير ذلك المرجع في إجراء ISR. على سبيل المثال:
class Foo():
def __init__(self):
self.bar_ref = self.bar # Allocation occurs here
self.x = 0.1
self.tim = machine.Timer(-1, freq=2, callback=self.cb, hard=True)
def bar(self, _):
self.x *= 1.2
print(self.x)
def cb(self, t):
# Passing self.bar would cause allocation.
micropython.schedule(self.bar_ref, 0)
ومن التقنيات الأخرى تعريف الدالة وإنشاء نسختها في الباني أو تمرير Foo.bar() مع الوسيط self.
استخدام كائنات Python¶
ينشأ قيد إضافي على الكائنات بسبب طريقة عمل Python. فعندما يُنفَّذ تصريح import يُترجَم كود Python إلى bytecode، حيث يُربط سطر واحد من الكود نمطياً بعدة شيفرات بايتية. وعندما يعمل الكود يقرأ المفسّر كل شيفرة بايتية وينفذها كسلسلة من تعليمات كود الآلة. وبما أن المقاطعة يمكن أن تقع في أي وقت بين تعليمات كود الآلة، فقد يكون سطر كود Python الأصلي قد نُفِّذ جزئياً فقط. وبالتالي فإن كائن Python مثل مجموعة أو قائمة أو قاموس مُعدَّل في الحلقة الرئيسية قد يفتقر إلى الاتساق الداخلي في لحظة وقوع المقاطعة.
النتيجة النمطية هي كما يلي. في حالات نادرة سيعمل إجراء ISR في اللحظة الزمنية الدقيقة التي يكون فيها الكائن مُحدَّثاً جزئياً. وعندما يحاول إجراء ISR قراءة الكائن، ينتج عن ذلك انهيار. ولأن مثل هذه المشكلات تحدث نمطياً في مناسبات نادرة وعشوائية فقد يصعب تشخيصها. وهناك طرق للالتفاف حول هذه المشكلة، موصوفة في الأقسام الحرجة أدناه.
من المهم أن نكون واضحين بشأن ما يشكل تعديلاً للكائن. فتغيير محتويات مصفوفة أو bytearray آمن. وذلك لأن البايتات أو الكلمات تُكتب كتعليمة كود آلة واحدة غير قابلة للمقاطعة: وبلغة برمجة الزمن الحقيقي تكون الكتابة ذرّية. والأمر نفسه ينطبق على تحديث عنصر قاموس لأن العناصر هي كلمات آلة، كونها أعداداً صحيحة أو مؤشرات إلى كائنات. وقد يُنشئ كائن مُعرَّف من قبل المستخدم نسخة من مصفوفة أو bytearray. ومن الصحيح أن يغير كل من الحلقة الرئيسية وإجراء ISR محتويات هذه.
ينشأ الخطر عندما تُغيَّر بنية الكائن، لا سيما في حالة القواميس. فإضافة أو حذف المفاتيح يمكن أن يطلق إعادة تجزئة (rehash). وإذا عمل إجراء ISR صلب أثناء إجراء إعادة تجزئة وحاول الوصول إلى عنصر، فقد يحدث انهيار. وداخلياً تُنفَّذ المتغيرات العمومية كقاموس. وبالتالي ينبغي للبرنامج الرئيسي إنشاء جميع المتغيرات العمومية الضرورية قبل بدء عملية تولّد مقاطعات صلبة. كما ينبغي لكود التطبيق أن يتجنب حذف المتغيرات العمومية.
تدعم MicroPython الأعداد الصحيحة ذات الدقة الاعتباطية. وتُخزَّن القيم بين 230 -1 و -230 في كلمة آلة واحدة. أما القيم الأكبر فتُخزَّن ككائنات Python. وبالتالي لا يمكن اعتبار التغييرات على الأعداد الصحيحة الطويلة ذرّية. واستخدام الأعداد الصحيحة الطويلة في إجراءات ISR غير آمن لأن تخصيص الذاكرة قد يُحاوَل أثناء تغير قيمة المتغير.
تجاوز قيد الفاصلة العائمة¶
بشكل عام من الأفضل تجنب استخدام الفواصل العائمة في كود ISR: فأجهزة العتاد تتعامل عادةً مع الأعداد الصحيحة، ويتم التحويل إلى الفواصل العائمة عادةً في الحلقة الرئيسية. ومع ذلك هناك بعض خوارزميات DSP التي تتطلب الفاصلة العائمة. وعلى المنصات ذات الفاصلة العائمة العتادية (مثل OpenMV Cams المبنية على STM32) يمكن استخدام مُجمّع ARM Thumb المضمّن للالتفاف حول هذا القيد. وذلك لأن المعالج يخزن قيم الفاصلة العائمة في كلمة آلة؛ ويمكن مشاركة القيم بين إجراء ISR وكود البرنامج الرئيسي عبر مصفوفة من الفواصل العائمة.
استخدام micropython.schedule¶
تمكّن هذه الدالة إجراء ISR من جدولة دالة رد نداء للتنفيذ "قريباً جداً". وتُصفّ دالة رد النداء للتنفيذ الذي سيقع في وقت لا تكون فيه الكومة مقفلة. ومن ثَمّ يمكنها إنشاء كائنات Python واستخدام الفواصل العائمة. كما يُضمن لدالة رد النداء أن تعمل في وقت يكون فيه البرنامج الرئيسي قد أكمل أي تحديث لكائنات Python، فلن تصادف دالة رد النداء كائنات مُحدَّثة جزئياً.
الاستخدام النمطي هو التعامل مع عتاد المستشعر. فيكتسب إجراء ISR البيانات من العتاد ويمكّنه من إصدار مقاطعة أخرى. ثم يجدول دالة رد نداء لمعالجة البيانات.
ينبغي لدوال رد النداء المجدولة أن تمتثل لمبادئ تصميم معالج المقاطعة المبيّنة أدناه. وذلك لتجنب المشكلات الناتجة عن نشاط الإدخال/الإخراج وتعديل البيانات المشتركة التي يمكن أن تنشأ في أي كود يستبق حلقة البرنامج الرئيسي.
يجب النظر في وقت التنفيذ بالنسبة إلى التردد الذي يمكن أن تحدث به المقاطعات. فإذا وقعت مقاطعة أثناء تنفيذ دالة رد النداء السابقة، ستُصفّ نسخة أخرى من دالة رد النداء للتنفيذ؛ وستعمل بعد اكتمال النسخة الحالية. ولذلك فإن معدل تكرار المقاطعة المرتفع المستمر ينطوي على خطر نمو الصف بلا قيود والفشل النهائي بـ RuntimeError.
إذا كانت دالة رد النداء المراد تمريرها إلى schedule() دالة مرتبطة، فضع في اعتبارك الملاحظة في "إنشاء كائنات Python".
الاستثناءات¶
إذا أطلق إجراء ISR استثناءً فلن ينتشر إلى الحلقة الرئيسية. وستُعطَّل المقاطعة ما لم يُعالَج الاستثناء بواسطة كود ISR.
التفاعل مع asyncio¶
عندما يعمل إجراء ISR يمكنه أن يستبق مجدول asyncio. وإذا قام إجراء ISR بعملية asyncio فقد يتعطل عمل المجدول. وينطبق هذا سواء أكانت المقاطعة صلبة أم لينة، كما ينطبق إذا مرّر إجراء ISR التنفيذ إلى دالة أخرى عبر micropython.schedule. وعلى وجه الخصوص فإن إنشاء المهام أو إلغاءها غير صالح في سياق إجراء ISR. والطريقة الآمنة للتفاعل مع asyncio هي تطبيق روتين مشارك (coroutine) مع تزامن يُنفَّذ بواسطة asyncio.ThreadSafeFlag. ويوضح المقطع التالي إنشاء مهمة استجابةً لمقاطعة:
tsf = asyncio.ThreadSafeFlag()
def isr(_): # Interrupt handler
tsf.set()
async def foo():
while True:
await tsf.wait()
asyncio.create_task(bar())
في هذا المثال سيكون هناك قدر متغير من زمن الاستجابة بين تنفيذ إجراء ISR وتنفيذ foo(). وهذا متأصل في الجدولة التعاونية. ويعتمد أقصى زمن استجابة على التطبيق والمنصة لكنه قد يُقاس نمطياً بعشرات الملي ثانية.
قضايا عامة¶
هذه مجرد مقدمة موجزة لموضوع برمجة الزمن الحقيقي. وينبغي للمبتدئين ملاحظة أن أخطاء التصميم في برامج الزمن الحقيقي يمكن أن تؤدي إلى أعطال يصعب تشخيصها بشكل خاص. وذلك لأنها يمكن أن تحدث نادراً وعلى فترات عشوائية جوهرياً. ومن الحاسم إنجاز التصميم الأولي بشكل صحيح وتوقع المشكلات قبل نشوئها. ويجب تصميم كل من معالجات المقاطعة والبرنامج الرئيسي مع فهم للقضايا التالية.
تصميم معالج المقاطعة¶
كما ذُكر أعلاه، ينبغي تصميم إجراءات ISR لتكون بسيطة قدر الإمكان. وينبغي أن تعود دائماً في فترة زمنية قصيرة وقابلة للتنبؤ. وهذا مهم لأنه عندما يعمل إجراء ISR، لا تعمل الحلقة الرئيسية: حتماً تشهد الحلقة الرئيسية توقفات في تنفيذها عند نقاط عشوائية في الكود. ويمكن أن تكون مثل هذه التوقفات مصدراً للأخطاء التي يصعب تشخيصها لا سيما إذا كانت مدتها طويلة أو متغيرة. ولفهم تبعات وقت تشغيل إجراء ISR، يلزم إدراك أساسي لأولويات المقاطعة.
تُنظَّم المقاطعات وفقاً لمخطط أولويات. وقد يُقاطَع كود ISR نفسه بمقاطعة ذات أولوية أعلى. ولهذا تبعات إذا تشاركت المقاطعتان البيانات (انظر الأقسام الحرجة أدناه). وإذا وقعت مثل هذه المقاطعة فإنها تُدخل تأخيراً في كود ISR. وإذا وقعت مقاطعة ذات أولوية أدنى أثناء عمل إجراء ISR، فستتأخر حتى يكتمل إجراء ISR: وإذا كان التأخير طويلاً جداً، فقد تفشل المقاطعة ذات الأولوية الأدنى. وهناك قضية أخرى مع إجراءات ISR البطيئة هي حالة وقوع مقاطعة ثانية من النوع نفسه أثناء تنفيذها. وستُعالَج المقاطعة الثانية عند انتهاء الأولى. ومع ذلك إذا تجاوز معدل المقاطعات الواردة باستمرار قدرة إجراء ISR على خدمتها فلن تكون النتيجة سارّة.
وبالتالي ينبغي تجنب بنى الحلقات أو تقليلها. وينبغي عادةً تجنب الإدخال/الإخراج إلى أجهزة غير الجهاز المُقاطِع: فالإدخال/الإخراج مثل الوصول إلى القرص وتصاريح print والوصول إلى UART بطيء نسبياً، وقد تتباين مدته. وهناك قضية أخرى هنا وهي أن دوال نظام الملفات ليست قابلة لإعادة الدخول: فاستخدام إدخال/إخراج نظام الملفات في إجراء ISR والبرنامج الرئيسي سيكون خطراً. وبشكل حاسم لا ينبغي لكود ISR أن ينتظر حدثاً. والإدخال/الإخراج مقبول إذا كان من الممكن ضمان عودة الكود في فترة قابلة للتنبؤ، على سبيل المثال تبديل حالة دبوس أو مصباح LED. وقد يكون الوصول إلى الجهاز المُقاطِع عبر I2C أو SPI ضرورياً لكن ينبغي حساب أو قياس الوقت المستغرق لمثل هذه الوصولات وتقييم تأثيرها على التطبيق.
عادةً تكون هناك حاجة لمشاركة البيانات بين إجراء ISR والحلقة الرئيسية. وقد يتم ذلك إما عبر المتغيرات العمومية أو عبر متغيرات الصنف أو النسخة. وتكون المتغيرات نمطياً من النوع الصحيح أو المنطقي، أو مصفوفات أعداد صحيحة أو بايتات (تتيح مصفوفة الأعداد الصحيحة المخصصة مسبقاً وصولاً أسرع من القائمة). وحيث يعدّل إجراء ISR قيماً متعددة يلزم النظر في الحالة التي تقع فيها المقاطعة في وقت يكون فيه البرنامج الرئيسي قد وصل إلى بعض القيم وليس كلها. وهذا يمكن أن يؤدي إلى عدم اتساق.
ضع في اعتبارك التصميم التالي. يخزن إجراء ISR البيانات الواردة في bytearray، ثم يضيف عدد البايتات المستلمة إلى عدد صحيح يمثل إجمالي البايتات الجاهزة للمعالجة. ويقرأ البرنامج الرئيسي عدد البايتات، ويعالجها، ثم يصفّر عدد البايتات الجاهزة. وسيعمل هذا حتى تقع مقاطعة مباشرةً بعد أن يقرأ البرنامج الرئيسي عدد البايتات. فيضع إجراء ISR البيانات المضافة في المخزن المؤقت ويحدّث العدد المستلم، لكن البرنامج الرئيسي قد قرأ العدد بالفعل، فيعالج البيانات المستلمة أصلاً. وتُفقَد البايتات الواردة حديثاً.
هناك طرق متنوعة لتجنب هذا الخطر، أبسطها استخدام مخزن مؤقت دائري. وإذا لم يكن من الممكن استخدام بنية ذات أمان خيوط متأصل فإن طرقاً أخرى موصوفة أدناه.
إعادة الدخول¶
قد ينشأ خطر محتمل إذا تشاركت دالة أو طريقة بين البرنامج الرئيسي وإجراء ISR واحد أو أكثر أو بين عدة إجراءات ISR. والقضية هنا هي أن الدالة قد تُقاطَع نفسها وتعمل نسخة أخرى من تلك الدالة. وإذا كان هذا سيحدث، فيجب تصميم الدالة لتكون قابلة لإعادة الدخول. وكيفية القيام بذلك موضوع متقدم يتجاوز نطاق هذا الدرس.
الأقسام الحرجة¶
مثال على قسم حرج من الكود هو ذلك الذي يصل إلى أكثر من متغير يمكن أن يتأثر بإجراء ISR. فإذا حدث ووقعت المقاطعة بين الوصول إلى المتغيرات الفردية، فستكون قيمها غير متسقة. وهذه حالة من خطر يُعرف بحالة التسابق: حيث يتسابق إجراء ISR وحلقة البرنامج الرئيسي لتغيير المتغيرات. ولتجنب عدم الاتساق يجب استخدام وسيلة لضمان ألا يغيّر إجراء ISR القيم طوال مدة القسم الحرج. وإحدى طرق تحقيق ذلك هي إصدار machine.disable_irq() قبل بداية القسم، و machine.enable_irq() في نهايته. وفيما يلي مثال على هذا النهج:
import machine
import micropython
import array
import random
import time
micropython.alloc_emergency_exception_buf(100)
class BoundsException(Exception):
pass
ARRAYSIZE = const(20)
index = 0
data = array.array('i', [0] * ARRAYSIZE)
def callback1(t):
global data, index
for x in range(5):
data[index] = random.getrandbits(30) # simulate input
index += 1
if index >= ARRAYSIZE:
raise BoundsException('Array bounds exceeded')
tim = machine.Timer(-1, freq=100, callback=callback1, hard=True)
for loop in range(1000):
if index > 0:
irq_state = machine.disable_irq() # Start of critical section
for x in range(index):
print(data[x])
index = 0
machine.enable_irq(irq_state) # End of critical section
print('loop {}'.format(loop))
time.sleep_ms(1)
tim.deinit()
يمكن أن يتألف القسم الحرج من سطر واحد من الكود ومتغير واحد. ضع في اعتبارك مقطع الكود التالي.
count = 0
def cb(): # An interrupt callback
count += 1
def main():
# Code to set up the interrupt callback omitted
while True:
count += 1
يوضح هذا المثال مصدراً دقيقاً للأخطاء. فالسطر count += 1 في الحلقة الرئيسية يحمل خطر حالة تسابق محدداً يُعرف بالقراءة-التعديل-الكتابة. وهذا سبب كلاسيكي للأخطاء في أنظمة الزمن الحقيقي. ففي الحلقة الرئيسية تقرأ MicroPython قيمة count، وتضيف إليها 1، ثم تكتبها مجدداً. وفي مناسبات نادرة تقع المقاطعة بعد القراءة وقبل الكتابة. فتعدّل المقاطعة count لكن تغييرها يُكتب فوقه بواسطة الحلقة الرئيسية عند عودة إجراء ISR. وفي نظام حقيقي قد يؤدي هذا إلى أعطال نادرة وغير قابلة للتنبؤ.
كما ذُكر أعلاه، ينبغي توخي الحذر إذا عُدِّلت نسخة من نوع Python مدمج في الكود الرئيسي وتمت الوصول إلى تلك النسخة في إجراء ISR. وينبغي اعتبار الكود الذي يؤدي التعديل قسماً حرجاً لضمان أن تكون النسخة في حالة صالحة عندما يعمل إجراء ISR.
يلزم توخي حذر خاص إذا تشاركت مجموعة بيانات بين إجراءات ISR مختلفة. والخطر هنا هو أن المقاطعة ذات الأولوية الأعلى قد تقع عندما يكون ذو الأولوية الأدنى قد حدّث البيانات المشتركة جزئياً. والتعامل مع هذا الوضع موضوع متقدم يتجاوز نطاق هذه المقدمة، عدا ملاحظة أن كائنات mutex الموصوفة أدناه يمكن استخدامها أحياناً.
تعطيل المقاطعات طوال مدة القسم الحرج هو الطريقة المعتادة والأبسط للمتابعة، لكنه يعطل جميع المقاطعات بدلاً من تلك الوحيدة التي يُحتمل أن تسبب مشكلات فقط. ومن غير المرغوب فيه عموماً تعطيل مقاطعة لفترة طويلة. ففي حالة مقاطعات المؤقت يُدخل تبايناً في وقت وقوع دالة رد النداء. وفي حالة مقاطعات الأجهزة، يمكن أن يؤدي إلى خدمة الجهاز متأخراً جداً مع احتمال فقدان البيانات أو أخطاء التجاوز (overrun) في عتاد الجهاز. ومثل إجراءات ISR، ينبغي أن يكون للقسم الحرج في الكود الرئيسي مدة قصيرة وقابلة للتنبؤ.
هناك نهج للتعامل مع الأقسام الحرجة يقلل جذرياً من الوقت الذي تُعطَّل فيه المقاطعات وهو استخدام كائن يُسمى mutex (الاسم مشتق من مفهوم الاستبعاد المتبادل mutual exclusion). فيقفل البرنامج الرئيسي الـ mutex قبل تشغيل القسم الحرج ويفتحه في النهاية. ويختبر إجراء ISR ما إذا كان الـ mutex مقفلاً. وإذا كان كذلك، يتجنب القسم الحرج ويعود. والتحدي التصميمي هو تحديد ما ينبغي لإجراء ISR فعله في حالة رفض الوصول إلى المتغيرات الحرجة. ويمكن العثور على مثال بسيط لـ mutex هنا. لاحظ أن كود الـ mutex يعطل المقاطعات بالفعل، لكن فقط طوال مدة ثماني تعليمات آلة: وفائدة هذا النهج هي أن المقاطعات الأخرى لا تتأثر فعلياً.
المقاطعات و REPL¶
يمكن لمعالجات المقاطعات، مثل تلك المرتبطة بالمؤقتات، أن تستمر في العمل بعد انتهاء البرنامج. وقد ينتج عن هذا نتائج غير متوقعة حيث ربما كنت تتوقع أن يكون الكائن الذي يطلق دالة رد النداء قد خرج عن النطاق. على سبيل المثال على OpenMV Cam:
def bar():
foo = machine.Timer(-1, freq=4, callback=lambda t: print('.', end=''), hard=True)
bar()
يستمر هذا في العمل حتى يُعطَّل المؤقت صراحةً أو تُعاد تهيئة اللوحة بـ Ctrl-D.