I/O portok

[Szerzői jogok]

A fejezet tartalma:

Az I/O portok vezérlő regiszterei

Ebben a fejezetben a PIC24H mikrovezérlő család általános célú ki- és bemeneteivel, az úgynevezett I/O portokkal foglalkozunk. A mikrovezérlők az I/O portokon keresztül tartják a kapcsolatot a külső áramkörökkel: fogadhatják a külső eszközök logikai jeleit, vagy vezérelhetik azokat. Vizsgálhatjuk például egy nyomógomb állapotát, vagy ki/be kapcsolhatunk egy LED-et. Az általános célú ki- és bemeneteknek a mikrovezérlő kivezetésein osztozkodniuk kell a speciális célú perifériákkal (soros port, számlálóbemenet, analóg komparátor, impulzusszélesség modulátor kimenet stb.).

Az általános célú ki- és bemeneteket a hatékony kezelés érdekében az adatút szélességének megfelelő bit-csoportokra vannak osztva, tehát a 8 bites mikrovezérlőknél  a portok is 8 bitesek, a PIC24 családnál pedig értelemszerűen 16 bitesek. Ha megnézzük valamelyik "százlábú" adatlapját, pl. a PIC24FJ256GB110 mikrovezérlőét, akkor abban az I/O portok impozáns listáját látjuk (Ports A, B, C, D, E, F, G). A mi 28 lábú PIC24HJ128GP502 mikrovezérlőnk esetében azonban a kivezetések korlátozott száma miatt jóval szerényebb a lista.

Az alábbi ábrán kiszíneztük az általános célú ki- bemenetként használható lábakat. Amint látjuk, csak a B port teljes, az A port meglehetősen hiányos, mindössze 5 bitnyi. Az A port bitjeit RA0, RA1,...,RA4 jelöli, ami az angol "register A" elnevezés rövidítéseként is értelmezhetünk. Ez azért is indokolt, mert a portok a programozáskor egy-egy Speciális Funkciójú Regiszternek (SFR) látszanak, ugyanis az adatmemória elkülönített címtartományába vannak leképezve. A B port bitjeit hasonló módon RB0, RB1,..., RB15 jelzi az ábrán. Az 1. ábrán látható elnevezések sokaságából sejthető, hogy a mikrovezérlő egy-egy kivezetésén sok funkció osztozik, melyekről a későbbiekben lesz szó. Most az általános célú ki/bemeneti funkcióra koncentrálunk, melyeket az RA0..RA4 és az RB0..RB15 elnevezések jelölnek.

1. ábra: A PIC24HJ128GP502 mikrovezérlő lábkiosztása.
Piros színezéssel az A port, zölddel a B port kivezetéseit jelöltük meg.


Az I/O portok működésének megértéséhez nézzük meg egy ki/bemenet vázlatos felépítését! Az egyszerűség kedvéért itt egy olyan esetet mutatunk be, amikor az általános célú ki/bemenet nem osztozik más perifériával a mikrovezérlő adott kivezetésén.

Az I/O portok vezérlése Speciális Funkciójú Regiszterek (SFR) segítségével történik. Minden I/O porthoz tartozik négy olyan regiszter, amelyek kifejezetten a port működésével társíthatók. Elnevezésükben az "x" betű az aktuális port betűjelével ("A", "B",...) helyettesítendő.

TRISx: Adatáramlási irány regiszter
PORTx: I/O Port regiszter
LATx: I/O Latch (adat retesz) regiszter
ODCx: Nyitott nyelőelektródás (Open Drain) üzemmódot vezérlő regiszter

A mikrovezérlő minden I/O kivezetéséhez  a TRISx, PORTx, LATx, és ODCx regiszterek  egy- egy bitje tartozik.

Az ábrán látható, hogy az I/O port kivezetéshez  a kimeneti meghajtóáramkörön kívül egy Schmitt-triggeres bemeneti áramkör is kapcsolódik, amely a külvilágból származó jeleket juttatja el a belső áramköri egységekhez. Ezen keresztül történik a Port olvasása.


                                                                         2. ábra: Az I/O port felépítésének vázlata

A következő ábrán láthatjuk, hogy mi az előnye a hiszterézissel rendelkező Schmitt-trigger bemenőfokozat alkalmazásának: a reális, zajokkal terhelt bemenőjelből kiszűri azokat a zajokat, melyeknek amplitúdója kisebb, mint a felső és alsó küszöbszint közötti különbség, s lassan változó bemenő jelek esetén határozottabbá teszi az átbillenést. A Schmitt-trigger hiszterézise abban nyilvánul meg, hogy a kimenet csak akkor billen magas szintre, amikor a a bemenő jel eléri a felső küszöbszintet, s mindaddig megmarad ebben az állapotban,amíg a bemenő jel le nem csökken az alsó küszöbszint alá.


3. ábra: Schmitt-triggeres bemenet jelalakja és hiszterézise

A TRIS regiszterek

Minden I/O porthoz tartozik egy-egy TRIS regiszter. A PIC24HJ128GP502 mikrovezérlő esetében tehát TRISA és TRISB regiszterekről beszélhetünk. Ezen regiszterek vezérlőbitjei határozzák meg az I/O porton az adatáramlás irányát, vagyis azt, hogy a hozzájuk tartozó kivezetés kimentként vagy bemenetként működjön. Ha egy TRIS bit '1'-be van állítva, akkor a hozzá tartozó kivezetés bementként működik, ha a TRIS bit '0', akkor pedig kimenetként. Könnyen memorizálhatjuk ezt a feltételt, ha arra gondolunk, hogy az '1' az Input szó kezdőbetűjére, a '0' pedig az Output szó kezdőbetűjére hasonlít. RESET alkalmával minden I/O kivezetés bemenetnek lesz beállítva.

Fentiekből következően a TRISx regiszterek segyítségével bitenként beállítható az adatáramlás iránya. Így tehát beállítható az is, hogy mondjuk PORTB 0. bitje kimenet legyen, az 1. bitje digitális bemenet, a 2. bitje analóg bemenet, a 3. bitje ismét kimenet, és így tovább.

A PORTx regiszterek

Az I/O lábakon az adatot a PORTx regiszteren keresztül érhetjük el. A PORTx regiszter kiolvasásával az I/O lábak állapotát olvashatjuk be, míg a PORTx regiszter írásával a port kimenő adatregiszterébe írhatjuk be az adatot, amellyel tulajdonképpen a kiemenet állapotát állítjuk be. Nem ilyen egyszerű a helyzet azoknál az utasításoknál, amelyek beolvasás-módosítás-visszaírás (read-mofiy-write)  típusú műveletek, amilyenek például a BSET és a BCLR bitmanipuláló utasítások is. Ezeknél egyetlen bit tartalmának módosítása megkívánja az egész port beolvasását, majd a tartalom módosítása utáni visszaírását. Kellő óvatossággal kell ezeket az utasításokat használni, mert kellemetlen meglepetések érhetnek bennünket az átgondolatlan használatnak. A probléma gyökere az, hogy ha ezek az utasítások a PORTx regisztert olvassák vissza, akkor mindig a bemenetek aktuális állapotot olvassák be, holott - egy bit módosítása esetén - nekünk nem erre van szükségünk!

Két esetet nézzünk meg:

Tegyük föl, hogy beírunk valamilyen számot a PORTB kimenő adatregiszterébe, majd néhány lábat átkapcsolunk bemenetnek (a TRISB regiszter megfelelő bitjeinek 1-be állításával). Ha most a kimenetnek hagyott bitek közül valamelyiket megváltoztatjuk egy BSET PORTB,#n vagy BCLR PORTB,#n utasítással, akkor mi történik? A mikrovezérlő beolvassa a PORTB pillanatnyi állapotát (a bemenetnek beállított lábakon azt, ami éppen jön kívülről), módosítja az n-edik bitet, majd az így kapott eredményt visszaírja a PORTB adatregiszterébe, felülírva annak korábbi tartalmát. Így nemcsak az n-edik bit állapota változhat, hanem azoké is, amelyek bementként működnek. A meglepetés akkor ér bennünket, ha kimentként állítjuk be ezen biteket, s arra számítunk, hogy az eredeti tartalom van PORTB adatregiszterében.   

A másik eset akkor fordul elő, ha kimenetként használt biteket módosítunk egyenként, gyorsan egymás után, a PORTx címzésével, s a mikrovezérlő sebességétől, és a kimenetek kapacitív terhelésétől függően nemkívánt eredményre juthatunk. Az alábbi ábrán egy olyan esetet mutatunk be, amikor a A port 0. és 1. bitjét írjuk két, egymást követő BSET utasítással. Nagy CPU sebességnél és nagy kapacitív terhelés esetén a nemvárt eredmény az lesz, hogy csak az 1. bit kerül '1' állapotba, a 0. bit pedig nem. 

4. ábra: A read-modify-write probléma szemléletése helytelen portkezelés esetén

Az időskálán számozással jelöltük az egymás után bekövetkező eseményeket:

  1. A BSET PORTA,#0 utasítás véget ért, a PORTA 0. bitjéhez tartozó RA0 kimeneten növekedni kezd a feszültség (a kapacitív terhelés miatt viszonylag lassan).
  2. BSET PORTA,#1 utasítás végrehajtása a PORTA beolvasásával kezdődik. A kimenet lassan növekedő feszültség még nem érte el a magas szint küszöbfeszültségét (általában VH = 0.8*VDD), emiatt PORTA 0. bitjének beolvasott értéke '0' lesz!
  3. P0RTA 0. bitjének kimenőjele eléri a magas logikai szintet.
  4. A BSET PORTA,#1 utasítás véget ért, a módosított érték (mely szerint PORTA 0. bitje '0', az 1. bitje pedig '1') beírásra került az adatregiszterbe. RA0 kimenő feszültsége csökkenni kezd, RA1 kimenőjele pedig növekedik. Az idő múlásával beáll a nemkívánt végeredmény: RA0 = 0, RA1 =1.

A LATx regiszterek

