Az előző részben megismerkedtünk még több regiszterrel, ezek közül is a 16-bitesekkel. Most viszont megnézzük végre, hogyan használhatók az AVR portjai.
Minden AVR különböző mennyiségű GPIO-Regisztert kezel (GPIO – General Purpose Input/Output). Ezek a regiszterek azért szükségesek mert:
- Meghatározzák a kontroller melyik kivezetése legyen kimenet, melyik legyen bemenet.
- Megadják a kimenetek állapotát.
- Megállapítják a benetek állapotát
A GPIO-ba tehát digitális állapotok íródnak be, illetve olvasunk abból. Például egy kimeneten feszültség jelenik meg, vagy a bemenet jelei tárolódnak benne. Az adatlap Electrical Charateristics/DC Characteristics részében találhatóak a feszültség értékek (V_OL, V_OH a kimeneteké, V_IL, V_IH a bemeneteké).
Az analóg bemeneti értékek feldolgozását, és analóg jelek előállítását egy későbbi fejezetben vesézzük ki. Az AVR minden portját regiszterek vezérlik. Minden portnak 3 regisztere van:
DDRx:
A PORTx adatirány regisztere. Az x helyére a port betűjele kerül: A,B,C,D stb ( az alkalmazott kontroller portjainak számától függően).
PINx:
A PORTx bemeneti címe. A PINx –ben lévő bitek írják le a porton bementként definiált lábak pillanatnyi állapotát. Ha a bit értéke 1, magas, ha 0, akkor alacsony szinten van a bemenet.
PORTx:
A PORTx adatregisztere. Ez a regiszter a port kimeneteinek vezérléséhez szükséges. A DDRx –ben bemenetként beállított lábakhoz a PORTx a belső felhúzóellenállást aktiválja, vagy kikapcsolja azt (1 = aktív).
Adatirány meghatározása
Először az alkalmazott lábak adatirányát kell meghatározni. Ahhoz hogy ezt megtegyük, a port adatirányregiszterét kell leírnunk. A porton kimenetként használni kívánt lábakat úgy állíthatjuk be, hogy a hozzájuk tartozó helyiértékű bitnek értéket adunk (1). Ha egy lábat bemenetként kívánunk beállítani, a hozzátartozó helyiértékű bit értékét törölni kell (0).
+---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | BIT ÉRTÉKE +---+---+---+---+---+---+---+---+ | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT HELYIÉRTÉKE +---+---+---+---+---+---+---+---+
Tegyük fel, hogy a B porton a 0-tól 4-ig számozott lábakat kimenetkét, a megmaradt 5-től 7-ig számozott lábakat bemenetként akarjuk beállítani. Ehhez a B port adatirányregiszteréhez a következő bitkonfigurációt kell hozzárendelni:
#include <avr/io.h>
int main() { // A 0,1,2,3 és 4-ik biteknek értéket adunk // Binárisan 00011111 = Hexadecimálisan 1F DDRB = 0x1F; /* közvetlen hozzárendelés - átláthatatlan */ // Átláthatóbb alternatíva – Bináris írásmód DDRB = 0b00011111; /* közvetlen hozzárendelés - átlátható */ /* Részletes írásmód: azonosítható funkció, többet kell gépelni, de átláthatóbb */ DDRB = (1 << DDB0) | (1 << DDB1) | (1 << DDB2) | (1 << DDB3) | (1<< DDB4); }
Az 5-től 7ig számozott lábak bemenetként működnek. További példák:
// A PORTB minden lába kimenet: DDRB = 0xff; // A nulladik láb újra bemenet, a többi változatlan marad: DDRB &= ~(1<<DDB0); // a harmadik, és negyedik láb bemenet lesz, a többi változatlan marad: DDRB &= ~((1<<DDB3) | (1<<DDB4)); // a nulladik és a harmadik láb ismét kimenet lesz, a többi változatlan marad: DDRB |= (1<<DDB0) | (1<<DDB3); // Minden láb bemenet: DDRB = 0x00;
Az I/O regiszter számára előre definiált Bitek
A Bitek ( Például: PCx, PINCx, DDCx ) az avr-libc io*.h fáljában vannak definiálva, ezek csupán a jobb olvashatóságot szolgálják. Ezeket a definíciókat nem kötelező használni, lehet egyszerűen PAx, PBx, PCx, stb. Is alkalmazni, akkor is ha a DDRx vagy a PINx regiszterekhez akarunk hozzáférni. A fordító számára a (1<<PC7), (1<<DDC7), (1<<PINC7) kifejezések az (1<<7) kifejezéssel azonosak. (Pontosabban az előfeldolgozó a (1<<PC7) kifejezést kicseréli (1<<7) –el )
Itt látható egy részlet az ATmega32 C portjáról, az iom32.h fájlból:
/* PORTC */ #define PC7 7 #define PC6 6 #define PC5 5 #define PC4 4 #define PC3 3 #define PC2 2 #define PC1 1 #define PC0 0 /* DDRC */ #define DDC7 7 #define DDC6 6 #define DDC5 5 #define DDC4 4 #define DDC3 3 #define DDC2 2 #define DDC1 1 #define DDC0 0 /* PINC */ #define PINC7 7 #define PINC6 6 #define PINC5 5 #define PINC4 4 #define PINC3 3 #define PINC2 2 #define PINC1 1 #define PINC0 0
Kimenetek
Ha egy kimenetként definiált lábat logikai 1 szintre akarunk állítani, a portregiszterben hozzátartozó helyiértékű bitnek adunk értéket.
#include <avr/io.h> ... PORTB = 0x04; /* vagy PORTB = (1 << PB2) */ // Áttekinthetőbb alternatívírásmód PORTB = 0b0000100; /* Közvetlen hozzárendelés – átlátható */
A fenti kódrészletben található utasításokkal a PB2 kimenetnek adunk értéket. ( Figyeljünk arra, hogy a Bitek helyiértékei 0-tól kezdőnek, és nem 1-től ) A hozzárendelő operátor = használatával minden egyes bit a porton egyszerre kap értéket. Szükség lehet olykor olyan műveletre is, amikor csak egy vagy néhány láb állapotát szeretnénk megváltoztatni. Ilyenkor a változtatni kívánt láb portjának minden bit-jét először be kell olvasni – tehát meg kell állapítani hogy van e értéke vagy sem – és aztán csak a kívánt biteket megváltoztatni. Tehát ha csak például a B port 3. lábát ( 2. Helyiértékű bit) akarjuk HIGH szintre állítani, és a többit változatlanul hagyni, akkor az alábbi formát alkalmazzuk:
#include <avr/io.h> ... PORTB = PORTB | 0x04; /* szebben: PORTB = PORTB | ( 1< /* egyszerűbben a |= operátor használatával: */ PORTB |= (1<<PB2); /* Egyszerre töbet is lehet: */ PORTB |= (1<<PB4) | (1<<PB5); /* PB4 és PB5 "HIGH" */
A lábakat kikapcsolni, tehát a kimeneteket LOW szintre állítani a következőképpen lehet:
#include <avr/io.h> ... PORTB &= ~(1<<PB2); /* B Port 2. Bit-jét törli, így PB2 lábon LOW szint lesz */ PORTB &= ~( (1<<PB4) | (1<<PB5) ); /* PB4, és PB5 LOW szintre állítva*/
Azokban a forráskódokban amik az avr-gcc régebbi verzióihoz íródtak, az sbi és a cbi függvénnykkel oldották meg a bitenkénti értékadást, vagy törlést. Ezek a függvények a mostani verziókban már nem elérhetőek, használatuk nem lehetséges.
Abban az esetben, ha a kimenetek kezdő állapota kritikus, figyelni kell az adatirány és a kimeneti értékek beállításának sorrendjére: A kimeneti lábakat, amik kezdőértékként HIGH állapotba kell inicializálni:
Először a PORTx regiszter bitjeinek kell értéket adni.,
végül az adatirányt kimenetre állítani.
Ebből ered a következő sorrend egy eddig bemenetként, lekapcsolt felhúzó ellenállással konfigurált láb esetén:
PORTx beállítása: Belső felhúzó ellenállás aktív
DDRx beállítása: Kimenet (”HIGH”) 
Az először DDRx, és aztán PORTx sorrend egy rövid ”low-puls”-t okozhat, ami külső felhúzó ellenállásokat is ”felülbírálja”. A (kedvezőtlen) sorrend: Bemenet -> DDRx beállítása: kimenet (LOW szinten, így a PORTx- t lenullázza (reset) -> PORTx beállítása: Kimenetet magasra állítani. Lásd még az adatlap Configuring the PIN részében.
Bemenetek:
Jelcsatolás
A legegyszerűbb eset, amikor a jeleket közvetlenül egy másik digitális kapcsolásból visszük át. sőt, ha ez a kapcsolás TTL szintű, akár közvetlenül is összeköthetjük a kimenetet a kontrollerünk egyik bemeneti lábával.
Amennyiben a kapcsolás nem TTL szintű jeleket bocsát ki, úgy a szintet illeszteni (opto-csatoló, feszültségosztó, ”level-shifter” /szintillesztő/ ) kell.
A kapcsolások test pontjait magától értetődően össze kell egymással kötni. A szoftver, ami a kontrolleren fut, végső soron teljesen mindegy miként dolgozza fel a jelet, mi úgy is kizárólag a jelszinteket tudjuk vizsgálni egy mérőműszerrel a kontrollerünk lábain, hogy azokon Logikai 1, (feszültség nagyobb mint kb. 0,7 V x Vcc ), vagy Logikai 0 (feszültség kisebb mint kb. 0,2 V x Vcc ) szint van jelen. Részletes információt arról hogy egy bemeneten mekkora feszültség milyen logikai szintnek felel meg, a felhasznált kontroller adatlapjának DC Characteristics részében találhatunk.
A Port lábak állapotainak lekérdezése közvetlenül a regiszter neveiken keresztül történik.
Fontos, hogy a bemenetek lekérdezéséhez ne a PORTx port-regisztert alkalmazzuk, hanem a PINx bemeneti regisztert, különben nem a bemenetek állapotát olvassuk be, hanem a belső felhúzó ellenállások állapotát. A lábállapotok PORTx –en keresztültörténő lekérdezése PINx helyett gyakori probléma az AVR-ekel való ismerkedéskor.
Tehát ha a D port aktuális jelállapotait akarjuk lekérdezni, és egy bPortD nevű változóba elmenteni, azt a következő módon tehetjük meg:
#include <avr/io.h> #include ... uint8_t bPortD; ... bPortD = PIND; ...
A C- Bitműveletekkel le lehet kérdezni a bit-ek állapotát:
#include <avr/io.h> ... /* Lefut az utasítás ha a Bit1-nek a PINC-ben (1) értéke van */ if ( PINC & (1<<PINC1) ) { /* itt csinálunk valamit */ } /* Lefut az utasítás, ha Bit2-nek a PINB-ben (0) nincs értéke*/ if (!(PINB & (1<<PINB2))) { /* do something.. */ } ...
Belső felhúzóellenállások
Egy AVR-ben a ki-és bemeneti Portok lábaihoz (GPIO) rendelkezésre állnak hozzákapcsolható belső felhúzó ellenállások (többnyire több 10 kΩ, ATmega16 esetében például ez az érték 20-50 kΩ). Ezek sok esetben használhatóak külső felhúzóellenállások helyett.
A belső felhúzóellenállások a tápfeszültségről (Vcc) a PORTx regiszteren keresztül kapcsolhatóak a Port lábakhoz, ha a lábak Bemenetként vannak konfigurálva.
Ha a Port egyik lábának értéke 1, akkor a felhúzó ellenállás aktív, 0 értéknél inaktív. Vagy a külső, vagy a belső felhúzó ellenállást használjuk, de sohasem a kettőt együtt.
A példában a D port minden lába bemenetként van beállítva, és minden Pull-Up aktív. A továbbiakban a PC7 láb bemenetként beállított, és annak a felhúzó ellenállása aktiválva lett, mindezt a port többi lábainak változtatása nélkül.(PC0-PC6)
#include <avr/io.h> ... DDRD = 0x00; /* A D port minden lába bemenet */ PORTD = 0xff; /* A Port minden lábán a belső felhúzóellenállások aktívak */ ... DDRC &= ~(1<<DDC7); /* PC7 láb bemenet */ PORTC |= (1<<PC7); /* Belső felhúzóellenállás bekapcsolása a PC7 láb számára */
Nyomógombok, kapcsolók
Két módszert különböztetünk meg a mechanikus érintkezők mikrokontrollerhez való kapcsolására: Active Low, és Active High. Ez így jó magyarosan hangzik, de lássuk mik is ezek a kifejezések.
Active Low:
Ennél a módszernél az érintkező a kontroller bemeneti lába, és a test közé van kapcsolva, így nyitott kapcsoknál kizárjuk a kontroller számára nem kívánatos jeleket. A tápfeszültség, és a bemeneti kapocs közé úgynevezett Pull-Up (felhúzó) ellenállás kapcsolódik. Ez arra szolgál, hogy nyitott kapcsoknál a bemenetet logika 1 szinten tartsa.
Active High:
Itt az érintkező a tápfeszültség, és a bemeneti láb közé van kapcsolva. Nyitott állapotban nem lesz nem kívánatos jel a bemeneten, mivel a bemeneti láb, és a test közé van az ellenállás kapcsolva. Ez a bemenet logikai 0 szinten tartásáért felel.
A Pull-Up és Pull-Down ellenállások értékei nem kritikusak, mindenesetre ha túl magas értéket választunk, nem érjük el vele a kívánt hatást. A gyakorlatban ez az érték általában néhány 10 kΩ körül van. Az AVR-ek lábai legtöbbször rendelkeznek belső Pull-Up ellenállással, amik mint a példánkban, nyomógomboknál, mechanikus kontaktusoknál nagyszerűen alkalmazható külső Pull-Up helyett. Belső Pull-Down ellenállás viszont sajnos nincs, azt külső ellenállás formájában illeszthetjük az áramkörünkhöz.
Prell mentesítés
Minden mechanikus érintkezőnek – legyen az kapcsoló, vagy akár relé – megvan az a kellemetlen tulajdonsága, hogy prelleg. Ez azt jelenti, hogy az érintkezők záródásakor nem egyből a végleges, tökéletes kapcsolat, hanem közben a másodperc tört része többszöri ki- és bekapcsolás szerű impulzusok jönnek létre.
Ha egy mikrokontrollerrel meg kéne számolni, hogy hányszor nyomtunk meg egy gombot, ki kell küszöbölnünk a prellegést, különben a kapcsolás közbeni impulzusok is egy – egy gombnyomásnak számítanának. Ezt a jelenséget a program tervezésekor okvetlen számításba kell venni.
A következő egyszerű példában az AVR egy gombnyomás estében 200 ms-ot vár. Az időzítésre kényes alkalmazásokban másik megoldást kell használni ( Például a nyomógombállapotok lekérdezése egy Timer-Interrupt-Service-Routine- ban ).
#include <avr/io.h> #include <inttypes.h> #ifndef F_CPU #warning "F_CPU még nem definiált, definiálva 3686400 értékkel" #define F_CPU 3686400UL /* kvarc 3.6864 Mhz */ #endif #include <util/delay.h> /* régi avr-libc: #include <avr/delay.h> */ /* Egyszerű függvény prellmentesítéshez */ inline uint8_t debounce(volatile uint8_t *port, uint8_t pin) { if ( !(*port & (1 << pin)) ) { /* a láb testre van húzva, 100ms-ot vár */ _delay_ms(100); if ( *port & (1 << pin) ) { /* idő a gomb elengedéséhez */ _delay_ms(50); _delay_ms(50); return 1; } } return 0; } int main() { DDRB &= ~(1 << PB0); // PB0 bemenet PORTB |= (1<< PB0); // Pull UP bekapcsolása ... if (debounce(&PINB, PB0)) { // ha a PB0-án levő gomb le van nyomva, Ledet a PB7-en ki vagy be kapcsolni PORTD = PORTD ^ (1 << PD7); } }
A fenti rutinnak sajnos van több hátránya is:
Csak az elengedést detektálja
A main függvényt benyomott gombnál mindig 100 ms –al késlelteti
Elveszti a gombnyomásokat, minél több teendője van a main függvénynek.
Végül csak a cikk végére értünk. Legközelebb megnézzük, hogyan lehet analóg feszültségértékeket átalakítani digitális értékekké.