Der ESP32 verfügt neben den beiden Hauptprozessoren über einen weiteren Rechenkern,
der im Hintergrund unabhängig einfache Steurungsaufgaben erledigen kann.
Dieser Prozessor ist in der RTC-Domain untergebracht.
Programmcode und Daten werden im RTC-Slow-Memory, einem 8KB SRAM-Bereich, abgelegt.
Das System hat Zugriff auf die Peripherie der RTC-Unit.
Es kann während der verschiedenen Betriebsmodi der Hauptprozessoren laufen,
also auch während einer Tiefschlafphase des Hauptsytems.
Der ULP-Prozessor begnügt sich mit ca. 1.5mA und
ist weniger anspruchsvoll an die Untergrenze der Versorgungsspannung.
Das Laden des Programmcodes und den Start des Subsystems ist vom Hauptprogramm zu erledigen.
Dies erfolgt zumeist nach dem Booten in Abhängigkeit von der Ursache des Reset.
Hat der Co-Prozessor seine Arbeit erledigt, schickt er sich selbst wieder in eine stromsparende Schlafphase.
Zuvor kann er entweder entscheiden das Hauptsystem aufzuwecken oder bei wachem System per RTC-Interrupt neue Messergebnisse melden.
Der Datenaustausch zwischen Haupt- und Subsystem erfolgt über Variablen im RTC-Speicher.
Die Abarbeitung der Befehle erfolgt in Echtzeit,
d.h. es ist vorhersagbar, wann ein Befehl ausgeführt wird.
Man muss sich nicht (wie im Real-Time-OS) darum sorgen, ob höher priorisierte Tasks den Steuerungsablauf unterbrechen.
Protokollimplementationen wie bspw. für den One-Wire-Bus (Temperatursensor DB18B20) lassen sich über den CoProzessor
releativ einfach realisieren.
Die Architektur ist recht einfach gehalten.
Ein Z80 aus den 1980'ern sieht daneben wie ein Rechenbolide aus.
Es stehen 4 allgemeine 16-Bit-Register und ein 8-Bit-Schleifengegister zur Verfügung.
Die Logik der Rechneneinheit (ALU) beschränkt sich auf Addition, Subtraktion,
logisches UND / ODER, sowie bitweises Lins-/Rechtsschieben.
Dazu kommen Sprungbefehle sowie Befehle zur Ablaufsteuerung.
Angetrieben wird das Ganze durch einen internen 8MHz-Takt.
Programmiert wird der Co-Processor in Assembler. Hochsprachen stehen nicht zur Verfügung.
Hier hilft aber die Simplizität.
Der Einarbeitungsaufwand ist nicht hoch und ein Merkzettel mit einer Befehlsübersicht lässt sich auf einer A4-Seite zusammenfassen.
Zur Integration in eigene Projekte stellt das Framework die notwendigen Funktionen und Makros bereit.
Das Arbeiten nach Schablonen bspw. aus den Beispielprogrammen erleichtert den Einstieg.
Wer vor der Assembler-Programmierung zurückschreckt, sollte einen Blick in die Verwandschaft riskieren.
Der ESP32S2 verfügt zusätzlich über einen ULP-RISC-V Prozessor, der alternativ zur ULP-FSM gernutzt werden kann.
Der ist deutlich komfortabler in C zu programmieren und weist gerinfügig bessere Leistungewerte auf.
Vorbereitung
Im Folgenden wird davon ausgegangen, dass unter Eclipse CDT mit Espressif-Tools programmiert wird und das IDF-Framework installiert ist.
Vor Verwendung des ULP-Prozessors müssen in jedem Projekt einige Vorbereitung getroffen werden.
Config-Variablen setzen
In der zum Projekt gehörenden Datei sdkconfig sind zwei Variablen zu setzen,
die im Build-Prozess die Einbindung des ULP-Systems beinflussen.
Das Editieren der Datei erfolgt entweder im historischen Look auf der Kommandozeile per idf.py menuconfig
oder deutlich komfortabler mit einem Doppelclick im Projekt-Explorer von Eclipse.
Die betreffenden Punkte sind über die Baumstruktur zu finden -> Component config -> ESP32-specific.
Der zu reservierende Speicher wird mit 1024 Bytes initialisiert.
Je nach Anzahl der Befehle + Daten muss der Bereich ggf. vergrößert werden.
Jedes Kommando und jede Variable belegen jeweils 4 Bytes.
Entweder setzt man den Wert gleich höher (max. 8MB) an oder reagiert auf Fehlermeldungen im Build-Prozess.
Unterverzeichnis anlegen
Das bzw.die Assemblerfiles werden in einem separaten Ordner gespeichert,
der auf Ebene der C-Quellen angelegt wird.
Der Name ist beliebig, i.A wird er mit ulp bezeichnet.
Die Assemblerfiles erhalten den Suffix .S.
Die Struktur dieser Files ist recht einfach.
Es loht sich jedoch mit Templates zu arbeiten.
Anpassen des CMake-Prozesses
Auf der Verzeichnisebene des C-Quellcodes befindet sich eine Datei CMakeLists.txt.
Hier sind einige Textzeilen anzuhängen, die im Build-Prozess gelesen werden
und den Ablauf beeiflussen.
# ULP support additions to component CMakeLists.txt.
set(ulp_app_name ulp_${COMPONENT_NAME})
set(ulp_s_sources "ulp/ds18b20.S")
set(ulp_exp_dep_srcs "main_ds18b20.c")
ulp_embed_binary(${ulp_app_name} ${ulp_s_sources} ${ulp_exp_dep_srcs})
Die Parameter sind anzupassen und haben folgende Bedeutung:
ulp_app_name: interner Name im Build-Prozess, muss i.d.R. nicht angepasst werden.
ulp_s_sources: Pfade und Namen aller einzubindenden Assemblerfiles.
ulp_exp_dep_srcs: Pfade und Namen aller C-Soure-Files,
die in Beziehung zum ULP-Programm stehen.
Pfadangaben können absolut oder relativ zu CMakeLists.txt sein.
Die Einträge werden in Hochkomma eingeschlossen.
Mehrere Einträge sind durch Leerzeichen zu trennen.
Speziell der 3.Parameter verlangt einige Sorgfalt.
Fehlende Einträge werden durch CMake nicht erkannt.
Hintergrund: Zum Datenaustausch zwischen Haupt- und Co-Programm werden im Build-Prozess
automatisch Header-Dateien zu jedem Assemblerfile erstellt,
die Referenzen auf die global definierten Variablen enthalten.
Da der Buildprozess mehrere PC-Rechnerkerne verwenden kann, ist nicht vorherzusehen,
ob zuerst eine Header-Datei neu erstellt wird oder das abhängige C-File bearbeitet wird.
Dadurch kann es vorkommen, dass nach Änderungen in einem ASM-File der nächste Build mit Referenzen
auf die Vorgängerversion des ASM-Files gebaut wird.
Eine verzweifelte Fehlersuche ist hier vorprogrammiert.
Die Auflistung der betroffenen C-Quellen in diesem Parameter vermeidet den Ärger.
Das Assembler-File
Im ulp-Ordner ist min. ein Assemblerfile anzulegen,
auf das in der CMakeLists.txt zu verweisen ist.
Das Auslagern von Codeteilen in weitere .S-Dateien ist möglich.
Diese Dateien sind per #include einzubinden und/oder
im zweiten ULP-Parameter der CMakeLists.txt zu registrieren.
Abhänig von der Struktur kann es dabei zu Fehlermeldungen wg. doppelter Referenzen kommen.
Ein Assemblerfile endet per Namenskonvention mit dem Suffix .S.
Es besteht aus mehrerern Segmenten.
.bbs: enthält alle global und local zu definierenden Variablen.
Die Werte werden automatisch mit 0 initialisiert und sind durch das CoProgramm veränderbar.
Auf global definiete Variablen besteht auch Zugriff durch das Hauptprogramm.
.data: enthält alle Konstanten.
Arraywerte sind durch Komma zu trennen.
Zeilenumbrüche innerhalb eines Arrays sind nur maskiert (Backslash) erlaubt.
.text: Dieses Segment enthält den Programmcode.
Es muss eine globale Marke für den Programmeintritt definiert sein.
Dieser Punkt wird i.A. mit entry bezeichnet.
Die Datensegmente sind optional.
Ein Reihenfolgezwang besteht nicht.
Im Code-Segment werden die Befehle der Reihenfolge nach abgearbeitet.
Progrmmverzweigungen per JUMP-Befehl sind möglich.
Der Programmablauf endet mit einem HALT.
Bei Befehls- und Registerbezeichnern ist eine beliebige Groß-/Kleinschreibung möglich.
Variablenbezeichner, Label, u.ä. sind aber case-sensitive.
Hier ein simples Template ohne tiefgründige Funktion:
Die globale Variable "var1" wird in jedem Programmzyklus um 1 erhöht.
In Abhängikeit von Bit0 wird der RTC-Port 8 (GPIO 33) getoggelt.
Das Hauptgramm hat Zugriff auf "var1".
Allerdings erfährt es in diesem Fall nichts von einer abgelaufenen Aktion.
ULP-Programmierung
Die nachfolgenden Angaben beziehen sich auf den CoProzessor des ESP32.
Der ULP der Typenreihe ESP32S2 verfügt über einen erweiterten Befehlsatz.
Der Quellcode ist aufwärtskompatibel, nicht jedoch der Binärcode.
Zur Unterbringung von Code und Daten steht ein Speicherbereich von 8kB zur Verfügung.
Im Build-Prozess werden Binärdaten erstellt, die im Datenbereich des Hauptprogramms abgelegt werden.
Per Anweisung im Hauptprgramm werden die Daten in den RTC-Slow-Mem geladen und bei Bedarf das CoProgramm gestartet.
Daher ist es nicht möglich, den CoProzessor völlig autark zu betreiben.
Zur Initialisierung wird immer das ESP32-Hauptprogramm benötigt.
Grundsätzlich werden alle Befehle und Daten des ULP-Programms im 32-bit-Raster ausgerichtet.
Jeder ASM-Befehl wird incl. Parametern in ein 32-bit-Wort übersetzt.
Auf Variablen kann der CoProessor nur zugreifen, wenn diese an 32-bit-Grenzen ausgerichtet sind.
Daher sind nur .long-Typen als Variablen sinnvoll.
Der Assembler nimmt zwar Typangaben wir ".byte" entgegen und setzt die im Binärcode ordnungsgemäß um.
Doch was nützt das, wenn der ULP-Prozessor nicht darauf zugreifen kann.
Das Resultat eines Assemblerlaufs lässt sich als Hexdump im Build-Ordner anschauen: build/ulp_xxx.bin.S.
Der belegte Speicherplatz wird darin in 32-bit-Words angezeigt.
Die Register
Der ULP-Prozessor verfügt über 4 allgemeine 16-Bit-Register R0, R1, R2, R3.
Register R0 hat bei einigen Befehlen Spezialaufgaben.
In JUMPR wird sein Wert mit einem Zahlenwert verglichen und daraus die Sprungbedingung abgeleitet.
Beim Lesen interner CPU-Register wird in R0 das Ergebnis zurückgeliefert.
In der Datenbreite von 16 Bit liegt eine Diskrepanz zur 32-bit-Breite von Variablen.
In der Tat lassen sich nur die unteren 16 Bit einer Variablen im Speicher lesen und schreiben.
Dies muss beim Zugriff auf diese Daten aus dem Hauptprogramm beachtet werden,
aus dessen Sicht es sich um UINT32 handelt,
aber nur die unteren 16Bit gültig sind.
Die oberen 16 Bit enthalten den Wert des Programm-Counters zum Zeitpunkt der Schreibens der Variablen (zzgl. Linksshift).
Nach dem Lesen einer Variablen im Hauptprogramm sollte der Wert mit & 0xFFFF maskiert werden.
Als Spezialregister für Zählschleifen steht außerdem der Stage-Counter zur Verfügung.
Er hat eine Breite von 8Bit.
Der Wert lässt sich zurücksetzen, erhöhen und verringern.
Das war's, mehr Register gibt es nicht.
Es gibt auch keinen Stack, in den man mal schnell pushen könnte.
Dererlei Funktionen müssen über Speichervariablen nachgebildet werden.
Die Flags
Auch die Flags des CoProzessors sind übersichtlich.
Es gibt das Zero-Flag EQ und das Overflow-Flag OV.
Beide Flags werden durch ALU-Operationen gesetzt
und können im JUMP-Befehl als Bedingung ausgewertet werden.
Die Logik beim Sezten der Flags ist nicht immer nachvollziehbar.
Bei ADD un SUB werden die Flags gesetzt wie erwartet.
Ein LSH beinflusst das OV-Flag nicht, auch wenn eine 1 nach links rausgeschoben wird.
Dafür wird das EQ bei jedem MOVE-Kommando beeinflusst.
Aber was soll's. Man muss es nur wissen.
Zahlenwerte
Einige Befehle nehmen direkte Zahlenwerte (Immediate) entgegen.
Unzulässige Werte bemängelt der Assembler nicht in jedem Fall.
Die eingegeben Zahlen werden intern durch Maskierung auf den zulässigen Wertebereich begrenzt.
Dies kann zu unerwarteten Ergebnissen führen.
Die Doku beschreibt die Zahlenwerte i.A. als 16-bit-signed. Ob man die Werte als signiert oder
unsigniert betrachtet, hängt vom Anwendungsfall ab.
Bei vergleichenden Operationen (JUMPR / JUMPS) werden immer unsignierte Werte miteinander verglichen.
Hier eine Zusammenfassung unter Weglassen der Peripherie-Befehle.
ALU - Die Recheneinheit
Folgende Befehle nutzen die ALU:
ADD Rdst=Rsrc1+Rsrc2 Addition
SUB Rdst=Rsrc1-Rsrc2 Subtraktion
AND Rdst=Rsrc1&Rsrc2 Logisches UND
OR Rdst=Rsrc1|Rsrc2 Logisches ODER
MOVE Rdst=Rsrc1 Register mit Wert laden
LSH Rdst=Rsrc1<<Rsrc2 bitweise Linkschieben
RSH Rdst=Rsrc1>>Rsrc2 bitweise Rechtsschieben
Hinweise:
Alle Befehle beeinflussen das Zero-Flag
Nur Add und SUB beeinflussen das Overflow-Flag
Alle Operationen werden mit 16-Bit-Werten ausgeführt
Der maximale Shiftwert bei LSH/RSH beträgt 0x0F (0x10 => 0x00, 0x11 => 0x01)
...
Speicherzugriff
Zum Zugriff auf Speicher-Variablen stehen folgende Befehle zur Verfügung:
ST Rsrc,Rdst,offset mem[rdst+offset] = Rsrc
LD Rdst,Rsrc,offset Rdst = mem[Rsrc+offset]
Hinweise:
Die Adressierung des Speichers im 2.Parameter erfolgt im 32-Bit-Raster
Der Offset wird in Bytes angegeben
Der Offset lässt sich nur als Direktwert angeben [0..0x7FF]
Beispiel:
.bss
arr: .long 0 //Beispiel-Array
.long 0
.text
MOVE R3, arr //Zeiger auf 1.Arrayelement
MOVE R0, 1234
ST R0, R3, 0
MOVE R0, 5678
ST R0, R3, 4 //2.Element über Offset adressieren
//oder
ADD R3, R3, 1 //Zeiger auf nächsten 32-Bit-Speicherplatz
ST R0, R3, 0 //2.Element mit Nulloffset adressieren
Sprungbefehle
JUMP Sprung zu abs. Adresse, EQ, OV als Bedingung möglich
JUMPR Sprung zu rel. Adresse, Bedingung: Vergleich R0 mit 16Bit-Direktwert
JUMPS Sprung zu rel. Adresse, Bedingung: Vergleich STAGE_CNT mit 8Bit-Direktwert
Hinweise:
Die rel. Sprungdistanz errechnet der Assembler selbst (Adressierung per label)
Die max. rel. Sprungdistanz beträgt +/- 64 Befehle
Derzeit verursacht der Assembler (2.28.51.20170517) einen Fehler beim JUMPR-Befehl:
Direktwerte > 0x7FFF führen zu einer unsinnigen Fehlermeldung
Es kann nur als Vergleichswert im JUMPS-Befehl genutzt werden
nützlich als Schleifenzähler
Ablaufsteuerung
HALT Ende des ULP-Programms, Startet WakeUp-Timer wenn enabled
WAKE Aufwecken aus dem DeepSleep oder RTC-Interrupt
SLEEP Auswahl [0..4] eines Sleep-Profils
WAIT U16 Ausführung des Programms für n*125ns unterbrechen
NOP Mache für die Dauer eines Befehls nichts (750ns)
Desweiteren ist ein Befehl TSENS zur Abfrage eines internen Temperatursensors dokumentiert.
Dieser Sensor scheint aber obsolete zu sein.
Das Ergebnis dieses Befehls ist immer 128.
Makros
Speziell Befehle zum Zugriff auf interne Register sind ungeeignet für das Langzeitgedächtnis.
Die Definition von Makros hilft hier weiter.
Folgende Zeilen lassen sich zur Steuerung eines einzelnen RTC-Ports in den Quelltext einbinden:
Im Buildprozesses wird der Binärcode des ULP-Programms erzeugt und im Datenbereich des Hauptprogramms abgelegt.
In Zwischenschritten werden dabei zahlreiche Dateien im build-Verzeichnis des Projektes automatisch angelegt.
Deren Inhalt muss man nicht kennen.
Zum Verständnis der Vorgänge kann es aber hilfreich sein, sich die eine oder andere anzuschauen.
Hält man sich an die o.g. Namenskonventionen,
ist in der Datei build/ulp_main.bin.S der Maschinencode in Hexform zu sehen.
Interessant sind die vergebenen globalen Namen, die im Hauptprogramm zu verwenden sind.
Etwas tiefer versteckt ist die Header-Datei build/esp-idf/main/ulp_main/ulp_main.h.
Sie enthält Deklarationen der globalen Variablen des ASM-Files.
Den Variablennamen wird dabei der Prefix ulp_ vorangestellt.
Über diese Namen hat das Hauptprogramm Zugriff auf die Variablen im RTC-Memory
und ein Datenaustausch mit dem Co-Programm ist möglich.
Der Header muss in den C-Quellen per #include ulp_main.h eingebunden werden.
Pfadangaben sind dabei nicht erforderlich.
Bekanntmachung
Im C-Sourcefile, in dem das ULP-Programm geladen werden soll, ist der Datenbereich zu deklarieren
und sind die erforderlichen Definitionen einzubinden:
Das Laden des Maschinencodes in den RTC-Slow-Mem, die Initialisierung und der Start des ULP-Programms werden im Hauptprogramm erledigt.
Der Zeitpunkt hängt vom Anwendungsfall ab.
In vielen Fällen ist es sinnvoll,
dies gleich nach dem Programmstart in Abhängigkeit vom letzten Resetgrund zu erledigen.
Kommt der Soc bspw. aus dem DeepSleep, was ggf. durch den ULP-Prozessor angestoßen wurde,
ist davon auszugehen, dass der Code intakt ist und keine Initialisierung erforderlich ist.
Nach allen anderen Reset-Ursachen ist der Code erneut hochzuladen.
Anschließend können globale Variablen und ggf. die verwendeten RTC-Ports initialisiert werden.
ulp_load_binary: Laden des Binärcodes in den RTC-Speicher
ab Addresse des RTC-Slow_Mem (0),
Zeiger auf Datenbereich des Binarycodes,
Länge der Daten in 32Bit-Worten
ulp_set_wakeup_period: Ruhezeit des ULP-Programms nach einem HALT.
Es lassen sich bis zu 5 (Reg[0..4]) unterschiedliche Zeitspannen in µs setzen,
die im ULP-Programm durch den SLEEP-Befehl ausgewählt werden können.
Default ist Register 0.
Die maximale Zeit wird durch die 32-Bit-Variable begrenzt (2^32µs ==> ca.71 Minuten).
ulp_run: Start des ULP-Programms.
Es erfolgt ein Sprung zu der im ASM-File als zur global definierten Marke .entry.
Wecker einschalten
In Anwendungen, die in den DeepSleep geschickt werden und durch ein WAKE aus dem Co-Programm wieder zum Leben erweckt werden sollen,
muss als WakeUp-Quelle der ULP-Prozessor zugelassen werden. Dies erfolgt mit folgendem Codeschnipsel bspw. zum Abschluss des Hauptprogramms.
Der ULP-Prozessor kann nur einen Teil der Pads des ESP ansteuern.
Insgesamt sind dem RTC-Subsystem 18 Ein-/Ausgänge zugeordnet.
Die Nummerierung erfolgt von 0 bis 17.
Es besteht allerdings kein Zusammenhang mit der Nummerierung der GPIO über den IO-Mux.
D.h.: Ein und dasselbe Pad wird in den beiden Systemen über unterschiedliche Pin-Nummern angesprochen.
Eine Übersicht und die Zuordnung ist in der
RTC_MUX Pin List
zu finden.
Es gelten die gleichen Einschränkungen, die auch bei der Funktionalität als GPIO zu beachten sind.
Ein Teil der Pads kann nur als Eingang verwendet werden. Andere können mit speziellen
Funktionen
belegt sein.
Vor Verwendung der Pins im RTC-Subsystem müssen die Ports initialisiert werden.
Dies erfolgt i.A im Hauptprozess.
Das Framework stellt das notwendige
API
bereit.
Unbedingt zu beachten ist, dass hier die GPIO-Nummerierung zu verwenden ist und nicht die Nummerierung des RTC-Subsystems.
Über diese Befehle können auch im Hauptprogramm die RTC-Pads gesteuert werden
Wichtig ist an dieser Stelle rtc_gpio_init().
Auch die allgem. Konfiguration wie I/O-Richtung oder pullup/dwn ist hier sinnvoll.
Die Änderung der Eigenschaften ist später im ULP-Programm über Registerbefehle möglich.
Der Befehlssatz des Assemblers kennt keine Befehle zur Steuerung der RTC-IO.
Das Schalten von Ausgängen oder Einlesen von Eingängen erfolgt über das Schreiben von Registern.
Da sich hierbei äußerst sperrige Befehlszeilen ergeben, ist die Verwendung oben beschriebener Macros sinnvoll.
Kopiert man sich die Macros an den Anfang seines Assemblerfiles, wird der IO-Zugriff zum Kinderspiel.
Der Assembler ersetzt vor dem Build die Macro-Bezeichner durch die definierten Befehle und fügt zusätzlich die Argumente an vorgegebener Stelle ein.
Hierdurch lässt sich zwar kein Speicherplatz sparen, der Code gewinnt aber deutlich an Lesbarkeit.
I2C - Ports
Grundsätzlich verfügt die RTC-Unit über eine I2C-Funktionalität.
Die sinnhafte Verwendung ist allerdings durch mehrere Faktoren eingeschränkt.
Es ist lediglich das Schreiben und Lesen einzelner 8-Bit-Werte möglich.
Eine Multibyte-Funktonalität kennt das System nicht.
Sollen bspw. mehrere Register eines Slave gelesen werden, so muss jedes Register neu adressiert werden.
Auch wenn der Slave das Single-Byte-Reading zulassen sollte, kann dies zu erheblichem Bit-Overhead führen.
Außerdem sind die verwendbaren Pads durch andere
Funktionen
in ihrer Verwendbarkeit eingeschränkt.
Solange es nur um die simple Kommunikation mit einem Slave geht, lässt sich das I2C-Protokoll auch Bit-by-Bit über beliebige RTC-IO nachbilden.
Ein Beispiel zur Kommunikation mit einem Sensormodul BME280 ist
hier
zu finden.
Die Initialisierung des Sensorchips sowie die rechenintensive Kompensation und Auswertung der Messergebnisse übernimmt der Mainprocess.
Der ULP-Process wird timergesteuert angestoßen, initialisiert die Messung, wartet bis zur Bereitstellung der Ergebnisse, und liest die Daten-Register.
Anschließend entscheidet er anhand der Raw-Daten, ob der Hauptprozess zu starten ist.
Analog-Digital-Wandler
Die Ansteuerung der ADC-Ports und das Einlesen von Messwerten ist mit dem ULP-Prozessor möglich.
Ein Beispiel ist in den IDF-Examples zu finden.
RTC Interrupt
Im vorhergehenden Scenario wurde davon ausgegangen,
dass Haupt- und Co-Programm zeitversetzt ausgeführt werden.
Das Hauptprogramm befindet sich im DeepSleep,
das Co-Programm erwacht zeitgesteuert, führt seine Aktionen aus,
weckt bei Bedarf das Hauptprogramm und geht wieder in den Haltzustand.
Aber auch zeitgleich zum Hauptprogramm kann der ULP-Prozessor sinnvolle Aufgaben übernehmen.
Von Vorteil ist, dass das ULP-Programm im wahren Echtzeitbetrieb abläuft.
Unterbrechungen durch einen höher priorisierten Prozess sind nicht zu erwarten.
Damit lassen sich einfache Kommunikationsprotokolle mit zeitkritischen Merkmalen implementieren,
die durch die Hardware des ESP32 nicht abgedeckt werden.
Aufgabe des Co-Programms ist es dann,
eine Meldung über das Vorliegen neuer Daten zu senden.
Eine Möglichkeit wäre es, eine globale Variable als Flag zu setzen.
Das Hauptprogramm müsste dieses Flag zyklisch abfragen und nach Übernahme der Daten zurücksetzen.
Deutlich eleganter ist das Auslösen eines RTC-Interrupts durch das ULP-Programm.
Dies erfolgt ebenfalls durch den WAKE-Befehl des ULP-Prozessors.
Der Unterschied liegt dabei im Hauptprogramm.
Statt eine WakeUp-Quelle freizugeben,
wird das ULP Interruptbit im entsprechenden Register gesetzt
und ein Interrupt-Hander initialisiert.
Die eigentliche Datenverarbeitung wird an einen separaten Task niedriger Priorität delegiert
und die ISR-Routine so schnell wie möglich wieder verlassen.
nach oben