Az I/O portokhoz tartozó LATx (esetünkben LATA és LATB) regiszterek segítségével a beolvasás - módosítás - visszaírás típusú utasítások fenti problémái elkerülhetők. A LATx regiszterre történő hivatkozáskor íráskor ugyanúgy a kimeneti adatregisztert érjük el, mintha a PORTx regiszterre hivatkoztunk volna, olvasásakor és a read-modify-write típusú bitmanipuláló utasításoknal pedig nem a bemeneti lábak, hanem a kimenő adatregiszter bitjeinek állapotát olvassuk vissza. Egy LATx regiszteren végrehajtott beolvasás-módosítás-visszaírás típusú utasítás esetében tehát nem fordulhat elő, hogy a bemenet pillanatnyi állapota íródik vissza a kimenő adatregiszterbe.  

Aranyszabályként tehát jegyezzük meg, hogy a kimeneti adatregiszter írásakor, módosításakor, visszaolvasásakor mindig a LATA vagy LATB regiszterekre hivatkozzonk! Ha pedig a bemenet aktuális állapotára vagyunk kíváncsiak, akkor a PORTA PORTB regiszterekre hivatkozzunk!

A következő példában a LATA regisztert használjuk PORTA két bitjének 1-be állítására:
BSET LATA, #0    ;Port A 0. bitjét ‘1’-be állítja
BSET LATA, #1    ;Port A 1. bitjét ‘1’-be állítja

A PORTx és LATx  regiszterek összehasonlítása így összegezhető:
 • A PORTx regiszterbe történő írás az adatot a port adatregiszterébe írja
 • A LATx regiszterbe történő írás az adatot a port adatregiszterébe írja
 • A PORTx regiszterből történő olvasás a ki/bemeneti lábak állapotát olvassa
 • A LATx regiszterből történő olvasás a port adatregiszterének állapotát olvassa

Az ODCx regiszterek

A PORTx, LATx és TRISx adatáramlást vezérlő regisztereken kívül arra is lehetőség van, hogy az I/O portok minden kimenetnek beállított kivezetésére külön-külön beállíthassuk, hogy logikai kimenetként vagy nyitott nyelőelektródás kimenetként működjenek. Ez az adott porthoz tartozó ODCx (Open Drain Control - nyitott nyelőelektródás mód vezérlése) nevű regiszter segítségével történik. Ha az ODCx regiszter valamelyik bitjét '1'-be állítjuk, akkor a hozzá tartozó kimenet nyitott nyelőelektródás üzemmódban működik. RESET alkalmával a regiszter 0 tartalommal  kerül alaphelyzetbe.

A nyitott nyelőelektródás üzemmód többek között arra használható, hogy több kimenetet egyszerűen, egyetlen felhúzó ellenállás felhasználásával köthessünk VAGY kapcsolatba.Egy másik hasznos lehetőség a szintátalakítás, amelyre akkor van szükségünk, ha a 3.3 V-os tápfeszültséggel működő mikrovezérlőnkkel magasabb jelszintű (pl. 5 V-os) logikai bemenetet kell vezérelnünk. A PIC24 mikrovezérlők "csak digitális feladatkörű" kivezetései elviselik, hogy bemenetként vagy nyitott nyelőelektródás kimenetként kapcsolva a tápfeszültségnél magasabb jelszintet kapcsoljunk rájuk. Így egy nyitott nyelőelektródás kimenet és egy külső, +5 V-ra kötött felhúzó ellenállás segítségével 5 V-os tápfeszültséggel működő logikai bemeneteket is vezérelhetünk. Az adatlap VIH paramétere adja meg, hogy mekkora a "csak digitális funkciójú" nyitott nyelő elektródás kimenetre kapcsolható maximális feszültség.

Az alábbi ábrán az adatlap alapján színezéssel megjelöltük azokat a kivezetéseket, amelyeknek csak digitális feladatkörük van. Kizárólag ezek tolerálják a 3.3 V-os tápfeszültségnél magasabb (5 V-os) bejövő jelszintet.

5. ábra: A lábkiosztási rajzon megjelöltük  5 V-os jelszintet toleráló bemeneteket

A mikrovezérlő kimenetek nyitott nyelőelektródás üzemmódja az eddig ismertetett általános célú I/O port használatán kívül az egyéb perifériák kimeneteinél is használhatók (amikor egy megosztott feladatkörű I/O lábat egy másik funkcióra konfigurálunk).

Hibaigazítás: Ne higgyünk az adatlapnak! Az adatlapban és a Referencia Kézikönyvben leírtakkal ellentétben a PIC14HJ128GP502 mikrovezérlő minden ki/bemenetéhez (RA0..4, RB0..15) implementálva van egy-egy bit az ODCA és ODCB regiszterekben, tehát a mikrovezérlő bármelyik kimenete nyitott nyelőelektródás üzemmódba kapcsolható. 

Az I/O portok programozása

Ahhoz, hogy a C programokban szimbolikus nevekkel hivatkozhassunk a regiszterekre,a fordítónak "ismernie" kell ezen neveket. Ehhez a program elején be kell csatolni a megfelelő fejléc állományt. Erre két módszer van: a kényelmesebb az, amit eddig is használtunk: a pic24hxxxx.h állományt csatoljuk be, s ez a feltételes fordítás direktíváinak felhasználásával "elintézi nekünk", hogy az MPLAB Projekt Wizard-ban, vagy az MPLAB Configure menüje Select Device pontjában kiválasztott eszköz konfigurációs állomány tartalma legyen hozzáfűzve a programunkhoz. Tulajdonképpen ugyanez történik a PIC24 támogatói programkönyvtár használatánál is, csak ott a pic24_all.h fejléc állomány csatolja be nekünk a pic24hxxxx.h állományt.

A másik módszer az, hogy közvetlenül az adott eszköz konfigurációs állományára hivatkozunk. A PIC24HJ128GP502 mikrovezérlő esetén ez az MPLAB C30 fordítójának support/PIC24H/h mappájában található p24HJ128GP502.h állomány. Ebben találjuk meg a fejezet elején felsorolt regiszterek definiálását.

Ha belenézünk a p24HJ128GP502.h állományba, akkor láthatjuk, hogy az A porthoz tartozó TRIS regiszterre TRISA néven hivatkozhatunk. Kiadhatunk pl. egy

TRISA = 0x0000;

utasítást, amely az A port minden kivezetését (RA0..RA4) kimentnek állít be. De ugyanezt a TRISA regisztert címezhetjük bitenként is.  Az alábbi két utasítás például a  TRISA regiszter 0. bitjét '1'-be, az 1. bitjét pedig '0'-ba állítja:

TRISAbits.TRISA0 = 1;
TRISAbits.TRISA1 = 0;

Tömörebb írásra is van lehetőség,mivel a fenti két utasítás így is írható:

_TRISA0 = 1;
_TRISA1 = 0;

Hasonlóan történik a többi regiszter kezelése is:

TRISB = 0xF00F;                  //RB4..RB11 legyen kimenet, RB0..3 és RB12..15 pedig bemenet
PORTB = 0x05A0;                 //RB4..RB11 legyen  0101 1010
while ((PORTB&0x0001) == 0); //A program addig vár, amíg RB0 alacsony, RB0==1 esetén mehet tovább
while ((PORTB&0x0008)==1);   //A program addig vár, amíg RB3 magas, RB3==0 esetén mehet tovább

Az I/O portok egyes bitjeihez _RA0, RA1,...,_RA4, illetve _RB0, _RB1,...,_RB15 néven, vagy a hosszabb jelöléssel: PORTAbits.RA0 .. PORTAbits.RA4, illetve PORTBbits.RB0 .. PORTBbits.RB15 jelöléssel férhetünk hozzá. A fenti várakozó ciklusokat például így is írhatjuk:
while (_RB0 == 0);                //Másképp is írhatjuk: while (!_RB0);
while (_RB3==1);                  //Másképp is írhatjuk: while (_RB3);

A LATx regiszterek neve LATA, illetve LATB. Az egyes bitekhez _LATA0, _LATA1,..,_LATA4, _LATB0, .., _LATB15 néven férünk hozzá, vagy a hosszabb LATAbits.LATA0 stb. jelöléssel.  

A PORTx és LATx regiszterek különbözősége

Körültekintőnek kell lenni a portbitek egyenkénti írásánál, mert ahogy "A PORTx regiszterek" c. szakaszban már említettük, ezek olvasás/módosítás/visszaírás típusú műveletek, melyek nemkívánt eredményre vezethetnek, mint például az alábbi példában is, ha helytelenül végezzük a bitmanipulációs műveleteket.

Az alábbi programot  az MPLAB szimulátorában futtattuk, utasításonként léptetve (F8 - Step Over). Az ábrán pirossal megjelöltük azokat az eredményeket, amelyek meglepően eltérnek a "várt" eredménytől.

1. lista: Hibás portkezelés (chap08/port01.c)

A nemvárt események és magyarázatuk:

1. Az A port minden bitjét kimenetnek állítottuk, s LATA=0xffff után a LATA regiszterben a várt értéket találjuk (az A port csak öt bites, így 0x1F a korrekt érték), de PORTA miért 0x1C, miért nulla RA0 és RA1?
RESET-kor az analóg funkciók közül az ADC engedélyezett állapotba kerül, ezért az AN0 és AN1 bemeneti funkció érvényesül, az általános célú kimeneti funkció pedig nincs engedélyezve. Ha az RA0 és RA1 lábakat is kimenetként akarjuk használni, akkor le kell tiltani ezeken a lábakon az analóg funkciókat!

2. Az _RA0=0 utasítástól azt várjuk, hogy csak az A port 0. bitjét törli, utána mégis 0x1C olvasható a LATA regiszterben! Miért?
Ahogy "A PORTx regiszterek" c. szakaszban már említettük, hogy a PORTx regiszterekre hivatkozó olvasás/módosítás/visszaírás típusú műveletek a PORTx pillanatnyi állapotát  veszik figyelembe, és ennek a módosítása íródik vissza a a kimeneti adatregiszterbe. Mivel PORTA pillanatnyi értéke 0x1C, az _RA0=0 utasítás nem módosít rajta (mivel RA0 már nulla), s ez az érték kerül beírásra a LATA regiszterbe, tehát szándékunktól eltérően RA1 is törlődik. A problémát elkerülhetjük, ha a PORTA regiszter helyett a LATA regiszteren végezzük a bitműveletet. Ehhez , persze, tudnunk kell, hogy a C fordító hogyan értelmezi a fenti utasításokat. Az MPLAB fejlesztői környezetben ezt pl. a View menü Disassembly Listing menüpontjában megnyitható ablakban nézhetjük meg:

_RA0 = 0;    →    bclr PORTA,#0  ;Nem ajánlott!
_RA1 = 0;    →    bclr PORTA,#1  ;Nem ajánlott!

Láthatjuk, hogy "rossz lóra tettünk", hiszen pont azzal a módszerrel kezeljük a biteket, ami erősen ellenjavallt! Helyette a _LATAx bitek használata ajánlott:

_LATA0 = 0;    →    bclr LATA,#0
_LATA1 = 0;    →    bclr LATA,#1

3. Ha az _RB0=1 utasítás után a LATB regiszter tartalma 0x0001, akkor PORTB tartalma miért 0x0000?
A jelenség magyarázata ugyanaz, mint ami az 1. pontban olvasható. A RESET-kor engedélyezett állapotba kerülő ANx bemeneti funkciók PORTB 0-3. és 12-15. bitjeit is érinti. Ha ezeket kimenetként akarjuk használni, akkor le kell tiltani ezeken a lábakon az analóg funkciókat!

4. Ha az _RB1=1 utasítással '1'-be állítjuk az 1. bitet,akkor miért törlődik a 0. bit?
A jelenség a 2.pontban leírtak alapján érthető meg. A fordító így értelmezi az általunk írottakat:

_RB1 = 1;    →    bset PORTB,#1  ;Nem ajánlott!

Mivel PORTB pillanatnyi tartalma nulla, így a módosítás és visszaírás után csak az utoljára beállított bit lesz '1' állapotban. Megoldás: a kívánt eredmény eléréséhez a LATB regiszterre kell hivatkozni!

5. Ha az _RB2=1 utasítással '1'-be állítjuk a 2. bitet, akkor miért törlődik a 1. bit?
A jelenség a 4. pontban leírtak alapján érthető meg.

6. Ha a LATB regiszter tartalma 0xFFFF, akkor PORTB tartalma miért 0x0FF0?
A jelenség magyarázata ugyanaz, mint ami az 1. pontban olvasható. A RESET-kor engedélyezett állapotba kerülő ANx bemeneti funkciók PORTB 0-3. és 12-15. bitjeit is érinti. Ha ezeket kimenetként akarjuk használni, akkor le kell tiltani ezeken a lábakon az analóg funkciókat! A közbenső biteket (RB4..RB11) ez a probléma nem érinti, ezért ezek a bitek úgy működnek, ahogy elvártuk.

A portkezelés javított változatban

