Bekannte Fallstricke¶
Probleme, die auf dieser Hardware bzw. mit diesem Framework-Stand schon einmal Zeit gekostet haben -- mit Symptom, Ursache und Regel.
PSRAM und Display-Flackern¶
Das RGB-Panel liest seinen Framebuffer per DMA in Echtzeit direkt aus dem
PSRAM, ohne Bounce-Buffer (fb_in_psram = 1, kein
bounce_buffer_size_px in TRGBArduinoSupport/src/TRGBSuppport.cpp).
Jeder konkurrierende PSRAM-Zugriff kann den DMA kurz aushungern und wird
als Bildverschiebung sichtbar. Zwei unabhängige Ursachen:
1. Häufiger PSRAM-Zugriff aus einem Hot-Path -- Symptom: Pixel
nach rechts verschoben, mit Umbruch, unregelmäßig.
Beispiel: Statistics::timeData.currentMinMax lag im PSRAM und wurde bei
jeder BLE-Sensor-Notification beschrieben → Flackern etwa 1×/s.
Regel: Vor dem Verschieben einer Datenstruktur ins PSRAM ihre
Zugriffshäufigkeit und ihren Auslöser prüfen, nicht nur die Größe.
- OK: groß, aber selten berührt (z. B. Verlaufs-Ring, alle 5-15 s).
- Nicht OK: auch kleine Felder, die aus BLE-Callbacks, pro LVGL-Frame oder
sonst unvorhersehbar oft geschrieben/gelesen werden -- inkl. Arrays, die
LVGL direkt referenziert (lv_chart_set_ext_y_array).
2. PSRAM-Allokation vor trgb.init() -- Symptom: Bild nach links
verschoben, leicht unregelmäßig, mit Pausen bis ~2 s.
TRGBSuppport.cpp legt die beiden 460-KB-LVGL-Draw-Buffer mit einfachem
heap_caps_malloc(..., MALLOC_CAP_SPIRAM) ohne Alignment an. Alles, was
vorher PSRAM belegt, verschiebt deren Lage. Statische Objekte (siehe
Singletons.cpp) werden vor main() konstruiert, also vor trgb.init().
Regel: Nie PSRAM im Konstruktor eines statisch angelegten Objekts
allokieren, egal wie selten die Daten benutzt werden. Stattdessen aus
einem setup(), das nach trgb.init() läuft (Muster:
Statistics::allocPsramBuffers()).
Diagnose: Serial-CLI mem gibt über WebInstr::reportDisplayBuffers()
Adressen und Alignment der Draw-Buffer aus. Den Diagnose-Build vor dem
Fix flashen und messen -- nur den reparierten Stand zu messen zeigt den
guten Zustand und führt zum falschen Schluss.
Offen: Ob der eigentliche Hebel das Alignment ist (dann würde ein Patch auf
heap_caps_aligned_alloc(64, ...) über apply_patches.py die ganze
Klasse erledigen) oder nur die Lage, ist nicht bewiesen.
Starkes Flackern während OTA-Updates ist unabhängig davon eine Eigenschaft der Hardware: Solange der Flash geschrieben oder gelöscht wird, steht der Bus, über den das Panel sein Bild aus dem PSRAM liest. Dasselbe im Kleinen ist die dritte Ursache:
3. NVS-Schreiben -- Symptom: ein einzelnes kurzes Flackern, regelmäßig (vor 2026-10 alle 45-75 s beim Fahren, 2-3×/10 min im Stand). Siehe den nächsten Abschnitt.
NVS-Schreiben und Display-Flackern¶
Nicht der einzelne Schreibvorgang flackert, sondern das Löschen einer NVS-Seite (Sektor-Erase, ~100 ms gemessen). Das NVS hängt neue Werte nur an; ist die Seite voll, kopiert es die noch gültigen Einträge um und löscht eine Seite. Wie oft das passiert, lässt sich ausrechnen:
- Eine Seite fasst 126 Einträge. Eine Zahl (bis 64 Bit) kostet 1 Eintrag,
ein String oder Blob
2 + Größe/32(aufgerundet). EinputFloat()ist ein Blob, also 3 Einträge. - Pro Löschung werden etwa
126 × (1 − Füllgrad)Einträge frei. Der Füllgrad steht auf/debug/nvs. - Löschungen pro Zeit = geschriebene Einträge pro Zeit / frei werdende Einträge.
Beispiel vor 2026-10: Füllgrad ~70 % → ~36 Einträge pro Löschung (gemessen:
jeder zwölfte putFloat() dauerte ~100 ms länger). Die Statistik schrieb
beim Fahren ~48 Einträge/min → eine Löschung alle ~45 s.
Regeln:
- Nichts periodisch Schnelles ins NVS. Der Hebel ist die Schreibrate, nicht die Partitionsgröße: eine größere Partition senkt nur den Füllgrad und bringt höchstens Faktor 126/36.
- Zusammengehörige Werte als ein Struct-Blob speichern
(
NvsUtil::loadBlob()/saveBlob(),src/NvsUtil.h), nicht als einzelne Schlüssel -- vor allem keine einzelnen Floats. - Unveränderte Werte schreibt das NVS nicht (ESP-IDF vergleicht vor dem Schreiben). Ein eigenes „nur wenn geändert" spart nur den Lesezugriff.
- Die Fahrstatistik (
Statistics::persistNow()) schreibt einen Blob von 8 Einträgen alle 5 min, beim Anhalten und vor dem Ausschalten -- das ist rund eine Löschung in 30-50 min. Wer dort etwas hinzufügt, rechnet nach. - NVS nur aus Tasks mit genug Stack schreiben, nie aus dem UI-Task (siehe
„UI-Task: kaum Stack übrig"). Muster:
Statistics::requestPersist()setzt nur ein Flag, geschrieben wird im nächstencycle().
BLE-Stack ist NimBLE, nicht Bluedroid¶
Mit der pioarduino-Plattform (Arduino-ESP32 3.3.x) läuft der BLE-Stack auf
NimBLE. BLEAddress ist eine Kompatibilitätsschicht über beide Stacks und
verhält sich dort asymmetrisch:
getNative()liefert unter NimBLE die Bytes umgekehrt (Bluedroid: Anzeigereihenfolge).BLEAddress(uint8_t[6])kopiert unter NimBLE ebenfalls umgekehrt --getNative()raus / Konstruktor rein ist also kein Roundtrip.equals()/operator==vergleicht zusätzlich den Adresstyp.
Regel (src/BLEDevices.cpp): mit getNative() speichern, beim Laden per
memcpy in getNative() eines default-konstruierten BLEAddress
zurückschreiben; Peers mit dem dateilokalen sameAddress() vergleichen,
nicht mit equals(). Bei Problemen mit Peer-Identität oder
Adressanzeige zuerst hier suchen.
TrailBridge: keine gespeicherte Adresse, und kein Disconnect, auf den Verlass ist¶
Android wirbt unter einer Adresse, die wechselt -- auf diesem Handy bei jedem Neustart der App und nach einem fehlgeschlagenen Verbindungsversuch, nicht nur alle paar Minuten. Der TrailBridge-Slot merkt sich deshalb keine Adresse, auch nicht im RAM (bis 2026-10 tat er das und wies das Handy nach dem ersten Verbindungsabbruch bis zum Neustart ab). Er nimmt das erste gefundene Handy, solange er keine Verbindung hat, und weist jede andere TrailBridge-Adresse ab, solange er eine hat: Das verbundene Handy wirbt weiter, möglicherweise schon unter einer neuen Adresse.
Wird die App beendet oder neu gestartet, hält Android die Verbindung selbst aufrecht; es
kommt kein Disconnect. Die tote Verbindung wird stattdessen am Heartbeat des Protokolls
erkannt: 30 s ohne Nav- oder GPS-Frame beenden sie (BLEDevices::checkNavAlive()), der
nächste Scan verbindet neu. Gemessen 2026-10-02: 46 s vom Neustart der App bis zur neuen
Verbindung.
Arduino String und c_str()¶
BLE-readValue()/getValue() liefern unter Arduino-ESP32 3.x String.
Ein c_str()-Zeiger ist nur gültig, solange das String-Objekt lebt und
nicht verändert wird. Beim Weiterreichen an LVGL oder über
Task-/Queue-/Callback-Grenzen prüfen, dass niemand den Zeiger behält.
lv_label_set_text() kopiert, lv_label_set_text_static() nicht.
Zwei USB-Ports¶
Das Board hat einen nativen USB-CDC-Port (verschwindet bei Hard-Reset kurz
vom Bus) und einen externen USB-Seriell-Wandler (bleibt enumeriert). Ob ein
Reset stattgefunden hat, daher an Boot-Markern im Log erkennen (z. B.
"👨🏭 Start" aus BLEDevices::scanAndConnectTask()), nicht an
USB-Disconnect-Events.
Beim Öffnen von /dev/ttyACM0 aus eigenen Skripten nicht dtr = False/rts =
False vor open() setzen: pyserial schaltet danach erst DTR ab, und DTR=0 bei RTS=1
löst beim USB-JTAG-Port einen Reset aus ("Reset reason: USB"). Mit den Standardwerten
(beide gesetzt) bleibt das Board an.
UI-Task: kaum Stack übrig¶
Der UI-Task (UIFacade::initDisplay(), 4096 Byte) hat im Betrieb weniger als 1 KB frei
(mem → STACK UI Task). EEZ-Actions und LVGL-Timer laufen in diesem Task. Alles
Schwere dort nicht direkt ausführen, sondern einen eigenen kurzlebigen Task starten:
NVS-Schreiben, SD-Zugriffe, delay(), Rendern eines ganzen Screens (lv_snapshot legt
zusätzlich ~500 Byte Display-Strukturen auf den Stack). Muster: shutdownTask() in
src/ui/RimRidgeSettingsCustFunc.cpp, snapTask() in src/UiDebug.cpp
(mit UIFacade::runLocked()).
Fehlende Zeilen im Debug-Log¶
BCLogger::log() wartet höchstens 100 ms auf den Datei-Mutex und verwirft die Zeile
danach (nur "File Log output blocked" auf printf). Das passiert, wenn gerade ein
SD-Flush oder ein NVS-Schreibvorgang läuft, z. B. beim Speichern der Distanz. Eine
fehlende Logzeile beweist also nicht, dass der Code nicht lief; lieber eine Folgewirkung
prüfen (Reset-Grund, gespeicherter Wert, spätere Zeile).
EEZ Studio / LVGL-Build¶
src/ui_eez/wird bei jedem Export komplett überschrieben -- eigene Logik gehört nachsrc/ui/RimRidge*CustFunc.*.- EEZ-generierter Code inkludiert
<lvgl/lvgl.h>; der Shiminclude/lvgl/lvgl.hleitet auf<lvgl.h>um. - Bei der Umrechnung von SquareLine-Layouts gilt: Positionen von Kindern eines Flex-Containers sind bedeutungslos, LVGL rechnet sie zur Laufzeit.
LVGL-Widgets auf EEZ-Screens¶
Alle Punkte hier scheitern still: kein Build-Fehler, nur falsches Verhalten auf dem Gerät.
- Tastatur (
lv_keyboard) mit den RimRidge-Fonts: Diese Fonts haben keineLV_SYMBOL_*-Glyphen (Backspace, OK), die Tasten nutzen daher die eingebauteMONTSERRAT_22. Das Standardlayout hat 12 Tasten pro Reihe (hier 30 px);RimRidgeWifiCustFunc.cppsetzt eigene Maps und einen eigenen Tasten-Handler, der EEZ-Canvas zeigt deshalb das LVGL-Standardlayout, das Gerät das QWERTZ-Layout. - Gesten kommen nicht an. Zwei Bedingungen, beide nötig:
SCROLLABLEauf dem Screen-Root und auf dem Container löschen. EEZ lässt es auf Page-Roots standardmäßig an; ein scrollbarer Vorfahre unterdrückt die Gesten-Erkennung für den ganzen Druck.GESTURE_BUBBLEauf dem Container mit dem Handler löschen (Kinder dürfen es behalten). LVGL reicht die Geste an den ersten Vorfahren ohne dieses Flag weiter -- sonst landet sie am Screen-Root. Handler direkt auf einem Screen-Root brauchen das nicht.- Große Arcs/Slider schlucken Touches. Nur-Anzeige-Arcs (Speed-,
Distanz-Ring) brauchen
clickableFlag: false, sonst beanspruchen sie jeden Druck für ihr eigenes Ziehen, und Gesten/Klicks darunter feuern nie. zoom/anglewirkt nicht auf Bilder im FormatINDEXED_*oderALPHA_1/2/4BIT(LVGL 8.4 transformiert nur Formate, die der Decoder komplett dekodiert:TRUE_COLOR*,ALPHA_8BIT,RGB565A8). Betrifft die alten Nav-Icons insrc/ui/img/ui_img_nav_*allimages.c(1 bit) -- diese nur in nativer Größe verwenden. Umrechnen aufALPHA_8BITkostet ~4 KB pro 64-px-Icon; Flash ist knapp.- Icons rendern schwarz ohne
img_recolorimlocalStyles, weil die RimRidge-Icons reine Alpha-Masken sind. Bei jedem neuen Icon-Widget prüfen. Ebenso: Das Platzhalter-Bitmap im Canvas sollte ungefähr die native Auflösung des Bitmaps haben, das zur Laufzeit eingesetzt wird --zoomskaliert relativ dazu. - Platzhalter bleibt sichtbar. Widgets, die zur Laufzeit per
lv_img_set_src()befüllt werden, starten mit dem sichtbaren EEZ-Platzhalter. Wenn die C-Logik nur auf Übergänge reagiert (static bool shown), beim ersten Aufruf einmal explizit in den richtigen Anfangszustand zwingen. - Geometrie zur Laufzeit. Grundregel: Position/Größe gehören ins
.eez-project, nicht in C. Bewusste Ausnahme: der "geschobene" Zustand vonrr_nav_pillbei sichtbarer Spur-Anzeige (EEZ kann keine zustandsabhängige Geometrie ausdrücken). Die Ruheposition im Canvas bleibt maßgeblich.
xUIDrawMutex und EEZ-Actions¶
EEZ-Action-Callbacks laufen synchron in lv_timer_handler(), und das ruft
UIFacade::updateHandler() bei gehaltenem xUIDrawMutex auf. Der Mutex
ist nicht rekursiv; ein zweites xSemaphoreTake aus demselben Task läuft
in den Timeout. Jede UIFacade-Methode, die eine Action erreichen kann,
braucht das Muster
bool uiTask = isDrawTask(); if (uiTask || xSemaphoreTake(...)).
LVGL nur unter xUIDrawMutex -- auch im UI-Task selbst¶
- Symptom: endlose Zeilen
lcd_panel: esp_lcd_panel_draw_bitmap(35): start position must be smaller than end positionauf der seriellen Konsole, danach „Task watchdog … async_tcp", laufendUI Task(Core-Dump 2026-10-02 00:22). - Ursache: Zwei Tasks tragen gleichzeitig eine Invalid-Fläche ein (
_lv_inv_area()), übrig bleibt eine aus beiden gemischte mity2 < y1. Daran läuftrefr_area()in LVGL 8.4 in einer Schleife mit negativer Zeilenzahl; jeder Durchlauf ruft den Display-Treiber mit einer ungültigen Fläche. - Der Fast- und der Slow-Block in
UIFacade::updateHandler()liefen ohne Mutex, weil sie im UI-Task laufen. Das schützt aber nur vorlv_timer_handler(), nicht vor dem BLE-Task (Nav-Frames) und demesp_timer-Task (Fahrzustand), die unter dem Mutex zeichnen. Aufgefallen ist es erst mit der TrailBridge-Testfahrt: Nav-Frame und simulierte Geschwindigkeit kommen beide im Sekundentakt. - Regel: jeder LVGL-Aufruf außerhalb von
lv_timer_handler()braucht den Mutex, auch im UI-Task. Der Core-Dump enthält nur Stacks; die Fläche steht insub_areaim Frame vonrefr_area.
Struct-Layouts: time_t ist 8 Byte¶
Auf dieser Toolchain ist time_t 8 Byte, nicht 4 -- relevant für
BCLogger::LogData und alles, was binär geschrieben und in Tools/
gelesen wird. Echte Offsets ermitteln statt zählen: absichtlich falsches
static_assert(offsetof(T, feld) == 999, "x") -- GCC meldet den echten
Wert in "the comparison reduces to ...".
WLAN: Geheimnisse, Hotspot-Speicher, Suchen¶
- Passwörter nur in POST-Bodies und nie in einer Logzeile.
WebInstrund die Konsole schreiben die URL bzw. die Befehlszeile auf die SD-Karte, die der Log-Dienst hochlädt./wifi/addund/wifi/apnehmen deshalb POST-Bodies, undSerialConsole::run()loggtwifi add|apsetohne Argumente. Alles Neue, das ein Geheimnis verarbeitet, braucht dasselbe. - Der Hotspot kostet ~14 KB internen Heap (AP + STA, DNS-Task, Clients). Im Leerlauf
bleiben ~26 KB; mit einem angeschlossenen Handy drückte
/debug/ui/snap(Screenshot über HTTP) das Minimum auf 3,8 KB (2026-10-03). Keine Screenshots über den Hotspot; normale Seiten sind unkritisch. WifiWebserver::cfgMutexnicht halten, währendui.*aufgerufen wird: Der UI-Task nimmt erstxUIDrawMutexund fragt dannwebservernach dem Status; die umgekehrte Reihenfolge verklemmt.- Eine Suche dauert ~10 s, solange BLE sucht (geteiltes Funkmodul), und lässt sich nicht starten, solange ein Verbindungsversuch läuft -- die automatische Verbindung nimmt dann das letzte Ergebnis.
- Android nutzt für den Browser weiter das Mobilfunknetz, wenn das WLAN kein Internet hat und
sich nicht als Captive Portal meldet. DNS + Umleitung des Hotspots (
startCaptiveDns(), Not-found-Handler) machenhttp://192.168.4.1/wifivom Handy aus erreichbar.
Serielle Konsole¶
src/SerialConsole.* hält die Eingabezeile (Prompt, halb getippter Befehl) als letzte
Bildschirmzeile. Zwei Regeln:
- Serial-Ausgaben aus anderen Tasks gehen über
bclogoder werden inSerialConsole::Output out(console);eingeschlossen -- sonst landen sie mitten in der Eingabezeile. Befehls-Callbacks laufen im Loop-Task ohne sichtbaren Prompt und dürfen direktSerial.printbenutzen, sollten aber mit Zeilenumbruch enden. - Keine ANSI-Escape-Sequenzen und kein
\aausgeben. Der Standardfilter von miniterm undpio device monitorzeigt ESC und die meisten Steuerzeichen als Symbol (␛[K). Die Konsole zeichnet deshalb nur mit\r,\bund Leerzeichen.
Befehle mit console.addCmd() registrieren, nicht mit cli.addCmd(), sonst gibt
es keine Tab-Completion (SimpleCLI bietet keine Liste der Befehle an).
Webserver: Regex-Routen und Stack-Größe¶
ASYNCWEBSERVER_REGEX bleibt bewusst aus: AsyncCallbackWebHandler::canHandle()
baut für jede Regex-Route bei jedem Request ein std::regex auf dem
internen Heap. Routen als Präfix-Routen anlegen (siehe
src/WifiWebserver.cpp).
CONFIG_ASYNC_TCP_STACK_SIZE ist auf gemessene Auslastung plus Reserve
eingestellt (Details im Kommentar in platformio.ini). Nach Änderungen an
Web-Handlern oder OTA neu messen (mem auf der Serial-CLI), bevor der
Wert weiter gesenkt wird.
Interner Heap ist das knappe Gut¶
PSRAM ist reichlich da, aber alles unter CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL
(4 KB) sowie Task-Stacks, FreeRTOS-Queues/-Puffer, offene SD-Dateien (~4 KB
je Datei), WLAN, BLE-Verbindungen und jede TCP-Verbindung des Webservers
kommen aus dem internen Heap. Gemessen 2026-09-26 (mem, Zeile MEM int=):
nach dem Boot mit WLAN + TrailBridge + HR ca. 50–64 KB frei; 8 parallele
HTTP-Requests ziehen davon ~45–60 KB ab. Läuft der interne Heap leer,
hängen Webserver/mDNS, und das Gerät kann abstürzen.
Regel: Jeden neuen dauerhaften Puffer/Stack/offenen File im internen RAM mit
mem vorher/nachher messen (Leerlauf und unter Web-Last). Dateien, die selten
geschrieben werden, nicht die ganze Sitzung offen halten. Die Middleware in
WifiWebserver.cpp lehnt Requests unter 20 KB freiem internem Heap mit 503
ab -- das schützt aber erst nach dem Annehmen der Verbindung.
SD-Karte: offene Dateien nicht löschen oder umbenennen¶
CONFIG_FATFS_FS_LOCK ist 0: FATFS verhindert nicht, dass eine Datei gelöscht
oder umbenannt wird, die ein anderer Task noch offen hat -- das Ergebnis ist ein
beschädigtes Dateisystem, kein Fehlercode. Die Dateien der laufenden Sitzung in
/BIKECOMP/CUR/ sind die ganze Fahrt offen. BCLogger::deleteFile() und der
Cleanup lassen sie deshalb aus (isActiveSessionFile()), ebenso alles in CUR/,
solange der Finalizer (LogSessions) läuft. Neue Wege, Dateien zu löschen oder
zu verschieben, brauchen dieselbe Prüfung.
I²C: Wire nur unter I2CBus::Guard¶
Touch-Controller (UI-Task), BME280 (esp_timer-Task und der Task, der die
Radumdrehungen liefert) und BMI160 (ImuTask) teilen sich Wire. Die eigene Sperre von
TwoWire deckt nur die Übertragung auf dem Bus ab; der Empfangspuffer wird gelesen,
nachdem sie freigegeben ist -- von available()/read() und vom Rückgabewert von
requestFrom(). Startet ein anderer Task dazwischen eine Übertragung, bekommt der
erste die Bytes des anderen Geräts oder eine falsche Länge. Steht der Lese-Index erst
einmal hinter der Länge, bleibt available() wahr, während read() nichts liefert:
while (Wire.available()) Wire.read(); in der SparkFun-BME280-Bibliothek endet dann
nie. Im esp_timer-Task war das ein Task-Watchdog-Reset (Core-Dump vom 2026-10-02), im
ImuTask zeigt es sich als „short read"-Fehler.
Regel: einen I2CBus::Guard (src/I2CBus.h) vom Beginn einer Übertragung halten, bis
ihre Bytes aus Wire abgeholt sind; Bibliotheksaufrufe als Ganzes einschließen. Der
Touch-Callback aus TRGBArduinoSupport bekommt die Sperre über I2CBus::guardTouch().
Ein neues I²C-Gerät oder ein neuer Bibliotheksaufruf braucht dasselbe.
BMI160: Register-Reads nie ungeprüft¶
BMI160Gen::serial_buffer_transfer() prüft nicht, ob requestFrom() alle
Bytes geliefert hat, und lässt bei einem kurzen Read alte Pufferbytes stehen.
Beim FIFO-Füllstand (2 Byte) ergab das sporadisch Werte von ~80 statt ~2
Frames; der leere FIFO liefert dann 0x8000-Frames (-16 g auf allen Achsen),
die als 31-g-Stöße erkannt wurden. Regel: FIFO-Zähler und -Daten über
I2CSensors::imuRead() lesen (Repeated Start, Längenprüfung) und
0x8000/0x8000/0x8000-Frames verwerfen -- beides ist dort umgesetzt, die
Zähler stehen auf /debug/imu.
Abstürze ohne USB: Neustart-Grund und Core-Dump¶
- Beim Boot steht im Debug-Log
Reset reason: …. Bei PANIC oder einem Watchdog liegt ein Core-Dump in der Partitioncoredump. Jeder Absturz überschreibt den vorigen. - Anzeige unter
/debug/coredump: Task, PC, Backtrace und ob der Dump zur laufenden Firmware passt. - Der Dump wird nur auf Anfrage gelesen, nie beim Boot. Ein früherer
Kopierversuch beim Boot hat eine Endlosschleife aus Abstürzen ausgelöst
(TODO in
BCLogger::setup()). Ohne USB wäre das Gerät dann nicht mehr erreichbar. - Auflösen geht nur mit der ELF genau der abgestürzten Firmware. Vor jedem neuen Build
.pio/build/trgb-esp32-s3/firmware.elfsichern, wenn noch ein Dump aussteht. Ob die Datei passt, zeigen die ersten Zeichen vonsha256sum firmware.elf. Befehl:curl -o coredump.bin http://<ip>/debug/coredump.elf esp-coredump info_corefile -t raw \ --gdb ~/.platformio/packages/tool-xtensa-esp-elf-gdb/bin/xtensa-esp32s3-elf-gdb \ -c coredump.bin firmware.elf-t raw, weil die Partition vor der ELF noch einen Kopf hat. - Bei „Task watchdog … IDLE0“ ist der Task mit dem Absturz meist nur der, der gerade lief.
Wichtig ist die Thread-Liste: Wer steht nicht in einer Wartefunktion
(
0x400559e0 in ??= blockiert)?
Nie vTaskDelay(0) in einer Task-Schleife mit hoher Priorität¶
vTaskDelay(0)gibt nur an Tasks gleicher oder höherer Priorität ab, nie an IDLE0.- Der UI-Task (Priorität 20) hat mit
vTaskDelay(next_ms)so lange gerechnet, wie LVGL0zurückgab (Rendering kam nicht hinterher, viele Updates nach BLE-Reconnects). Nach 5 s hat der Task-Watchdog das Gerät neu gestartet (Core-Dump 2026-09-27). - Jetzt gilt ein Minimum von 2 ms in
UIFacade::updateHandler(). Dasselbe gilt für jede eigene Schleife: immer mindestens 1 Tick warten.