Programmegszakítások
A fejezet tartalma:- A programmegszakítás alapjai
- Az MKL25Z128VLK4 mikrovezérlő megszakítási rendszere
- Megszakítások kezelése az mbed környezetben
- Az InterrupIn objektumosztály
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:
- a CPU időt arra pazaroljuk, hogy lekérdezéseket hajtson végre
- tovább rontja a hatékonyságot, ha túl ritkán, vagy túl gyakran végezzük a lekérdezést.
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 program hasznos tevékenységgel töltse az idejét, míg az események bekövetkezésére vár
- az esemény bekövetkeztekor viszont haladéktalanul reagáljon
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:
- a CPU befejezi annak az utasításnak a végrehajtását, amelynek során a programmegszakítási kérelem (valamelyik megszakítási jelzőbit bebillenése) történt
- a veremtárban automatikusan elmentésre kerül a CPU néhány fontos regisztere a regiszterkészletből: R0..R3, R12, LR/R14 (Link regiszter), PC/R15 (programszámláló, azaz a visszatérési cím), valamint az xPSR állapotjelző bitek.
- a Link Regiszter (LR/R14) egy speciális értéket vesz fel (EXC_RETURN)
- a PC programszámlálóba automatikusan betöltődik a beérkezett programmegszakítási kérelemhez tartozó előre meghatározott cím (amit interrupt vektornak nevezünk), ami azt eredményezi, hogy a program futása eltérül, az interrupt vektornál folytatódik.
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. |
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:
- FRDM-KL25Z kártya
- A D3 (PTA12) bemenet és a GND közé egy nyomógombot kell kötni az alábbi ábra szerint.
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:
- FRDM-KL25Z kártya
- A D3 (PTA12) bemenet és a GND közé egy nyomógombot kell kötni a 3. ábra szerint.
#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