Menulis interrupt handler

Pada hardware yang sesuai, MicroPython menawarkan kemampuan untuk menulis interrupt handler dalam Python. Interrupt handler - juga dikenal sebagai interrupt service routine (ISR) - didefinisikan sebagai fungsi callback. Fungsi-fungsi ini dieksekusi sebagai respons terhadap suatu peristiwa seperti pemicu timer atau perubahan tegangan pada pin. Peristiwa semacam itu dapat terjadi kapan saja dalam eksekusi kode program. Hal ini membawa konsekuensi yang signifikan, beberapa spesifik untuk bahasa MicroPython. Yang lain umum untuk semua sistem yang mampu merespons peristiwa waktu nyata. Dokumen ini membahas masalah spesifik bahasa terlebih dahulu, diikuti oleh pengenalan singkat pemrograman waktu nyata bagi mereka yang baru mengenalnya.

Pengantar ini menggunakan istilah yang samar seperti "lambat" atau "secepat mungkin". Hal ini disengaja, karena kecepatan bergantung pada aplikasi. Durasi yang dapat diterima untuk ISR bergantung pada laju terjadinya interupsi, sifat program utama, dan kehadiran peristiwa bersamaan lainnya.

Masalah MicroPython

Emergency exception buffer

Jika terjadi kesalahan dalam ISR, MicroPython tidak dapat menghasilkan laporan kesalahan kecuali buffer khusus dibuat untuk tujuan tersebut. Proses debugging disederhanakan jika kode berikut disertakan dalam program apa pun yang menggunakan interupsi.

import micropython

micropython.alloc_emergency_exception_buf(100)

Emergency exception buffer hanya dapat menyimpan satu stack trace exception. Ini berarti bahwa jika exception kedua dilempar selama penanganan exception sementara heap dikunci, stack trace exception kedua tersebut akan menggantikan yang asli - bahkan jika exception kedua ditangani dengan bersih. Hal ini dapat menyebabkan pesan exception yang membingungkan jika buffer kemudian dicetak.

Kesederhanaan

Karena berbagai alasan, penting untuk menjaga kode ISR sesingkat dan sesederhana mungkin. Kode tersebut hanya harus melakukan apa yang harus dilakukan segera setelah peristiwa yang menyebabkannya: operasi yang dapat ditunda harus didelegasikan ke loop program utama. Biasanya ISR akan berurusan dengan perangkat hardware yang menyebabkan interupsi, mempersiapkannya untuk interupsi berikutnya. ISR akan berkomunikasi dengan loop utama dengan memperbarui data bersama untuk menunjukkan bahwa interupsi telah terjadi, dan kemudian kembali. ISR harus mengembalikan kontrol ke loop utama secepat mungkin. Ini bukan masalah spesifik MicroPython sehingga dibahas lebih detail di bawah.

Komunikasi antara ISR dan program utama

Biasanya ISR perlu berkomunikasi dengan program utama. Cara paling sederhana untuk melakukan ini adalah melalui satu atau lebih objek data bersama, baik yang dideklarasikan sebagai global atau dibagikan melalui kelas (lihat di bawah). Ada berbagai pembatasan dan bahaya seputar hal ini, yang dibahas lebih detail di bawah. Integer, bytes dan objek bytearray umumnya digunakan untuk tujuan ini bersama dengan array (dari modul array) yang dapat menyimpan berbagai tipe data.

Penggunaan metode objek sebagai callback

MicroPython mendukung teknik canggih ini yang memungkinkan ISR berbagi variabel instance dengan kode yang mendasarinya. Teknik ini juga memungkinkan kelas yang mengimplementasikan device driver untuk mendukung beberapa instance perangkat. Contoh berikut menyebabkan dua LED berkedip pada laju yang berbeda.

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"))

Dalam contoh ini, instance red menggerakkan LED merah dari virtual timer 1 Hz: setiap kali timer aktif, red.cb() dipanggil, mengubah status LED merah. Instance green beroperasi serupa dengan timer 0,8 Hz yang mengubah status LED hijau. Penggunaan metode instance memberikan dua manfaat. Pertama, sebuah kelas tunggal memungkinkan kode dibagikan di antara beberapa instance hardware. Kedua, sebagai bound method, argumen pertama fungsi callback adalah self. Ini memungkinkan callback mengakses data instance dan menyimpan status antara pemanggilan berturut-turut. Misalnya, jika kelas di atas memiliki variabel self.count yang diset ke nol di konstruktor, cb() dapat menginkrementasi counter. Instance red dan green kemudian akan mempertahankan hitungan independen dari jumlah kali setiap LED berubah status.

Pembuatan objek Python

ISR tidak dapat membuat instance objek Python. Ini karena MicroPython perlu mengalokasikan memori untuk objek dari penyimpanan blok memori bebas yang disebut heap. Hal ini tidak diizinkan dalam interrupt handler karena alokasi heap tidak bersifat re-entrant. Dengan kata lain, interupsi mungkin terjadi ketika program utama sedang dalam proses melakukan alokasi - untuk menjaga integritas heap, interpreter tidak mengizinkan alokasi memori dalam kode ISR.

Konsekuensi dari hal ini adalah ISR tidak dapat menggunakan aritmatika floating point; ini karena float adalah objek Python. Demikian pula, ISR tidak dapat menambahkan item ke list. Dalam praktiknya, mungkin sulit untuk menentukan dengan tepat konstruksi kode mana yang akan mencoba melakukan alokasi memori dan memicu pesan kesalahan: alasan lain untuk menjaga kode ISR singkat dan sederhana.

Salah satu cara untuk menghindari masalah ini adalah agar ISR menggunakan buffer yang dialokasikan sebelumnya. Misalnya, konstruktor kelas membuat instance bytearray dan flag boolean. Metode ISR menetapkan data ke lokasi dalam buffer dan menetapkan flag. Alokasi memori terjadi di kode program utama saat objek diinstansiasi, bukan di ISR.

Metode I/O library MicroPython biasanya menyediakan opsi untuk menggunakan buffer yang dialokasikan sebelumnya. Misalnya machine.I2C.readfrom_into() membaca ke dalam buffer yang dapat diubah yang disediakan oleh pemanggil: ini memungkinkan penggunaannya dalam ISR.

Cara membuat objek tanpa menggunakan kelas atau global adalah sebagai berikut:

def set_volume(t, buf=bytearray(3)):
    buf[0] = 0xa5
    buf[1] = t >> 4
    buf[2] = 0x5a
    return buf

Compiler menginstansiasi argumen buf default ketika fungsi dimuat untuk pertama kali (biasanya ketika modul tempat fungsi tersebut diimpor).

Instance pembuatan objek terjadi ketika referensi ke bound method dibuat. Ini berarti bahwa ISR tidak dapat meneruskan bound method ke suatu fungsi. Satu solusi adalah membuat referensi ke bound method di konstruktor kelas dan meneruskan referensi tersebut di ISR. Misalnya:

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)

Teknik lain adalah mendefinisikan dan menginstansiasi metode di konstruktor atau meneruskan Foo.bar() dengan argumen self.

Penggunaan objek Python

Pembatasan lebih lanjut pada objek timbul karena cara kerja Python. Ketika pernyataan import dieksekusi, kode Python dikompilasi menjadi bytecode, dengan satu baris kode biasanya memetakan ke beberapa bytecode. Ketika kode berjalan, interpreter membaca setiap bytecode dan mengeksekusinya sebagai serangkaian instruksi kode mesin. Mengingat bahwa interupsi dapat terjadi kapan saja di antara instruksi kode mesin, baris kode Python asli mungkin hanya dieksekusi sebagian. Akibatnya, objek Python seperti set, list, atau dictionary yang dimodifikasi di loop utama mungkin tidak memiliki konsistensi internal pada saat interupsi terjadi.

