Skip to content

zephyr-cp: freeze .mpy modules named in circuitpython.toml - #23

Open
tyeth wants to merge 1 commit into
zephyr-cp-pico2w-softapfrom
zephyr-cp-frozen-modules
Open

zephyr-cp: freeze .mpy modules named in circuitpython.toml#23
tyeth wants to merge 1 commit into
zephyr-cp-pico2w-softapfrom
zephyr-cp-frozen-modules

Conversation

@tyeth

@tyeth tyeth commented Sep 9, 2026

Copy link
Copy Markdown
Owner

The port had no frozen-module plumbing (the Python builder never runs make; FROZEN_MPY_DIR is a hard error). This reimplements the pipeline in cptools/build_circuitpython.py behind a per-board opt-in:

FROZEN_MPY_DIRS = ["frozen/Adafruit_CircuitPython_BLE"]

Stages the trees with tools/preprocess_frozen_modules.py, adds the frozen -D flags to the qstr pass as well as the compile (.frozen on sys.path is behind MICROPY_MODULE_FROZEN), then after the qstr headers exist runs mpy-cross -s per module and tools/mpy-tool.py -f -q against the same collected qstr list that produced qstrdefs.generated.h, so the frozen pool numbers correctly from MP_QSTRnumber_of. makemanifest.py is bypassed (it hard-codes genhdr/qstrdefs.preprocessed.h, which this builder never produces). pre_zephyr_build_prep.py builds mpy-cross; tools/ci_fetch_deps.py learns the toml key (it had a TODO).

Measured, not estimated (Pico 2 W, gc.mem_alloc() delta after gc.collect()gc.mem_free() is unusable here, the split heap grows on demand and shows a negative delta):

import adafruit_ble + advertising.standard + services.nordic heap used time
frozen (.frozen/adafruit_ble/__init__.py) 10,384 B 0.055 s
.mpy files on CIRCUITPY (/mpylib/...) 21,792 B 0.132 s

Flash cost: Pico W 1,208,636 → 1,236,972 B (+28,336 B for the 20 modules), static RAM unchanged (238,996 B). The Pico W opts in (~42 KB heap; a BLE node cannot otherwise hold adafruit_ble). The Pico 2 W does not: it has the heap, and freezing would pin the library version for everyone — that is the trade-off, stated rather than forced.

Pico W final image with this whole series: 1,242,092 B flash (79.40 %), 238,996 B RAM (88.41 %) — TCP alone is most of the RAM growth from 84.4 %.

🤖 Generated with Claude Code

The port had no frozen-module support: its Python-driven build never runs
make, and the freeze pipeline lives in py/circuitpy_mpconfig.mk. Reimplement
the pipeline in cptools/build_circuitpython.py behind a per-board opt-in,
FROZEN_MPY_DIRS = ["frozen/<lib>", ...] in circuitpython.toml (paths relative
to the repository root, as $(TOP)/... is in mpconfigboard.mk):

- tools/preprocess_frozen_modules.py stages the trees into
  <build>/frozen_mpy (repo directory dropped, __version__ filled in, examples
  and tests left out), before the qstr pass;
- MICROPY_QSTR_EXTRA_POOL / MICROPY_MODULE_FROZEN_MPY are added to the flags
  for the qstr pass as well as the compile -- Q(.frozen) and the sys.path
  entry that uses it are behind MICROPY_MODULE_FROZEN;
- after genhdr/qstrdefs.generated.h and root_pointers.h exist, mpy-cross
  compiles each module (-s with the module path, so mpy-tool derives the
  frozen name from it) and tools/mpy-tool.py -f -q emits frozen_content.c,
  fed the same collected qstr list that produced the generated header so the
  frozen pool numbers from MP_QSTRnumber_of correctly. tools/makemanifest.py
  is bypassed: it insists on genhdr/qstrdefs.preprocessed.h, which this
  builder never produces. Only MPY freezing: MICROPY_MODULE_FROZEN_STR would
  reference the mp_frozen_str_* tables makemanifest emits.
- frozen_content.c is compiled with the no-qstr sources (it defines its own
  MP_QSTR_* enum values and must never go through extraction).

pre_zephyr_build_prep.py builds mpy-cross first when a board freezes modules
(honouring MICROPY_MPYCROSS), and tools/ci_fetch_deps.py learns the toml key
so CI initialises the right frozen/ submodules (it had a TODO for this).

The Pico W opts in with adafruit_ble: it has ~42 KB of heap and a BLE node
cannot otherwise load the library. Measured on a Pico 2 W (same core, same
flags): importing adafruit_ble plus its advertising.standard and
services.nordic modules costs 10,384 B of heap frozen vs 21,792 B from .mpy
files on CIRCUITPY (gc.mem_alloc() delta after gc.collect(); gc.mem_free()
is not usable here, the split heap grows on demand), 0.055 s vs 0.132 s.
Freezing the 20 modules adds 28,336 B of flash and no static RAM on the
Pico W (1,208,636 -> 1,236,972 B with the rest of this series). The Pico 2 W
is not opted in: it has the heap, and a frozen copy would pin the library
version for everyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant