Programmegszakítások

A fejezet tartalma:

A programmegszakítás alapjai

A mikrovezérlőnek programfutás közben sok esetben kell különböző eseményekre reagálnia. Az előző fejezet példáiban a nyomógomb állapotváltozásaival (lenyomás, felengedés) változtattuk meg a LED állapotát (sötét, világít, esetleg villog), vagy a Timer1 órát figyeltük, hogy letelt-e az előírt késleltetési idő. 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. Például a dsPIC33FJ128GP802 digitális jelvezérlőben lévő sztereó audio DAC (digitál-analóg jelátalakító) 100 kHz-es frekvencián is képes dolgozni, ez azt jelenti, hogy 10 µs-onként 2x16 bit új adatot kell előállítani és beleírni a DAC kimeneti pufferébe. 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 eddig bemutatott példákban kizárólag a lekérdezéses (polling) módszert használtuk az események bekövetkeztének vizsgálatára, ami nem túl hatékony módszer:
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 pontosan í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énybe kö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 PIC24 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 RETFIE (return from interrupt - visszatérés programmegszakításból) utasítást kell végrehajtani, melynek hatására visszatérünk a félbehagyott program folytatásához. Ennek hatására a státuszregiszter, a CPU prioritási szintje és a programszámláló visszaáll a veremtárban elmentett eredeti értékre, s a főprogram folytatódhat, mintha mi sem történt volna.

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. A fenti á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 interupt 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). Az ISR meghívhat 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.

A PIC24 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. Például a Timer1 időzítőhöz a T1IF jelzőbit tartozik, az UART1 soros port RX bementéhez az U1RXIF bit, a bemeneti szintváltozásokat érzékelő áramkörök interruptjához (change notifications) a CNIF jelzőbit, és így tovább. Az interrupt jelzőbitek általában az IFS0 - IFS4 speciális funkciójú regiszterekben találhatók (lásd az adatlap interrupt vezérlő regisztereket bemutató 3-4. táblázatát!).

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 PIC24 mikrovezérlőben a legtöbb 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 a mikrovezérlőben olyan interruptok, amelyek nem maszkolhatók (nem tilthatók le). Az interrupt engedélyező bitek az IEC0 - IEC4 speciális funkciójú regiszterekben találhatók.

A programmegszakítási rendszer részletei

Ebben a szakaszban a PIC24 mikrovezérlő programmegszakítási rendszerének részleteivel foglalkozunk, ismertetve az interrupt vektorok táblázatát, a programmegszakítás prioritásokat és tisztázzuk a programmegszakítások és a hibaesemények (traps) közötti különbségeket. A programmegszakításra vonatkozó részletes információkat a Microchip PIC24HJ128GP502 mikrovezérlőjének honlapján az adatlap Interrupt Controller c. fejezetében, és a PIC24H Family Reference Manual 32. fejezetében találjuk meg.

Interrupt vektorok

Amikor egy interrupt történik, a PIC24 mikrovezérlő előveszi a programmegszakítást kiszolgáló programrész kezdőcímét a memória elején elhelyezkedő interrupt vektorok táblázatából és átadja a vezérlést. A programmemória legelején az úgynevezett RESET vektor található. Ennek azért kell itt lennie, mert bekapcsoláskor vagy reset után a mikrovezérlő utasításszámlálója nullázódik, tehát a PC = 0x000000 címről kezdi el végrehajtani az utasításokat. Itt tehát egy érvényes GOTO __reset utasításnak kell állnia, ami átadja a vezérlést a felhasználói program kezdetére (ehhez kellett assembly programozáskor definiálnunk a globális __reset címkét). Az interrupt vektorok táblázata (IVT) a RESET vektorral, és az alternatív interrupt vektorok táblázata (AIVT) a következő ábrán látható módon helyezkednek el a programtároló memória elején.

2 ábra: A RESET vektor, a kivételek és az interrupt vektorok táblázata a memóriában

A RESET vektort követően helyezkednek el az interrupt vektorok a 0x000004 címtől kezdődően. Minden interrupt vektor egy-egy 24 bites címet tartalmaz, ami a programmegszakítást kiszolgáló rutin (ISR) belépési pontjára mutat. Az interrupt vektorok táblázatának első, elkülönített része a hardveres kivételek és a szoftveres csapdák részére van fenntartva. Ezt követően helyezkednek el (az ábrán zöld színnel jelölve) a belső és a külső forrásból származó felhasználói programmegszakításokhoz tartozó interrupt vektorok. A PIC24HJ128GPX02/X04 mikrovezérlő család tagjai 45 független interrupt forrást és 5 nem maszkolható kivételt kezelnek. A többi interrupt vektor a további fejlesztéseknek van fenntartva. Más mikrovezérlő családnál természetesen a perifériák számától és kiépítettségétől függően más eltérő lehet az interrupt források listája.

A 0x000100 címtől kezdődően egy alternatív interrupt vektor táblázat (AIVT) található. Ez arra jó, hogy például a hardveres nyomkövetésnél gyorsan át lehessen kapcsolni a felhasználói és a felügyeleti környezet között. Természetesen másra is használható, például ott, ahol különböző algoritmusok vagy alternatív rendszerek (pl. bootloader és felhasználói program) közötti váltogatásra van szükség. Az alternatív interrupt vektorok táblázata egyetlen bit módosításával, az INTCON2 regiszter 15. bitjének (ALTIVT) '1'-be állításával aktiválható. Ha az alternatívára nincs szükségünk, akkor AIVT az IVT-vel megegyezően töltendő ki.

1. táblázat: A PIC24HJ128GP502 mikrovezérlő interrupt forrásai
VEC IVT cím AIVT cím C30 név Megszakítás Forrása
1 0x000006 0x000106 __OscillatorFail Oszcillátor hiba
2 0x000008 0x000108 __AddressError Memóriacímzés hiba
3 0x00000A 0x00010A __StackError Verem hiba
4 0x00000C 0x00010C __MathError Matematikai hiba
5 0x00000E 0x00010E __DMACError DMAC hiba
8 0x000014 0x000114 __INT0Interrupt INT0 – külső megszakítás 0
9 0x000016 0x000116 __IC1Interrupt IC1 – Input Compare 1
10 0x000018 0x000118 __OC1Interrupt OC1 – Output Compare 1
11 0x00001A 0x00011A __T1Interrupt T1 – Timer 1
12 0x00001C 0x00011C __DMA0Interrupt DMA1 – DMA csatorna 0
13 0x00001E 0x00011E __IC2Interrupt IC2 – Input Capture 2
14 0x000020 0x000120 __OC2Interrupt OC2 – Output Compare 2
15 0x000022 0x000122 __T2Interrupt T2 – Timer 2
16 0x000024 0x000124 __T3Interrupt T3 – Timer 3
17 0x000026 0x000126 __SPI1ErrInterrupt SPI1E – SPI1 hiba
18 0x000028 0x000128 __SPI1Interrupt SPI1 – SPI1 átvitel vége
19 0x00002A 0x00012A __U1RXInterrupt U1RX – UART1 vevő
20 0x00002C 0x00012C __U1TXInterrupt U1TX – UART1 adó
21 0x00002E 0x00012E __ADC1Interrupt AD1 – ADC1 konvertálás kész
22 0x000030 0x000130 __DMA1Interrupt DMA1– DMA csatorna 1
24 0x000034 0x000134 __SI2C1Interrupt SI2C1 – I2C 1 Szolga esemény
25 0x000036 0x000136 __MI2C1Interrupt MI2C1 – I2C 1 Mester esemény
26 0x000038 0x000138 __CMPInterrupt CMP – Komparátor
27 0x00003A 0x00013A __CNInterrupt CN – Input Megváltozás
28 0x00003C 0x00013C __INT1Interrupt INT1 – külső megszakítás 1
30 0x000040 0x000140 __IC7Interrupt IC7 – Input Capture 7
31 0x000042 0x000142 __IC8Interrupt IC8 – Input Capture 8
32 0x000044 0x000144 __DMA2Interrupt DMA2 – DMA csatorna 2
33 0x000046 0x000146 __OC3Interrupt OC3 – Output Compare 3
34 0x000048 0x000148 __OC4Interrupt OC4 – Output Compare 4
35 0x00004A 0x00014A __T4Interrupt T4 – Timer 4
36 0x00004C 0x00014C __T5Interrupt T5 – Timer 5
37 0x00004E 0x00014E __INT2Interrupt INT2 – külső megszakítás 2
38 0x000050 0x000150 __U2RXInterrupt U2RX – UART2 vevő
39 0x000052 0x000152 __U2TXInterrupt U2TX – UART2 adó
40 0x000054 0x000154 __SPI2ErrInterrupt SPI2E – SPI2 hiba
41 0x000056 0x000156 __SPI2Interrupt SPI2 – SPI2 átvitel vége
42 0x000058 0x000158 __C1RxRdyInterrupt C1RX – CAN1 RX adat kész
43 0x00005A 0x00015A __C1Interrupt C1 – CAN1 esemény
44 0x00005C 0x00015C __DMA3Interrupt DMA3 – DMA csatorna 3
53 0x00006E 0x00016E __PMPInterrupt Parallel Master Port
54 0x000070 0x000170 __DMA4Interrupt DMA4 – DMA csatorna 4
69 0x00008E 0x00018E __DMA5Interrupt DMA5 – DMA csatorna 5
70 0x000090 0x000190 __RTCCInterrupt RTCC – valós idejű óra
73 0x000096 0x000196 __U1ErrInterrupt UART1 hiba
74 0x000098 0x000198 __U2ErrInterrupt UART2 hiba
75 0x00009A 0x00019A __CRCInterrupt CRC generátor
76 0x00009C 0x00019C __DMA6Interrupt DMA6 – DMA csatorna 6
77 0x00009E 0x00019E __DMA7Interrupt DMA7 – DMA csatorna 7
78 0x0000A0 0x0001A0 __C1TxReqInterrupt ECAN1 - TX adatkérés
Az első oszlopban (VEC) tüntettük fel az interrupt sorszámát, amelyet a mikrovezérlő az INTTREG nevű speciális funkciójú regiszterbe  (az INTTREG<6:0> bitekre) tölt be az interrupt végrehajtásakor, az interrupt prioritási szintjével együtt. Ez a regiszter arra használható, hogy hibakeresésnél, vagy kiszolgálatlan interruptok okozta megszakítások okának kiderítéséhez megállapítsuk, hogy melyik interrupt vektor okozta a legutolsó megszakítást. A későbbiekben majd mutatunk példát a használatára.

A táblázat negyedik oszlopában azt a nevet tüntettük fel, amelyet a C30 fordítóhoz használnunk kell, ha az adott interrupthoz kiszolgáló szubrutint írunk C nyelven. Ezek a nevek egyébként a a C30 fordító telepítési könyvtárában található p24HJ128GP502.gld állományban vannak definiálva.

Interrupt prioritási szintek

A PIC24 mikrovezérlő többszintű prioritás kezelésére készült, ezért arra is lehetőség van, hogy egy magasabb  programmegszakítási kérelem megszakítsa egy alacsonyabb prioritású programmegszakítás kiszolgálását. Az interupt prioritások szintje 0-tól 15-ig terjed. Az egyes interrupt forrásokhoz tartozó prioritási szintek az IPCx speciális funkciójú regiszterekben állíthatók be 0 és 7 közötti értékre. A mikrovezérlő bekapcsolásakor mindegyik interrupt forrás prioritása 4-es prioritásszintre áll be.

A CPU aktuális prioritásszintje egy négybites érték, amelyet két részletre osztva két regiszter tárol. A felhasználó által is állítható alsó három bit (IPL<2:0>) az SR státusz regiszterben található (SR<7:5>), míg a legnagyobb helyiértékű bit (IPL<3>) a CORCON regiszter 3.bitjén helyezkedik el (CORCON<3>).

