Az ESP32 programozása

Bevezető

Történelmi kontextus

A programozható mikrovezérlők fejlődésének óriási löketet adott az Arduino megjelenése, mely egyszerű használatával és olcsó árával a korábbiaknál szélesebb körben tette lehetővé az IoT projektek elterjedését. Ennek a kifejlődése egy hosszú folyamat volt, és a 2000-es évekre tehető.

Ezt követően viszont hihetetlen tempóban fejlődtek a mikrokontrollerek tovább. Az Arduino-nak igen sokféle változata került forgalomba, számos gyártótól, különböző konfigurációkban.

2014-ben jelent meg az ESP8266-os kódú chippel ellátott család, ami azon felül, hogy lényegesen több memóriát és gyorsabb processzort tartalmazott, mint pl. az Arduino UNO, wifi modullal is el volt látva.

2016-ban jelent meg az ESP32-es család, melyben tovább fejlesztették a lehetőségeket. Kétmagos processzorral és több memóriával látták el. Tartalmaz wifit, Bluetooth-ot, mágneses érzékelőt és még számos egy-b, kisebb-nagyobb újítást.

A mikrovezérlők összehasonlítása

A mikrovezérlőket nehéz összehasonlítani, mivel mindegyik egy családot jelent. Ezren felül van a forgalomban levő különböző lapkák száma. Kisebb-nagyobb eltérések családon belül is vannak.

Az alábbi táblázat néhány tipikus konfiguráció néhány jellegzetes értékét hasonlítja össze:

Mikrovezérlő Frekvencia Mag Memória
Arduino 16 MHz 1 32 kB
ESP8266 160 MHz 1 512 kB
ESP32 240 MHz 2 4096 kB

Hangsúlyozom, hogy a táblázat célja nem a patikamérleg pontosságú összehasonlítás (ez esetben meg kellene mondani a pontos típust, vagy azt, hogy mit értünk pontosan memória alatt), hanem inkább az, hogy illusztrálja a fejlődés ívét, valamint hogy legyen nagyságrendi elképzelésünk az egyes családok közötti különbségekről.

Érdekességképpen érdemes megjegyezni, hogy az Arduino UNO méretei nagyságrendileg összevethetőek mondjuk egy Commodore 64 vagy ZX Spectrum méreteivel. Az ESP32 memóriája pedig megegyezik az első PC-m, egy 386-os memóriájával.

Fontos hangsúlyozni, hogy ezek mindegyike igen kedvező árú: míg az említett számítógépekért annak idején több havi fizetést kellett adni, ezek a mikrovezérlők pár (10 alatti) dolláros tételek. Pl. Magyarországon a https://www.microcontroller.hu/ webshopban az árak – típustól függően – nagyrészt 1500 és 3000 Ft között mozognak. Tehát akár magyarországi beszerzés esetén is abszolút hozzáférhető a mikrokontrollerekkel való bütykölés hobbi!

Programozás C++-ban

Az Arduino IDE feltelepítése

A teleépítés és beállítás lépései:

Akik ismerik a stílusomat, azok tudják, hogy nem rejtem véka alá az időközben felmerülő problémákat sem. A beállítások során egyszer úgy lefagyott nálam az Arduino IDE, hogy csak a számítógép újraindítása segített.

LED villogó

A mikrokontrollerek „Helló, világ!” projektje a LED villogó. AZ esetek többségében van a mikrovezérlőn egy beépített LED, melyre a LED_BUILTIN konstanssal hivatkozhatunk. A specifikáció alapján az ESP32 esetén ez a 2. Ám – most előre szaladok – nálam ez nem működött. Lehet, hogy azon a verzión, amivel próbálkoztam, ilyen nincs, vagy egyszerűen csak hibás. Így a LED villogtatóhoz is ki kell alakítani egy egyszerű áramkört.

Az áramkörök kialakításához tudnunk kell azt, hogy melyik láb mit jelent. Hosszabb keresés után bukkantam csak rá arra a pinout ábrára, amely az általam hazsnált mikrovezrélőnek felel meg:

esp32-pinout.jpgEgyébként magán a mikrovezérlőn is rajta vannak a jelek. Alakítsuk ki a következő áramkört:

  • ESP32 GPIO02 (G2; nálam jobb oldalon alulról az 5.) – LED+ (hosszabb szára)
  • LED+ – előtét ellenállás (pár száz ohm) egyik szára
  • előtét ellenállás másik szára – ESP GND (nálam a jobb felső)

Készítsük el a kö9vetkező kódot:

void setup() {
  pinMode(2, OUTPUT);
}
 
void loop() {
  digitalWrite(2, HIGH);
  delay(500);
  digitalWrite(2, LOW);
  delay(500);
}

Kapcsoljuk össze az ESP32-t és a számítógépet egy USB kábellel.

Az Arduino IDE-ben a következőket hajtsuk végre:
  • Eszközök → Alaplap → ESP32 Arduino → ESP Dev Module.
  • Eszközök → Port: itt automatikusan ki kell, hogy legyen választva a megfelelő COM (nálam pl. COM3).
  • A többit hagyjuk alapértelmezett értéken.
  • Töltsük fel programot a Vázlat → Feltöltés (Ctrl+U) menüpontot (billentyűkombinációt) kiválasztva.

Ha szerencsénk van, akkor feltöltődik, és látjuk villogni a LED-et. Ha nincs szerencsénk, akkor a következőt tehetjük

  • Ha Az Eszközök → Port szürke, nincs ott semmi, akkor az azt jelenti, hogy nem ismerte fel a mikrovezérlőt. Ez esetben egy lehetséges megoldás az, hogy fel kell telepíteni a mikrovezérélő meghajtóját. Az ESP32 a CP2102 jelű chip-et tartalmazza; erre kell rákeresni. Ezt a https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers oldalról tudjuk letölteni.
  • Céges laptopokon lehetséges, hogy a portok le vannak tiltva. Ilyen esetben ne céges laptoppal próbálkozzunk.
  • Nálam elég hektikus volt, hogy elindult-e a feltöltés. Időnként igen, máskor viszont flash módba kellett tenni. Akik régóta „űzik az ipart”, azok a flash mód hallatán kissé összerezdülhetnek, mert annak idején ehhez még akár forrasztani is kellett. Az ESP32 esetén ez már egyszerű: tartsuk nyomva a BOOT nyomógombot (a jobb oldalit) amikor elkezdjük feltölteni a programot.
A feltöltéssel kapcsolatos tapasztalat:
  • a fordítás meglepően hosszú (mondjuk egy Arduino UNO-hoz képest);
  • a kapcsolódás hektikus volt, és még sikeres kapcsolódás esetén is eltartott jó pár másodpercig;
  • maga a feltöltés viszont kellemes meglepetést okozott, lassúbbra számítottam.

Sebesség teszt

Hajtsunk végre egy sebesség tesztet! A teszt a következő: egymillió, 0 és 9 közötti véletlen egész számot adjunk össze. Az eredménynek a nagy számik törvénye miatt kb. 4,5 milliónak kell lennie.

Mielőtt feltöltenénk a programot, nyissuk meg a soros monitort: Eszközök → Soros monitor (Ctrl+Shift+M), és a jobb alsó sarokban állítsuk be a baud-ot 9600-ra. A program:

void setup() {
  Serial.begin(9600);
  delay(500);
  Serial.println("Start");
  int before = millis();
 
  long result = 0;
  for (int i=0; i<100; i++) {
    for (int j=0; j<10000; j++) {
      result += random(10);
    }
    yield();
  }
 
  int after = millis();
  Serial.print("Result: ");
  Serial.println(result);
  Serial.print("Difference: ");
  Serial.print(after - before);
  Serial.println(" ms");
}
 
void loop() {}

Némi magyarázat a kódhoz:

  • Bizonyos mikrovezérlők leállnak, ha bizony ideig nincs a külvilág számára értékelhető művelet. A yield() vagy egy delay() hívás megoldja a problémát. Ilyet az Arduino UNO (lefagyott), az ESP8266 (időnként kivételt dobott és újraindult) és az ESP32 (többmagos tesztelés) esetén is tapasztaltam.
  • A számláló típusa long, mivel az Arduino UNO esetén az int csak 16 bites.

