Skip to content
This repository was archived by the owner on Oct 30, 2026. It is now read-only.

Build as a CircuitPython 11 user C module; survive a reload; never spin a board off USB - #19

Merged
bdbarnett merged 5 commits into
mainfrom
cp-phase2-usermod
Oct 8, 2026
Merged

bdbarnett merged 5 commits into
mainfrom
cp-phase2-usermod

Conversation

@bdbarnett

@bdbarnett bdbarnett commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

CircuitPython 11 builds user C modules the way MicroPython does, so this repository can now be handed to any port's make as USER_C_MODULES, with no patches to CircuitPython's tree (micropython.mk). The patch-script route for 10.x still works.

  • LVGL goes in SRC_USERMOD_LIB_C, compiled and linked but never scanned for qstrs; the module, bindings, allocator, JPEG shim and assert handler in SRC_USERMOD_C.
  • The board ports' late -Werror flags are turned off per object, for this module's objects only.
  • The build stops early, with the fix in the message, when CIRCUITPY_GIFIO is 1 (gifio's AnimatedGIF collides with LVGL's).
  • Without a sibling lvgl-bindings/ the pinned commit is fetched into .deps/ (scripts/fetch_bindings.sh).
  • -DMICROPY_QSTR_OFFSET_BYTES=4: CircuitPython 11.0 caps a qstr pool at 64 KB of names and LVGL brings about 53 KB. The setting needs a CircuitPython that has it (a patch, for now); elsewhere it is ignored.

Two fixes that matter on a board:

  • A reload starts a new VM without clearing root pointers, so LVGL's state pointed into the previous VM's heap: lv.is_initialized() read garbage and the first display_create asked for 7.6 MB. The module now answers __init__ by delegation, drops that state, and puts itself in sys.modules, so the hook runs once per VM rather than on every import lvgl (built-ins aren't kept there otherwise, and a second import used to drop live state).
  • Out of memory used to raise MemoryError through LVGL's C, leaving a refresh half done for the next pass to spin on, and a board spinning in C drops off USB until someone presses reset. The allocator now returns NULL, and LVGL's assertion calls lv_circuitpython_assert_failed(): safe mode on a board, abort() on unix. On unix, filling the heap and creating labels spun until killed before and aborts now.

Checked: unix (coverage) renders and the pixels read back exact. On a Feather RP2040 + 3.5" TFT FeatherWing with this as its only user C module, a full LVGL screen through pydevices' display driver runs from code.py at boot (first 480x320 frame in 10 flushes, about 0.4 s, 50 KB of heap left), and a 1/8-scale capture of the frame sent to the panel matches the unix render sample for sample, outside the machine-name line and the animated spinner. import lvgl a second time after LVGL is running no longer crashes.

Pins lvgl-bindings 2858c42 (PyDevices/lvgl-bindings#29).

micropython.mk lets CircuitPython 11's USER_C_MODULES build LVGL and the
generated bindings with no patches to CircuitPython's tree: LVGL in
SRC_USERMOD_LIB_C (never scanned for qstrs), the module, bindings and
allocator in SRC_USERMOD_C, the board ports' late -Werror flags turned off
per object, a stop when gifio is on (its AnimatedGIF collides with LVGL's),
and four-byte qstr offsets where CircuitPython has them. Without a sibling
lvgl-bindings checkout the pinned commit is fetched into .deps/.

A reload starts a new VM without clearing root pointers, so LVGL's state
pointed into the previous VM's heap and the first display_create asked for
megabytes. The module now answers __init__, which runs on its first import
in each VM, and drops that state.

Pins lvgl-bindings at the CircuitPython 11 integer fix (PR branch).
LVGL's allocator raised MemoryError when the heap was full, a longjmp out of
LVGL's C that left a refresh half done; the next pass spun on it, and a
board spinning in C drops its drive and serial port until someone presses
reset. The allocator now returns NULL (m_malloc_maybe, m_realloc_maybe), and
lv_assert_circuitpython.c is LVGL's assert handler on CircuitPython
(lvgl-bindings' lv_conf.h names it): safe mode on a board, abort() on unix.

Pins lvgl-bindings at the commit that names the handler (PR branch).
Built-in modules stay out of sys.modules, so the import system ran the
reload hook on every `import lvgl` and dropped live LVGL state: a second
import after display_driver had set LVGL up segfaulted the unix build in
lv_display_get_default(). The hook now puts lvgl in sys.modules, which a
later import in the same VM finds first and a new VM starts without.
@bdbarnett
bdbarnett merged commit f444919 into main Oct 8, 2026
@bdbarnett
bdbarnett deleted the cp-phase2-usermod branch October 8, 2026 17:13
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant