💻 Arduino programozás - C++

Utoljára frissítve: 2024. 06. 15. 12:30:15

Az Arduino egy szabad szoftveres, nyílt forráskódú elektronikai fejlesztőplatform, arra tervezve, hogy a különböző projektekben az elektronikus eszközök könnyebben hozzáférhetőek, kezelhetőek legyenek. Széles tömegek számára elérhető, mivel olcsó, könnyen beszerezhető, egyszerűen programozható, és csatlakoztatható más eszközökhöz. Az Arduino eszközöket a saját programnyelvén az Arduino-n lehet programozni. Bár ez a nyelc nagyon ahsonló a C++-hoz, pár különbség mégis van. Ennek a segédletnek a célja, hogy az Arduino nyelv és függvénykészlet alapvető elemeit bemutassa.

A segédlet végén összegyűjtöttünk pár valós alakalmazásokra hasznos kódpéldát ill. egyszerű, komplett programokat, amelyek azonnal kipróbálhatóak és (remélhetőleg) megkönnyítik az Arduino felprogramozását.

A segédletet a Ganz Ifjúsági Műhely készíti és teszi nyilvánossá. A segédletet szerkeszti: Kimmel Gábor.

A tartalomhoz hozzájárultak:

  1. Kimmel Gábor (kimmelgabor@gmail.com)
  2. Kiss Attila (werdase@gmail.com)

Alapszabályok


Pár egyszerű szabály betartásával már el tudjuk kezdeni az Arduino nyelven írt programunk írását:

  • A parancsokat pontosvessző zárja
digitalWrite(13, HIGH);
  • Egy sorba több parancs is írható (de nem javasolt!)
digitalWrite(13, HIGH); int i = 0;
  • Parancsblokkot { és } karakterekkel tudunk írni.
  • Elágazásra az if, else és switch parancsokat tudjuk használni.
  • Ciklusokhoz a while, és for parancsokat használhajtuk.
  • Az Arduino programnak előre definiált struktúrája van.
  • Az Aurduino rendszernek van egy nagy halmaz előre elkészített függvénykészlete.
  • Kommentsort a // jelekkel lehet írni, komment-blokkot a /* karakterekkel kezdünk és */ karakterekkel zárunk.
/* Ez egy komment */ digitalWrite(13, HIGH); //Ez is

Jót teszünk magunknak és másoknak, ha a következő tanácsokat is betartjuk:

  1. A változónevek legyenek mindig egyértelműek.
  2. Mindig használjunk kommenteket. Nem muszáj minden sort kommentelni, de az összetartózó részek legyenek leírva, hogy mi mit csinál.
  3. Ha gyakran ismétlünk bizonyos részeket, törekedjünk arra, hogy metódusba szervezzük ezeket a részeket - NE a kódrészletet másoljuk le többször!

Nyelvi elemek listája


Jel Magyarázat
; Parancs végét jelző karakter
'
Karakter írására alkalmas jel
" Szöveg írására alkalmas jel
! Logikai NEM
|| Logikai VAGY
&& Logikai ÉS
| Bitenkénti VAGY
& Bitenkénti ÉS
<< Bitek eltolása balra
>> Bitek eltolása jobbra
== Egyenlőségvizsgálat
!=
Egyenlőtlenségvizsgálat
+, -, *, /
Matematikai alapműveletek
= Értékadás
+=, -=, *=, /=
Értékmódosítás
++, --
Eggyel növelés ill. csökkentés

Változók, típusok


Típusok


A számítógép memóriája szigorúan véve csak egész számokat tud tárolni, de okos tervezéssel tudunk törtszámokat, karaktereket, szöveget, sőt, ennél összetettebb adatfajtákat is használni.

Számszerű típusok


A számszerű típusok közül el kell választani a teljes számokat és a törtszámokat tárolni képes típusokat. Előbbiek csak tizedesvessző nélküli számokat tudnak tárolni (tehát 1-et és 2-t igen, 1,5-öt már nem), utóbbiak viszont képesek mindkettőt.

