Digitális ki és bemenetek kezelése alacsony szinten

  • 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!

Mennyire volt hasznos amit olvastál? Értékelés után szövegesen is leírhatod megjegyzéseidet és véleményedet!

Kattints egy csillagra az értékeléshez!

Szövegesen is leírhatod véleményedet! Ha kérdésed van, ne felejtsd el megadni az email címedet!