Számlálók, időzítők

A fejezet tartalma:

A számlálók/időzítők felépítése

Az I/O portok után talán az időzítők (Timer-ek) tekinthetők a PIC mikrovezérlők legfontosabb perifériáinak. Az időzítők valójában számlálók, amelyek bemenetére a mikrovezérlő oszcillátorának a leosztott frekvenciájú jele is rákapcsolható, s akkor az ismert TTMR periódus idejű jelekből n darab leszámlálása egy Tn időtartamot határoz meg:
Tn = n * TTMR
   
PIC24HJ128GP502 mikrovezérlő a speciális célú időzítőkön (watchdog, bekapcsolási késleltetés) 5 db 16 bites, általános célú időzítő/számláló áramkört tartalmaz.  Ez az öt időzítő hasonló tulajdonságokkal rendelkezik, s néhány részlet kivételével funkcionálisan azonos áramkörökből épülnek fel. Ha az eltéréseiket is figyelembe vesszük, akkor az alábbi három típusba sorolhatjuk őket:
Mindegyik Timer modul használható önállóan, mint 16 bites időzítő vagy számláló. Az egymást követő sorszámú B és C típusú időzítők azonban összekapcsolhatók 32 bites időzítőként (vagy számlálóként).

Mindegyik 16 bites Timer modulhoz tartozik néhány írható/olvasható regiszter:
TMRx: 16-bites számláló regiszter
PRx: 16-bit Periódus regiszter
TxCON: 16-bit Konfigurációs regiszter

Minden Timer modulhoz hozzátartoznak a programmegszakítást vezérlő alábbi bitek is:
Interrupt engedélyező vezérlőbit (TxIE)
Interrupt jelzőbit (TxIF)
Interrupt prioritás vezérlő bitek (TxIP<2:0>)

Az időzítők felépítését a PIC24H Family Reference Manual 11. fejezete ismerteti részletesen, mi is ezt követjük az alábbiakban.

Timer1 (A típusú időzítő/számláló)

A Timer1 időzítő vázlatos felépítése az alábbi ábrán látható. A Timer1 időzítő abban tér el a többitől, hogy:
 
1. ábra: Timer1 vázlatos felépítése (a PIC24H Family Reference Manual-ból átvett ábra)

Timer1 a fenti sajátosságain kívül a többi számlálóhoz hasonlóan használható az utasításciklus órajelére kapcsolva (utasításciklus-számlálóként), vagy külső órajellel, melyet a B típusú időzítőkhöz hasonlóan az előosztó kimenetén szinkronizálunk az utasításciklus órajeléhez. Előosztó nemcsak a külső, hanem a belső jelforrások esetében is használható. A választható osztásarányok: 1:1, 1:8, 1:64, 1:256. A többi számlálóhoz hasonlóan Timer1-hez is tartozik egy periódus-regiszter és egy összehasonlító áramkör, amely törli a számlálót és engedélyezett állapotban bebillenti az interrupt jelzőbitet, ha a számláló elérte a periódus regiszterben megadott értéket.

Timer2 és Timer4 (B típusú időzítő/számláló)

A B típusú időzítők vázlatos felépítése az alábbi ábrán látható. A külső órajel szinkronizálása ezeknél is az előszámláló kimenetén történik. A B típusú időzítőknél nincs lehetőség aszinkron működésre, ezért a CPU alvás üzemmódjában leáll a számlálás. A CPU tétlen üzemmódjában azonban van lehetőség a számlálásra - a TxCON regiszter TSIDL bitje mondja meg, hogy tétlen üzemmódban leálljon-e az adott számláló, vagy sem.

A B típusú időzítők egy C típusúval sorba kapcsolhatók (32 bites üzemmód), ekkor a B típusú időzítő lesz a bemeneti fokozat, a számlálólánc alacsonyabb helyiértékű fele, s a beállításokat a B típusú számláló vezérlőregiszterébe kell beírni. A vele összekapcsolt C típusú időzítő vezérlőregisztere ilyenkor hatástalan, nincs figyelembe véve.

2. ábra: A B típusú időzítők vázlatos felépítése (a PIC24H Family Reference Manual-ból átvett ábra)

Timer3 és Timer5 (C típusú időzítő/számláló)

A C típusú időzítők (Timer3 és Timer5) vázlatos felépítése az alábbi ábrán látható. Ezeknél az időzítőknél az utasításciklusok órajeléhez történő időzítés a bementen (az előszámláló előtt) történik. A C típusú számlálók sajátossága, hogy az összehasonlító áramkör kimenőjele az Analóg-digitál konverter triggerelésére is használható.

A 32 bites üzemmódban a C típusú időzítő lesz a számlálólánc magasabb helyiértékű fele, s ebben keletkezik és kontrollálható az interrupt jelzőbit is.

3. ábra: A C típusú időzítők vázlatos felépítése (a PIC24H Family Reference Manual-ból átvett ábra)

A TxCON vezérlőregiszterek

A TxCON vezérlőregiszterek vonatkozásában csak apróbb eltérések vannak. Az alábbi ábrán a B típusú időzítők TxCON regiszterének (x = 2, vagy 4) bitkiosztását mutatjuk be. Az eltérések: A és C típusnál nincs T32 bit (a 32 bites üzemmód beállítása), s az A típusnál (Timer1) van egy TSYNC bit is, ami a szinkron és aszinkron működés közötti választást lehetővé teszi.

4. ábra: A B típusú időzítők TxCON vezérlőregisztere

Az egyes bitek jelentése:
TON  - '1' = számlálás indítása, '0' = a számlálás leállítása
TSIDL - '1' = tétlen állapotban leáll a számlálás, '0' = tétlen állapotban is folyik a számlálás
TGATE - belső jelforrás (TCS = 0) esetén '1' = a kapuzott számlálás engedélyezése
TCKPS - előszámláló osztásaránya ('00' = 1:1, '01' = 1:8, '10' = 1:64, '11' = 1:256)
T32 - '1' = 32 bites üzemmód, '0' = 16 bites üzemmód
TCS - '1' = külső jelforrás, '0' belső jelforrás (FOSC/2)

Makrók és támogatói függvények az időzítők kezeléséhez

A PIC24 támogatói programkönyvtár számos makrót és függvényt definiál az időzítők kényelmes programozásához. Az alábbi listán a lib/include/pic24_timer.h állományban definiált, a Timer2 T2CON vezérlő regiszterének beállításához használható makrókat és bitmaszkokat mutatjuk be, melyekhez hasonló definíciókat a magasabb sorszámú időzítőkhöz is találunk a PIC24 támogatói programkönyvtárban.
/* T2CON: TIMER2 CONTROL REGISTER */
#define T2_ON               0x8000      /* Timer2 BE */
#define T2_OFF              0x0000      /* Timer2 KI */
#define T2_OFF_ON_MASK      (~T2_ON)

#define T2_IDLE_STOP        0x2000 /* tétlen állapotban leáll a számlálás */
#define T2_IDLE_CON         0x0000 /* tétlen állapotban is megy a számlálás */
#define T2_IDLE_MASK        (~T2_IDLE_STOP)

#define T2_GATE_ON          0x0040 /* a számláló kapuzása engedélyezve */
#define T2_GATE_OFF         0x0000 /* a számláló kapuzása letiltva */
#define T2_GATE_MASK        (~T2_GATE_ON)

#define T2_PS_1_1           0x0000      /* Előszámláló 1:1 */
#define T2_PS_1_8           0x0010      /*             1:8 */
#define T2_PS_1_64          0x0020      /*            1:64 */
#define T2_PS_1_256         0x0030      /*           1:256 */
#define T2_PS_MASK          (~T2_PS_1_256)

#define T2_32BIT_MODE_ON    0x0008      /* Timer2 és Timer 3 32 bites módban */
#define T2_32BIT_MODE_OFF   0x0000      /* 32 bites mód letiltva */
#define T2_32BIT_MODE_MASK   (~T2_32BIT_MODE_ON)

#define T2_SOURCE_EXT       0x0002      /* Külső órajelforrás */
#define T2_SOURCE_INT       0x0000      /* Belső órajelforrás */
#define T2_SOURCE_MASK      (~T2_SOURCE_EXT)
 
A fenti makrók segítségével a T2CON regiszter beállítása az alábbi (öndokumentáló) módon történhet: T2CON = T2_OFF | T2_IDLE_CON | T2_GATE_OFF
               | T2_32BIT_MODE_OFF
               | T2_SOURCE_INT
               | T2_PS_1_64 ;
Ennek eredménye ugyanaz, mintha azt írtuk volna, hogy T2CON = 0x0020; - csupán az olvashatóságban van köztük különbség.

Az alábbi programsor bemutatja,  hogy hogyan használhatjuk fel a fenti listában definiált bitmaszkokat a T2CON regiszter valamelyik bitmezőjének módosítására a többi bitmező módosítása nélkül (itt most az előszámláló osztási arányát módosítjuk 1:8-ra):
T2CON = ( T2CON & T2_PS_MASK ) | T2_PS_1_8; 
Az egyes bitekhez nincs külön makró definiálva, mivel több számláló is használja ugyanazt az elnevezést, így nem használhatjuk például a _TON makrót sem. Helyette így kapcsolhatjuk be Timer2-nél a számlálást:
T2CONbits.TON = 1;  //bekapcsolja timer2-nél a számlálást
A lib/common/pic24_timer.c állomány támogatói függvényeket biztosít az időzítők kezeléséhez, melyek közül néhányat bemutatunk az alábbi listán: 
uint16 msToU16Ticks(uint16 u16_ms, uint16 u16_pre) {
u16_pre
  float f_ticks = FCY;
  uint16 u16_ticks;
  f_ticks = (f_ticks*u16_ms)/u16_pre/1000L;
  ASSERT(f_ticks < 65535.5);
  u16_ticks = roundFloatToUint16(f_ticks);  //back to integer
  return u16_ticks;
}

uint16 usToU16Ticks(uint16 u16_us, uint16 u16_pre) {
  float f_ticks = FCY;
  uint16 u16_ticks;
  f_ticks = (f_ticks*u16_us)/u16_pre/1000000L;
  ASSERT(f_ticks < 65535.5);
  u16_ticks = roundFloatToUint16(f_ticks);  //back to integer
  return u16_ticks;
}

#define getTimerPrescale(TxCONbits) getTimerPrescaleBits(TxCONbits.TCKPS)
uint16 getTimerPrescaleBits(uint8 u8_TCKPS) {
  uint16 au16_prescaleValue[] = { 1, 8, 64, 256 };
  ASSERT(u8_TCKPS <= 3);
  return au16_prescaleValue[u8_TCKPS];
}
A msToU16Ticks(uint16 u16_ms, uint16 u16_pre) függvény a milliszekundumban adott periódusidőt (u16_ms paraméter) számítja át óraimpulzus-számra az előszámláló osztásarányának (u16_pre paraméter) és az utasításfrekvencia (FCY előredifiniált makró) felhasználásával. Ugyanígy működik a usToU16Ticks() függvény is, csak itt mikroszekundumban adott időzítést konvertálunk óraimpulzus-számra. Mindkét függvényben lebegőpontos számábrázolást és aritmetikát használunk a közbenső adattárolásnál, ezért időkritikus folymatoknál nem célszerű a használatuk.