A 0 szint jelenti a legalacsonyabb prioritást, ez gyakorlatilag letiltást jelent, hiszen a futó programot sem tudja megszakítani. A 7-es szint a legmagasabb felhasználó által beállítható szint. A 7-es prioritású interruptot a DISI (Disable Interrupt = interrupt letiltás) utasítás sem tiltja le. A 8-tól 15-ig terjedő prioritásszintek az úgynevezett nem maszkolható hibaeseményeknek (Traps) vannak fenntartva (oszcillátor hiba, címzéshiba, verem túlcsordulás, matematikai hiba, DMA hiba, és egyéb, a jelenlegi vezérlőkben még nem implementált kivételek).

Bekapcsoláskor, vagy RESET-kor az IPL bitek törlődnek. A CPU tehát 0-ás prioritáson fut, ami azt jelenti, hogy bármelyik, akár az 1-es prioritású interrupt is meg tudja szakítani a főprogram futását. Az IPL<2:0> bitek írhatók, tehát a futó program prioritásszintje megváltoztatható. Az IPL<3> bit azonban, amelyik a 8-as, vagy annál magasabb prioritású interruptok (hibainterruptok, csapdák) esetén billen be  nem írható, csak törölhető.

Ha egy programmegszakítási kérelem érkezik, akkor annak prioritása magasabb kell, hogy legyen, mint az éppen futó program aktuális prioritása (IPL<3:0>), hogy érvényesüljön. Ha egyidejűleg több, azonos prioritású programmegszakítási kérelem érkezik, akkor közülük az alacsonyabb sorszámú vektorhoz tartozó (lásd az 1. táblázat VEC oszlopát!) érvényesül elsőként.

Mielőtt a programmegszakítást kiszolgáló programrészre kerülne a vezérlés, az aktuális IPL elmentésre kerül a veremtárban a visszatérési címmel együtt, s az IPL<3:0> bitekre beíródik az elfogadott programmegszakítási kérelemhez tartozó prioritásszint. Ez megakadályozza, hogy a programmegszakítás kiszolgálását hasonló vagy kisebb prioritású interrupt félbeszakítsa. Magasabb prioritású interrupt azonban megszakíthatja, s ezt az interruptok egymásba ágyazásának (nesting) nevezzük, ami egyébként letiltható az INTCON regiszter NSTDIS bitjének (INTCON1<15>) '1'-be állításával. C nyelven ez _NSTDIS = 1; formában írható.

Az interruptok egymásba ágyazásának letiltásakor minden felhasználói interrupt 7-es prioritásszinten fut, ami megakadályozza, hogy bármilyen további felhasználói interrupt érvényesülhessen, mielőtt az aktuális megszakítás kiszolgálása befejeződik. Ezen kívül IPL<2:0> csak olvasható válik, s az interrupt prioritások csak arra szolgálnak, hogy az egyidejű kérelmek érvényesülési sorrendjét megszabják. 

Kivételek, csapdák

A PIC24 programmegszakítások speciális forrásai a csapdák, amelyek belső forrásból származó, nem maszkolható interruptok, amelyek közül némelyek azonnal félbeszakítják a program futását (kemény csapdák - hard traps) vagy néhány további utasítás végrehajtását még engedélyezik, mielőtt a programmegszakítás kiszolgálása megkezdődne (puha csapdák - soft traps). A puha csapdák közé tartozik a DMA (direkt memória hozzáférés) hiba, a matematikai hiba (nullával történő osztás), vagy a veremhiba,ami akkor keletkezik, ha veremmutató 0x800 alá süllyed, vagy meghaladja az SPLIM regiszterben beállított felső határt. 

2. táblázat: A nem maszkolható programmegszakítások
Megszakítás Forrása Kategória Prioritás Az eseményhez tartozó jelzőbit(ek)
Oszcillátor hiba kemény 14 _OSCFAIL (oscillator fail, INTCON1<1>)
_CF (clock fail, OSSCON<3>)
Memóriacímzés hiba kemény 13 _ADDRERR (address error, INTCON1<3>)
Verem hiba lágy 12 _STKERR (stack error, INTCON1<2>)
Matematikai hiba lágy 11 _MATHERR  (math error, INTCON1<4>)
DMAC hiba lágy 10 _DMACERR (DMA conflict, INTCON1<5>)

Interrupt késedelem

Az interrupt késedelem (vagy látencia) az az idő, ami programmegszakítási kérelmet jelző bit bebillenésétől a interrupt lekezeléséig eltelik. A telefonos hasonlatnál maradva: az az idő, ami a telefon csengetésétől kezdve addig tart, amíg a telefonba bele nem szólunk, hogy "Halló!". Az interrupt késedelem idejének minimalizálása sok esetben fontos szempont lehet, mint ahogy a telefonálásnál is fontos, hogy minél hamarabb felvegyük a telefont, különben a hívó fél megunja a várakozást és abbahagyja a hívást.

Az interrupt késedelem két részből tevődik össze:
  1. az az idő, ami az interrupt jelzőbit bebillenésétől az ISR első utasításának végrehajtásáig tart
  2. azoknak az utasításoknak a végrehajtási ideje, amelyek az interrupt forrásának felismerése és kiszolgálása előtt végre kell hajtanunk.
Mivel az utóbbi erőteljesen alkalmazásfüggő, az interrupt késedelem szűkebben vett fogalmába többnyire csak az első szakaszt szokás beleérteni, amit a mikrovezérlő hardver felépítése szab meg. Az alábbi ábrán a PIC24H Family Reference Manual 32. fejezetében  leírtak alapján bemutatjuk, hogy a PIC24H mikrovezérlő esetében az interrupt késedelem  hardver által megszabott része 4 utasításciklus. Ugyanennyi a késedelem a kétciklusos utasítások megszakítása esetében is.

3.a ábra: Interrupt késedelem az ISR első utasításáig


Miután az ISR végrehajtása elkezdődött, az interrupt tényleges kiszolgálásáig tartó további késedelem attól függ is, hogy a processzor állapotának elmentése mennyire komplikált. Egy bonyolult interrupt kiszolgáló program esetén sok regiszter elmentését igényelheti (minden regisztert el kell menteni, amelyet az ISR utasításai módosítanak). Egyszerűbb kiszolgálás esetén a PUSH.S (az árnyékregiszterekbe mentés) utasítás, amely elmenti W0, W1, W2, W3 regiszterek, valamint a státuszregiszter tartalmát jelentősen lerövidítheti ezt az időt. Azonban az árnyékregiszterekből csak egy példány létezik, így egy olyan interrupt, ami az árnyékregiszterekbe ment, nem szakítható meg olyan interrupttal,ami szintén használja az árnyékregisztereket.

A programmegszakításból való visszatérésnél két utasításciklus szükséges a RETFIE utasítás végrehajtására, és még egy további utasításciklust vesz igénybe a soron következő utasítás elővétele a memóriából.

3.b ábra: Visszatérés a programmegszakításból
 

Mibe kerül nekünk az interrupt használata?

Interuptot használó programok tervezésénél célszerű odafigyelni arra, hogy a CPU idejének hány százalékát foglalja le az interruptok kiszolgálása. Ennek kiszámításához az alábbi mennyiségeket definiáljuk:
A fenti jelölésekkel a CPU százalékos leterhelése egy adott ISR által így írható:

ISR% = [(Ibelép + Itörzs + Ikilép) x FISR]/Fcy x 100 %                 (9.1)

Az alábbi táblázatban a különböző gyakoriságú interruptok okozta CPU terhelést mutatjuk be, Fcy = 40 MHz estére, Itörzs = 50 utasításciklussal számolva.

3. táblázat: különböző gyakoriságú interruptok okozta CPU terhelés, Fcy = 40 MHz-re számolva
TISR= 10 ms 1 ms 100 µs 10 µs
ISR% = 0.01 % 0.14 % 1.43 % 14.3 %
A 9.1 egyenlet két kulcseleme az interruptok bekövetkezésének gyakorisága (FISR) és a programmegszakítást kiszolgáló rutin utasításciklusainak száma (Itörzs), ezek növekedése növeli a CPU terhelését. Mivel FISR többnyire az I/O fizikai tulajdonságaitól függ, elsősorban Itörzs az, amelyikre a programozónak befolyása van. Aranyszabályként annyi mondható, hogy készítsük az interrupt kiszolgáló programot olyan rövidre, amennyire csak lehetséges.  Amíg az interrupt kiszolgálása folyik, nem csak a főprogram folytatása, hanem a kiszolgálás alatt állóval azonos vagy vagy kisebb prioritású megszakítási kérelmek érvényesülése is akadályozva van. Ideális esetben az interrupt kiszolgálásakor csak a legszükségesebb I/O műveletek elvégzése történik (pl. interrupt jelzőbit törlése, adatkiolvasás és pufferbe tárolás), majd beállítunk egy vagy több szemafort, ami jelzi az előtérben futó programnak, hogy egy adott esemény bekövetkezett.  

Interrupt kiszolgálása C nyelvű programokban

A programmegszakítás kiszolgáláshoz először nézzünk egy egyszerű esetet, például a matematikai hiba interruptot! Az alábbi ábra baloldali felében egy C nyelvű eljárással szolgáljuk ki a _MathError hibainterruptot. Az egyszerűség kedvéért nem csinálunk mást, csak töröljük a hibajelző bitet és a hardveres ciklusszámlálót. Ez utóbbira azért van szükség, mert az osztást, ami a hibainterruptot kiválthatta (nullával történő osztás esetén), REPEAT utasítással lehet megvalósítani, s ezt a ciklust értelmetlen volna folytatni.

A függvény neve kötelezően meg kell, hogy egyezzen a kiszolgálni kívánt interrupt 1. táblázat-beli nevével (estünkben _MathError). Jelezni kell azt is, hogy ez egy programmegszakítást kiszolgáló eljárás. Itt az _ISR makrót használtuk annak jelzésére, hogy ez egy interrupt kiszolgáló eljárás. Az ábra jobboldali oszlopában bemutatjuk azt is, hogy a fordító milyen assembly kódot generált (lásd: isr_vs_isrfast.s). Láthatjuk, hogy fölösleges mentések is vannak benne: automatikusan elmentésre kerül a Programterület Láthatóság  eredeti beállítása (a PSVPAG regiszter), arra az esetre számítva, ha az interrupt kiszolgálásakor használnánk a PSV-t).

4. a ábra: A _MathError interrupt kiszolgálása _ISR makróval (lásd: isr_vs_isrfast.s)

A 4.b ábrán bemutatjuk, hogy rövidebb és kisebb interrupt késedelemmel járó kódot kapunk akkor, ha _ISR helyett az _ISRFAST makrót használjuk, amely a  no_auto_psv attribútumot használja, s így a PSVPAG regiszter tartalma nem módosul és nem is kerül elmentésre.

4. b ábra: A _MathError interrupt kiszolgálása _ISRFAST makróval (lásd: isr_vs_isrfast.c )

Megjegyzés: Az ECE3274 kurzus PIC24 támogatói programkönyvtára újradefiniálja az _ISR és _ISRFAST makrókat az include/pic24_util.h állományban, s ezek különböznek a Microchip C30 fordítójával települő "gyári" definíciótól! Az újradefiniált makrók így néznek ki: #define _ISR __attribute__((interrupt)) __attribute__ ((auto_psv))

#define _ISRFAST __attribute__((interrupt)) __attribute__ ((no_auto_psv))

Az alapértelmezett interruptkezelő

Mielőtt a gyakorlati példákra térnénk, ismerkedjünk meg az ECE3274 kurzus PIC24 támogatói programkönyvtár alapértelmezett interruptkezelőjével! Azokhoz az interrupt vektorokhoz, amelyekhez nem definiálunk interrupt kiszolgáló eljárást, a PIC24 C fordítója automatikusan a _DefaultInterrupt() eljárást rendeli, ami csak egy szoftveres reset utasítást tartalmaz. Természetesen a _DefaultInterrupt() eljárás újradefiniálható, ha a programozó úgy akarja. Az alábbi programrészletek a PIC24 támogatói könyvtár common/pic24_util.c állományában található _DefaultInterrupt() megvalósítás egyszerűsített változatt mutatják be:

1. lista: A _DefaultInterrupt megszakítás kiszolgálása a PIC24 támogatói programkönyvtárban
static _PERSISTENT const char* sz_lastError;     //_PERSISTENT változókat használunk,
_PERSISTENT char* sz_lastTimeoutError;           //melyek reset után is megmaradnak
static _PERSISTENT INTTREGBITS INTTREGBITS_last;
#define u16_INTTREGlast BITS2WORD(INTTREGBITS_last)

void _ISR _DefaultInterrupt(void) {              //Elmentjük az interrupt számát
   u16_INTTREGlast = INTTREG;                    //és prioritását
   reportError("Unhandled interrupt, ");         //Hibaüzenet megadása: "kezeletlen
}                                                //programmegszakítás"

void reportError(const char* sz_errorMessage) {
  //Csak az első hibaüzenet érvényesül, a többit figyelmen kívül hagyja
  if (sz_lastError == NULL) {  
    sz_lastError = sz_errorMessage;
    asm ("reset");                               //Szoftveres RESET végrehajtása
  }
}
A fenti _DefaultInterrupt() interrupt kiszolgáló eljárásban az u16_INTTREGlast megmaradó (persistent) változóban elmentjük az INTTREG regiszter tartalmát, ami az elcsípett interrupt 1. táblázat-beli sorszámát és prioritását tartalmazza. Ennek alapján majd meg tudjuk állapítani, hogy melyik interrupt maradt kiszolgálatlanul. A reportError() függvény pedig egy hibaüzenetre hivatkozó mutatót tárol el az ugyancsak megmaradó sz_lastError változóban, majd egy szoftveres újraindítást hajt végre.

A fenti programrészlet egyúttal azt is bemutatja, hogy mire használható az INTTREG regiszter, ami csak a PIC24H és dsPIC33 mikrovezérlő családokban található meg (PIC24F-ben és dsPIC30F-ben tehát nincs).

Megjegyzés:
a változó név elején az 'sz_' karakterek a "zero terminated string" (nullával határolt karakterfüzér) típusra utalnak, a néhány sorral föntebbi 'u16_' pedig "unsigned 16-bit" (előjel nélküli,16 bites) változótípust jelez.

A _DefaultInterrupt() által megjegyzett hiba az újraindítás után kerül feldolgozásra: a printResetCause() eljárás állapítja meg és írja ki, hogy mi váltotta ki az újraindítást. A bennünket érdeklő (a fentebb definiált _DefaultInterrupt()-tal kapcsolatos) rész  így néz ki:


2. lista: A hiba okának kiírása a PIC24 támogatói programkönyvtárban
void printResetCause(void) {
...
  if (sz_lastError != NULL) {       //Ha van _DefaultInterrupt által eltárolt hibaok...
    outString("Error trapped: ");
    outString(sz_lastError);        //kiírja az eltárolt mutatóval hivatkozott üzenetet
    if (u16_INTTREGlast != 0) {
      outString("Priority: ");      
      outUint8(INTTREGBITS_last.ILR);    //kiírja az elcsípett megszakítás prioritását
      outString(", Vector number: ");    //és sorszámát,melynek alapján a kiszolgálatlan
      outUint8(INTTREGBITS_last.VECNUM); //interrupt azonosítható.
    }
    outString("\n\n");
    sz_lastError = NULL;       //Hibaváltozók törlése az újabb felhasználás
    u16_INTTREGlast = 0;       //biztosítása érdekében
  }

Megjegyzés: Talán nehezen érthető a fenti kódban INTTREGBITS_last és u16_INTTREGlast definíciója, ezért érdemes tisztázni a részleteket: az INTTREGBITS_last változó INTTREGBITS típusú struktúra, melyet a mikrovezérlő header állománya (esetünkben a p24HJ128GP502.h állomány) definiál. Ehhez a struktúrához bitenként vagy bitcsoportonként férhetünk hozzá. A fenti programban az utóbbira láthatunk példát (INTTREGBITS_last.ILR, illetve INTTREGBITS_last.VECNUM). Az u16_INTTREGlast ezzel szemben egy inline makró, amely ugyanezen bitekhez "egy darabban", 16 bites szóként enged hozzáférést. A definiálásahoz felhasznált BITS2WORD() makró a PIC24 támogatói programkönyvtár include/pic24_all.h állományában található.

Egy mintapélda az alapértelmezett interruptkezelő alkalmazására

Az alábbi példában a _DefaultInterrupt() interrupt kiszolgáló eljárást teszteljük úgy, hogy szándékosan matematikai kivételt generálunk egy nullával történő osztással. Ehhez az  u8_zero változót, amellyel osztani akarunk, volatile-nak kell deklarálnunk, másképp a PIC24 C fordító  kioptimalizálja az u8_zero = 1/u8_zero; kódot, s nem következik be a nullával történő osztás. Az itt bemutatott program a tankönyvi mintapéldák között chap09/trap_test.c néven található. Az eredeti forráskódot kiegészítettük egy WAIT_UNTIL_TRANSMIT_COMPLETE_UART1() makróval, hogy a magyarázó szöveget az újraindítás előtt még legyen ideje kiküldeni a soros porton.

Magyarázat: A volatile szó jelentése: illékony, felejtő. Olyan változót jelöl, amelynek tartalma nem biztos, hogy megmarad egyik értékadástól a másikig. Ilyen "illékony" változók például az SFR regiszterek, amelyek tartalmát a hardver megváltoztathatja egy beírás és egy későbbi visszaolvasás közben. 

Hardver követelmények:
Figyelem: A program lefordítása előtt ne felejtsük el beállítani a HARDWARE_PLATFORM makrót!

3. lista: A chap09/trap_test.c program listája:
#include "pic24_all.h"

int main (void) {
  volatile uint8_t u8_zero;
  configBasic(HELLO_MSG);
  while (1) {
    outString("Hit a key to start divide by zero test...");
 //Nyomjon egy gombot a nullával történő osztás indításához!
    inChar();
 //OK. Akkor nullával osztunk...
    outString("OK. Now dividing by zero.\n"                
      "The math errror trap is not handled, so\n"
      "the _DefaultInterrupt handler should be\n"  
      "called, causing the chip to reset.\n\n");
 //A matematikai kivételnek nincs külön kiszolgáló rutinja 
 //ezért a programnak a _DefaultInterrupt kezelőjére kell futnia
 //ami a mikrovezérlő újraindítását okozza.
    WAIT_UNTIL_TRANSMIT_COMPLETE_UART1(); //várunk a szövegkiírás befejezésére
    u8_zero = 0;
    u8_zero = 1/u8_zero;                  //osztás nullával, ami megszakítást okoz
    doHeartbeat();
  }
}

A program kimenete az alábbi ábrán látható. A kezdőüzenet után egy karakter érkezésére vár a program, majd a magyarázat kiírása után megtörténik az osztás nulla értékkel, s ennek következtében lefut a fentebb ismertetett _DefaultInterrupt() eljárás, amely újraindítja a mikrovezérlőt. Reset után az újrainduláskor a megmaradó változóknak köszönhetően ismert az újraindítás oka, s ez kiírásra kerül. A 2. táblázatból ellenőrizhetjük, hogy a 0x0B (decimálisan 11) prioritású interrupt valóban a Matematikai hiba, s a hozzá tartozó interrupt vektor sorszáma az 1. táblázat szerint tényleg 4.

5. ábra: A trap_test.c program kimenete

A MathError megszakítás kiszolgálása

Módosítsuk a nullával osztó példaprogramunkat úgy, hogy a _MathError programmegszakítást a 4.b ábrán bemutatott ISR szolgálja ki! Ehhez a programot bővítenünk kell a _MathError nevű programmegszakítást kiszolgáló eljárással, melyet az _ISRFAST attribútummal specifikáltunk,mivel nincs szükségünk a PSVPAG regiszter elmentésére. A főprogram gyakorlatilag megegyezik az előző példában szereplővel, csupán a kiírt üzenet más. Az itt bemutatott program a tankönyvi mintapéldák között chap09/trap_test_handled.c néven található. Az eredeti forráskódot ennél a programnál is kiegészítettük egy WAIT_UNTIL_TRANSMIT_COMPLETE_UART1() makróval, hogy a magyarázó szöveget az újraindítás előtt még legyen ideje kiküldeni a soros porton. (A program hardver követelményei azonosak az előző programéval.)

4. lista: A chap09/trap_test_handled.c program listája
#include "pic24_all.h"

//Interrupt kiszolgáló eljárás MathError-hoz
void _ISRFAST _MathError (void) {
  //nem csinál mást, csak törli a hibát, és folytatja a programot
  _MATHERR = 0;   //törli a _MATHERR jelzőbitet, ami a megszakítást okozta
  RCOUNT = 0;     //törli az RCOUNT számlálót, hogy megszakítsa az osztási ciklust
}

int main (void) {
  // Ha az u8_zero változó nem volatile volna, a fordító kioptimalizálná
  // az 1/u8_zero kódot, s nem történne meg a nullával osztás!
  volatile uint8_t u8_zero;

  configBasic(HELLO_MSG);
  while (1) {
//Nyomjon egy gombot a nullával osztás teszt indításához!
    outString("Hit a key to start divide by zero test...");
    inChar();
//OK. Akkor nullával osztunk...
//A _MathError interrupt kiszolgáló rutin megakadályozza,
//hogy ez a hiba újraindulást vagy hibajelzést okozzon.
    outString("OK. Now dividing by zero.\n"
        "The _MathError ISR should prevent the chip\n"
        "from resetting or reporting this error.\n\n");
    WAIT_UNTIL_TRANSMIT_COMPLETE_UART1(); //várunk a szövegkiírás befejezésére
    u8_zero = 0;
    u8_zero = 1/u8_zero;                  //osztás nullával, ami megszakítást okoz
    doHeartbeat();
  }// while(1) ciklus vége
}
A program kimenete az alábbi ábrán látható:

6. ábra: A trap_test_handled.c program kimenete

A bejelentkező üzenet után egy karakter érkezésére vár a program (ezt az ábrán látható Bully bootloader programban, vagy a PICkit2 UART Tool ablakában a Send gombra kattintva küldhetjük el), majd a magyarázat kiírása után megtörténik az osztás nulla értékkel, s ennek következtében lefut a _MathError() eljárás, amely törli a hibajelző bitet, de nem indítja újra a mikrovezérlőt, hanem visszaadja a vezérlés a főprogramnak, amely végtelen ciklusban ismétli a karakterre várást és a nullával történő osztást.

Bemeneti szint megváltozását jelző megszakítások

Amint az I/O portok című fejezetben említettük, a CNx jelzésű bemenetek (CN - Change Notification = értesítés a megváltozásról) lehetővé teszik, hogy a mikrovezérlő észlelje a bemenetein a külső jelszint megváltozását és programmegszakítást hajtson végre, amikor valamelyik bemeneten változás történik.  A változást érzékelő CNx bemenetekhez egy belső felhúzóáramkör is társul, melynek engedélyezése esetén külső áramkör nélkül is határozott (magas) állapotba kerül a bemenet, nem "lebeg". Az alábbiakban néhány példát mutatunk be a bemeneti jelszint megváltozását jelző programmegszakítások használatára.

Hardver követelmények

Az alábbi mintaprogramoknál (ahol más megjegyzés nem szerepel) a Kísérleti áramkör c. fejezetben ismertetett alapkapcsolást az alábbi ábrán látható módon egy nyomógombbal (SW1) és soros porti csatlakozással (PICkit2 UART Tool, vagy USB-UART átalakító) kell  kiegészíteni. A fejezet hátralevő részében szinte minden programnál ezt a kapcsolást fogjuk használni.

Megjegyzés: A nyomógombhoz nem építettünk be felhúzóellenállást, hanem a programban kapcsoljuk be a belső felhúzást!

7. ábra: Az alapkapcsolás kiegészítése nyomógombbal és UART kommunikációval

A bemeneti szint megváltozását jelző interrupt kipróbálása

tankönyvi mintapéldák között található a chap09/change_test.c  program a bemeneti szint változását figyelő funkció kipróbálására ad lehetőséget. Az RB7 bemeneten a bemeneti szint megváltozását figyelő funkciót (CN23) engedélyezzük. A program egy értesítő üzenetet ír ki  minden alkalommal, amikor az SW1 nyomógombot lenyomjuk vagy felengedjük, s ennek hatására szintváltozást jelző programmegszakítás történik.

Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
A 28-pin Starter Board és a  Microstick Plus kártyákon tehát minden áramköri kiegészítés nélkül futtatható a program. A felsorolt hardverkülönbségek kiegyenlításáről feltételes fordítási direktívák gondoskodnak, ha helyesen beállítottuk a HARDWARE_PLATFORM makró értékét.

5. lista: A change_test.c program listája:
#include "pic24_all.h"

//-- SW1 definiálása HARDWARE_PLATFORM függő módon -----------
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS)
//-- Microstick Plus kártya ----------------------------------
#define SW1        _RA2        //SW1 nyomógomb RA2-re csatlakozik
inline void CONFIG_SW1() {     //külső felhúzással
  CONFIG_RA2_AS_DIG_INPUT();
  CONFIG_RA3_AS_DIG_OUTPUT();  //Magas szintre állítjuk az RA3 lábat
  _RA3 = 1;                    //(kiegészítő áramkörök bekapcsolása)
  DELAY_US(100);               //Várunk, amíg beáll a felhúzás
  ENABLE_RA2_CN_INTERRUPT();   //CN30IE = 1
}
#elif (HARDWARE_PLATFORM == STARTER_BOARD_28P)
//-- 16-bit 28-pin Starter Board   ---------------------------
#define SW1        _RB5        //SW1 nyomógomb RB5-re csatlakozik
inline void CONFIG_SW1() {     //külső felhúzással
  CONFIG_RB5_AS_DIG_INPUT();
  ENABLE_RB5_CN_INTERRUPT();   //CN27IE = 1
}
#else
//-- Alapértelmezett hardver platform ------------------------
#define SW1        _RB7        //SW1 nyomógomb RB7-re csatlakozik
inline void CONFIG_SW1() {
  CONFIG_RB7_AS_DIG_INPUT();
  ENABLE_RB7_PULLUP();
  DELAY_US(1);                 //Várunk, amíg beáll a felhúzás
  ENABLE_RB7_CN_INTERRUPT();   //CN23IE = 1
}
#endif
//-- HARDWARE_PLATFORM függő rész vége ------------------------

