Kategória: Arduino.
Áttekintés
A hardverközeli programozás során nem (ill. nem csak) a magasabb szintű függvényeket használjuk, hanem a chip regisztereit közvetlenül állítjuk, ill. hardverközeli függvényeket hívunk. Ennek számos előnye és számos hátránya van. Előnyei:
- Bizonyos funkciók csak ilyen módon érhetőek el. Pl. csak így tudjuk elérni az EEPROM memóriát, vagy azt, hogy adat kerüljön a programmemóriába.
- A magasabb szintű függvények használatának szinte mindig van valamekkora "ára": többnyire lehet jobban optimalizálni, mint ahogyan azt egy általános fordító teszi. Erre ritkán, de szükség lehet, pl. fotó készítésekor a beépített függvények sebessége nem elegendő, de közvetlen hardver szintű programozással el tudjuk érni a kívánt sebességet.
- Segítségével jobban meg tudjuk érteni azt, hogy mi történik legalul. Pl. megtudhatjuk, hogy a várakozás során sohasem pontosan annyi ideig függeszti fel a futást a processzor, amennyit megadunk neki, mivel az csak az órajel többszöröse lehet.
Ugyanakkor ne feledkezzünk meg a hátrányokról sem:
- A kód hordozhatósága lényegében megszűnik, vagy legalábbis rendkívüli módon leszűkül. Ha nagyon picit eltérő lapkára szeretnénk feltölteni a kódot, lehetséges, hogy módosítanunk kell rajta.
- A kód olvashatósága is jelentősen romlik.
Ezeket figyelembe véve igyekezzünk csak akkor használni a lent felsorolt technikákat, ha feltétlenül muszáj.
Konstansok tárolása a programmemóriában
Amint arról már volt szó, az Arduino külön memóriahelyen tárolja a programot és az adatot. A program memória Arduino UNO esetén 32 kB, az adta memória viszont csak 2 kB. A konstans jellegű adatokat érdemes tehát a program memóriában tárolni. Ezt a PROGMEM és F() makrók segítségével tudjuk megtenni:
const int dataInProgmem[] PROGMEM = {1, 2, 3, 4, 5}; void setup() { Serial.begin(9600); Serial.println(F("Reading data from PROGMEM.")); for (int i = 0; i < 5; i++) { int dataFromProgmem = pgm_read_word(dataInProgmem + i); Serial.println(dataFromProgmem); } } void loop() {}
Hasonlítsuk össze a következővel, ahol külön nem jelezzük, és az adat automatikusan az SRAM-ba kerül, nem a program memóriába:
int dataInSRAM[] = {1, 2, 3, 4, 5}; void setup() { Serial.begin(9600); Serial.println("Reading data from SRAM."); for (int i = 0; i < 5; i++) { int dataFromSRAM = dataInSRAM[i]; Serial.println(dataFromSRAM); } } void loop() {}
Ez utóbbi fordítási eredménye az alábbi:
Sketch uses 1790 bytes (5%) of program storage space. Maximum is 32256 bytes.
Global variables use 222 bytes (10%) of dynamic memory, leaving 1826 bytes for local variables. Maximum is 2048 bytes.
Az előbbi, PROGMEM-es változat viszont ez:
Sketch uses 1840 bytes (5%) of program storage space. Maximum is 32256 bytes.
Global variables use 188 bytes (9%) of dynamic memory, leaving 1860 bytes for local variables. Maximum is 2048 bytes.
Látható, hogy a PROGMEM-es esetben több adat került a program memóriába, melynek mérete 32256 bájt, a másikban viszont a dinamikus memóriába, melynek a mérete mindössze 2048 bájt.
A PSTR("…") szintén olyan makró, melynek segítségével szöveget tudunk tárolni a program memóriában.
A program memóriáról részletesen olvashatunk itt: https://www.arduino.cc/reference/en/language/variables/utilities/progmem/. A makrók és függvények a avr/pgmspace.h fejlécben vannak deklarálva, viszont az újabb fejlesztőeszközök esetén már nem kell explicit importálni. A specifikációja egyébként itt található: https://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html.
Adat tárolása EEPROM-ban
Az EEPROM-ban tárolt adat kikapcsoláskor is megmarad. Az EEPROM könyvtár (https://www.arduino.cc/en/Reference/EEPROM) függvényeit tudjuk felhasználni adatok olvasására ill. írására.
A következő példában a lapkára szerelt 13-as LED villan először egyet, majd kettőt, hármat, és így tovább, és 10 után ismét egyet. Érdemes lekapcsolni mondjuk ötnél, és újraindítást követően ellenőrizni, hogy hatot villan-e.
#include <EEPROM.h> const int address = 0; int data; void setup() { pinMode(13, OUTPUT); digitalWrite(13, LOW); } void loop() { EEPROM.get(address, data); if (data > 10 || data < 1) { data = 1; EEPROM.put(address, 1); } for (int i = 0; i < data; i++) { digitalWrite(13, HIGH); delay(100); digitalWrite(13, LOW); delay(100); } data++; EEPROM.put(address, data); delay(1000); }
A példában a 0 című memóriaterületről olvasunk ill. írunk. A kód kicsit defenzív, mivel a feltöltés törli az EEPROM memóriát, ott elvileg nem lehet egy korábbi program által hátrahagyott "szemét", mindenesetre egy negatív vagy túl nagy kezdeti értékre is fel van vértezve a program.
Az EEPROM-ban tárolt adat ki- és beolvasása lassú, ráadásul korlátos, hogy összesen hányszor lehet ezt megtenni (kb. százezer ciklus), így indokolatlanul nem érdemes használni.
Hardverközeli portelérés
Az Arduino által nyújtott felület elfedi a hardverbeli eltéréseket a különböző lapkák között. Annak érdekében, hogy ugyanaz a kód többféle lapkával is használható legyen, továbbá amiatt, hogy a kód olvasható maradjon, célszerű a magasabb szintű függvényeket használni, pl. pinMode(), digitalWrite() stb. Azonban jó, ha ismerjük a saját eszközünk processzorát is, ugyanis előfordulhat, hogy bizonyos eszközöknél már nem megengedett a közvetítésből adódó lassulás, közvetlenül, hardver szinten kell programoznunk. Ill. ami valószínűbb, belefutunk ilyen kódba, pl. bizonyos könyvtárakba, és hogy meg tudjuk azt is érteni.
A port kezelés dokumentációja itt található: https://www.arduino.cc/en/Reference/PortManipulation. Az Arduino UNO ATmega328P chip-et tartalmaz, mely hardver szinten 3*3 regisztert definiál port elérésre:
- B: a 8-13 lábak elérésére
- C: az A0-A5 lábak elérésére
- D: a 0-7 lábak elérésére
Mindhárom port esetén 3 műveletet tudunk végrehajtani (x jelzi a regisztert, tehát B, C vagy D):
- DDR[x]: adat irány regiszter. Ezzel tudjuk beállítani, hogy az adott láb bemenet vagy kimenet legyen, tehát a pinMode() regiszter szintű párja. 1 az output, 0 az input. Írni és olvasni is tudjuk.
- PORT[x]: adat regiszter. Ezzel tudunk írni az adott lábra.
- PIN[x]: bemeneti regiszter. Ezzel tudunk beolvasni.
Példák:
- DDRB |= 32;: a B regiszterről van szó, tehát a 8-13 közötti portokról. DDR, tehát az adat irányáról beszélünk. A 32 binárisan kódolva 00100000, a | művelet pedig a bitenkénti logikai vagy. Ennek eredményeként a DDRB regiszter 6. bitje 1 lesz, a többi változatlan. A 6. bit pedig a 13-as lábat jelenti. Így ez az utasítás egyenértékű ezzel: pinMode(13, OUTPUT);.
- PORTB ^= 32;: a fentihez hasonló logika mentén végiggondolva, a 13-as lábon levő értéket változtatja meg (a ^ a bitenkénti logikai kizáró vagy, így a 6. bit fog változni, a többi marad). Ez tehát nagyjából megegyezik a state = HIGH - state; digitalWrite(13, state); utasításokkal.
Még egy gyakori utasítás, egészen pontosan makró: _BV(x). Ez egyenértékű egy bit balra léptetésével x lépést. Ezzel lehet adott bitet kiválasztani. Pl. a _BV(0) eredménye bináris 00000001 lesz, a _BV(5) eredménye pedig bináris 00100000, azaz decimális 32. Jelenleg pont erre van szükségünk.
A delay() hardverszintű megfelelője _delay_ms(), így a kezdeti LED villogtató program regiszterekkel val megvalósítása az alábbi:
void setup() { DDRB |= _BV(5); } void loop() { PORTB ^= _BV(5); _delay_ms(1000); }
Hardverközeli soros kommunikáció
A TX-RX kommunikációt a Serial osztály függvényeivel célszerű megvalósítani. Ezt közvetlenül a státusz regiszterek, azon belül a státusz bitek beállításával is meg tudjuk tenni. Az érintett regiszterek: UCSR0A , UCSR0B és UCSR0C. Ezzel kapcsolatban viszonylag kevés dokumentációt találunk; egy blogbejegyzést olvashatunk a témáról itt: https://appelsiini.net/2011/simple-usart-with-avr-libc/.
Hardverközeli PWM kezelés
A PWM hardver szintű programozásához a TCCRnA és TCCRnB regisztereket kell megfelelően beállítani, melynek részletesebb leírása megtalálható a következő oldalakon:
- https://playground.arduino.cc/Main/TimerPWMCheatsheet/
- https://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM
Hardverközeli TWI
A TWI (más néven I2C) interfészt tipikusan közvetetten érjük el, az adott hardver könyvtárán keresztül. Mivel a tipikus felhasználása az, hogy hardver szinten küldünk adatokat a célhardvernek, tehát nem is assembly, hanem közvetlen gépi kódú programozásról van itt szó, ez a téma kívül esik e tanuló jellegű leírás keretein. A téma mélyét nem említve, az érintett regiszterek az alábbiak:
- TWBR (Two Wire Interface Bit Rate Register) - ezzel tudjuk a bitrátát beállítani
- TWCR (Two Wire Interface Control Register) - a regiszter bitjeit beállítva (a 8 bitből 7-et) tudjuk vezérelni az adatforgalmat
- TWSR (Two Wire Interface Status Register) - segítségével a státuszt tudjuk kiolvasni; a kódok jelentését a processzor adatlapja tartalmazza
- TWDR (Two Wire Interface Data Register) - a küldendő adatot tartalmazza
A következő függvény adott I2C címre adott regiszternek adott értéket ad:
void twiWrite(uint8_t addr, uint8_t reg, uint8_t dat){ TWCR = _BV(TWINT) | _BV(TWEN) | _BV(TWSTA); while (!(TWCR & _BV(TWINT))) {} TWDR = addr; TWCR = _BV(TWINT) | _BV(TWEN); while (!(TWCR & _BV((TWINT))) {} TWDR = reg; TWCR = _BV(TWINT) | _BV(TWEN); while (!(TWCR & _BV(TWINT))) {} TWDR = dat; TWCR = _BV(TWINT) | _BV(TWEN); while (!(TWCR & _BV(TWINT))) {} TWCR = _BV(TWINT) | _BV(TWEN) | _BV(TWSTO); }
További adatforrások:
- https://playground.arduino.cc/Main/WireLibraryDetailedReference - a TWI általános leírása
- https://playground.arduino.cc/Code/ATMELTWI - a TWI mély bugyrai
- A processzor specifikáció 22. fejezete szól a TWI-ről.
Hardverközeli időzítés
A regiszterek beállításával elért időzítésről egy kiváló összefoglalót találhatunk itt: https://www.instructables.com/id/Arduino-Timer-Interrupts/. Lássuk, hogyan is néz ki a már bemutatott fél másodperces megszakításokkal LED-et villogtató példa:
void setup(){ pinMode(13, OUTPUT); cli(); TCCR1A = 0; TCCR1B = 0; TCNT1 = 0; OCR1A = 7882; // = 16,000,000 / (2 * 1024) - 1 (must be < 65536) TCCR1B |= (1 << WGM12); TCCR1B |= (1 << CS12) | (1 << CS10); TIMSK1 |= (1 << OCIE1A); sei(); } ISR(TIMER1_COMPA_vect) { digitalWrite(13, 1 - digitalRead(13)); } void loop() {}