2016-05-01
Az elmúlt két hetet C++ tanulásával, Arduino forrás file-k és fórumok böngészésével valamint ide nem illő kifejezések mormolásáva töltöttem. Szerencsémre családom nagyon türelmes. Utálhatnám is azokat akik kitalálták C/C++-t, az Arduino-t, de most, hogy sikerlüt összehoznom amit szerettem volna, azt hiszem köszönet illeti őket, velük együtt azokat az egyetemi oktatókat, akik publikálták jegyzeteiket a C++-ról, illetve azokat a fórumlakókat, akik hozzám hasonló tévelygőknek segítenek.
Van ez a dialektika nevű dolog, amit még talán a görögök találhattak föl, vagyis, hogy egy éremnek két oldala van. Szóval, az Arduino kódokban jó megoldások találhatók, általánosan oldják meg a feladatokat, hogy mindenre jók legyenek. Sajnos ennek ára is van, tele vannak sose használt rutinokkal, úgy hízik velül a kód, mint a ... Ez a korlátozott memóriájú uC környezetben komoly hátrány. És a terjengős programot a uC-nek be is kell futnia, az meg idő. Különben nem a core-ban, hanem a libraries-ben találhatók borzalmak is. Az egyetemi jegyzetek, oktató blog-ok tök jó, hogy hozzáférhetőek, de általában nem oktatnak, hanem egyfajta kivonatát képezik egy C++ manuálnak. Kevés példaprogramot tatalmaznak, főleg nem olyan kontextusban, hogy van egy problémám, ezzel a C++ nyelvi elemmel oldható meg. Pedig volt ez a Kernighan/Ritchie páros által elkövetet forrásmunka a kezdetek kezdetén, amiben a C nyelvet példákon keresztül mutatták be. Sokan valószinűleg elméleti matematikusok lehetnek, mert hardware közeli kódot jegyzetekbe az ember nem talál. Viszont lehet találni eröltetett példákat (lehet, hogy aki írta, az sem értette valójában mire kell az adott elem), és angol tudás nélkül nehezen kisilabizálható megnevezéseket. A C++ oktatást már a programozás a programozásért szintjére emelik (L'art pou l'art), vagyis szerintem öncélúvá valik az előadásuk. Persze így a nebulóktól az egész C++ manual visszakérdezhető, és bármikor sarokba állíthatóak, hogy na ugye, hogy nem tudod? Garantált a C++ megutálása. Szerintem, ha valakinek valami részletere szüksége lesz, akkor majd utána néz a neten. Ha tananyagot állítanék össze (előfordult már velem felsőfukú szinten is), a tanulóknak az anyag gerincét oktatnám/követelném meg, és ezen belül is igyekeznék az egyes elemeket úgy bemutatni, hogy az mire jó. Különben boccs, befejeztem a füstölgést.
Tehát amit irigyeltem az Arduino-tól, az egyszerű programozás, pár soros progrqammal megoldható mindenféle feladat, és úgy tűnik a programozásban sem kell annyira otthon lenni a készen kapott objektumok használatához. Ezt valójában a C++ teszi lehetővé. Tehát nézzük, mire jutottam ezzal a feladattal, hogy egszerűen akartam a soros portra kiiratni számokat, szövegeket?
/*******************************************************************************
* Author - Kiraly Tibor
* www.tkiraaly.hu
* Date - 2016.04.30.
* Chip - AVR
* Compiler - avr-gcc (WinAVR)
*
* kprint es kserial kiprobalasa
*
*******************************************************************************/
#define F_CPU 4 MHZ
#include "tkiraaly_atmega8.h"
#include <util/delay.h>
#include "tkiraaly_kserial.cpp"
int main( void)
{
U8 c= '.';
int n= -1986;
USART_9600_8N1;
kserial s( 8);
for(;;)
{
s.kputchar( c);
s.print( " - ");
s.print( c, '3');
s.print( " - ");
s.print( c, 'x');
s.print( " - ");
s.print( c, 'b');
s.print( " - ");
s.print( n);
s.print( " - ");
s.print( -n, '3');
s.print( " - RAM");
s.print( F( " - Flash"));
s.newline();
_delay_ms( 500);
}
}
Roppant királyúl érzem magam :). Így néz ki kimenet a terminál programban.
Még mindig királyság ez a kserial class. Nézzünk be a motorháztető alá! Most így néz ki a "tkiraaly_kserial.cpp", később lehet, hogy majd bővítem. Az osztály a kprint-ből származik. A kprint oldja meg a számok konvertálását stringre, illetve a stringek kiírását. A objektum inicializálásánál meg kell adni, hányas portot kezelje, Atmegáéknál 0..3 lehet. Van benne egy newline(), ami ugye a következő sor elejére léptet, de a fontosabb függvény a kputchar(), ami egy betüt ír ki a soros portra. Ezzel a függvénnyel fog dolgozni a kprint is. A fuggvény elég egyszerű lenne, csak a több port kezelése miatt lett kicsit megbonyolítva. Láttunk már hasonló megoldást a "tkiraaly_kpin.c"-ben, hogy csak annak a portnak a kódja kerül beszúrásra a forráskódba, amelyik ténylegesen létezik az adott uC-ben. Ha mondjuk egy Atmega8-nál nem a 0-ás portot adjuk meg, pedig csak az van, akkor ugye nem fog semmi történni.
/*******************************************************************************
* Author - Kiraly Tibor
* www.tkiraaly.hu
* Date - 2016.04.30.
* Chip - AVR
*
* kserial - soros portra kiiro objektum
*
* Ez a kprint-bol szarmaztatott osztaly.
* Alkalmazhato az Atmega csaladban, tipustol fuggoen, 0..3 soros portra
* lehet az objektumot letrehozni.
* A soros portot a korabbi makroimmal kell inicializalni, pl.:
* USART_SET_9600_8N1
*
*******************************************************************************/
#ifndef tkiraaly_kserial
#define tkiraaly_kserial
#include "tkiraaly_kprint.cpp"
#include "tkiraaly_avr_macros.h"
class kserial : public kprint
{
public:
U8 _serial_port;
kserial( U8 s) { _serial_port= s;}; // 0, 1, 2, 3
virtual void kputchar( U8);
void newline( void);
};
void kserial::kputchar( U8 c) // egy betu kiirasa
{
switch ( _serial_port)
{
#ifdef UCSRB // ATmega8 eseten
case 0:
while( !B_R( UCSRA, UDRE));
UDR= c;
break;
#endif
#ifdef UCSR0B // csak az lesz definialva, amelyik az adott uC-ben van
case 0:
while( !B_R( UCSR0A, UDRE0)); // 1, ha kesz az adat fogadasara
UDR0= c;
break;
#endif
#ifdef UCSR1B
case 1:
while( !B_R( UCSR1A, UDRE1));
UDR1= c;
break;
#endif
#ifdef UCSR2B
case 2:
while( !B_R( UCSR2A, UDRE2));
UDR2= c;
break;
#endif
#ifdef UCSR3B
case 0:
while( !B_R( UCSR3A, UDRE3));
UDR3= c;
break;
#endif
}
}
void kserial::newline( void) // kocsi vissza, soremeles
{
kputchar( '\r');
kputchar( '\n');
}
#endif
Így néz ki a "tkiraaly_kprint.cpp". Királyúl érzem magam :). Az a lényege, hogy bármire megoldja a formattált kiírást, csak annyit vár el, hogy kapjon egy függvényt, amivel betünként ki tudja pötyögtetni amit kell. Én úgy gondoltam, hogy ugyan mindenféle kiírási variációt leprogramozhatnék, de amit használni fogok az a char/unsigned char (nálm U8), valamint az int/ unsigned int (nálam U16) bináris/decimális/hexadecimális formája, és a string kiírása RAM-ból, illetve program (FLASH) memóriából. Aki még most ismerkedik a C++szal itt egy érdekes dolgot figyelhet meg, a függvény overloading-ot. Nem tudom ki volt az a bárgyú Shakespeare fordító, aki ezt túlterhelésnek fordította, és az a lektor, aki ezt jóváhagyta, hozzáértő nem lehetett. Bár ugynígy a falra mászok , hogy szilícum wafert ostyának fordítják szelet helyett, még jó hogy a chip kicsit meghonosodott nálunk, és nem szilícium morzsákról beszélünk. Látszólag arról van szó, hogy a C++-ban több függvénynek is lehet ugyanaz a neve. A nevük ugyanaz, de figyeljuk, meg, hogy eltérő adatokat fogadnak. A C++ ezzel egyszerűsíti a programozó munkáját, hogy több adathoz is használhatja ugyanazt a nevet, a fordító az adat alapján dönti el, hogy melyik változatnak adja azt. Persze a programozónak előzetesen meg kell írnia az összes adattípusra a függvényváltozatot :). Tehát ha valaki valami egyébre vágyik, mondjuk írja ki a "+" előjelet is, vagy kezeljen mondjuk long int-et, nyílván beleírhatja. A legfontosabb dolgokat leírtam a fejlécben.
/*******************************************************************************
* Author - Kiraly Tibor
* www.tkiraaly.hu
* Date - 2016.04.30.
* Chip - AVR
*
* kprint - szamokat es szoveget kiiro objektum
*
* Igazabol ez egy absztrekt osztaly, lcd-hez, soros port-hoz... stb.
* A belole szarmaztatott osztalyban kell egy kputchar() fuggvenyt
* definialni, ami elvegzi egy betu kiirasat.
* A szamokat alap esetben elojelesen, decimalisan irja ki.
* Opcionalis formatumok:
* X - hexadecimalis int - 0x0000
* x - hexadecimalis char - 0x00
* b - binaris char - 0B11110000
* 1..5 - fix hosszusagu decimalis - 1-5 szamjegy + elojel, ha van
* 0 - valtozu hosszusagu decimalis
*
* Ha a szam nem fer ki a megadott formatumban, *****-kat ir ki.
*
*******************************************************************************/
#ifndef tkiraaly_kprint
#define tkiraaly_kprint
#include <avr/pgmspace.h>
#include "tkiraaly_avr_macros.h"
#define F( s) (const khelper *)( PSTR( s)) // makro khelper-hez
class khelper; // seged tipus PSTR nyomtatashoz
class kprint
{
public:
kprint() {};
virtual void kputchar( U8) {}; // egy betut ir ki
void print( char n, U8= '0'); // format - b, x, 0..3
void print( U8 n, U8= '0');
void print( int n, U8= '0'); // format - X, 0..5
void print( U16 n, U8= '0');
void print( const char *);
void print( const khelper *);
};
void kprint::print( const char *s) // string kiirasa RAM-bol
{
while( *s) kputchar( *s++);
}
void kprint::print( const khelper *s) // string kiirasa FLASH-bol
{
U8 c;
PGM_P p = ( PGM_P)( s);
while (1) {
c = pgm_read_byte( p++); // ez valojaban makro
if (c == 0) break;
kputchar( c);
}
}
void kprint::print( char data, U8 format) // char kiirasa - b, x, 0..3
{
print( (int)data, format);
}
void kprint::print( U8 data, U8 format) // unsigned char kiirasa - b, x, 0..3
{
print( ( U16)data, format);
}
void kprint::print( int data, U8 format) // int kiirasa - X, 0..5
{
U8 e= ' '; // elojel
if( data < 0) // minusz
{
data= -data;
e= '-';
}
kputchar( e);
print( ( U16)data, format);
}
void kprint::print( U16 data, U8 format) // unsigned int kiirasa - X, 0..5
{
char buf[ 9]; // betuknek + \0
char * p= buf; // buffer eleje
U8 a= 10; // alap
U8 h= 0; // hossz
U8 t= '0'; // feltolto betu
U8 i;
switch( format)
{
case 'b': a= 2; h= 8; break; // 0B00001111
case 'x': a= 16; h= 2; break; // 0x00
case 'X': a= 16; h= 4; break; // 0x0000
case '0': h= 5; break; // 00000
case '1':
case '2':
case '3':
case '4':
case '5': h= format- '0'; t= ' '; // 0..00000
}
*p= '\0'; // \0
i= h;
while( i--)
{
if ( data == 0) // feltoltes
{
if ( format != '0') *++p= t;
}
else // szamjegy kiszamolasa
{
*++p= data% a; // a maradek a helyiertek
if( *p > 9) *p+= 'A'- 10; // 9-nel nagyobb, akkor ABC..
else *p+= '0'; // 9-ig szamjegy
data/= a;
}
}
if ( data > 0) // hiba, nem fert ki *****
{
p= buf;
i= h;
while( i--) *++p= '*';
}
switch ( format)
{
case 'b': kputchar( '0'); kputchar( 'B'); break; // 0B
case 'x':
case 'X': kputchar( '0'); kputchar( 'x'); break; // 0x
}
while( *p) kputchar( *p--); // kiras visszafele
}
#endif
Na és mik a kevésbé fontos dolgok? Vagy éppen a nagyon is fontosak, amiket apró betükkel szoktak írni? Volt amire rá kellet jönnöm, volt amiről fórumokon olvastam. A közreadott kódok, higgyétek el nagyon szépen követhetők, szerintem nem tartalmaznak felesleget. Különben nem kell elhinni. Az Arduino core print osztályából indultam ki. Ahhoz kapcsolódik a serial, meg a string, meg mások is. Teljesen korrekt kód, tanulményozzátok! A prospektusok leírják, hogy van olyan eset, amikor úgynevezett virtuális függvényt használunk. Ez tipikusan az az eset. A print osztály a szülő osztály, első sorban konverziós függvények gyűjteménye a későbbi gyermek periféria kezelő osztályokhoz. Még nem tudjuk, hova kell kiírnunk, csak azt tudjuk, hogy lesz egy függvényünk, aminek majd betünként kell átadnunk a kiírandókat. C-ben ez egy mutató átadásával oldható meg, ami egy olyan függvényre mutat, ami egy char-t vár. De ez sokkal cifrább. Két éjszakát küzdöttem vele. Persze jobb lett volna ha nem autodidakta módon tanulgatnám a C++-t, de hát a C-vel is így voltam... meg a többivel. Szóval azt írják, hogy a szép megoldás, ha tisztán virtuális függvényt használunk.
virtual void kputchar( U8)= 0;
Ilyenkor kapunk egy rejtélyes hibaüzenetet, ami a "__cxa_pure_virtual" mutatóra vonatkozik. Biztos van aki vágja, hogy a virtuális függvény használatánál a fordító program generál egy hibakezelő függvényt, ami futás közben standard hibakimenetre kiírná, ha netán a virtuális függvény nem definálnánk a gyerek osztályban. Na de egy uC-ben hol van standard hibakimenet? Hát ez az! Én azt a megoldást találtam, hogy rögtön megadtam neki egy üres függvény törzset. Ez ugyanúgy nem csinál semmit, de ilyenkor a fordító nem generál hozzá hibajelzést. Erre sem öt perc alatt jöttem rá.
virtual void kputchar( U8) {};
Az Arduino print és serial osztálya, illetve a LiquidCrystal library-je is elvitte néhány napomat. Én az egyszerűség kedvéért külön függvényt használtam a betü kiírásra, de ott több kiírásnak ugyanaz a neve, és egy darabig emésztgettem, ahogy az "using" belekavarásával, csak a betü kiírást definiálja újra.
Fontos dolog, hogy hol tároljuk a kiírandó szövegeket (string). Alapvetően a fordító bepakolja a RAM-ba. Valójában ez úgy történik, hogy a kiinduló adatok természetesen a program (FLASH) memóriában tárolódna, de rögtön a program indulásakor egy rutin átpakolja a RAM-ba, adatoknak ott a helye. Ámde RAM-ból nincs túl sok. Tehát ha egy szószátyár programot írunk, a sok szöveg úgyis fix, jobb ha a program memóriában vannak. C-ben egyszer már megtanultuk, hogy a "PSTR" makróval megdumájuk a fordítót hova tegye a string-et, majd a "pgm_read_byte()"-tal ami NEM FÜGGVÉNY, HANEM MACRO, szépen betünként előszedegetjük. Nem értettem teljesen meg, de valahogy eltérő a fordítása/kezelése ennek C-ben meg C++-ban, ezért ílyen egyszerűen nem müxik. Egy ötösért ki keresi ki a megoldást az Arduino print-ből? Ja nem, már feltettem ide, igaz ez kicsit egyszerűbben néz ki. Definiálunk egy üres osztályt. Ezt sem tudtam, hogy ilyet lehet... Csak a mutató típusa kedvéért.
#define F( s) (const khelper *)( PSTR( s)) // makro khelper-hez
class khelper; // seged tipus PSTR nyomtatashoz
Azután tartozik hozzá egy F() macro, ami megmondja a fordítónak, hogy a string-ünket a PSTR() macro szerint tegye a FLASH-be, és a visszadott mutatót rögtön konvetálja az üres osztályra. Persze nem történik semmi, csupán a fordító nyilvántatásában kerül módosításra, hogy ez más nem egy string-re, mutat. Különben C++-ban a típus konverzióra is van külön cast utasítás, abból is több, ebben a szituációban a reinterpret_cast-ot használták, akármit is jelentsen :).
s.print( " - RAM");
s.print( F( " - Flash"));
Azután amikor a print megkapja a mutatót, ami erre az osztályra mutat, akkor rögön konvertálja "PGM_P" típusra, és innen FLASH-ben tárolt string-ként kezeli. De ez az egész a fordítás alatt játszódik le. L'art pour l'art. Viszont müxik. Ki lehetett az a programozó, aki kitalálta, hogy így lehet fordító fejét átverni?
void kprint::print( const khelper *s) // string kiirasa FLASH-bol
{
U8 c;
PGM_P p = ( PGM_P)( s);
while (1) {
c = pgm_read_byte( p++); // ez valojaban makro
if (c == 0) break;
kputchar( c);
}
}
Itt a vége, fuss el véle, legytek az én vendégeim, innen letölthetitek a programokat, miegymást összecsomagolva.