Hasil khas adalah sebagai berikut. Sesekali ISR akan berjalan pada saat yang tepat ketika objek sedang diperbarui sebagian. Ketika ISR mencoba membaca objek, terjadi crash. Karena masalah seperti itu biasanya terjadi pada kesempatan langka dan acak, masalah ini bisa sulit didiagnosis. Ada cara untuk menghindari masalah ini, yang dijelaskan di Critical Sections di bawah.

Penting untuk memperjelas apa yang dimaksud dengan modifikasi suatu objek. Mengubah isi array atau bytearray adalah aman. Ini karena byte atau word ditulis sebagai instruksi kode mesin tunggal yang tidak dapat diinterupsi: dalam istilah pemrograman waktu nyata, penulisan bersifat atomik. Hal yang sama berlaku untuk memperbarui item dictionary karena item adalah machine word, berupa integer atau pointer ke objek. Objek yang ditentukan pengguna mungkin menginstansiasi array atau bytearray. Adalah valid bagi loop utama dan ISR untuk mengubah isi dari kedua jenis tersebut.

Bahaya muncul ketika struktur suatu objek diubah, terutama dalam kasus dictionary. Menambahkan atau menghapus kunci dapat memicu rehash. Jika hard ISR berjalan saat rehash sedang berlangsung dan mencoba mengakses item, crash dapat terjadi. Secara internal, global diimplementasikan sebagai dictionary. Akibatnya, program utama harus membuat semua global yang diperlukan sebelum memulai proses yang menghasilkan hard interrupt. Kode aplikasi juga harus menghindari penghapusan global.

MicroPython mendukung integer dengan presisi arbitrer. Nilai antara 230 -1 dan -230 akan disimpan dalam satu machine word. Nilai yang lebih besar disimpan sebagai objek Python. Akibatnya, perubahan pada integer panjang tidak dapat dianggap atomik. Penggunaan integer panjang dalam ISR tidak aman karena alokasi memori dapat dicoba saat nilai variabel berubah.

Mengatasi keterbatasan float

Secara umum, sebaiknya hindari penggunaan float dalam kode ISR: perangkat hardware biasanya menangani integer dan konversi ke float biasanya dilakukan di loop utama. Namun ada beberapa algoritma DSP yang memerlukan floating point. Pada platform dengan hardware floating point (seperti OpenMV Cam berbasis STM32) inline ARM Thumb assembler dapat digunakan untuk mengatasi keterbatasan ini. Ini karena prosesor menyimpan nilai float dalam machine word; nilai dapat dibagikan antara ISR dan kode program utama melalui array float.

Menggunakan micropython.schedule

Fungsi ini memungkinkan ISR untuk menjadwalkan callback untuk dieksekusi "segera". Callback diantrekan untuk dieksekusi yang akan terjadi pada saat heap tidak dikunci. Karenanya, callback dapat membuat objek Python dan menggunakan float. Callback juga dijamin berjalan pada saat program utama telah menyelesaikan pembaruan objek Python apa pun, sehingga callback tidak akan menemui objek yang diperbarui sebagian.

Penggunaan tipikal adalah untuk menangani hardware sensor. ISR memperoleh data dari hardware dan memungkinkannya untuk mengeluarkan interupsi lebih lanjut. ISR kemudian menjadwalkan callback untuk memproses data.

Callback yang dijadwalkan harus mematuhi prinsip desain interrupt handler yang diuraikan di bawah. Ini untuk menghindari masalah yang diakibatkan oleh aktivitas I/O dan modifikasi data bersama yang dapat timbul dalam kode apa pun yang mendahului loop program utama.

