WINAVR-ben kényelmesen, avagy a legkényelmesebb nyelv mégis a C !

2013-07-19

 

A GCC preprocessor-ról szóló előző írásom azért született, mert néhány gondolat körvonalazódott a fejemben lévő brain-space-ben. Gyakran szoktam az Interneten keresgélni, ki/hogyan oldott meg AVR mikróvezérlőkkel (uC) kapcsolatos problémákat C-ben. Ilyen rémisztő kódokat talál az ember:


TIMSK |= (1 << TOIE0);
EICRB = (EICRB | (1 << ISC40)) & ~ (1 << ISC41);
EIMSK |= (1 << INT4);

Hosszú ideig bogarászhatjuk, hogy melyik regiszter milyen bitjét... vagy tudjuk fejből az uC regiszter és bit készletét, vagy nem... Én az utóbbi csapatba tartozom. Ezért kezdtem el használni a makró előfeldolgozó program nyújtotta lehetőségeket, és igyekeztem szimbolikus állandókat használni. Egyszer találkoztam egy makróval, a soros port sebességének beállítására, amely megmutatta számomra, hogy állandók kiszámolására is alkalmazható a preprocesszor:


#define MYUBR          (F_CPU/(BAUDRATE*16UL))-1

UBRRL= MYUBR;
UBRRH= (MYUBR) >> 8;

Nézzünk néhány korábban is használt makrómat, assembler utasítások beillesztésére:


#define IT_ENABLE     __asm__ __volatile__ ("sei" ::)    // megszakitas engedelyezese
#define IT_DISABLE    __asm__ __volatile__ ("cli" ::)    // megszakitas tiltasa
#define NOP           __asm__ __volatile__ ("nop" ::)    // egy ures utasitas beszurasa

A következő makrók a Z80 assembly-t idézik, és kezelhetővé teszik a regiszterekkel kapcsolatos bit műveleteket:


#define BS(sfr,bit)   sfr|= ( 1 << ( bit))               // BIT-SET - egy regiszter adott bitjenek 1-re allitas
#define BC(sfr,bit)   sfr&= ~( 1 << ( bit))              // BIT-CLEAR - egy regiszter adott bitjenek 0-ra allitas
#define BTS(sfr,bit)  bit_is_set( sfr, bit)              // BIT-TEST-SET - egy regiszter egy bitjenek vizsgalata 1-re
#define BTC(sfr,bit)  bit_is_clear( sfr, bit)            // BIT-TEST-CLEAR - egy regiszter egy bitjenek vizsgalata 0-ra

Ezeket használva így néz ki egy LED kezelése:


#define LED            4
#define LED_ENABLE     BS( DDRD, LED)
#define LED_BE         BC( PORTD, LED)
#define LED_KI         BS( PORTD, LED)

int main( void)
{
   LED_ENABLE;
   LED_BE;
   ...

Ezek már egészen használható megoldások. Mikor az Internetet túrtam, Basic meg másik C, meg Arduino, meg megint más kódokat is láttam. Igaz ugyan, hogy regiszter/forráskód szinten WINAVR-ben enyém az összes lehetőség a uC programozására, de nagyon irigyeltem, hogy azokban egy-két utasítással elintézik mondjuk a soros port inicializálását. És olyan utasítások vannak bennük, amelyek beszédesek. Programozásnál külön tanulság, ha valami miatt néhány hónapig nem foglalkozom a kedvenc uC-immel, utána a saját kódomon is el kell gondolkoznom. Mindig igyekeztem olyan kódot írni, ami mások számára is értheő, de kérdéses, hogy a kódjamon mások hogyan tudnak elmenni. Sokat segítenek a weblapomra feltett munkáim, azokból ki lehet egy-két dolgot ollózni. De ha megnézzük ezeket a kódokat, és változtatni kell rajtuk, akkor megint bele kell mélyedni a regiszterekbe és bitjeikbe.

Általában problémákat szeretnénk megoldani. A megoldáshoz meg kell nézzük az adott uC milyen lehetőségeket biztosít, milyen regisztereket kell felprogramoznunk, és lekódolni az algoritmusokat. Így jött a gondolat, amely már egy másik szintre emeli a munkát, hogy probléma függő makrókat kellene készíteni, amelyek kompletten tartalmaznák a regisztereket, biteket, az adatokat, illetve kiszámolnák azokat szükség esetén. Nézzünk egy egyszerű példát a port bitek kimenetre állítására:


#define PIN_D0_OUT    BS( DDRD, 0)
#define PIN_D1_OUT    BS( DDRD, 1)
...

Munka közben azután úgy gondoltam, hogy ugyan a regiszterek számos kombinációra adnak lehetőséget, de a gyakorlatban nem mindegyiket használja az ember, pontosabban én :), vagyis nem minden kombinációt kell megírni, hanem csak azokat, amelyek általában előrébb mozdítják az embert. Sikerült a soros port komplett konfigurálását is egyetlen makróval megoldanom, pontosabban az általam használt értékekre egy-eggyel:


#define USART_SET_9600_8N1   UBRRL= F_CPU/ 16UL/ 9600- 1;  \
                             UBRRH= ( F_CPU/ 16UL/ 9600- 1)>> 8;  \
                             UCSRC= SET_UCSRC+ ASYNCRON+ BIT_8+ PARITY_NO+ STOPBIT_1;  \
                             UCSRB= RX_ENABLE+ TX_ENABLE+ IT_RX_ENABLE
                             
#define USART_SET_19200_8N1  UBRRL= F_CPU/ 16UL/ 19200- 1;  \
                             UBRRH= ( F_CPU/ 16UL/ 19200- 1)>> 8;  \
                             UCSRC= SET_UCSRC+ ASYNCRON+ BIT_8+ PARITY_NO+ STOPBIT_1;  \
                             UCSRB= RX_ENABLE+ TX_ENABLE+ IT_RX_ENABLE
                             
#define USART_SET_115200_8N1 UBRRL=F_CPU/ 16UL/ 115200- 1;  \
                             UBRRH=( F_CPU/ 16UL/ 115200- 1)>> 8;  \
                             UCSRC=SET_UCSRC+ ASYNCRON+ BIT_8+ PARITY_NO+ STOPBIT_1;  \
                             UCSRB=RX_ENABLE+ TX_ENABLE+ IT_RX_ENABLE

Ezeket használva így néz ki a soros port inicializálása:


#define F_CPU                4000000             // orajel 4MHz

int main( void)
{
   USART_SET_9600_8N1;
   ...

Az alábbi egy makró csomag, a T0 számláló kezeléséhez ( viszonylag ez a legegyszerűbb, a többi a NORMAL-on kívül még számos üzemmóddal rendelkezik, CTC, PWM...):


#ifndef T0_DIVIDER
   #define T0_DIVIDER        1                   // 1-8-64-256-1024
#endif

#define T0_STOP              TCCR0=0

#if T0_DIVIDER == 1
   #define T0_NORMAL         TCCR0=0B00000001
#endif

#if T0_DIVIDER == 8
   #define T0_NORMAL         TCCR0=0B00000010
#endif

#if T0_DIVIDER == 64
   #define T0_NORMAL         TCCR0=0B00000011
#endif

#if T0_DIVIDER == 256
   #define T0_NORMAL         TCCR0=0B00000100
#endif

#if T0_DIVIDER == 1024
   #define T0_NORMAL         TCCR0=0B00000101
#endif

#define T0_COUNT_EXTERN_FALL TCCR0=0B00000110

#define T0_COUNT_EXTERN_RISE TCCR0=0B00000111

#define T0_IT_OVF_ENABLE     BS(TIMSK,TOIE0)

#define T0_IT_OVF_DISABLE    BC(TIMSK,TOIE0)

#define T0_T(usec)           TCNT0=255-(usec/((T0_DIVIDER*1000000)/F_CPU))

Ezeket használva így lehet beállítani, hogy T0, kb. 100 usec múlva generáljon egy megszakítást (amit persze le kell kezelni):


#define F_CPU                4000000             // orajel 4MHz
define T0_DIVIDER            64

int main( void)
{
   T0_T( 100);
   T0_IT_OVF_ENABLE;
   T0_NORMAL;
   ...

Szólnom kell arról, hogy jelenleg ATmega8 uC-t használok, az idézett kódok azon futnak. A definíciók eltérnek a standard WINAVR-hez adott .h file-tól. Egy saját tkiraaly_atmega8.h file-t használok, amibe beletettem a makróimat is. Nem vizsgáltam, hogy más AVR uC-ken is helytállóak-e? Pontosabban, korábban ATiny2313-mal dolgoztam, és voltak regiszterek, bitek, amelyek eltértek. Ha valaki nem is használja a tkiraaly_atmega8.h-t, a makrók, esetleg kis módosítással akkor is alkalmazhatóak. Nem vagyok még kész a uC összes egységének beálítására szolgáló makrókkal, ahogy haladok egyik programról a másikra, úgy gyarapodnak. Hiba is előfordulhat bennük, nem tudom kipróbálni az összes variációt.

Érdekes kérdés, hogy miért használjunk makrókat, mikor az adott modulok kezeléséhez írhatunk függvényeket, függvény könyvtárakat is? Régebben (Clipper, C) én is azt az utat jártam, hogy saját függvény könyvtárat készítettem. A GCC/WINAVR-nél még nem jártam utána hogy lehet relokálható object-ekből library-t készíteni, amiből a link-elés során csak a szükségesek fordulnának be a kódba. A makró definíciók nem kerülnek befordításra, csak ha hivatkozunk rájuk. Másrészt nem kellene vele foglalkoznom, de nem tetszik, amit függvény hívások esetén csinál a C. Ment egy csomó regisztert, néha feleslegesen, utána visszatölti, ami növeli a kódot, meg viszi a végrehajtási időt is. Ha valami gyorsabb dolgot szeretne az ember írni, ez nem mindegy. Azután van még egy fontos dolog, a makrók a fordítás során értékelődnek ki, tehát a fordító számítógépen fut, nem azon a uC-n, amelyen majd a lefordított programunk fog futni. Ez fordítva is igaz, ha valamit futás közben kell állítgatnunk, arra a makró nem biztos hogy alkalmas.

Egy kis lazítás a magasröptű makrókombinációk után. Az alábbi definíciók azért születtek, mert közismert, hogy a lusaság a fejlődés mozgatórugója, és ez alól hogy lehetnék én kivétel? :)


#define U8                   unsigned char
#define U16                  unsigned int

Végül egy habkönnyű megoldás LCD karakterek definiáláshoz, amit korábban az AVR LCD - Magyar betűk című lapomon mutattam be:


#define Cx________   0
#define Cx_______X   1
#define Cx______X_   2
#define Cx______XX   3
#define Cx_____X__   4
#define Cx_____X_X   5
#define Cx_____XX_   6
#define Cx_____XXX   7
#define Cx____X___   8
#define Cx____X__X   9
#define Cx____X_X_  10
#define Cx____X_XX  11
#define Cx____XX__  12
#define Cx____XX_X  13
#define Cx____XXX_  14
#define Cx____XXXX  15
#define Cx___X____  16
#define Cx___X___X  17
#define Cx___X__X_  18
#define Cx___X__XX  19
#define Cx___X_X__  20
#define Cx___X_X_X  21
#define Cx___X_XX_  22
#define Cx___X_XXX  23
#define Cx___XX___  24
#define Cx___XX__X  25
#define Cx___XX_X_  26
#define Cx___XX_XX  27
#define Cx___XXX__  28
#define Cx___XXX_X  29
#define Cx___XXXX_  30
#define Cx___XXXXX  31


const uint8_t magyar_betuk[] PROGMEM =
{
   0,
   Cx______X_,
   Cx_____X__,
   Cx____XXX_,
   Cx_______X,
   Cx____XXXX,
   Cx___X___X,
   Cx____XXXX,
   Cx________,
   0xFF
};

Kicsit hunyorítsunk ;-).

Itt a vége, fuss el véle, legytek az én vendégeim, innen letölthetitek a hozzávalókat összecsomagolva.