Töltsük fel a programot (Ctrl+U). Az eredmény nálam a következő:

Start
Result: 4496311
Difference: 682 ms

Összehasonlításul töltsük fel a programot egy ESP8266-os eszközre, pl. egy NodeMCU-ra!

  • Eszközök → Alaplap → ESP8266 Boards → Generic 8266 Module
  • Port: meglepő módon, ha ugyanarra az USB-be dugjuk is, más lett a COM (COM4).

Nálam itt is időnként meg kellett nyomni a FLASH gombot (ami ugyanott helyezkedik el, mint az ESP32-n a BOOT), míg máskor rendesen működött. Az ESP32-höz képest meglepően lassú a feltöltés. Az eredmény:

Start
Result: 4503448
Difference: 3544 ms

Ugyanazt a feladatot tehát az ESP8266 több mint ötször annyi idő alatt oldotta meg, mint az ESP32.

Végül hajtsuk végre Arduino UNO-n is:

  • Eszközök → Alaplap → Arduino AVR Boards → Arduino Uno
  • Port: válasszuk ki az egyetlen portot, amit látunk.

Az eredmény:

Start
Result: 4495507
Difference: 25122 ms

Itt tehát a futásidő az ESP8266 hétszerese, az ESP32-nek viszont kb. 37-szerese.

Kétmagos sebesség teszt

A cikk egyik legizgalmasabb része jön: hogyan tudjuk kihasználni a kétmagos processzort! A processzor magok jelölése 0 és 1, és a főprogram az 1-es magon fut. Így a tesztet a következőképpen alakítottam ki:

  • Van egy segédfüggvény, ami félmillió véletlen számot ad össze.
  • A főprogramból indítunk egy szálat, ami meghívja ezt a segédfüggvényt, és az eredményt a globális változóba teszi.
  • A főprogram maga is meghívja ezt a segédfüggvényt.
  • Abban az esetben, ha a főprogram előbb végzett, megvárja, hogy a másik szál is befejeződjön.
  • Majd összegzi az eredményt.

A kód:

TaskHandle_t otherTask;
long core0Result = -1;
 
void setup() {
  Serial.begin(9600);
  delay(500);
  Serial.println("Start");
  int before = millis();
 
  xTaskCreatePinnedToCore(core0Task, "Core 0", 10000, NULL, 1, &otherTask, 0);
  long core1Result = addRandomNumbers();
  while (core0Result < 0) {
    delay(1);
  }
  int result = core0Result + core1Result;
 
  int after = millis();
  Serial.print("Result: ");
  Serial.println(result);
  Serial.print("Difference: ");
  Serial.print(after - before);
  Serial.println(" ms");
}
 
void core0Task(void *pvParameters) {
  core0Result = addRandomNumbers();
  while (true) {
    delay(1000);
  }
}
 
long addRandomNumbers() {
  long result = 0;
  for (int i=0; i<50; i++) {
    for (int j=0; j<10000; j++) {
      result += random(10);
    }
    yield();
  }
  return result;
}
 
void loop() {}

xTaskCreatePinnedToCore() utolsó paramétere az, hogy melyik magon fusson. Igazából lehet kettőt is létrehozni, 0 és 1 utolsó paraméterrel, de úgy egy nagyon picivel lassúbb lesz, mivel az 1-esen fut a főprogram is.

Fontos még az, hogy a másik szál nem fejeződhet be, emiatt van az, hogy egy végtelen ciklusban várunk 1 másodpercet.

Eredmény:

Start
Result: 4501793
Difference: 383 ms

A futási idő az egymagos változatnak kb. 56%-a, ami egész jól megközelíti az elvi minimum 50%-ot. Az az érdekes, hogy ez függetlennek tűnik a feladat méretétől; pl. a tízmillió véletlen szám összeadása esetén a futási idők 6815 ms és 3824 ms lettek, ami szinte tökéletesen tízszerese az egymilliós esetnek, és az arány ott is kb. 56%.

