Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

MicroPython for Microcontrollers  Module 13 – Ports and boards: RP2040/RP2350, ESP32 and the rest

MicroPython, CircuitPython and other ports

How MicroPython 1.29 compares with CircuitPython 10.3, which other ports MicroPython runs on and in which support tier, and a thin layer for portable code.

  • Beginner
  • 15 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: MicroPython and CircuitPython share a language and part ways at the hardware · MicroPython runs on other ports too, in four support tiers · A thin hardware layer moves the logic between platforms · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Compare MicroPython with CircuitPython in hardware API, start-up files, editing and concurrency
  • Name MicroPython's other ports and what each support tier promises
  • Write a thin hardware layer so that application logic runs on both platforms unchanged
  • Choose a port and a board for a project from its needs

Before you start

The same Raspberry Pi Pico can run MicroPython 1.29 or CircuitPython: A Python for microcontrollers, based on MicroPython, with its own hardware modules such as board, digitalio and busio, a USB drive for editing, and no interrupts or threads. 10.3.1, and the two look alike until the first line of hardware code: Pin("LED", Pin.OUT) in one, digitalio.DigitalInOut(board.LED) in the other. CircuitPython is based on MicroPython and keeps its language, but changed the hardware API, the way you edit a board and the files that start a program.

This lesson compares the two, maps the other ports MicroPython itself runs on with their support tiers, and builds the thin hardware layer that lets one application run on both.

MicroPython and CircuitPython share a language and part ways at the hardware

CircuitPython’s own README lists how it differs from MicroPython. The differences that change your code:

Area MicroPython 1.29 CircuitPython 10.3.1
Hardware API machine: Pin, I2C, SPI, UART, PWM board pin names with digitalio, busio and others; no machine
Program files boot.py, then main.py, after each reset boot.py once at start-up, then code.py after every reload
Editing the REPL, and tools such as mpremote over USB serial the CIRCUITPY USB drive; saving a file reruns the program
Concurrency Pin.irq(), timer callbacks, asyncio, threads on some ports no interrupts or threads; async and await on some boards
After a crash the next reset runs boot.py and main.py again, whatever caused it safe mode, which leaves your code unrun until you fix it

MicroPython gives you the chip’s features through machine and modules such as rp2 and esp32, and leaves the workflow to tools. CircuitPython gives every board the same higher-level modules, a USB drive to edit on, and error messages in more than 10 languages, at the price of interrupts and threads. Both run Python 3 code, so logic that never touches the hardware moves between them unchanged.

The rule of thumb

Choose MicroPython for interrupts, timers, threads or chip features such as PIO; choose CircuitPython for a classroom or a first project, where editing on a USB drive and its device libraries matter most.

Checkpoint

Check: MicroPython against CircuitPython

  1. Question 1 of 2 Which file holds a CircuitPython program that reruns every time you save it from the computer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: code.py on the CIRCUITPY drive

    CircuitPython's workflow is edit on the drive, save, see it run. MicroPython runs boot.py and main.py after a reset and leaves editing to tools.

  2. Question 2 of 2 A project must react to a button within a millisecond while its main loop sleeps. Which platform has the tool for it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: MicroPython, with a Pin.irq() handler

    That is the rule of thumb of the lesson: interrupts, timers and threads point to MicroPython.

MicroPython runs on other ports too, in four support tiers

MicroPython is built separately for each family of chips or operating system, and each build is a port: MicroPython adapted to one family of chips or one operating system, such as rp2 for the RP2040 and RP2350, esp32, or unix. Each port builds the firmware for its boards. More in “What MicroPython is and where it runs”. The other ports reach far beyond the Pico and the ESP32. Since 1.27 every port has a support tier: One of MicroPython's four levels of care for a port. Tier 1 ports such as rp2, esp32 and unix are the most actively maintained, tested and documented. More in “What MicroPython is and where it runs”:

Tier Ports in 1.29 What the tier promises
1 esp32, mimxrt, rp2, samd, stm32, unix, windows active maintenance, a stable Python API, hardware tests for releases, port documentation
2 alif, embed, nrf, psoc-edge, renesas-ra, webassembly, zephyr active maintenance, a stable API, hardware tests for releases
3 cc3200, esp8266, pic16bit builds and tests pass in CI
M bare-arm, minimal, qemu maintenance, development and testing of MicroPython itself

Two of the other ports matter even without a board. The unix and windows ports, in Tier 1, run MicroPython on a computer, which makes them the test bench for code that will run on a chip. The webassembly port, in Tier 2, runs MicroPython in a web browser; its npm package, @micropython/micropython-webassembly-pyscript, is its PyScript variant. The ESP8266, once a favourite board, now sits in Tier 3: it builds, but new features go elsewhere.

