From 288455195dfbebbcff490bb98a0100b01008c73f Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 16:12:58 -0600 Subject: [PATCH] MicroPython: LVGL asserts cost 44 KB less flash The assert handler from #13 passed the full source path, the line and the function name, and each of LVGL's ~1,230 assert sites tested the weak handler before calling it. On an ESP32-S3 that added 50.8 KB to the firmware (23 KB of code, 20 KB of function names, 6 KB of paths). With CONFIG_SPIRAM_XIP_FROM_PSRAM, where ESP-IDF copies code and constants into PSRAM in 64 KB pages, it pushed both over a page boundary and took 128 KB from the heap. Now each site passes the file's base name (__FILE_NAME__, or __FILE__ on a compiler without it) and the line to a noreturn handler: a load, a move and a call. The message is "LVGL assert failed at lv_display.c:614". Built for ESP32_GENERIC_S3, the growth over v9.5.53 is 6.3 KB and stays within the same pages. The smoke test triggers an assert (set_tile_cnt(256)) on MicroPython, checks the message and that LVGL keeps working; it fails against the previous handler. --- lv_conf.h | 21 ++++++++++----- ports/micropython/src/lv_assert_micropython.c | 15 ++++++----- tools/test_lvgl_smoke.py | 27 +++++++++++++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/lv_conf.h b/lv_conf.h index 2c760f5..fd2c73b 100644 --- a/lv_conf.h +++ b/lv_conf.h @@ -305,15 +305,24 @@ #else /* On MicroPython a spinning assert locks the scheduler out: USB serial and * WebREPL go silent until a power cycle. The MicroPython port's handler - * raises RuntimeError naming the function, file and line instead, which - * the nearest Python frame or LVGL callback reports, and the board stays - * reachable. It is weak here, so a build without it still links and - * halts as before. */ + * raises RuntimeError naming the file and line instead, which the nearest + * Python frame or LVGL callback reports, and the board stays reachable. + * + * LVGL has over a thousand assert sites, so each one is kept to a call + * with two arguments: the file's base name and the line, which are + * enough to find the assert. The handler doesn't return, so nothing + * follows the call. Passing the full path and the function name as well + * cost about 50 KB of flash on an ESP32-S3, and with code run from PSRAM + * (XIP) it took 128 KB of PSRAM from the heap. */ #define LV_ASSERT_HANDLER_INCLUDE #if !defined(PYCPARSER) && !defined(__ASSEMBLY__) - __attribute__((weak)) void lv_micropython_assert_failed(const char *file, int line, const char *func); + __attribute__((noreturn)) void lv_micropython_assert_failed(const char *file, int line); + #endif + #ifdef __FILE_NAME__ + #define LV_ASSERT_HANDLER lv_micropython_assert_failed(__FILE_NAME__, __LINE__); + #else + #define LV_ASSERT_HANDLER lv_micropython_assert_failed(__FILE__, __LINE__); #endif - #define LV_ASSERT_HANDLER { if (lv_micropython_assert_failed) { lv_micropython_assert_failed(__FILE__, __LINE__, __func__); } while(1); } #endif /*------------- diff --git a/ports/micropython/src/lv_assert_micropython.c b/ports/micropython/src/lv_assert_micropython.c index 2245aa2..ae56e6f 100644 --- a/ports/micropython/src/lv_assert_micropython.c +++ b/ports/micropython/src/lv_assert_micropython.c @@ -6,9 +6,9 @@ * to spin forever, and on a board that locks the MicroPython scheduler out: * USB serial and WebREPL go silent and only a power cycle brings it back. * - * Instead this raises RuntimeError("LVGL assert failed in - * (:)"). LVGL's log is off in MicroPython builds, so the - * function and line are how to find the assertion's message in LVGL's source. + * Instead this raises RuntimeError("LVGL assert failed at :"). + * LVGL's log is off in MicroPython builds, so the file and line are how to + * find the assertion's message in LVGL's source. * * Where the exception lands decides what LVGL state needs repair: * @@ -33,7 +33,7 @@ #include "lvgl/src/display/lv_display_private.h" -void lv_micropython_assert_failed(const char *file, int line, const char *func); +MP_NORETURN void lv_micropython_assert_failed(const char *file, int line); // From the generated binding: how many Python callbacks LVGL is inside. int mp_lv_nesting_depth(void); @@ -48,8 +48,9 @@ static void lv_micropython_end_pass(void) { } } -void lv_micropython_assert_failed(const char *file, int line, const char *func) { - // Keep only the file's own name. A loop, not strrchr: some ports' +void lv_micropython_assert_failed(const char *file, int line) { + // Keep only the file's own name, for a compiler without __FILE_NAME__ + // (lv_conf.h passes __FILE__ there). A loop, not strrchr: some ports' // C libraries (stm32's) don't provide it. const char *base = file; for (const char *p = file; *p; p++) { @@ -60,5 +61,5 @@ void lv_micropython_assert_failed(const char *file, int line, const char *func) if (mp_lv_nesting_depth() == 0) { lv_micropython_end_pass(); } - mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("LVGL assert failed in %s (%s:%d)"), func, base, line); + mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("LVGL assert failed at %s:%d"), base, line); } diff --git a/tools/test_lvgl_smoke.py b/tools/test_lvgl_smoke.py index 5792168..77a4a30 100755 --- a/tools/test_lvgl_smoke.py +++ b/tools/test_lvgl_smoke.py @@ -739,6 +739,32 @@ def test_object_lifetime_and_pointer_validation(lv): print("OK: deleted-object and pointer validation") +def test_assert_raises(lv, disp): + # On MicroPython a failed LVGL assert raises RuntimeError naming the + # source file and line, and LVGL keeps working. CircuitPython reboots + # into safe mode and CPython halts, so only MicroPython can check it. + if _runtime_target() != "micropython": + return + try: + disp.set_tile_cnt(256) # LVGL asserts tile_cnt < 256 + except RuntimeError as exc: + msg = str(exc) + else: + _fail("set_tile_cnt(256) returned instead of raising") + prefix = "LVGL assert failed at lv_display.c:" + if not msg.startswith(prefix) or not msg[len(prefix):].isdigit(): + _fail("unexpected assert message: {}".format(msg)) + if disp.get_tile_cnt() >= 256: + _fail("the asserting call changed the display") + label = lv.label(lv.screen_active()) + label.set_text("after assert") + lv.refr_now(disp) + if label.get_text() != "after assert": + _fail("LVGL stopped working after an assert") + label.delete() + print("OK: an LVGL assert raises RuntimeError at file:line and LVGL carries on") + + def test_target_exceptions(lv): # LV_USE_TJPGD is 0 on every target: the JPEG decoder is jpegio's, # registered via lv_image_decoder_create by displayif, the CircuitPython @@ -773,6 +799,7 @@ def main(): test_refr_now(lv, disp) test_pointer_buffer_dereference(lv, disp) test_widget(lv) + test_assert_raises(lv, disp) test_struct_fields_and_arrays(lv) test_line_points_list_and_buffer(lv) test_struct_array_fields(lv)