Sebesség teszt laptopon

Érdekességképpen végrehajtottam a tesztet egy átlagos laptopon. Ott az alábbi C++ kódot fordítottam le egy 11-es verziójú G++ fordítóval és futtattam le:

#include <iostream>
#include <chrono>
 
using namespace std;
 
int main() {
    cout << "Start" << endl;
    auto before = std::chrono::system_clock::now();
 
    long result = 0;
    for (int i = 0; i < 1000000; i++) {
        result += rand() % 10;
    }
 
    auto after = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = after - before;
    cout << "Result: " << result << endl;
    cout << "Difference: " << 1000 * elapsed_seconds.count() << " ms" << endl;
}

Nincs tehát benne semmilyen többmagos optimalizálás. Az eredmény eléggé hektikus, 10 ms körüli, de volt 3 ms is:

Start
Result: 4501334
Difference: 11.5479 ms

Tehát az ESP32 egyelőre messze van a laptopok teljesítményétől.

Programozás Pythonban

A Python programozás jelentősége

Elérkeztünk a cikk másik izgalmas részéhez: a mikrokontrollerek programozása Pythonban! Elmagyarázni, hogy miért tartom ezt különlegesnek kicsit olyan, mint a vicc magyarázat, de azért mégis megteszem.

Ha lemegyünk a legalacsonyabb szintekig, akkor olyan szintű műveleteket látunk, mint pl. a bit léptetés. Nagy előrelépés ehhez képest az, hogy van egy fix utasításkészletű IC, amit lehet gépi kódban, vagy akár assemblyben programozni. További hatalmas lépés az, hogy C++-ban is lehet programozni, és a fordító lefordítja arra a bizonyos architektúrára a forrást. De ha belegondolunk, itt még mindig arról van szó, hogy figyelembe vesszük az adott processzor sajátosságait, és lényegében továbbra is biteket mozgatunk jobbra-balra, csakhogy ezt két szinttel feljebb tesszük: megírjuk a kódot C++-ban, au lefordítja gépi kódra, és ez utóbbi állítgatja a biteket. A C++ ugyanis akármennyire is magas szintű programozási nyelv, de platformfüggő.

A Python viszont néhány fontos tekintetben alapvetően eltér a C++-tól:

  • A Python intepretált nyelv, ami azt jelenti, hogy egy interpreter sorról sorra értelmezi az utasításokat. Tehát itt elszakadunk a konkrét architektúrától! Ez viszont azt is jelenti, hogy kell egy Python interpreter, ami a mikrovezérlőn fut.
  • Az előző pontból következik, hogy a Python platformfüggetlen. Persze lehet érvelni, hogy a C++ is pont ennyire platformfüggetlen, és a forráskód valóban az, és az is igaz, hogy mindkét esetben platformfüggő könyvtárakat kell használni. Ám lényeges különbség, hogy a C++ esetén a fordítás következő pontja már platformfüggő, míg a Python kódból nem lesz platformfüggő bináris, hanem maga a futtató környezet a platformfüggő. Erre persze bizonyos absztrakciós szint felett mondhatjuk, hogy lényegtelen, olyan gyakorlati jelentősége viszont mégis van, hogy az interpretáltságnak köszönhető pl. az, hogy sorról sorra is végre tudunk hajtani utasításokat, nem kell feltétlenül az egész programot minden esetben lefordítani és feltölteni.
  • A Python létezésének a legfőbb oka a könnyű tanulhatóság, a tömör, átlátható kód. A C ill. C++ létezésének a legfőbb oka az, hogy adott hardverre optimalizált kódot tudjunk írni. A hardverek szédületes fejlődése következtében, amikor már többnyire nincs jelentősége minden egyes órajelre vagy minden egyes bitre optimalizálni. Viszont az emberi munkaerő drága, és sokkal fontosabbá vált, hogy a kódot minél rövidebb idő alatt meg lehessen írni, valamint a karbantartás során minél könnyebben átlátható legyen. Így nem véletlen, hogy a Python népszerűsége szárnyal, míg a többi, több tanulást igénylő nyelvek népszerűsége süllyed vagy alacsony szinten stagnál.

