Programmegszakítások

A fejezet tartalma:

A programmegszakítás alapjai

A mikrovezérlőnek programfutás közben sok esetben különböző eseményekre kell reagálnia. Ilyen események lehetnek például nyomógombok állapotváltozásai (lenyomás, felengedés), kapacitív érzékelő vagy analóg komparátor billenési szintjeinek átlépése, ADC konverzió befejezése, időzítőn beállított idő letelte. Ennél sokkal bonyolultabb esetek is adódnak, amikor például a mikrovezérlő USB vagy soros vonalon kommunikál a számítógéppel, vagy más időkritikus folyamatot kell kiszolgálni. Az ilyen jellegű feladatok ellátásának megtervezésekor fel kell mérni, hogy elég hatékony lesz-e a programunk, vagy belefullad a teendőkbe?       

Az egyszerű programokban többnyire a lekérdezéses (polling) módszert használjuk az események bekövetkeztének vizsgálatára, de ez nem túl hatékony módszer, komolyabb programokban nem célravezető megközelítés:
Képzeljük el, milyen volna, ha a mobiltelefont hívásjelzés nélkül használnánk, s időnként elővennénk a zsebünkből, s beleszólnánk: "Halló, van valaki a vonalban?". Bizonyára nevetségesnek tűnik, pedig az eddigi programjaink többnyire így kezelték az eseményeket. A fenti módszer hátránya is nyilvánvaló: vagy túl gyakran nézegetjük a telefont, fölöslegesen pazarolva ezzel az időt, vagy túl ritkán vesszük elő, s akkor lemaradhatunk egy fontos hívásról.

Sokkal hatékonyabb az a módszer, ha a telefon rendelkezik hívásjelzéssel, ami a programmegszakításhoz (interrupt) hasonlítható: ha hívás érkezik, cseng a telefon. Abbahagyom, amit éppen csinálok, s felveszem a telefont (kiszolgálom a megszakítást). A hívás befejeztével visszatérhetek a korábbi tevékenység folytatásához.

A mikrovezérlők felépítése a programmegszakítások révén lehetővé teszi, hogy:
A programmegszakítás (interrupt) azt jelenti, hogy a program futása egy külső vagy belső esemény bekövetkezte miatt megszakad, s majd csak a programmemória egy másik helyén elhelyezett utasítássorozat (a progammegszakítás kiszolgálását végző kód) lefutása után tér vissza a CPU az eredeti program folytatásához, ahogy ezt az alábbi ábrán láthatjuk.


1 ábra: A program normál menetének megszakítása

Természetesen ahhoz, hogy a programmegszakítás kiszolgálása után zavartalanul folytatódhasson a program futása, programmegszakításkor el kell menteni a futó program állapotát (beleértve azt is, hogy melyik a soron következő utasítás, s hogy mi volt a státuszbitek tartalma), visszatéréskor pedig helyre kell állítani. A hatékony, és "takarékos" működés érdekében az állapotmentések és visszaállítás egy része automatikusan (hardveresen megtörténik), a többit pedig - amennyiben egyáltalán szükséges - szoftveresen kell végeznünk.

Az MKL25Z128VLK4 mikrovezérlő megszakítási rendszere