Az alábbi ábrán bemutatjuk a fenti program kijavított változatát, C és assembly nyelven is. Az előző (hibás programhoz képest két fő változtatásra volt szükség:
  1. Az AD1PCFGL=0x1fff utasítás letiltja az ADC bemeneteit (AN0-tól AN12-ig). Ennek részleteit a PIC24HJ128GP502 vagy a dsPIC33FJ128GP802 mikrovezérlő adatlapjának ADC-re vonatkozó fejezetében találjuk meg. A lényege az, hogy ha az ADC analóg bemeneteinek működését vezérlő AD1PCFGL regiszter valamelyik bitjét '1'-be állítjuk, akkor a hozzá tartozó ANx bemenet letiltásra kerül.
  2. A bitműveleteket a PORTx regiszterek helyett a LATx regisztereken végezzük.

2. lista: Helyes portkezelés (chap08/port01b.c)
 

A ki/bement megosztása más perifériákkal

Néhány PIC24H eszköz esetében, különösen a kis lábszámú típusoknál az általános célú I/O porton kívül számos periféria osztozik a mikrovezérlő egy-egy kivezetésén. Az alábbi ábrán egy olyan példát mutatunk be, ahol az I/O porton kívül két további periféria osztozik az adott kivezetésen: az A periféria és a B periféria. Ezt az I/O láb melletti PERA/PERB/PIO felirat is jelzi. A feliratok sorrendje a perifériák prioritását is jelzi, s ahogy a vázlatos rajzon is látható, valóban az A periféria rendelkezik a legnagyobb prioritással a ki/bemenet vezérlésre. Ha az A és B perifériák egyidejűleg engedélyezve vannak, akkor is az A periféria ragadja magához a vezérlést.



6. ábra: Megosztott funkciójú port felépítésének vázlata

Megjegyzés: Az általános célú I/O portok bizonyos lábai az ADC modulhoz is kapcsolódnak. Ezeknél a digitális ki/bemenet használatához az analóg funkciót az AD1PCFGL regiszter  megfelelő bitjének '1'-be állításával le kell tiltani, akkor is, ha az ADC egyébként ki van kapcsolva.

AD1PCFGL=0x1fff;   //Az összes analóg bemenet tiltása

Az I/O port regisztereihez hasonlóan bitenkénti hozzáférésre is van lehetőség:

_PCFG10 = 1;   Letiltja az analóg funkciót, és digitális
_TRISB14= 1;   bementként konfigurálja az RB14 lábat


_PCFG9  = 1;   Letiltja az analóg funkciót, és digitális
_TRISB15= 0;  bementként konfigurálja az RB15 lábat

Megjegyzés: Amint az ábrán is látható, az AN9 bemenet (melyet az AD1PCFGL 9. bitjével, vagy másképpen az _PCFG9 nevű bitváltozó '1'-be állításával lehet letiltani) az RB15 I/O lábbal közös, az AN10 bemenet pedig RB14-gyel osztozik.                                7. ábra: A példában szereplő analóg bemenetek

Szoftveres bemenetvezérlés

Az I/O lábhoz tartozhat olyan periféria funkció, ami bementként működik, tehát nem tiltja le az általános célú I/O port kimenetét. Például ilyen tulajdonságú az Input Capture, vagy a számlálók  bemenete. Ilyen esetben az I/O port megfelelő bitjét kimenetként beállítva (a TRIS regiszter megfelelő bitjébe '0'-t írva) a PORTx I/O kimenetén keresztül szoftveresen manipulálhatjuk az adott periféria bemenetét. Ez a lehetőség nagyon hasznos lehet tesztelésnél, amikor a külső jel még nincs rákötve a bemenetre, szimulálni szeretnénk annak hatását.    

Általában az alábbi perifériák teszik lehetővé a bemenetük szoftveres vezérlését:
A legtöbb soros kommunikációs periféria, amikor engedélyezve van, teljesen átveszi az vezérlést az I/O láb fölött, úgyhogy az ilyen periféria bemenete nem manipulálható a PORTx regiszteren keresztül. Ilyen típusú perifériák az alábbiak:

Bemeneti szint megváltozásának jelzése

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 PIC24H család tagjainál legfeljebb 24 változásérzékelő bemenet lehet - az alacsony lábszámú PIC24HJ128GP502 esetében mindegyik I/O portlábhoz tartozik változásérzékelő funkció.

Az alábbi ábrán csak a CN0 bit funkcionális felépítését mutatjuk meg, a többi 23 bithez is ugyanilyen felépítés tartozik. A bemeneti jelet a mikrovezérlő rendszeresen mintavételezi, s a két, sorbakötött D tároló mindig az aktuális és az előző mintavételezés eredményét tartalmazza. Ha szintváltás történt, akkor kimenetük különböző lesz, s a kizáró-vagy áramkör kimenete '1' állapotba kerül. Ha a CN0 bemenet programmegszakítási kérelmi funkcióját engedélyeztük a CNEN1 regiszter 0. bitjének beállításával, akkor egy programmegszakítási kérelem jut a mikrovezérlő interrupt kezelő áramköreibe. (A többi CNx bemenet engedélyezése hasonlóan, a CNEN1 vagy CNEN2 regiszterek megfelelő bitjének beállításával történhet.)

A változást érzékelő CN bemenetekhez egy 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". Ennek engedélyezése a CNPU1, CNPU2 regiszterek megfelelő bitjének beállításával történhet.


8. ábra: A CN0 bemeneti szint változást detektáló áramkör felépítésének vázlata

Port konfiguráló makrók

Az "A Kísérleti áramkör" c. fejezetben röviden már említettük, hogy a PIC24 támogatói programkönyvtár többek között az I/O portok konfigurálásában is segítségünkre van. Számos, az alábbiakhoz hasonló makrót definiál, amelyek elrejtik a felhasználó elől az I/O port lábak beállításának technikai részleteit:
 
CONFIG_RB15_AS_DIG_OUTPUT();
CONFIG_RB15_AS_DIG_INPUT();

Ehhez hasonló makrók minden ki/bemenethez rendelkezésre állnak. Mivel a makrodefiníciók függenek a használt mikrovezérlő típusától, ezért a programkönyvtár lib/include/devices alkönyvtárában minden mikrovezérlőhöz tartozik egy-egy konfigurációs állomány, amelyek közül a megfelelőt az lib/include/pic24_ports.h fájl automatikusan becsatolja - annak függvényében, hogy melyik mikrovezérlőt választottuk az MPLAB menüjében.

További makrók állnak rendelkezésre a belső felhúzások vagy a nyitott nyelőelektródás kimenet beállításához:

ENABLE_RB15_PULLUP();                  //Belső felhúzás engedélyezése az RB15 bemeneten
DISABLE_RB15_PULLUP();                 //Belső felhúzás letiltása az RB15 bemeneten
ENABLE_RB15_OPENDRAIN();            //RB15 nyitott nyelőelektródás üzemmód bekapcsolása  
DISABLE_RB15_OPENDRAIN();           //RB15 nyitott nyelőelektródás üzemmód kikapcsolása
CONFIG_RB8_AS_DIG_OD_OUTPUT;    //Digitális kimenet és OD üzemmód egy makróba összevonva

Természetesen a fentiekhez hasonlóan bármelyik Rx regiszter n. bitje megnevezhető Rxn formában. Azonban azok a bemenetek nem konfigurálhatók belső felhúzással, amelyekhez nem tartozik bemeneti szint megváltozását jelző (CN) funkció. Ha ilyesmivel próbálkoznánk, a fordítóprogram hibajelzést adna, mivel a CN funkcióval nem rendelkező bemenetekhez nincs definiálva felhúzást beállító makró.

Nézzük meg részletesebben az I/O portok konfigurálása szolgáló makrókat a lib/include/devices/ mappában található pic24hj128gp502_ports.h állományban! Mintaként nézzük meg az RB0-ra vonatkozó makrókat:

#define ENABLE_RB0_PULLUP() _CN4PUE = 1        //Belső felhúzás engedélyezése
#define DISABLE_RB0_PULLUP() _CN4PUE = 0       //Belső felhúzás letiltása
#define ENABLE_RB0_CN_INTERRUPT() _CN4IE = 1   //Változásjelző megszakítás engedélyezés
#define DISABLE_RB0_CN_INTERRUPT() _CN4IE = 0  //Változásjelző megszakítás letiltása
#define ENABLE_RB0_OPENDRAIN() _ODCB0 = 1      //Nyitott nyelőelektródás mód
#define DISABLE_RB0_OPENDRAIN() _ODCB0 = 0     //Ellenütemű kimeneti mód
#define DISABLE_RB0_ANALOG() _PCFG2 = 1        //RB0 analóg funkció letiltása
#define ENABLE_RB0_ANALOG() _PCFG2 = 0         //RB0 analóg funkció engedélyezése


Ha visszalapozunk a fejezet elére, az 1. ábrához, akkor a bekötési razról leolvashatjuk, hogy az RB0 kivezetéshez a CN4 változásjelző bemenet, illetve az AN2 analóg bemenet tartozik. Így érthető, hogy a hozzátartzó makrók a _CN4PUE, _CN4IE, ODCB, valamint a _PCFG2 biteket állítják be. Nem árt, ha tudjuk, hogy mit jelentenek ezen rövídített jelölések, amelyeket a Microchip C30 support/PIC24H/h mappájában található fejléc állományok definiálnak:
_CN4PUE a belső felhúzást engedélyező  CNPU1 regiszter 4. bitjét (CN4PUE) jelenti
 _CN4IE    
a változásjelző megszakítást engedélyező CNEN1 regiszter 4. bitjét (CN4IE) jelenti
_ODCB0  
a nyitott nyelőelektródás módot engedélyező ODCB regiszter 0. bitjét (ODCB0) jelenti
_PCFG2   a bemenetek analóg funkcióját letiltó AD1CPCFGL regiszter 2. bitjét (PCFG2) jelenti

A normál (tehát ellenütemű) digitális kimeneti üzemmód beállítása valamivel összetetteb feladat, mert le kell tiltanuk a belső felhúzást, a nyitott nyelőelektródás módot, és a portláb analóg funkcióját, s nullára kell állítani a porthoz tartozó TRIS regiszter megfelelő bitjét:
static inline void CONFIG_RB0_AS_DIG_OUTPUT() {
  DISABLE_RB0_PULLUP();
  DISABLE_RB0_OPENDRAIN();
  _TRISB0 = 0;
  _PCFG2 = 1;
}

Ehhez nagyon hasonló, csupán egyetlen szóban (DISABLE helyett ENABLE) különbözik a nyitott nyelőelektródás digitális kimenet konfigurálása:
static inline void CONFIG_RB0_AS_DIG_OD_OUTPUT() {
  DISABLE_RB0_PULLUP();
  ENABLE_RB0_OPENDRAIN();
  _TRISB0 = 0;
  _PCFG2 = 1;
}

Fentiektől nem sokban különbözik a digitális bemenet beállítása sem. Alapértelmezetten letiltjuk a belső felhúzást és a portláb analóg funkcióját, s '1'-be kell állítani a porthoz tartozó TRIS regiszter megfelelő bitjét:
static inline void CONFIG_RB0_AS_DIG_INPUT() {
  DISABLE_RB0_PULLUP();
  _TRISB0 = 1;
  _PCFG2 = 1;
}
Az analóg bemenetek konfigurálásánál két dolgot kell megjegyeznünk:
  1. Nem az RB0 elnevezésre hivatkozunk, hanem az analóg bemenet sorszámára, azaz AN2-re.
  2. Az analóg funkció beállítása úgy történik, hogy digitális bemenetre állítjuk be a megfelelő portlábat, majd töröljük az AD1PCFGL regiszterben a hozzá tartozó bitet (jelen esetben PCFG2).
static inline void CONFIG_AN2_AS_ANALOG() {
  CONFIG_RB0_AS_DIG_INPUT();
  _PCFG2 = 0;
}

Mintaprogram: RGB LED vezérlése

Az eddigi szárnypróbálgatások után most egy olyan programot mutatunk be, ami egy RGB LED-et vezérel: bizonyos időközönként más-más színre váltva. Ehhez a kísérleti áramkörünket ki kell egészíteni egy RGB LED-del és három áramkorlátozó ellenállással.  A program időzítését úgy szervezzük, hogy körülbelül 2 másodkercenként kerüljön sor a színváltásra, s minden színváltáskor kb. 50 ms ideig felvillantjuk az életjelző LED-et. A későbbi programjainknál is hasznos lehet, ha egy "életjel"-et vagy állapotjezést mutató funkciót is beépítünk, ami jelzi, hogy fut a program, vagy egy adott állapotban van. Ez a program nem szerepel a Tanköny eredeti mintapéldái között, ezt a PIC-kwik projekt keretében dolgoztuk ki.

Az RGB LED bekötése

Az RGB LED,ahogy a neve is jelzi, egy tokban három, különböző színű LED-et tartalmaz, méghozzá a három alapszínnek megfelelő vöröset (R = Red), kéket (B = Blue) és zöldet (G = Green). Ebből a három alapszínből elvileg bármilyen színárnyalat kikeverhető a három LED megfelelő arányú fényintenzitásának beállításával. A keveréshez azonban szükségünk lesz valami fényeloszlatóra. Legegyszerűbb valami áttetsző fehér műanyagot használni (gyógyszeres flakon, tejfölöspohár, vagy fogkrémes tubus kupakja). Az átlátszó tok miatt ugyanis bántóan külön látszik a három szín.

A mostani példában természetesen nem vállalkozunk a fényintenzitás fokozatmentes vezérlésére (majd egy későbbi fejezetben, az impulzusszélesség-modulációnál foglalkozunk vele), csak a digitális kimenetekhez illő ki/be kapcsolgatással váltogatjuk a színeket. A három láb állaoptát kapcsolgatva így összesen 23 = 8 kombinációt tudunk beállítani (ebből egy a kikapcsolt állapot, amikor egyik LED sem világít).

Kísérleteinkhez a képen is látható Cree gyártmányú, 5 mm-es tokozású LC503NPP1-20H-A3 típust használtuk. A bekötésnél arra kell ügyelni, hogy a leghosszabb láb a közös katód. A másik középső kivezetés a kék színű LED anódja, mellette a zöld LED anód kivezetése található, a másik szélső kivezetés pedig a vörös LED anódja.

9. ábra: A LC503NPP1-20H-A3 típusú RGB LED bekötése

Az RGB LED adatlapja a gyártó honlapjáról letölthető. Az adatlap egy helyi másolata itt található.

Az adatlap szerint a diódák maximális nyitóirányú árama 25 mA (B és G), illetve 50 mA (R) lehet, aminek kihasználásához ekkora áram leadására képes meghajtóáramkört kellene használni (pl. tranzisztor, vagy FET-es kapcsoló). Erre most nem vállalkozunk, ezért a PIC24 mikrovezérlő kimenetéhez igazodva úgy korlátozzuk a kimenő áramot, hogy a maximálisan megengedett 4 mA kimeneti áramot ne lépjük túl. Az általunk használt 1 kOhm-os áramkorlátozó ellenállással 2-2,5 mA körüli árammal számolhatunk (a LED-ek nyitófeszültségének függvényében). A LED-ek így erősen "éheztetett" üzemmódban működnek, de a kipróbáláshoz ilyen kis áramoknál is elegendőnek bizonyult a fénye. 

Hardver követelmények

Az RGB LED-del kiegészített kísérleti áramkör az alábbi ábrán látható. A közös katódot a földpontra kell kötni, az R, B, G anódkivezetéseket pedig egy-egy áramkorlátozó ellenálláson keresztül az RB12, RB13, RB14 kimenetekre kötöttük. Természetesen köthettük volna bármelyik más szabad kimenetre is. Itt azért pont ezeket a lábakat választottuk, mert a Microstick Plus és a 16-bit 28-pin Starter Board már tartalmaznak LED-eket a B port legfelső 4 bitjén, tehát ezeken a kártyákon akár az RGB LED beszerzése nélkül is kipróbálhatjuk a programot. Sőt, a Microstick Plus kártya esetén a háromból két színt a kártyán levő LED-ek helyesen mutatnak, mivel a kártyán RB14 és RB15-re kék LED-ek, RB12-re és RB13-ra pedig vörös LED-ek vannak kötve).

Az "életjel" funkciót ellátó LED a korábbiakhoz hasonló módon RB15-re csatlakozik, s mivel egy külső felhúzó ellenállással is rendelkezik, a mikrovezérlő RESET állapotában folyamatos fénnyel világít. A mikrovezérlő RB15 kimenetét nyitott nyelőelektródás üzemmódjába kapcsolva a LED lesöntölésével érhetjük el a villogást.

10. ábra: A kísérleti áramkör kiegészítése az RGB LED-del

A program megtervezése

A főprogram - a kezdeti beállítások után - végtelen ciklusban fut. A ciklus törzsében kb. 50 ms ideig felvillantjuk az éltjelző LED-et, s aktualizáljuk az RGB LED állapotát. Az RGB színkomponenseket vezérlő kimeneteket egyenként definiáltuk, s bitenként állítjuk be, így a program szükség esetén más bekötéshez is egyszerűen módosítható. Az aktuális szín kódját az u8_color nevű változóban tároljuk. Az 50 ms késletetés leteltével kikapcsoljuk az életjelző LED-et, s további 1950 ms-ot várunk, majd léptetjük a színkódot (az u8_color nevű változó magasabb helyiértékű bitjeivel és túlcsordulásaival nem törődünk, mindig csak az alsó három bitet használjuk fel). A program kódja az alábbi listán látható:

3. lista: RGB LED vezérlése (rgb_led.c)
#include "pic24_all.h"

//-- Az R G B kimenetek és konfigurálásuk definiálása
//-- Ezeknek a makróknak az átírásával adaptálhatja a programot
//-- olyan hardverre, ahol az RGB LED más portlábakra van bekötve
#define LED_RED    _LATB12
#define CONFIG_LED_RED() CONFIG_RB12_AS_DIG_OUTPUT()
#define LED_GREEN  _LATB13
#define CONFIG_LED_GREEN() CONFIG_RB13_AS_DIG_OUTPUT()
#define LED_BLUE   _LATB14
#define CONFIG_LED_BLUE() CONFIG_RB14_AS_DIG_OUTPUT()

