การเขียนตัวจัดการอินเทอร์รัปต์¶
บนฮาร์ดแวร์ที่รองรับ MicroPython มีความสามารถในการเขียนตัวจัดการอินเทอร์รัปต์ด้วย Python ตัวจัดการอินเทอร์รัปต์ — หรือที่รู้จักกันในชื่อ interrupt service routines (ISR) — ถูกกำหนดเป็นฟังก์ชันคอลแบ็ก ฟังก์ชันเหล่านี้ทำงานเพื่อตอบสนองต่อเหตุการณ์ เช่น การทริกเกอร์จากตัวจับเวลาหรือการเปลี่ยนแปลงแรงดันที่พิน เหตุการณ์ดังกล่าวอาจเกิดขึ้นได้ตลอดเวลาระหว่างการทำงานของโปรแกรม ซึ่งส่งผลกระทบอย่างมีนัยสำคัญ บางอย่างเป็นเรื่องเฉพาะของ MicroPython ส่วนอื่น ๆ เป็นเรื่องทั่วไปสำหรับระบบทุกประเภทที่สามารถตอบสนองต่อเหตุการณ์แบบเรียลไทม์ เอกสารนี้จะกล่าวถึงปัญหาเฉพาะของภาษาก่อน แล้วตามด้วยการแนะนำสั้น ๆ เกี่ยวกับการเขียนโปรแกรมแบบเรียลไทม์สำหรับผู้ที่ยังไม่คุ้นเคย
บทนำนี้ใช้คำที่กว้าง ๆ เช่น "ช้า" หรือ "เร็วที่สุดเท่าที่จะทำได้" ซึ่งตั้งใจเช่นนั้น เนื่องจากความเร็วขึ้นอยู่กับแอปพลิเคชัน ระยะเวลาที่ยอมรับได้สำหรับ ISR ขึ้นอยู่กับอัตราที่อินเทอร์รัปต์เกิดขึ้น ลักษณะของโปรแกรมหลัก และการมีอยู่ของเหตุการณ์พร้อมกันอื่น ๆ
เคล็ดลับและแนวปฏิบัติที่แนะนำ¶
ส่วนนี้สรุปประเด็นที่อธิบายอย่างละเอียดด้านล่าง และแสดงรายการคำแนะนำหลักสำหรับโค้ดตัวจัดการอินเทอร์รัปต์
ทำให้โค้ดสั้นและเรียบง่ายที่สุดเท่าที่จะทำได้
หลีกเลี่ยงการจัดสรรหน่วยความจำ: ห้ามเพิ่มข้อมูลลงในรายการหรือแทรกลงในดิกชันนารี ห้ามใช้เลขทศนิยม
พิจารณาใช้
micropython.scheduleเพื่อหลีกเลี่ยงข้อจำกัดดังกล่าวเมื่อ ISR ต้องส่งคืนหลายไบต์ ให้ใช้
bytearrayที่จัดสรรไว้ล่วงหน้า หากต้องการแชร์จำนวนเต็มหลายค่าระหว่าง ISR และโปรแกรมหลัก ให้พิจารณาใช้อาร์เรย์ (array.array)เมื่อมีการแชร์ข้อมูลระหว่างโปรแกรมหลักและ ISR ควรพิจารณาปิดใช้งานอินเทอร์รัปต์ก่อนเข้าถึงข้อมูลในโปรแกรมหลัก แล้วเปิดใช้งานใหม่ทันทีหลังจากนั้น (ดู Critical Sections)
จัดสรร emergency exception buffer (ดูด้านล่าง)
ปัญหาของ MicroPython¶
Emergency exception buffer¶
หากเกิดข้อผิดพลาดใน ISR MicroPython จะไม่สามารถสร้างรายงานข้อผิดพลาดได้ เว้นแต่จะสร้างบัฟเฟอร์พิเศษขึ้นมาเพื่อจุดประสงค์นี้ การดีบักจะง่ายขึ้นหากมีการรวมโค้ดต่อไปนี้ในโปรแกรมที่ใช้อินเทอร์รัปต์
import micropython
micropython.alloc_emergency_exception_buf(100)
emergency exception buffer สามารถเก็บ stack trace ของ exception ได้เพียงรายการเดียว ซึ่งหมายความว่าหากมีการโยน exception ที่สองระหว่างการจัดการ exception ขณะที่ heap ถูกล็อก stack trace ของ exception ที่สองจะแทนที่อันเดิม — แม้ว่า exception ที่สองจะได้รับการจัดการเรียบร้อยแล้ว สิ่งนี้อาจนำไปสู่ข้อความ exception ที่สับสนหากบัฟเฟอร์ถูกพิมพ์ออกมาในภายหลัง
ความเรียบง่าย¶
ด้วยเหตุผลหลายประการ จึงเป็นสิ่งสำคัญที่จะทำให้โค้ด ISR สั้นและเรียบง่ายที่สุดเท่าที่จะทำได้ ควรทำเฉพาะสิ่งที่ต้องทำทันทีหลังจากเหตุการณ์ที่ทำให้เกิดการเรียกใช้งาน: การดำเนินการที่สามารถเลื่อนออกไปได้ควรมอบหมายให้กับ main program loop โดยทั่วไป ISR จะจัดการกับอุปกรณ์ฮาร์ดแวร์ที่ก่อให้เกิดอินเทอร์รัปต์ เพื่อเตรียมพร้อมสำหรับอินเทอร์รัปต์ครั้งถัดไป ISR จะสื่อสารกับ main loop โดยการอัปเดตข้อมูลที่แชร์เพื่อระบุว่าอินเทอร์รัปต์ได้เกิดขึ้น แล้วจึงส่งคืนการควบคุม ISR ควรส่งคืนการควบคุมไปยัง main loop ให้เร็วที่สุดเท่าที่จะทำได้ นี่ไม่ใช่ปัญหาเฉพาะของ MicroPython จึงมีรายละเอียดเพิ่มเติมที่ below
การสื่อสารระหว่าง ISR และโปรแกรมหลัก¶
โดยปกติ ISR จำเป็นต้องสื่อสารกับโปรแกรมหลัก วิธีที่ง่ายที่สุดในการทำเช่นนี้คือผ่านออบเจ็กต์ข้อมูลที่แชร์ร่วมกันตั้งแต่หนึ่งรายการขึ้นไป ไม่ว่าจะประกาศเป็น global หรือแชร์ผ่าน class (ดูด้านล่าง) มีข้อจำกัดและอันตรายหลายอย่างเกี่ยวกับการทำเช่นนี้ ซึ่งจะครอบคลุมในรายละเอียดด้านล่าง ออบเจ็กต์ประเภท integers, bytes และ bytearray มักใช้เพื่อจุดประสงค์นี้ รวมถึงอาร์เรย์ (จาก array module) ที่สามารถเก็บข้อมูลประเภทต่าง ๆ ได้
การใช้เมธอดของออบเจ็กต์เป็นคอลแบ็ก¶
MicroPython รองรับเทคนิคอันทรงพลังนี้ ซึ่งช่วยให้ ISR สามารถแชร์ตัวแปร instance กับโค้ดพื้นฐานได้ นอกจากนี้ยังช่วยให้ class ที่ implement device driver สามารถรองรับ device instance หลายตัวได้ ตัวอย่างต่อไปนี้ทำให้ 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"))
ในตัวอย่างนี้ instance red ขับ LED สีแดงจาก virtual timer ที่ 1 Hz: ทุกครั้งที่ timer ทำงาน red.cb() จะถูกเรียกใช้เพื่อสลับสถานะ LED สีแดง Instance green ทำงานในทำนองเดียวกันด้วย timer ที่ 0.8 Hz เพื่อสลับ LED สีเขียว การใช้ instance method มีประโยชน์สองประการ ประการแรก class เดียวช่วยให้สามารถแชร์โค้ดระหว่าง hardware instance หลายตัว ประการที่สอง ในฐานะ bound method อาร์กิวเมนต์แรกของฟังก์ชันคอลแบ็กคือ self ซึ่งช่วยให้คอลแบ็กเข้าถึงข้อมูล instance และบันทึกสถานะระหว่างการเรียกใช้งานต่อเนื่องได้ ตัวอย่างเช่น หาก class ข้างต้นมีตัวแปร self.count ที่ตั้งค่าเป็นศูนย์ใน constructor cb() สามารถเพิ่มตัวนับได้ Instance red และ green จะรักษานับจำนวนครั้งที่ LED แต่ละดวงเปลี่ยนสถานะแยกกัน
การสร้างออบเจ็กต์ Python¶
ISR ไม่สามารถสร้าง instance ของออบเจ็กต์ Python ได้ เนื่องจาก MicroPython จำเป็นต้องจัดสรรหน่วยความจำสำหรับออบเจ็กต์จากพื้นที่ที่เรียกว่า heap ซึ่งไม่ได้รับอนุญาตในตัวจัดการอินเทอร์รัปต์ เพราะการจัดสรร heap ไม่ใช่ re-entrant กล่าวคือ อินเทอร์รัปต์อาจเกิดขึ้นในขณะที่โปรแกรมหลักกำลังดำเนินการจัดสรรอยู่กลางคัน — เพื่อรักษาความสมบูรณ์ของ heap ตัวแปลภาษาจึงไม่อนุญาตให้จัดสรรหน่วยความจำในโค้ด ISR
ผลที่ตามมาจากข้อนี้คือ ISR ไม่สามารถใช้การคำนวณเลขทศนิยมได้ เพราะ float เป็นออบเจ็กต์ Python ในทำนองเดียวกัน ISR ไม่สามารถเพิ่มรายการลงใน list ได้ ในทางปฏิบัติอาจเป็นเรื่องยากที่จะระบุว่าโครงสร้างโค้ดใดจะพยายามจัดสรรหน่วยความจำและก่อให้เกิดข้อความผิดพลาด ซึ่งเป็นอีกเหตุผลหนึ่งในการทำให้โค้ด ISR สั้นและเรียบง่าย
วิธีหนึ่งในการหลีกเลี่ยงปัญหานี้คือให้ ISR ใช้บัฟเฟอร์ที่จัดสรรไว้ล่วงหน้า ตัวอย่างเช่น constructor ของ class สร้าง instance ของ bytearray และ boolean flag เมธอด ISR จัดสรรข้อมูลไปยังตำแหน่งในบัฟเฟอร์และตั้งค่า flag การจัดสรรหน่วยความจำเกิดขึ้นในโค้ดโปรแกรมหลักเมื่อ instance ออบเจ็กต์ถูกสร้าง ไม่ใช่ใน ISR
เมธอด I/O ของ MicroPython library มักมีตัวเลือกในการใช้บัฟเฟอร์ที่จัดสรรไว้ล่วงหน้า ตัวอย่างเช่น machine.I2C.readfrom_into() อ่านข้อมูลลงใน mutable buffer ที่ผู้เรียกใช้ส่งมา ซึ่งช่วยให้สามารถใช้งานใน ISR ได้
วิธีหนึ่งในการสร้างออบเจ็กต์โดยไม่ต้องใช้ class หรือ global มีดังนี้:
def set_volume(t, buf=bytearray(3)):
buf[0] = 0xa5
buf[1] = t >> 4
buf[2] = 0x5a
return buf
คอมไพเลอร์จะสร้าง instance ของอาร์กิวเมนต์ buf ค่าเริ่มต้นเมื่อโหลดฟังก์ชันเป็นครั้งแรก (โดยปกติจะเกิดขึ้นเมื่อ import module ที่มีฟังก์ชันนั้น)
การสร้าง instance ของออบเจ็กต์เกิดขึ้นเมื่อมีการสร้างการอ้างอิงไปยัง bound method ซึ่งหมายความว่า ISR ไม่สามารถส่ง bound method ไปยังฟังก์ชันได้ วิธีแก้ไขอย่างหนึ่งคือสร้างการอ้างอิงไปยัง bound method ใน constructor ของ class และส่งการอ้างอิงนั้นใน 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)
เทคนิคอื่น ๆ ได้แก่ การกำหนดและสร้าง instance ของเมธอดใน constructor หรือส่ง Foo.bar() พร้อมกับอาร์กิวเมนต์ self
การใช้งานออบเจ็กต์ Python¶
ข้อจำกัดเพิ่มเติมเกี่ยวกับออบเจ็กต์เกิดขึ้นจากวิธีการทำงานของ Python เมื่อมีการรันคำสั่ง import โค้ด Python จะถูกคอมไพล์เป็น bytecode โดยหนึ่งบรรทัดของโค้ดมักจะแมปกับ bytecode หลายตัว เมื่อโค้ดทำงาน ตัวแปลภาษาจะอ่าน bytecode แต่ละตัวและดำเนินการเป็นชุดคำสั่ง machine code เนื่องจากอินเทอร์รัปต์อาจเกิดขึ้นได้ตลอดเวลาระหว่างคำสั่ง machine code บรรทัดโค้ด Python ต้นฉบับอาจทำงานได้เพียงบางส่วน ดังนั้น ออบเจ็กต์ Python เช่น set, list หรือ dictionary ที่ถูกแก้ไขใน main loop อาจขาดความสอดคล้องภายในในขณะที่อินเทอร์รัปต์เกิดขึ้น
ผลลัพธ์ที่พบได้ทั่วไปมีดังนี้ ในบางครั้ง ISR จะทำงานในช่วงเวลาที่แน่นอนเมื่อออบเจ็กต์กำลังถูกอัปเดตเพียงบางส่วน เมื่อ ISR พยายามอ่านออบเจ็กต์ก็จะเกิดการ crash เนื่องจากปัญหาดังกล่าวมักเกิดขึ้นในโอกาสที่หายากและสุ่ม จึงอาจวินิจฉัยได้ยาก มีวิธีแก้ไขปัญหานี้ ซึ่งอธิบายไว้ใน Critical Sections ด้านล่าง
สิ่งสำคัญคือต้องเข้าใจอย่างชัดเจนว่าอะไรคือการแก้ไขออบเจ็กต์ การเปลี่ยนแปลงเนื้อหาของ array หรือ bytearray นั้นปลอดภัย เนื่องจากไบต์หรือคำถูกเขียนด้วยคำสั่ง machine code เดียวซึ่งไม่สามารถถูกขัดจังหวะได้: ในภาษาของการเขียนโปรแกรมแบบเรียลไทม์ การเขียนนั้นเป็น atomic เช่นเดียวกันกับการอัปเดต dictionary item เนื่องจาก item เป็น machine word ที่เป็น integer หรือ pointer ไปยังออบเจ็กต์ ออบเจ็กต์ที่ผู้ใช้กำหนดอาจสร้าง array หรือ bytearray ได้ ทั้ง main loop และ ISR สามารถแก้ไขเนื้อหาเหล่านี้ได้อย่างถูกต้อง
อันตรายเกิดขึ้นเมื่อโครงสร้างของออบเจ็กต์ถูกเปลี่ยนแปลง โดยเฉพาะในกรณีของ dictionary การเพิ่มหรือลบ key อาจทริกเกอร์การ rehash ถ้า hard ISR ทำงานขณะที่กำลัง rehash และพยายามเข้าถึงรายการ อาจเกิด crash ได้ ภายในระบบ global ถูก implement เป็น dictionary ดังนั้นโปรแกรมหลักควรสร้าง global ที่จำเป็นทั้งหมดก่อนเริ่มกระบวนการที่สร้าง hard interrupt โค้ดแอปพลิเคชันควรหลีกเลี่ยงการลบ global ด้วย
MicroPython รองรับจำนวนเต็มที่มีความแม่นยำตามอำเภอใจ ค่าระหว่าง 230 -1 และ -230 จะถูกเก็บในหน่วยความจำเดียวของ machine word ค่าที่ใหญ่กว่าจะถูกเก็บเป็นออบเจ็กต์ Python ดังนั้นการเปลี่ยนแปลงจำนวนเต็มขนาดยาวจึงไม่สามารถถือว่าเป็น atomic ได้ การใช้จำนวนเต็มขนาดยาวใน ISR ไม่ปลอดภัย เนื่องจากอาจมีการพยายามจัดสรรหน่วยความจำขณะที่ค่าของตัวแปรเปลี่ยนแปลง
การเอาชนะข้อจำกัดของ float¶
โดยทั่วไปแล้ว การหลีกเลี่ยงการใช้ float ในโค้ด ISR เป็นสิ่งที่ดีที่สุด: อุปกรณ์ฮาร์ดแวร์มักจัดการกับจำนวนเต็ม และการแปลงเป็น float มักทำใน main loop อย่างไรก็ตาม มีอัลกอริทึม DSP บางอย่างที่ต้องการการคำนวณเลขทศนิยม บนแพลตฟอร์มที่มี hardware floating point (เช่น OpenMV Cams ที่ใช้ STM32) สามารถใช้ inline ARM Thumb assembler เพื่อหลีกเลี่ยงข้อจำกัดนี้ได้ เนื่องจากโปรเซสเซอร์เก็บค่า float ใน machine word ค่าสามารถแชร์ระหว่าง ISR และโค้ดโปรแกรมหลักผ่านอาร์เรย์ของ float ได้
การใช้ micropython.schedule¶
ฟังก์ชันนี้ช่วยให้ ISR สามารถกำหนดเวลาการเรียกใช้คอลแบ็ก "เร็ว ๆ นี้" คอลแบ็กจะถูกจัดคิวสำหรับการทำงานซึ่งจะเกิดขึ้นในเวลาที่ heap ไม่ถูกล็อก ดังนั้นจึงสามารถสร้างออบเจ็กต์ Python และใช้ float ได้ นอกจากนี้คอลแบ็กยังรับประกันว่าจะทำงานในเวลาที่โปรแกรมหลักได้อัปเดตออบเจ็กต์ Python เสร็จสิ้นแล้ว ดังนั้นคอลแบ็กจะไม่พบออบเจ็กต์ที่อัปเดตเพียงบางส่วน
การใช้งานทั่วไปคือการจัดการฮาร์ดแวร์ sensor ISR จะรับข้อมูลจากฮาร์ดแวร์และเปิดใช้งานเพื่อออก interrupt อีกครั้ง จากนั้นจึงกำหนดเวลาคอลแบ็กเพื่อประมวลผลข้อมูล
คอลแบ็กที่กำหนดเวลาควรปฏิบัติตามหลักการออกแบบตัวจัดการอินเทอร์รัปต์ที่อธิบายด้านล่าง เพื่อหลีกเลี่ยงปัญหาที่เกิดจากกิจกรรม I/O และการแก้ไขข้อมูลที่แชร์ ซึ่งอาจเกิดขึ้นในโค้ดใด ๆ ที่ preempt โปรแกรมหลัก
เวลาการทำงานต้องพิจารณาในความสัมพันธ์กับความถี่ที่อินเทอร์รัปต์สามารถเกิดขึ้นได้ หากอินเทอร์รัปต์เกิดขึ้นในขณะที่คอลแบ็กก่อนหน้ากำลังทำงานอยู่ อินสแตนซ์เพิ่มเติมของคอลแบ็กจะถูกจัดคิวสำหรับการทำงาน ซึ่งจะทำงานหลังจากอินสแตนซ์ปัจจุบันเสร็จสิ้น ดังนั้น อัตราการเกิดอินเทอร์รัปต์ที่สูงอย่างต่อเนื่องจึงมีความเสี่ยงที่คิวจะขยายตัวไม่สิ้นสุดและในที่สุดจะล้มเหลวด้วย RuntimeError
หากคอลแบ็กที่จะส่งไปยัง schedule() เป็น bound method ให้ดูหมายเหตุใน "Creation of Python objects"
Exceptions¶
หาก ISR ยก exception จะไม่แพร่กระจายไปยัง main loop อินเทอร์รัปต์จะถูกปิดใช้งานเว้นแต่ exception จะได้รับการจัดการโดยโค้ด ISR
การเชื่อมต่อกับ asyncio¶
เมื่อ ISR ทำงาน อาจ preempt ตัวกำหนดเวลาของ asyncio หาก ISR ดำเนินการ asyncio การทำงานของตัวกำหนดเวลาอาจถูกรบกวน ซึ่งใช้ได้ไม่ว่าอินเทอร์รัปต์จะเป็นแบบ hard หรือ soft และยังใช้ได้หาก ISR ส่งการดำเนินการไปยังฟังก์ชันอื่นผ่าน micropython.schedule โดยเฉพาะอย่างยิ่ง การสร้างหรือยกเลิก task ไม่ถูกต้องใน ISR context วิธีที่ปลอดภัยในการโต้ตอบกับ asyncio คือการ implement coroutine โดยใช้การซิงโครไนซ์ผ่าน asyncio.ThreadSafeFlag ตัวอย่างต่อไปนี้แสดงการสร้าง task เพื่อตอบสนองต่ออินเทอร์รัปต์:
tsf = asyncio.ThreadSafeFlag()
def isr(_): # Interrupt handler
tsf.set()
async def foo():
while True:
await tsf.wait()
asyncio.create_task(bar())
ในตัวอย่างนี้จะมีปริมาณเวลาแฝงที่แตกต่างกันระหว่างการทำงานของ ISR และการทำงานของ foo() ซึ่งเป็นคุณสมบัติโดยธรรมชาติของ cooperative scheduling เวลาแฝงสูงสุดขึ้นอยู่กับแอปพลิเคชันและแพลตฟอร์ม แต่โดยทั่วไปอาจวัดได้ในหน่วยสิบมิลลิวินาที
ปัญหาทั่วไป¶
นี่เป็นเพียงการแนะนำสั้น ๆ เกี่ยวกับหัวข้อการเขียนโปรแกรมแบบเรียลไทม์ ผู้เริ่มต้นควรทราบว่าข้อผิดพลาดในการออกแบบโปรแกรมแบบเรียลไทม์อาจนำไปสู่ข้อบกพร่องที่วินิจฉัยได้ยากเป็นพิเศษ เนื่องจากอาจเกิดขึ้นได้นาน ๆ ครั้งและในช่วงเวลาที่โดยพื้นฐานแล้วสุ่ม จึงเป็นสิ่งสำคัญอย่างยิ่งที่จะต้องทำให้การออกแบบเริ่มต้นถูกต้องและคาดการณ์ปัญหาก่อนที่จะเกิดขึ้น ทั้งตัวจัดการอินเทอร์รัปต์และโปรแกรมหลักต้องได้รับการออกแบบโดยคำนึงถึงปัญหาต่อไปนี้
การออกแบบตัวจัดการอินเทอร์รัปต์¶
ดังที่กล่าวไว้ข้างต้น ISR ควรได้รับการออกแบบให้เรียบง่ายที่สุดเท่าที่จะทำได้ ควรส่งคืนการควบคุมเสมอในช่วงเวลาที่สั้นและคาดเดาได้ สิ่งนี้มีความสำคัญเพราะเมื่อ ISR กำลังทำงาน main loop จะไม่ทำงาน: หลีกเลี่ยงไม่ได้ที่ main loop จะประสบกับการหยุดชะงักในการทำงานที่จุดสุ่มในโค้ด การหยุดชะงักดังกล่าวอาจเป็นแหล่งของ bug ที่วินิจฉัยได้ยาก โดยเฉพาะอย่างยิ่งหากระยะเวลานานหรือแปรผัน เพื่อทำความเข้าใจผลกระทบของเวลาทำงาน ISR จำเป็นต้องมีความเข้าใจพื้นฐานเกี่ยวกับ interrupt priority
อินเทอร์รัปต์ถูกจัดระเบียบตาม priority scheme โค้ด ISR เองอาจถูกขัดจังหวะโดยอินเทอร์รัปต์ที่มี priority สูงกว่า ซึ่งมีผลกระทบหากอินเทอร์รัปต์สองตัวแชร์ข้อมูลร่วมกัน (ดู Critical Sections ด้านล่าง) หากมีอินเทอร์รัปต์ดังกล่าวเกิดขึ้น จะเพิ่มความล่าช้าเข้าไปในโค้ด ISR หากมีอินเทอร์รัปต์ที่มี priority ต่ำกว่าเกิดขึ้นในขณะที่ ISR กำลังทำงาน จะถูกเลื่อนออกไปจนกว่า ISR จะเสร็จสิ้น: หากความล่าช้านานเกินไป อินเทอร์รัปต์ priority ต่ำกว่าอาจล้มเหลว อีกปัญหาหนึ่งของ ISR ที่ช้าคือกรณีที่อินเทอร์รัปต์ประเภทเดียวกันที่สองเกิดขึ้นระหว่างการทำงาน อินเทอร์รัปต์ที่สองจะได้รับการจัดการเมื่อตัวแรกสิ้นสุด อย่างไรก็ตาม หากอัตราของอินเทอร์รัปต์ขาเข้าเกินความสามารถของ ISR ในการให้บริการอย่างต่อเนื่อง ผลลัพธ์จะไม่น่าพึงพอใจ
ดังนั้น ควรหลีกเลี่ยงหรือลดโครงสร้างการวนซ้ำให้น้อยที่สุด ควรหลีกเลี่ยง I/O ไปยังอุปกรณ์อื่นที่ไม่ใช่อุปกรณ์ที่ทำให้เกิดอินเทอร์รัปต์: I/O เช่น การเข้าถึงดิสก์ คำสั่ง print และการเข้าถึง UART ค่อนข้างช้า และระยะเวลาอาจแปรผัน อีกปัญหาหนึ่งคือฟังก์ชัน filesystem ไม่ใช่ reentrant: การใช้ filesystem I/O ใน ISR และโปรแกรมหลักจะเป็นอันตราย สิ่งสำคัญที่สุดคือโค้ด ISR ไม่ควรรอเหตุการณ์ I/O ยอมรับได้หากโค้ดสามารถรับประกันการส่งคืนในช่วงเวลาที่คาดเดาได้ เช่น การสลับ pin หรือ LED การเข้าถึงอุปกรณ์ที่ทำให้เกิดอินเทอร์รัปต์ผ่าน I2C หรือ SPI อาจจำเป็น แต่ควรคำนวณหรือวัดเวลาที่ใช้สำหรับการเข้าถึงดังกล่าวและประเมินผลกระทบต่อแอปพลิเคชัน
มักมีความจำเป็นต้องแชร์ข้อมูลระหว่าง ISR และ main loop ซึ่งอาจทำได้ผ่าน global variable หรือผ่าน class หรือ instance variable ตัวแปรมักเป็น integer หรือ boolean หรืออาร์เรย์ integer หรือ byte (อาร์เรย์ integer ที่จัดสรรไว้ล่วงหน้าให้การเข้าถึงที่เร็วกว่า list) เมื่อมีการแก้ไขหลายค่าโดย ISR จำเป็นต้องพิจารณากรณีที่อินเทอร์รัปต์เกิดขึ้นในเวลาที่โปรแกรมหลักเข้าถึงบางส่วน แต่ไม่ใช่ทั้งหมดของค่า ซึ่งอาจนำไปสู่ความไม่สอดคล้องกัน
พิจารณาการออกแบบต่อไปนี้ ISR เก็บข้อมูลที่เข้ามาใน bytearray แล้วบวกจำนวนไบต์ที่ได้รับเข้ากับจำนวนเต็มที่แสดงจำนวนไบต์ทั้งหมดที่พร้อมสำหรับการประมวลผล โปรแกรมหลักอ่านจำนวนไบต์ ประมวลผลไบต์เหล่านั้น แล้วล้างจำนวนไบต์ที่พร้อม สิ่งนี้จะทำงานได้จนกว่าอินเทอร์รัปต์จะเกิดขึ้นหลังจากที่โปรแกรมหลักได้อ่านจำนวนไบต์ ISR จะใส่ข้อมูลที่เพิ่มมาลงในบัฟเฟอร์และอัปเดตจำนวนที่ได้รับ แต่โปรแกรมหลักได้อ่านตัวเลขไปแล้ว จึงประมวลผลเฉพาะข้อมูลที่ได้รับมาแต่เดิม ไบต์ที่มาถึงใหม่จะสูญหาย
มีหลายวิธีในการหลีกเลี่ยงอันตรายนี้ วิธีที่ง่ายที่สุดคือการใช้ circular buffer หากไม่สามารถใช้โครงสร้างที่มี thread safety โดยธรรมชาติ มีวิธีอื่น ๆ อธิบายไว้ด้านล่าง
Reentrancy¶
อันตรายที่อาจเกิดขึ้นได้หากฟังก์ชันหรือเมธอดถูกแชร์ระหว่างโปรแกรมหลักและ ISR หนึ่งตัวหรือมากกว่า หรือระหว่าง ISR หลายตัว ปัญหาคือฟังก์ชันอาจถูกขัดจังหวะเองและมีอินสแตนซ์เพิ่มเติมของฟังก์ชันนั้นทำงาน หากต้องการให้สิ่งนี้เกิดขึ้น ฟังก์ชันต้องได้รับการออกแบบให้เป็น reentrant วิธีการทำเช่นนี้เป็นหัวข้อขั้นสูงที่อยู่นอกเหนือขอบเขตของบทช่วยสอนนี้
Critical sections¶
ตัวอย่างของ critical section ของโค้ดคือส่วนที่เข้าถึงมากกว่าหนึ่งตัวแปรที่สามารถได้รับผลกระทบจาก ISR หากอินเทอร์รัปต์เกิดขึ้นระหว่างการเข้าถึงตัวแปรแต่ละตัว ค่าของตัวแปรจะไม่สอดคล้องกัน นี่คือตัวอย่างของอันตรายที่เรียกว่า race condition: ISR และ main program loop แข่งกันเพื่อแก้ไขตัวแปร เพื่อหลีกเลี่ยงความไม่สอดคล้องกัน ต้องใช้วิธีการเพื่อให้แน่ใจว่า ISR จะไม่แก้ไขค่าตลอดระยะเวลาของ critical section วิธีหนึ่งในการบรรลุเป้าหมายนี้คือออก machine.disable_irq() ก่อนเริ่ม section และ 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()
critical section อาจประกอบด้วยโค้ดหนึ่งบรรทัดและตัวแปรหนึ่งตัว พิจารณา code fragment ต่อไปนี้
count = 0
def cb(): # An interrupt callback
count += 1
def main():
# Code to set up the interrupt callback omitted
while True:
count += 1
ตัวอย่างนี้แสดงแหล่งที่มาของ bug ที่ซับซ้อน บรรทัด count += 1 ใน main loop มี race condition เฉพาะที่เรียกว่า read-modify-write ซึ่งเป็นสาเหตุคลาสสิกของ bug ในระบบเรียลไทม์ ใน main loop MicroPython อ่านค่าของ count บวก 1 และเขียนกลับ ในบางครั้ง อินเทอร์รัปต์จะเกิดขึ้นหลังจากการอ่านและก่อนการเขียน อินเทอร์รัปต์แก้ไข count แต่การเปลี่ยนแปลงจะถูกเขียนทับโดย main loop เมื่อ ISR ส่งคืน ในระบบจริง สิ่งนี้อาจนำไปสู่ความล้มเหลวที่หายากและคาดเดาไม่ได้
ดังที่กล่าวไว้ข้างต้น ควรระมัดระวังหาก instance ของ Python built in type ถูกแก้ไขในโค้ดหลักและ instance นั้นถูกเข้าถึงใน ISR โค้ดที่ดำเนินการแก้ไขควรถือว่าเป็น critical section เพื่อให้แน่ใจว่า instance อยู่ในสถานะที่ถูกต้องเมื่อ ISR ทำงาน
ต้องระมัดระวังเป็นพิเศษหาก dataset ถูกแชร์ระหว่าง ISR ที่แตกต่างกัน อันตรายคืออินเทอร์รัปต์ที่มี priority สูงกว่าอาจเกิดขึ้นเมื่ออินเทอร์รัปต์ที่มี priority ต่ำกว่าได้อัปเดตข้อมูลที่แชร์เพียงบางส่วน การจัดการกับสถานการณ์นี้เป็นหัวข้อขั้นสูงที่อยู่นอกเหนือขอบเขตของบทแนะนำนี้ นอกจากระบุว่าออบเจ็กต์ mutex ที่อธิบายด้านล่างบางครั้งสามารถนำมาใช้ได้
การปิดใช้งานอินเทอร์รัปต์ตลอดระยะเวลาของ critical section เป็นวิธีปกติและง่ายที่สุด แต่จะปิดการใช้งานอินเทอร์รัปต์ทั้งหมดแทนที่จะปิดเฉพาะตัวที่อาจก่อให้เกิดปัญหา โดยทั่วไปไม่เป็นที่ต้องการที่จะปิดการใช้งานอินเทอร์รัปต์เป็นเวลานาน ในกรณีของ timer interrupt จะทำให้เกิดความแปรผันในเวลาที่คอลแบ็กเกิดขึ้น ในกรณีของ device interrupt อาจทำให้อุปกรณ์ได้รับบริการช้าเกินไปโดยอาจสูญเสียข้อมูลหรือเกิดข้อผิดพลาด overrun ในฮาร์ดแวร์อุปกรณ์ เช่นเดียวกับ ISR critical section ในโค้ดหลักควรมีระยะเวลาที่สั้นและคาดเดาได้
แนวทางในการจัดการกับ critical section ที่ลดเวลาที่อินเทอร์รัปต์ถูกปิดใช้งานอย่างมากคือการใช้ออบเจ็กต์ที่เรียกว่า mutex (ชื่อได้มาจากแนวคิด mutual exclusion) โปรแกรมหลัก lock mutex ก่อนรัน critical section และ unlock ที่ท้าย ISR ทดสอบว่า mutex ถูก lock หรือไม่ ถ้าใช่จะหลีกเลี่ยง critical section และส่งคืน ความท้าทายในการออกแบบคือการกำหนดว่า ISR ควรทำอะไรในกรณีที่การเข้าถึงตัวแปรที่สำคัญถูกปฏิเสธ ตัวอย่างง่าย ๆ ของ mutex สามารถหาได้ ที่นี่ โปรดทราบว่าโค้ด mutex ปิดการใช้งานอินเทอร์รัปต์ก็จริง แต่เป็นเพียงช่วงเวลาของคำสั่ง machine instruction แปดตัวเท่านั้น ประโยชน์ของวิธีการนี้คืออินเทอร์รัปต์อื่น ๆ แทบไม่ได้รับผลกระทบ
อินเทอร์รัปต์และ REPL¶
ตัวจัดการอินเทอร์รัปต์ เช่น ที่เกี่ยวข้องกับตัวจับเวลา อาจยังทำงานต่อไปหลังจากโปรแกรมสิ้นสุด ซึ่งอาจให้ผลลัพธ์ที่ไม่คาดคิดเมื่อคุณอาจคาดว่าออบเจ็กต์ที่ยก callback จะออกไปจาก scope แล้ว ตัวอย่างเช่นบน OpenMV Cam:
def bar():
foo = machine.Timer(-1, freq=4, callback=lambda t: print('.', end=''), hard=True)
bar()
การทำงานนี้จะดำเนินต่อไปจนกว่า timer จะถูกปิดใช้งานอย่างชัดเจน หรือบอร์ดถูก reset ด้วย Ctrl-D