ESP32 to go

Der Pragmatische Architekt  –  136 Kommentare

2016 stellte Espressif eine leistungsfähige Familie von Microcontrollern auf Basis des ESP32 vor. Dieses Blog hat den ESP32 zwar bereits früher thematisiert, aber zum Auftakt einer Reihe von Beiträgen zu diesem Thema werden ESP32 und entsprechende Boards noch mal genauer beleuchtet.

Das chinesische Chip-Unternehmen Espressif hat vor wenigen Jahren durch seinen ESP8266-Chip große Euphorie bei Makern ausgelöst. Der ESP8266 als System-on-a-Chip (SoC) integriert sowohl einen leistungsstarken Microcontroller als auch eine WiFi-Komponente. Entsprechende Boards sind inzwischen schon für eine Handvoll Euros zu haben. Ich habe ESP-01-Boards bereits in diesem Blog genutzt, um Arduinos preiswert mit dem WLAN beziehungsweise dem Internet zu verbinden. Es ist dabei aber leicht zu übersehen, dass der ESP8266 in vielen Aspekten mit Arduinos ATMEL-Chips mithalten kann.

Warum ist der ESP32 interessant?

Wie auch bei Vierrad-Enthusiasten üblich soll zuerst ein Blick unter die Motorhaube erfolgen. ESP32-Chips enthalten eine ganze Reihe von interessanten Merkmalen:

  • Sie beherbergen meistens zwei 32-Bit-Prozessorkerne des Typs Tensilica Xtensa LX6, die mit 160 MHz bis 240 MHz Taktfrequenz arbeiten. “Meistens” deshalb, weil es auch eine ESP32-Variante mit nur einem Kern gibt.
  • Der “Laderaum” sorgt für komfortables Wohngefühl: Mit 520 KB RAM und 448 KB ROM dürften für die meisten Embedded-Anwendungen genügend Reserven vorhanden sein.
  • Im Gegensatz zu vielen Arduino-Boards verwendet der ESP32 3,3V als Betriebsspannung. Der entscheidende Vorteil: Weil viele Sensoren und Aktuatoren ebenfalls mit 3,3V arbeiten, entfällt der sonst notwendige Pegelwandler zwischen 3,3V- und 5V-Komponenten.
  • Da heutzutage das Thema Energiesparen vorherrscht, sei auf den Low-Power-Modus des ESP32 verwiesen. Im Tiefschlaf (Deep Sleep) verbraucht ein ESP32-Board nur einen Bruchteil der sonst benötigten Leistung. Das ermöglicht autonome Systeme, die im Batterie-/Akku-Betrieb mit langer Laufzeit auskommen müssen.
  • Diverse Schnittstellen verbinden den Microcontroller mit der Außenwelt. Darunter befinden sich UARTs, SPI, CAN, I2C, I2S, PWM – ich spare mir aufgrund des Trägheitsprinzips die Beschreibung der diversen Hardwareschnittstellen, da sie bis auf CAN schon in vergangenen Folgen zur Sprache kamen. Mit CAN ist ein standardisiertes Bussystem gemeint, das häufig in Anwendungen der Automatisierungstechnik zum Einsatz kommt. Autohersteller und ihre Zulieferer nutzen CAN zur Kommunikation verschiedener Steuereinheiten miteinander.
  • Damit sich analoge Werte in digitale umwandeln lassen, verfügt der Chip über diverse ADCs (Analog Digital Converter). Umgekehrt erlauben DACs (Digital Analog Converter) die Wandlung digitaler in analoge Signale.
  • Zur drahtlosen Kommunikation mit anderen Geräten integriert der ESP32 sowohl WLAN als auch Bluetooth. Letzteres war beim ESP8266 nicht möglich.
  • Ohne Firmware lässt sich ein Microcontroller nicht nutzen. Die Firmware des ESP32 ist in einem seriell anschließbaren externen Flash-Speicher untergebracht. Es macht daher meistens nur wenig Sinn, einen Standalone-ESP32-Chip zu erwerben, sondern stattdessen ein Board, das den Firmwarespeicher neben anderer Features – zum Beispiel einen USB-Anschluss – umfasst.
  • Zu guter Letzt: Ein integrierter Hall-Sensor erlaubt die Messung elektromagnetischer Felder. Die im ESP32 integrierte Crypto-Einheit finden in Anwendungen Einsatz, um die Kryptographie-Operationen zu beschleunigen.

