Pisanje rukovatelja prekidima

Na prikladnom sklopovlju MicroPython nudi mogućnost pisanja rukovatelja prekidima u Pythonu. Rukovatelji prekidima - poznati i kao prekidne uslužne rutine (ISR-ovi) - definirani su kao funkcije povratnog poziva. One se izvršavaju kao odgovor na događaj poput okidanja mjerača vremena ili promjene napona na pinu. Takvi se događaji mogu dogoditi u bilo kojem trenutku izvršavanja programskog koda. To nosi značajne posljedice, neke specifične za jezik MicroPython. Druge su zajedničke svim sustavima sposobnim odgovarati na događaje u stvarnom vremenu. Ovaj dokument najprije obrađuje pitanja specifična za jezik, a zatim slijedi kratak uvod u programiranje u stvarnom vremenu za one koji su novi u tome.

Ovaj uvod koristi neodređene izraze poput „sporo” ili „što je brže moguće”. To je namjerno, jer brzine ovise o aplikaciji. Prihvatljiva trajanja ISR-a ovise o stopi kojom se prekidi pojavljuju, prirodi glavnog programa i prisutnosti drugih istodobnih događaja.

Pitanja vezana uz MicroPython

Međuspremnik za hitne iznimke

Ako u ISR-u dođe do pogreške, MicroPython ne može izraditi izvješće o pogrešci osim ako se za tu svrhu ne stvori poseban međuspremnik. Otklanjanje pogrešaka je pojednostavljeno ako je sljedeći kod uključen u bilo koji program koji koristi prekide.

import micropython

micropython.alloc_emergency_exception_buf(100)

Međuspremnik za hitne iznimke može sadržavati samo jedan trag stoga iznimke. To znači da ako se druga iznimka izbaci tijekom rukovanja iznimkom dok je gomila zaključana, trag stoga te druge iznimke zamijenit će izvorni - čak i ako se druga iznimka uredno obradi. To može dovesti do zbunjujućih poruka o iznimkama ako se međuspremnik kasnije ispiše.

Jednostavnost

Iz raznih je razloga važno da kod ISR-a bude što kraći i jednostavniji. Trebao bi raditi samo ono što se mora obaviti odmah nakon događaja koji ga je izazvao: operacije koje se mogu odgoditi treba delegirati glavnoj programskoj petlji. Tipično će ISR rukovati sklopovskim uređajem koji je izazvao prekid, pripremajući ga za sljedeći prekid. Komunicirat će s glavnom petljom ažuriranjem dijeljenih podataka kako bi naznačio da se prekid dogodio, te će se vratiti. ISR bi trebao vratiti kontrolu glavnoj petlji što je brže moguće. Ovo nije pitanje specifično za MicroPython pa je detaljnije obrađeno u nastavku.

Komunikacija između ISR-a i glavnog programa

Obično ISR treba komunicirati s glavnim programom. Najjednostavniji način za to je putem jednog ili više dijeljenih podatkovnih objekata, deklariranih kao globalni ili dijeljenih putem klase (vidi dolje). Postoje razna ograničenja i opasnosti oko toga, koja su detaljnije obrađena u nastavku. Cijeli brojevi, objekti bytes i bytearray često se koriste u tu svrhu zajedno s poljima (iz modula array) koja mogu pohraniti razne tipove podataka.

Korištenje metoda objekta kao povratnih poziva

MicroPython podržava ovu moćnu tehniku koja omogućuje ISR-u dijeljenje varijabli instance s temeljnim kodom. Također omogućuje klasi koja implementira upravljački program uređaja podršku za više instanci uređaja. Sljedeći primjer uzrokuje treperenje dviju LED dioda različitim brzinama.

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

U ovom primjeru instanca red upravlja crvenom LED diodom iz virtualnog mjerača vremena od 1 Hz: svaki put kad se mjerač vremena okine, poziva se red.cb(), prebacujući stanje crvene LED diode. Instanca green radi slično s mjeračem vremena od 0,8 Hz prebacujući stanje zelene LED diode. Korištenje metoda instance donosi dvije prednosti. Prvo, jedna klasa omogućuje dijeljenje koda između više sklopovskih instanci. Drugo, kao vezana metoda, prvi argument funkcije povratnog poziva je self. To omogućuje povratnom pozivu pristup podacima instance i spremanje stanja između uzastopnih poziva. Na primjer, kad bi gornja klasa imala varijablu self.count postavljenu na nulu u konstruktoru, cb() bi mogao povećavati brojač. Instance red i green tada bi održavale neovisne brojeve koliko je puta svaka LED dioda promijenila stanje.

Stvaranje Python objekata

ISR-ovi ne mogu stvarati instance Python objekata. To je zato što MicroPython treba dodijeliti memoriju za objekt iz zalihe slobodnih memorijskih blokova zvane heap. To nije dopušteno u rukovatelju prekidima jer dodjela gomile nije ponovno ulazna. Drugim riječima, prekid se može dogoditi dok glavni program djelomično izvršava dodjelu - kako bi se očuvao integritet gomile, interpreter zabranjuje dodjele memorije u kodu ISR-a.

Posljedica toga je da ISR-ovi ne mogu koristiti aritmetiku s pomičnim zarezom; to je zato što su decimalni brojevi Python objekti. Slično, ISR ne može dodati stavku na listu. U praksi može biti teško točno odrediti koje će konstrukcije koda pokušati izvršiti dodjelu memorije i izazvati poruku o pogrešci: još jedan razlog da kod ISR-a bude kratak i jednostavan.

Jedan način izbjegavanja ovog problema je da ISR koristi unaprijed dodijeljene međuspremnike. Na primjer, konstruktor klase stvara instancu bytearray i logičku zastavicu. Metoda ISR-a dodjeljuje podatke lokacijama u međuspremniku i postavlja zastavicu. Dodjela memorije događa se u kodu glavnog programa kad se objekt instancira, a ne u ISR-u.

Metode ulaza/izlaza biblioteke MicroPython obično pružaju opciju korištenja unaprijed dodijeljenog međuspremnika. Na primjer machine.I2C.readfrom_into() čita u promjenjivi međuspremnik koji dostavlja pozivatelj: to omogućuje njegovu uporabu u ISR-u.

Način stvaranja objekta bez korištenja klase ili globalnih varijabli je sljedeći:

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

Prevoditelj instancira zadani argument buf kad se funkcija učita prvi put (obično kad se modul u kojem se nalazi uveze).

Instanca stvaranja objekta događa se kad se stvori referenca na vezanu metodu. To znači da ISR ne može proslijediti vezanu metodu funkciji. Jedno rješenje je stvoriti referencu na vezanu metodu u konstruktoru klase te proslijediti tu referencu u ISR-u. Na primjer:

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)

Druge su tehnike definiranje i instanciranje metode u konstruktoru ili prosljeđivanje Foo.bar() s argumentom self.

Korištenje Python objekata

Dodatno ograničenje na objekte proizlazi iz načina na koji Python radi. Kad se izvrši naredba import, Python kod se prevodi u bytecode, pri čemu se jedna linija koda tipično preslikava na više bajtkodova. Kad se kod izvršava, interpreter čita svaki bajtkod i izvršava ga kao niz instrukcija strojnog koda. S obzirom na to da se prekid može dogoditi u bilo kojem trenutku između instrukcija strojnog koda, izvorna linija Python koda može biti samo djelomično izvršena. Posljedično, Python objekt poput skupa, liste ili rječnika izmijenjen u glavnoj petlji može u trenutku kad se prekid dogodi biti bez unutarnje dosljednosti.

Tipičan ishod je sljedeći. U rijetkim će prilikama ISR raditi u točnom trenutku kad je objekt djelomično ažuriran. Kad ISR pokuša pročitati objekt, rezultat je rušenje. Budući da se takvi problemi tipično pojavljuju u rijetkim, nasumičnim prilikama, mogu se teško dijagnosticirati. Postoje načini za zaobilaženje ovog problema, opisani u Kritični odjeljci u nastavku.

Važno je biti jasan u pogledu onoga što čini izmjenu objekta. Mijenjanje sadržaja polja ili bytearraya je sigurno. To je zato što se bajtovi ili riječi zapisuju kao jedna instrukcija strojnog koda koja se ne može prekinuti: u terminologiji programiranja u stvarnom vremenu, zapis je atomaran. Isto vrijedi za ažuriranje stavke rječnika jer su stavke strojne riječi, budući da su cijeli brojevi ili pokazivači na objekte. Korisnički definiran objekt može instancirati polje ili bytearray. Ispravno je da i glavna petlja i ISR mijenjaju sadržaj tih.

Opasnost nastaje kad se izmijeni struktura objekta, osobito u slučaju rječnika. Dodavanje ili brisanje ključeva može pokrenuti ponovno raspršivanje. Ako se tvrdi ISR izvršava dok je ponovno raspršivanje u tijeku i pokuša pristupiti stavci, može doći do rušenja. Interno su globalne varijable implementirane kao rječnik. Posljedično, glavni bi program trebao stvoriti sve potrebne globalne varijable prije pokretanja procesa koji generira tvrde prekide. Aplikacijski kod također bi trebao izbjegavati brisanje globalnih varijabli.