Típus Bájtok száma
Minimum érték
Maximum érték
int 2 -32 768 32 767
uint 2 0 65 535
short 2 -32 768 32 767
ushort 2 0 65 535
long 4 -2 147 483 648 2 147 483 647
ulong 4 0 4 294 967 295

Karakteres típusok


  1. char
  2. String

Léteznek más, bonyolultabb típusok is, erről a beépített függvényeknél tudsz továbbolvasni.

Változók


Változókat nekünk kell kézzel létrehozni ha használni akarjuk őket. Először meg kell adni a típusukat, utána az azonosítójukat vagy nevüket. Egyből ezután érdmes nekik értéket is adni (ld Értékadás). A legjobb olyan értéket adni, amiről tudjuk, hogy az az alapértelmezett érték, tehát "valós" körülmények között nem fordulhat elő. Ha pl. 0-5 közötti számokat akarunk a változóba írni, egy jó alapértelmezett érték a -1.

int valtozo = -1; //Típus és név, majd értékadás

Attól függően, hogy hol hozzuk létre a változót beszélhetünk globális és lokális változókról.

int globalis;

void setup()
{
	globalis = 0;
}

void loop()
{
	int lokalis = 0;
	
	globalis = globalis + 1; //1, 2, 3, 4, stb.
	lokalis = lokalis + 1; //mindig 1!
}

A fenti programban létrehoztunk egy globális és lokális változót. A globális értéke folyamatosan nőni fog, míg a lokálisé ugyanaz marad. Miért?

A lokális változók "megsemmisülnek", amint a blokk, amiben létrehoztuk őket véget ér. A globális változónkat egyből a program elején hoztuk létre, tehát "örökéletű" marad. Lokális változót olyan helyen érdemes használni, ahol csak ideiglenesen van szükségünk egy plusz tárolóhelyre, globálisat pedig ott, ahol az egész programot befolyásoló adatokat akarunk tárolni.

Tömbök


A tömbök ugyanolyan típusú változóknak a csoportja. Tömböt bármilyen típusból létre lehet hozni.

int[] valtozo = int[3];

Itt létrehoztunk egy 3 méretű tömböt. A különböző elemekhez az ún. indexeren keresztül tudunk hozzáférni. Az indexek mindig 0-tól kezdődnek.

valtozo[0] = 1;
valtozo[1] = 5;
valtozo[2] = 10;

Nagyon figyelni kell arra, hogy nehogy túlindexeljük a tömböket (nagyobb vagy kisebb index, mint ami létezik)!

Eggyel bonyolultabb, amikor többdimenziújú tömböke thozunk létre. Ezek olyan tömbök, amelynek az elemei maguk is tömbök. Többdimenziójú tömbök pl. Kijelzőre kikerülő adatok tárolására nagyon hasznosak.

int tobbdimenzios[2][3] =
{
  { 1, 2, 3 },
  { 4, 5, 6 }
};

A tömbök különösen hasznosak, ha ciklusokban tudjuk őket használni!

Műveletek


Értékadás


Egy változót nem elég csak létrehozni, értéket is kell adni neki, hogy használható legyen. Egy változó értéke többféleképpen módosítható.

A legegyszerűbb értékadás, amikor egyértelműen megadjuk a változó értékét

int i;
int j;

i = 3;
j = -5;

i = j; //i=-5

Ha egy változót csak egy egyszerű számmal szeretnénk módosítani, használhatjuk a négy alapműveletet egyszerűsítő értékadásokat is.

int i = 0;

i += 3; //i=3
i *= 2; //i=6
i -= 4; //i=2
i /= 2; //i=1

Különleges művelet az inkrementálás és a dekrementálás. Ez esetben pontosan 1-el tudjuk növelni vagy csökkentei egy változó értékét.

int i = 0;

i++; //i=1
i--; //i=0

Aritmetikai műveletek


Az aritmetikai műveletek olyan műveletek, amiket számokon lehet használni. Mivel az Arudino nyelvben a legtöbb adat amivel dolgozunk számszerű, ezért a legtöbb változón használhatóak. Eredményük mindig egy igaz-hamis érték lesz. Leginkább fletételek írására használhatjuk ezeket a műveleteket.