Az ASSERT() függvény hívása a kiszámolt paraméter ellenőrzésére szolgál  (hogy beleesik-e a 16 bites érvényességi tartományba). Ha a kiszámolt érték nem ábrázolható 16 biten, akkor meghívásra kerül a ReportError() függvény, ami elmenti a hiba okát és szoftveres újraindítást hajt végre. A hibaüzenet akkor kerül kiírásra (a soros porton keresztül), amikor az újraindítás után a főprogram meghívja ResetCause() (az újraindítás oka) függvényt.

A lib/include/pic24_timer.h állományban definiált getTimerPrescale(TxCONbits) makró a getTimerPrescaleBits(uint8 u8_TCKPS) függvényt hívja meg, amelyik a TxCON regiszterből kiolvasott TCKPS bitmezőhöz tartozó előosztási arányt (1, 8, 64 vagy 256) adja visszatérési értékül.   

Ezek a függvények jól használhatók periodikus interruptok előállításánál a periódus regiszter beállítására.

Periodikus programmegszakítások

Ebben a fejezetben a Timer2 és Timer3 időzítőket fogjuk használni, s kizárólag belső órajellel, kapuzás nélkül (tehát TCS =0, TGATE = 0 beállítással) dolgozunk. Ebben az üzemmódban Timer2 interrupt jelzőbitje (T2IF) '1'-be billen, amikor a TMR2 számláló értéke megegyzik a PR2 periódus regiszterbe írt értékkel. Az interrupt jelzőbit bebillenésével egyidőben a TMR2 számláló törlődik, s egy újabb számlálási periódus kezdődik, így a programmegszakítások periodikusan, azonos időközönként következnek be. A periódus idejét az utasításciklus TCY periódus ideje, az előszámláló PRE osztási aránya, valamint a PR2 periódus regiszter értéke szabja meg:

TT2IF = (PR2 + 1) * PRE * TCY = (PR2 + 1) * PRE /FCY

A gyakorlatban azonban többnyire egy adott TT2IF periódus időhöz tartozó PR2 értéket keressük, amit a fenti egyenletből könnyen kifejezhetünk:

PR2 = (TT2IF*FCY/PRE) - 1

A fentebb ismertetett támogatói függvények felhasználásával a PR2 regisztert így állíthatjuk be egy adott időzítéshez, például TT2IF = 15 ms periódushoz:
PR2 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T2CONbits)) - 1; A kiszámolt értéket eggyel csökkentenünk kellett, mert Timer2 periódusa = PR2 + 1.

Az alábbi táblázatban a különböző előosztási arányhoz tartozó PR2 periódus számot láthatjuk, TT2IF = 15 ms és FCY = 40 MHz esetén. Az első két érték érvénytelen, mert a 16 bites PR2 regiszterben nem ábrázolhatók.

1. táblázat: PRE és PR2 értékek TT2IF = 15 ms és FCY = 40 MHz esetén.
PRE = 1:1 1:8 1:64 1:256
PR2 = 599999 (érvénytelen) 74999 (érvénytelen) 9374 2343

Négyszögjel előállítása Timer2 interrupttal

A Timer2 periodikus interruptjának kipróbálására nézzük meg a tankönyvi mintapéldák között található a chap09/squarewave.c programot, amely négyszögjelet állít elő az RB2 kimeneten a Timer2-vel keltett periodikus programmegszakítás segítségével. A programban úgy konfiguráljuk a Timer2 időzítőt, hogy 15 ms-onként generáljon programmegszakítást. Az interrupt kiszolgálásakor töröljük a T2IF jelzőbitet és ellenkező állapotba billentjük az RB2 kimenetet. A kimenő jel minden második programmegszakításkor kerül azonos fázisba, a négyszögjel periódusideje tehát 2 * 15 ms = 30 ms.

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

#define WAVEOUT _LATB2           //az RB2 kivezetést használjuk kimenetként
inline void CONFIG_WAVEOUT() {
  CONFIG_RB2_AS_DIG_OUTPUT();
}

//-- Timer2 interrupt kiszolgálása
void _ISRFAST _T2Interrupt (void) {
  WAVEOUT = !WAVEOUT;           //átbillenti a kimenet állapotát
  _T2IF = 0;                    //törli az interrupt jelzőbitet
}

#define ISR_PERIOD  15          //interrupt periódus idő ms egységekben

void  configTimer2(void) {
//-- T2CON beállítása öndukumentáló módon
//-- Helyettesíthető volna a T2CON = 0x0020 értékadással
  T2CON = T2_OFF | T2_IDLE_CON | T2_GATE_OFF
          | T2_32BIT_MODE_OFF
          | T2_SOURCE_INT
          | T2_PS_1_64 ;        //végeredményben: T2CON = 0x0020
//-- El kell venni 1-et PR2 beállításához, mert a periódus PR2 + 1 lesz!
  PR2 = msToU16Ticks(ISR_PERIOD, getTimerPrescale(T2CONbits)) - 1;
  TMR2  = 0;                    //törli a számlálót
  _T2IF = 0;                    //törli az interrupt jelzőbitet
  _T2IP = 1;                    //beállítja az interrupt prioritást
  _T2IE = 1;                    //engedélyezi az interruptot
  T2CONbits.TON = 1;            //elindítja a számlálást
}

int main (void) {
  configBasic(HELLO_MSG);
  CONFIG_WAVEOUT();             //a kimenet konfigurálása
  configTimer2();               //Timer2 konfigurálása
  while (1) {                   //Az interrupt elvégzi a többi munkát
    doHeartbeat();                 //LED villogtatással jelezzük, hogy fut a program
  }                             // while(1) ciklus vége
}


Timer2 beállítását a configTimer2() eljárás végzi. Ebben először beállítjuk a T2CON regisztert,  ügyelve rá, hogy a TON bit ekkor még nulla legyen. Az 1. táblázat szerint legalább 1:64-es előosztást kell használnunk (a programban T2_PS_1_64 beállítás szerepel, ezen kívül még a T2_PS_1_256 opció lenne használható). PR2 beállítását a fentebb ismertetett támogatói függvények segítségével végezhetjük. A számláló regiszter törlése és a programmegszakítás engedélyezése után indítjuk a számlálást TON bekapcsolásával (T2CONbits.TON = 1;). A beállított interrupt prioritási szint konkrét értékének nincs jelentősége. 

A főprogramban a számlálás elindítása után nincs tennivalónk, az interrupt kiszolgáló eljárás elvégzi a munkát. A while(1) ciklusban így csak az életjelző LED villogtatásával foglalkozunk, ami jelzi, hogy fut a program. Természetesen nem ez a legmegfelelőbb módszer a négyszögjel előállítására, ez a program csupán illusztráció a periodikus programmegszakítás bemutatására.

A program által generált négyszöghullám ellenőrzéséhez az alábbi ábra szerint vezessük ki RB2 jelét az ICSP csatlakozó nem használt 6. tüskéjére, s ekkor a PICkit2 Logikai Analizátor üzemmódjában a 3. csatornán láthatjuk a jelet.  Ügyeljünk rá, hogy a triggerelést a Ch3 csatornára állítsuk be, s a mintavételezési frekvenciát a minimális értékre állítsuk be, a 6. ábrán láthatóak szerint.

5. ábra: A négyszögjel ellenőrzésére használt kísérleti áramkör


6. ábra: A squarewave.c program kimenete a PICkit2 Logikai Analizátor ablakában

Piezo hangkeltő megszólaltatása Timer2 megszakítással

Ez a program az előbbi mintapélda, a squarewave.c program módosított változata, ahol a kimenetet RB2-ről RB5-re, a félperiódusok idejét (a megszakítások periódusidejét) pedig 15 ms-ról 500 µs-ra változtattuk. A program tehát kb. 1 kHz-es négyszöghullám jelet állít elő az RB5 kimeneten, egy piezo buzzer megszólaltatásához.

Hardver követelmények:
Hardver különbségek az alapértelmezettől elérő kártyáknál:
A program alig különbözik az előző példaprogramtól. Az eltérések: a kimenet átkerült az RB5 lábra, az ISR_PERIOD makró értékét µs egységekben kell megadni, s a korábbi msToU16Ticks() függvény helyett most a usToU16Ticks() függvényt kell használni a PR2 periódus regiszterbe írandó szám kiszámításához.

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

#define WAVEOUT _LATB5          //az RB5 kivezetést használjuk kimenetként

inline void CONFIG_WAVEOUT() {  //a WAVEOUT kimenet konfigurálása
  CONFIG_RB5_AS_DIG_OUTPUT();
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS)
//-- Az alábbiak csak a Microstick Plus kártya esetén kellenek! ------
  CONFIG_RA3_AS_DIG_OUTPUT();  //Magas szintre állítjuk az RA3 lábat
  _RA3 = 1;                    //(kiegészítő áramkörök bekapcsolása)
  DELAY_MS(10);                //Várunk, amíg beáll a tápfeszültség
#endif
}

#define ISR_PERIOD  500         //interrupt periódusidő us egységekben

//Timer2 interrupt kiszolgálása
void _ISRFAST _T2Interrupt (void) {
  WAVEOUT = !WAVEOUT;           //átbillenti a kimenet állapotát
  _T2IF = 0;                    //törli az interrupt jelzőbitet
}

void  configTimer2(void) {
//-- T2CON beállítása öndukumentáló módon
  T2CON = T2_OFF | T2_IDLE_CON | T2_GATE_OFF
          | T2_32BIT_MODE_OFF
          | T2_SOURCE_INT
          | T2_PS_1_64 ;
//-- Vegyük észre, hogy most a usToU16Ticks() függvényt használjuk!
//-- El kell venni 1-et PR2 beállításához, mert a periódus PR2 + 1 lesz!
  PR2 = usToU16Ticks(ISR_PERIOD, getTimerPrescale(T2CONbits)) - 1;
  TMR2  = 0;                    //törli a számlálót
  _T2IF = 0;                    //törli az interrupt jelzőbitet
  _T2IP = 1;                    //beállítja az interrupt prioritást
  _T2IE = 1;                    //engedélyezi az interruptot
  T2CONbits.TON = 1;            //elindítja a számlálást
}

int main (void) {
  configBasic(HELLO_MSG);
  CONFIG_WAVEOUT();             //a kimenet konfigurálása
  configTimer2();               //Timer2 konfigurálása
  while (1) {                   //Az interrupt elvégzi a többi munkát
    doHeartbeat();              //LED illogtatással jelezzük, hogy fut a program
  }                             // while(1) ciklus vége
}

Négyszögjel előállítása Timer3 használatával

Az alábbi kódrészletben megmutatjuk, hogy hogyan írhatjuk át a fenti programokat Timer3 használatához. Az átírás szinte mechanikusan végezhető: T2 helyett mindenhol T3-at, TMR2 és PR2 helyett TMR3-at, illetve PR3-at írunk.

Egyetlen új sor van a programban: ahhoz, hogy Timer3 beállításának hatása legyen, biztosnak kell lennünk abban, hogy Timer2 és Timer3 nincsenek összekapcsolva 32 bites számlálóláncként. Ezért a konfigurálást azzal kell kezdenünk, hogy Timer2 vezérlő regiszterében (T2CON) kikapcsoljuk a T32 bitet. 

3. lista: Timer3 konfigurálása (programrészlet)
//Timer3 interrupt kiszolgálása
void _ISRFAST _T3Interrupt(void) {
  WAVEOUT = !WAVEOUT;           //átbillenti a kimenet állapotát
  _T3IF = 0;                    //törli az interrupt jelzőbitet
}

#define ISR_PERIOD  15          //interrupt periódusidő ms egységekben

void  configTimer2(void) {
  //Biztosítsuk magunkat arról, hogy TMR2 és TMR3 független
  //időzítőként működnek!
  T2CONbits.T32 = 0;
  //T3CON beállítása öndukumentáló módon
  //Helyettesíthető volna a T3CON = 0x0020 értékadással
  T3CON = T3_OFF | T3_IDLE_CON | T3_GATE_OFF
          | T3_32BIT_MODE_OFF
          | T3_SOURCE_INT
          | T3_PS_1_64 ;        //végeredményben: T3CON = 0x0020
  //El kell venni 1-et PR3 beállításához, mert a periódus PR3 + 1 lesz!
  PR3 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T3CONbits)) - 1;
  TMR3  = 0;                    //törli a számlálót
  _T3IF = 0;                    //törli az interrupt jelzőbitet
  _T3IP = 1;                    //beállítja az interrupt prioritást
  _T3IE = 1;                    //engedélyezi az interruptot
  T3CONbits.TON = 1;            //elindítja a számlálást
}

LED villogtatása periodikus interrupttal

Az előző program által keltett négyszögjel periódusidejének növelésével LED villogtató programot is készíthetünk. A tankönyvi mintapéldák között található a chap09/ledflash_timer.c program éppen ezt mutatja be. Ebben a programban Timer3-at úgy konfiguráljuk, hogy 300 ms-onként okozzon programmegszakítást az RB14 kimenetre kötött LED villogtatásához.

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

/// LED1 konfigurálása
#define CONFIG_LED1() CONFIG_RB14_AS_DIG_OUTPUT()
#define LED1  _LATB14     //LED1 állapota

//Timer3 interrupt kiszolgáló rutinja
void _ISRFAST _T3Interrupt (void) {
  LED1 = !LED1; //átbillenti a LED állapotát
  _T3IF = 0;    //törli Timer3 interrupt jelzőbitjét
}

#define ISR_PERIOD     300      // interrupt periódusidő ms-ban

void  configTimer3(void) {
  //Az alábbi sor biztosítja, hogy Timer2,3 független számlálók legyenek
  T2CONbits.T32 = 0;     // 32-bit üzemmód kikapcsolása
  //T3CON konfigurálása öndukumentáló módon
  //helyettesíthető ezzel: T3CON = 0x0030
  T3CON = T3_OFF |T3_IDLE_CON | T3_GATE_OFF
          | T3_SOURCE_INT
          | T3_PS_1_256 ; 
  PR3 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T3CONbits)) - 1;
  TMR3  = 0;                       //törli a számlálót
  _T3IF = 0;                       //törli az interrupt jelzőbitet
  _T3IP = 1;                       //beállítja az interrupt prioritását
  _T3IE = 1;                       //engedélyezi a programmegszakítást
  T3CONbits.TON = 1;               //bekapcsolja a számlálót
}

int main (void) {
  configBasic(HELLO_MSG);
  /** I/O konfigurálás ****/
  CONFIG_LED1();       //LED1 konfigurálása
  LED1 = 1;
  /** Timer3 konfigurálása és használata ***/
  configTimer3();
  while (1) {
    //tétlen módba lépünk, amíg az időzítés le nem telik.
    //Timer3 programmegszakítása majd felébreszti a CPU-t a tétlen üzemmódból
    IDLE();  //makró  __asm__ volatile ("pwrsav #1") végrehajtásához
  }
  return 0;
}
Timer3 beállítását a configTimer3() eljárás végzi. Ebben először kikapcsoljuk a 32 bites üzemmódot, hogy Timer2 és Timer3 függetlenek legyenek, majd beállítjuk a T3CON regisztert,  ügyelve rá, hogy a TON bit ekkor még nulla legyen. A 300 ms-os periódusidőhöz az 1:256 előosztást kell használnunk, hogy a periódus idő elegendően hosszú (néhányszor 100 ms) lehessen. PR2 beállítását a fentebb ismertetett támogatói függvények segítségével végezhetjük. sok száma beférjen a 16 bites PR3 regiszterbe. A számláló regiszter törlése és a programmegszakítás engedélyezése után indítjuk a számlálást TON bekapcsolásával (T3CONbits.TON = 1;). A beállított interrupt prioritási szint konkrét értékének nincs jelentősége. 

A _T3Interrupt kiszolgálásakor minden beérkező megszakításkor átbillentjük LED1 állapotát és töröljük a T3IF interrupt jelzőbitet. Az interrupt periódusidőt azért kellett megnyújtani, hogy szemmel is követhető legyen a LED villogása.

A főprogramban a számlálás elindítása után nincs tennivalónk, az interrupt kiszolgáló eljárás elvégzi a munkát. A while(1) ciklusban így tétlen állapotba helyezzük a CPU-t,ahonnan majd a Timer3 időzítő programmegszakítása ébreszti fel. Ahhoz, hogy Timer3 a tétlen állapotban is számláljon, a T3CON vezérlő regiszterben nulláznunk kell a TSIDL bitet (lásd 4. ábra!) , amit vagy a T3CONbits.TSIDL = 0 paranccsal, vagy - ahogy a programban is tettük - T3CON beállításakor a T3_IDLE_CON opció használatával érhetünk el. Arra vigyázzunk, hogy alvás állapotba ne kapcsoljuk át a mikrovezérlőt, mert akkor leáll az időzítés alapjául szolgáló oszcillátor is! Alvás üzemmódban kizárólag Timer1 képes működni, feltéve, hogy aszinkron módban, külső jelforrásból számlál!

Figyelem! FCY = 40 MHz esetén a periódusidőt a programban látható értéknél nem vehetjük sokkal nagyobbra, mert a 16 bites PR3 regiszterbe nem írhatunk 65535-nél nagyobb számot! ISR_PERIOD legnagyobb értékét az alábbi képlettel számolhatjuk ki (milliszekundum egységekben):

                      ISR_PERIOD_MAX = 65536 * 256* 1000 / FCY = 419 ms

A bemenet mintavételezése

A periodikus megszakítások egyik szokványos felhasználási területe a bemenet(ek) mintavételezése. Az I/O portok c. fejezetben foglalkoztunk LED kapcsolgatásával nyomógomb felhasználásával. Most visszatérünk a korábbi feladatokhoz, s megmutatjuk, hogyan oldhatjuk meg azokat a periodikus interruptok felhasználásával. Érdemes összehasonlítani az ott ismertetett programokat a mostaniakkal!

Hardver feltételek:

Mielőtt a programok ismertetéshez kezdenénk, elevenítsük fel a korábban használt kapcsolást! A képen látható alkatrészek közül C4, C5, R2 és az "életjel" HB_LED az alapkapcsolás részei.

A hatpólusú csatlakozó és a PICkit2 (vagy valamilyen más USB-UART átalakító) a mikrovezérlőnk soros porti kommunikációját biztosítja.

A feladatokhoz szükséges áramköri többlet tehát az RB14 kimenetre csatlakozó, áramkorlátozó soros ellenállással ellátott LED1, amit kapcsolgatni fogunk, továbbá az RB7 bemenetre kötött SW1 nyomógomb és az RB6 bemenetre csatlakozó SW2 választó kapcsoló.

Az RB12 kivezetésre is köthetünk egy LED-et (LED2). Ennek a feladatok megoldása szempontjából nincs szerepe, csupán az SW2 kapcsoló (melyet akár egy drót darabkával is helyettesíthetünk) állapotát jelzi majd vissza.

Az SW2 kapcsolónak csak annál a feladatnál lesz szerepe, amelynél a ki- és bekapcsolt állapotokat egy villogó állapottal is kibővítjük.

Az alábbi programoknál tehát ennek az áramkörnek
a meglétét feltételezzük.
                                                                 
                                                               7. ábra: A kísérleti áramkör kiegészítése (LED1, SW1, SW2)

Hardver különbségek az alapértelmezettől elérő kártyáknál:

A fenti áramkörhöz tartozó és a felsorolt hardver különbségeket is figyelembe vevő hardverfüggő definíciók az alábbi listában láthatók. Ez a rész közös a most következő programokban, tehát csak egyszer mutatjuk be!

5. lista: Hardverfüggő definíciók a 7. ábrán bemutatott kapcsoláshoz
#include "pic24_all.h"

///--- 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_RAW    _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_RAW    _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_RAW    _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

/// SW2 kapcsoló konfigurálása (ezt nem kell pergésmentesíteni)
#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
}

#define OFF !ON                //A LED-ek kikapcsol állapota
//-- HARDWARE_PLATFORM függő rész vége ------------------------

//A pergésmentesített kapcsoló állapota, amelyet az időzítő
//programmegszakítás kiszolgálója állít be
volatile uint8_t u8_valueSW1 = 1;         //kezdetben magas szinten

#define SW1             u8_valueSW1       //a pergésmentesített nyomógomb állapota
#define SW1_PRESSED()   SW1==0            //lenyomott állapot feltétele
#define SW1_RELEASED()  SW1==1            //felengedett állapot feltétele

LED ki-/bekapcsolása nyomógombbal

A tankönyvi mintapéldák között található chap08/ledtoggle.c program a véges állapotgép megközelítést használta arra, hogy egy LED állapotát átkapcsolja, amikor egy nyomógombot megnyomunk, majd elengedünk. Abban a programban egy pergésmentesítő késleltetést használtunk a nyomógombhoz csatlakozó bemenet szoftveres lekérdezésénél. Ha belegondolunk, ez is mintavételezés volt, hiszen csak a pergésmentesítő késleltetés leteltével vizsgáltuk meg újra a bemenet állapotát. A késleltetésen alapuló szoftveres mintavételezés nagy hátránya azonban, hogy a késleltetés idejére lefoglalja a CPU-t, így az nem tud más feladattal foglalkozni.

Most bemutatjuk, hogy a Timer3 periodikus programmegszakításainak a felhasználásával hogyan végezhetjük a mintavételezést  hardveres időzítéssel, tehermentesítve a főprogramot. Az alábbi programrészletet a chap09/ledtoggle_timer.c tankönyvi példaprogramból vettük. Kihagytuk belőle az 5. listán bemutatott hardverfüggő definiálásokat, valamint azokat a lényegtelen részeket, amelyek már az előző fejezetben bemutatott chap08/ledtoggle.c programban is szerepeltek.

6. lista: Részletek a chap09/ledtoggle_timer.c programból
//Timer3 interrupt kiszolgáló rutinja
void _ISRFAST _T3Interrupt (void) {
  u8_valueSW1 = SW1_RAW;       //mintavételezi a nyomógomb bemenetet
  _T3IF = 0;                   //törli az interrupt jelzőbitet
}

#define ISR_PERIOD     15      // interrupt periódusidő ms-ban