MicroPython podržava cijele brojeve proizvoljne preciznosti. Vrijednosti između 230 -1 i -230 pohranit će se u jednu strojnu riječ. Veće vrijednosti pohranjuju se kao Python objekti. Posljedično, promjene dugih cijelih brojeva ne mogu se smatrati atomarnima. Korištenje dugih cijelih brojeva u ISR-ovima nije sigurno jer se dodjela memorije može pokušati kad se vrijednost varijable promijeni.

Prevladavanje ograničenja decimalnih brojeva

Općenito je najbolje izbjegavati korištenje decimalnih brojeva u kodu ISR-a: sklopovski uređaji obično rukuju cijelim brojevima, a pretvorba u decimalne brojeve obično se obavlja u glavnoj petlji. Međutim, postoji nekoliko DSP algoritama koji zahtijevaju pomični zarez. Na platformama sa sklopovskim pomičnim zarezom (poput OpenMV Cam uređaja temeljenih na STM32) za zaobilaženje ovog ograničenja može se koristiti ugrađeni ARM Thumb asembler. To je zato što procesor pohranjuje decimalne vrijednosti u strojnu riječ; vrijednosti se mogu dijeliti između ISR-a i koda glavnog programa putem polja decimalnih brojeva.

Korištenje micropython.schedule

Ova funkcija omogućuje ISR-u zakazivanje povratnog poziva za izvršenje „vrlo uskoro”. Povratni poziv stavlja se u red za izvršenje koje će se dogoditi u trenutku kad gomila nije zaključana. Stoga može stvarati Python objekte i koristiti decimalne brojeve. Također je zajamčeno da će se povratni poziv izvršiti u trenutku kad je glavni program dovršio svako ažuriranje Python objekata, tako da povratni poziv neće naići na djelomično ažurirane objekte.

Tipična upotreba je rukovanje senzorskim sklopovljem. ISR prikuplja podatke iz sklopovlja i omogućuje mu da izda dodatni prekid. Zatim zakazuje povratni poziv za obradu podataka.

Zakazani povratni pozivi trebali bi se pridržavati načela dizajna rukovatelja prekidima opisanih u nastavku. To je radi izbjegavanja problema koji proizlaze iz ulazno/izlazne aktivnosti i izmjene dijeljenih podataka koji se mogu pojaviti u svakom kodu koji preuzima prednost pred glavnom programskom petljom.

Vrijeme izvršenja treba uzeti u obzir u odnosu na frekvenciju kojom se prekidi mogu pojaviti. Ako se prekid dogodi dok se prethodni povratni poziv izvršava, dodatna instanca povratnog poziva bit će stavljena u red za izvršenje; ona će se izvršiti nakon što trenutna instanca završi. Trajno visoka stopa ponavljanja prekida stoga nosi rizik neograničenog rasta reda i konačnog kvara s RuntimeError.

Ako je povratni poziv koji se prosljeđuje funkciji schedule() vezana metoda, razmotrite napomenu u „Stvaranje Python objekata”.

Iznimke

Ako ISR izbaci iznimku, ona se neće proširiti na glavnu petlju. Prekid će biti onemogućen osim ako iznimku obradi kod ISR-a.

Povezivanje s asyncio

Kad se ISR izvršava, može preuzeti prednost pred asyncio rasporediteljem. Ako ISR obavi asyncio operaciju, rad rasporeditelja može biti poremećen. To vrijedi bez obzira je li prekid tvrd ili mek, a vrijedi i ako je ISR proslijedio izvršenje drugoj funkciji putem micropython.schedule. Konkretno, stvaranje ili otkazivanje zadataka nije valjano u kontekstu ISR-a. Siguran način interakcije s asyncio je implementacija korutine sa sinkronizacijom koju obavlja asyncio.ThreadSafeFlag. Sljedeći ulomak ilustrira stvaranje zadatka kao odgovor na prekid:

tsf = asyncio.ThreadSafeFlag()


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


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

U ovom primjeru bit će promjenjive količine kašnjenja između izvršenja ISR-a i izvršenja foo(). To je svojstveno kooperativnom raspoređivanju. Maksimalno kašnjenje ovisi o aplikaciji i platformi, ali se tipično može mjeriti u desecima ms.

Opća pitanja