Checkpoint

Check: Other ports and their support tiers

  1. Question 1 of 2 In which support tier is MicroPython's esp8266 port in 1.29?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Tier 3

    Tier 3 means builds and tests pass in CI, but the port is not actively maintained and has no stable-API promise.

  2. Question 2 of 2 What does Tier 1 promise that Tier 2 does not?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Prioritised bug reports, patch releases for regressions, and port-specific documentation

    The tier table lists minimum requirements; a lower tier may tick more boxes, but these three define Tier 1.

A thin hardware layer moves the logic between platforms

To run one application on both platforms, keep it away from the hardware, behind a board layer: A small module that implements a fixed set of methods, such as led() and button_pressed(), with one platform's hardware API, so that the application above it runs on every platform that has one.. The application below asks a board object three things: to light the LED, whether the button is pressed, and how many milliseconds have passed. Two small modules answer them, one with machine.Pin and Signal for MicroPython, one with digitalio for CircuitPython; the clock comes in as an argument, so a test can pass its own. The program runs the same application on both layers, with stand-ins for each platform:

One application, two board layers

ports/run_both.py

"""The same application on the MicroPython layer and the CircuitPython layer, run on stand-ins of both platforms."""
import app
import bench
import hw_circuitpython
import hw_micropython

TIMES = range(0, 2000, 250)
print("ms            " + "".join("%5d" % t for t in TIMES))
for name, layer in (("MicroPython  ", hw_micropython), ("CircuitPython", hw_circuitpython)):
    bench.reset()
    board = layer.Board(clock=bench.clock)
    seen = []
    for t in TIMES:
        bench.press(1200 <= t < 1700)  # the button is held from 1,200 to 1,700 ms
        app.step(board)
        seen.append(bench.led().strip())
        bench.advance(250)
    print(name + " " + "".join("%5s" % s for s in seen))

Output

ms                0  250  500  750 1000 1250 1500 1750
MicroPython      on   on  off  off   on   on   on  off
CircuitPython    on   on  off  off   on   on   on  off

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 run_both.py

ports/app.py

"""The application: blink once a second while idle, stay lit while the button is held.
It talks only to a board object, never to machine or digitalio, so it runs on either platform."""


def step(board):
    if board.button_pressed():
        board.led(True)
    else:
        board.led(board.now_ms() // 500 % 2 == 0)

ports/hw_micropython.py

"""The board layer for MicroPython: machine.Pin and Signal, with ticks that survive the wrap-around."""
import time

from machine import Pin, Signal


class Board:
    def __init__(self, led_pin="LED", button_pin=14, clock=None):
        self._led = Signal(Pin(led_pin, Pin.OUT, value=0))
        self._button = Signal(Pin(button_pin, Pin.IN, Pin.PULL_UP), invert=True)  # pressed pulls low
        if clock is None:
            t0 = time.ticks_ms()

            def clock():
                return time.ticks_diff(time.ticks_ms(), t0)

        self.now_ms = clock  # milliseconds since start; a test passes its own clock

    def led(self, on):
        self._led.value(1 if on else 0)

    def button_pressed(self):
        return bool(self._button.value())

ports/hw_circuitpython.py

"""The board layer for CircuitPython: board pin names and digitalio, with time.monotonic_ns()."""
import time

import board
import digitalio


class Board:
    def __init__(self, led_pin=board.LED, button_pin=board.GP14, clock=None):
        self._led = digitalio.DigitalInOut(led_pin)
        self._led.switch_to_output(value=False)
        self._button = digitalio.DigitalInOut(button_pin)
        self._button.switch_to_input(pull=digitalio.Pull.UP)
        if clock is None:
            t0 = time.monotonic_ns()

            def clock():
                return (time.monotonic_ns() - t0) // 1_000_000

        self.now_ms = clock  # milliseconds since start; a test passes its own clock

    def led(self, on):
        self._led.value = bool(on)

    def button_pressed(self):
        return not self._button.value  # pressed pulls low

machine.py (stand-in) · ports/machine.py

"""A stand-in for machine.Pin and machine.Signal, enough for the board layer beside it. Never copy it to a board."""
import bench

FAKE = True


class Pin:
    IN, OUT, PULL_UP = 0, 1, 1

    def __init__(self, id, mode=-1, pull=None, value=None):
        self.id = id
        if value is not None:
            bench.write(id, value)

    def value(self, v=None):
        if v is None:
            return bench.read(self.id)
        bench.write(self.id, v)


class Signal:
    def __init__(self, pin, invert=False):
        self.pin, self.invert = pin, invert

    def value(self, v=None):
        if v is None:
            return self.pin.value() ^ self.invert
        self.pin.value(v ^ self.invert)


class I2C:
    """machine.I2C with MSC-ENV1 (made up) at 0x41: TEMP in registers 3 and 4, 21.5 °C."""

    def __init__(self, id=0, *, scl=None, sda=None, freq=400_000):
        self.regs = bytes([0xE1, 0, 0, 0x0A, 0xC0])

    def readfrom_mem_into(self, addr, memaddr, buf):
        buf[:] = self.regs[memaddr:memaddr + len(buf)]

board.py (stand-in) · ports/board.py

"""A stand-in for CircuitPython's board module on a Pico: pin names as objects. Never copy it to a board."""
FAKE = True


class _Pin:
    def __init__(self, name):
        self.name = name


LED = _Pin("LED")
GP14 = _Pin("GP14")
GP4 = _Pin("GP4")
GP5 = _Pin("GP5")

digitalio.py (stand-in) · ports/digitalio.py

"""A stand-in for CircuitPython's digitalio, enough for the board layer beside it. Never copy it to a board."""
import bench

FAKE = True
_IDS = {"LED": "LED", "GP14": 14}  # the names the bench uses


class Pull:
    UP, DOWN = "UP", "DOWN"


class DigitalInOut:
    def __init__(self, pin):
        self._id = _IDS[pin.name]

    def switch_to_output(self, value=False, drive_mode=None):
        bench.write(self._id, int(value))

    def switch_to_input(self, pull=None):
        pass

    @property
    def value(self):
        return bool(bench.read(self._id))

    @value.setter
    def value(self, v):
        bench.write(self._id, int(v))

ports/bench.py

"""The test bench for both board layers: a clock you move, a button you press, and the LED's level."""
_levels = {14: 1}  # GP14 idles high through its pull-up
_now = [0]


def reset():
    _levels.clear()
    _levels[14] = 1
    _now[0] = 0


def clock():
    return _now[0]


def advance(ms):
    _now[0] += ms


def press(down):
    _levels[14] = 0 if down else 1


def read(pin):
    return _levels.get(pin, 0)


def write(pin, value):
    if pin != 14:  # the button is an input; writes to it change nothing
        _levels[pin] = 1 if value else 0


def led():
    return "on " if _levels.get("LED") else "off"

Both rows are the same: the LED blinks once a second and stays lit while the button is held from 1,200 to 1,700 ms. app.py imports neither platform, so a change to the application is made once, and each layer keeps what really differs: pin names, the button’s polarity, the clock.

Checkpoint

Check: A thin hardware layer

  1. One question In the lesson's design, where does the button's active-low wiring belong?

    Choose one answer.

    Show the answer: One question

    Answer: In each board layer, which turns the pin's level into button_pressed()

    The layer keeps what differs between boards and platforms: pin names, polarity and the clock. The application keeps the logic.

Check yourself

Test what you know of the two platforms, the ports and the layer between them, one question at a time.

Practice that adapts to you

Practise this lesson

6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 What is CircuitPython's relation to MicroPython?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: It is based on MicroPython and keeps its language, with its own hardware API and workflow

    Shared language, separate hardware API: logic moves between them, hardware code does not.

  2. Question 2 of 6 A CircuitPython board crashes hard and restarts. What does it do before you fix the code?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: It enters safe mode and does not run your code, so the drive stays reachable for the fix

    MicroPython has no such stop: every reset runs boot.py and main.py again, so code that crashes the board at start-up does it again at every reset until you guard against it, as the watchdog lesson's recovery patterns do.

  3. Question 3 of 6 Which MicroPython port runs in a web browser?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: webassembly, a Tier 2 port

    The browser and the unix port both let code run without a board, for teaching and for tests.

  4. Question 4 of 6 Why do the unix and windows ports matter to someone who only ships code for a Pico?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: They run MicroPython itself on a computer, so tests see MicroPython's behaviour without a board

    CPython and MicroPython differ in places, so a test that passes on the unix port is closer to the board's truth.

  5. Question 5 of 6 What does the application of the lesson import?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: Neither machine nor digitalio; it only calls the board object it is given

    The application depends on three methods; each layer implements them with its platform's API.

  6. Question 6 of 6 Why does each board layer take a clock argument?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: So a test can pass a clock it controls and get the same output every run, while a board uses the platform's clock by default

    Injecting the clock, like injecting the bus in the drivers lesson, makes behaviour that depends on time testable.

Porting MicroPython code to CircuitPython, call by call Pro: opens with a Pro, Premium or Ultimate pass

Most MicroPython code moves to CircuitPython in a handful of patterns, and each has a trap that fails quietly or late. This part maps the everyday calls side by side, then reproduces the trap that stops most I2C ports.

In this part:

  • The everyday calls side by side, with the traps
  • Start-up, files and state behave differently
  • The same register read on both, reproduced

1 runnable example (board.py (stand-in), busio.py (stand-in), i2c_port.py, machine.py (stand-in)) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Board layers with a contract, tested on both platforms Pro: opens with a Pro, Premium or Ultimate pass

A board layer is only as portable as its promises. This part writes those promises down as a contract, tests every layer against it, and draws the line between what goes in the layer and what stays out.

In this part:

  • Writing the contract down
  • One set of tests for every layer
  • Two board layers behind one contract
    • Results of the sample tests
  • What stays outside the layer

1 exercise with sample tests · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Choosing a port and a board, case by case Pro: opens with a Pro, Premium or Ultimate pass

The right platform follows from a project’s needs, and four questions settle most cases. This part puts them in a table that answers back, then walks through four projects.

In this part:

  • Four questions that settle most choices
  • Four projects, four choices

about 6 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

Warm-up (fresher): What is CircuitPython, and how does it differ from MicroPython?

CircuitPython is a version of Python for microcontrollers that is based on MicroPython and keeps its language core. It replaces MicroPython’s machine module with higher-level modules such as board, digitalio and busio that look the same on every board, and makes the board appear as a USB drive called CIRCUITPY, where saving code.py reruns the program. It leaves out interrupts and threads, which MicroPython has, and adds a safe mode after crashes and translated error messages. Code that never touches the hardware runs on both.

Three longer questions about ports follow; the link under each leads to what it tests and a first step:

  1. Mid to senior

    A MicroPython logger on a Pico reads an I2C sensor with readfrom_mem_into(), blinks a status LED from a Timer callback and appends readings to a file on the board. It must move to a CircuitPython board. What breaks, in which order do you fix it, and what do you test?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A gadget ships on a Pico with MicroPython and, for schools, on a CircuitPython board. Design one codebase for both: its structure, the contract of the hardware layer, and how CI proves that both builds work.

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A startup will sell a battery-powered Wi-Fi sensor and update it for five years. Its founders liked CircuitPython in a workshop. Which platform do you recommend, and how do you justify it with support tiers and the features each platform has?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How does CircuitPython's hardware API differ from MicroPython's?

    CircuitPython has no machine module: it uses board pin names with digitalio, busio and other modules, such as digitalio.DigitalInOut(board.LED) in place of Pin("LED", Pin.OUT).

  2. Which files start a program in CircuitPython and in MicroPython?

    CircuitPython runs boot.py once at start-up, then code.py after every reload. MicroPython runs boot.py, then main.py, after each reset.

  3. How do you change a program on each platform?

    CircuitPython shows the board as the CIRCUITPY USB drive, and saving a file reruns the program. MicroPython uses the REPL and tools such as mpremote over USB serial.

  4. Which concurrency tools does CircuitPython leave out?

    Interrupts and threads, though some boards have async and await. MicroPython has Pin.irq(), timer callbacks, asyncio and, on some ports, threads.

  5. Which support tier is the esp8266 port in, and what does that promise?

    Tier 3: it is built and tested by continuous integration but not actively maintained, so new features go to other ports.

  6. Which MicroPython ports run without a microcontroller?

    The unix and windows ports, Tier 1, run on a computer as a test bench; the webassembly port, Tier 2, runs in a browser, and its PyScript variant is on npm.

  7. What lets one application run on MicroPython and CircuitPython unchanged?

    A thin board layer for each platform with the same small set of methods, such as led() and button_pressed(), and the clock passed in, so the application imports neither platform.

Key takeaways

  • MicroPython and CircuitPython run the same Python language, but CircuitPython replaces machine with board, digitalio and busio, starts from code.py on a USB drive, and has no interrupts or threads.
  • MicroPython’s ports are sorted into four support tiers: rp2, esp32 and five others in Tier 1, webassembly in Tier 2, the ESP8266 in Tier 3.
  • A thin board layer, one per platform, with the clock passed in, lets one application run unchanged on both.

Terms in this lesson

board layer
A small module that implements a fixed set of methods, such as led() and button_pressed(), with one platform's hardware API, so that the application above it runs on every platform that has one.
CircuitPython
A Python for microcontrollers, based on MicroPython, with its own hardware modules such as board, digitalio and busio, a USB drive for editing, and no interrupts or threads.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.