volatile uint8_t u8_cFlag = 0;

//-- A bemeneti változást jelző megszakítást kiszolgáló eljárás
void _ISRFAST _CNInterrupt (void) {
  _CNIF = 0;                   // a megszakítást jelző bit törlése
  u8_cFlag = 1;
}

int main (void) {
  configBasic(HELLO_MSG);
//-- SW1 nyomógomb konfigurálása és a hozzá tartozó
//-- CN megszakítás engedélyezése
  CONFIG_SW1();
//-- A bemenet változását jelző megszakítás beállítása 
  _CNIF = 0;                  //A megszakítást jelző bit törlésa
  _CNIP = 2;                  //Prioritás beállítása
  _CNIE = 1;                  //engedélyezzük a változást jelző megszakítást
  while (1) {
    if (u8_cFlag) {
      DELAY_MS(15);           //várunk a pergés megszűnésére
      u8_cFlag = 0;
      outString("Change notification interrupt occurred\n");
    }
    doHeartbeat();
  }
}
 
Az SW1 nyomógomb definiálását platformfüggő módon végezzük, hogy a fentebb felsorolt hardver különbségek miatt. A feltételes direktívákkal megtűzdelt #else  és #endif direktívák közötti szakasz vonatkozik az alapértelmezett kísérleti áramkör esetére. Ebben a részben definiáljuk a CONFIG_SW1() makrót, ami RB7-et digitális bemenetként konfigurálja, engedélyezi a belső felhúzást (tehát nincs szükség külső felhúzóellenállásra), s azt is engedélyezi, hogy a CN23  bemenet programmegszakítást kérjen a bemenet logikai szintjének minden megváltozásakor. Az ENABLE_RB7_CN_INTERRUPT() makró végeredményben a _CN23IE=1 utasításra lesz kifejtve. A belső felhúzás engedélyezése után célszerű egy rövid várakozást beiktatni az interrupt engedélyezése előtt, hogy időt hagyjuk a bemeneti szintek stabilizálódására, s az első VDD-re történő felhúzás ne okozzon fölöslegesen szintváltozást jelző programmegszakítást.

A Microstick Plus kártya esetében az SW1 nyomógomb az RA2 bemenetre van kötve, s a mikrovezzérlő adatlapjában vagy a korábbi fejezetek bekötési rajzain láthatjuk, hogy ennek a bemenetnek az "élesítéséhez" a CN30 változásjelző bemenet megszakítását kell engedélyeznünk. A nyomógomb ezen a kártyán külső felhúzással rendelekezik, de a felhúzása az RA3 kimenetről kap magas szintet.

A 28-pin Starter Board esetében az SW1 nyomógomb az RB5 bemenetre van kötve, ennek a bemenetnek az "élesítéséhez" a CN27 változásjelző bemenet megszakítását kell engedélyeznünk. A nyomógomb külső felhúzással rendelkezik, a belső felhúzás bekapcsolására tehát nincs szükség

A _CNInterrupt() eljárásban töröljük a CNIF jelzőbitet és beállítjuk az u8_cFlag eseményjelzőt. Az ISR végrehajtása után a programfutás a while(1) {} ciklusban folytatódik. Minden gombnyomás két programmegszakítást okoz: egyet lenyomáskor, egyet pedig felengedéshez. A kontaktusok pergéséből adódóan további programmegszakítási kérelmek keletkezhetnek. Ezek hatását a főprogramban elhelyezett 15 ms időtartamú késleltetéssel próbáljuk kiszűrni.

A főprogramban a while(1) {} ciklus előtt töröljük az esetleg beállt CNIF programmegszakítási kérelmet jelző bitet (az összes CNx bemenet engedélyezett programmegszakítási kérelme ebben az egyetlen jelzőbitben összegződik), majd engedélyezzük a CNIF jelzőbit érvényesülését a CNIE bit '1'-be állításával (ez a Change Notification interrupt általános engedélyezése) és prioritási szintet rendelünk hozzá (a konkrét prioritási szintnek itt nincs jelentősége, csak az a fontos, hogy nullánál nagyobb legyen!).

A while(1) {} ciklusban minden alkalommal, amikor a bemeneti szint megváltozását jelző programmegszakítás történt (az u8_cFlag '1'-be van állítva), kiíratjuk, hogy "Változásjelző programmegszakítás történt", s a nyomógomb pergését kiváró késleltetés letelte után töröljük az eseményjelzőt.

A program egy futtatásának képernyőképe az alábbi ábrán látható.

8. ábra: A change_test.c program kimenete

Nyomógomb pergésének vizsgálata

tankönyvi mintapéldák között található a chap09/change_bounce.c program azt mutatja be, hogyan használhatjuk a bemeneti szint változását jelző programmegszakítást az RB7-es bemenetre kötött nyomógomb pergésének detektálására, a CN23 bemeneti jelszint változását figyelő funkció felhasználásával.

A működés elve az, hogy minden jelszintváltozást jelző programmegszakításkor növelünk egy számlálót. Ha egy szoftveres pergésmentesítéssel figyelt gomblenyomás és felengedés után kettőnél több interrupt következett be, akkor megállapítjuk, hogy kontaktuspergés történt.

Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
Megjegyzés: Az eredeti programhoz képest megváltoztattuk a nyomógomb bekötését: az alapértelmezett kísérleti áramköznél RB13 helyett RB7-re került át, s a HARDWARE_PLATFORM-tól függő feltélteles fordítási direktíváknak köszönhetően a 28-pin Starter Board és a  Microstick Plus kártyákon a gyárilag ráépített nyomógombokat használjuk.

6. lista: A chap09/change_bounce.c program listája:
#include "pic24_all.h"

//-- SW1 definiálása HARDWARE_PLATFORM függő módon -----------
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS)
//-- Microstick Plus kártya ----------------------------------
#define SW1        _RA2        //SW1 nyomógomb RA2-re csatlakozik
inline void CONFIG_SW1() {     //külső felhúzással
  CONFIG_RA2_AS_DIG_INPUT();
  CONFIG_RA3_AS_DIG_OUTPUT();  //Magas szintre állítjuk az RA3 lábat
  _RA3 = 1;                    //(kiegészítő áramkörök bekapcsolása)
  DELAY_US(100);               //Várunk, amíg beáll a felhúzás
  ENABLE_RA2_CN_INTERRUPT();   //CN30IE = 1
}
#elif (HARDWARE_PLATFORM == STARTER_BOARD_28P)
//-- 16-bit 28-pin Starter Board   ---------------------------
#define SW1        _RB5        //SW1 nyomógomb RB5-re csatlakozik
inline void CONFIG_SW1() {     //külső felhúzással
  CONFIG_RB5_AS_DIG_INPUT();
  ENABLE_RB5_CN_INTERRUPT();   //CN27IE = 1
}
#else
//-- Alapértelmezett hardver platform ------------------------
#define SW1        _RB7        //SW1 nyomógomb RB7-re csatlakozik
inline void CONFIG_SW1() {
  CONFIG_RB7_AS_DIG_INPUT();
  ENABLE_RB7_PULLUP();
  DELAY_US(1);                 //Várunk, amíg beáll a felhúzás
  ENABLE_RB7_CN_INTERRUPT();   //CN23IE = 1
}
#endif
//-- HARDWARE_PLATFORM függő rész vége ------------------------

volatile uint8_t bcnt;           //a megszakításokat ebben számláljuk
#define SW1_PRESSED()   SW1==0 //lenyomott állapot feltétele
#define SW1_RELEASED()  SW1==1 //felengedett állapot feltétele

//-- A bemeneti szintváltozást jelző programmegszakítás kiszolgálása
void _ISRFAST _CNInterrupt (void) {
  _CNIF = 0;                   //törli a szintváltozást jelző bitet
  bcnt++;                      //inkrementálja a pergésszámlálót
}

int main (void) {
  configBasic(HELLO_MSG);
  /** A nyomógomb konfigurálása */
  CONFIG_SW1();                //a CN bemenet egyedi engedélyezése is itt történik
 /** A Change Notification megszakítás általános engedélyezése  */
  _CNIF = 0;                   //A CNIF interrupt jelzőbit törlése
  _CNIP = 2;                   //prioritás beállítása
  _CNIE = 1;                   //a változásjelző megszakítás általános engedélyezése
  while (1) {
    bcnt = 0;                  //a számláló nullázása
    outString("Press & release switch... ");
//-- Nyomja meg és engedje fel a nyomógombot!   
    while (SW1_RELEASED());
    DELAY_MS(DEBOUNCE_DLY );
    while (SW1_PRESSED());
    DELAY_MS(DEBOUNCE_DLY );
    if (bcnt != 2) outString("..bounced: ");  //pergett
    else outString("..no bounce: ");          //nem pergett
    outUint8(bcnt);                           //számláló kiírása
    outString("\n");                          //sorvége jel kiírása
  }
}