uint8_t u8_color=0;          //RBG színkombináció

int main(void) { 
    configClock();           //az oszcillátor konfigurálása
    CONFIG_HB_LED();         //Az "életjelző" LED  konfigurálása
    CONFIG_LED_RED();        //A vörös LED  konfigurálása
    CONFIG_LED_GREEN();      //A zöld LED  konfigurálása
    CONFIG_LED_BLUE();       //A kék LED  konfigurálása
   
  while( 1) {
    HB_LED = 1;              //Az életjelző LED villantása
    LED_RED = u8_color & 1;        //LED_RED állapotának aktualizálása
    LED_GREEN = (u8_color>>1) & 1; //LED_GREEN állapotának aktualizálása
    LED_BLUE = (u8_color>>2) & 1;  //LED_BLUE állapotának aktualizálása
    DELAY_MS(50);            //villantás vége
    HB_LED = 0;       
    DELAY_MS(1950)           //Várakozás ~ 2 s
    u8_color++;              //Színkód léptetése
  }
}

A program elején elhelyezett makrók definiálásával egy hardver absztrakciós réteget hoztunk létre. Ez azt jelenti, hogy a program későbbi részében már nem kell róla tudni, hogy kék LED az RB14 vagy az RB12 lábra csatlakozik. Ha például megváltoztatjuk a kapcsolást, a programnak csak ezt a részét kell módosítani.

A főprogram első részében a PIC24 támogató programkönyvtár configClock() függvényének meghívásával beállítjuk az alapértelmezett órajelet, majd konfiguráljuk az életjelző LED-et és az RGB LED-et vezérlő kimeneteket.


A végtelen ciklusban a következő tevékenységek zajlanak:

Nyomógombbal vezérelt bemenet kezelése

A nyomógombbal történő bemenetvezérlés tipikus esetét mutatja be az alábbi ábra. Amikor a nyomógomb nincs benyomva, akkor a bemenetet a 10 kOhm-os ellenállás magas szintre húzza fel. A nyomógomb benyomásakor a bemenetet alacsony szintre húzza.  Helytelen volna a nyomógomb felhúzás nélküli bekötése, mert akkor a kapcsoló nyitott állapotában a bemenet "lebeg", s a környezeti elektronikus zajok függvényében ugrál a bemeneti szint '0' és '1' között. A nyomógombhoz tehát vagy külső, vagy belső (a CN bemeneteknél említett gyenge felhúzás bekapcsolásával) felhúzást kell használni. 

11. ábra: A bemeneteket vezérlő nyomógomb bekötése

Megjegyzés: A Microstick Plus kártya esetén a kártyán található nyomógomb az RA2 bemenetre van kötve, de a külső felhúzás csak akkor működik, ha a kártyának tápfeszültséget adó RA3 kivezetést kimenetnek állítjuk, és magas szintre húzzuk!

Pergésmentesítés

A kapcsolók vagy nyomógombok használatánál problémát okozhat az érintkezők mechanikus "pergése", ami miatt a ki- és bekapcsolásnál úgy érzékeljük, mintha gyors egymásutánban többször ki/bekapcsolódna a kapcsoló. Különösen akkor kellemetlen az a jelenség, ha pl. egy nyomógomb benyomásainak számától függően más-más tevékenységet kell aktivizálni.

Az ábra felső részén azt az esetet mutatja, amikor a kapcsoló egyszerűen egy inverter bemenetére kapcsolódik, s egy felhúzó ellenállás biztosítja a stabil magas szintet a kapcsoló kikapcsolt állapotában. A kimenő feszültség idődiagramján láthatjuk, hogy ki-és bekapcsoláskor zavaró impulzusok keletkeznek.

A hardveres pergésmentesítés bonyolultabb eseteivel (amikor pl.egy kétállású kapcsoló egy R-S billenőkört vezérel) itt nem foglalkozunk. A mikrovezérlő Schmitt-triggeres bemenő fokozata azonban egy egyszerűbb megoldást is kínál: ha egy R-C kör segítségével megintegráljuk a zavaró impulzusokkal terhelt jelet, akkor az alsó ábrán látható módon "kisimul" a jel, a Schmitt-trigger pedig biztosítja a határozott billenést. Az ábrán látható két ellenállás közül a felső 10 kOhm-os nagyságrendű lehet, s a kondenzátorral együtt a kikapcsoláskor működő integrálás időállandóját határozza meg. A másik ellenállás, amely a bemenet biztonságos lehúzásához a felsőnél legalább egy nagyságrenddel kisebb értékű kell, hogy legyen (1 kOhm, vagy kevesebb) a bekapcsolási időállandót szabja meg. A kondenzátor értéke pl. 470 nF lehet.

A szoftveres pergésmentesítés a bemenő jel időbeli lefutásának elemzésén alapul. A szakirodalomban elterjedt megoldások vagy egy 10-30 ms-os késleltetést iktatnak be az első állapotváltozást követően (így egyszerűen kihagyják a pergéses szakaszt), vagy a jelszint stabilizálódását ellenőrzik (hogy pl. legalább egy minimálisan megszabott időtartam eltelt-e jelszintváltozás nélkül). Ez utóbbi esetben többnyire a mikrovezérlő beépített időzítőit érdemes használni az időtartam ellenőrzésére.

Mintaprogram: RGB LED nyomógombos vezérlése 

Egészítsük ki az előző példa áramkörét egy nyomógombbal is, amelyet az RB7 bemenetre kötünk! Az alábbi rajzon a pergésmentesített változatot mutatjuk be, de a kondenzátort és a 470 Ohmos ellenállást most elhagyhatjuk, mert a programot úgy szervezzük, hogy alkalmas legyen a szoftveres pergésmentesítésre.


13. ábra: A kísérleti áramkör kiegészítése RGB LED-del és az SW1 nyomógombbal

Feladat: Írjuk át a korábbi, RGB LED-et vezérlő programunkat úgy, hogy a színváltások csak akkor történjenek, ha lenyomjuk és felengedjük az RB7 bemenetre kötött SW1 nyomógombot! Mellékfeltételként ügyeljünk arra, hogy az RB15 kimenetre kötött "életjelző" LED folyamatosan villogjon, az SW1 nyomógomb állapotától függetlenül!

A program megtervezése

A program jelentős része megegyezik a korábbi RGB LED vezérlő programmal, ezért csak az eltéréseket ismertetjük részletesen. A főprogram - a kezdeti beállítások után - most is végtelen ciklusban fut. A nyomógomb megfelelő válaszidejű kiszolgálásához igazodva a futó időt most rövidebb, 0,01 másodperces szeletekre tagoljuk, ez lesz az alapegység. Minden tevékenységsorozat egy-egy ilyen századmásodperces időszelet leteltével kezdődik. Az életjelző LED vezérlését rábízzuk a támogatói programkönyvtár doHeartbeat() nevű függvényére. Ha rendszeresen hívogatjuk, akkor az a beépített számlálójával gondoskodik róla, hogy a HB_LED kb. 2 Hz-es frekvenciával villogjon.

A nyomógomb kiszolgálásának megtervezése némi megfontolást igényel. A legrosszabb, amit elkövethetünk, az, ha ilyesmivel próbálkozunk:

if (_RB7==0) {
     u8_color++;
     LED_RED = u8_color&1;               //Az RGB LED beállítása
     LED_GREEN = (u8_color>>1) & 1;
     LED_BLUE = (u8_color>>2) & 1;

}

Mi a probléma? Az, hogy a fenti kóddal amíg a nyomógombot lenyomva tartjuk, minden időszeletben (tehát századmásodpercenként!) léptetjük a színt, holott mi gombnyomásonként csak egyszer akartuk.

A helyes megoldás az, ha a gomb lenyomásának érzékelésekor csak beállítunk egy jelzőt, vagy számláljuk az eltelt időszeleteket, s csak akkor léptetjük a színkódot, amikor a gombot újra felengedett állapotban találjuk, s be van billentve a jelző, vagy a lenyomott állapotban leszámlált időszeletek száma meghalad egy küszöbértéket. Ez az utóbbi eset a szoftveres pergésmentesítésnek felel meg, s az alábbi programban ezt a pergésmentesítő megoldást mutatjuk be:
  1. Minden századmásodpercben rátekintünk az RB7 bemenetre. Ha alacsony szinten van, azaz a gombunk benyomott állapotban van, akkor megnöveljük az u16_period nevű számlálót.
  2. Ha pedig magas szinten találjuk az RB7 bemenetet, akkor megnézzük, hogy a u16_period számlálónk értéke nagyobb-e egy megadott küszöbértéknél (például 10-nél). Ha nagyobb, akkor ez azt jelenti, hogy előzőleg legalább 10 időszeleten keresztül le volt nyomva az RB7-re csatlakozó nyomógomb. Ekkor léptetjük a színkódot , majd kiírjuk az RGB LED-et vezérlő kimenetekre  az R-B-G színkódot.
  3. Amikor az RB7 bemenetet magas szinten találjuk, akkor a u16_period számlálót mindenképpen nullázzuk, függetlenül attól, hogy hosszabb vagy rövidebb ideig volt előtte benyomva a gomb. Így a következő benyomáskor megint nulláról kezdi a számlálást.

4. lista: RGB LED vezérlése nyomógombbal (button.c)

#include <pic24_all.h>

//-- Az R G B kimenetek és konfigurálásuk definiálása
//-- Ezeknek a makróknak az átírásával adaptálhatja a programot
//-- olyan hardverre, ahol az RGB LED más portlábakra van bekötve
#define LED_RED    _LATB12
#define CONFIG_LED_RED() CONFIG_RB12_AS_DIG_OUTPUT()
#define LED_GREEN  _LATB13
#define CONFIG_LED_GREEN() CONFIG_RB13_AS_DIG_OUTPUT()
#define LED_BLUE   _LATB14
#define CONFIG_LED_BLUE() CONFIG_RB14_AS_DIG_OUTPUT()

//-- 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() {
  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
}
#elif (HARDWARE_PLATFORM == STARTER_BOARD_28P)
//-- 16-bit 28-pin Starter Board   ---------------------------
#define SW1        _RB5        //SW1 nyomógomb RB5-re csatlakozik
#define CONFIG_SW1() CONFIG_RB5_AS_DIG_INPUT();
#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
}
#endif
//-- HARDWARE_PLATFORM függő rész vége ------------------------

#define SW1_PRESSED()  SW1==0  //lenyomott állapot feltétele
#define SW1_RELEASED() SW1==1  //felengedett állapot feltétele

uint8_t  u8_color=0;           //RBG színkombináció
uint16_t u16_period=0;         //lenyomáshossz-számláló

int main(void) { 
    configClock();             //Az oszcillátor konfigurálása
    configHeartbeat();         //Életjelző LED konfigurálása
    CONFIG_SW1();              //Az SW1 nyomógomb konfigurálása
    CONFIG_LED_RED();          //A vörös LED  konfigurálása
    CONFIG_LED_GREEN();        //A zöld LED  konfigurálása
    CONFIG_LED_BLUE();         //A kék LED  konfigurálása
   
  while(1) {
    if(SW1_PRESSED()) {        //Gomb benyomva?
      u16_period++;            //időszelet számlálás
    }
    else {
      if(u16_period > 10) {
        u8_color++;            //Színkód léptetése
        LED_RED = u8_color&1;  //Az RGB LED beállítása
        LED_GREEN = (u8_color>>1) & 1;
        LED_BLUE = (u8_color>>2) & 1;
      }
      u16_period=0;            //új lenyomás várása
    }
    DELAY_MS(10);              //10 ms várakozás
    doHeartbeat();             //életjelzés, hogy fut a program
  }
}

Az órajel és a portok konfigurálása hasonlít az előző programban látottakhoz, különbség csak abban van, hogy a HB_LED mellett az életjelző funkciót ellátó függvény működéséhez szükséges számlálót is elő kell készíteni, ezért most a configHeartbeat() inicializáló függvényt kell meghívnuk. Új elem, hogy az SW1 nyomógombot fogadó bemenetet is inicializálni kell a CONFIG_SW1() makróval.

A támogatott hardver platformok esetén logikusnak látszik, hogy az esetleg már meglevő nyomógombot használjuk SW1 gyanánt. A Microstick Plus kártya esetén ez az RA2 bemenetre van kötve, de a külső felhúzása csak akkor működik, ha az RA3 kivezetés kimenetre van állítva és magas szintre húztuk. A 28-pin Starter Board esetén pedig RB5-re van kötve az SW1 felhasználói nyomógomb. A programban feltételes fordítási direktívák (#if ... #elif ... #else ... #endif) gondoskodnak róla, hogy csak az adott hardverhez tartozó definíciók kerüljenek bele a programba. Az elégaztatás a pic24_libconfig.h állományban definiált HARDWARE_PLATFORM nevű makró értékének vizsgálatával történik. Az #else ágba az alapértelmezett (DEFAULT_PLATFORM) hardverhez való beállítások szerepelnek. Ebben az RB7 bemenethez tartozó belső felhúzást is bekapcsoljuk, így a nyomógomb felhúzóellenállása is megspórolható...

Megjegyzések:

LED kapcsolgatása és státuszgép programozása

A mikrovezérlőkhöz köthető legegyszerűbb  eszközök a nyomógomb, a kapcsoló, vagy egy LED. A következő mintaprogramok kipróbálásához az alábbi ábrán bemutatott módon egészítsük ki a Kísérleti áramkör című fejezetben bemutatott alapkapcsolásunkat!

Amint látjuk, a korábbiakkal megegyezően most is az RB15-re csatlakozik az "életjelző" LED. Ennek a kimenetnek feltétlenül nyitott nyelőelektródás üzemmódban kell működnie, mert a kimenet túlterhelődne, ha a közvetlenül a LED-re csatlakozó lábon bekapcsolnánk a felhúzó p-csatornás FET-et! (A támogatott hardver platformok között azonban számos kártyánál másképp van kialakítva az életjelző LED meghajtása: a LED áramkorlátozó ellenálláson keresztül csatlakozik a mikrovezérlőhöz. Ebben az esetben természetesen nincs szükség a nyitott nyelőelektródás módra.)

Egy másik LED-et (LED1) most az RB14 kimentre kötünk, áramkorlátozó ellenállással. Ez a LED csak az RB14 kimenetről egy kap felhúzást, tehát ez a kimenet nem lehet nyitott nyelőelektródás módban.

Az RB7 bemenetre egy nyomógomb csatlakozik, ezzel szeretnénk kapcsolgatni LED1-et. Itt nem alkalmaztunk hardveres pergésmentesítést, azt majd szoftveresen fogjuk megvalósítani. Sőt, a külső felhúzást is elhagytuk, ezért feltétlenül be kell kapcsolnunk a belső felhúzást! Ne feledjük el, hogy a belső felhúzások használatakor, vagy a Microstick Plus kártya járulékos áramköreinek bekapcsolásakor meg kell várnunk, amíg a a jelszintek beállnak!

14. ábra: A kísérleti áramkör kiegészítése  LED1-gyel, az SW1 nyomógombbal és az SW2 kapcsolóval

Az RB6 kivezetésre pedig egy kapcsolót kötöttünk, amelyet az első feladatnál nem használunk, csak a második feladatnál lesz rá szükség. Itt is elhagytuk a külső felhúzást, tehát ezen a lábon is be kell kapcsolni a belső felhúzást!

Az alábbi mintapéldáknál használni fogjuk a mikrovezérlő és a PC közötti kommunikációt, ezért csatlakoztanunk kell az USB-TTL átalakítót is.  A rajzon bemutatjuk azt is, hogy ha nem rendelkezünk USB-TTL átlakítóval, akkor végszükség esetén a PICkit2 készüléket is használhatjuk UART kommunikációs eszközként. Ügyeljünk rá, hogy a földpont (VSS) és a tápfeszültség (VDD) is be legyen kötve! Ilyen esetben PICkit2 saját kezelőprogramjának UART Tool módját kell használni, s ügyeljünk rá, hogy a kommunikáció sebessége legfeljebb 38400 bit/s lehet, a sorvége konvenció pedig CR/LF legyen. Ehhez az MPLAB Project/Build options menüpontban a C30 fordítónál a DEFAULT_BAUDRATE=38400, valamint a SERIAL_EOL_DEFAULT=SERIAL_EOL_CR_LF makrókat kell definiálnunk.

1. feladat: LED ki-/bekapcsolása nyomógombbal

Az első feladatban a fenti rajzon látható LED1 nyomógombot kell ki- és bekapcsolni a nyomógomb segítségével. Ez gyakorlatilag azt jelenti, hogy a nyomógomb minden lenyomása és felengedés után a LED1 állapotát ellenkezőjére kell váltani.

Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
A feladat egy lehetséges megoldását a tankönyv ledtoggle_nofsm.c mintaprogramja mutatja be, amelyet a PIC-kwik projekt igényeihez igazítva némileg átalakítottunk. Ebben - feltételes fordítási direktívák segítségével - a fentebb felsorolt hardver különbségek már figyelembe vannak véve, tehát a HARDVER_PLATFORM makró értékének helyes beállítása esetén a program a PIC-kwik, PIC-ador, mini-Bully, Microstick, Microstick Plus és 28-pin Starter Board kártyákon változtatás nélkül használható.

Megjegyzés: Belső felhúzások használatakor, vagy a Microstick Plus kártya járulékos áramköreinek bekapcsolásakor meg kell várnunk, amíg a a jelszintek beállnak! A CONFIG_SW1() inline függvényben akalmazott késleltetésekkel kapcsolatos megfontolásokról az előző program megjegyzésinél olvashatunk.

5. lista: LED ki-be kapcsolgatása nyomógombbal (ledtoggle_nofsm.c)
#include "pic24_all.h"
///--- LED1  konfigurálása -----------------------------------
#define LED1  _LATB14          //LED1 RB14-re csatlakozik
#define CONFIG_LED1() CONFIG_RB14_AS_DIG_OUTPUT()

//-- 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() {
  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
}
#elif (HARDWARE_PLATFORM == STARTER_BOARD_28P)
//-- 16-bit 28-pin Starter Board   ---------------------------
#define SW1        _RB5        //SW1 nyomógomb RB5-re csatlakozik
#define CONFIG_SW1() CONFIG_RB5_AS_DIG_INPUT();
#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
}
#endif
//-- HARDWARE_PLATFORM függő rész vége ------------------------
 
#define SW1_PRESSED()   SW1==0 //a lenyomott állapot feltétele
#define SW1_RELEASED()  SW1==1 //a felengedett állapot feltétele

int main (void) {
  configBasic(HELLO_MSG);      // "életjel" és ART beállítása, üzenet kiírás
  CONFIG_SW1();                // a nyomógomb bemenet konfigurálása
  CONFIG_LED1();               // a LED kimenet konfigurálása
  LED1 = 0;                    // LED1 kezdetben legyen kikapcsolva
  while (1) {
    // lenyomásra várunk
    while (!SW1_PRESSED()) doHeartbeat();
    DELAY_MS(DEBOUNCE_DLY);    //pergésmentesítés
    // felengedésre várunk
    while (!SW1_RELEASED()) doHeartbeat();
    DELAY_MS(DEBOUNCE_DLY);    // pergésmentesítő késleltetés
    LED1 = !LED1;              // LED1 állpotának átbillentése
  }
}

A program első részében LED1 és SW1, valamint a beállításukat végző konfigrációs eljárások definiálása lehetővé teszi, hogy a program belsejében már ne kelljen tudni a hardver olyan részleteiről, hogy mi melyik ki/bementre van kötve. Hasonlóan a program belsejének hardverfüggetlenítését szolgálja az SW1 lenyomott és felengedett állapotához tartozó feltételek szimbolikus nevekhez történő kapcsolása is. A programnak ez a része nagymértékben hasonlít az előző mintapéldákra.

A főprogram végtelen ciklusban fut, melynek törzse a feladat megfogalmazása szerint így írható:
  1. Gomblenyomásra várunk
  2. Késleltetés (pergésmentesítés céljából)
  3. Gombfelengedésre várunk
  4. LED1 bekapcsolása
  5. Késleltetés (pergésmentesítés céljából)
  6. Gomblenyomásra várunk
  7. Késleltetés (pergésmentesítés céljából)
  8. Gombfelengedésre várunk
  9. Késleltetés (pergésmentesítés céljából)
  10. LED1 kikapcsolása