Allerdings gibt es nicht nur eine Variante des ESP32, sondern gefühlt ein gutes Dutzend. Die heißen dann auch mal ESP32S, firmieren je nach Größe als WROOM oder WROVER, haben verschiedene Erweiterungen. Uns sollen deren Unterschiede kalt lassen. Für die Hardware-Interessierten verweise ich auf die Webseite esp32.net.

Come on Board

Selbstredend existiert nicht das eine ESP32-Board, sondern Dutzende. Solche mit Display und solche ohne, solche mit LoRA-Kommunikation und solche ohne. Dazu verschiedenste Formfaktoren, nach außen gelegte Pins und dergleichen mehr.

Ein Board des Typs ESP32 (Bild: amazon.de)

Als NodeMCU firmieren die Boards, die im Auslieferungszustand die Skriptsprache LUA und die entsprechende Firmware beherbergen. Grundsätzlich gibt es unterschiedliche Firmware-Optionen, die auf ESP32-Boards laufen, darunter zum Beispiel MicroPython (oder CircuitPython auf Adafruit-Boards), FreeRTOS (RTOS = Real-Time Operating System), und der Arduino Core für ESP32. Letztere Option nutze ich für den Rest dieses Blog-Postings.

Pin-Belegung des ESP32 Smart Thing von SparkFun (Bild: SparkFun.com)

Zu den häufig anzutretenden Vertreter ihrer Gattung gehören beispielsweise das Original-ESP32-Dev-Modul von Espressif und seine zahlreichen Klone sowie das DOIT ESP32 DevKit V1. Diese gibt es in der Regel für Preise von 5 bis 10 Euro bei den üblichen Verdächtigen (eBay, Amazon, Watterott, EXP-TECH, Alibaba ....). Das finanzielle Risiko hält sich also in Grenzen. Eine Bestellung in China kommt noch etwas billiger, lohnt sich wegen der längeren Lieferzeiten aber nur, wenn es einem nicht schon in den Fingern juckt.

Eine Frage der Programmierung

Es gibt diverse Optionen, die eine Host-/Target-Entwicklung mit ESP32-Boards unterstützen:

  • Von Espressif selbst steht die ESP-IDF-Plattform im Angebot.
  • Das Open-Source-Projekt ESP32 Arduino Core liefert eine Sammlung von Bibliotheken und Werkzeugen, mit der sich die Arduino IDE nutzen lässt, um Software für ESP32-Boards zu programmieren. Weiterer Vorteil: Leistungsstarke Entwicklungsumgebungen wie Visual Studio Code verfügen über Plug-ins für Arduino und Arduino ESP32 Core.

In Rahmen dieses Blogs dient die Arduino IDE in Kombination mit dem Arduino Core für ESP32 als Werkzeug der Wahl. Sie ist kostenlos und bietet ausreichende Funktionalität. Man könnte sie gewissermaßen als MVP (Minimal Viable Produkt) bezeichnen. Aber keine Sorge! In späteren Folgen kommt auch noch die Alternative Visual Studio Code zur Sprache.

Arduino IDE

Um ESP32-Boards unter der Arduino IDE zu nutzen, müssen Entwickler zunächst eine möglichst aktuelle Version der IDE für Windows, Linux, oder macOS herunterladen. Das Installationspaket steht auf der Downloadseite von arduino.cc zur Verfügung. Auf die Installation gehe ich an dieser Stelle nicht weiter ein und verweise auf frühere Postings. Dazu gibt es YouTube-Ressourcen wie die hier.

Arbeiten mit der Arduino IDE

Nach Installation der Arduino IDE ist folgende URL des gewünschten ESP32-Boardmanagers in der Einstellungsseite (Windows: File | Preferences, macOS: Arduino | Preferences) einzutragen: https://dl.espressif.com/dl/package_esp32_index.json.

Anschließend sollte man im Boardsmanager (Tools | Boards | Boardsmanager) nach "ESP32" suchen. Dort müsste das Paket "esp32 by Espressif Systems" auftauchen, das sich mit Install installieren lässt.

Das war es auch schon. Besser gesagt fast. Die (chinesischen) ESP32-Boards enthalten häufig einen UART-to-USB-Baustein von SiLabs. UART steht für Universal Asynchronous Receiver Transmitter und dient der seriellen Kommunikation zwischen Host-PC und Embedded Board über USB. Um am Windows-, Linux-, macOS-Computer mit dem Board kommunizieren zu können, ist ein entsprechender Treiber notwendig. Den gibt es über das Internet unter der URL <SiLabs>).

Von Mappings und anderen Schikanen

Um die verschiedenen Pins eines ESP32-Boards mit symbolischem Namen innerhalb der Arduino IDE anzusprechen, existieren sogenannte Mappings, bereitgestellt durch den Arduino Core. Dazu finden sich im Installationsverzeichnis der IDE (Arduino | hardware | espressif | esp32 | variants | esp32) entsprechende Deklarationen in pins_arduino.h. Wie aus dem folgenden Bild ersichtlich, erweist sich ein solches Mapping als wichtig, um etwas Ordnung ins Namenschaos zu bringen.

Das Mapping der ESP32-Pins auf die Arduino IDE (Bild: LastMinuteEngineers.com)

Funktion der Tasten

Im Regelfall befinden sich zwei Tasten auf einem ESP32-Board, eine EN-Taste und eine BOOT-Taste. Der ESP32 kennt zwei Betriebsmodi, Normalmodus und Firmware-Update-Modus. Für das Einspielen neuer Software (eigenes Programm plus Laufzeitumgebung/Firmware) müssen Entwickler das Board daher in den Uploadmodus versetzen. Die EN-Taste sorgt lediglich für einen Restart. Stattdessen ist für den Software-Upload folgendes Vorgehen notwendig:

  1. BOOT-Taste drücken und gedrückt halten
  2. EN-Taste betätigen und wieder loslassen
  3. BOOT-Taste loslassen

Achtung:

  • Dieses Vorgehen können einige Boards auch automatisieren.
  • Die Tasten haben auf unterschiedlichen Boards unterschiedliche Namen (z. B. "RS" und "0", "BOOT" und "RST").
Ein Board des Typs SparkFun ESP32 Thing mit Tasten "RST" und "0"

Sollte beim Kompilieren eines Programms innerhalb der IDE zu einer Fehlermeldung mit roter Schrift kommen, genügt es meist, die BOOT-Taste ein bisschen gedrückt zu halte, um sie anschließend wieder los zu lassen.

Das erste Mal

Nun erfolgt die erste Prüfung des jungfräulichen ESP32-Boards. Die Arduino IDE soll einen ersten Einblick vermitteln, dass die Programmierung exakt auf dieselbe Weise erfolgen kann wie bei Arduino-Boards.

Sobald der ESP32 am Hostcomputer angeschlossen ist, sucht man im Tools-Menü nach dem benutzten Board und stellt es ein. Zudem gibt im Ports-Bereich den vom Board verwendeten seriellen (SLab-)Port ein.

Ich selbst nutze zum Experimentieren das ESP32 Smart Thing von SparkFun sowie ein ESP32-Board von Watterott, besitze aber aus Preisgründen zahlreiche Boards des Typs NodeMCU32 sowie Lolin32 aus chinesischer Produktion.

ESP32-Boards gibt es fast so viele wie Sand am Meer

Um das ESP32-Board zum ersten Mal zu programmieren, öffnet man in der Arduino IDE ein neues Projekt beziehungsweise einen neuem Sketch und gibt nachfolgenden Code ein. Der Sketch geht davon aus, dass sich die eingebaute LED an Pin 2 befindet. Sollte Entwickler ein anderes Board besitzen, lesen sie dessen Beschreibung und definieren sie gegebenenfalls für die Konstante LED einen anderen Wert. Danach lässt man das Programm übersetzen und aufs Board übertragen. Nach dem Öffnen des seriellen Monitors gibt man 115.200 Baud als Geschwindigkeit ein. Um den seriellen Monitor zu öffnen, navigiert man über den Menüpfad Tools | Serial Monitor.