A már unásig ismételt CONFIG_SW1() makró és a főprograminicializáló része ugyanaz, mint az előző programban. Az új elem az, hogy deklaráltunk egy bcnt nevű változót, amit a _CNInterrupt() eljárásban minden alkalommmal növelgetünk (bcnt++).

Újdonság az előző programhoz képest az is, hogy az SW1 nyomógomb lenyomott, illetve felengedett állapotának vizsgálatához is definiáltunk egy-egy makrót (SW1_PRESSED(), SW1_RELEASED()). Ezekre most azért van szükség, mert a megszakítások bekövetkezésétől függetlenül, szoftveres pergésmentesítéssel kell detektálnunk a nyomógomb lenyomását és felengedését.

while(1) {} ciklusban töröljük az eseményszámlálót, kiírjuk a "nyomja meg és engedje fel a nyomógombot!" üzenetet, várunk a lenyomásra, a pergésmentesítő késleltetés után várunk a felengedésre, majd egy újabb pergésmentesítő késleltetés után kiértékeljük a történteket. Ha kettőnél több programmegszakítás volt, akkor megállapítjuk a pergés tényét. Végül kiírjuk az eredményt. A program kimenete az alábbi ábrán látható:

9. ábra: A change_bounce.c program kimenete

Felébresztés alvás/tétlen állapotból

A bemeneti jelszint megváltozását jelző programmegszakítások többek között arra is használhatók, hogy "felébresszék" a processzort az energiatakarékos alvás (sleep) vagy tétlen (idle) üzemmódból.  A tankönyvi mintapéldák között található a chap09/change_wakeup.c programban az RB7-as bemenetre kötött SW1 nyomógomb segítségével, a CN23 jelszint változását figyelő funkció felhasználásával "ébresztjük fel" a CPU-t.

A hardver követelmények megegyeznek az előző mintaprogramoknál leírtakkal. A program listájából a rövidség kkedvéért most kihagytuk a hardverfüggő definíciókat (SW1 definiálása és konfigurálása), mivel az szóról szóra megegyezik az előző programokban látható definíciókkal.

7. lista: A chap09/change_wakeup.c program listája (részlet):
#include "pic24_all.h"

//Eljárás a szintváltozást jelző programmegszakítás kiszolgálására
void _ISRFAST _CNInterrupt (void) {
  _CNIF = 0;    //törli a szintváltozást jelző bitet
}

int main (void) {
  configBasic(HELLO_MSG);
 /** A nyomógomb konfigurálása ***********/
  CONFIG_SW1();      //a CN bemenet egyedi engedélyezése is itt történik
 /** A Change Notification megszakítás általános engedélyezése  */
  _CNIF = 0;         //A CNIF interrupt jelzőbit törlése
  _CNIP = 2;         //prioritás beállítása
  _CNIE = 1;         //a változásjelző programmegszakítás általános engedélyezése
  while (1) {
    outString("Entering Sleep mode. Press button to wake.\n");
    //Kiírás: Alvás üzemmódba lépek. Nyomja meg a gombot a felébresztéshez!
    //Megvárjuk a kiírás befejezését az alvás üzemmódba lépés előtt
    WAIT_UNTIL_TRANSMIT_COMPLETE_UART1();
    SLEEP();         //makró asm("pwrsav #0") végrehajtásához
  }
}
A főprogram a while(1) {} ciklus előtti része is azonos a korábbi programokban látottakkal: töröljük az esetleg beállt CNIF programmegszakítási kérelmet jelző bitet, engedélyezzük a CNIF jelzőbit érvényesülését a CNIE bit '1'-be állításával és 2-es prioritási szintet rendelünk hozzá.

while(1) {} ciklusban egy figyelmeztető üzenet kiírása után az energiatakarékos "alvás" üzemmódba helyezzük a processzort, ahonnan csak az SW1 gomb megnyomásának hatására éled fel.

A _CNInterrupt() eljárásban nincs más tennivalónk, csupán töröljük a CNIF jelzőbitet. Az ISR  végrehajtása után a programfutás a a while(1) {} ciklusban folytatódik. A kontaktusok pergéséből adódó jelszintváltásoktól eltekintve minden gombnyomás két programmegszakítást okoz: egyet lenyomáskor, egyet pedig felengedéskor. 

A program kimenete az alábbi ábrán látható. Megfigyelhető, hogy többször is "felébresztettük" a mikrovezérlőt.

10. ábra: A change_wakeup.c program kimenete

Felébresztés alvásból, programmegszakítás nélkül

Az adatlap szerint arra is van lehetőség, hogy olyan programmegszakítási kérelemmel ébresszük fel az "alvó CPU-t, ami nem okoz valódi programmegszakítást, mert nem elegendő hozzá a prioritása. Ha az előző példában nullára állítjuk a megszakítás prioritását (_CNIP = 0), akkor az nem képes valódi megszakítást okozni,de az adatlap szerint felkellene ébreszteni a CPU-t. A tankönyvi mintapélda (change_wakeup_noint.c) azonban már a szerzőknek sem működött (PIC24HJ32GP202 esetén), s ugyanz a tapasztalatunk az újabb mikrovezérlőkkel is (PIC24HJ128GP502 és PIC24HJ64GP502).

Az interrupt késedelem meghatározása

tankönyvi mintapéldák között található chap09/change_latency.c program segítségével megmérhetjük, illetve ellenőrizhetjük az interrupt késedelem mértékét. A program működési elve az, hogy az RB2 kimenetet összekötünk a CN7 változásfigyelő bemenetként konfigurált RB3 bemenettel,  s az RB2 kimenet állapotának átbillentésével programmegszakítást generálunk. A programmegszakításhoz rendelt _CNInterrupt eljárásban állítjuk helyre az RB2 jelet. Az RB2 jelet oszcilloszkópon figyelve, a Tcy utasításciklus, vagy az 1/Tcy = Fcy frekvencia ismeretében  az RB2 beállítása és törlése közötti utasításciklusok száma meghatározható.



Megjegyzések
Mikrovezérlő FCYA definiálandó makró
PIC24F 4 MHzCLOCK_CONFIG=FRC_FCY4MHz
PIC24H3685 KHzCLOCK_CONFIG=FRC_FCY3685KHz
Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
8.a lista: A chap09/change_latency.c program listája
#include "pic24_all.h"

//-- A változást jelző megszakítás kiszolgálása
void _ISRFAST _CNInterrupt (void) {
  _LATB2 = 0;                //RB2 törlése
  Nop();                     //hagyjunk időt a jelterjedésre, hogy
  Nop();                     //az interrupt jelzőbit törlése valóban töröljön.
  Nop();
  _CNIF = 0;                 //törli a change notification interrupt jelzőbitet
}

int main (void) {
  configClock();             //Az órajel generátor beállítása
  CONFIG_RB2_AS_DIG_OUTPUT();
  _LATB2 = 0;
  CONFIG_RB3_AS_DIG_INPUT(); //RB3 legyen digitális bemenet
  ENABLE_RB3_CN_INTERRUPT(); //CN7 megszakításjelző engedélyezése
//-- A változást jelző megszakítás globális engedélyezése
    _CNIF = 0;               //törli az interrupt jelzőbitet
    _CNIP = 2;               //beállítja a prioritást
    _CNIE = 1;               //a Change Notification interrupt engedélyezése
    
    while( 1) {
    _LATB2 = 1;              //megszakítást keltünk
    Nop();                   //hagyjunk időt a jel terjedésére
    Nop();
    }

Ha nem rendelkezünk oszcilloszkóppal...

Ha nem rendelkezünk oszcilloszkóppal, akkor a PICkit2 logikai analizátor üzemmódjának segítségével is végezhetjük a méréseket.  A kényelmes használat érdekében nem az eredeti a chap09/change_latency.c programot használjuk, hanem annak egy módosított változatát (chap09/change_latency2.c). Módosítottuk a kapcsolást is, hogy az ICSP programozó csatlakozóra dugott PICkit2-vel tudjuk elvégezni a méréseket. Az eredeti programmal ellentétben nem RB3, hanem az RB1 bemenetre vezetjük az RB2 kimenet jelét. Ehhez a portlábhoz az CN5 változásfigyelő bemenet tartozik. A módosított kapcsolásból kihagytuk a tápegységet, mivel ennél a kísérletnél a PICkit2 is el tudja látni tápfeszültséggel az áramkörünket.

11. ábra: A módosított kísérleti áramkör az interrupt késedelem meghatározásához

Figyelem! Ez a kapcsolás és ez a program csak a PICkit2-vel közvetlenül programozott PIC-kwik kísérleti áramkörrel használható!

A módosított programban nem használjuk a PIC24 támogatói programkönyvtárat, s az oszcillátor frekvenciáját a szokásosnál jóval alacsonyabbra állítjuk be. Fosc most a beépített oszcillátor névlegesen 7.3728 MHz-es frekvenciájának csupán 1/16-a (kb. 460 kHz), Fcy pedig ennek fele: Fcy = 230 400 Hz (7.3728 Mhz /32). Erre a lassításra egyrészt azért van szükség, hogy a port késleltetési ideje elhanyagolható legyen az utasításciklus idejéhez képest, másrészt pedig figyelembe kell vennünk, hogy a PICkit2 logikai analizátor módban max. 1 MHz-el tudja mintavételezni a bejövő jeleket.

Az időmérés elősegítésére, Tcy (= 1/Fcy) ellenőrzéséhez még megtoldottuk a programot annyival, hogy az RB0 kimeneten egy ismert szélességű (5 Tcy időtartamú) impulzust állítunk elő, s az ismeretlen szélességű RB2 jelet ehhez fogjuk viszonyítani.

8.b lista: A change_latency2.c program listája
#include <p24hxxxx.h>
#define _ISRFAST __attribute__((interrupt)) __attribute__ ((no_auto_psv))

//--- Interrupt kiszolgáló eljárás a változásjelző megszakításhoz
void _ISRFAST _CNInterrupt (void) {
  _LATB2 = 0;   //RB2 törlése
  Nop();        //hagyjunk időt a jelterjedésre, hogy
  Nop();        //az interrupt jelzőbit törlése valóban töröljön.
  Nop();
  _CNIF = 0;    //törli a change notification interrupt jelzőbitet
}

main()

//--- Órajelgenerátor átkapcsolása: belső FRC/16
// Fcy = 230 400 Hz (7.3728 Mhz /32 )
   _TUN=0;
   __builtin_write_OSCCONH(0x06);
   __builtin_write_OSCCONL(0x01);
//--- Megvárjuk az átkapcsolást
     while(OSCCONbits.COSC != 0b001) {};

//--- I/O portok inicializálása 
    AD1PCFGL = 0x1fff;   // Analóg bemenetek tiltása
    TRISB    = 0xfffa;   // RB0 és RB2 legyen kimenet
    LATB     = 0x0000;   // PORTB kezdőállapot = 0x0000
//--- Időméréshez: RB0 5 Tcy idejére lesz magas szintű
    Nop();
    _LATB0 = 1;          //RB0 legyen magas szintű
    Nop();
    Nop();
    Nop();
    Nop();
    _LATB0 = 0;          //RB0 visszaállítása

//--- ENABLE_RB1_CN_INTERRUPT();
    _CN5IE=1;            //RB1 = CN5
//--- A Change Notification interrupt konfigurálása
    _CNIF = 0;           //törli az interrupt jelzőbitet
    _CNIP = 2;           //beállítja a prioritást
    _CNIE = 1;           //a Change Notification interrupt engedélyezése
   
    while( 1) {
      _LATB2 = 1;        //trigger interrupt by bringing high
      Nop();             //give the CN time to propagate
      Nop();
    }
}