Vegyük észre, hogyha lemondunk a LED állapotainak nyilvántartásáról, akkor a fenti várakozó és késleltető állapotok összevonhatók:
  1. Gomblenyomásra várunk
  2. Késleltetés (pergésmentesítés céljából)
  3. Gombfelengedésre várunk
  4. Késleltetés (pergésmentesítés céljából)
  5. LED1 állapotának átbillentése
Ezt utóbbi tevékenység-sorozatot valósítja meg a fenti program is, kiegészítve azzal, hogy a várakozások során az "életjel" LED villogtatásáról is gondoskodik. A pergésmentesítés most úgy történik, hogy a lenyomás (vagy felengedés) észlelésekor kivárunk annyi időt (kb. 15 ms, de a DEBOUNCE_DLY makró konkrét értéke a pic24_libconfig.h állományban megváltoztatható), amíg a nyomógomb mechanikai pergése biztosan lecseng.

Megjegyzés: Ha zavar bennünket, hogy LED1 állapotának átkapcsolása nem a gomb lenyomásakor, hanem a felengedésekor történik, akkor helyezzük át a LED1 = ~LED1; utasítást  az első késleltetés elé!

Egy másik megközelítés: véges állapotgép módszer

A fenti programban tulajdonképpen két domináns állapotot látunk: a lenyomásra és a felengedésre történő várakozást. A LED kapcsolgatása csak egy járulékos művelet amit az utóbbi állapotból az elsőbe történő átmenetkor kell elvégezni. Ha tehát a feladatot a véges állapotgép modell alapján akarjuk megoldani, akkor erre a két állapotra kell koncentrálnunk amelyeket táblázatos formában így foglalhatunk össze:
Pillanatnyi állapot Következő állapot Átmenet feltétele Tevékenység
Lenyomásra vár Felengedésre vár SW == 0 -
Felengedésre vár Lenyomásra vár SW == 1 LED1 = !LED1
Az állapotok és az állapot-átmenetek egy véges állapotgép formájában is felrajzolhatók:

15. ábra: A feladat állapotdiagramja

A tankönyv ledtoggle.c mintaprogramjának (melyet a PIC-kwik projekthez igazodva egy kicsit átalakítottunk) itt csak néhány részletét mutatjuk be. A hardverfüggő részben LED1 és SW1 definiálása ugyanúgy történik, mint az előző programnál. Az első eltérés az, hogy program elején a deklarációs részben definiálunk egy STATE állapotjelző változótípust, s az  e_lastState és e_mystate lesznek e típus példányai. Az e_mystate változó tárolja a pillanatnyi állapotot, e_lastState pedig az előzőt (e két változó összehasonlításából lehet megtudni, hogy történt-e állapotváltozás a legutóbbi kiíratás óta). A névben az e_ előtag az "enumerated" típusra utal.
typedef enum  {                   //A lehetséges állapotok felsorolása
  STATE_RESET = 0,
  STATE_WAIT_FOR_PRESS,
  STATE_WAIT_FOR_RELEASE
} STATE;

STATE e_lastState = STATE_RESET;  //Változó a legutóbbi állapot tárolására

A főprogramban futó végtelen ciklust törzse képezi a véges állapotgépet, amelynek működése a fenti táblázat, illetve ábra alapján remélhetőleg könnyen követhető. Azt, hogy a pillanatnyi állapotban mit kell csinálni, a switch(e_mystate) utasítás dönti el. A továbblépés feltételeit pedig az egyes "case" ágakban található if utasítások vizsgálják.

6. lista: Az állapotgépes megközelítést használó főprogram  (ledtoggle.c  részlet)
int main (void) {
  STATE e_mystate;
  configBasic(HELLO_MSG);         // "életjel" és UART beállítása, üzenet kiírása
  CONFIG_SW1();                   // SW1 konfigurálása
  CONFIG_LED1();                  //LED1 konfigurálása
  DELAY_US(100);                  //a felhúzások beállására várunk
  e_mystate = STATE_WAIT_FOR_PRESS; //induláskor lenyomásra várunk

  while (1) {
    printNewState(e_mystate);     //nyomkövető üzenet kiírása állapotváltáskor
    switch (e_mystate) {
      case STATE_WAIT_FOR_PRESS:  //Ha lenyomásra várunk
        if (SW1_PRESSED())  {
//-- A következő utasítással lenyomáskor váltjuk LED1 állapotát
          LED1 = !LED1;           //LED1 állapotának átbillentése
          e_mystate = STATE_WAIT_FOR_RELEASE;
        }
        break;
      case STATE_WAIT_FOR_RELEASE://Ha felengedésre várunk
        if (SW1_RELEASED()) {
//-- A következő utasítással felengedéskor váltjuk LED1 állapotát
//        LED1 = !LED1;           //LED1 állapotának átbillentése
          e_mystate = STATE_WAIT_FOR_PRESS;
        }
        break;
      default:                    //Alapértelmezett ág
        e_mystate = STATE_WAIT_FOR_PRESS;
    }                             //Switch vége
    DELAY_MS(DEBOUNCE_DLY);       //Pergésmentesítő késleltetés
    doHeartbeat();                //életjelző LED villogtatása
  }                               // a while (1) ciklus vége
}

Megjegyzés: A switch utasítás alapértelmezett (default) ága akkor aktivizálódik, ha általunk nem értelmezhető állapotkódot tartalmaz az e_mystate változó, vagy reset állapot (pl. STATE_RESET).
A program állapotváltásait nyomon követhetjük az UART kimenet figyelésével. A program bekapcsoláskor vagy reset utáni újrainduláskor kiír egy rövid jelentést,majd a nyomógomb megnyomásakor és felengedésekor értesít bennünket az állapot megváltozásáról. A program az 1. és 2. állapotok között cirkulál.

16. ábra: A ledtoggle.c program futtatásának képernyőképe

Egy összetettebb feladat

2. feladat: az előző áramkörikapcsolásnál maradva készítsünk olyan programot, amely az SW2 kapcsoló állásától függően elágaztatja a programot, a következő módon:
  1. LED1 kezdetben legyen kikapcsolva!
  2. Az SW1 nyomógomb megnyomását és elengedését követően LED1-et kapcsoljuk be!
  3. Az SW1 nyomógomb újabb megnyomását és elengedését követően a programot az SW2 állásától függően így folytassuk: 
    • Ha SW2 =0 (a kapcsoló zár), akkor az 1. pontnál folytassuk!
    • Ha SW2 = 1 (a kapcsoló nyitott), akkor a 4. pontnál folytassuk!
  4. Villogtassuk LED1-et (LED1 állapotának átbillentése, majd 100 ms várakozás következzen)
  5. Az SW1 nyomógomb megnyomását követően kapcsoljuk be LED1-et (stabilan világítson)!
  6. Az SW1 nyomógomb felengedését követően folytassuk a programot az 1. pontnál!
Ennél a feladatnál kénytelenek vagyunk megkülönböztetni az első és a második gombnyomásra, illetve felengedésre történő várakozást, mivel a második gombnyomást követően figyelnünk kell SW2 állapotát, és ennek függvényében különböző ágakon folytatódik a program. Így tehát a pillanatnyi állapot lehet:
Lenyomásra vár 1.
Felengedésre vár 1.
Lenyomásra vár 2.
Felengedésre vár 2.
Ezek mellett - mivel egy másik ágon is folytatódhat a program - be kell vezetnünk két további állapotot:
Lenyomásra vár 3.     <-- Ezt praktikus okokból "Villog"-nak fogjuk nevezni!
Felengedésre vár 3.

A lehetséges állapotokat, a rájuk következő állapotokat, az állapotváltás feltételeit és az adott állapotban elvégzendő tevékenységet az alábbi táblázatban foglaltuk össze: 
Pillanatnyi állapot Következő állapot Átmenet feltétele Tevékenység
Lenyomásra vár 1. Felengedésre vár1. SW1 == 0 LED1 = 0
Felengedésre vár 1. Lenyomásra vár 2. SW1 == 1
Lenyomásra vár 2. Felengedésre vár 2 SW1 == 0 LED1 = 1
1Felengedésre vár 2. Villog SW1==1 && SW2==1
Lenyomásra vár 1. SW1==1 && SW2==0
Villog Felengedésre vár 3. SW1 == 0 2LED1=!LED1
Felengedésre vár 3. Lenyomásra vár 1. SW1 == 1 LED1 = 1
1. A "Felengedésre vár 2." állapotból SW2 állásától függően elágazik a program.
2. Villogtatásnál LED1 állapotváltásai között 100 ms szünetet tartunk.

A véges állapotgép állapotváltásainak diagramja az alábbi ábrán látható:

17. ábra: A ledsw1.c program állapotváltásainak diagramja

A programot az előző feladat alapján önállóan is elkészíthetjük, de a tankönyv ledsw1.c mintaprogramja is bemutat egy kidolgozott megoldást. A tankönyvi mintaprogramot a PIC-kwik projekt keretében több vonatkozásban is módosítottuk, illetve átalakítottuk, hogy minél szélesebbkörű támogatást nyújtson a rendelekzésünkre álló hardver platformokon. A programon az alábbi módosításokat eszközöltük:
  1. A támogatott hardver platformokhoz igazodva hardverütközések miatt megváltoztattuk a lábkiosztást (SW1 RB7-re, SW2 pedig RB6-ra került az eredeti RB13 és RB12 lábakról).
  2. Az előző programokhoz hasonlóan feltételes direktívákkal egyenlítjük ki a hardver különbségeket.
  3. További hardver absztakciót vezettünk be: a LED vezérléséhez ON és OFF makrókat vezettünk be (erre azért volt szükség, mert a 28-pin Starter Board LED-jei ellentétes fázisban működnek, mint a többi kártyán).
  4. Egy további LED felhasználásával visszajelezzük az SW2 állapotát. LED2 akkor világít, ha SW2 nyitott (ekkor fog a villogtatási ágon haladni a program). Ez különösen a Microstick Plus kártya esetén hasznos segítség, ahol kapcsoló helyett a forgásérzékelőt (rotary encoder) használjuk fel.
Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
A program elég terjedelmes, ezért részletenként mutatjuk be. Az alábbi listán a hardver absztrakciós rész látható. Vegyük észre, hogy SW1 mellett itt a LED-ek bekapcsolt állapotához tartozó jelszintet is definiáljuk az ON makróval! Erre azért lesz szükség, mert a 28-pin Starter Board esetén a többi hardver platformmal ellentétes jelszintet kell kiadnunk ahhoz, hogy a LED világítson. Ha tehát azonos viselkedést várunk a programtól, ennél a kártyánál gondoskodnunk kell a kiadott jel invertálásáról.

 7. lista: Hardver absztrakciós réteg (ledsw1.c részlet)