Az MKL25Z128VLK4 mikrovezérlő ARM Cortex-M0+ maggal rendelkezik, megszakítási rendszerét a Cortex™-M0+ Devices Generic User Guide dokumentum 2.3 alfejezete ismerteti részletesen. Az ARM Cortex-M0+ mikrovezérlők megszakítási rendszere az NVIC beépített megszakításvezérlőn (Nested and Vectored Interrupt Controller) alapul, ami legfeljebb 48 kivételt (Exceptions) kezel. Az első 16 kivétel az ARM Cortex-M0+ processzormagból származó megszakítási jel, a következő 32 pedig a perifériák és a külső forrásból származó megszakítási kérelmek. Szűkebb szóhasználatban csak ez utóbbi 32 forrást nevezzük megszakításnak (Interrupts) és számozzuk 0-tól 31-ig (Interrupt #0 - Interrupt #31). Különleges prioritása és vektorának elhelyezkedése miatt az ARM Cortex-M0+ processzormagból származó kivételek közé soroljuk az NMI nem maszkolható megszakítást is, annak ellenére, hogy az külső forrásból fogad megszakítási kérelmet.

2. ábra: Az ARM Cortex-M0+ mikrovezérlők megszakítási rendszerének vázlata

A megszakítási rendszer tulajdonságai

A megszakítások priorizálhatók - az Interrupt #0 - Interrupt #31 megszakítások prioritása  egyenként beállítható 0-3 közötti értékre. A kisebb szám magasabb prioritási szintet jelent. A kivételek prioritása kötött.

A megszakítások egymásba  skatulyázhatók - a  magasabb prioritású megszakítási kérelem beérkezése megszakíthatja az alacsonyabb prioritású megszakítás kiszolgálását (nesting - erre utal az NVIC első betűje).

A megszakítási rendszer vektoros - a megszakítási kérelem beérkezése és érvényesülésekor egy ún. vektortáblából vett címre adódik át a vezérlés a megszakítást kiszolgáló eljáráshoz. Mind a 48 kivételnek, illetve megszakításnak külön bejegyzése van a vektortáblában, így a megszakítási forrás vizsgálata nélkül közvetlenül a megfelelő kiszolgáló eljáráshoz kerül a vezérlés.

A megszakítási késedelem 12 órajelciklus - a megszakítási jel beérkezése és a megszakítást kiszolgáló eljárásba lépés között eltelt idő (interrupt latency) 12 órajelciklus. Ennek során befejeződik az éppen végrehajtás alatt álló utasítás és automatikusan elmentésre kerülnek a veremtárba a legfontosabb regiszterek. A megszakítás kiszolgálása után újabb 12 órajelciklusba kerül az elmentett regiszterek visszaállítása és a megszakított programba történő visszalépés.

Megszakítások láncolása (Interrupt tail-chaining) - ha egy megszakítás kiszolgálása közben újabb megszakítási kérelem érkezett, akkor az első megszakítás végén nem történik meg a regiszterek helyreállítása, hanem az újabb megszakítási kérelem kiszolgálására kerül a vezérlés. Ez a váltás csak 6 órajelciklust vesz igénybe, szemben a regiszter visszatöltés majd újra elmentés 2x12 órajelciklusával.

Későn érkező megszakítás (late arrival) - egy újabb optimalizálási lehetőség, hogy a megszakításba lépéskor a regiszterek elmentése végén az NVIC mégegyszer kiértékel, hogy melyik a legmagasabb prioritású érvényes megszakítási kérelem. Ha a regiszterek elmentése közben késve beérkezik egy magasabb prioritású kérelem, akkor annak a kiszolgálása kezdődik meg a korábban érkező alacsonyabb prioritású kérelem helyett.

Az MKL25Z128VLK4 mikrovezérlőben minden interrupt forráshoz tartozik egy jelzőbit (interrupt flag), ami '1'-be áll be, amikor a hozzá tartozó esemény bekövetkezik. Ahhoz hogy egy ilyen megszakítási kérelem (az '1' állapotba  billent jelzőbit) érvényesülhessen, további feltételeknek is teljesülnie kell.

A megszakítások maszkolhatók - A telefonos analógiánál maradva: van olyan időszak, amikor  nem akarjuk, hogy hívásokkal zavarjanak bennünket a moziban, színházban, vagy egy munkahelyi értekezleten. Ilyenkor kikapcsoljuk a hívásjelzést, és az esetleges hívásokat figyelmen kívül hagyjuk. A mikrovezérlőkben szinte minden interrupt forráshoz találunk egy engedélyező bitet, amit '1'-be kell állítani ahhoz, hogy a hozzá tartozó programmegszakítási kérelem érvényesüljön. Ha az engedélyező bit '0', akkor az adott programmegszakítás "maszkolva van", vagyis le van tiltva. Az engedélyező bit '0' állapota csak a programmegszakítási kérelem érvényesülését akadályozza meg (nem ugrik el a program az interrupt forrásához tartozó ISR vektorhoz), az interrupt jelzőbit ettől még bebillenhet és programozott lekérdezéssel (polling) vizsgálható. Vannak az MKL25Z128VLK4 mikrovezérlőben olyan megszakítások, amelyek nem maszkolhatók (nem tilthatók le). Ilyen az NMI (nem maszkolható megszakítás)


Az MKL25Z128VLK4 mikrovezérlőben egy programmegszakítási kérelem érvényesülésekor a következő dolgok történnek:
Visszatéréskor a programmegszakítás kiszolgálása végén egy RETURN utasítás (BX <Reg> vagy POP {<Reg1>,<Reg2>,..,PC} utasítás) végrehajtásakor a Link Regiszterbe előzőleg beállított EXC_RETURN feltétel miatt beindul a  "visszatérés programmegszakításból" (return from interrupt) mechanizmus, mellyel visszatérünk a félbehagyott program folytatásához. Ennek hatására automatikusan helyreáállításra kerülnek a programmegszakításkor automatikusan elmentett regiszterek  és a programszámláló is visszaáll a veremtárban elmentett eredeti értékre, s a főprogram folytatódhat, mintha mi sem történt volna.

Interrupt kiszolgáló rutin

Azt a kódrészletet, amelyik a programmegszakítás kiszolgálást végzi, interrupt kiszolgáló rutinnak (angolul Interrupt Service Routine, vagy röviden ISR) nevezzük. Az 1. ábrát nézve, azt hihetnénk, hogy ez egy szubrutin, amelyet a főprogram meghívott. A valóságban azonban nem a főprogram hívja meg, hanem automatikusan, a hardver által kiváltott esemény hatására aktiválódik. Például, ha a soros porton egy adat érkezik, akkor az interrupt kiszolgáló rutin kiolvassa az UART port regiszteréből az adatot, elmenti egy pufferterületre, és visszaadja a vezérlést. Azt is szokták mondani, hogy az interrupt kiszolgálása a háttérben történik, a főprogram pedig az előtérben fut.

Bonyolultabb esetekben nem csupán a föntebb felsorolt regiszterek tartalmát kell elmenteni programmegszakításkor, hanem mindazon regisztereket, amelyeket a programmegszakítást kiszolgáló programrészlet módosít a futása során (szüksége lehet például a  munkaregiszterekre). Erről többnyire a C fordító gondoskodik.

Az ISR meghívhat más szubrutinokat is, sőt, előfordulhat az is, hogy pont azt a szubrutint hívja meg, amelynek futását félbeszakította. Ha ilyen esetek előfordulását megengedjük, akkor a program tervezésénél ügyelnünk kell arra, hogy újra meghívható (reentrant) legyen az eljárás, azaz statikus változók helyett dinamikus tárfoglalást használjon a paraméterei és a lokális változói tárolására, s az újrabelépések száma lehetőleg minél kisebb legyen (nehogy túlcsorduljon a veremtár a dinamikus helyfoglalások miatt).

Megszakítások kezelése az mbed környezetben

Bár az mbed környezet sok mindent elfed előlünk a megszakítások kezelése kapcsán: közvetlenül nem találkozunk sem  az NVIC megszakításvezérlő beállításaival, sem a perifériák megszakítást engedélyező és -jelző bitjeinek kezelésével, de saját függvényeinket meghívathatjuk a megszakítást kiszolgáló rutinokból. Ilyen példát láthattunk  az analóg komparátor működését vizsgáló 05_comparator_demo programunkban, ahol a cmp_rise_ISR() és a cmp_fall_ISR() függvényeinket a komparátor felfutásra, illetve lefutásra aktiválódó megszakításaihoz rendeltük hozzá. Az ilyen függvényeket visszahívandó (callback) függvényeknek nevezzük, s az eseményvezérelt programozásnál van nagy jelentőségük.

További lehetőség a megszakítások használatára a digitális bemenetek állapotváltozási eseményeinek figyelése, melyekhez az alább tárgyalt InterruptIn objektumosztály felhasználásával férünk hozzá, s csatolhatunk hozzájuk visszahívandó függvényeket. Az időzítőkkel kapcsolatos megszakítások használatát, mint például  Ticker (periodikusan ismétlődő megszakítások) és Timeout (egyszeri megszakítás) külön fejezetben tárgyaljuk.

A visszahívandó függvények megírásánál ügyelnünk kell arra, hogy a függvény lehetőleg rövid legyen, s ne tartalmazzon időigényes feldolgozásokat, kiíratásokat, vagy blokkoló várakozásokat. Az időigényes feladatokat külön függvényekben, vagy a főprogramban végezzük el, s a visszahívási függvényekben legfeljebb egy-egy jelző beállításával, vagy egy véges állapotgép állapotának megváltoztatásával jelezzük, hogy a kívánt feladatot el kell végezni.

Az InterrupIn objektumosztály

A digitális bemenetek állapotváltozási eseményei is válthatnak ki megszakításokat. Ezek konfigurálása és kezelése az InterruptIn objektumosztály felhasználásával történik, legfontosabb tagfüggvényeit az alábbi táblázatban foglaltuk össze. A részletes dokumentáció a InterruptIn periféria-könyvtár honlapján található.

1. táblázat: Az InterruptIn objektumosztály legfontosabb tagfüggvényei
Függvény
Használat
InterruptIn név(pin)
Létrehoz egy "név" nevű InterruptIn  objektumot és a "pin" paraméterrel megadott digitális bemenet megszakításinak kezelését az objektumhoz rendelei.
mode(pinmode)
Az objektumhoz tartozó digitális bemenet felhúzási módjának (PullUp, PullNone) beállítása .
enable_irq()
Az objektumhoz tartozó megszakítás engedélyezése.
rise(*fptr)
A felfutáshoz tartozó megszakítást engedélyezi és visszahívandó függvényt rendel hozzá (*fptr: függvény pointer)
fall(*fptr)
A lefutáshoz tartozó megszakítást engedélyezi és visszahívandó függvényt rendel hozzá (*fptr: függvény pointer)
disable_irq()
Az objektumhoz tartozó megszakítás letiltása.
Megjegyzés: A FRDM-KL25Z kártya esetében csak az A és a D port kivezetéseihez (PTA0-PTA31, PTD0-PTD31) rendelhetünk megszakításkezelő  InterruptIn objektumot!

Mintapélda: LED kapcsolgatása nyomógombbal

Az alábbi programban belső felhúzással ellátott digitális bemenetnek állítjuk a D3 (PTA12) bemenetet. A bemenet és a föld közé pedig egy nyomógombot kell kötni a vezérléshez. Digitális kimenetnek állítjuk a D13 (PTD1) kimenetet - ehhez a kivezetéshez van kötve a kártyára szerelt RGB LED kék LED-jének a katódja. A bemenet lefutó élre történő megszakításokhoz a button_pressed() visszahívandó függvényt rendeljük hozzá, amelyben minden visszahíváskor ellenkezőjére váltjuk a D13 kimenet állapotát. A főprogramban az inicializálás után nincs teendőnk, a nyomógomb állapotának figyelése és a LED állapotváltása a háttérben (megszakítási szinten) történik.

Hardver követelmények:


3. ábra: A nyomógomb bekötése a led_button programhoz


1. lista: 07_button_interrupt/main.cpp program listája
#include "mbed.h"

InterruptIn button(D3); // Pusbutton input
DigitalOut led(D13); // LED output (the blue LED)

void button_pressed() {
led = !led; // LED state is changed at every button press
}

int main() {
button.mode(PullUp); // Enable internal pullup
button.fall(&button_pressed); // Attach function to falling edge
while (true) {
wait(0.2f); // Nothing to do. Just wait
}
}

A program tanulsága, hogy a nyomógomb kontaktusainak pergése miatt nagyon megbízhatatlanul működik. A pergés kiküszöbölésére a következő fejezetben mutatunk majd példát (mintavételezés Ticker használatával). A főprogramban pedig a tétlen várakozás helyett valamelyik alacsony fogyasztású energiatakarékos módot használhatnánk.

Mintapélda: Nyomógomb pergésének vizsgálata

Az alábbi programban arra használjuk a D3 digitális bemenethez tartozó megszakításokat, hogy egy nyomógomb lenyomási és felengedési ciklusa során a beérkező lenyomási eseményeket számláljuk vele. Ha egynél nagyobb számot kapunk, akkor "pergett" a bemenet. Mivel a lenyomás-felengedési ciklust a főprogramban követni akarjuk, ezért a megszakítási eseményeken kívül szükségünk van a bemenet állapotának vizsgálatára is. Ezt itt úgy oldottuk meg, hogy a A D3 (PTA12) bemenetet egyidejűleg belső felhúzással ellátott digitális bemenetnek (DigitalIn objektum) és megszakításjelző InterruptIn bemenetnek is beállítjuk. A bemenet és a föld közé pedig egy nyomógombot kell kötni a vezérléshez.

A bemenet lefutó élre történő megszakításokhoz a button_pressed() visszahívandó függvényt rendeljük hozzá, amelyben számláljuk a megszakításokat. A főprogramban minden lenyomás-felengedési ciklus elején töröljük a számlálót, majd a lefutott ciklus végén kiíratjuk az eseményt. A lenyomások és felengedések után egy-egy pergésmentesítő várakozás garantálja a ciklus helyes felismerését.

Hardver követelmények:
2. lista: 07_button_bounce/main.cpp program listája
#include "mbed.h"

DigitalIn mybutton(D3,PullUp); // Pushbutton input
InterruptIn button(D3); // Pusbutton interrupt
Serial pc(USBTX,USBRX); // UART0 via OpenSDA
volatile uint16_t counts; // counter variable

void button_pressed() {
counts++; // counts button presses
}

int main() {
button.mode(PullUp); // Enable internal pullup
button.fall(&button_pressed); // Attach function to falling edge
while (true) {
counts = 0; // Clear counter
pc.printf("Press & release switch... \r\n");
while (mybutton); // Wait for button press
wait_ms(20); // Debounce delay
while (!mybutton); // Wait for button release
wait_ms(20); // Debounce delay
pc.printf("Button pressed %d times\r\n",counts);
}
}

Megjegyzés: A counts változó tartalmát a főprogramon kívül a megszakításból meghívott button_pressed() visszahívandó függvényben - azaz megszakítási szinten is módosítjuk, ezért a változó deklarálásánál használnunk kell a volatile módosítót. Ez garantálja, hogy a fordító nem tesz semmiféle feltételezést a változó értékére vonatkozóan, hanem minden hozzáférésnél tényleges memóriaműveletet fordít bele a programba.

A program futásának eredménye az alábbi ábrán látható. Megfigyelhetjük, hogy 1 és 8 közötti értékeket kaptunk  (minden 1-nél nagyobb szám pergést jelent).

4. ábra: A 07_button_bounce program futási eredménye