A programmegszakítást kiszolgáló eljárásban RB2 törlése (emlékeztető: a read-modify-write típusú problémák elkerülésére RB2 beállításához a LATB regiszterbe írunk!) után azért iktatunk be három NOP utasítást, mert a CNx bemeneteken a jelterjedésre legalább 2 Tcy idő kell, így ha hamarabb törölnénk a CNIF jelzőbitet, az még visszabillenne az interrupt kiszolgáló rutin végéig.

Az órajelgenerátor beállításkor  alaphelyzetbe állítjuk az oszcillátort hangoló regisztert, beálltjuk a 6-os üzemmódot (Fosc = Fin/16), és kezdeményezzük az üzemmód átváltását, s megvárjuk az átkapcsolás végét.

Az I/O portok inicializálása most abból áll, hogy letiltjuk az analóg bemeneteket, RB0 és RB2 kimenetnek lesz beállítva (TRISB legalsó négy bitje: 1010), s alacsony szintre állítjuk a kimeneteket. Ezután kiadjuk az RB0 kimeneten az 5 Tcy szélességű jelet. Ha közvetlenül egymás után adnánk ki a bitbeállító és bittörlő utasításokat, akkor 1 Tcy ideig lenne RB0 magas szinten. Ha négy NOP utasítást közéjük iktatunk, akkor 4+1, azaz 5 Tcy ideig lesz RB0 maga szinten.

Az RB1 lábhoz tartozó CN5 bemenet engedélyezése a _CN5IE = 1; utasítással történik. A belső felhúzás engedélyezésére most nincs szükségünk.

A változást jelző interrupt engedélyezéséhez az előző példában látottakhoz hasonlóan töröljük az esetleg beállt CNIF programmegszakítási kérelmet jelző bitet, engedélyezzük a CNIF jelzőbit érvényesülését a CNIE bit '1'-be állításával és egy tetszőleges prioritási szintet rendelünk hozzá. Jegyezzük meg, hogy a programmegszakítás engedélyezése előtt mindig töröljük az interrupt jelzőbiteket, mert az engedélyezés előtti tiltás a jelzőbitek beállását nem, csupán azok érvényesülését akadályozta meg!

A 11. ábrán láthatjuk, hogy RB0 illetve az RB2 kimenettel összekötött RB1 rá van kötve az ICSP csatlakozóra, így ha a programot elindítjuk a beégetés után, és a PICkit2 kezelőprogramjában a Tools menüben elindítjuk a Logic Tool-t és Logikai Analizátor üzemmódba kapcsoljuk, akkor vizsgálhatjuk vele a kimenőjeleket. Most az RB0 jele a CH1 csatornára, az RB1-gyel összekötött RB2 jele pedig a CH2 csatornára kerül. Állítsuk be a mintavételezési sebességet (1 MHz) és a triggerelést (CH1 felfutás) az alábbi ábrán látható módon, majd kattintsunk a RUN gombra! A PICkit2 ekkor várakozó állapotba kerül, vár RB0 jelének felfutására. Ha a kísérleti áramkörünket újraindítjuk a RESET gomb megnyomásával, akkor bekövetkezik a triggerelés és kirajzolódik RB0 és RB2 hullámformája.

 
12. ábra: A change_latency2.c program kimenetének vizsgálata a PICkit2 logikai analizátora segítségével

A "Cursors" bepipálásával bekapcsolhatjuk a markerjeleket. X a bal egérgombbal, Y pedig a jobb egérgombbal rögzíthető. A Ch 1. csatorna RB0 kimenő jelét mutatja, az 5 Tcy szélességű impulzust 22 µs-nek mértük. Ne feledjük azonban, hogy a mintavételezés 1 MHz frekvenciával történik. Így csak annyit állíthatunk, hogy az 5 Tcy periódus 21 µs-nál több, de 23 µs-nál kevesebb.

A Ch 2. csatorna RB1, pontosabban a vele összekapcsolt RB2 jelét mutatja. A magas és az alacsony jelszintű szakaszok hossza egyaránt kb. 35 µs. , ami 8 utasításciklust jelent. Tcy pontosabb meghatározásához egy 80 utasításciklusnyi periódust mértünk meg, melynek ideje 348 µs. Ebből Tcy = 348 µs/80 = 4,35 µs. Az 5 Tcy hosszúságú impulzus akkor 5*4.35 µs = 21,75 µs, így nem csoda,hogy 22 µs-nek mértük. A 8 Tcy hosszúságú szakaszok pontosabban meghatározott időtartam pedig 34,8 µs. A névleges Fosc = 7.3728 MHz frekvenciából számolt Tcy ciklusidő pedig 4,3403 µs lenne, tehát a tényleges frekvencia elég jól megközelíti a névleges értéket.

Annak megértéséhez, hogy miért éppen 8 utasításciklus idejéig van magas, illetve alacsony állapotban az RB2, s hogy mennyi ebből az az idő, ami az interrupt késedelem rovására írható, meg kell néznünk alaposabban a végtelen ciklusban végrehajtott utasításokat. Az alábbi ábrán a C nyelvű utasítások mellett a fordító által generált assembly utasításokat is bemutatjuk, alattuk pedig a ciklus idődiagramját, melyen bejelöltük az egyes utasítások helyét is.


13. ábra: A change_latency2.c program végtelen ciklusának utasításai és idődiagramja

Az RB2 kimenet a bset LATB,#2 utasítás hatására kerül magas állapotba. Ezt követően két további utasításciklus kell ahhoz, hogy a CN5 bemenetre vezetett jel bebillentse a CNIF jelzőbitet, így a programmegszakítási kérelem a bra utasítás végrehajtása közben jelenik meg. A CPU főbb állapotjelzőinek elmentése és az interrupt vektor elővétele és az interruptot kiszolgáló eljáráshoz ugrás 4 további utasításciklust vesz igénybe, így RB2 törlése az interruptot követő 5. utasításciklusban, illetve az '1'-be állító bset utasítást követő 8. utasításciklusban történik meg (bclr LATB,#2).

A programmegszakítást kiszolgáló kódrészlet az előbb említett bclr LATB,#2 utasítással kezdődik. Utána azért iktattunk be három nop utasítást, hogy a CN5 bemenet állapota stabilizálódjon addigra,amikor a CNIF interruptkérő jelzőbitet töröljük. Az interruptból való visszalépés újabb 3 utasításciklust vesz igénybe. A visszatérés után a program a while(1) {} ciklus törzsének első utasításával, az RB2 kimenet '1'-be állításával folytatódik, s a program futása így vég nélkül a while(1) {} ciklus törzse és a _CNInterrupt() eljárás között pattog.

Végeredményben az interrupt kiszolgáló eljárásba való eljutás 4 utasításciklust vesz igénybe (Ibelép = 4 Tcy), az interrupt kiszolgálása 5 utasításciklusig tart (Itörzs = 5), a visszatérés pedig további 3 utasításciklust vesz igénybe (Ikilép = 3 Tcy). (A definíciókat lásd a Mibe kerül nekünk az interrupt használata? című szakasznál!) Összeségében tehát a program minden 16 utasításciklusból 12-t, azaz a futási idő 75 %-át az interrupt kiszolgálásával tölti el. Ez riasztóan magas aránynak tűnik, de ne feledjük, hogy ez egy speciális kísérleti program volt, s itt most nem volt semmi egyéb feladata a mikrovezérlőnek. 

Külső megszakítások, átkonfigurálható kivezetések

Az INTx interrupt bemenetek további lehetőséget nyújtanak arra, hogy valamilyen külső esemény hatására programmegszakítás keletkezzen a PIC24 mikrovezérlőkben. A megfelelő INTxEP polaritásválasztó bit segítségével az is  beállítható, hogy az adott INTx bemenethez rendelt INTxIF interrupt jelzőbit felfutó vagy lefutó élre billenjen '1'-be (INTxEP=1 a lefutó, INTxEP=0 pedig a felfutó élt választja). A legtöbb PIC24 mikrovezérlő három INTx bementtel rendelkezik: INT0, INT1, INT2. Ha megnézzük az alábbi ábrán a PIC24HJ128GP502 mikrovezérlő kivezetéseinek jelölését, akkor azt látjuk, hogy ezek közül csak INT0 van kivezetve (a 16. lábon). Hol van a többi bemenet? S hol vannak az adatlapon említett perifériák (2 UART port, 2 SPI port, 5 számláló, 4 Input Capture, 4 Output Compare és 1 ECAN modul) kivezetései? Nos, ezek üzembehelyzéséhez a mikrovezérlő "átkonfigurálható kivezetések" (remappable pins) tulajdonságának felhasználásával nekünk kell egyenként kivezetéseket rendelni a perifériákhoz.

14. ábra: A PIC24HJ128GP502 mikrovezérlő kivezetéseinek elrendezése. Az ábrán megjelöltük az átkonfigurálható kivezetéseket.

 Az átkonfigurálható kivezetések száma korlátozott: A PIC24HJ128GP502 mikrovezérlő esetében 16 kivezetés közül választhatunk (az ábrán színezéssel megjelöltük az átkonfigurálható kivezetéseket), a nagyobb lábszámú mikrovezérlőkön pedig 26 db. kivezetés áll rendelkezésre az átkonfiguráláshoz. A perifériák kivezetésekhez történő társítása két regisztercsoport segítségével történik, s meg kell különböztetnünk a periféria bemeneteket és kimeneteket, ezek hozzárendelése ugyanis függetlenül és eltérő logikával történik.

Bemeneti hozzárendelés

A periféria bemenetek hozzárendelése a mikrovezérlő kivezetéseihez a perifériák szerint történik, az egyes periféria bemenetekhez tartozó vezérlőregiszterekbe írt bitkombináció mondja meg, hogy az adott bemenet melyik lábhoz legyen hozzárendelve.

Az RPINRx regiszterek szolgálnak arra, hogy a perifériák és a kivezetések hozzárendelését szabályozzák. Minden regiszter 5 bites mezőket tartalmaz, amelyek egy-egy konfigurálható bemenethez tartoznak. Az 5 bites mezőbe írt szám mondja meg annak a kivezetésnek a sorszámát, amelyik az adott bemenethez rendelődik. Az érvényes számkombinációk tartománya az átkonfiguráláshoz felhasználható lábak számától függ.

A mellékelt ábrán egy átkonfigurálható bemenet, az 1-es UART port RX bemenetének lábkiválasztó áramkörének vázlatos felépítése látható. A periféria bemenetére egy 16-ból-1 multiplexer kimenete kapcsolódik (a nagyobb lábszámú mikrovezérlőkben pedig 26-ból-1 multiplexerek vannak), s a multiplexer bemenet                         15. ábra: Az U1RX bemenet lábkiválaszó áramkörének
kiválasztása a megfelelő RPINxR regiszterbe írt                     vázlatos rajza
5 bites kóddal állítható be.