Waktu eksekusi perlu dipertimbangkan dalam kaitannya dengan frekuensi terjadinya interupsi. Jika interupsi terjadi sementara callback sebelumnya sedang dieksekusi, instance lebih lanjut dari callback akan diantrekan untuk dieksekusi; ini akan berjalan setelah instance saat ini selesai. Oleh karena itu, laju pengulangan interupsi yang tinggi dan berkelanjutan membawa risiko pertumbuhan antrian yang tidak terkendali dan kegagalan akhirnya dengan RuntimeError.

Jika callback yang akan diteruskan ke schedule() adalah bound method, perhatikan catatan di "Pembuatan objek Python".

Exception

Jika ISR memunculkan exception, exception tersebut tidak akan merambat ke loop utama. Interupsi akan dinonaktifkan kecuali exception ditangani oleh kode ISR.

Antarmuka ke asyncio

Ketika ISR berjalan, ISR dapat mendahului penjadwal asyncio. Jika ISR melakukan operasi asyncio, operasi penjadwal dapat terganggu. Hal ini berlaku baik interupsi bersifat hard maupun soft dan juga berlaku jika ISR telah meneruskan eksekusi ke fungsi lain melalui micropython.schedule. Secara khusus, membuat atau membatalkan task tidak valid dalam konteks ISR. Cara aman untuk berinteraksi dengan asyncio adalah dengan mengimplementasikan coroutine dengan sinkronisasi yang dilakukan oleh asyncio.ThreadSafeFlag. Fragmen berikut mengilustrasikan pembuatan task sebagai respons terhadap interupsi:

tsf = asyncio.ThreadSafeFlag()


def isr(_):  # Interrupt handler
    tsf.set()


async def foo():
    while True:
        await tsf.wait()
        asyncio.create_task(bar())

Dalam contoh ini akan ada jumlah latensi yang bervariasi antara eksekusi ISR dan eksekusi foo(). Ini merupakan hal yang inheren dalam cooperative scheduling. Latensi maksimum bergantung pada aplikasi dan platform tetapi biasanya dapat diukur dalam puluhan ms.

Masalah umum

Ini hanyalah pengenalan singkat tentang subjek pemrograman waktu nyata. Pemula harus mencatat bahwa kesalahan desain dalam program waktu nyata dapat menyebabkan kesalahan yang sangat sulit didiagnosis. Ini karena kesalahan tersebut dapat terjadi jarang dan pada interval yang pada dasarnya acak. Sangat penting untuk mendapatkan desain awal yang benar dan mengantisipasi masalah sebelum muncul. Baik interrupt handler maupun program utama perlu dirancang dengan mempertimbangkan masalah-masalah berikut.

Desain interrupt handler

Seperti disebutkan di atas, ISR harus dirancang agar sesederhana mungkin. ISR harus selalu kembali dalam waktu yang singkat dan dapat diprediksi. Ini penting karena ketika ISR berjalan, loop utama tidak berjalan: loop utama pasti mengalami jeda dalam eksekusinya pada titik-titik acak dalam kode. Jeda seperti itu dapat menjadi sumber bug yang sulit didiagnosis terutama jika durasinya panjang atau bervariasi. Untuk memahami implikasi waktu jalannya ISR, diperlukan pemahaman dasar tentang prioritas interupsi.

Interupsi diorganisasi menurut skema prioritas. Kode ISR itu sendiri dapat diinterupsi oleh interupsi dengan prioritas lebih tinggi. Hal ini memiliki implikasi jika dua interupsi berbagi data (lihat Critical Sections di bawah). Jika interupsi seperti itu terjadi, interupsi tersebut menyisipkan penundaan ke dalam kode ISR. Jika interupsi dengan prioritas lebih rendah terjadi sementara ISR sedang berjalan, interupsi tersebut akan ditunda hingga ISR selesai: jika penundaannya terlalu lama, interupsi dengan prioritas lebih rendah mungkin gagal. Masalah lebih lanjut dengan ISR yang lambat adalah kasus ketika interupsi kedua dari jenis yang sama terjadi selama eksekusinya. Interupsi kedua akan ditangani saat interupsi pertama berakhir. Namun jika laju interupsi yang masuk secara konsisten melebihi kapasitas ISR untuk melayaninya, hasilnya tidak akan menyenangkan.

Oleh karena itu, konstruksi perulangan harus dihindari atau diminimalkan. I/O ke perangkat selain perangkat yang menginterupsi harus dihindari: I/O seperti akses disk, pernyataan print, dan akses UART relatif lambat, dan durasinya dapat bervariasi. Masalah lebih lanjut di sini adalah fungsi filesystem tidak bersifat reentrant: menggunakan filesystem I/O dalam ISR dan program utama akan berbahaya. Yang sangat penting, kode ISR tidak boleh menunggu suatu peristiwa. I/O dapat diterima jika kode dapat dijamin kembali dalam periode yang dapat diprediksi, misalnya mengubah status pin atau LED. Mengakses perangkat yang menginterupsi melalui I2C atau SPI mungkin diperlukan, tetapi waktu yang dibutuhkan untuk akses tersebut harus dihitung atau diukur dan dampaknya pada aplikasi harus dinilai.

Biasanya perlu membagikan data antara ISR dan loop utama. Ini dapat dilakukan melalui variabel global atau melalui variabel kelas atau instance. Variabel biasanya bertipe integer atau boolean, atau array integer atau byte (array integer yang dialokasikan sebelumnya menawarkan akses lebih cepat daripada list). Jika beberapa nilai dimodifikasi oleh ISR, perlu mempertimbangkan kasus di mana interupsi terjadi pada saat program utama telah mengakses sebagian, tetapi tidak semua, dari nilai-nilai tersebut. Ini dapat menyebabkan inkonsistensi.

Pertimbangkan desain berikut. ISR menyimpan data yang masuk dalam bytearray, kemudian menambahkan jumlah byte yang diterima ke integer yang mewakili total byte yang siap untuk diproses. Program utama membaca jumlah byte, memproses byte, kemudian menghapus jumlah byte yang siap. Ini akan berfungsi sampai interupsi terjadi tepat setelah program utama membaca jumlah byte. ISR memasukkan data yang ditambahkan ke dalam buffer dan memperbarui jumlah yang diterima, tetapi program utama sudah membaca jumlahnya, sehingga memproses data yang diterima semula. Byte yang baru tiba hilang.

Ada berbagai cara untuk menghindari bahaya ini, yang paling sederhana adalah menggunakan circular buffer. Jika tidak memungkinkan untuk menggunakan struktur dengan thread safety yang inheren, cara lain dijelaskan di bawah.

Reentrancy

Bahaya potensial dapat terjadi jika suatu fungsi atau metode dibagikan antara program utama dan satu atau lebih ISR atau antara beberapa ISR. Masalahnya adalah bahwa fungsi itu sendiri mungkin diinterupsi dan instance lebih lanjut dari fungsi tersebut dijalankan. Jika ini harus terjadi, fungsi harus dirancang agar bersifat reentrant. Cara melakukan ini adalah topik lanjutan di luar cakupan tutorial ini.

Critical section

Contoh critical section kode adalah kode yang mengakses lebih dari satu variabel yang dapat dipengaruhi oleh ISR. Jika interupsi kebetulan terjadi di antara akses ke variabel individual, nilainya akan tidak konsisten. Ini adalah contoh bahaya yang dikenal sebagai race condition: ISR dan loop program utama berlomba untuk mengubah variabel. Untuk menghindari inkonsistensi, harus digunakan suatu cara untuk memastikan bahwa ISR tidak mengubah nilai selama durasi critical section. Salah satu cara untuk mencapai ini adalah dengan mengeluarkan machine.disable_irq() sebelum awal bagian, dan machine.enable_irq() di akhir. Berikut adalah contoh pendekatan ini:

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 dapat terdiri dari satu baris kode dan satu variabel. Pertimbangkan fragmen kode berikut.

count = 0


def cb(): # An interrupt callback
    count += 1


def main():
    # Code to set up the interrupt callback omitted
    while True:
        count += 1

Contoh ini mengilustrasikan sumber bug yang halus. Baris count += 1 di loop utama membawa bahaya race condition tertentu yang dikenal sebagai read-modify-write. Ini adalah penyebab klasik bug dalam sistem waktu nyata. Di loop utama, MicroPython membaca nilai count, menambahkan 1 padanya, dan menuliskannya kembali. Sesekali interupsi terjadi setelah pembacaan dan sebelum penulisan. Interupsi memodifikasi count tetapi perubahannya ditimpa oleh loop utama ketika ISR kembali. Dalam sistem nyata, ini dapat menyebabkan kegagalan yang langka dan tidak dapat diprediksi.

Seperti disebutkan di atas, perhatian harus diberikan jika instance dari tipe bawaan Python dimodifikasi dalam kode utama dan instance tersebut diakses dalam ISR. Kode yang melakukan modifikasi harus dianggap sebagai critical section untuk memastikan bahwa instance dalam keadaan valid ketika ISR berjalan.

Perhatian khusus perlu diberikan jika dataset dibagikan antara ISR yang berbeda. Bahayanya adalah bahwa interupsi dengan prioritas lebih tinggi mungkin terjadi ketika interupsi dengan prioritas lebih rendah telah memperbarui data bersama sebagian. Menangani situasi ini adalah topik lanjutan di luar cakupan pengantar ini, selain untuk dicatat bahwa objek mutex yang dijelaskan di bawah terkadang dapat digunakan.

Menonaktifkan interupsi selama durasi critical section adalah cara yang biasa dan paling sederhana untuk dilanjutkan, tetapi ini menonaktifkan semua interupsi daripada hanya yang berpotensi menyebabkan masalah. Umumnya tidak diinginkan untuk menonaktifkan interupsi dalam waktu lama. Dalam kasus timer interrupt, ini memperkenalkan variabilitas pada waktu ketika callback terjadi. Dalam kasus device interrupt, hal ini dapat menyebabkan perangkat dilayani terlambat dengan kemungkinan kehilangan data atau overrun error dalam hardware perangkat. Seperti ISR, critical section dalam kode utama harus memiliki durasi yang singkat dan dapat diprediksi.

Pendekatan untuk menangani critical section yang secara drastis mengurangi waktu interupsi dinonaktifkan adalah dengan menggunakan objek yang disebut mutex (nama berasal dari gagasan mutual exclusion). Program utama mengunci mutex sebelum menjalankan critical section dan membuka kuncinya di akhir. ISR menguji apakah mutex dikunci. Jika ya, ISR menghindari critical section dan kembali. Tantangan desain adalah mendefinisikan apa yang harus dilakukan ISR jika akses ke variabel kritis ditolak. Contoh sederhana mutex dapat ditemukan di sini. Perhatikan bahwa kode mutex memang menonaktifkan interupsi, tetapi hanya selama delapan instruksi mesin: manfaat dari pendekatan ini adalah bahwa interupsi lain hampir tidak terpengaruh.

Interupsi dan REPL

Interrupt handler, seperti yang terkait dengan timer, dapat terus berjalan setelah program berakhir. Ini mungkin menghasilkan hasil yang tidak terduga di mana Anda mungkin mengharapkan objek yang memunculkan callback telah keluar dari cakupan. Misalnya pada OpenMV Cam:

def bar():
    foo = machine.Timer(-1, freq=4, callback=lambda t: print('.', end=''), hard=True)

bar()

Ini terus berjalan sampai timer secara eksplisit dinonaktifkan atau board direset dengan Ctrl-D.