void  configTimer3(void) {
//-- Az alábbi sor biztosítja, hogy Timer2,3 független számlálók legyenek
  T2CONbits.T32 = 0;           // 32-bit üzemmód kikapcsolása
//-- T3CON konfigurálása öndukumentáló módon
//-- helyettesíthető ezzel: T3CON = 0x0020
  T3CON = T3_OFF |T3_IDLE_CON | T3_GATE_OFF
          | T3_SOURCE_INT
          | T3_PS_1_64 ; 
  PR3 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T3CONbits)) - 1;
  TMR3  = 0;                   //törli a számlálót
  _T3IF = 0;                   //törli az interrupt jelzőbitet
  _T3IP = 1;                   //beállítja az interrupt prioritását
  _T3IE = 1;                   //engedélyezi a programmegszakítást
  T3CONbits.TON = 1;           //bekapcsolja a számlálót
}

int main (void) {
  STATE e_mystate;             //Az állpotgép állapotjelzője
  configBasic(HELLO_MSG);
  CONFIG_SW1();                //nyomógomb konfigurálása
  CONFIG_LED1();               //LED1 konfigurálása
  LED1 = OFF;                  //Kezdetben ne világítson!
  configTimer3();              //Timer3 beállítása
  e_mystate = STATE_WAIT_FOR_PRESS;  //Kezdéskor lenyomásra várunk

  /* Figyeljük meg, hogy a perodikus programmegszakításkor történő mintavételezés
   * folöslegessé teszi a pergésmentesítő késleltetést a while ciklus végén
   */
  while (1) {
    printNewState(e_mystate);  //nyomkövető kiírás minden állapotváltáskor
    switch (e_mystate) {
      case STATE_WAIT_FOR_PRESS:
        if (SW1_PRESSED()) {
          e_mystate = STATE_WAIT_FOR_RELEASE;
        }
        break;
      case STATE_WAIT_FOR_RELEASE:
        if (SW1_RELEASED()) {
          LED1 = !LED1;        //átbillenti a LED állapotát
          e_mystate = STATE_WAIT_FOR_PRESS;
        }
        break;
      default:
        e_mystate = STATE_WAIT_FOR_PRESS;
    } //switch(e_mystate) vége
    doHeartbeat();             //ledvillogtatással jelezzük, hogy fut a program
  }                            // while(1) ciklus vége
}
A nyomógomhoz kötött bemenet mintavételezése az ISR_PERIOD konstans ms-ban megadott értéke szerinti  időközönként történik, s a bemenet állapotát a Timer3 interruptot kiszolgáló eljárás elmenti az u8_valueSW1 változóba. Az SW1_PESSED() és SW1_RELEASED() makrók ennek a változónak az értékét fogják vizsgálni.

Timer3 konfigurálása megegyezik a Négyszögjel előállítása Timer3 használatával c. szakaszban leírtakkal, semmi újat nem tartalmaz.

A főprogram while(1) ciklusában a véges állapotgép megközelítést alkalmaztuk, a chap08/ledtoggle.c programhoz hasonlóan. A program a "lenyomásra várás" és a "felengedésre várás" állapotok között váltakozik, s felengedéskor váltja LED1 állapotát. A program nyomkövetési céllal minden állapotváltáskor kiírja a soros porton az aktuális állapotát.

Megjegyzés: Figyeljük meg, hogy az u8_valueSW1 változót volatile módosítóval deklaráltuk. Ezzel azt jelezzük a  fordítóprogramnak, hogy a változó külső hatásra módosulhat egyik programbeli értékadástól a másikig, így letiltunk vele bizonyos optimalizálásokat. Ilyen volatile módosítóval kell deklarálni minden hardver regisztert, és minden olyan változót, amelyet interrupt kiszolgáló eljárás módosíthat.

Megfontolások a mintavételezési frekvencia megválasztásához

Milyen időközönként kell végeznünk a mintavételelzést ahhoz, hogy a pergésmentesítés jól működjön? Az interrupt (vagyis a mintavételezés) periódus ideje legyen hosszabb a kontaktus pergésének várható időtartamánál (ez a nyomógombonként változó, de általában 5 ms-nál kevesebb), de legyen rövidebb, mint a várható legrövidebb impulzus szélességének a fele. Kézzel működtetett nyomógomboknál a leggyorsabb lenyomás/felengedés együttes ideje 100 ms lehet, tehát a programban szereplő 15 ms periódusidő mindkét feltételnek megfelel.

LED, kapcsolók és szemaforok

Az előző programban a főprogramra hárult az a feladat, hogy a nyomógomb programmegszakításban mintavételezett állapotait figyelje és nyilvántartsa. Ha az a feladat, hogy egy lenyomás és elengedés ciklust figyeljünk, s ennek lezajlása után végezzünk el valamilyen feladatot, akkor vezessünk be egy jelzőt (szemafor), s a nyomógomb állapotát a megszakítási szinten kezeljük. Így levehetjük ennek terhét a főprogramról.

7. lista: Részlet a chap09/button_semaphore.c programból (a definíciós rész az 5. listán látható)
//-- A szemafor változó
volatile uint8_t u8_pressAndRelease = 0;

typedef enum  {
  STATE_RESET = 0,             //RESET utáni állapot
  STATE_WAIT_FOR_PRESS,        //Lenyomásra vár
  STATE_WAIT_FOR_RELEASE       //felengedésre vár
} STATE;

volatile STATE e_mystate = STATE_RESET;

//-- Timer3 programmegszakítások kiszolgálása
void _ISRFAST _T3Interrupt (void) {
  if (!u8_pressAndRelease) {
    //-- szemafor törölve (fel volt használva)
    //-- újabb lenyomás/felengedés ciklus kezdődhet
    switch (e_mystate) {
      case STATE_WAIT_FOR_PRESS:
        if (SW1_PRESSED()) {
          e_mystate = STATE_WAIT_FOR_RELEASE;
        }
        break;
      case STATE_WAIT_FOR_RELEASE:
        if (SW1_RELEASED()) {
          //Lezajlott egy teljes lenyomás/felengedés
          //Beállítjuk a szemafort
          u8_pressAndRelease = 1;
          e_mystate = STATE_WAIT_FOR_PRESS;
        }
        break;
      default:
        e_mystate = STATE_WAIT_FOR_PRESS;
    }
  }

  _T3IF = 0;                       //töröljük a megszakítást kérő jelzőbitet
}

#define ISR_PERIOD     15          // ms egységekben
void  configTimer3(void) {
  //biztosítjuk, hogy Timer2 és Timer3 függetlenek legyenek
  T2CONbits.T32 = 0;               // 32-bites mód kikapcsolva
  //T3CON beállítása öndokumentáló módon
  //helyettesíthető volna ezzel: T3CON = 0x0020
  T3CON = T3_OFF |T3_IDLE_CON | T3_GATE_OFF
          | T3_SOURCE_INT
          | T3_PS_1_64 ;
  PR3 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T3CONbits)) - 1;
  TMR3  = 0;                       //Timer3 számlálóját töröljük
  _T3IF = 0;                       //interrupt jelzőbit törlése
  _T3IP = 1;                       //prioritás beállítása
  _T3IE = 1;                       //programmegszakítás engedélyezése
  T3CONbits.TON = 1;               //Timer3 indítása
}


int main (void) {
  configBasic(HELLO_MSG);
  /** GPIO config ***************************/
  CONFIG_SW1();                    //SW1 nyomógomb konfigurálása
  CONFIG_LED1();                   //A LED konfigurálása
  LED1 = OFF;                      //Induláskor ne világítson!
  configTimer3();                  //Timer3 konfigurálása

  /**** Minden lenyomás/felengedés ciklus átbillenti a LED állapotát ******/

  while (1) {
    //lenyomás és felengedés lezajlására várunk
    if (!u8_pressAndRelease) {
      doHeartbeat();               //jelezzük, hogy fut a program
    } else {
      //beállt a szemafor
      LED1 = !LED1;                //LED állapot átbillentése
      u8_pressAndRelease = 0;      //Szemafor törlésa
    }
  }                                // end while (1)
}

Egy összetettebb feladat

Szintén az I/O portok c. fejezetben foglalkoztunk azzal az összetettebb LED kapcsolgatási feladattal is, amelyben a ki/bekapcsoláson kívül - egy másik kapcsoló állásától függően - beiktattunk egy LED villogtatás állapotot is. A program kódja a chap08/ledsw1.c állományban található. Abban a programban is periodikus szoftveres lekérdezéssel vizsgáltuk a nyomógomb állapotát, késleletetéssel oldottuk meg a pergésmentesítést, s véges állapotgép módszerrel szerveztük a főprogram logikáját, amelyben külön állapotként tartottuk nyilván a lenyomásra és a felengedésre történő várakozást. 

Emlékeztetőül a feladat: A 7. ábrán látható áramkörhöz készítsünk olyan programot, amelyben az SW1 nyomógombbal váltogathatjuk a LED1 állapotait, s 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!
Most bemutatjuk a feladatnak egy másfajta megoldását, amelyben a Timer3 periodikus programmegszakításainak a felhasználásával végezzük a mintavételezést, s az interrupt kiszolgáló eljárásra bízzuk a lenyomás és felengedés figyelését. Az interrupt kezelőtől azt várjuk, hogy egy szemafort állítson be, amikor a lenyomást követő felengedés is megtörtént. A főprogramban ezáltal csökkenthetjük a nyilvántartott állapotok számát, egyszerűsödik a kód.

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:

2. táblázat: A véges állapotgép modell táblázatos összefoglalása
Pillanatnyi állapot Következő állapot Átmenet feltétele Tevékenység
Gombnyomásra vár 1. 2. gombnyomásra vár P&R LED1 = 0
1Gombnyomásra vár 2. Villog P&R és SW2==1 LED1 = 1
Lenyomásra vár 1. P&R és SW2==0 LED1 = 1
Villog Felengedésre vár 3. P 2LED1=!LED1
Felengedésre vár Gombnyomásra vár 1. P&R LED1 = 1
Megjegyzések: 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.

Jelmagyarázat:  P - lenyomás, R - felengedés, P&R vagy PNR - lenyomás és felengedés

Az állapotváltásokat diagram formájában is ábrázolhatjuk:

8. ábra: A véges állapotgép állapotváltásainak diagramja

A kidolgozott mintaprogram a chap09/ledsw1_timer.c állományban található. Itt csak két részletét mutatjuk be. Timer3 interrupt kezelője egy mini állapotgép, melynek két állapota van: gomlenyomásra vár (STATE_WAIT_FOR_PRESS), vagy gombfelengedésre vár (STATE_WAIT_FOR_RELEASE). Amikor a felengedés megtörtént, akkor '1'-be állítjuk a u8_pnrSW1 szemafort, jelezve a főprogramnak, hogy lezajlott egy PNR (press and release = lenyomás és felengedés) ciklus. A szemaforon kívül bizonyos esetekben a nyomógomb mintavételezett állapotára is szükségünk lesz, amelyet az u8_valueSW1 változóban  tárol a program. Még egy apróság: az if (SW1_PRESSED() && (u8_pnrSW1 == 0)) feltételvizsgálat második részfeltétele biztosítja, hogy ne kezdjünk újabb ciklus feldolgozásába, amíg a szemafor nem került felhasználásra (a szemafort a főprogram felhasználás után nullázza). 