4. táblázat: A PIC24HJ128GP502 mikrovezérlő átkonfigurálható periféria bemenetei
Bemenet funkció Név Hozzárendelés RPn-hez
Külső Interrupt 1 INT1 _INT1R = n;
Külső Interrupt 2 INT2 _INT2R = n;
Timer2 külső órajel T2CK _T2CKR = n;
Timer3 külső órajel T3CK _T3CKR = n;
Timer4 külső órajel T4CK _T4CKR = n;
Timer5 külső órajel T5CK _T5CKR = n;
Input Capture 1 IC1 _IC1R = n;
Input Capture 2 IC2 _IC2R = n;
Input Capture 7 IC7 _IC7R = n;
Input Capture 8 IC8 _IC8R = n;
Output Compare Fault A OCFA _OCFAR = n;
UART1 Vevő (Rx) U1RX _U1RXR = n;
UART1 adásra kész (CTS) U1CTS _U1CTSR = n;
UART2 Vétel (Rx) U2RX _U2RXR = n;
UART2 adásra kész (CTS) U2CTS _U2CTSR = n;
SPI1 adatbemenet SDI1 _SDI1R = n;
SPI1 órajelbemenet SCK1 _SCK1R = n;
SPI1 szolga kiválasztás bemenet SS1 _SS1R = n;
SPI2 adatbemenet SDI2 _SDI2R = n;
SPI2 órajelbemenet SCK2 _SCK2R = n;
SPI2 szolga kiválasztás bemenet SS2 _SS2R = n;
ECAN1 vétel CIRX _CIRXR = n;

Kimeneti hozzárendelés

A bemenetekkel ellentétben, a periféria kimenetek kivezetésekhez történő hozzárendelése az átkonfigurálható kivezetések szerint történik: az RPORx vezérlő regiszterek egy-egy 5 bites mezője valamelyik RPn kivezetéshez tartozik, s az adott mezőbe írt bitkombináció azt mondja meg, hogy az adott láb melyik periféria kimenetre csatlakozik.

Az alábbi táblázatban bemutatjuk a PIC24HJ128GP502 mikrovezérlő átkonfigurálható periféria kimeneteit, s azok kódját. A felsorolt kódok között szerepel a nulla is, ami lehetővé teszi, hogy egy adott kivezetés az alapértelmezett funkcióját lássa el és ne kapcsolódjon semelyik átkonfigurálható periféria kimenethez sem.

5. táblázat: A PIC24HJ128GP502 mikrovezérlő átkonfigurálható periféria kimenetei
Név Funkció Kód Példa
NULL Alapértelmezett portkivezetés 00000 _RPnR = 0;
C1OUT 1. Komparátor kimenete 00001 _RPnR = 1;
C2OUT 2. Komparátor kimenete 00010 _RPnR = 2;
U1TX UART1 adás (Tx) 00011 _RPnR = 3;
U1RTS UART1 adáskérés (RTS) 00100 _RPnR = 4;
U2TX UART2 adás (Tx) 00101 _RPnR = 5;
U2RTS UART2 adáskérés (RTS) 00110 _RPnR = 6;
SDO1 SPI1 adatkimenet 00111 _RPnR = 7;
SCK1OUT SPI1 órajelkimenet 01000 _RPnR = 8;
SS1OUT SPI1 Szolga kiválasztás kimenet 01001 _RPnR = 9;
SDO2 SPI2 adatkimenet 01010 _RPnR = 10;
SCK2OUT SPI2 órajelkimenet 01011 _RPnR = 11;
SS2OUT SPI2 Szolga kiválasztás kimenet 01100 _RPnR = 12;
C1TX ECAN1 adás 10000 _RPnR = 16;
OC1 Output Compare 1 10010 _RPnR = 18;
OC2 Output Compare 2 10011 _RPnR = 19;
OC3 Output Compare 3 10100 _RPnR = 20;
OC4 Output Compare 4 10101 _RPnR = 21;
Mivel az átkonfigurálható periféria ki-és bemenetek hozzárendelése a mikrovezérlő kivezetéseihez programfutás közben történik, többféle védelmi korlátozást lehet használni a konfigurációs regiszterek véletlen fölülírásának kivédésére. A PIC24H mikrovezérlő család háromféle lehetőséget kínál a konfigurációs regiszterek védelmére:
A vezérlőregiszterek zárolása azt jelenti, hogy ha az OSCCONL regiszter IOLOCK bitje '1' állapotban van, akkor az RPINx és RPORx vezérlő regisztereket nem tudjuk módosítani. Az írásvédelem feloldásához az IOLOCK bitet törölni kell, de ne feledjük, hogy az OSCCONH, OSCCONL regiszterek csak speciális parancssorozattal módosíthatók. Az MPLAB C30 fordítója az említett speciális parancssorozat kiváltására a _builtin_write_OSCCONL(adat) és a _builtin_write_OSCCONL(adat) beépített függvényeket kínálja. Mellesleg az oszcillátor üzemmódjának átkapcsolásához is használtuk már ezeket a beépített függvényeket.  

A direkt felülírás elleni védelmen túlmenően az RPINRx és RPORx regiszterek tartalmát árnyékregiszterek felhasználásával folyamatosan monitorizálja a hardver. Bármilyen eltérés esetén, amit pl. egy cellatartalmat módosító külső elektromos zavar is kiválthat, egy konfiguráció eltérés miatti újraindítás (configuration mismatch reset) történik

A biztonság további növelését jelenti az, ha a mikrovezérlőt úgy konfiguráljuk hogy megakadályozzuk az RPINRx és RPORx regiszterek egynél többszöri írását. Az IOL1WAY konfigurációs bit (FOSC<5>) bekapcsolása blokkolja az IOLOCK bitet, miután '1'-be állítottuk azt. A javasolt beállítási mód az, hogy elvégzünk minden konfigurálást, majd '1'-be állítjuk az IOLOCK bitet, s a program további menete során már nincs lehetőség a konfigurálás megváltoztatására, sem szándékosan, sem véletlenül.

A konfigurálható ki-/bemenetek használata

Bekapcsolás vagy reset után az RPINx és RPORx vezérlő regiszterek és az IOLOCK bit egyaránt nullázódnak. A konfigurációs regiszterek tehát írhatók (nincsenek zárolva), de erre szükségünk is van, ha bármelyik átkonfigurálható perifériát használni akarjuk. Az RPINx és RPORx regiszterek törlése resetkor azt eredményezi, hogy minden konfigurálható periféria bemenet az RP0 lábhoz lesz hozzárendelve, a konfigurálható kimenetek közül viszont egyik sem lesz kivezetve, hiszen minden konfigurálható kivezetés alapértelmezett állapotba kerül.

A PIC24 támogatói programkönyvtár include/pic24_ports.h állományában definiált makrók nagymértékben megkönnyítik a konfigurálható ki-és bemenetek beállítását. Például az INT1 bemenetet így rendelhetjük hozzá az RP6 lábhoz:
CONFIG_INT1_TO_RP(6); Ha a kiválasztott RPn lábhoz analóg funkciók is tartoznak, akkor az adott kivezetést digitális módba kell állítanunk a megfelelő működéshez (a hozzá tartozó _PCFGx bitet '1'-be kell állítani). Például a PIC24HJ128GP502 mikrovezérlő esetében  az alábbi kóddall állíthatjuk be az RP14 lábat digitális üzemmódba és rendelhetjük hozzá az INT2 külső programmegszakítás bemenetet:
CONFIG_RP14_AS_DIG_PIN();
CONFIG_INT2_TO_RP(14);

A legbiztosabb az, ha mindig kiadjuk a CONFIG_RPN_AS_DIG_PIN(); makrót, amikor konfigurálható ki- vagy bemenetet rendelünk valamelyik RPn lábhoz, mert a makró nem generál kódot, ha az adott lábhoz  nem tartozik analóg funkció. Természetesen ha biztosak vagyunk benne, hogy az adott lábhoz nem tartozik analóg bemenet, akkor elhagyhatjuk a fenti makrót.
   
A tankönyvi példaprogramokban gyakran használjuk az UART portot (egészen konrétan az 1. UART portot, mert van másik is...), úgy, hogy az U1Rx bemenet RP10-re, az U1TX kimenetet pedig RP11-re van konfigurálva. Ennek beállítása a fenti táblázatok alapján például így lehetséges:
_U1RXR = n;
_RPnR = 3;

A PIC24 támogatói programkönyvtár makrójával azonban így is írhatjuk:
CONFIG_U1RX_TO_RP(10);
CONFIG_U1TX_TO_RP(11);

Felébresztés alvásból INTx segítségével

A külső programmegszakítások kiróbálására nézzük most meg a korábban bemutatott change_wakeup.c program átdolgozott változatát, amelyben a CN megszakítás helyett az INT1 megszakítást használjuk a mikrovezérlő felébresztésére. Tetszés szerint, akár felfutó, akár lefutó éllel kelthetünk programmegszakítást. Az alábbi listán az _INT1EP=1 beállítással a negatív (lefutó) élt választottuk (a földre záró, tehát lehúzó nyomógombnál ez tűnik természetes választásnak). Az _INT1EP=0 beállítással választhatnánk a felfutó élt. A program a tankönyvi mintapéldák között található, a chap09/int1_wakeup.c állományban. A programot a PIC-kwik projekt keretében módosítottuk (megváltozatttuk az eredeti kapcsolást, s feltételes fordítási direktívákkal kiegyenlítjük az alább felsorolt hardver különbségeket.

Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
9. lista: A chap09/int1_wakeup.c program listája
#include "pic24_all.h"

//-- SW1 definiálása HARDWARE_PLATFORM függő módon -----------
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS)
//-- Microstick Plus kártya ----------------------------------
#define SW1        _RB6        //SW1 helyett a forgásérzékelőt használjuk!
inline void CONFIG_SW1() {
  CONFIG_RB6_AS_DIG_INPUT();
  CONFIG_RA3_AS_DIG_OUTPUT();  //Magas szintre állítjuk az RA3 lábat
  _RA3 = 1;                    //(kiegészítő áramkörök bekapcsolása)
  DELAY_US(100);               //Várunk, amíg beáll a felhúzás
  CONFIG_INT1_TO_RP(6);        //INT1 hozzárendelése RP6-hoz
}
#elif (HARDWARE_PLATFORM == STARTER_BOARD_28P)
//-- 16-bit 28-pin Starter Board   ---------------------------
#define SW1        _RB5        //SW1 nyomógomb RB5-re csatlakozik
inline void CONFIG_SW1() {     //külső felhúzással
  CONFIG_RB5_AS_DIG_INPUT();
  CONFIG_INT1_TO_RP(5);        //INT1 hozzárendelése RP5-hoz
}
#else
//-- Alapértelmezett hardver platform ------------------------
#define SW1        _RB6        //SW1 nyomógomb RB6-re csatlakozik
inline void CONFIG_SW1() {
  CONFIG_RB6_AS_DIG_INPUT();
  ENABLE_RB6_PULLUP();         //Belső felhúzás bekapcsolása
  DELAY_US(1);                 //Várunk, amíg beáll a felhúzás
  CONFIG_INT1_TO_RP(6);        //INT1 hozzárendelése RP6-hoz
}
#endif
//-- HARDWARE_PLATFORM függő rész vége ------------------------

//Eljárás az INT1 programmegszakítás kiszolgálására
void _ISRFAST _INT1Interrupt (void) {
  _INT1IF = 0;                 //törli az interrupt jelzőbitet
}

int main (void) {
  configBasic(HELLO_MSG);
  CONFIG_SW1();                //Nyomógomb konfigurálása
//-- Az INT1 interrupt beállítása és engedélyezése
  _INT1IF = 0;                 //Az interrupt jelzőbit törlése
  _INT1IP = 2;                 //prioritás beállítása
  _INT1EP = 1;                 //negatív élre érzékeny
  _INT1IE = 1;                 //az INT1 interrupt engedélyezése
  while (1) {
    outString("Entering Sleep mode, press button to wake.\n");
    //Kiírás: Alvás üzemmódba lépek. Nyomja meg a gombot a felébresztéshez!
    //Megvárjuk a kiírás befejezését az alvás üzemmódba lépés előtt
    WAIT_UNTIL_TRANSMIT_COMPLETE_UART1();
    SLEEP();                   //asm("pwrsav #0") végrehajtása
  }
}
Az _INT1Interrupt() eljárásban nincs más tennivalónk, csupán töröljük az INT1IF jelzőbitet. A megszkítást kiszolgáló eljárás végrehajtása után a programfutás a a while(1) {} ciklusban folytatódik. A kontaktusok pergéséből adódó jelszintváltásoktól eltekintve minden gombnyomás egy programmegszakítást okoz: a gomb lenyomásakor.
 