Ovo je tek kratak uvod u temu programiranja u stvarnom vremenu. Početnici bi trebali primijetiti da pogreške u dizajnu programa u stvarnom vremenu mogu dovesti do kvarova koji se osobito teško dijagnosticiraju. To je zato što se mogu pojaviti rijetko i u intervalima koji su u biti nasumični. Ključno je dobro postaviti početni dizajn i predvidjeti probleme prije nego što nastanu. I rukovatelji prekidima i glavni program moraju biti dizajnirani uz razumijevanje sljedećih pitanja.

Dizajn rukovatelja prekidima

Kao što je gore spomenuto, ISR-ovi bi trebali biti dizajnirani da budu što jednostavniji. Trebali bi se uvijek vratiti u kratkom, predvidljivom razdoblju vremena. To je važno jer dok se ISR izvršava, glavna petlja se ne izvršava: glavna petlja neizbježno doživljava pauze u svom izvršavanju na nasumičnim mjestima u kodu. Takve pauze mogu biti izvor pogrešaka koje se teško dijagnosticiraju, osobito ako je njihovo trajanje dugo ili promjenjivo. Kako bi se razumjele implikacije vremena izvršavanja ISR-a, potrebno je osnovno razumijevanje prioriteta prekida.

Prekidi su organizirani prema shemi prioriteta. Kod ISR-a sam može biti prekinut prekidom višeg prioriteta. To ima implikacije ako dva prekida dijele podatke (vidi Kritični odjeljci u nastavku). Ako se takav prekid dogodi, on unosi kašnjenje u kod ISR-a. Ako se prekid nižeg prioriteta dogodi dok se ISR izvršava, bit će odgođen dok ISR ne završi: ako je kašnjenje predugo, prekid nižeg prioriteta može zakazati. Daljnji problem sa sporim ISR-ovima je slučaj u kojem se drugi prekid istog tipa dogodi tijekom njegova izvršavanja. Drugi prekid bit će obrađen po završetku prvog. Međutim, ako stopa dolaznih prekida dosljedno premašuje kapacitet ISR-a da ih obrađuje, ishod neće biti sretan.

Posljedično, petljaste konstrukcije trebalo bi izbjegavati ili svesti na minimum. Ulaz/izlaz prema uređajima osim prema uređaju koji prekida obično bi trebalo izbjegavati: ulaz/izlaz poput pristupa disku, naredbi print i pristupa UART-u relativno je spor, a njegovo trajanje može varirati. Daljnji problem ovdje je da funkcije datotečnog sustava nisu ponovno ulazne: korištenje ulaza/izlaza datotečnog sustava u ISR-u i glavnom programu bilo bi opasno. Ključno je da kod ISR-a ne smije čekati na događaj. Ulaz/izlaz je prihvatljiv ako se može jamčiti da će se kod vratiti u predvidljivom razdoblju, na primjer prebacivanje stanja pina ili LED diode. Pristup uređaju koji prekida putem I2C ili SPI može biti potreban, ali vrijeme potrebno za takve pristupe treba izračunati ili izmjeriti te procijeniti njegov utjecaj na aplikaciju.

Obično postoji potreba za dijeljenjem podataka između ISR-a i glavne petlje. To se može učiniti bilo putem globalnih varijabli bilo putem varijabli klase ili instance. Varijable su tipično tipa cijelog broja ili logičke vrijednosti, ili polja cijelih brojeva ili bajtova (unaprijed dodijeljeno polje cijelih brojeva nudi brži pristup od liste). Tamo gdje ISR mijenja više vrijednosti, potrebno je razmotriti slučaj u kojem se prekid dogodi u trenutku kad je glavni program pristupio nekima, ali ne svim vrijednostima. To može dovesti do nedosljednosti.

Razmotrite sljedeći dizajn. ISR pohranjuje dolazne podatke u bytearray, zatim dodaje broj primljenih bajtova cijelom broju koji predstavlja ukupan broj bajtova spremnih za obradu. Glavni program čita broj bajtova, obrađuje bajtove, zatim poništava broj bajtova spremnih. To će raditi sve dok se prekid ne dogodi neposredno nakon što je glavni program pročitao broj bajtova. ISR stavlja dodane podatke u međuspremnik i ažurira broj primljenih, ali je glavni program već pročitao broj, pa obrađuje izvorno primljene podatke. Novopristigli bajtovi su izgubljeni.

Postoje razni načini izbjegavanja ove opasnosti, a najjednostavniji je korištenje kružnog međuspremnika. Ako nije moguće koristiti strukturu s inherentnom sigurnošću dretvi, drugi su načini opisani u nastavku.

Ponovni ulaz