8. lista: Részlet a chap09/ledsw1_timer.c programból - Timer3 interrupt kiszolgáló eljárásának listája:
typedef enum  {                //SW1 állapotainak felsorolása
  STATE_WAIT_FOR_PRESS = 0,    //Lenyomásra várunk
  STATE_WAIT_FOR_RELEASE,      //Felengedésre várunk
} ISRSTATE;

volatile uint8_t u8_valueSW1  = 1;    //kezdetben magas szint
volatile uint8_t u8_pnrSW1 = 0;       //Igaz, ha SW1 megnyomása/felengedése megtörtént

//-- Timer3 megszakításainak kiszolgálása
void _ISRFAST _T3Interrupt (void) {
  static ISRSTATE e_isrState = STATE_WAIT_FOR_PRESS;
  u8_valueSW1 = SW1_RAW;       //mintavételezi a nyomógomb állapotát
  switch (e_isrState) {
    case STATE_WAIT_FOR_PRESS:
      if (SW1_PRESSED() && (u8_pnrSW1 == 0))
        e_isrState = STATE_WAIT_FOR_RELEASE;
      break;
    case STATE_WAIT_FOR_RELEASE:
      if (SW1_RELEASED()) {
        e_isrState = STATE_WAIT_FOR_PRESS;
        u8_pnrSW1 = 1;         //beállítja a szemafort (pnr = Pressed aNd Released)
        break;
      }
    default:
      e_isrState = STATE_WAIT_FOR_RELEASE;
  }
  _T3IF = 0;                   //törli a timer interrupt jelzőbitjét
}

Az alábbi programrészlet a főprogramot mutatja be, amely már a szemafor kezeléséhez és 8. ábrán látható állapotváltásokhoz van igazítva. Az első gomblenyomás és felengedésre várakozás (STATE_WAIT_FOR_PNR1) állapotából akkor lép tovább a a program, ha az u8_pnrSW1 szemafor '1'-be állt, jelezve, hogy lezajlott egy lenyomás és felengedés ciklus. Ekkor törölnünk kell a szemafort, hogy jelezzük az ISR számára a felhasználás megtörténtét.

Figyeljük meg, hogy a villogás (STATE_BLINK) állapotból a lenyomás (SW1_PRESSED) teljesülésekor lépünk tovább, a felengedésre vár (STATE_WAIT_FOR_RELEASE3) állapotban azonban nem az SW1_RELEASED feltételt vizsgáljuk, hanem az u8_pnrSW1 szemafor beállását, amelyet törölnünk is kell kilépés előtt!

9. lista: Részlet a chap09/ledsw1_timer.c programból - a főprogram listája
typedef enum  {
  STATE_RESET = 0,
  STATE_WAIT_FOR_PNR1,
  STATE_WAIT_FOR_PNR2,
  STATE_BLINK,
  STATE_WAIT_FOR_RELEASE3
} STATE;

int main (void) {
  STATE e_mystate;
  configBasic(HELLO_MSG);
  CONFIG_SW1();                //nyomógomb konfigurálása
  CONFIG_SW2();                //kapcsoló konfigurálása
  CONFIG_LED1();               //LED1 konfigurálása
  CONFIG_LED2();               //LED1 konfigurálása
  /** Timer3 konfigurálása ***/ 
  configTimer3();
  e_mystate = STATE_WAIT_FOR_PNR1;

  while (1) {
    printNewState(e_mystate);  //Nyomkövető üzenet kíírása minden állapotváltáskor
    LED2 = SW2 ^ OFF;          //LED2 = ON, ha SW2 nyitott)
    switch (e_mystate) {
      case STATE_WAIT_FOR_PNR1:
        LED1 = OFF;            //kikapcsolja a LED-et
        if (u8_pnrSW1) {
          u8_pnrSW1 = 0;       //törli a szemafort
          e_mystate = STATE_WAIT_FOR_PNR2;
        }
        break;
      case STATE_WAIT_FOR_PNR2:
        LED1 = ON;             //bekapcsolja a LED-et
        if (u8_pnrSW1) {
          u8_pnrSW1 = 0;       //törli a szemafort
          //SW2 állapota dönti el, hogy merre folytassuk
          if (SW2) e_mystate = STATE_BLINK;
          else e_mystate = STATE_WAIT_FOR_PNR1;
        }
        break;
      case STATE_BLINK:
        LED1 = !LED1;          //villogtatás a következő lenyomásig
        DELAY_MS(100);         //villogtatási késleltetés
        if (SW1_PRESSED()) e_mystate = STATE_WAIT_FOR_RELEASE3;
        break;
      case STATE_WAIT_FOR_RELEASE3:
        LED1 = ON;             //a LED felengedésig világít
        //az u8_pnrSW1 szemafort figyeljük a SW1_RELEASED helyett,
        //mert a szemafort törölnünk kell a felengedéskor történő
        //beállítása után!
        if (u8_pnrSW1) {
          u8_pnrSW1 = 0;
          e_mystate = STATE_WAIT_FOR_PNR1;
        }
        break;
      default:
        e_mystate = STATE_WAIT_FOR_PNR1;
    }                          //switch(e_mystate) vége
    doHeartbeat();             //jelzi, hogy fut a program
  }                            //a while(1) ciklus vége
}

A program nyomkövető kiírásai az alábbi ábrán láthatók. Figyeljük meg, hogy az SW2 bemenet állapotától függően két különböző ágon cirkulál a program! Az áttekinthetőség érdekében az ábrán utólag bejelöltük SW2 állapotait.

9. ábra: A chap09/ledsw_timer.c program nyomkövető kiírásai

A státuszgép interrupt szinten történő megvalósítása

Emlékeztetünk: a fenti példában főprogram egyszerűsítését az tette lehetővé, hogy a feladat egy részét az interrupt kiszolgáló eljárásba csoportosítottuk át. Ezt akár odáig is fokozhatjuk, hogy a teljes állapotgépet az ISR-ben valósítjuk meg. A tankönyvi mintapéldák között található chap09/ledsw_timer2.c program, amelyben az állapotgép megvalósítása most az ISR-ben található. Ebben a megvalósításban kénytelenek vagyunk visszatérni az eredeti állapotgép modellhez, mert most külön-külön foglalkoznunk kell a lenyomásra váró és felengedésre váró állapotokkal. A program releváns részletei az alábbi listákon láthatók:

10. lista: Részlet a chap09/ledsw1_timer2.c programból - Timer3 megszakításainak kiszolgálása
//-- Az állapotgép lehetséges állapotainak felsorolása
typedef enum  {
  STATE_RESET = 0,             //RESET utáni állapot
  STATE_WAIT_FOR_PRESS1,       //Első lenyomásra várunk
  STATE_WAIT_FOR_RELEASE1,     //Első felengedésre várunk
  STATE_WAIT_FOR_PRESS2,       //Második lenyomásra várunk
  STATE_WAIT_FOR_RELEASE2,     //Második felengedésre várunk
  STATE_BLINK,                 //Villogtatás
  STATE_WAIT_FOR_RELEASE3      //Harmadik felengedésre várunk
} STATE;

//-- Minden változót, amelyet a programmegszakítás módosít
//-- volatile típusúnak kell deklarálni!
volatile uint8_t u8_valueSW1  = 1;    //kezdetben magas szintről induljon
volatile uint8_t doBlink = 0;         //kezdetben ne villogjon
STATE e_mystate;

//-- Timer3 megszakításainak kiszolgálása
void _ISR _T3Interrupt (void) {
  u8_valueSW1 = SW1_RAW;       //mintavételezi a nyomógomb állapotát
  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()) {
        //eldönti, merre folytassuk
        if (SW2) e_mystate = STATE_BLINK;
        else e_mystate = STATE_WAIT_FOR_PRESS1;
      }
      break;
    case STATE_BLINK:
      doBlink = 1;
      if (SW1_PRESSED()) {
        doBlink = 0;
        e_mystate = STATE_WAIT_FOR_RELEASE3;
      }
      break;
    case STATE_WAIT_FOR_RELEASE3:
      LED1 = ON;   //bekapcsolt állapotban hagyja a LED-et
      if (SW1_RELEASED()) e_mystate = STATE_WAIT_FOR_PRESS1;
      break;
    default:
      e_mystate = STATE_WAIT_FOR_PRESS1;
  }
  _T3IF = 0;                 //törli a timer interrupt jelzőbitjét
}

A főprogramban nem sok tennivaló maradt. Az inicializálás után, a while(1) ciklusban csak a nyomkövető kiíratások elvégzése, és a LED-ek villogtatása (az életjelző LED villogtatását a do Heartbeat() meghívásával, az RB14-re kötött  LED1 villogtatását pedig a LED1 = !LED1;  DELAY_MS(100); utasításokkal végezzük).

11. lista: Részlet a chap09/ledsw1_timer2.c programból - a főprogram
int main (void) {

  configBasic(HELLO_MSG);
  /** I/O konfigurálás ****/
  CONFIG_SW1();                //nyomógomb konfigurálása
  CONFIG_SW2();                //kapcsoló konfigurálása
  CONFIG_LED1();               //LED1 konfigurálása
  CONFIG_LED2();               //LED2 konfigurálása
  /** Timer3 konfigurálása ***/ 
  configTimer3();
  e_mystate = STATE_WAIT_FOR_PRESS1;
  /* A while ciklusban csak a ledvillogtatás doBlink szemaforját figyeljük */
  while (1) {
    printNewState(e_mystate);  //nyomkövető kiírás minden állapotváltáskor
    if (doBlink) {
      LED1 = !LED1;
      DELAY_MS(100);
    }
    doHeartbeat();             //ledvillogtatással jelezzük, hogy fut a program
  }                            // while(1) ciklus vége
}

A főprogramban látható ledvillogtatást - legalábbis ebben a formában - természetesen nem tehetjük az interrupt kiszolgáló eljárásba, mert a használt szoftveres késleltetéssel megsértenénk azt az elvet, hogy az interrupt kiszolgálónak minél rövidebbnek kell lennie. Ráadásul a 100 ms-os késleltetés alatt több interrupt is keletkezne, amelyek kiszolgálatlanul maradnának (a mintavételezéshez ugyanis 15 ms-onként generálunk megszakítást).

Azt azonban megtehetnénk, hogy egy változóban számlálgatjuk az interruptokat, s ha minden hetedik interruptnál billentjük át a LED állapotát és nullázzuk a számláláshoz használt változót, akkor 7 * 15 ms = 105 ms-onként billenne ellenkező állapotba LED1. Ennek kidolgozását azonban az olvasóra hagyjuk.

Forgásjeladó (rotary encoder) mintavételezése

A digitális forgásérzékelőket (rotary encoder) többnyire a mechanikus tengelyek fogásírány- és szögelfordulás jeladójaként használják, de annak sincs akadálya, hogy egy műszer előlapján elhelyezett forgásjeladót kézzel tekergetve egy paramétert állítsunk be vele. A digitális forgásjeladók lehetnek abszolút, vagy növekményes (inkrementális) típusúak. Az abszolút típusúak az aktuális helyzet visszajelzésére is alkalmasak, a növekményesek csak a változásról értesítenek bennünket. A növekményes típusú forgásjeladók alapvetően mechanikai, optikai, vagy mágneses érzékelés elvén működnek. Ezek közül mi a csak a mechanikus elven működő típussal foglalkozunk. A mechanikus típusú forgásjeladó két kontaktust kapcsolgat úgy, hogy a kontaktusok kapcsolási fázisa 90 º-kal (azaz egy negyed periódussal) el van tolódva. Három kivezetése van: a kontaktusok közös pontját a földre kötjük, a másik két kivezetést pedig a mikrovezérlő két digitális bemenetére kötjük. Ezeket a bemeneteket az előző példákban használt nyomógombokhoz hasonlóan külső vagy belső felhúzással kell ellátni. Az alábbi ábrán bemutatjuk egy ilyen kétbites, Gray kódolású növekményes forgásjeladó jelalakját jobbra, illetve balra forgatás esetén.

10. ábra: Kétbites, Gray kódolású növekményes forgásjeladó jelalakja

Amint látjuk, balra forgatásnál a 00, 01, 11, 10, 00, ... szekvenciát kapjuk, a jobbra forgatásra pedig a 00, 10, 11, 01, 00, ... szekvencia jellemző. A gray kódnál a szomszédos kódok csak 1-1 bitben különböznek egymástól. A kódsorozat felismeréséhez az aktuális (éppen beolvasott) kódost mindig a legutoljára látott kóddal hasonlítjuk össze, így a forgásirány könnyen meghatározható.

Paraméter változtatása forgásérzékelővel

Az alábbi programban egy kétbites növekményes Gray kódú forgásjeladó (rotary encoder) kezelését mutatjuk be. A forgásjeladó segítségével egy paramétert változtatunk, melynek aktuális értékét minden léptetés után kiíratunk. A gyakorlati alkalmazásoknál célszerű a változtatható paramétert mindkét irányba szoftveresen határolni (az alábbi programban önkényesen választott határok 0 és 0x20), ami azt jelenti, hogy a maximális vagy minimális érték elérése után már hiába tekerjük tovább a forgásjeladót, az érték már nem változik, csak akkor, ha irányt váltunk.
   
Hardver követelmények:
Megjegyzés: A Microstick Plus kártya már tartalmazza a forgásérzékelőt és az USB-TTL átalakítót, de magas szintre kell állítanunk az RA3 kimenetet, ez biztosítja a külső felhúzások tápellátását!

A program bemutatása előtt külön ismertetjük a forgásjeladó állapotát kiértékelő és a forgásjeladóval szabályozott paraméter értékét aktualizáló függvényt, amit majd egy másik programban is használni fogunk. A processRotaryData() függvényt a Timer3 periodikus megszakításait kiszolgáló eljárásból akkor hívjuk meg, ha a forgásjeladó jeleit fogadó bemeneteken változást észlelünk az egymást követő mintavételezések során. A függvény feladata az, hogy a bemenetek aktuális állapotából, és az előző mintavételezésnél tapasztalt állapotokból megállapítása, hogy melyik irányba történ elfordulás. A függvény további feladata az, hogy a forgásjeladóvel vezérelt változó értékét aktualizálja, figyelembe véve az elfordulást, és a paraméterre előírt korlátokat. A függvény visszatérési értéke 0 lesz, ha érvényes állapotváltozást detektált, s 1 lesz, ha az egymást követő állapotok nem illeszkednek be a 10. ábrán bemutatott Gray kódok sorozatába.

A függvény paraméterei az alábbiak:
A delta nevű változó a növekmény értéke, ami -1, 0, vagy +1 lehet. A delta érték meghatározása a 10. ábrán bemutatott szekvenciák felismerésén alapul. A +1 érték a balra (pozitív forgásirány), a -1 érték a jobbra (negatív forgásirány) történő elfordulást jelzi. A 0 érték azt jelenti, hogy az egymást követő állapotok nem illeszkednek be a 10. ábrán bemutatott Gray kódok sorozatába. Ilyen érvénytelen eset észlelése esetén a főprogram "Rotary state error" hibaüzenetet ír ki.

12. lista: A forgásjeladó kezelését bemutató chap09/rot_enc.c program listája
//-- Ez a függvény aktualizálja a forgásérzékelővel vezérelt
//-- változó értékét, de lehatárolja azt 0 és max közé
uint8_t processRotaryData(volatile uint8_t u8_curr, volatile uint8_t u8_last,
                          volatile uint8_t *cntr, volatile uint8_t max) {
  int8_t delta = 0;
  // A jobb megértéshez az állapotok a Gray kód sorrendjében vannak felsorolva
  switch (u8_curr) {
    case 0:
      if (u8_last == 1) delta = 1;
      else if (u8_last == 2) delta = -1;
      break;
    case 1:
      if (u8_last == 3) delta = 1;
      else if (u8_last == 0) delta = -1;
      break;
    case 3:
      if (u8_last == 2) delta = 1;
      else if (u8_last == 1) delta = -1;
      break;
    case 2:
      if (u8_last == 0) delta = 1;
      else if (u8_last == 3) delta = -1;
      break;
    default:
      break;
  }
  if (delta == 0) return(1); //hiba, illegális állapot
  //határolás és aktualizálás.
  if (( *cntr == 0 && delta == -1)
      || (*cntr == max && delta == 1)) return 0; //határolás
  (*cntr) = (*cntr) + delta;
  return 0;
}

A hibavizsgálat és a határolás a függvény listájának végén található. Hiba esetén '1' lesz a visszatérési érték. A határolás úgy történik, hogy ha az alsó határon vagyunk és csökkentés következne, vagy a ha felső határnál újabb növelés következne, akkor nem változtatjuk meg a szabályozott paraméter értékét, hanem egyszerűen kilépünk.

A forgásjeladó kezelését bemutató program többi része a 13. listán látható. A program főbb részei:
13. lista: A forgásjeladó kezelését bemutató chap09/rot_enc.c program listája (részlet)  
#include "pic24_all.h"


//-- A forgásérzékelő RB6 és RB7-re van kötve
#define ROT1_RAW _RB7
#define ROT0_RAW _RB6
#define GET_ROT_STATE() ((ROT1_RAW << 1) | ROT0_RAW)

/// A forgásérzékelő konfigurálása
inline void configRotaryEncoder() {
  CONFIG_RB7_AS_DIG_INPUT();    //RB7 legyen digitális bemenet
  CONFIG_RB6_AS_DIG_INPUT();    //RB7 legyen digitális bemenet
//-- Az alábbi két utasítás csak a Microstick Plus kártya esetén szükséges,
//-- a kiegészítő áramkörök (pl. külső felhúzás) tápfeszültségének bekapcsolásához
#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)
#else
//-- Az alapértelmezett kísérleti áramkörök esetén belső felhúzást használunk
  ENABLE_RB7_PULLUP();          //belső felhúzás engedélyezése
  ENABLE_RB6_PULLUP();          //enable the pullup
  DELAY_US(1);                  //wait for pullups to settle
#endif
}

#define ROT_MAX  32             //önkényesen választott határérték

volatile uint8_t u8_valueROT = 0;
volatile uint8_t u8_lastvalueROT = 0;
volatile uint8_t u8_errROT = 0;
volatile uint8_t u8_cntrROT = 0;

//Interrupt kiszolgáló eljárás Timer3 számára
void _ISRFAST _T3Interrupt (void) {
  u8_valueROT = GET_ROT_STATE();// 0 & 3 közötti szám
  if (u8_lastvalueROT != u8_valueROT) {
    u8_errROT = processRotaryData(u8_valueROT, u8_lastvalueROT, &u8_cntrROT, ROT_MAX);
    u8_lastvalueROT = u8_valueROT;
  }
  _T3IF = 0;                    //megszakításjelző bit törlése
}

//-- ISR_PERIOD eredeti értéke 15 volt, de a Microstick Plus
//-- kártya forgásérzékelője akkor sokat tévesztett. 5-10 ms beállítással
//-- viszont sokkal stabilabb a működése.
#define ISR_PERIOD     5       // megszakítás gyakoriság ms-ban
void  configTimer3(void) {
  //Timer2,3 szétválasztása
  T2CONbits.T32 = 0;     // 32-bites mód kikapcsolása
  //T3CON konfigurálása öndokumentáló módon
  T3CON = T3_OFF | T3_IDLE_CON | T3_GATE_OFF
          | T3_SOURCE_INT
          | T3_PS_1_64 ;  //results in T3CON= 0x0020
  PR3 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T3CONbits)) - 1;
  TMR3  = 0;                    //TIMER3 számláló törlése
  _T3IF = 0;                    //megszakításjelző bit törlése
  _T3IP = 1;                    //prioritás beállítása
  _T3IE = 1;                    //megszakítás engedélyezése
  T3CONbits.TON = 1;            //Timer3 bekapcsolása
}

int main (void) {
  uint8_t u8_lastCnt;
  configBasic(HELLO_MSG);
  configRotaryEncoder();
  u8_valueROT = GET_ROT_STATE();
  u8_lastvalueROT = u8_valueROT;
  u8_lastCnt = u8_cntrROT;
  configTimer3();
  while (1) {
    if (u8_lastCnt != u8_cntrROT) {
      u8_lastCnt = u8_cntrROT;
      outUint8(u8_lastCnt);
      outString("\n");
      if  (u8_errROT) {
        outString("Rotary state error\n");
        u8_errROT = 0;
      }
    }
    doHeartbeat();              //jelzi, hogy fut a program
  }                             // end while (1)
}
A program egy futtatásának eredménye az alábbi ábrán látható (itt Microstick Plus kártyát használtunk).

11. ábra: A rot_enc.c  program futtatásának eredménye


A forgásérzékelő állapotainak nyomon követése

Ebben a programban azt mutatjuk be, hogy a forgásérzékelő állapotait hogyan követhetjük nyomon egy, a megszakítási szinten kezelt buffer segítségével, amelyben az egymást követő állapotokat rögzítjük. A program nagy mértékben megegyezik az előző példával (a rot_enc.c mintapéldával), csupán kiegészítettük azt.

Hardver követelmények:
Megjegyzés: A Microstick Plus kártya már tartalmazza a forgásérzékelőt és az USB-TTL átalakítót, de magas szintre kell állítanunk az RA3 kimenetet, ez biztosítja a külső felhúzások tápellátását!

A hardverfüggő definíciók megegyeznek az előző programban használtakkal. A 12. listán bemutatott processRotaryData() függvényt használjuk most is a forgásjeladó állapotainak kiértékelésére, csupán annyival bővítettük, hogy ha nyomkövetés van előírva (az u8_startTrace változó értéke nullától különbözik) és még van hely a nyomkövetéshez használt au8_tbuff[] bufferben, akkor eltároljuk az aktuális állapotot.

Timer3 konfigurálása és megszakításainak kiszolgálása megegyezik az előző mintapéldában látottakkal. A főprogramban most várunk arra, hogy beteljen a nyomkövető buffer, majd kiíratjuk az adatsort.

13. lista: A forgásjeladó kezelését bemutató chap09/rot_enc.c program listája (részlet)  
#include "pic24_all.h"

#define TMAX 16                  //A nyomkövetéhez használt buffer mérete
volatile uint8_t au8_tbuff[TMAX];
volatile uint8_t u8_tcnt = 0;
volatile uint8_t u8_startTrace = 0;

//-- A forgásérzékelő RB6 és RB7-re van kötve
#define ROT1_RAW _RB7
#define ROT0_RAW _RB6
#define GET_ROT_STATE() ((ROT1_RAW << 1) | ROT0_RAW)

/// A forgásérzékelő konfigurálása
inline void configRotaryEncoder() {
  CONFIG_RB7_AS_DIG_INPUT();     //RB7 legyen digitális bemenet
  CONFIG_RB6_AS_DIG_INPUT();     //RB7 legyen digitális bemenet
//-- Az alábbi két utasítás csak a Microstick Plus kártya esetén szükséges,
//-- a kiegészítő áramkörök (pl. külső felhúzás) tápfeszültségének bekapcsolásához
#if (HARDWARE_PLATFORM == MICROSTICK_PLUS)
  CONFIG_RA3_AS_DIG_OUTPUT();   //Kimenetnek állítjuk az RA3 lábat
  _RA3 = 1;                     //és magas szintre álltjuk (kiegészítő áramkörök bekapcsolása)
#else
//-- Az alapértelmezett kísérleti áramkörök esetén belső felhúzást használunk
  ENABLE_RB7_PULLUP();          //belső felhúzás engedélyezése
  ENABLE_RB6_PULLUP();          //enable the pullup
  DELAY_US(1);                  //wait for pullups to settle
#endif
}

/* A forgásjeladó új állapotának kiértékelése */
uint8_t processRotaryData(volatile uint8_t u8_curr, volatile uint8_t u8_last,
                          volatile uint8_t *cntr, volatile uint8_t max) {
  int8_t delta = 0;
//-- Ha még van szabad hely a bufferben, akkor eltároljuk az állapotot
  if (u8_startTrace && (u8_tcnt != TMAX)) {
    au8_tbuff[u8_tcnt] = u8_curr;
    u8_tcnt++;
  }
  // A jobb megértéshez az állapotok a Gray kód sorrendjében vannak felsorolva
  switch (u8_curr) {
    case 0:
      if (u8_last == 1) delta = 1;
      else if (u8_last == 2) delta = -1;
      break;
    case 1:
      if (u8_last == 3) delta = 1;
      else if (u8_last == 0) delta = -1;
      break;
    case 3:
      if (u8_last == 2) delta = 1;
      else if (u8_last == 1) delta = -1;
      break;
    case 2:
      if (u8_last == 0) delta = 1;
      else if (u8_last == 3) delta = -1;
      break;
    default:
      break;
  }
  if (delta == 0) return(1); //hiba, illegális állapot
  //határolás és aktualizálás.
  if (( *cntr == 0 && delta == -1)
      || (*cntr == max && delta == 1)) return 0; //határolás
  (*cntr) = (*cntr) + delta;
  return 0;
}

#define ROT_MAX  32             //önkényesen választott határérték

volatile uint8_t u8_valueROT = 0;
volatile uint8_t u8_lastvalueROT = 0;
volatile uint8_t u8_errROT = 0;
volatile uint8_t u8_cntrROT = 0;

//Interrupt kiszolgáló eljárás Timer3 számára
void _ISRFAST _T3Interrupt (void) {
  u8_valueROT = GET_ROT_STATE();// 0 & 3 közötti szám
  if (u8_lastvalueROT != u8_valueROT) {
    u8_errROT = processRotaryData(u8_valueROT, u8_lastvalueROT, &u8_cntrROT, ROT_MAX);
    u8_lastvalueROT = u8_valueROT;
  }
  _T3IF = 0;                    //megszakításjelző bit törlése
}

//-- ISR_PERIOD eredeti értéke 15 volt, de a Microstick Plus
//-- kártya forgásérzékelője akkor sokat tévesztett. 5-10 ms beállítással
//-- viszont sokkal stabilabb a működése.
#define ISR_PERIOD     5        // megszakítás gyakoriság ms-ban
void  configTimer3(void) {
  //Timer2,3 szétválasztása
  T2CONbits.T32 = 0;            // 32-bites mód kikapcsolása
  //T3CON konfigurálása öndokumentáló módon
  T3CON = T3_OFF | T3_IDLE_CON | T3_GATE_OFF
          | T3_SOURCE_INT
          | T3_PS_1_64 ;
  PR3 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T3CONbits)) - 1;
  TMR3  = 0;                    //TIMER3 számláló törlése
  _T3IF = 0;                    //megszakításjelző bit törlése
  _T3IP = 1;                    //prioritás beállítása
  _T3IE = 1;                    //megszakítás engedélyezése
  T3CONbits.TON = 1;            //Timer3 bekapcsolása
}

int main (void) {
  uint8_t u8_i;
  configBasic(HELLO_MSG);
  configRotaryEncoder();        //a forgásérzékelő beállítása
  u8_valueROT = GET_ROT_STATE();
  u8_lastvalueROT = u8_valueROT;
  configTimer3();               //periodikus megszakítások beállítása
  while (1) {
    u8_startTrace = 1;
    if (u8_tcnt == TMAX) {
      outString("-------------\n");
      u8_startTrace = 0;
      for (u8_i = 0; u8_i < TMAX; u8_i++) {
        outUint8(au8_tbuff[u8_i]);
        outString("\n");
      }
      u8_tcnt = 0;
    }
    doHeartbeat();              //jelzi, hogy fut a program
  }                             // end while (1)
}
A program két futtatásának eredménye az alábbi ábrán látható (a könnyebb összehasonlíthatóság érdekében a két futási eredményt összemontíroztuk). A számoszlopokban a 10. ábrán bemutatott kódsorozatokat ismerhetjük fel. Az ábrán csak egy-egy buffernyi kiírás látható. Hosszabb futtatás esetén az egyes buffer kiírásokat szaggatott vonal választja el.

Megjegyzés: A program elindulás után látszólag nem csinál semmit. Ez az adatgyűjtés időszaka. A forgásérzékelőt addig kell tekergetni, amíg a buffer be nem telik, s akkor a képernyőn megjelenik egy buffernyi kiírás a programban látható beállítással 16 db. adat!

11. ábra: A rot_enc_trace.c  program futtatásának eredménye

Billentyű mátrix kezelése

Beágyazott rendszerek adatbeviteli eszközéül gyakran használnak a nyomógombos telefon tárcsázójára emlékeztető billentyű mátrixot (például riasztóknál, számkódos záraknál, mobiltelefonokban, távíránítókban). A billentyűmátrixokban a nyomógombok sorokba és oszlopokba vannak rendezve, s minden lenyomáskor a megnyomott gomb a hozzá csatlakozó oszlop és sor sínvezetékek között létesít kontaktust.

A működés elve

A következő programban egy elterjedten használt 4x3-as billentyű mátrixot próbálunk ki, s a lenyomott billentyű kódját kiírjuk a számítógép képernyőjére. A billentyűzet fényképe és a programban használt bekötés az alábbi ábrán látható.
 

12. ábra: 4x3-as billentyűzet és a példaprogramban használt bekötés

Megjegyzések

Hardver követelmények:
A program forráskódja az alábbi listán látható. Vegyük észre, hogy a C0...C3 oszlopvezetékekhez tartozó RB6...RB9 kivezetéseket bemenetnek definiáljuk, s a belső felhúzást bekapcsoljuk. Az R0...R2 sorvezetékekhez csatlakozó kivezetéseket viszont kimenetnek definiáljuk (ennek megfelelően  a _LATB5..._LATB3 szimbólumokat rendeljük az R0...R2 makrókhoz), amelyeket alaphelyzetben alacsony szintre húzunk. Így könnyen megállapíthatjuk, hogy a C0...C3 oszlopvezetékek valamelyike le van-e húzva (lásd: KEY_PRESSED() makró), s ha igen, akkor melyik (lásd: doKeyScan() első felét, u8_col meghatározását).  

A lenyomott gombhoz tartozó sor meghatározása egy picivel bonyolultabb: a sorvezetékeket egyenként kell lehúzni (a többit pedig felhúzni), s közben vizsgálni, hogy melyik sorvezeték lehúzása eredményez alacsony szintet a C0...C3 bemenetek valamelyikén.
14. lista: A 4x3 billentyű mátrix kezelését bemutató chap09/keypad.c program listája
#include "pic24_all.h"

//-- Az oszlopvezetékekhez tartozó bemenetek definiálása 
#define C0 _RB9
#define C1 _RB8
#define C2 _RB7
#define C3 _RB6
//-- Az oszlopvezetékekhez tartozó bemenetek beállítása 
static inline void CONFIG_COLUMN() {
  CONFIG_RB9_AS_DIG_INPUT();
  ENABLE_RB9_PULLUP();
  CONFIG_RB8_AS_DIG_INPUT();
  ENABLE_RB8_PULLUP();
  CONFIG_RB7_AS_DIG_INPUT();
  ENABLE_RB7_PULLUP();
  CONFIG_RB6_AS_DIG_INPUT();
  ENABLE_RB6_PULLUP();
}
//-- A sorvezetékekhez tartozó kimenetek definiálása
#define R0 _LATB5
#define R1 _LATB4
#define R2 _LATB3

//-- A sorvezetékekhez tartozó kimenetek beállítása
#define CONFIG_R0_DIG_OUTPUT() CONFIG_RB5_AS_DIG_OUTPUT()
#define CONFIG_R1_DIG_OUTPUT() CONFIG_RB4_AS_DIG_OUTPUT()
#define CONFIG_R2_DIG_OUTPUT() CONFIG_RB3_AS_DIG_OUTPUT()

void CONFIG_ROW() {
  CONFIG_R0_DIG_OUTPUT();
  CONFIG_R1_DIG_OUTPUT();
  CONFIG_R2_DIG_OUTPUT();
}
//-- Minden sorvezeték lehúzása
static inline void DRIVE_ROW_LOW() {
  R0 = 0;
  R1 = 0;
  R2 = 0;
}
//-- Minden sorvezeték magas szintre állítása
static inline void DRIVE_ROW_HIGH() {
  R0 = 1;
  R1 = 1;
  R2 = 1;
}
//-- A billentyű mátrixhoz kapcsolódó ki- és bemenetek inicializálása
void configKeypad(void) {
  CONFIG_ROW();
  DRIVE_ROW_LOW();
  CONFIG_COLUMN();
  DELAY_US(1);                 //vár a belső felhúzások beállására
}

/** \brief Egy sorvezeték alacsony szintre húzása
 *
 * Egy sorvezeték alacsony szintre húzása, a többi sorvezetéket magas
 * szintre állítjuk.
 * \param u8_x a lehúzni kívánt sorvezeték sorszáma (0..2)
 */
void setOneRowLow(uint8_t u8_x) {
  switch (u8_x) {
    case 0:
      R0 = 0;
      R1 = 1;
      R2 = 1;
      break;
    case 1:
      R0 = 1;
      R1 = 0;
      R2 = 1;
      break;
    default:
      R0 = 1;
      R1 = 1;
      R2 = 0;
      break;
  }
}
#define NUM_ROWS 3             //Sorok száma
#define NUM_COLS 4             //Oszlopok száma
//-- A sorokba és oszlopokba rendezett gombokhoz tartozó karakterkódok táblázata
const uint8_t au8_keyTable[NUM_ROWS][NUM_COLS] = { {'1', '4', '7', '*'},
  {'2', '5', '8', '0'},
  {'3', '6', '9', '#'}
};