Az tehát egy különösen izgalmas dolog, hogy egy aprócska és olcsó mikrovezérlő kapacitása eléri aszt a szintet, hogy tűrhető mértékben elfut rajta a Python interpreter, és még programot is lehet rá tölteni.

Azért félre ne értse bárki: a C++-t sem kell temetni, mert azokban az esetekben, amikor tényleg számít a futási idő vagy a felhasznált memória, akkor igenis azt kell használni; a Python ilyen esetekben nem opció. És valójában igen gyorsan el is jön ez a szint; majd látni fogjuk az egymillió véletlen szám összeadásánál.

A MicroPython feltelepítése

A MicroPython az „igazi” Python mikrovezérlőkre optimalizált változata. Nem tartalmaz mindent, amit az igazi Python tartalmaz, ill. tartalmaz olyan könyvtárakat, amelyek csak itt használhatóak.

A feltelepítése többféleképpen történhet; egy módszer az alábbi:

  • Töltsük le és telepítsük fel a számítógépünkre a Pythont a https://www.python.org/ oldalról. Telepítés során engedjük meg azt, hogy a PATH környezeti változót módosítsa.
  • Töltsük le az uPyCraft nevű programot innen: https://randomnerdtutorials.com/uPyCraftWindows, és indítsuk el.
  • Válasszuk ki a megfelelő lapkát: Tools → Board → esp32
  • Válasszuk ki a megfelelő soros portot: Tools → Serial → COM3 (vagy amennyi)
  • Ha még nincs rajta a MicroPython, akkor fel tudjuk tölteni. Az alábbi beállításokkal próbálkozhatunk:
    • board: esp32
    • burn_addr: ezt nem tudom, hogy mit jelent; ha kettővel lejjebb:
      • az uPyCraft által ajánlott verziót töltjük fel, akkor általában a 0x0 működött,
      • a netről letöltött változatban viszont inkább a 0x1000 volt a nyerő.
    • erase_flash: yes
    • Firmware Choose:
      • választhatjuk az uPyCraft-ot: ez esetben egy korábbi (nálam: 1.9-es verziójú) MicroPython kerül feltelepítésre,
      • vagy választhatjuk a Users-t. Ez utóbbi esetben először le kell töltenünk a legfrissebb MicroPythont (az írás pillanatában ez az 1.17-es) a https://micropython.org/download/ oldalról: ESP32 Expressif → Firmware v1.17 (20210902); a .bin kiterjesztésűt kell letölteni. Pl. nálam ez a esp32-20210902-v1.17.bin; egy kb. másfél megabájtos fájl. A Users esetén ennek az elérési útvonalát adjuk meg.
  • Kattintsunk az OK gombra. A feltöltés van, amikor sikerül, van, amikor nem. Ez utóbbi esetben kaphatunk erase fail és/vagy Permission error hibaüzenetet. Általában megoldja a problémát az, ha a feltöltést úgy indítjuk el, hogy nyomva tartjuk a BOOT nyomógombot (tehát a jobb oldalit). Amikor elkezdte, már elengedhetjük.

Esetenként elég sokat kell kínlódni, szóval sajnos ilyen tekintetben sincs ez még igazán élére vasalva.

REPL

A REPL a read-eval-print-loop rövidítése, ami magyarul kb. ezt jelenti: olvasás-kiértékelés-kiírás-ciklus. Tehát itt nem kell forrásfájlt létrehozni és feltölteni, hanem közvetlenül is kiadhatunk parancsokat:

>>> 3+2
5
>>> print('Hello, world!')
Hello, world!
>>>

A fenti példában a 3+2 a mikrovezérlő számolódott ki, Pythonban!

LED villogó

Készítsük el LED villogót MicroPythonban is!

  • Alakítsuk ki a fenti, C++ példában említett áramkört.
  • Hozzunk létre egy új fájlt: File → New
  • Másoljuk be az alábbi programot:
from machine import Pin
from time import sleep
 
led = Pin(2, Pin.OUT)
 