Potencijalna opasnost može nastati ako se funkcija ili metoda dijeli između glavnog programa i jednog ili više ISR-ova ili između više ISR-ova. Problem je ovdje u tome što funkcija sama može biti prekinuta i može se pokrenuti dodatna instanca te funkcije. Ako se to ima dogoditi, funkcija mora biti dizajnirana da bude ponovno ulazna. Kako se to radi napredna je tema izvan opsega ovog vodiča.

Kritični odjeljci

Primjer kritičnog odjeljka koda je onaj koji pristupa više od jednoj varijabli na koju ISR može utjecati. Ako se prekid dogodi između pristupa pojedinim varijablama, njihove će vrijednosti biti nedosljedne. To je primjer opasnosti poznate kao stanje natjecanja: ISR i glavna programska petlja natječu se u mijenjanju varijabli. Kako bi se izbjegla nedosljednost, mora se primijeniti sredstvo koje osigurava da ISR ne mijenja vrijednosti tijekom trajanja kritičnog odjeljka. Jedan način postizanja toga je izdavanje machine.disable_irq() prije početka odjeljka i machine.enable_irq() na kraju. Evo primjera ovog pristupa:

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

Kritični odjeljak može se sastojati od jedne linije koda i jedne varijable. Razmotrite sljedeći ulomak koda.

count = 0


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


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

Ovaj primjer ilustrira suptilan izvor pogrešaka. Linija count += 1 u glavnoj petlji nosi specifičnu opasnost stanja natjecanja poznatu kao čitaj-mijenjaj-zapiši. To je klasičan uzrok pogrešaka u sustavima u stvarnom vremenu. U glavnoj petlji MicroPython čita vrijednost count, dodaje joj 1 i zapisuje je natrag. U rijetkim se prilikama prekid dogodi nakon čitanja i prije zapisivanja. Prekid mijenja count, ali njegovu promjenu prepisuje glavna petlja kad se ISR vrati. U stvarnom sustavu to bi moglo dovesti do rijetkih, nepredvidljivih kvarova.

Kao što je gore spomenuto, treba biti oprezan ako se instanca ugrađenog Python tipa izmijeni u glavnom kodu i toj se instanci pristupa u ISR-u. Kod koji obavlja izmjenu treba smatrati kritičnim odjeljkom kako bi se osiguralo da je instanca u valjanom stanju kad se ISR izvršava.

Posebnu pažnju treba obratiti ako se skup podataka dijeli između različitih ISR-ova. Opasnost je ovdje u tome što se prekid višeg prioriteta može dogoditi kad je onaj nižeg prioriteta djelomično ažurirao dijeljene podatke. Rješavanje ove situacije napredna je tema izvan opsega ovog uvoda, osim napomene da se ponekad mogu koristiti mutex objekti opisani u nastavku.

Onemogućivanje prekida tijekom trajanja kritičnog odjeljka uobičajen je i najjednostavniji način postupanja, ali onemogućuje sve prekide, a ne samo onaj koji ima potencijal izazvati probleme. Općenito je nepoželjno onemogućiti prekid na dulje vrijeme. U slučaju prekida mjerača vremena to unosi promjenjivost u vrijeme kad se povratni poziv dogodi. U slučaju prekida uređaja to može dovesti do prekasnog opsluživanja uređaja s mogućim gubitkom podataka ili pogreškama prekoračenja u sklopovlju uređaja. Poput ISR-ova, kritični odjeljak u glavnom kodu trebao bi imati kratko, predvidljivo trajanje.

Pristup rješavanju kritičnih odjeljaka koji radikalno smanjuje vrijeme tijekom kojeg su prekidi onemogućeni je korištenje objekta zvanog mutex (naziv izveden iz pojma međusobnog isključivanja). Glavni program zaključava mutex prije izvršavanja kritičnog odjeljka i otključava ga na kraju. ISR provjerava je li mutex zaključan. Ako jest, izbjegava kritični odjeljak i vraća se. Izazov dizajna je definiranje onoga što bi ISR trebao učiniti u slučaju da je pristup kritičnim varijablama uskraćen. Jednostavan primjer mutexa može se pronaći ovdje. Imajte na umu da kod mutexa onemogućuje prekide, ali samo tijekom trajanja osam strojnih instrukcija: prednost ovog pristupa je da su drugi prekidi praktički nepogođeni.

Prekidi i REPL

Rukovatelji prekidima, poput onih povezanih s mjeračima vremena, mogu nastaviti raditi nakon što program završi. To može proizvesti neočekivane rezultate tamo gdje ste možda očekivali da je objekt koji izaziva povratni poziv izašao iz opsega. Na primjer, na OpenMV Camu:

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

bar()

Ovo nastavlja raditi sve dok se mjerač vremena izričito ne onemogući ili se ploča ne resetira s Ctrl-D.