A program CONFIG_SW1() makrója RB13-at digitális bemenetként konfigurálja, engedélyezi a belső felhúzást (nincs szükség külső felhúzó ellenállásra).

A főprogram elején a nyomógomb konfigurálása után az INT1 bemenetet az RP6 lábhoz társítjuk (vagyis az RB6 bemenethez, amire a nyomógombot kötöttük). INT1 konfigurálásához töröljük az esetleg beállt INT1IF programmegszakítási kérelmet jelző bitet, beállítjuk, hogy lefutó élre (a gomb lenyomásakor) keletkezzen programmegszakítás, 2-es prioritási szintet rendelünk hozzá és engedélyezzük az interrupt érvényesülését az INT1IE bit '1'-be állításával.

while(1) {} ciklusban egy figyelmeztető üzenet kiírása után az energiatakarékos "alvás" üzemmódba helyezzük a processzort, ahonnan csak az SW1 gomb megnyomásának hatására éled fel.

A program kimenete megegyezik a change_wakeup.c programéval (lásd 10. ábra).

Az INT0 megszakítás használata

Természetesen, az INT0 megszakítást is kipróbálhatjuk. A fentihez nagyon hasonló lesz a program, a legnagyobb eltérést az okozza, hogy INT0 kivezetése kötött, fixen a 16. lábhoz (RB7) van rendelve.

Az eltérések (az int1_wakeup.c programhoz képest):
Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
A hardver különbségek kiegyenlítése most egyszerűbb, hiszen az INT0 bemenet kötött helye miatt minden platformnál az RB7 kivezetést kell használnunk. A fő különbség csupán annyi, hogy a Microstick Plus kártya használata esetén az RA3 kimenetet is magasra kell állítanunk, s a felhúzás bekapcsolása miatt hosszabb ideig (100 µs) kell várakoznunk.

10. lista: A  chap09/int0_wakeup.c program listája
#include "pic24_all.h"

//-- SW1 és konfiguráló makrójának definiálása  -----------
#define SW1        _RB7        //SW1 nyomógomb RB7/INT0-ra csatlakozik
inline void CONFIG_SW1() {
  CONFIG_RB7_AS_DIG_INPUT();
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS) 
  CONFIG_RA3_AS_DIG_OUTPUT();  //Magas szintre állítjuk az RA3 lábat
  _RA3 = 1;                    //(kiegészítő áramkörök bekapcsolása)
#endif 
  ENABLE_RB7_PULLUP();         //Belső felhúzás bekapcsolása
  DELAY_US(100);               //Várunk, amíg beáll a felhúzás


//Eljárás az INT1 programmegszakítás kiszolgálására
void _ISRFAST _INT0Interrupt (void) {
  _INT0IF = 0;                 //törli az interrupt jelzőbitet
}

int main (void) {
  configBasic(HELLO_MSG);
  CONFIG_SW1();                //A nyomógomb konfigurálása
//-- Az INT0 interrupt beállítása és engedélyezése
  _INT0IF = 0;                 //Az interrupt jelzőbit törlése
  _INT0IP = 2;                 //prioritás beállítása
  _INT0EP = 1;                 //negatív élre érzékeny
  _INT0IE = 1;                 //az INT0 interrupt engedélyezése
  while (1) {
    outString("Entering Sleep mode, press button to wake.\n");
    // Finish sending characters before sleeping
    WAIT_UNTIL_TRANSMIT_COMPLETE_UART1();
    SLEEP();        //macro for asm("pwrsav #0")
  }
}

Nyomógomb pergésének detektálása INTx  használatával

Az alábbi program azt mutatja be, hogyan használhatjuk az INT1 külső programmegszakítást az RB6-os bemenetre kötött nyomógomb pergésének detektálására.

A működés elve az, hogy minden jelszintváltozást jelző programmegszakításkor növelünk egy számlálót. Ha egy szoftveres pergésmentesítéssel figyelt gomblenyomás és felengedés után egynél több interrupt következett be, akkor megállapítjuk, hogy kontaktuspergés történt.

A program egy apró javítást tartalmaz a korábban bemutatott chap09/change_bounce.c programhoz képest: a főprogramban a nyomógomb felengedésekor a számlálóról egy másolatot készítünk, nehogy a számláló viszgálata és a kiírás között beérkező újabb  megszakítás(ok) ellentmondásos kiíráshoz vezessen(ek) (pl. ...no bounce: 0x03).

A programot a PIC-kwik projekt keretében módosítottuk (megváltozatttuk az eredeti kapcsolást, s feltételes fordítási direktívákkal kiegyenlítjük az alább felsorolt hardver különbségeket).

Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
11. lista: A chap09/int1_bounce.c program listája:
#include "pic24_all.h"

//-- SW1 definiálása HARDWARE_PLATFORM függő módon -----------
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS)
//-- Microstick Plus kártya ----------------------------------
#define SW1        _RB6        //SW1 helyett a forgásérzékelőt használjuk!
inline void CONFIG_SW1() {
  CONFIG_RB6_AS_DIG_INPUT();
  CONFIG_RA3_AS_DIG_OUTPUT();  //Magas szintre állítjuk az RA3 lábat
  _RA3 = 1;                    //(kiegészítő áramkörök bekapcsolása)
  DELAY_US(100);               //Várunk, amíg beáll a felhúzás
  CONFIG_INT1_TO_RP(6);        //INT1 hozzárendelése RP6-hoz
}
#elif (HARDWARE_PLATFORM == STARTER_BOARD_28P)
//-- 16-bit 28-pin Starter Board   ---------------------------
#define SW1        _RB5        //SW1 nyomógomb RB5-re csatlakozik
inline void CONFIG_SW1() {     //külső felhúzással
  CONFIG_RB5_AS_DIG_INPUT();
  CONFIG_INT1_TO_RP(5);        //INT1 hozzárendelése RP5-hoz
}
#else
//-- Alapértelmezett hardver platform ------------------------
#define SW1        _RB6        //SW1 nyomógomb RB6-re csatlakozik
inline void CONFIG_SW1() {
  CONFIG_RB6_AS_DIG_INPUT();
  ENABLE_RB6_PULLUP();         //Belső felhúzás bekapcsolása
  DELAY_US(1);                 //Várunk, amíg beáll a felhúzás
  CONFIG_INT1_TO_RP(6);        //INT1 hozzárendelése RP6-hoz
}
#endif
//-- HARDWARE_PLATFORM függő rész vége ------------------------

#define SW1_PRESSED()  (SW1==0)
#define SW1_RELEASED() (SW1==1)

volatile uint8_t u8_bcnt;      //a megszakításokat ebben számláljuk

//-- Eljárás az INT1 programmegszakítás kiszolgálására
void _ISRFAST _INT1Interrupt (void) {
  _INT1IF = 0;                 //törli az interrupt jelzőbitet
  u8_bcnt++;                   //növeli a pergésszámlálót
}

int main (void) {
  uint8 u8_cnt;
  configBasic(HELLO_MSG);
  CONFIG_SW1();                //A nyomógomb konfigurálása
//-- Az INT1 interrupt beállítása és engedélyezése
  _INT1IF = 0;                 //Az interrupt jelzőbit törlése
  _INT1IP = 2;                 //prioritás beállítása
  _INT1EP = 1;                 //negatív élre érzékeny
  _INT1IE = 1;                 //az INT1 interrupt engedélyezése
  while (1) {
    u8_bcnt = 0;               //a számláló nullázása
    outString("Press & release switch... ");
    //Kiírás: Nyomja meg és engedje fel a nyomógombot!   
    while (SW1_RELEASED());
    DELAY_MS(DEBOUNCE_DLY);
    while (SW1_PRESSED());
    DELAY_MS(DEBOUNCE_DLY);
    u8_cnt = u8_bcnt;          //átmásolja u8_bcnt értékét, hogy ne változzon
    if (u8_cnt != 1) outString("..bounced: "); //pergett
    else outString("..no bounce: ");           //nem pergett
    outUint8(u8_cnt);          //számláló kiírása
    outString("\n");           //sorvége jel kiírása
  }
}
Mivel a program a change_bounce.c és az int1_wakeup.c programokból könnyen összeollózható, nem fűzünk hozzá külön magyarázatot.

A program kimenete az alábbi ábrán látható:

16. ábra: Az int1_bounce.c program kimenete

Nyomógomb pergésének detektálása az INT0 megszakítás segítségével

Természetesen, az INT0 megszakítást is kipróbálhatjuk a nyomógomb pergésének detektálására. Az előzőhöz nagyon hasonló lesz a program, a legnagyobb eltérést az okozza, hogy INT0 kivezetése kötött, fixen a 16. lábhoz (RB7) van rendelve.

Az eltérések (az int1_bounce.c programhoz képest):
A hardware követelmények és a hardverfüggő részek definiálása megyegyeznek az int0_wakeup.c programnál korábban bemutatottakkal.


12. lista A chap09/int0_bounce.c program listája
#include "pic24_all.h"

//-- SW1 és konfiguráló makrójának definiálása  -----------
#define SW1        _RB7        //SW1 nyomógomb RB7/INT0-ra csatlakozik
inline void CONFIG_SW1() {
  CONFIG_RB7_AS_DIG_INPUT();
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS) 
  CONFIG_RA3_AS_DIG_OUTPUT();  //Magas szintre állítjuk az RA3 lábat
  _RA3 = 1;                    //(kiegészítő áramkörök bekapcsolása)
#endif 
  ENABLE_RB7_PULLUP();         //Belső felhúzás bekapcsolása
  DELAY_US(100);               //Várunk, amíg beáll a felhúzás
}

#define SW1_PRESSED()  (SW1==0)
#define SW1_RELEASED() (SW1==1)

//Interrupt Service Routine for INT0
volatile uint8_t u8_bcnt;
void _ISRFAST _INT0Interrupt (void) {
  _INT0IF = 0;                 //törli az interrupt jelzőbitet
  u8_bcnt++;                   //növeli a pergésszámlálót
}

int main (void) {
  uint8 u8_cnt;
  configBasic(HELLO_MSG);
  CONFIG_SW1();                //A nyomógomb konfigurálása
//-- Az INT0 interrupt beállítása és engedélyezése
  _INT0IF = 0;                 //Az interrupt jelzőbit törlése
  _INT0IP = 2;                 //prioritás beállítása
  _INT0EP = 1;                 //negatív élre érzékeny
  _INT0IE = 1;                 //az INT0 interrupt engedélyezése
  while (1) {
    u8_bcnt = 0;               //a számláló nullázása
    outString("Press & release switch... ");
    //Kiírás: Nyomja meg és engedje fel a nyomógombot!   
    while (SW1_RELEASED());
    DELAY_MS(DEBOUNCE_DLY);
    while (SW1_PRESSED());
    DELAY_MS(DEBOUNCE_DLY);
    u8_cnt = u8_bcnt;          //átmásolja u8_bcnt értékét, hogy ne változzon
    if (u8_cnt != 1) outString("..bounced: "); //pergett
    else outString("..no bounce: ");           //nem pergett
    outUint8(u8_cnt);          //számláló kiírása
    outString("\n");           //sorvége jel kiírása
  }
}