Skriva avbrottshanterare¶
På lämplig hårdvara erbjuder MicroPython möjligheten att skriva avbrottshanterare i Python. Avbrottshanterare - även kända som interrupt service routines (ISR) - definieras som återanropsfunktioner. Dessa körs som svar på en händelse, exempelvis en timerutlösning eller en spänningsändring på ett stift. Sådana händelser kan inträffa när som helst under exekveringen av programkoden. Detta för med sig betydande konsekvenser, varav vissa är specifika för MicroPython-språket. Andra är gemensamma för alla system som kan svara på realtidshändelser. Detta dokument behandlar först de språkspecifika frågorna, följt av en kort introduktion till realtidsprogrammering för dig som är ny på området.
Den här introduktionen använder vaga termer som ”långsam” eller ”så snabbt som möjligt”. Detta är avsiktligt, eftersom hastigheter är applikationsberoende. Acceptabla varaktigheter för en ISR beror på den takt med vilken avbrott inträffar, huvudprogrammets natur och förekomsten av andra samtidiga händelser.
Tips och rekommenderad praxis¶
Detta sammanfattar de punkter som beskrivs nedan och listar de viktigaste rekommendationerna för kod i avbrottshanterare.
Håll koden så kort och enkel som möjligt.
Undvik minnesallokering: ingen tillägg till listor eller infogning i ordlistor, ingen flyttalsaritmetik.
Överväg att använda
micropython.scheduleför att kringgå begränsningen ovan.Där en ISR returnerar flera byte, använd en förallokerad
bytearray. Om flera heltal ska delas mellan en ISR och huvudprogrammet, överväg en array (array.array).Där data delas mellan huvudprogrammet och en ISR, överväg att inaktivera avbrott innan du kommer åt data i huvudprogrammet och att återaktivera dem omedelbart efteråt (se Kritiska sektioner).
Allokera en buffert för nödfallsundantag (se nedan).
MicroPython-specifika frågor¶
Bufferten för nödfallsundantag¶
Om ett fel uppstår i en ISR kan MicroPython inte producera någon felrapport om inte en speciell buffert skapas för ändamålet. Felsökning förenklas om följande kod inkluderas i alla program som använder avbrott.
import micropython
micropython.alloc_emergency_exception_buf(100)
Bufferten för nödfallsundantag kan endast hålla en undantagsstackspårning. Detta innebär att om ett andra undantag kastas under hanteringen av ett undantag medan heapen är låst, kommer det andra undantagets stackspårning att ersätta den ursprungliga - även om det andra undantaget hanteras felfritt. Detta kan leda till förvirrande undantagsmeddelanden om bufferten skrivs ut senare.
Enkelhet¶
Av flera anledningar är det viktigt att hålla ISR-koden så kort och enkel som möjligt. Den bör endast göra det som måste göras omedelbart efter den händelse som orsakade den: operationer som kan skjutas upp bör delegeras till huvudprogrammets slinga. Vanligtvis hanterar en ISR den hårdvaruenhet som orsakade avbrottet och gör den redo för nästa avbrott. Den kommunicerar med huvudslingan genom att uppdatera delade data för att indikera att avbrottet har inträffat, och sedan returnerar den. En ISR bör återlämna kontrollen till huvudslingan så snabbt som möjligt. Detta är inte en specifik MicroPython-fråga och behandlas därför mer ingående nedan.
Kommunikation mellan en ISR och huvudprogrammet¶
Normalt behöver en ISR kommunicera med huvudprogrammet. Det enklaste sättet att göra detta är via ett eller flera delade dataobjekt, antingen deklarerade som globala eller delade via en klass (se nedan). Det finns olika begränsningar och faror kring detta, vilka behandlas mer ingående nedan. Heltal, bytes- och bytearray-objekt används ofta för detta ändamål tillsammans med arrayer (från modulen array) som kan lagra olika datatyper.
Användning av objektmetoder som återanrop¶
MicroPython stöder denna kraftfulla teknik som gör det möjligt för en ISR att dela instansvariabler med den underliggande koden. Den gör det också möjligt för en klass som implementerar en enhetsdrivrutin att stödja flera enhetsinstanser. Följande exempel får två lysdioder att blinka i olika takt.
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"))
I detta exempel driver instansen red den röda lysdioden från en virtuell 1 Hz-timer: varje gång timern utlöses anropas red.cb(), vilket växlar den röda lysdioden. Instansen green fungerar på liknande sätt med en 0,8 Hz-timer som växlar den gröna lysdioden. Användningen av instansmetoder ger två fördelar. För det första gör en enda klass det möjligt att dela kod mellan flera hårdvaruinstanser. För det andra är, eftersom det är en bunden metod, återanropsfunktionens första argument self. Detta gör det möjligt för återanropet att komma åt instansdata och att spara tillstånd mellan på varandra följande anrop. Om klassen ovan exempelvis hade en variabel self.count satt till noll i konstruktorn, skulle cb() kunna öka räknaren. Instanserna red och green skulle då upprätthålla oberoende räkningar av hur många gånger respektive lysdiod har bytt tillstånd.
Skapande av Python-objekt¶
En ISR kan inte skapa instanser av Python-objekt. Detta beror på att MicroPython behöver allokera minne för objektet från ett lager av lediga minnesblock som kallas heap. Detta är inte tillåtet i en avbrottshanterare eftersom heapallokering inte är reentrant. Med andra ord kan avbrottet inträffa när huvudprogrammet är mitt uppe i att utföra en allokering - för att bevara heapens integritet tillåter tolken inte minnesallokeringar i ISR-kod.
En följd av detta är att en ISR inte kan använda flyttalsaritmetik; detta beror på att flyttal är Python-objekt. På liknande sätt kan en ISR inte lägga till ett element i en lista. I praktiken kan det vara svårt att avgöra exakt vilka kodkonstruktioner som försöker utföra minnesallokering och framkalla ett felmeddelande: ytterligare en anledning att hålla ISR-koden kort och enkel.
Ett sätt att undvika detta problem är att låta ISR:en använda förallokerade buffertar. En klasskonstruktor skapar exempelvis en bytearray-instans och en boolesk flagga. ISR-metoden tilldelar data till positioner i bufferten och sätter flaggan. Minnesallokeringen sker i huvudprogramkoden när objektet instansieras snarare än i ISR:en.
MicroPythons I/O-metoder i biblioteket erbjuder vanligtvis ett alternativ att använda en förallokerad buffert. Exempelvis läser machine.I2C.readfrom_into() in i en föränderlig buffert som anroparen tillhandahåller: detta möjliggör dess användning i en ISR.
Ett sätt att skapa ett objekt utan att använda en klass eller globala variabler är följande:
def set_volume(t, buf=bytearray(3)):
buf[0] = 0xa5
buf[1] = t >> 4
buf[2] = 0x5a
return buf
Kompilatorn instansierar standardargumentet buf när funktionen laddas för första gången (vanligtvis när modulen den finns i importeras).
En instans av objektskapande sker när en referens till en bunden metod skapas. Detta innebär att en ISR inte kan skicka en bunden metod till en funktion. En lösning är att skapa en referens till den bundna metoden i klasskonstruktorn och att skicka den referensen i ISR:en. Till exempel:
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)
Andra tekniker är att definiera och instansiera metoden i konstruktorn eller att skicka Foo.bar() med argumentet self.
Användning av Python-objekt¶
En ytterligare begränsning för objekt uppstår på grund av hur Python fungerar. När en import-sats exekveras kompileras Python-koden till bytecode, där en kodrad vanligtvis mappas till flera bytecodes. När koden körs läser tolken varje bytecode och exekverar den som en serie maskinkodsinstruktioner. Eftersom ett avbrott kan inträffa när som helst mellan maskinkodsinstruktioner, kan den ursprungliga raden Python-kod vara endast delvis exekverad. Följaktligen kan ett Python-objekt som en mängd, lista eller ordlista som modifieras i huvudslingan sakna intern konsistens i det ögonblick avbrottet inträffar.
Ett typiskt utfall är följande. Vid sällsynta tillfällen körs ISR:en exakt i det ögonblick då objektet är delvis uppdaterat. När ISR:en försöker läsa objektet uppstår en krasch. Eftersom sådana problem typiskt inträffar vid sällsynta, slumpmässiga tillfällen kan de vara svåra att diagnostisera. Det finns sätt att kringgå detta problem, vilka beskrivs i Kritiska sektioner nedan.
Det är viktigt att vara tydlig med vad som utgör en modifiering av ett objekt. Att ändra innehållet i en array eller bytearray är säkert. Detta beror på att byte eller ord skrivs som en enda maskinkodsinstruktion som inte kan avbrytas: med realtidsprogrammeringens terminologi är skrivningen atomär. Detsamma gäller uppdatering av ett element i en ordlista eftersom elementen är maskinord, antingen heltal eller pekare till objekt. Ett användardefinierat objekt kan instansiera en array eller bytearray. Det är giltigt för både huvudslingan och ISR:en att ändra innehållet i dessa.
Faran uppstår när strukturen hos ett objekt ändras, särskilt i fallet med ordlistor. Att lägga till eller ta bort nycklar kan utlösa en omhashning. Om en hård ISR körs medan en omhashning pågår och försöker komma åt ett element, kan en krasch uppstå. Internt implementeras globala variabler som en ordlista. Följaktligen bör huvudprogrammet skapa alla nödvändiga globala variabler innan en process som genererar hårda avbrott startas. Applikationskoden bör också undvika att ta bort globala variabler.
MicroPython stöder heltal med godtycklig precision. Värden mellan 230 -1 och -230 lagras i ett enda maskinord. Större värden lagras som Python-objekt. Följaktligen kan ändringar av långa heltal inte betraktas som atomära. Användning av långa heltal i en ISR är osäker eftersom minnesallokering kan komma att försökas när variabelns värde ändras.
Att överkomma flyttalsbegränsningen¶
Generellt är det bäst att undvika att använda flyttal i ISR-kod: hårdvaruenheter hanterar normalt heltal och konvertering till flyttal görs normalt i huvudslingan. Det finns dock några få DSP-algoritmer som kräver flyttal. På plattformar med hårdvarustöd för flyttal (såsom de STM32-baserade OpenMV Cam-kamerorna) kan den inline-monterade ARM Thumb-assemblern användas för att kringgå denna begränsning. Detta beror på att processorn lagrar flyttalsvärden i ett maskinord; värden kan delas mellan ISR:en och huvudprogramkoden via en array av flyttal.
Använda micropython.schedule¶
Denna funktion gör det möjligt för en ISR att schemalägga ett återanrop för exekvering ”mycket snart”. Återanropet köas för exekvering som äger rum vid en tidpunkt då heapen inte är låst. Därför kan det skapa Python-objekt och använda flyttal. Återanropet är också garanterat att köras vid en tidpunkt då huvudprogrammet har slutfört eventuell uppdatering av Python-objekt, så återanropet kommer inte att stöta på delvis uppdaterade objekt.
Typisk användning är att hantera sensorhårdvara. ISR:en hämtar data från hårdvaran och gör det möjligt för den att utlösa ytterligare ett avbrott. Den schemalägger sedan ett återanrop för att bearbeta datan.
Schemalagda återanrop bör följa de principer för utformning av avbrottshanterare som beskrivs nedan. Detta för att undvika problem som beror på I/O-aktivitet och modifiering av delade data, vilka kan uppstå i all kod som föregriper huvudprogrammets slinga.
Exekveringstiden måste beaktas i förhållande till den frekvens med vilken avbrott kan inträffa. Om ett avbrott inträffar medan det föregående återanropet exekveras, kommer ytterligare en instans av återanropet att köas för exekvering; denna körs efter att den aktuella instansen har slutförts. En ihållande hög avbrottsupprepningstakt medför därför en risk för obegränsad kötillväxt och slutligt fel med ett RuntimeError.
Om återanropet som ska skickas till schedule() är en bunden metod, beakta anmärkningen i ”Skapande av Python-objekt”.
Undantag¶
Om en ISR kastar ett undantag kommer det inte att propagera till huvudslingan. Avbrottet kommer att inaktiveras om inte undantaget hanteras av ISR-koden.
Gränssnitt mot asyncio¶
När en ISR körs kan den föregripa asyncio-schemaläggaren. Om ISR:en utför en asyncio-operation kan schemaläggarens funktion störas. Detta gäller oavsett om avbrottet är hårt eller mjukt och gäller även om ISR:en har skickat exekveringen vidare till en annan funktion via micropython.schedule. I synnerhet är det ogiltigt att skapa eller avbryta uppgifter i ett ISR-sammanhang. Det säkra sättet att interagera med asyncio är att implementera en korutin med synkronisering utförd av asyncio.ThreadSafeFlag. Följande fragment illustrerar skapandet av en uppgift som svar på ett avbrott:
tsf = asyncio.ThreadSafeFlag()
def isr(_): # Interrupt handler
tsf.set()
async def foo():
while True:
await tsf.wait()
asyncio.create_task(bar())
I detta exempel kommer det att finnas en varierande mängd latens mellan exekveringen av ISR:en och exekveringen av foo(). Detta är inneboende i kooperativ schemaläggning. Den maximala latensen är applikations- och plattformsberoende men kan typiskt mätas i tiotals ms.
Allmänna frågor¶
Detta är endast en kort introduktion till ämnet realtidsprogrammering. Nybörjare bör notera att designfel i realtidsprogram kan leda till fel som är särskilt svåra att diagnostisera. Detta beror på att de kan inträffa sällan och med intervall som i huvudsak är slumpmässiga. Det är avgörande att få den initiala designen rätt och att förutse problem innan de uppstår. Både avbrottshanterare och huvudprogrammet behöver utformas med insikt i följande frågor.
Utformning av avbrottshanterare¶
Som nämnts ovan bör en ISR utformas för att vara så enkel som möjligt. Den bör alltid returnera inom en kort, förutsägbar tidsperiod. Detta är viktigt eftersom huvudslingan inte körs medan ISR:en körs: oundvikligen upplever huvudslingan pauser i sin exekvering vid slumpmässiga punkter i koden. Sådana pauser kan vara en källa till svårdiagnostiserade buggar, särskilt om deras varaktighet är lång eller variabel. För att förstå konsekvenserna av en ISR:s körtid krävs en grundläggande förståelse för avbrottsprioriteter.
Avbrott organiseras enligt ett prioritetsschema. ISR-kod kan i sig själv avbrytas av ett avbrott med högre prioritet. Detta får konsekvenser om de två avbrotten delar data (se Kritiska sektioner nedan). Om ett sådant avbrott inträffar inför det en fördröjning i ISR-koden. Om ett avbrott med lägre prioritet inträffar medan ISR:en körs, kommer det att fördröjas tills ISR:en är klar: om fördröjningen är för lång kan avbrottet med lägre prioritet misslyckas. En ytterligare fråga med långsamma ISR:er är fallet där ett andra avbrott av samma typ inträffar under dess exekvering. Det andra avbrottet hanteras när det första avslutas. Men om takten för inkommande avbrott konsekvent överstiger ISR:ens kapacitet att betjäna dem blir utfallet inte lyckligt.
Följaktligen bör slingkonstruktioner undvikas eller minimeras. I/O till andra enheter än den avbrytande enheten bör normalt undvikas: I/O såsom diskåtkomst, print-satser och UART-åtkomst är relativt långsam, och dess varaktighet kan variera. En ytterligare fråga här är att filsystemfunktioner inte är reentranta: att använda filsystem-I/O i en ISR och i huvudprogrammet skulle vara riskabelt. Avgörande är att ISR-kod inte bör vänta på en händelse. I/O är acceptabelt om koden kan garanteras returnera inom en förutsägbar period, exempelvis att växla ett stift eller en lysdiod. Att komma åt den avbrytande enheten via I2C eller SPI kan vara nödvändigt, men tiden som sådana åtkomster tar bör beräknas eller mätas och dess inverkan på applikationen bedömas.
Det finns vanligtvis ett behov av att dela data mellan ISR:en och huvudslingan. Detta kan göras antingen genom globala variabler eller via klass- eller instansvariabler. Variabler är typiskt heltals- eller booleska typer, eller heltals- eller byte-arrayer (en förallokerad heltalsarray erbjuder snabbare åtkomst än en lista). Där flera värden modifieras av ISR:en är det nödvändigt att beakta fallet där avbrottet inträffar vid en tidpunkt då huvudprogrammet har kommit åt några, men inte alla, av värdena. Detta kan leda till inkonsistenser.
Betrakta följande design. En ISR lagrar inkommande data i en bytearray och lägger sedan till antalet mottagna byte till ett heltal som representerar det totala antalet byte redo för bearbetning. Huvudprogrammet läser antalet byte, bearbetar dem och nollställer sedan antalet byte som är redo. Detta fungerar tills ett avbrott inträffar precis efter att huvudprogrammet har läst antalet byte. ISR:en lägger den tillagda datan i bufferten och uppdaterar det mottagna antalet, men huvudprogrammet har redan läst antalet och bearbetar därför den data som ursprungligen mottogs. De nyligen anlända byten går förlorade.
Det finns olika sätt att undvika denna fara, det enklaste är att använda en cirkulär buffert. Om det inte är möjligt att använda en struktur med inneboende trådsäkerhet beskrivs andra sätt nedan.
Reentrans¶
En potentiell fara kan uppstå om en funktion eller metod delas mellan huvudprogrammet och en eller flera ISR:er eller mellan flera ISR:er. Problemet här är att funktionen i sig själv kan avbrytas och att ytterligare en instans av den funktionen körs. Om detta ska kunna ske måste funktionen utformas för att vara reentrant. Hur detta görs är ett avancerat ämne som ligger utanför denna handlednings omfattning.
Kritiska sektioner¶
Ett exempel på en kritisk kodsektion är en som kommer åt mer än en variabel som kan påverkas av en ISR. Om avbrottet råkar inträffa mellan åtkomsterna till de enskilda variablerna kommer deras värden att vara inkonsistenta. Detta är ett exempel på en fara känd som ett kapplöpningstillstånd (race condition): ISR:en och huvudprogrammets slinga kapplöper om att ändra variablerna. För att undvika inkonsistens måste ett medel användas för att säkerställa att ISR:en inte ändrar värdena under den kritiska sektionens varaktighet. Ett sätt att uppnå detta är att utfärda machine.disable_irq() före sektionens början och machine.enable_irq() vid slutet. Här är ett exempel på detta tillvägagångssätt:
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()
En kritisk sektion kan bestå av en enda kodrad och en enda variabel. Betrakta följande kodfragment.
count = 0
def cb(): # An interrupt callback
count += 1
def main():
# Code to set up the interrupt callback omitted
while True:
count += 1
Detta exempel illustrerar en subtil felkälla. Raden count += 1 i huvudslingan medför en specifik kapplöpningsfara känd som läs-modifiera-skriv. Detta är en klassisk orsak till buggar i realtidssystem. I huvudslingan läser MicroPython värdet av count, lägger till 1 till det och skriver tillbaka det. Vid sällsynta tillfällen inträffar avbrottet efter läsningen men före skrivningen. Avbrottet modifierar count men dess ändring skrivs över av huvudslingan när ISR:en returnerar. I ett verkligt system kan detta leda till sällsynta, oförutsägbara fel.
Som nämnts ovan bör försiktighet iakttas om en instans av en inbyggd Python-typ modifieras i huvudkoden och den instansen kommer åt i en ISR. Koden som utför modifieringen bör betraktas som en kritisk sektion för att säkerställa att instansen är i ett giltigt tillstånd när ISR:en körs.
Särskild försiktighet behöver iakttas om en datamängd delas mellan olika ISR:er. Faran här är att avbrottet med högre prioritet kan inträffa när det med lägre prioritet delvis har uppdaterat de delade data. Att hantera denna situation är ett avancerat ämne som ligger utanför denna introduktions omfattning, förutom att notera att mutex-objekt som beskrivs nedan ibland kan användas.
Att inaktivera avbrott under en kritisk sektions varaktighet är det vanliga och enklaste sättet att gå tillväga, men det inaktiverar alla avbrott snarare än enbart det som har potential att orsaka problem. Det är generellt oönskat att inaktivera ett avbrott under lång tid. I fallet med timeravbrott inför det variabilitet i tidpunkten då ett återanrop sker. I fallet med enhetsavbrott kan det leda till att enheten betjänas för sent med möjlig dataförlust eller överskridningsfel i enhetshårdvaran. Liksom en ISR bör en kritisk sektion i huvudkoden ha en kort, förutsägbar varaktighet.
Ett tillvägagångssätt för att hantera kritiska sektioner som radikalt minskar den tid under vilken avbrott är inaktiverade är att använda ett objekt som kallas mutex (namnet härlett från begreppet ömsesidig uteslutning, mutual exclusion). Huvudprogrammet låser mutexet innan det kör den kritiska sektionen och låser upp det vid slutet. ISR:en testar om mutexet är låst. Om det är det undviker den den kritiska sektionen och returnerar. Designutmaningen är att definiera vad ISR:en bör göra i händelse av att åtkomst till de kritiska variablerna nekas. Ett enkelt exempel på ett mutex finns här. Observera att mutex-koden faktiskt inaktiverar avbrott, men endast under varaktigheten av åtta maskininstruktioner: fördelen med detta tillvägagångssätt är att andra avbrott praktiskt taget är opåverkade.
Avbrott och REPL:en¶
Avbrottshanterare, såsom de som är associerade med timrar, kan fortsätta att köras efter att ett program har avslutats. Detta kan ge oväntade resultat där man kanske hade förväntat sig att objektet som utlöser återanropet skulle ha hamnat utanför sitt omfång. Till exempel på en OpenMV Cam:
def bar():
foo = machine.Timer(-1, freq=4, callback=lambda t: print('.', end=''), hard=True)
bar()
Detta fortsätter att köras tills timern uttryckligen inaktiveras eller kortet återställs med Ctrl-D.