Skip to content

Back to Home from System restarts a 264 KB RP2040: the heap is too fragmented after System #49

Closed

Description

@bdbarnett

On a Raspberry Pi RP2040 board (Waveshare RP2040-Touch-LCD-1.28, 240x240 GC9A01, MicroPython 1.30 preview, about 223 KB of heap), going back to Home from System restarts the board instead of switching. The launcher's heap check is what restarts it, so nothing crashes, but every return to Home costs a full boot.

To reproduce: put appkit, touchkit and the Home and System apps on the board as .mpy files, boot with appkit.run(), open System from Home's gear, then press Home.

Measured on that board after gc.collect():

State Used Free Largest free block
Launcher baseline, first app start 127,472 B 113,120 B
Home open 115,264 B 112,704 B about 86 KB
System open 144,480 B 83,488 B about 22.6 KB

The launcher's thresholds come from that baseline: min_free 63,736 B and min_block 28,280 B, a quarter of the largest block. With System open, the biggest free block is already under 28 KB. Free memory is plentiful; it's the fragmentation that fails the check. LVGL's two draw buffers are 11,520 B each, so they aren't what holds the heap. Reading an AppScope's worker property while System is open also fails, with MemoryError allocating 24,576 B.

A side effect: System's Memory row reads gc.mem_free() between collections, so it can show "2 KB free, 220 KB used" while a collection would leave 83 KB free. That figure says the heap is full when it isn't.

What's still open is what fragments it. The candidates are the touchkit modules that System imports (touchkit.net, touchkit.settings and touchkit.shell load while System opens, and they stay after it closes), long-lived objects allocated in between System's LVGL widgets, and the code of the .mpy modules loaded into the heap. Possible fixes: import what System needs up front, before the baseline is measured; base min_block on what an app needs rather than on a quarter of the first measurement; or have the Memory row collect before it reads.

Activity

  1. bdbarnett commented on Nov 10, 2026

    @bdbarnett
    ContributorAuthor

    Fixed in #51, and the threshold is unchanged.

    The switch was fragmenting the heap itself. The launcher checked the largest free block from inside the calls that delivered the switch. On this board call frames live on the heap, so those frames, the request's timer and its closure had all been allocated while the old app ran, and they split the space the close had just freed. System's close also imported wifi_manager, which then stayed loaded. Now a request uses two timers made with the launcher and closes the old app from the timer. The heap check and the next app's start run on the main loop's next turn, or from a scheduled call when nothing runs the loop. System's close no longer imports anything.

    Measured on the Waveshare RP2040-Touch-LCD-1.28 by tapping through 20 Home-System round trips: no restart. The largest free block after each close stayed at 49-62 KB, and was 38 KB after the first close, while System's modules load. The threshold is 28 KB. Before the change it fell to 13 KB, and the board restarted within ten trips. Free heap after a close holds at 106 KB, so nothing leaks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions