Skip to content

Repository files navigation

cmods

An optional multi-module workspace layout and build helper for MicroPython and CircuitPython.

cmods makes it easy to build custom firmware containing multiple user C modules side-by-side (such as displayif, lvgl-micropython, and pygraphics):

  • MicroPython external C modules (USER_C_MODULES, micropython.mk / micropython.cmake)
  • CircuitPython native extensions (shared-bindings / shared-module, via each repo's apply_cp_patches.sh)

Note for Experienced Developers: cmods is completely optional. If you already have an established build habit (such as standard make USER_C_MODULES=... or custom CMake workflows), you can continue building MicroPython and CircuitPython exactly as you always have. This workspace is provided as a friction-free aggregator to make multi-module firmware development faster and more convenient.

🚀 Workspace setup

1. Get the tooling

Option A — clone this repo (recommended when starting fresh):

git clone https://github.com/PyDevices/cmods.git
cd cmods

Option B — copy into an existing build workspace (when you already have a directory with MicroPython / CircuitPython / usermods):

Copy the contents of this repo into that workspace root (the directory that should contain build_mp.sh, manifest-*.py, and optionally patches/). Do not nest a second cmods/ folder unless you intend that to be the workspace root.

2. Add the repos you need

Either clone them into the workspace, or clone them as siblings and symlink:

# Into the workspace
git clone https://github.com/micropython/micropython.git micropython
cd micropython && git submodule update --init --recursive && cd ..

# Or as siblings + symlink (example layout: ../micropython next to the workspace)
ln -s ../micropython micropython

Repeat for each usermod or runtime you want (displayif, pygraphics, lvgl-micropython, circuitpython, …). Each MicroPython usermod must be an immediate subdirectory of the workspace (clone or symlink) and provide a micropython.mk there (optional manifest.py for frozen Python).

Patches (optional; naming convention)

patches/ is optional. When present, build_mp.sh applies every file whose name contains micropython-<port> for the selected --port (e.g. micropython-unix, micropython-windows). This workspace currently ships two such patches (windows networking/SSL, unix scheduler depth). Other ports find no matches and skip. Details: patches/README.md.

Quick build (after setup)

# Optional — only for LVGL
git clone https://github.com/PyDevices/lvgl-micropython.git lvgl-micropython
git clone https://github.com/PyDevices/lvgl-bindings.git lvgl-bindings
cd lvgl-bindings && git submodule update --init lvgl && cd ..
./lvgl-bindings/regenerate_lvmp.sh

./build_mp.sh --port unix --variant standard

The LVGL clone and regenerate_lvmp.sh steps are optional — use them only when building with LVGL. For other user C modules, add those repos (or symlinks) instead.

How it works

  • USER_C_MODULES=$(pwd)MicroPython discovers */micropython.mk in immediate subdirectories
  • manifest-micropython.py — frozen Python from cmod sibling repos, then includes the MicroPython upstream freeze via FROZEN_MANIFEST_UPSTREAM
  • manifest-circuitpython.py — same aggregator shape for CircuitPython (build_cp.sh)
  • build_mp.sh — sets FROZEN_MANIFEST_UPSTREAM to the freeze file MicroPython would use for the selected port/board/variant (same as a manual make without override)
  • build_cp.sh — auto-discovers */apply_cp_patches.sh (optional extensions) and uses manifest-circuitpython.py for all ports
  • micropython.cmake — aggregates */micropython.cmake for CMake ports (ESP32, RP2)

Build scripts

Script Role
build_mp.sh Any MicroPython port (interactive or --port / --board / --variant)
build_cp.sh CircuitPython ports (interactive or --port / --board / --variant)
build_runtimes.sh Desktop/wasm interpreters for local pydevices-examples work — see below

Examples:

./build_mp.sh                                          # interactive
./build_mp.sh --port unix --variant standard
./build_mp.sh --port rp2 --board RPI_PICO2_W
./build_mp.sh --port esp32 --board ESP32_GENERIC_P4 --variant C6_WIFI
./build_cp.sh                                          # interactive
./build_cp.sh --port unix --variant coverage
./build_runtimes.sh --only mp-unix,cp-unix

build_runtimes.sh builds the host interpreters used by PyDevices and installs them under workspace bin/ (micropython, micropython.exe, circuitpython, and the wasm micropython.{mjs,wasm} pair). Targets are mp-unix, mp-windows, mp-wasm, and cp-unix. When the pydevices core repo sits as a sibling of this workspace, the script copies the built binaries directly into pydevices/bin/ to publish them. Use --only to build a subset, or --install-only to refresh installs from an existing build. Re-run after changing usermods (or frozen manifests) that link into these binaries.

Desktop SDL (usdl2): when displayif is present, MicroPython unix / windows and CircuitPython unix link native import usdl2 from that repo (not a separate usdl2 usermod). Unix needs libsdl2-dev. Windows needs an unpacked SDL2 MinGW development ZIP under the workspace (e.g. SDL2-2.30.10/); build_mp.sh auto-sets SDL2_DEV or you can export it (see displayif tools/sdl2_dev_env.sh).

🎨 Hardware example: ESP32-P4 display + touch

End-to-end bring-up for the Waveshare ESP32-P4-WIFI6-Touch-LCD-4B (4″ 720×720 ST7703 on MIPI DSI, GT911 on I2C) using displayif + pydevices-examples. This is not stock MicroPython — firmware must include the displayif mipidsi cmod.

Board configs (pydevices):

Runtime Path
MicroPython board_configs/fbdisplay/esp32-p4-wifi6-touch-lcd-4b
CircuitPython board_configs/cp/fbdisplay/esp32-p4-wifi6-touch-lcd-4b
git clone https://github.com/PyDevices/displayif.git displayif

# C6_WIFI — this board’s ESP32-C6 WiFi/BLE coprocessor (use C5_WIFI if yours is C5)
./build_mp.sh --port esp32 --board ESP32_GENERIC_P4 --variant C6_WIFI

build_mp.sh can flash when the build finishes (offset from board.json, 0x2000 for ESP32_GENERIC_P4). Manual flash:

esptool -b 460800 --before default_reset --after hard_reset \
  write_flash 0x2000 micropython/ports/esp32/build-ESP32_GENERIC_P4/firmware.bin

The cmods build discovers pydevices/manifest.py and freezes the core product packages. Install the matching board config (and optional examples from pydevices-examples) with mpremote:

mpremote mip install --target "." \
  "github:PyDevices/pydevices/board_configs/fbdisplay/esp32-p4-wifi6-touch-lcd-4b"
# optional: mpremote mip install --target "./examples" "github:PyDevices/pydevices-examples/packages/examples.json"

Smoke checks:

mpremote run displayif/tools/test_mipidsi_smoke.py
import board_config
import eventsys

display_drv = board_config.display_drv
runtime = eventsys.Runtime.from_board_config(board_config)
display_drv.fill_rect(0, 0, 200, 200, 0xF800)
display_drv.show()

# Touch: poll until quit
while not runtime.quit_requested:
    for e in runtime.poll():
        print(e)

Pinout matches the Waveshare BSP (reset 27, backlight 26, I2C 7/8, GT911 @ 0x5D). If the panel stays black, check backlight polarity (backlight_on_high=False in the board config) and the displayif P4 DSI LDO path (channel 3 @ 2.5 V). Validate display/touch over USB serial before WiFi.

Related repos

Repo Role
lvgl-micropython LVGL MicroPython glue
lvgl-bindings LVGL binding generator
lvgl-circuitpython LVGL CircuitPython glue (separate workflow)

CircuitPython does not use USER_C_MODULES. Clone lvgl-circuitpython into this workspace if you want CP and MP trees side by side.

CircuitPython (optional extensions; see lvgl-circuitpython README for CP clone setup):

Native CP modules here follow Adafruit’s Extending CircuitPython architecture (shared-bindings / shared-module / CIRCUITPY_*), but stay out-of-tree: each extension keeps spikes in its own repo and apply_cp_patches.sh copies them into a local (uncommitted) CircuitPython tree. Adafruit’s Learn guide assumes in-tree edits; there is no official out-of-tree C-module path. Per-repo READMEs map Learn steps → spikes/patches.

./build_cp.sh --port unix --variant standard

build_cp.sh runs every sibling */apply_cp_patches.sh when present (pygraphics, LVGL, …). Clone only the extensions you need. Optional: place a user_post_mpconfigport.mk at the workspace root (CircuitPython’s user-config hook; build_cp.sh passes -I when it exists) to freeze Adafruit asyncio/ticks for multimer.AsyncTimer. See CircuitPython building.md and multimer building docs.

MicroPython frozen asyncio (required for multimer.AsyncTimer on unix/windows):

# pydevices/manifest.py is discovered automatically.
# Add only personal extras to the optional manifest-user.py.
./build_mp.sh --port unix --variant standard
./build_mp.sh --port windows --variant dev

Direct build (without this tooling)

Create any workspace directory, clone micropython and the usermods you need as siblings (or symlink them), and build from micropython/ with USER_C_MODULES pointing at the workspace root. See each usermod’s README (e.g. lvgl-micropython).

About

Build workspace and tooling for MicroPython, CircuitPython, and native PyDevices modules.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Contributors

Languages