Skip to content

MicroPython: LVGL's memory pools on ESP32, opt-in - #15

Merged
bdbarnett merged 3 commits into
mainfrom
esp32-lvgl-pools
Nov 10, 2026
Merged

bdbarnett merged 3 commits into
mainfrom
esp32-lvgl-pools

Conversation

@bdbarnett

@bdbarnett bdbarnett commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10. ESP32 firmware can now keep LVGL's allocations in pools held in GC blocks, opt-in with LVMP_POOL_TLSF=1 as on the other ports. Until now the ESP32 pool code compiled in only when MICROPY_MALLOC_USES_ALLOCATED_SIZE was 1, which the esp32 port leaves at 0, so it never reached a firmware.

ESP32 now uses the same TLSF pools as the other ports (#6), and the multi_heap branch goes. The allocator's aliases are renamed, since ESP-IDF's own multi_heap.h is already in scope on ESP32 and its type clashed with them. Set LVMP_POOL_TLSF where MicroPython's root-pointer scan sees it too: in the board's mpconfigboard.h, or in CFLAGS_EXTRA on a make port. A definition that reaches only the compiler leaves the pools' root pointer unregistered and the build fails. On the esp32 port, test for the target with CONFIG_IDF_TARGET, not ESP_PLATFORM: the port compiles user C modules a second time without ESP_PLATFORM, and that copy is the one linked.

The first pool is now 32 KB, and each new one doubles up to 256 KB, ten pools (about 2 MB) in all. gc.collect() scans a pool whole, used or not, so one 256 KB pool made every collection pay for room LVGL wasn't using.

I built ESP32_GENERIC_P4 (16 MB, the Waveshare ESP32-P4-WIFI6-DEV-KIT with a 5" DSI panel, about 31 MB of GC heap in PSRAM) from one tree three ways: no pools, one 256 KB pool, and growing pools from 32 KB. Then I ran the heap_layout.py script from #6 on the board, two runs per build (ranges over both):

fresh one hole many holes
50 LVGL labels created and deleted, no pools 14.1-14.3 ms 14.3 ms 42.6-48.8 ms
same, 256 KB pool 13.6 ms 13.6-13.7 ms 13.8 ms
same, 32 KB first pool 13.6 ms 13.6-13.7 ms 13.7-14.2 ms
full refresh, worst of 20, no pools 66.0-66.3 ms 64.7-64.9 ms 64.7-71.2 ms
same, 256 KB pool 63.8-64.4 ms 63.7-63.8 ms 63.8 ms
same, 32 KB first pool 63.7-63.9 ms 63.8 ms 63.7-63.9 ms
gc.collect(), no pools 2.9 ms 68.0-68.5 ms 62.1-62.5 ms
same, 256 KB pool 11.4 ms 73.7-74.1 ms 70.1-70.2 ms
same, 32 KB first pool 6.1 ms 71.3-71.8 ms 65.3-65.4 ms
heap in use after the screen is built 95 KB
same, 256 KB pool / 32 KB first pool 320 / 157 KB

The median refresh was 63.4-64.0 ms in every build and layout: drawing dominates it on this board.

LVGL's own allocations stay flat on a fragmented heap with the pools and get three times slower without them, the same shape as the STM32 runs in #6. The growing pools keep that and halve the collection cost, but a collection on a fresh heap still takes about twice as long (this screen filled the 32 KB pool and opened a 64 KB one). That's too much to impose on every ESP32 build, so the pools stay opt-in: worth turning on for a long-running app that builds and tears down widgets, not by default. The last firmware, from this branch's head with the pools off, boots and runs its apps. I didn't test an ESP32 without PSRAM.

ESP32 builds were meant to keep LVGL's allocations in pools held in GC
blocks, but the pool code compiled in only when
MICROPY_MALLOC_USES_ALLOCATED_SIZE was 1, which the esp32 port leaves at
0, so every LVGL allocation still walked the GC allocation table (#10).

ESP32 now uses the same TLSF pools as other ports, on by default, and
LVMP_POOL_TLSF=0 turns them off. The switch keys on CONFIG_IDF_TARGET,
which mpconfigport.h brings in from sdkconfig.h, not on ESP_PLATFORM: the
esp32 port compiles user C modules a second time without ESP_PLATFORM,
and that copy is the one linked.
gc.collect() scans a pool whole, used or not. With one 256 KB pool, a
collection on a fresh ESP32-P4 heap took 11.4 ms against 2.9 ms without
pools, while LVGL kept about 30 KB in it. Pools now start at 32 KB and
double up to 256 KB, with ten of them (about 2 MB) before allocations go
back to gc_alloc().
Even with a 32 KB first pool, gc.collect() on a fresh ESP32-P4 heap took
6.1 ms with the pools against 2.9 ms without, too much to impose on every
ESP32 build for faster LVGL allocation on a fragmented heap. A build turns
them on with LVMP_POOL_TLSF=1, set where the root-pointer scan sees it
(mpconfigboard.h, or CFLAGS_EXTRA on make ports).
@bdbarnett bdbarnett changed the title MicroPython: LVGL's memory pools on ESP32 MicroPython: LVGL's memory pools on ESP32, opt-in Nov 10, 2026
@bdbarnett
bdbarnett merged commit c34fbee into main Nov 10, 2026
5 checks passed
@bdbarnett
bdbarnett deleted the esp32-lvgl-pools branch November 10, 2026 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

ESP32: LVGL's memory pools never reach the firmware

1 participant