A kettő legfontosabb ilyen művelet az egyenlőség- és különbözőségvizsgálat. Két további ilyen művelet a nagyobb- illetve kisebb vizsgálat.

int a = 10;  //hamis
int b = 15;  //igaz
int c = -3;  //igaz
int d = 10; //igaz 

int q = a == d; //egyenlőségvizsgálat - igaz
int r = b != d; //különbözőségvizsgálat - igaz
int s = b &gt; c; //relációs vizsgálat - igaz
int t = d &lt; c; //relációs vizsgálat - hamis

Logikai műveletek


Az Arduino nyelv egy erősen kapcsolásokhoz kapcsolódó nyelv. Emiatt logikai függvényeket is lehet vele írni.

A logikai függvények alapvetően három műveletet ismernek

  1. ÉS (AND)
  2. VAGY (OR)
  3. NEM (NOT)

Ezekből lehet további műveleteket lérehozni

  1. ÉS-NEM (NAND)
  2. VAGY-NEM (NOR)
  3. EGYENLŐ (EQU)
  4. KÜLÖNBÖZŐ (XOR)

Érdekes módon az Arduino (és C++) nyelvben nincs logikai változótípus. Helyette használható bármelyik szám, ahol a 0 a hamis, bármelyik másik érték (pl. 1) az igaz értéket jelenti. Így már lehet írni logikai kifejezéseket is. Ezeket több jel feldolgozására, vagy feltételek írására lehet használni. Érdemes viszont mindig egy igaz értéket használni, mert az EGYENLŐ és KÜLÖNÖZŐ műveletek számszerű egyenlőséget vizsgálnak!

A kódrészletek között találsz egy egyszerű logikai típus implementációt!

A nyelvben a három alapvető, ill. az egyenlőség és különbözőség-vizsgálatra beépített parancsok vanak. A NAND és NOR kifejezéseket ezekből lehet létrehozni.

int a = 0;  //hamis
int b = 1;  //igaz
int c = 3;  //igaz
int d = -5; //igaz 
int q = 0;

q = a &amp;&amp; b; //ÉS
q = a || c; //VAGY
q = !a      //NEM
q = b == d; //EGYENLŐ
q = a != c; //KÜLÖNBÖZŐ

q = !(a &amp;&amp; d) //ÉS-NEM
q = !(b ||c) //ÉS-VAGY

Mint a NAND és NOR kifejezéseken látható, a logikai műveletek szabadon kominálhatóak egymással. A műveletek sorrendje balról jobbra halad, zárójelekkel viszont vezérelhető melyik művelet fusson le előbb (hasonlóan a matematikai kifejezésekhez),

q = !(d || (c &amp;&amp; a) &amp;&amp; b == d) //igaz, ha d igaz vagy c és a igaz, és d és d egyenlő

Bitműveletek


Mivel a mikrokontroller digitális lábait felfoghatjuk biteknek is (0 és 1 értékeik lehetnek), ezért a mikrokontrollereknél kiemelkedően fontos műveletek a bitek manipulálása. Sok esetben kisebb és gyorsabb kód írható, ha jól tudunk bárrni a bitek alacsony szintű kezeléséhez való eszközökkel

Két számot nem csak matematikailag lehet kombinálni, hanem bitentént is - ÉSelni és VAGYolni.

int i = 1;   //00000001
int j = 8;   //00001000
int k = 248; //11111000
i = i | j;   //00001001,  i=9
k = k &amp; i;   //00001000,  k=16

Ennél még hasznosabb, ha egy változó bitjeit tudjuk eltolni. Ez, ha számszerűen nézzük a változókat teljesen véletlenszerű eredményeket ad, de ha bitenként nézzük, akkor nehéznek gondolt feladatokat lehet meglepően egyszerűen megcsinálni.

int i = 1;  //00000001
i = i &lt;&lt; 3; //00001000,  i=8
i = i &gt;&gt; 2; //00000010,  i=2

Egy nagyon egyszerű Knight-Rider kódrészlet például valahogy így nézne ki (ld. Elágazáskezelés):

int i = 1;
int irany = 0;

...

if(i == 1) //00000001
{
	irany = 0;
}
else if(i == 128) //10000000
{
	irany = 1;
}

if(irany == 0)
{
	i = i &lt;&lt; 1;
}
else
{
	i = i &gt;&gt; 1;
}
PORTD = i; //PORTD az 1-8 lábakat fogja össze, erről bővebben az oktatóktól!
delay(200);

Elágazáskezelés


Ha meg akarunk vizsgálni egy változót, hogy mi az értéke, vagy attól függően, hogy mi az értéke, más és mást akarunk csinálni az if, else, és switch parancsokat használhatjuk.

If és else


A következő kód egy villogót valósít meg. Ha az i változó értéke 1, akkor világít a led, ha 0 nem. A következő lépshez megcseréljük az értékét a változónak és várakozunk.

int i;
	
void setup()
{
  i = 0;
  pinMode(13, OUTPUT);
}

void loop()
{
  if(i == 0)
  {
  	i = 1;
  	digitalWrite(13, HIGH);
  }
  else
  {
  	i = 0;
  	digitalWrite(13, LOW);
  }
  delay(300);
}

Switch - többirányú elágazás


Ha több irányba akarunk elágazni használhatunk sok ife-else-t de erre jobb megoldás a switchek használata

int i;
	
void setup()
{
  i = 0;
  pinMode(13, OUTPUT);
}

void loop()
{
  //Ez így rossz!
  if(i == 0)
  {
    //...
  }
  else if(i == 1)
  {
    //...
  }
  else if(i == 2 || i == 4)
  {
    //...
  }
  else if(i == 3)
  {
    //...
  }
  
  //Ez így jó!
  switch(i)
  {
    case 1:
      break;
    
    //Több eset is összevonható
    case 2:
    case 4:
      break;
    
    case 3:
      break;
  }
  delay(300);
}

Ciklusok


A programozás egy másik alapja, hogy bizonyos műveletek egymás után többször, adott esetben ismeretlen sokszor kell ismételnünk.

digitalWrite(LED, HIGH);
delay(500);
digitalWrite(LED, LOW);
delay(500);
digitalWrite(LED, LOW);
delay(500);
digitalWrite(LED, LOW);

Honnan tudhatjuk, hogy 3-zor kell villogtatni a ledet? Lehet, hogy a felhasználó akarja beállítani, hogy hány villogás legyen. Egyszerű kódisméléssel erre már nem tudunk felkészülni. Erre és hasonló feladatokra valóak a ciklusok.

While ciklus


Tegyük fel, hogy soros porton akarunk beolvasni adatot. Ehhez először küldünk egy olvasási kérelmet, és ezután megvárjuk, hogy megérkezzen az adat. Hogyan kezeljük, ezt a várakozást?

Serial.begin(9600);
Serial.write('keres');
delay(1000);
char c = Serial.read();

Ezzel a megoldással két baj van: ha gyorsan megjö az adat, akkor hiába vártunk 1 másodpercet. Másrészt semmi garancia nincs rá, hogy 1 másodperc alatt megjön az adat. Szerencsére a soros portnak van egy available metódusa, ami a még be nem olvasott karakterek számát jelzi. Tehát addig kell csinálnunk a semmit, amíg nincs mit beolvasni. Ezt a while paranccsal tudjuk megtenni.

Serial.begin(9600);
Serial.write('keres');
while(Serial.available() == 0)
{
}
char c = Serial.read();

A while parancs hasonlóan működik mint az if, azzal a különbséggel, hogy egészen addig fogja ismételni a benne lévő parancsokat, amíg a megadott feltétele igaz.

A while ciklusnak vagy egy ritkábban használt változata, a do-while. Ez egyszer mindenképpen lefut, utána pedig a feltételtől függően még többször (vagy másodszor már nem). Általában nem ajánlott használni, ezért ebbe a segédletbe nem kerül be.

For ciklus


A for ciklus alapvetően nem sokban különbözik a while ciklustól. Igazából csak egy rövidítés, de nagyon hasznos tud lenni, mert rövidebbé és olvashatóbbá teszi a kódot

//For ciklusfelépítés
for(kezdes; feltetel; modositas)
{
	...
}

//Ugyanez a ciklus while-al
kezdes;
while(feltetel)
{
	...
	
	modositas;
}

A fenti két ciklus megegyezik. A for ciklus egyszerűen használható pl. olyan ciklusokban, ahol sorszámokra van szükségünk:

int i;
for(i = 0; i &lt; 10; i++)
{
	Serial.write(i);
	delay(200);
}

Ez sorrendben ki fogja írni 0-9 számokat és mindössze 5 sor kód. Ha az első 100 számot szeretnénk kiíratni - egy hosszú, de ismétlődő folyamat - akkor is ugyanígy nézne ki a kód, csak a feltétel részt kéne átírni. Ez sokkal átláthatóbb, mint 100 sorban ugyanazt minimális változtatással leírni.

Ahogy volt már róla szó, tömböket kifejezetten hasznos a ciklusokkal, egész pontosan a for ciklussal használni. Nagyon egyszerű pl. n darab elem kezelése (keresés, sorbarendezés, sorban kiírás stb.).

int[] tomb = int[10]; //Létrehozunk egy 10 elemű tömböt
for(i = 0; i &lt; 10; i++)
{
	tomb[i] = i * 2; //Egy sor kódból értéket adtunk 10 különböző változónak!
}

Metódusok


Sokszor hasznos tud lenni, ha egy adott kódrészletet többször is fel tudunk használni. Például ha egy programban valami tevékenységet akarunk jelezni, villogtathatjunk egy ledet:

digitalWrite(LED, HIGH);
delay(500);
digitalWrite(LED, LOW);

Az előző kódrészlettel azonban van egy nagy probléma: minden egyes villagtatási helyre be kell másolni. Ha utólag rájövünk, hogy pl. más sebességgel akarunk villogtatni, akkor ezt aránylag sok helyen kell módosítanunk. Ehelyett létrehozhatunk metódusokat:

void villog(int ido, int szam) //visszatérési érték, név, paraméterek
{
	int i;                         //
	for(i = 0; i@ &lt; szam; i++)     //
	{                              //
		digitalWrite(LED, HIGH);   // Metódustörzs
		delay(ido);                //
		digitalWrite(LED, LOW);    //
	}                              //
}

Egy metódusnak van neve, amivel hivatkozhatunk rá, törzse ahova a kódot írjuk, paraméterei és visszatérési értéke. Ennek a metódusnak a neve villog, visszatérési értéke void, van két paramétere (ido, és szam, mindkettő egy int), törzse pedig a blokkban található. Az ismert setup és loop szintén metódusok (void visszatérési érték, paraméter nélkül). Ezeket, mint már megtanultuk, muszáj definiálni.

Most egy olyan metódust hoztunk létre, amelynek nincs visszatérési értéke. Létrehozhatunk olyan metódusokat is, amelyek valamilyen értéket adnak vissza:

int negyzet(int a)
{
	return a * a;
}

Ez a metódus egy int értéket ad vissza (a void egy speciális visszatérési értéket, a semmilyen értéket jelöli). A metódusban használhatunk változókat, elégazásokat és ciklusokat is, de egy nem-void metódusnak mindig vissza kell adnia egy értéket a return paranccsal. A megírt metódusokat így használhatjuk:

int i;

...

void loop()
{
	...
	if(negyzet(i) == 25)
	{
		villog(300, 1);
	}
	else
	{
		i = negyzet(10); //i = 100
		villog(i, 3);
	}
	...
}

Voltaképpen ha belegondolunk, az alapprogramunk is pont így működik, a setup és loop metódusokat így használjuk:

//PROGRAM START
setup();
while(true) //Örökké futó ciklus
{
	loop();
}
//PROGRAM STOP
An unhandled exception has occurred. See browser dev tools for details. Reload 🗙