Heute möchte ich zeigen, welche "Leistungsressourcen" im ESP32 stecken. Es ist viel weniger bekannt, dass unser ESP32 Mikrocontroller kein Einzelprozessor ist, sondern ein Multiprozessor mit 2 Kernen. Im ESP arbeiten 2 Xtensa 32-bit LX6 CPUs, die sich RAM und ROM teilen. Damit unterscheidet er sich von seinem Vorgänger, dem ESP8266. Die beiden Kerne haben unterschiedliche Namen. CPU 0 wird auch Protokoll-CPU (PRO_CPU) und CPU 1 Anwendungs-CPU (APP_CPU) genannt. Die CPU 0 steuert das WLAN, Bluetooth und andere interne Peripheriegeräte wie SPI, I2C, ADC, usw., während die CPU 1 für unser Anwenderprogramm zur Verfügung steht. Sketche, die wir in der Hauptschleife schreiben und auf den ESP hochladen, werden ausnahmslos auf der CPU 1 ausgeführt, während die APP_CPU (CPU 0) standardmäßig für den Anwendungscode ausgelassen wird. Das folgende Diagramm zeigt die Standardverteilung der Aufgaben auf die CPUs:
Es ist zu erkennen, dass 2 Kerne, die die Leistung des ESP32 fast verdoppeln, nicht direkt zur freien Verwendung verfügbar sind.
Allerdings stellt das ESP Framework auch Funktionen mit der Arduino IDE zur Verfügung, die es erlauben, einzelne Aufgaben auf die ESP32-CPUs und damit auf die CPU zu verteilen.
TaskHandle_t NamedesTaskhadle; |
zur Verfügung. Um eine neue Aufgabe zu erstellen, verwenden wir die Funktion xTaskCreatePinnedToCore mit den folgenden Optionen:
xTaskCreatePinnedToCore ( |
Unser Ziel ist es, benutzerdefinierten Code als Task auf der CPU1 auszuführen. Daher wird unser Code als Task auf der CPU1 unabhängig von der CPU0 ausgeführt, wie in der folgenden Abbildung dargestellt:
Wir geben nun den folgenden Beispielcode in unsere IDE ein und laden ihn auf den ESP32:
TaskHandle_t Core0TaskHnd ; TaskHandle_t Core1TaskHnd ; void setup() { Serial.begin(9600); xTaskCreatePinnedToCore(CoreTask0,"CPU_0",1000,NULL,1,&Core0TaskHnd,0); xTaskCreatePinnedToCore(CoreTask1,"CPU_1",1000,NULL,1,&Core0TaskHnd,1); } void loop() { Serial.print ("Application CPU is on core:"); Serial.println (xPortGetCoreID()); delay (500); } void CoreTask0( void * parameter ) { for (;;) { Serial.print("CoreTask0 runs on Core: "); Serial.println(xPortGetCoreID()); yield(); delay (600); } } void CoreTask1( void * parameter ) { for (;;) { Serial.print("CoreTask1 runs on Core: "); Serial.println(xPortGetCoreID()); delay (700); } }
.
Mit der ESP internen Funktion xPortGetCoreID() können wir uns die Kernnummer ausgeben lassen, auf dem unser Codeabschnitt gerade läuft. Diese Kernnummer kann entweder den Wert 0 oder 1 annehmen. Diese Funktion nutzen wir, um seriell Informationen darüber auszugeben, auf welchem Core der Task gerade läuft:
Wir sehen nun in der Ausgabe das insgesamt 3 Tasks laufen. Ein Task mit dem Namen „CoreTask 0“ auf CPU 0, ein Task mit dem Namen „CoreTask1“ auf CPU 1 sowie unser Hauptscheifentask (loop) auf Core 1.
Bis jetzt klingt alles zu schön, um wahr zu sein. In der Tat haben wir mit der Nutzung der CPU 0 ein Problem, dem wir Beachtung schenken müssen: Wie auf der oberen Bild gezeigt, läuft auf der CPU 0 auch der Kernel Protokoll Task. Dieser Task kümmert sich unter anderem auch um den WiFi und TCP/IP Stack. Wenn dieser längeren Zeit nicht ausgeführt wird, weil zum Beispiel unser Task zu viel CPU Zeit fordert, kann das System insgesamt instabil werden und abstürzen. Wir müssen also dafür Sorge trage, dass unser eigener Task keine oder nur maximal sehr klein bemessene delay-Anweisungen erhält, damit der Kernel Protokoll Task genügend Rechenzeit zugewiesen bekommt.
Em aufmerksamen Leser wird ein weiteres Problem des Codes aufgefallen sein: Das Programm erzeugt 3 Tasks, die unabhängig voneinander z.T. auf unterschiedlichen CPU’s laufen, sich dennoch aber eine Ressource (den COM Port des ESP‘s) teilen. Da grundsätzlich die Tasks nichts voneinander „wissen“ und somit auch nicht, wann eine Ressource von einem anderen Task belegt oder verändert wird., kann es hier zu Kollisionen kommen. Diese verursachen ein nicht vorhersagbares Ergebnis, da nicht genau bestimmt werden kann, zu welcher Zeit welcher Task die Ressource verwendet. Solche Konstellationen können dann im besten Fall entweder in einer programmatischen Race Kondition oder gar in einem Deadlock enden. Was genau ein Deadlock ist, erklärt das Philosophenproblem, wo 5 Philosophen um einen Spaghetti-Tisch sitzen, sehr anschaulich. Ich möchte gegenseitige Probleme durch gegenseitigen Ausschluss (Mutex) und Kollisionen beim Zugriff auf gemeinsame Ressourcen Suche als Variablen oder Schnittstellen zu vermeiden.
Damit sind wir mitten im Thema der Interprozesskommunikation. Wir haben eine Menge über Tasks und Multitasking gelernt.
Mehr über Task Generierung und das Real Time Operation System (RTOS) finden Sie im zweiten Teil dieser Serie oder auf:
https://exploreembedded.com/wiki/index.php?title=Hello%20World%20with%20ESP32%20Explained
Und nun viel Spaß beim Experimentieren.
5 comments
PiffPoff
“Wir müssen also dafür Sorge trage, dass unser eigener Task keine oder nur maximal sehr klein bemessene delay-Anweisungen erhält, damit der Kernel Protokoll Task genügend Rechenzeit zugewiesen bekommt.”
Wenn der andere Task mehr Rechenzeit bekommen soll, dann ist es doch gut wenn der eigene task möglichst lange suspended ist.
Also ist es doch gut wenn der eigene task viele/lange delays hat, oder?
Siggi
Hallo und guten Tag,
herzlichen Dank für die Erklärung. Hat mir sehr dabei geholfen, ein flackerndes Display in den Griff zu bekommen. Unabhängig von der Berechnung wird jetzt die Anzeige über CPU0 ausgegeben.
Kleine Anmerkung zur Ressourcenaufteilung:
Wird eine Funktion aus dem Core Task aufgerufen, wird diese Funktion auch in der zugehörigen CPU ausgeführt.
Liebe Grüße Siggi
doob
{
Serial.begin(9600);
xTaskCreatePinnedToCore(CoreTask0,“CPU_0”,1000,NULL,1,&Core0TaskHnd,0);
xTaskCreatePinnedToCore(CoreTask1,“CPU_1”,1000,NULL,1,&Core0TaskHnd,1);
}
noch ein Tippfehler? sollte es beim zweiten pinning nicht Core1TaskHnd heißen?
Sven
CPU 1 ist für das Anwenderprogramm verantwortlich.
Der Tippfehler wird bestimmt zeitnah korrigiert.
veit
Diese Namentliche Unterscheidung wird getroffen, um zu verdeutlichen, dass die CPU 0 das WLAN, Bluetooth und andere interne Peripheriegeräte wie SPI, I2C, ADC usw. steuert, während die CPU 0 für unser Anwenderprogramm zur Verfügung steht.
bitte korrigieren …. irgendwas müsste von cpu 1 gemacht werden