while True:
  led.value(not led.value())
  sleep(0.8)
  • Mentsük el pl. blink.py néven, akárhol.
  • Töltsük fel és indítsuk el: Tools → DownloadAndRun (F5, ill. jobb oldalon felölről a negyedik ikon)
  • A File → Reflush Directory-val frissítsük a könyvtárakat.

Ha minden rendben történt, akkor elvileg láthatunk egy villogó LED-et!

Előfordulhatnak itt is problémák, elakadások. Ilyenkor segíthet az, ha legalább kétszer a Stop gombra kattintunk

Sebesség teszt

Töltsük fel az alábbi programot (speedtest.py):

import time
import random
 
print('Start')
before = time.time()
result = 0
for i in range(1000000):
  result += random.randint(0, 9)
after = time.time()
print('Result: ' + str(result))
print('Difference: ' + str(after-before) + ' s')

Futtatás után az eredmény:

Ready to download this file,please wait!
..
download ok
exec(open('speedtest.py').read(),globals())
Start
Result: 4502461
Difference: 13 s

Ezredmásodperc pontosan itt nem tudjuk megmondani a futási időt, de közel hússzor annyi ideig futott, mint C++-ban ugyanez a program.

Most töltsük fel a programot egy ESP8266-ra!

A feltöltéskor ne felejtsük el átállítani az esp32-t esp8266-ra. Sajnos pont ugyanezt nem tudjuk feltölteni, mert nincs az ESP8266-ra készített MicroPythonon belül random.randint(). Ehhez közel álló az urandom.getrandbits(), ami a paraméterként átadott bithosszú véletlen számot generál. Teszt célból most a 3-at használjuk, így egy 0-7 közötti véletlen számot kapunk eredményül. A kód:

import time
import urandom
 
print('Start')
before = time.time()
result = 0
for i in range(1000000):
  result += urandom.getrandbits(3)
after = time.time()
print('Result: ' + str(result))
print('Difference: ' + str(after-before) + ' s')

Az elvárt eredmény itt 3,5 millió körüli. A tényleges:

Start
Result: 3502652
Difference: 115 s

Közel kettő perc.

Kétmagos sebesség teszt

Végül lássuk, hogy Pythonban milyen kétmagos lehetőségek vannak! Töltsül fel az alábbi kódot:

import _thread
import time
import random
 
def addRandomNumbers():
  result = 0
  for i in range(500000):
    result += random.randint(0, 9)
  return result
 
threadResult = -1 
def testThread():
  global threadResult
  threadResult = addRandomNumbers()
  while True:
    time.sleep(1)
 
before = time.time()
print('Start')
_thread.start_new_thread(testThread, ())
mainResult = addRandomNumbers()
while threadResult < 0:
  time.sleep(0.001)
result = mainResult + threadResult
after = time.time()
print('Result:', result)
print('Difference: ' + str(after-before) + ' s')

Egy tipikus eredmény a következő:

Start
Result: 4503231
Difference: 8 s

A futási idő az egymagos változat bő 60%-a.

Itt érdemes megegyezni, hogy sok esetben kivárhatatlanul hosszú volt a futási idő. Ennek oka lehetett az, hogy lefagyott, de pl. az is, hogy véletlenül ugyanazon a magon futott a főprogram és a saját szál is, ami nagyon belassította.

Sebesség tesz laptopon

A program egymagos változatát lefuttattam laptopon is. Az eredmény: 700-800 ms körüli futási idő. Nagyságrendileg az, amit az ESP32 produkál C++-ban, egymagos változatban.

Összehasonlítások

Végül álljon itt a fenti tesztek eredménye egy táblázatban. A futási idők ezredmásodpercben értendőek.

  Arduino ESP8266 ESP32
Python 115.000 13.000
8.000
C++ 25.122 3.544 682
383

A tanulság:

  • Az ESP32 jelentősen gyorsabb mindkettőnél.
  • A C++-ban írt kód sokkal gyorsabb, mint a Pythonban.

Tehát ESP32-ben kis feladatoknál megfontolható a Python.

Ez a cikk a http://faragocsaba.hu/esp32 oldalon is megjelent.

(Statisztika: 7 704 megtekintés)