- Vezérlő belső regisztereinek megismerése, B,C,D portok
- Kivezetés beállítása ki és bemenetnek adat irány regiszter bitjeivel
- Bemenet értékének kiolvasás, PINx regiszter
- Kimenet értékének beállítása PORTx regiszter
- Miért is jó a belső regisztereket használni? Kimenet változási sebességének növelése!
Az Arduino környezetben nagyon kényelmesen használható függvényeket gyártottak számunkra, és ezek segítségével a kód könnyen olvasható és értelmezhető. Ezek a függvények elrejtik elölünk a vezérlő működését, ami egyszerűvé teszi a programozást. Azonban ennek ára van, hiszen a programkódunk – anélkül hogy erről tudnánk – lényegesen hosszabb és ezáltal a végrehajtás jóval lassúbb. Amatőr körülmények között ez egyáltalán nem számít. Lehetnek azonban olyan alkalmazások, amikben a sebességnek illetve a korlátozott flash program memória jobb kihasználásának fontos szerepe lehet. Konkrét példát fogok erre mutatni a digitális kimenet leírásának végén.
Lássuk első lépésben, mit is csinál a vezérlő, hogyan lehet a belsejében található regiszterek segítségével beállítani, hogy a kivezetés bemenet illetve kimenet legyen, és kiolvasni vagy beállítani annak állapotát. A vezérlő kivezetései közül mindegyik egy konkrét port regiszterhez tartozik. Nézzük meg pl. az ATMega328P pinout ábráját:
A sokféle jelölés között felismerhetjük az Arduino világban használatos kivezetés számozást. Pl. foglalkozzunk a 9-es kivezetéssel (bekarikáztam, barna szám). Ez a számozás ugyanazt a chip kivezetést rejti, amit pl. Arduino nano vagy Arduino Uno alaplapra rászitáztak:
Véletlenül egy kicsit más jelölésrendszerű ábrát választottam. Sajnos ez éppen egy jó zavarosra sikerült darab, mert szürkével a kivezetések sorszámát is feltüntették, ami semmire nem jó! Viszont „könnyítésként” nem egy sima sorszámot adtak az Arduino kivezetésnek, hanem IO9 felirattal (kék szín) jelezték a digitális kimenet sorszámát. Ez főleg azért zavaró, mert az alaplapra D9 feliratot szitáztak. Sebaj, hozzá kell szoknunk, hogy minden ábra más, ki kell tudni keresni, ami nekünk kell.
Visszatérve a feladatunkra, nézzük mindkét ábrán a bekarikázott 9-es Arduino kivezetést. Mindkét ábrán le tudjuk olvasni, hogy ez a kivezetés a PB1-hez tartozik, ami a PB port regiszter 1. bitjét jelenti. A port regiszter a vezérlőben az a regiszter, aminek minden bitje egy-egy kivezetés kimeneti vagy bemeneti állapotát tartalmazza. Értelem szerűen egy port regiszter egyetlen bitjével nem lehetne meghatározni a kivezetés adatirányát (ki/bemenet) és az állapotát is, ezért a vezérlőben a valóságban ez három regisztert jelent. Az egyik regisztert adat irány regiszternek nevezték el. Ennek neve a chip adatlapján DDRB. Ha alaposabban megnézzük az pinout ábrát, akkor láthatjuk, hogy egy ATMega328P-nek 19 kivezetése van, ami digitális ki és bemenetnek használható. Ehhez összesen 3 regiszter szükséges, ezek a B,C és D port-regiszterek. Ha alaposan megnézzük az Arduino nano pinout ábrát, akkor láthatjuk, hogy ennek még van két bónusz analóg bemenete (A6, A7), ezekhez nem adtak meg sárga színnel portregisztert, mert nincs neki. Ez a két kivezetést csak analóg bemenetnek lehet használni, nem digitális kivezetés, egyenlőre ne is foglalkozzunk vele.
Térjünk vissza a 9-es Arduino kivezetéshez. Ha az ATMega328P adatlapját nézzük, akkor azon azt fogjuk tapasztalni, hogy a chip ezen kivezetését a B portregiszter 1. bitjéhez rendelték hozzá. Tehát ha ezt a kivezetést szeretnénk bemenetnek állítani, akkor DDRB regiszter 1. bitjét 0 értékre kell beállítani (természetesen létezik DDRC és DDRD regiszter is a megfelelő kivezetéseknek). Ha kimenetnek szeretnénk beállítani, akkor értéke legyen 1. Hogyan lehet logikai műveletekkel egyetlen bitet beállítani úgy, hogy a többi bit ne változzon meg? A vezérlőnek van olyan belső utasítása, ami két regiszter tartalmát bitenként „és” illetve egy másik ami „vagy” kapcsolatba hozza. Az eredményt pedig egy újabb utasítással beleírja a DDRB regiszterbe. Ezeket az utasításokat nem kell ismernünk, szerencsére nagyon egyszerűen Arduino IDE környezetben is megírhatjuk ezeket a műveleteket a vezérlő nyelvezetének ismerete nélkül. Az Arduino IDE környezetet úgy hozták létre, hogy a vezérlő regisztereire az adatlapban feltüntetett neveikkel hivatkozhatunk. Tehát ha szeretnénk a DDRB regiszter tartalmát megváltoztatni, akkor csak le kell írnunk a „DDRB” megnevezést mintha egy sima változó lenne. De nekünk csak az 1. bitet kellene megváltoztatni, úgy, hogy a többi ne változzon. Figyelem az 1. bit, az nem jobbról a legelső bit, mert az informatikában szeretjük a 0-át is használni, így jobbról a legelső bit az valójában a 0. bit!! Tehát ezek alapján a következő utasítást kell leírnunk az Arduino kódban:
DDRB=DDRB & B11111101; //az és kapcsolat jele az „&”, de írhattunk volna „AND”-et is
Mit csinál ez a kód? Az egyenlőség jel jobb oldala kerül végrehajtásra először. Kiolvassa a DDRB regiszter tartalmát és bitenként „és” kapcsolatot hoz létre a „&” jel mögé írt bináris formátumban írt számmal (decimálisan 253, vagy hexa formátumban 0xFD). Mivel ebben a számban az 1. bit tartalma 0, az és kapcsolat eredménye ezen a biten mindenképpen 0. A többi bitnél a bit értéke nem változik meg, mert ha az és kapcsolat egyik bitje 1, akkor az eredmény a másik szám azonos bitjének értékével egyezik meg. Legyünk tudományosak és írjuk le egy példában:
Pl. a DDRB regisztert tartalma legyen „10011111”. Az és kapcsolat így néz ki:
DDRB 10011111
& 11111101
————————
10011101
Már csak azt kell tisztáznunk, hogyan kerül az eredmény a DDRB regiszterbe? Természetesen az egyenlőség bal oldalán szereplő változóba kerül az eredmény, ami most éppen a DDRB regiszter. Készen is vagyunk, az Arduino alaplapunk 9-es kivezetése bemenet lett, a többi B porthoz tartozó kivezetés adat iránya nem változott. Ez így teljesen azonos értékű a pinMode(9,INPUT); programsorral, csak éppen nem áll mögötte egy csomó előre megírt programsor, ami kideríti, hogy melyik port, melyik bitjéhez tartozik a 9-es kivezetés, és aztán beállítja az „INPUT” konstans értékre, ami meglepő módon épp 0 értéket jelent (írhattuk volna azt is, hogy pinMode(9,0). Persze nem is kényelmes a használata, mert ha úgy döntesz, hogy a kijelölt feladatra nem a 9-es hanem pl. a 3-as kivezetést akarod használni, akkor böngészheted át a programodat, hol kell a port nevet és a bitek értékét átírni!
Nézzük meg hogyan tudunk kimenetet csinálni a 9-es Arduino kivezetésből. Nyilván a DDRB regiszter 1. bitjét kell 1-re állítani, bármi is volt előtte. Erre pont a bitenkénti „vagy” kapcsolat lesz a megfelelő:
DDRB=DDRB | B00000010; //a vagy kapcsolat jele „|” (AltGr+W), de írhattunk volna „OR”-t is
Bemenet értékének kiolvasása
Már be tudtuk állítani az Arduino 9. kivezetését bemenetnek a B port PB1 bitjének 0-ba állításával, most olvassuk ki a kivezetésre kapcsolt digitális jelszintett. Emlékeztetőül a vezérlő adatlapjából ki lehet olvasni, hogy mekkora feszültséget tekint a vezérlő 0-nak vagy 1-nek. Úgy emlékszem, hogy 0-0.7V feszültségszint között a vezérlő 0 értéknek tekinti a bemenet állapotát, míg kb 2.4-5V között 1-nek. 0.7V és 2.4V feszültségszint között bizonytalan, hogy mi lesz az eredmény (5V tápfeszültség esetén érvényes amit leírtam). Hova is kerül a kiolvasott eredmény? Erre is van egy regiszter a neve PINB (és persze van PINC és PIND regiszter is a megfelelő kivezetéseknek). Fontos tudni, hogy a kivezetés kiolvasása és az eredmény PINB regiszterben történő megjelenése között eltelik egy vagy több órajelciklus. Ez esetenként problémát okozhat, de egyenlőre csak jegyezzük meg.
Tehát hogyan is tudjuk kiolvasni a bemenet értékét? Simán beírjuk a kódba a „PINB” nevet, és megkapjuk a kivezetések pillanatnyi állapotát. Ha egy kivezetés kimenet, akkor a PINB annak pillanatnyi állapotát is tartalmazza, de persze azt nekünk kell tudni, melyik bit kimeneti és melyik bit bemeneti érték. Problémát okoz, hogy itt összesen 8 bit értéke szerepel egyben, de mi csak egyetlen bit-re vagyunk kíváncsiak. Nézzünk egy programrészt, mit is lehet kezdeni ezzel a helyzettel:
byte regiszter;
regiszter=PINB & B00000010;
Ennek a programrésznek a végrehajtásakor létrejön egy byte típusú „regiszter” nevű változó, amiben betöltjük a PINB bitenkénti „és”-el maszkolt állapotát. Ha PINB-ben jobbról a második bit értéke 0, akkor a „regiszter” változó értéke 0 lesz. Ha ez a bit 1, akkor pedig PINB értéke 2. Nyilván közvetlenül egy if() feltételbe is beírhatjuk pl így:
if(PINB & B00000010==0) {}
Az if() akkor fogja igaznak tekinteni a feltételt, ha a 9-es kivezetésre kapcsolt jelszint 0. Persze ez így kicsit nehezebben követhető, mint egyszerűen leírni azt, hogy regiszter=digitalRead(9);! Viszont megspóroltuk a digitalRead() mögött álló jó néhány programsor, ami kideríti a kivezetés sorszámáról, hogy melyik port melyik bitje, és elvégzi a kiolvasást, kimaszkolja az egy bitet, és visszaadja a bemenet állapotát.
Ha egy kivezetést kimenetnek állítunk be, és a PINx regiszterből szeretnénk beolvasni a pillanatnyi állapotát, előfordulhat némi furcsaság. Írtam egy programot, ami először beállította a kimenetet, és rögtön a következő programsorban visszaolvasta PINx regiszter tartalmat, mert kíváncsi voltam egy másik bemenetnek konfigurált kivezetés állapotára. Azt tapasztaltam, hogy az előző utasításban beállított kimenet a PINx szerint nem abban az állapotban van, mint amibe az előző pillanatban küldtem?! Az adatlap adott rá választ, hogy egy órajel ciklust várni kell, hogy a kimenet aktuális tartalma eljusson a PINx regiszterbe is. Tehát a programba be kellett szúrnom egy NOP-ot (lásd kicsit lejjebb) ami beiktatott egy órajel ciklusnyi várakozást. Így már minden bit megfelelő állapotot mutatott!
Kimenet állapotának megváltoztatása
Elérkeztünk ahhoz a részhez, ahol új ismereteinket a későbbiekben nagy valószínűséggel használni is fogjuk valamire. A kimenetnek beállított kivezetés állapotának megváltoztatásához természetesen szintén van egy regiszter aminek PORTB a neve (és van PORTC és PORTD is a többi kivezetésnek). A 9. kivezetés állapotát a PORTB 1. bitje tartalmazza. Nem okoz meglepetést, ha leírom a bit 1-be illetve 0-ba állításának programsorait:
PORTB=PORTB & B11111101; //ez bebillenti a 9-es kivezetést 0-ba
PORTB=PORTB | B00000010; //ez bebillenti a 9-es kivezetést 1-be
Ugye milyen egyszerű?!
Még meg szeretném említeni, hogy abban az esetben, ha egy kivezetés bemenet, akkor a PORTx regiszter a felhúzó ellenállás ki és bekapcsolásának funkcióját veszi át, és nem a kimeneti szintet állítja be. Ha a megfelelő bitet 1-be billentjük, akkor a vezérlő áramkörei bekapcsolják a felhúzó ellenállást.
Összefogalalás
Alacsony szinten a digitális kivezetések működését három regiszter határozza meg a vezérlőben:
- DDRx – beállítja a kivezetés irányát, 0=bemenet 1=kimenet
- PORTx – Ha a kivezetés kimenet, akkor beállítja a kimenet értékét, ha bemenet akkor 1-el beállítja a felhúzó ellenállást
- PINx – Ha a kivezetés bemenet, akkor beolvassa annak állapotát, ha kimenet, akkor kiolvassa aktuális állapotát
Nézzünk egy tömör mintapéldát. Beállítjuk a 13. kivezetést kimenetnek (ezen van az alaplapra szerelt led), beállítjuk a 6-os kivezetést bemenetnek, és ha a bemenet 0 akkor lekapcsoljuk a led-et, ha pedig 1, akkor felkapcsoljuk a led-et:
setup() { DDRB=DDRB | B00010000; //Az Arduino 13 (PB5) kivezetés kimenet lesz DDRD=DDRD & B11011111; //Az Arduino 6 (PD6) kivezetés bemenet lesz PORTD=PORTD | B00100000; //bekapcsoljuk a 6-os kivezetésen a felhúzó ellenállást } loop() { if(PINB & B00100000==0) { PORTB=PORTB & B11101111;} //led kikapcsolása else { PORTB=PORTB | B00010000;} //led bekapcsolása }
Reményeim szerint a program nem igényel további magyarázatot!
Digitális kimenet működési sebességének növelése
Milyen hasznát vehetjük annak, ha a portregiszterek közvetlen használatával olvashatatlanná és nehezen javíthatóvá tettük a programunkat. Vegyünk egy egyszerű példát erre! Tegyük fel, hogy valakitől megbízást kapunk arra, hogy készítsünk az Arduino nano alaplapunk segítségével egy 1Mhz frekvenciával működő négyszögjel generátort, mert épp arra van szükség. A négyszögjelet a 6-os Arduino kivezetésen szeretnénk előállítani. Nosza lássunk neki, és írjuk meg a programot a szokásos Arduino utasításokkal:
void setup() { pinMode(6,OUTPUT); } void loop() { digitalWrite(6,HIGH); digitalWrite(6,LOW); }
Kössünk egy oszcilloszkópot a 6-os kimenetre, és nézzük meg az eredményt:
A négyszögjel tökéletes, de a frekvencia nem! Az oszcilloszkóp időalapja 2us/osztás állapotban volt a mérés idején. Jól látható, hogy a négyszögjel egy periódusának hossza 8us. Ez kb. 125Khz frekvenciának felel meg! Tehát ezzel a módszerrel nem lehet elérni a célt! Ehhez az ATMega328 vezérlő túl lassú ezzel a programmal.
Most próbáljuk meg közvetlen port írással ugyanezt:
void setup() { pinMode(6,OUTPUT); } void loop() { PORTD = B01000000; PORTD = B00000000; }
A 6-os kivezetésre kötött oszcilloszkópon ezt látjuk:
Az oszcilloszkóp időalapja 100ns/osztás állapotban. Tehát a periódusidő 380ns, frekvencia 2.64Mhz. Feltűnhet, hogy a négyszögjel erősen aszimmetrikus. Ennek oka, hogy amikor 1-et állítunk be a loop()-ban, a következő utasítással azonnal 0-ba küldjük a kimenetet, amihez mindössze egy órajel ciklust igényel a vezérlő. Mivel a vezérlő órajele 16Mhz, egy órajel ciklus 62,5ns. Kb. ezt látjuk az ábrán. Miért kell a következő felfutóélig kb. 300ns? Hát azért, mert a loop() függvénynek van adminisztrációja, amit az Arduino szoftver környezet elrejt elölünk, de sejthető, hogy a vezérlő itt még néhány dolgot elvégez, pl. visszaugrik a program memóriában a loop() első utasításához, tehát a memóriából be kell töltenie az ehhez szükséges címet stb., és ehhez idő kell. Ez a program már közelíti a kívánt eredményt, de fő problémája, hogy a négyszögjel aszimmetrikus, ráadásul a többi D porton található kimenetet nem tudjuk használni, mert a portra konstans adatot írtunk ki, vagyis a többi bit közben nem változhat. Bár a kívánt programban más funkcióról nem is esett szó, így ez most nem baj!
Próbáljuk meg úgy átalakítani a programot, hogy a D portnak csak egyetlen bitjéhez nyúljunk hozzá:
void setup() { pinMode(6,OUTPUT); } void loop() { PORTD = PORTD | B01000000; PORTD = PORTD & B10111111; }
Mindössze annyi változott, hogy a port tartalmát egy bitenkénti „és” illetve „vagy” művelettel állítjuk be. Ehhez nyilván több utastás szükséges, tehát a frekvencia csökkenését várjuk:
Láthatóan csökkent is. Az ábrán az oszcilloszkóp időalap továbbra is 100ns/osztás. A magas szint ideje növekedett durván a duplájára, hisz egy órajelciklus kell az „és” illetve „vagy” műveletre. Az kb. 125ns. Az alacsony szint ideje is megnőtt kb. egy órajel ciklussal, ennek okát nem tudom, nem látok ennyire mélyen a vezérlő belső működésébe. A periódusidő 500ns-ra növekedett, azaz a frekvencia kb 2Mhz.
Egyetlen bit megváltoztatására van külön bitművelet, most használjuk azt! Legalább látni fogunk egy másik megoldást is ugyanerre a programra:
void setup() { pinMode(6,OUTPUT); } void loop() { bitSet(PORTD,6); bitClear(PORTD,6); }
A program semmit nem változott, csak az Arduino környezetben rendelkezésre álló függvényt használtunk! Az eredmény oszcilloszkópon:
Láthatóan az eredmény ugyanaz. A tanulság ebből, hogy a bitSet() és a bitClear() jól lett megírva, nyugodtan használjuk kritikus sebesség igény esetén is!
Próbáljuk helyrehozni az aszimmetriát.
#define NOP __asm__("nop\n\t") void setup() { pinMode(6,OUTPUT); } void loop() { bitSet(PORTD,6); NOP; bitClear(PORTD,6); }
Programunk annyiban változott, hogy megjelent egy új elem „NOP” néven. Ez a „nincs művelet” rovidítése, és egy létező utasítás minden vezérlőben. Hatására a vezérlő egy órajel ciklusig (esetünkben 62,5ns-ig) nem csinál semmit. A program legelső sora példa arra, hogyan lehet egy meghatározott szövegrészt a fordítás előtt kicserélni egy másikra. Ez jelen esetben azért hasznos, mert így a jól olvasható „NOP”-ot lehet beírni a programra a __asm__(„nop\n\t”) szöveg helyett. Utóbbi szöveg egy gépikódú utasítást illeszt be az Arduino kódunkba. Ne akadjunk fenn ennek szintaktikáján, fogadjuk el, hogy így van és kész. Ha a program elejére beillesztettük a #define… kezdetű sort, akkor bátran írogassunk bárhol a programba „NOP;”-ot, és ezzel lassítunk a programon 62,5ns-ot!
Zárójelben jegyzem meg annak aki esetleg nem használta még a #define direktívát, hogy ez a fordítónak szóló utasítás, hogy első paraméterében megadott szöveget cserélje ki a második paraméterben megadott szövegre, mielőtt lefordítaná a programot. Pl. Arduino környezetben használhatjuk a HIGH illetve LOW jelszint megadási formát. Valójában valahol az Arduino háttér kódjai között szerepel a következő:
#define HIGH 1
#define LOW 0
Ez a két sor teszi lehetővé, hogy 0 és 1 helyett leírhatjuk a LOW és HIGH szavakat is.
Térjünk vissza a NOP-ot tartalmazó programocskánkra, és nézzük meg oszcilloszkópon a kimentet:
Az eredmény a várakozásnak megfelelő, a magas szint ideje kb. 190ns. Néhány NOP segítségével közel szimmetrikus négyszögjelet tudnánk varázsolni, aminek periódus ideje kb. 720ns, azaz 1.39Mhz. Ez a maximum, ennél nem lesz nagyobb. Tehát a kitűzött cél, az 1 Mhz teljesíthető, csak még néhány NOP-ot kell megfelelően elhelyezni a programban. A frekvencia nem lesz pontos, mert a beállítás felbontása 62.5ns. A négyszögjel szimmetriája sem lesz tökéletesen pontos. Kompromisszumokkal ez azonban sok mindenre jó lehet.
Záró gondolatként még azt nézzük meg, lehetséges-e a négyszögjel alacsony szintjének idejét csökkenteni. És igen, egy picit még lehet faragni rajta! Ehhez azt kell tudni, hogy az Arduino környezete, egy teljesen szabványos C++ nyelvi környezet, amit kissé átalakítottak kényelmünk érdekében. Egy C++ program mindig egy „main()” fügvénnyel kezdődik. Arduino-ban is, csak ezt nem látjuk! Az Arduino-ban elrejtett main() első lépésben meghívja a setup() függvényt, majd ha ez lefutott, egy while() végtelen ciklusban hívogatja a loop() függvényt! Akit ez bővebben érdekel, javaslom keresse meg a main.cpp file-t a telepített Arduino IDE megfelelő könyvtárában (nálam itt található: C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino), és ennek tartalmát vizsgálja meg. Fog találni egy programrészt:
int main(void) { init(); initVariant(); #if defined(USBCON) USBDevice.attach(); #endif setup(); for (;;) { loop(); if (serialEventRun) serialEventRun(); } return 0; }
A setup()-tól kezdődő rész amiről beszélek!
Visszatérve gyorsítási ügyünkre, egy függvényhívásnyi időt még megtakaríthatunk, ha el sem engedjük a programunkat a loop()-ba, hogy ott felesleges időt vesztegessen. Tegyünk be a setup()-ba egy while() végtelen ciklust és abban végezzük el a port írását. A program így néz ki:
void setup() { pinMode(6,OUTPUT); while (true) { bitSet(PORTD,6); bitClear(PORTD,6); } } void loop() { }
A program futásának eredménye az oszcilloszkópon:
Láthatjuk, hogy a magas szint ideje változatlan, azonban az alacsony szint 350ns-ról kb.220ns-ra csökkent. Nem pontosan értem, hogy miért nem osztható az idő 62.5ns-al, de mint említettem, nem látom át a vezérlő lelki világát. Így megelégszem az oszcilloszkópról leolvasott adattal. Ez egyébként az abszolút kicsalható maxium, ahol periódusidő kb. 380ns és a frekvencia 2.63Mhz. Ennél több nem fér ki a csövön!