///--- LED1  konfigurálása -----------------------------------
#define LED1  _LATB14     //LED1 RB14-re csatlakozik
#define CONFIG_LED1() CONFIG_RB14_AS_DIG_OUTPUT()
///--- LED2  konfigurálása -----------------------------------
#define LED2  _LATB12     //LED1 RB14-re csatlakozik
#define CONFIG_LED2() CONFIG_RB12_AS_DIG_OUTPUT()

//-- SW1 és ON definiálása HARDWARE_PLATFORM függő módon -----
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS)
//-- Microstick Plus kártya ----------------------------------
#define ON 1                   //Bekapcsolt LED állapot
#define SW1        _RA2        //SW1 nyomógomb RA2-re csatlakozik
inline void CONFIG_SW1() {
  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
}
#elif (HARDWARE_PLATFORM == STARTER_BOARD_28P)
//-- 16-bit 28-pin Starter Board   ---------------------------
#define ON 0                   //Bekapcsolt LED állapot
#define SW1        _RB5        //SW1 nyomógomb RB5-re csatlakozik
#define CONFIG_SW1() CONFIG_RB5_AS_DIG_INPUT();
#else
//-- Alapértelmezett hardver platform ------------------------
#define ON 1                   //Bekapcsolt LED állapot
#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
}
#endif
//-- HARDWARE_PLATFORM függő rész vége ------------------------

#define SW1_PRESSED()   SW1==0  //lenyomott állapot feltétele
#define SW1_RELEASED()  SW1==1  //felengedett állapot feltétele
#define OFF !ON

/// SW2 kapcsoló konfigurálása
#define SW2             _RB6   //SW2 kapcsoló RB6-ra csatlakozik
inline void CONFIG_SW2()  {
  CONFIG_RB6_AS_DIG_INPUT();   //RB6 legyen digitális bemenet
  ENABLE_RB6_PULLUP();         //belső felhúzás engedélyezése
}

Megjegyzések:
A korábban ismertetett meggondolások alapján definiáljuk a lehetséges állapotokat!
Szómagyarázat: STATE = állapot, WAIT FOR = vár valamire, PRESS = lenyomás,
                         RELEASE = felengedés, BLINK = villog
//--- A lehetséges állapotok felsorolása
typedef enum  {
  STATE_RESET = 0,              //Reset
  STATE_WAIT_FOR_PRESS1,        //Lenyomásra vár 1.
  STATE_WAIT_FOR_RELEASE1,      //felengedésre vár 1.
  STATE_WAIT_FOR_PRESS2,        //lenyomásra vár 2.
  STATE_WAIT_FOR_RELEASE2,      //felengedésre vár 2.
  STATE_BLINK,                  //villogás
  STATE_WAIT_FOR_RELEASE3       //felengedésre vár 3.
} STATE;

STATE e_mystate=STATE_RESET;;   //Változó az aktuális állapot tárolására
STATE e_LastState=STATE_RESET;  //Változó a legutóbbi állapot tárolására
Megjegyzés: a programban STATE típusúak a következő változók: a főprogram által használt e_mystate, továbbá a kiíratáshoz szükséges e_lastState (az előző állapot) globális változó. Természetesen STATE típusúnak kell deklarálni a kiíratást végző eljárás e_currentState (a pillanatnyi állapot) formális paraméterét is, mivel a kiírató függvényt a főprogram az e_mystate változóval fogja meghívni.

A főprogram a kezdeti beállításokat végző inicializáló résszel kezdődik, ahol a configBasic() eljárás beállítja a mikrovezérlő oszcillátorát, és a soros portot, kiír egy üdvözlő üzenetet, majd beállításra kerülnek a LED1 és LED2-höz kimenetek, valamint az SW2 kapcsolóhoz és az SW1 nyomógombhoz kapcsolódó bemenetek.

Ezután a program belép a végtelen ciklusba, amelyben egy switch() utasítást használunk a különböző kódszegmensek végrehajtására, az e_mystate változó állapotától függően. Mielőtt azonban a pillantnyi állapotnak megfelelő tevékenységbe fognánk, kijelezzük LED2 segítségével az SW2 kapcsoló állapotát, majd meghívjuk a printNewState() eljárást, amelyben nyomkövetési céllal minden állapotváltáskor kiíratjuk a pillanatnyi állapotot.

A switch() utasítás minden case blokkja megfelel egy-egy lehetséges állapotnak, törzsükban taláható az előirt tevékenység elvégzése és a továbblépés feltételének vizsgálata. A továbblépés úgy történik, hogya feltétel teljesülése esetén megváltoztatjuk az e_mystate változót, így a switch() utasítás következő meghívásakor már az új állapotnak megfelelő case blokk aktivizálódik.  Nagyon fontos, hogy minden case ágat zárjuk le egy break utasítással, máskülönben a program a feltételektől függetlenül "rácsorog" a következő case ágra!

Mivel minden állapotváltozást egy pergésre hajlamos nyomógomb lenyomása vagy felengedése vált ki, így most elfogadható az a megoldás, hogy a switch() utasítás minden végrahajtása után beiktatunk egy várakozást (a DEBOUNCE_DLY konstans a pic24_delay.h állományban van definiálva, s 15 ms késleltetést ír elő). Más körülmények között azonban zavaró lehet, hogy a program futását ilyen hosszú időre megakasztjuk. Bizonyos perifériák (USB, soros port) esetében például hibát okozhat, ha ennyi ideig nem szolgáljuk ki az időközben esetleg beérkező kérelmüket. Jegyezzük tehát meg, hogy a kritikus időzítésű esmények vagy folyamatok kezelésére mindig ügyelnünk kell!

8. lista: A főprogram listája (ledsw1.c részlet)
int main (void) {
  configBasic(HELLO_MSG);   // beállítja a villogást, és az UART portot,
  CONFIG_SW1();             //a nyomógomb konfigurálása
  CONFIG_SW2();             //a váltókapcsoló konfigurálása
  CONFIG_LED1();            //LED1 konfigurálása
  CONFIG_LED2();            //LED2 konfigurálása

  /***** A STÁTUSZGÉP INDÍTÁSA *****/
  e_mystate = STATE_WAIT_FOR_PRESS1; //kezdőállapot megadása
  while (1) {
    printNewState(e_mystate);   //állapotváltáskor nyomkövető üzenet kiírása
    LED2 = SW2 ^ OFF;       //LED2 = ON, ha SW2 nyitott)
    switch (e_mystate) {
      case STATE_WAIT_FOR_PRESS1:
        LED1 = OFF;         //kikapcsolja a LED-et
        if (SW1_PRESSED()) e_mystate = STATE_WAIT_FOR_RELEASE1;
        break;
      case STATE_WAIT_FOR_RELEASE1:
        if (SW1_RELEASED()) e_mystate = STATE_WAIT_FOR_PRESS2;
        break;
      case STATE_WAIT_FOR_PRESS2:
        LED1 = ON;          //bekapcsolja a LED-et
        if (SW1_PRESSED()) e_mystate = STATE_WAIT_FOR_RELEASE2;
        break;
      case STATE_WAIT_FOR_RELEASE2:
        if (SW1_RELEASED()) {
          //SW2 állapotától függ, hogy merre folytassuk
          if(SW2) e_mystate = STATE_BLINK;        //LED1 villogtatás, ha SW2 nyitva
          else e_mystate = STATE_WAIT_FOR_PRESS1; //LED1 kikapcsolás, ha SW2 zárt
        }
        break;
      case STATE_BLINK:
        LED1=!LED1;            //villogtatja a LED-et
        DELAY_MS(100);         //késleltetés kell, hogy lássuk
        if (SW1_PRESSED()) e_mystate = STATE_WAIT_FOR_RELEASE3;
        break;
      case STATE_WAIT_FOR_RELEASE3:
        LED1 = ON;             //LED1 állapotát befagyasztjuk '1'-re
        if (SW1_RELEASED()) e_mystate = STATE_WAIT_FOR_PRESS1;
        break;
      default:
        e_mystate = STATE_WAIT_FOR_PRESS1;
    }                          //switch(e_mystate)  vége
    DELAY_MS(DEBOUNCE_DLY);    //pergésmentesítő késleltetés
    doHeartbeat();             //életjelzés, hogy fut a program
  }                            //a while (1) ciklustörzs vége
}

Külön magyarázatot igényel az alábbi sor, amellyel LED2 állapotát kívánjuk beállítani, SW2 állapotától függően.
    LED2 = SW2 ^ OFF;       //LED2 = ON, ha SW2 nyitott)A hardver különbségek miatt azonban a LED-ek ON/OFF állapotának definícióját is figyelembe kell venni. Végeredményben az alábbi igzságtáblát írhatjuk fel:
SW2 ON LED2
1 1 1
0 1 0
1 0 0
0 0 1
Ebben pedig a LED2 = SW2 XOR (!ON) igazságtábláját ismerhetjük fel, amit így is írhatunk:

              LED2  =  SW2 XOR  OFF


A program állapotváltásait nyomon követhetjük az UART kimenet figyelésével. Az alábbi ábra azt az esetet mutatja be, amikor a PICkit2 készülék UART Tool üzemmódjában fogadjuk a soros jelet. Az ábrába utólag berajzoltuk SW2 állapotát. Láthatjuk, hogy SW2 zárt állapotában a program az 1. és 2. állapotok között cirkulál, az első feladatnál bemutatott státuszgépes programhoz hasonlóan. SW2 nyitásakor azonban a 2. felengedést követően a program egy újabb állapotba kerül, ahonnan csak egy újabb gombnyomás és felengedés után tér vissza alapállapotba.

18. ábra: A ledsw1.c program futtatásának képernyőképe