Jetzt müsste sowohl die Onboard-LED im Einsekundentakt blinken als auch der Text “Hallo, ESP32” wiederholt auf dem seriellen Monitor erscheinen.

const int LED = 5; // Eingebaute blaue LED an Pin 5
// Setup - läuft nach jedem Reset genau einmal
void setup() {
// Digitaler Ausgang steuert die LED
pinMode(LED, OUTPUT);
Serial.begin(115200); // Seriellen Port mit 115200 Baud initialisieren
}

// Unendliche Ereignisschleife; HIGH & LOW sind Spannungslevels!
void loop() {
Serial.println(“Hallo, ESP32”); // Ausgabe an seriellen Monitor senden
digitalWrite(LED, HIGH); // Es werde Licht
delay(1000); // Wartezeit von einer Sekunde
digitalWrite(LED, LOW); // Licht aus
delay(1000); // Wartezeit von einer Sekunde
}

Damit ist die Jungfernfahrt bereits erledigt. Wer experimentieren will, könnte zum Beispiel an Pin 5 auch eine externe LED anschließen oder andere Schikanen einbauen.

Arbeitsteilung

Da der ESP32 eine Mehrkernarchitektur aufweist, lassen sich echt parallele Threads beziehungsweise Tasks einsetzen. Das folgende Programm nutzt diese Möglichkeit exemplarisch, um Tasks mittels xTaskCreatePinnedToCore() zu erzeugen und sie an einen der beiden Kerne zu binden. Der erste Task taskOne läuft auf dem ersten Kern 0 der zweite taskTwo auf dem zweiten Kern 1. Beide Tasks haben Priorität 1 und erhalten 10.000 Bytes Stackgröße.

taskOne lässt die eingebaute LED jede halbe Sekunde blinken, während taskTwo jede halbe Sekunde Text am seriellen Monitor ausgibt.

TaskHandle_t task1; // Jeder Task benötigt einen Handle
TaskHandle_t task2;
// LED Pin ist die eingebaute Pin
const int ledPIN = 5;

void setup() {
Serial.begin(115200);
pinMode(ledPIN, OUTPUT);

// task ausgeführt in taskOne()
// auf dem ersten Core (Core 0), Prio: 1
xTaskCreatePinnedToCore(
taskOne, /* Funktion mit Code des Tasks */
"TaskOne", /* Name des Tasks */
10000, /* Stackgröße des Tasks */
NULL, /* Parameter des Tasks */
1, /* Priorität des Tasks */
&task1, /* Handle auf Task */
0); /* Task soll auf Kern 1 laufen */
delay(500);

// task ausgeführt in taskTwo()
// auf dem zweiten Core (Core 1), Prio: 1
xTaskCreatePinnedToCore(
taskTwo, /* Funktion mit Code des Tasks */
"TaskTwo", /* Name des Tasks */
10000, /* Stackgröße des Tasks */
NULL, /* Parameter des Tasks */
1, /* Priorität des Tasks */
&task2, /* Handle auf Task */
1); /* Task soll auf Kern 2 laufen */
delay(500);
}

// taskOne: LED soll alle 2 Sekunden blinken
void taskOne( void * optionalArgs ){
for(;;){
digitalWrite(ledPIN, HIGH);
delay(500);
digitalWrite(ledPIN, LOW);
delay(500);
}
}

// taskTwo: Ausgabe am seriellen Monitor alle 100 msec
void taskTwo( void * optionalArgs ){
Serial.print("Task 2 läuft auf Kern ");
Serial.println(xPortGetCoreID())
for(;;){
Serial.println("LED an");
delay(500);
Serial.println("LED aus");
delay(500);
}
}
void loop() { /* braucht es nicht */ }

Die Methode loop() bleibt in diesem Fall ungenutzt, weil sich die Ereignisschleife ohnehin in den endlos laufenden Tasks abspielt.

Das Beispiel ist zwar lehrreich, aber etwas theoretisch. In der Praxis könnte einer der Tasks die Kommunikation mit der Außenwelt übernehmen, während der zweite Messwerte von Sensoren liest.

Let's talk

In jedem Fall wäre es hilfreich, würden Tasks auch Information austauschen. Genau das demonstriert das nachfolgende Beispiel. Mittels xQueueCreate legt das Hauptprogramm eine Queue mit einem Slot des Datentyps unsigned long an. Das Kreieren der Tasks bewerkstelligt diesmal die Methode xTaskCreate. Das Programm implementiert eine simple Produzenten-Konsumenten-Konstellation.

Der Aufruf von vTaskDelay ist das ESP32-Pendant zu Arduinos delay, arbeitet aber mit höherer Auflösung, weshalb wir die gewünschte Zeitdauer noch durch die Konstante portTICK_PERIOD_MS teilen müssen. Die Konstante gibt an, wie viele Taktzyklen pro Millisekunde durchgeführt werden.

Um sicherzustellen, dass ein Wert in der Queue vorhanden und ungelesen ist oder bereits ein neuer geschrieben werden kann, fragt der Produzent queue auf Null ab, bevor er schreibt. Entsprechend prüft der Konsument, ob tatsächlich ein Wert in der Queue vorliegt.

QueueHandle_t  queue = NULL; // Queue anlegen

void setup()
{
printf("Starte zwei Tasks und eine Queue /n /n");
queue = xQueueCreate(20,sizeof(unsigned long));
if(queue != NULL){
printf("Queue kreiert \n");
vTaskDelay(1000/portTICK_PERIOD_MS); // Eine Sekunde warten
xTaskCreate(&produzent,"produzent",2048,NULL,5,NULL);
printf("Produzent gestartet \n");
xTaskCreate(&konsument,"konsument",2048,NULL,5,NULL);
printf("Konsument gestarted \n"); // Hier nutzen wir mal C/C++
} else {
printf("Queue konnte nicht angelegt werden \n");
}
}

void konsument(void *pvParameter)
{
unsigned long counter;
if (queue == NULL){
printf("Queue nicht bereit \n");
return;
}
while(1){
xQueueReceive(queue,&counter,(TickType_t )(1000/portTICK_PERIOD_MS));
printf("Empfangener Wert über Queue: %lu \n",counter);

vTaskDelay(500/portTICK_PERIOD_MS); // halbe Sekunde warten
}
}

void produzent(void *pvParameter){
unsigned long counter=1;
if(queue == NULL){
printf("Queue nicht bereit \n");
return;
}
while(1){
printf("An Queue gesendeter Wert: %lu \n",counter);
xQueueSend(queue,(void *)&counter,(TickType_t )0);
// ... schreibt den wert von Counter in die Queue
counter++;
vTaskDelay(500/portTICK_PERIOD_MS); // halbe Sekunde warten
}
}
void loop() {}

Deep Sleep

Ein ESP32-Board braucht im aktiven Zustand schon einige mA, im Schnitt etwa 150 bis 260 mA. Im aktiven Modus inklusive aller Komponenten wie WiFi und Bluetooth können das sogar bis zu 800 mA bei Spitzenlasten sein. Innerhalb von IoT-Anwendungen kommt es aber durchaus häufig vor, dass Geräte mittels Sensoren nur alle Minuten oder sogar Stunden kurzzeitige Messungen vornehmen, um sie anschließend per Kommunikationsprotokoll nach außen zu übertragen. Es macht also keinen Sinn, einen Embedded-Controller ständig aktiv arbeiten zu lassen. Bei einem autonomen, batteriebetriebenen Gerät müsste man ansonsten alle paar Stunden die Batterie ersetzen. Stellt man sich vor, dass das Gerät auf einem Baum in großer Höhe angebracht ist, dürfte die praktische Dimension dieses Problems klar sein.

Deshalb unterstützt der ESP32 diverse Sparmodi. Beim Deep-Sleep-Modus sind nur noch der RTC (Echtzeituhr) und der ULP-Koprozessor (ULP = Ultra Low Power) aktiv. Der benötigte Reststrom beträgt in diesem Fall gerade einmal 2,5 micro Ampere. Damit lässt sich gut auskommen.

Um vom Tiefschlaf zu erwachen, braucht der ESP32 keinen schönen Prinzen, obwohl auch das möglich wäre. Es gibt drei Optionen: Betätigen eines Touch-Pins, Ereignis an einem externen Pin (= Signalflanke) oder zeitgesteuertes Aufwecken mittels des ULP-Koprozessors. Diesen letzteren Fall beleuchtet nachfolgendes Beispiel.

Allerdings führt der Tiefschlaf auch eine Art digitale Demenz nach sich, weil die Daten (= Variablen) bis auf eine Ausnahme verloren gehen. Alle Daten, die mit dem Schlüsselattribut RTC_ATTR_DATA deklariert sind, bleiben erhalten. Immerhin beherbergt dieser persistente Speicher rund viermal so viele Daten wie ein gewöhnlicher Arduino Uno/Mega/Nano Speicher insgesamt mitbringt.

Im folgenden Programm liegt die Variable restarted im besagten Speicher. Sie zählt einfach mit, wie oft bereits ein Neustart erfolgte. Beim ersten Start (restarted == 0) blinkt die eingebaute LED kurz. Sind schon mehrere Restarts nach Tiefschafphasen erfolgt (restarted != 0), blinkt die LED häufiger.

Die Einleitung eines zeitgesteuerten Tiefschlafs beginnt mit dem Aufruf von esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR. Die Zeit in Sekunden gibt TIME_TO_SLEEP wieder. Da intern die Berechnung in Microsekunden erfolgt, muss der Umrechnungsfaktor uS_TO_S_FACTOR noch dazu multipliziert werden. Den eigentlichen Tiefschlaf leitet der folgende Aufruf ein: esp_deep_sleep_start():

#define uS_TO_S_FACTOR 1000000         // Microsekunden => Sekunden 
#define TIME_TO_SLEEP 3 // Schlaflänge in Sekunden :-)
RTC_DATA_ATTR int restartet = 0;
#define LEDPIN 2 // eingebaute LED
#define BLINK_DELAY 200 // Blinkfrequenz = 200 / BLINK_DELAY
void setup() {
Serial.begin(115200);
Serial.println(“Deep Sleep mittels Timer - Demo”);
pinMode(LEDPIN,OUTPUT); // LED

delay(500); // a bisserl Geduld



if(restartet == 0) // das erste Mal
{
Serial.println(“Initialer Start”);
digitalWrite(LEDPIN, HIGH); // Licht an
delay(10 * BLINK_DELAY);
digitalWrite(LEDPIN, LOW); // Spot aus
restartet++;
} else // nicht mehr jungfräulich
{
Serial.println(“Schon ein paar Mal aus Schlaf erwacht: “ + String(restartet)));
blinken(restartet);
}

// In Tiefschlaf versetzen
Serial.print(“Tiefschlaf wird initiiert für “ + String(TIME_TO_SLEEP));
Serial.println(“ Sekunden”);
esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
esp_deep_sleep_start(); // Schlaf beginnt
}
void blinken(byte n) { // n mal Blinken im BLINK_DELAY msec Abstand
Serial.println(“Blinken startet”);
for (i = 0; i < n; i++) {
Serial.println(“LED an”);
digitalWrite(LEDPIN, HIGH);
delay(BLINK_DELAY);
Serial.println(“LED aus”);
digitalWrite(LEDPIN, LOW);
}
}
void loop() {
Serial.println(“Unerreichter Code”);
}

Da das Programm den Tiefschlaf in setup() durchführt, kommt es in diesem Beispiel natürlich nie zur Ausführung der Methode loop().

Fazit

Nun sind wir am Ende dieses Beitrags angekommen, womit die Basis für eigene Experimente gelegt wäre. Dabei lag der Fokus auf die inneren Werte des ESP32. Wir haben die funktionale Architektur beleuchtet, einige besondere Aspekte wie Parallelisierung und Tiefschlaf betrachtet und dazu Beispiele kennen gelernt. Im Mittelpunkt der Programmierung stand dabei die Arduino IDE.

Beim nächsten Beitrag kommt die Interaktion des ESP32 mit der Außenwelt zur Sprache. Themen sind dann insbesondere die WiFi-Funktionalität des Microcontrollers.

Bis dahin viel Spaß beim Experimentieren!