Summary
The ADR-045 on-device LVGL display draws its initial frame at boot and then never repaints. Live data (vitals / CSI / activity / clock) is written into the LVGL object tree every loop, but it never reaches the panel — the screen is effectively static after the first frame.
Root cause: LVGL's tick never advances, so lv_timer_handler()'s display-refresh timer (LV_DISP_DEF_REFR_PERIOD = 30 ms) is never "due" and no flush is ever performed.
Root cause detail
main/lv_conf.h sets LV_TICK_CUSTOM 1 with esp_timer_get_time() — which would be headless-safe — but the managed lvgl/lvgl component is Kconfig-configured (LV_CONF_SKIP via lv_conf_internal.h), so lv_conf.h is ignored.
- The generated
sdkconfig contains # CONFIG_LV_TICK_CUSTOM is not set.
- Nothing in
firmware/esp32-csi-node/main/ ever calls lv_tick_inc() (grep-confirmed).
- Net effect:
lv_tick is stuck → lv_timer_handler() returns with no timer ready → the display refresh callback never runs → the panel is never updated after the first paint.
This is the same Kconfig-vs-lv_conf.h trap that also silently drops lv_conf.h font enables (fonts must be set via CONFIG_LV_FONT_MONTSERRAT_*).
Impact
Affects all ADR-045 display builds, independent of panel — the SH8601/RM67162 QSPI AMOLED HAL included. The on-device UI appears to "work" only in the sense that the first frame renders; none of the live readouts ever update.
How it was isolated
A backlight "breathe" beacon (LEDC PWM driven straight from the display task loop, bypassing LVGL and SPI entirely) kept pulsing while the on-screen content stayed frozen. That cleanly separates the layers:
| Backlight (LEDC) |
On-screen content |
Conclusion |
| pulsing |
frozen |
task loop alive; LVGL refresh dead → tick |
(Verified on a Waveshare ESP32-S3-Touch-LCD-1.69 / ST7789V2, but the defect is panel-independent.)
Fix
Register an esp_timer that drives the tick, in display_task_start() after lv_init():
static void lvgl_tick_cb(void *arg) { lv_tick_inc(2); }
...
const esp_timer_create_args_t tick_args = { .callback = &lvgl_tick_cb, .name = "lvgl_tick" };
esp_timer_handle_t t = NULL;
esp_timer_create(&tick_args, &t);
esp_timer_start_periodic(t, 2000); /* 2 ms — host-independent, works headless */
After this, lv_timer_handler() refreshes every 30 ms and the panel updates live (heartbeat blinks, charts/bars track data) on USB and on a bare power supply.
Related (same PR)
- No ST7789 SPI LCD HAL —
display_hal.c only implements the SH8601/RM67162 QSPI AMOLED path. Added display_hal_st7789.c (ESP-IDF built-in esp_lcd_new_panel_st7789 + CST816 touch + LEDC backlight) selected via a new DISPLAY_PANEL Kconfig choice, plus a compact 240×280 UI (display_ui_st7789.c) since the 4-view AMOLED UI is laid out for 368×448.
- Deployment note: the LCD board boot-loops from a startup-inrush brownout on a single host-USB port (full-screen draws + WiFi connect at once). Mitigated by avoiding full-white init frames + a dimmable backlight; a ≥2 A supply resolves it fully.
Environment
- ESP-IDF v5.4 (Docker
espressif/idf:release-v5.4), target esp32s3
lvgl/lvgl ~8.3 (managed component), LV_DISP_DEF_REFR_PERIOD=30
- firmware
esp32-csi-node v0.7.0, ADR-045
PR with the tick fix + ST7789 HAL incoming.
Summary
The ADR-045 on-device LVGL display draws its initial frame at boot and then never repaints. Live data (vitals / CSI / activity / clock) is written into the LVGL object tree every loop, but it never reaches the panel — the screen is effectively static after the first frame.
Root cause: LVGL's tick never advances, so
lv_timer_handler()'s display-refresh timer (LV_DISP_DEF_REFR_PERIOD= 30 ms) is never "due" and no flush is ever performed.Root cause detail
main/lv_conf.hsetsLV_TICK_CUSTOM 1withesp_timer_get_time()— which would be headless-safe — but the managedlvgl/lvglcomponent is Kconfig-configured (LV_CONF_SKIPvialv_conf_internal.h), solv_conf.his ignored.sdkconfigcontains# CONFIG_LV_TICK_CUSTOM is not set.firmware/esp32-csi-node/main/ever callslv_tick_inc()(grep-confirmed).lv_tickis stuck →lv_timer_handler()returns with no timer ready → the display refresh callback never runs → the panel is never updated after the first paint.This is the same Kconfig-vs-
lv_conf.htrap that also silently dropslv_conf.hfont enables (fonts must be set viaCONFIG_LV_FONT_MONTSERRAT_*).Impact
Affects all ADR-045 display builds, independent of panel — the SH8601/RM67162 QSPI AMOLED HAL included. The on-device UI appears to "work" only in the sense that the first frame renders; none of the live readouts ever update.
How it was isolated
A backlight "breathe" beacon (LEDC PWM driven straight from the display task loop, bypassing LVGL and SPI entirely) kept pulsing while the on-screen content stayed frozen. That cleanly separates the layers:
(Verified on a Waveshare ESP32-S3-Touch-LCD-1.69 / ST7789V2, but the defect is panel-independent.)
Fix
Register an
esp_timerthat drives the tick, indisplay_task_start()afterlv_init():After this,
lv_timer_handler()refreshes every 30 ms and the panel updates live (heartbeat blinks, charts/bars track data) on USB and on a bare power supply.Related (same PR)
display_hal.conly implements the SH8601/RM67162 QSPI AMOLED path. Addeddisplay_hal_st7789.c(ESP-IDF built-inesp_lcd_new_panel_st7789+ CST816 touch + LEDC backlight) selected via a newDISPLAY_PANELKconfig choice, plus a compact 240×280 UI (display_ui_st7789.c) since the 4-view AMOLED UI is laid out for 368×448.Environment
espressif/idf:release-v5.4), targetesp32s3lvgl/lvgl~8.3 (managed component),LV_DISP_DEF_REFR_PERIOD=30esp32-csi-nodev0.7.0, ADR-045PR with the tick fix + ST7789 HAL incoming.