Listing 2
Test2.ino
int i = 0, LED[] = {2, 0, 4, 16, 17, 5, 18, 19};
TaskHandle_t blink0_t,blink1_t,blink2_t,blink3_t,blink4_t,blink5_t,blink6_t,blink7_t;
void toggle(int led) {
digitalWrite(LED[led], !digitalRead(LED[led]));
char buffer[16];
sprintf(buffer,"LED: %d Core: %d",led,xPortGetCoreID());
Serial.println(buffer);
}
void blink0(void *) { while(true) {toggle(0); delay(1000);} }
void blink1(void *) { while(true) {toggle(1); delay(1100);} }
void blink2(void *) { while(true) {toggle(2); delay(1200);} }
void blink3(void *) { while(true) {toggle(3); delay(1300);} }
void blink4(void *) { while(true) {toggle(4); delay(1400);} }
void blink5(void *) { while(true) {toggle(5); delay(1500);} }
void blink6(void *) { while(true) {toggle(6); delay(1600);} }
void blink7(void *) { while(true) {toggle(7); delay(1700);} }
void setup() {
for (i = 0; i < 8; i++) pinMode(LED[i], OUTPUT);
Serial.begin(115200);
xTaskCreatePinnedToCore(blink0, "blink0", 1024, NULL, 1, &blink0_t, 0);
xTaskCreatePinnedToCore(blink1, "blink1", 1024, NULL, 1, &blink1_t, 1);
xTaskCreatePinnedToCore(blink2, "blink2", 1024, NULL, 1, &blink2_t, 0);
xTaskCreatePinnedToCore(blink3, "blink3", 1024, NULL, 1, &blink3_t, 1);
xTaskCreatePinnedToCore(blink4, "blink4", 1024, NULL, 1, &blink4_t, 0);
xTaskCreatePinnedToCore(blink5, "blink5", 1024, NULL, 1, &blink5_t, 1);
xTaskCreatePinnedToCore(blink6, "blink6", 1024, NULL, 1, &blink6_t, 0);
xTaskCreatePinnedToCore(blink7, "blink7", 1024, NULL, 1, &blink7_t, 1);
}
void loop() {
delay(1000);
}
Hand in Hand
Das vorherige Beispielprogramm hat gezeigt, wie sich viele unabhängige Tasks auf den beiden CPUs des ESP32 ausführen lassen. Richtig interessant wird es aber erst, wenn es gelingt, die einzelnen Prozesse untereinander zu synchronisieren. Eine solche Synchronisierung erlaubt den Tasks auch den Austausch von Informationen untereinander. Das Zauberwort hierbei lautet Semaphor.
Einen Semaphor kann man sich wie den Stab beim Staffellauf vorstellen: Nur wer ihn hat, darf durchstarten. Ein Task darf einen bestimmten Code nur dann ausführen, wenn er den entsprechenden Semaphor besitzt. Dabei handelt es sich um nichts anderes als eine Variable mit dem Typ SemaphoreHandle_t, die es mit der Funktion xSemaphoreCreateMutex(); zu initialisieren gilt. Zur Inanspruchnahme des Semaphors dient die Funktion xSemaphoreTake, xSemaphoreGive gibt ihn wieder zurück. Wenn ein Semaphor schon in Verwendung ist, blockiert die xSemaphoreTake-Funktion. Sie wartet, bis der Semaphor mit xSemaphoreGive wieder freigegeben wurde.
Damit kennen Sie jetzt die Grundlagen der Prozesssynchronisierung. Lassen Sie uns nun das Beispielprogramm so anpassen, dass aus dem wilden Durcheinanderblinken der acht LEDs ein synchronisierter Ablauf entsteht. Ein Task soll also immer erst dann starten, wenn sein Vorgänger die Arbeit einstellt. Wie Sie in Listing 3 sehen, sind die entsprechenden Funktionen xSemaphoreTake und xSemaphoreGive in die Endlosschleifen integriert.
Jetzt gilt es, noch ein kleines Problem zu lösen: Nach dem Erstellen sind alle Semaphoren erst einmal verfügbar, was bedeutet, dass alle Tasks direkt starten würden. Daher beansprucht die Schleife, die die Semaphoren initialisiert, erst einmal alle mit xSemaphoreTake(sem[i],portMAX_DELAY);. Auf diese Weise kann kein Prozess mit der Arbeit beginnen. Erst nach einer definierten Rückgabe des Semaphors zum Starten des ersten Tasks (xSemaphoreGive(sem[0]);) beginnt der Ablauf. Beim Erzeugen der Prozesse musste der Stack-Speicher erhöht werden, weil er jetzt zusätzlich die Informationen der Semaphoren enthält. Auch zu diesem Programm gibt es ein kurzes Youtube-Video [4].
Listing 3
Test3.ino
int i = 0, LED[] = {2, 0, 4, 16, 17, 5, 18, 19};
TaskHandle_t blink_t[8];
SemaphoreHandle_t sem[8];
void toggle(int led) {
digitalWrite(LED[led], !digitalRead(LED[led]));
char buffer[16];
sprintf(buffer,"LED: %d Core: %d",led,xPortGetCoreID());
Serial.println(buffer);
}
void blink0(void *) { while(true) {xSemaphoreTake(sem[0],portMAX_DELAY); toggle(0); delay(1000); xSemaphoreGive(sem[1]);} }
void blink1(void *) { while(true) {xSemaphoreTake(sem[1],portMAX_DELAY); toggle(1); delay(1100); xSemaphoreGive(sem[2]);} }
void blink2(void *) { while(true) {xSemaphoreTake(sem[2],portMAX_DELAY); toggle(2); delay(1200); xSemaphoreGive(sem[3]);} }
void blink3(void *) { while(true){xSemaphoreTake(sem[3],portMAX_DELAY); toggle(3); delay(1300); xSemaphoreGive(sem[4]);} }
void blink4(void *) { while(true){xSemaphoreTake(sem[4],portMAX_DELAY); toggle(4); delay(1400); xSemaphoreGive(sem[5]);} }
void blink5(void *) { while(true){xSemaphoreTake(sem[5],portMAX_DELAY); toggle(5); delay(1500); xSemaphoreGive(sem[6]);} }
void blink6(void *) { while(true){xSemaphoreTake(sem[6],portMAX_DELAY); toggle(6); delay(1600); xSemaphoreGive(sem[7]);} }
void blink7(void *) { while(true){xSemaphoreTake(sem[7],portMAX_DELAY); toggle(7); delay(1700); xSemaphoreGive(sem[0]);} }
void setup() {
for (i = 0; i < 8; i++) {
sem[i] = xSemaphoreCreateMutex();
xSemaphoreTake(sem[i],portMAX_DELAY);
pinMode(LED[i], OUTPUT);
}
xSemaphoreGive(sem[0]);
Serial.begin(115200);
xTaskCreatePinnedToCore(blink0, "blink0", 2048, NULL, 1, &blink_t[0], 0);
xTaskCreatePinnedToCore(blink1, "blink1", 2048, NULL, 1, &blink_t[1], 1);
xTaskCreatePinnedToCore(blink2, "blink2", 2048, NULL, 1, &blink_t[2], 0);
xTaskCreatePinnedToCore(blink3, "blink3", 2048, NULL, 1, &blink_t[3], 1);
xTaskCreatePinnedToCore(blink4, "blink4", 2048, NULL, 1, &blink_t[4], 0);
xTaskCreatePinnedToCore(blink5, "blink5", 2048, NULL, 1, &blink_t[5], 1);
xTaskCreatePinnedToCore(blink6, "blink6", 2048, NULL, 1, &blink_t[6], 0);
xTaskCreatePinnedToCore(blink7, "blink7", 2048, NULL, 1, &blink_t[7], 1);
}
void loop() {
delay(1000);
}
Dieses Beispiel zeigt, wie Sie verschiedene Tasks mit Semaphoren synchronisieren. Normalerweise verwendet man Semaphoren dazu, den Zugriff auf gemeinsam genutzte Speicherbereiche zu steuern: Es geht nie gut, wenn zwei Prozesse gleichzeitig in einem Bereich arbeiten, hier ist Datenverlust vorprogrammiert.
Lektüre
Zu guter Letzt möchte Ihnen der Autor noch einige Ressourcen mit auf den Weg geben, die den Einstieg in die Materie erleichtern. Den ersten Anlaufpunkt bietet die offizielle Dokumentation [5] des Herstellers, die unter anderem alle API-Funktionen des ESP32-RTOS beschreibt. Falls Sie FreeRTOS mit einem anderen Mikrocontroller einsetzen möchten, passt möglicherweise die Originaldokumentation [6] des Betriebssystems besser. Das liegt daran, dass der Hersteller die ESP32-Variante an einigen Stellen angepasst hat, um die MCU optimal zu nutzen.
Fazit
Beim Arbeiten mit FreeRTOS muss man höllisch aufpassen, nicht durch eine kleine Unachtsamkeit im Code den ESP32 zum Absturz zu bringen. Immerhin erhalten Sie üblicherweise eine Fehlermeldung, die Rückschlüsse auf die Fehlerursache zulässt. Mit FreeRTOS können Sie nicht nur die volle Leistung des ESP32 ausschöpfen. Es eröffnet auch viele weitere Möglichkeiten, die das Programmieren erleichtern. Wenn Sie über die ersten einfachen Schritte im Umgang mit dem ESP32 hinaus sind, sollten Sie sich ein wenig Zeit nehmen, um sich in FreeRTOS einzuarbeiten. Der Autor wünscht Ihnen viel Spaß dabei. (tle)
Der Autor
Martin Mohr erlebte die komplette Entwicklung der modernen Computertechnik live mit. Nach dem Studium entwickelte er überwiegend Java-Applikationen. Mit dem Raspberry Pi erwachte seine alte Liebe zur Elektronik wieder.
Infos
- FreeRTOS für den ESP32: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html
- ESP32-Basics: Martin Mohr, “Schneller Einstieg”, RPG 02/2022, S. 56, https://www.raspi-geek.de/47058
- Youtube-Video (
Test2.ino): https://youtu.be/UlF8M90DSJg: - Youtube-Video (
Test3.ino): https://youtu.be/V0z5IYS_EPA: - ESP32-RTOS-Dokumentation: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html
- FreeRTOS-Dokumentation: https://www.freertos.org



