MicroPython firmware¶
The template can generate a MicroPython project: firmware for a microcontroller (ESP32, RP2/Pico, STM32, ...), deployed with mpremote. Unlike a normal Python package, the code runs on the device, so the CPython toolchain (uv/ruff/pytest/basedpyright) is used for the parts that run on your computer:
firmware/— the code that runs on the device.boot.pyruns first, then MicroPython auto-runs/main.py.board_config.pyholds the board-specific pins;core/holds device-independent logic.tests/— CPythonpytesttests forfirmware/core.corenever importsmachine, so it runs under normal CPython.typings/— MicroPython type stubs (micropython-<port>-stubs), installed into a folder rather than the venv so they don't shadow CPython's stdlib during host-side checking.
[!NOTE] The type stubs come from the community josverl/micropython-stubs project — they are not published by the official MicroPython project. They are pinned (
~=) to the same MicroPython release the frozen build targets (micropython_versionincopier.yml→ freeze.py'sDEFAULT_TAG), so stubs and firmware stay in step. The firmware itself is the source of truth; treat the stubs as an editor aid.
Choosing a port¶
| Port | Typical board | Notes |
|---|---|---|
| esp32 | ESP32 DevKitC | Most common; default |
| esp8266 | NodeMCU | Legacy, low memory |
| rp2 | Raspberry Pi Pico / Pico W | RP2040 / RP2350 |
| stm32 | Pyboard | STM32 family |
| samd | Adafruit ItsyBitsy M0 | SAMD21 / SAMD51 |
| unix | your computer | Runs on the host — handy for tests |
| windows | your computer | Windows port |
| mimxrt | Teensy 4.x | NXP i.MX RT |
The micropython-<port>-stubs package describes the port's generic board.
Per-board differences (which GPIO the LED is on, etc.) are concentrated in
firmware/board_config.py, so moving to another board is a one-file edit.
Setting up the dev environment¶
uv sync # ruff / pytest / basedpyright / mpremote / pyrefly|ty
uv run pip install -r requirements-dev.txt --target typings # port stubs
The stubs are installed into typings/ (git-ignored) so basedpyright can
type-check firmware/ against MicroPython while tests/ and
firmware/core/ are checked against CPython. The secondary checker
(type_checker: pyrefly by default, or ty) runs over the CPython-side code
(tests/ + firmware/core/); the firmware's hardware files (main.py,
boot.py, board_config.py) are checked only by the dedicated basedpyright
pass against the stubs.
Running the tests¶
firmware/core/ is written to be importable under CPython, so the logic is
unit-tested with ordinary pytest:
uv run pytest
This runs tests/test_core.py, which exercises the device-independent logic
without any hardware.
Deploying to a device¶
Connect the device over USB and copy firmware/ to it (MicroPython runs
/main.py at boot):
uv run mpremote connect auto fs cp -r firmware/ :
uv run mpremote connect auto reset
or use the generated task-runner commands (task deploy, just deploy, ...).
mpremote connect auto picks the first USB serial device; use
mpremote connect list to see them and pass port:/dev/ttyACM0 (or a0)
to target a specific one.
To iterate on a single file without copying, mpremote run firmware/main.py
executes it from RAM.
Building a distributable firmware image (freeze)¶
The mpremote flow copies firmware/ to a device's filesystem. To produce a
distributable firmware image — a .bin/.uf2/.hex with your code
frozen into the MicroPython firmware — use the freeze task:
task freeze # or: just freeze / make freeze / poe freeze ...
This requires Docker (no cross toolchain needed on the host). It:
- clones a pinned MicroPython source tree into
build/micropython-src/(git-ignored) on first use, - freezes
firmware/into the image viafirmware/manifest.py(FROZEN_MANIFEST), running the port build inside the toolchain container, and - copies the artifact to
dist/, e.g.dist/<repo>-rp2-rpi_pico.uf2.
By default it builds the port you chose when generating the project
(micropython_port). Build a different port or board explicitly:
python tools/micropython/freeze.py --port rp2
python tools/micropython/freeze.py --port esp32 --board ESP32_GENERIC_S3
python tools/micropython/freeze.py --tag vX.Y.Z # pin the MicroPython version
| Port | Toolchain image | Default board | Artifact |
|---|---|---|---|
| esp32 | espressif/idf (several GB first pull) |
ESP32_GENERIC |
firmware.bin |
| esp8266 | larsks/esp-open-sdk |
ESP8266_GENERIC |
firmware.bin |
| rp2 | micropython/build-micropython-arm:bookworm |
RPI_PICO |
firmware.uf2 |
| stm32 | micropython/build-micropython-arm:bookworm |
PYBV11 |
firmware.dfu |
| samd | micropython/build-micropython-arm:bookworm |
ADAFRUIT_ITSYBITSY_M0_EXPRESS |
firmware.uf2 |
| mimxrt | micropython/build-micropython-arm:bookworm |
MIMXRT1020_EVK |
firmware.hex |
| unix | gcc:12-bookworm |
— | micropython (runs on the host) |
| windows | micropython/build-micropython-win-mingw |
— | micropython.exe |
Because the firmware is frozen in, MicroPython runs the frozen /boot.py and
/main.py at startup exactly like files copied to the device — the same
firmware/ tree drives both the mpremote development flow and the frozen
release image. The generated CI builds the firmware for your chosen port on
every push and attaches it to GitHub Releases on tag.
[!NOTE] The esp32 image (
espressif/idf) is several GB and the first build pulls MicroPython's ESP-IDF submodules, so the first esp32 freeze is slow. The rp2/arm image is much lighter — good for validating the setup.
Adding a unix-port CI smoke test¶
The generated CI runs the CPython core tests and type-checks firmware/
against the stubs, but does not run the firmware itself. To also smoke-run
the code on the MicroPython unix port in CI, add a job that builds the unix
port and runs main.py:
unix-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: sudo apt-get install -y build-essential libffi-dev pkg-config
- name: Build the unix port
run: |
git clone --depth 1 https://github.com/micropython/micropython.git
make -C micropython/mpy-cross
make -C micropython/ports/unix
- name: Run main.py on the unix port
run: |
cd firmware
../micropython/ports/unix/build-standard/micropython main.py
Note the unix port has no on-board LED, so board_config.py sets
LED_PIN = None; the sample main.py will raise if it tries to blink on a
port without hardware. Adjust the smoke command to the logic you actually run.