#define KEY_PRESSED() (!C0 || !C1 || !C2 || !C3) //lenyomva: ha bármelyik alacsony
#define KEY_RELEASED() (C0 && C1 && C2 && C3)    //felengedve: ha mind magas

/* \brief A lenyomott billentyű azonosítása
 *
 * Azonosítja a lenyomott billentyűt, s a hozzá tartozó karakterkóddal tér vissza.
 * Sikertelen azonosítás esetén az 'E' hibakóddal tér vissza.
 */
uint8_t doKeyScan(void) {
  uint8_t u8_row, u8_col;
//-- A lenyomott gombhoz tartozó oszlop meghatározása
  if (!C0) u8_col = 0;
  else if (!C1) u8_col = 1;
  else if (!C2) u8_col = 2;
  else if (!C3) u8_col = 3;
  else return('E');            //hiba: sikertelen azonosítás
//-- A lenyomott gombhoz tartozó sor meghatározása
  for (u8_row = 0; u8_row < NUM_ROWS; u8_row++) {
    setOneRowLow(u8_row);      //egy sorvezeték lehúzása
    if (KEY_PRESSED()) {
      DRIVE_ROW_LOW();         //minden sor lehúzása
      return(au8_keyTable[u8_row][u8_col]);
    }
  }
  DRIVE_ROW_LOW();             //minden sor lehúzása
  return('E');                 //hiba: sikertelen azonosítás
}

//-- Az állapotgép lehetséges állapotainak felsorolása ----
typedef enum  {
  STATE_WAIT_FOR_PRESS = 0,    //(első) lenyomásra vár
  STATE_WAIT_FOR_PRESS2,       //második lenyomásra vár
  STATE_WAIT_FOR_RELEASE,      //felengedésre vár
} ISRSTATE;

ISRSTATE e_isrState = STATE_WAIT_FOR_PRESS;  //állapotjelző
volatile uint8_t u8_newKey = 0;              //vett karakterkód

//-- Timer3 megszakításainak kiszolgálása -----------------
void _ISR _T3Interrupt (void) {
  switch (e_isrState) {
    case STATE_WAIT_FOR_PRESS:
      if (KEY_PRESSED() && (u8_newKey == 0)) {
//-- megbizonyosodik róla, hogy a lenyomás legalább két, egymást követő megszakítási periódusig tart
        e_isrState = STATE_WAIT_FOR_PRESS2;
      }
      break;
    case STATE_WAIT_FOR_PRESS2:
      if (KEY_PRESSED()) {
        //egy gombnyomás kiolvasásra kész
        u8_newKey = doKeyScan();
        e_isrState = STATE_WAIT_FOR_RELEASE;
      } else e_isrState = STATE_WAIT_FOR_PRESS;
      break;

    case STATE_WAIT_FOR_RELEASE:
      //billentyű felengedve
      if (KEY_RELEASED()) {
        e_isrState = STATE_WAIT_FOR_PRESS;
      }
      break;
    default:
      e_isrState = STATE_WAIT_FOR_PRESS;
      break;
  }
  _T3IF = 0;                   //törli a megszakításjelző bitet
}

#define ISR_PERIOD     15      //megszakítási periódus ms-okban

//-- Timer3 konfigurálása -----------------------------------
void  configTimer3(void) {
  //Timer2,3 szétválasztása
  T2CONbits.T32 = 0;           // 32-bites mód kikapcsolása
  //T3CON konfigurálása öndokumentáló módon
  //végeredményben T3CON = 0x0020
  T3CON = T3_OFF | T3_IDLE_CON | T3_GATE_OFF
          | T3_SOURCE_INT
          | T3_PS_1_64 ;
  PR3 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T3CONbits)) - 1;
  TMR3  = 0;                   //TIMER3 számláló törlése
  _T3IF = 0;                   //megszakításjelző bit törlése
  _T3IP = 1;                   //prioritás beállítása
  _T3IE = 1;                   //megszakítás engedélyezése
  T3CONbits.TON = 1;           //Timer3 bekapcsolása
}

int main (void) {
  configBasic(HELLO_MSG);
  configKeypad();
  configTimer3();
  while (1) {
    if (u8_newKey) {           //ha történt új lenyomás, akkor
      outChar(u8_newKey);      //kiíratjuk a lenyomott gombhoz tartozó karaktert
      u8_newKey = 0;           //töröljük, hogy ne írjuk ki mégegyszer
    }
    doHeartbeat();             //jelzi, hogy fut a program
  }
}

Timer3 inicializálása megegyezik a korábbi példákkal. A megszakítást kiszolgáló eljárásban egy egyszerű állapotgépet használunk a gomblenyomás figyelésére. Ennek segítségével figyelünk arra, hogy csak azt tekintsük érvényes gomblenyomásnak, ami két, egymást követő megszakítás alkalmával detektálható, azaz legalább 15 ms-ig tart. Ennek segítségével a nyomógomb pergését és az impulzusszerű zajokat is kiszűrjük.

Megjegyzés: A program nem foglalkozik azzal az esettel, ha egyidejűleg egynél több gomb is le van nyomva. Ilyenkor az a legkisebb sorszámú sor- és oszlop index kerül rögzítésre, amelyikben van lenyomott gomb. Az így kapott sor- és oszlop index azonban nem feltétlenül lesz összetartozó érték (tehát lehet, hogy egy olyan gomb kódját kapjuk eredményül, amit le sem nyomtunk), mivel az oszlop- és sor index egymástól függetlenül kerül meghatározásra. Jegyezzük tehát meg, hogy ez a program csak akkor működik helyesen, ha egyszerre csak egy gombot nyomunk le!

Aluláteresztő RC szűrő próbája

Az alábbi egyszerű példaprogram az RB2 kimeneten impulzussorozatot kelt, melyet egy aluláteresztő RC szűrőn keresztül vezetünk vissza az RB3 bemenetre. A főprogram vezérli a kimenetet, s ha a bemeneten szintváltozást észlel, akkor kiír egy "*" karaktert. A programban használt időzítés (TPW), illetve az aluláteresztő RC  szűrő paraméterei szabják meg, hogy átjut-e a jel a bemenetre, vagy sem.

Hardver követelmények:
15. lista: Az aluláteresztő RC szűrőt tesztelő chap09/filt_test.c program listája
#include "pic24_all.h"

#define CONFIG_TOUT() CONFIG_RB2_AS_DIG_OUTPUT()
#define TOUT       _LATB2      //a kimenet

#define TIN          _RB3      //a vizsgálandó bemenet
#define CONFIG_TIN()  CONFIG_RB3_AS_DIG_INPUT();

#define TPW  1                 //a kimenő impulzusok szélessége (ms-ban)

int main (void) {
  uint8_t u8_oldvalueTIN;
  configBasic(HELLO_MSG);
  TOUT = 1;                    //a TOUT kimenet vezérli a TIN bemenetet
  CONFIG_TIN();
  CONFIG_TOUT();
  DELAY_MS(10);                //vár a kimeneti állapot stabilizálódására
  u8_oldvalueTIN = TIN;
  while (1) {
    TOUT = !TOUT;
    DELAY_MS(TPW);
    if (u8_oldvalueTIN != TIN) {
      u8_oldvalueTIN = TIN;
      outString("*");
    }
  }
}

Szoftveres digitális szűrő

Ez a program szoftveres digitális szűrést valósít meg, s kiszűri az adott időtartamnál rövidebb bejövő impulzusokat. A feladat szoftveres pergésmentesítésnek is tekinthető. A programban szereplő paraméterekkel 20 ms hosszúságú (lásd TPW) impulzusokat keltünk az RB9 kimeneten, amelyeket az RB8 bemenetre vezetünk, s szoftveresen szűrünk. A szűrőn átjutó impulsusoknak legalább 15 ms időtartamúaknak (lásd MIN_STABLE) kell lenniük.

Hardver követelmények:
16. lista: A szoftveres digitális szűrőt megvalósító chap09/softfilt_test.c program listája
#include "pic24_all.h"

#define CONFIG_TOUT() CONFIG_RB9_AS_DIG_OUTPUT()
#define TOUT         _LATB9    //a kimenet
#define TIN_RAW        _RB8    //a mintavételezendő bemenet
#define CONFIG_TIN()  CONFIG_RB8_AS_DIG_INPUT();

#define ISR_PERIOD        1    // megszakítási időköz (ms-ban)
#define MIN_STABLE       15    // minimális impulzushozz (ms-ban)
#define MIN_STABLECOUNT  MIN_STABLE/ISR_PERIOD

uint16_t u16_stableCountTIN = 0;
uint8_t u8_rawTIN = 0;         // a nyers bemeneti állapot
uint8_t u8_oldrawTIN = 0;      // az előző mintavételezett állapot

//A pergésmentesített állapot, amit a megszakításban állítunk be, de
//a megszakításon kívül használjuk fel, ezért volatile-nak kell deklarálni.
volatile uint8_t u8_valueTIN = 0;

//-- Timer3 megszakításainak kiszolgálása
void _ISRFAST _T3Interrupt (void) {
  u8_rawTIN = TIN_RAW;         //mintavételezi a bemenetet
  if (u8_rawTIN != u8_oldrawTIN) {
    //megváltozott a bemenet, töröljük a számlálót
    u16_stableCountTIN = 0;
    u8_oldrawTIN = u8_rawTIN;
  } else {
    u16_stableCountTIN++;
    if (u16_stableCountTIN >= MIN_STABLECOUNT) {
      //az új állapot előállt!
      u8_valueTIN = u8_rawTIN;
    }
  }
  _T3IF = 0;                   //Timer3 megszakításjelző bitjének törlése
}



void  configTimer3(void) {
  //Timer2,3 szétválasztása
  T2CONbits.T32 = 0;           // 32-bites mód kikapcsolása
  //T3CON konfigurálása öndokumentáló módon
  T3CON = T3_OFF |T3_IDLE_CON | T3_GATE_OFF
          | T3_SOURCE_INT
          | T3_PS_1_1 ;        //Az 1 ms-os periódushoz 1:1 leosztást kell!
  PR3 = msToU16Ticks (ISR_PERIOD, getTimerPrescale(T3CONbits)) - 1;
  TMR3  = 0;                   //TIMER3 számláló törlése
  _T3IF = 0;                   //megszakításjelző bit törlése
  _T3IP = 1;                   //prioritás beállítása
  _T3IE = 1;                   //megszakítás engedélyezése
  T3CONbits.TON = 1;           //Timer3 bekapcsolása
}


uint8_t u8_oldvalueTIN = 0;

#define TPW  20                // TOUT kimenő impulzus szélessége

int main (void) {
  configBasic(HELLO_MSG);
  TOUT = 0;                    // TOUT vezérli a TIN bemenetet
  CONFIG_TIN();
  CONFIG_TOUT();
  configTimer3();
  while (1) {
    TOUT = !TOUT;
    DELAY_MS(TPW);
    if (u8_valueTIN != u8_oldvalueTIN) {
      u8_oldvalueTIN = u8_valueTIN;
      outString("*");
    }
  }
}