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 1 – Getting started: boards, firmware, the REPL and mpremote

Install firmware and use the REPL

Pick the right MicroPython firmware, install it on a Pico by copying a file or on an ESP32 with esptool, then use the REPL keys and the raw REPL.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Choose the firmware file for a board from its name, variant and file type
  • Install MicroPython on a Pico by copying a UF2 file, and on an ESP32 with esptool
  • Verify and record which firmware a board runs
  • Use the REPL keys Ctrl-C, Ctrl-D, Ctrl-E, Tab and the _ variable
  • Explain how the raw REPL frames a reply, and why tools such as mpremote rely on it

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose the firmware file for a board from its name, variant and file type
  • Install MicroPython on a Pico by copying a UF2 file, and on an ESP32 with esptool
  • Verify and record which firmware a board runs
  • Use the REPL keys Ctrl-C, Ctrl-D, Ctrl-E, Tab and the _ variable
  • Explain how the raw REPL frames a reply, and why tools such as mpremote rely on it

Before you start

On this page

MicroPython’s download pages list separate firmware files for the Pico and the Pico W, five builds for one ESP32 board, and preview builds of the next release beside every release. Pick the wrong file and the board may still start, then fail the first time your code touches Wi-Fi, which is a confusing place to discover a download mistake.

This lesson installs the right file on a Pico with a file copy and on an ESP32 with esptool, then teaches the prompt you get afterwards, the REPL: Read-eval-print loop: the interactive >>> prompt that MicroPython runs on a board's USB serial port or UART. Each line you type is compiled, run, and its result printed.: the keys that stop, restart and paste, and the raw mode that tools such as mpremote use to run your code, byte by byte.

The firmware file must match your board, not only its chip

The Pico and the Pico W share the RP2040 chip, and the Pico firmware runs on both. Only the Pico W file, though, includes the network module and the driver for the board’s Wi-Fi chip, so on a Pico W with the Pico file, import network raises ImportError. An ESP32 module needs the same care: the ESP32_GENERIC download page offers a plain build and four variants, and tells you which board each one is for.

On the download page Choose it when Watch out for
RPI_PICO, RPI_PICO_W, RPI_PICO2_W It is your board’s exact name The Pico file on a Pico W runs, without the network module
ESP32_GENERIC, plain Most ESP32 boards with 4 MiB of flash or more It is the default when no variant below fits
ESP32_GENERIC-SPIRAM A WROVER module, or any board with SPIRAM (PSRAM) The page recommends it for every board with SPIRAM
ESP32_GENERIC-D2WD An ESP32-D2WD chip, with only 2 MiB of flash The plain build is made for 4 MiB of flash or more
ESP32_GENERIC-UNICORE A single-core ESP32, such as a SOLO module
ESP32_GENERIC-OTA You will update MicroPython itself over the air It leaves less room in flash for your files
A release, not a preview Always, for a first install Previews are automatic builds of the next release, still in development

Every file name has the same parts: the board, an optional variant, an eight-digit build stamp, the version and the file type, .uf2 for the Pico boards and .bin for an ESP32. The program below splits six names into those parts and checks each against the board it is meant for:

Firmware file names, split and checked against a board Python · firmware/firmware_name.py
"""Split MicroPython firmware file names into their parts, and check each one against the board it is meant for.

The names follow the pattern of MicroPython's download pages: BOARD, an optional -VARIANT, an eight-digit build
stamp, -v and the version, an optional preview suffix, then the file type.
"""

import re

NAME = re.compile(
    r"^(?P<board>[A-Z0-9_]+?)(?:-(?P<variant>[A-Z0-9]+))?-(?P<stamp>\d{8})"
    r"-v(?P<version>\d+\.\d+\.\d+)(?P<preview>-preview\.[0-9a-z.]+)?\.(?P<kind>uf2|bin|app-bin)$"
)

# For each board: its firmware name and the file type its download page installs.
BOARDS = {
    "Pico": ("RPI_PICO", "uf2"),
    "Pico W": ("RPI_PICO_W", "uf2"),
    "Pico 2 W": ("RPI_PICO2_W", "uf2"),
    "ESP32 WROOM": ("ESP32_GENERIC", "bin"),
    "ESP32 WROVER": ("ESP32_GENERIC", "bin"),
}
# Builds whose firmware includes the network module (the Wi-Fi boards).
NETWORK = {"RPI_PICO_W", "RPI_PICO2_W", "ESP32_GENERIC"}


def check(file_name, board):
    """The problems of installing file_name on board, as a list of sentences (empty: the file fits)."""
    parts = NAME.match(file_name)
    if parts is None:
        return ["not named like a MicroPython firmware file"]
    want, kind = BOARDS[board]
    problems = []
    if parts["board"] != want:
        problems.append(f"built for {parts['board']}, not {want}")
        if want in NETWORK and parts["board"] not in NETWORK:
            problems.append("that build has no network module, so Wi-Fi code fails with ImportError")
    if parts["kind"] == "app-bin":
        problems.append("not the .bin file that the download page says to install")
    elif parts["kind"] != kind:
        problems.append(f"a .{parts['kind']} file, but this board installs a .{kind} file")
    if parts["preview"]:
        problems.append("a preview build of the next release, not a release")
    return problems


CASES = [
    ("RPI_PICO-20260824-v1.29.0.uf2", "Pico"),
    ("RPI_PICO-20260824-v1.29.0.uf2", "Pico W"),
    ("RPI_PICO2_W-RISCV-20260824-v1.29.0.uf2", "Pico 2 W"),
    ("ESP32_GENERIC-SPIRAM-20260824-v1.29.0.bin", "ESP32 WROVER"),
    ("ESP32_GENERIC-20260824-v1.29.0.app-bin", "ESP32 WROOM"),
    ("RPI_PICO_W-20261016-v1.30.0-preview.147.g9e348d293c.uf2", "Pico W"),
]

for file_name, board in CASES:
    parts = NAME.match(file_name)
    print(file_name)
    print(f"  board {parts['board']}, variant {parts['variant'] or '-'}, version {parts['version']}")
    problems = check(file_name, board)
    print(f"  on the {board}: {'fits' if not problems else '; '.join(problems)}")

Output

RPI_PICO-20260824-v1.29.0.uf2
  board RPI_PICO, variant -, version 1.29.0
  on the Pico: fits
RPI_PICO-20260824-v1.29.0.uf2
  board RPI_PICO, variant -, version 1.29.0
  on the Pico W: built for RPI_PICO, not RPI_PICO_W; that build has no network module, so Wi-Fi code fails with ImportError
RPI_PICO2_W-RISCV-20260824-v1.29.0.uf2
  board RPI_PICO2_W, variant RISCV, version 1.29.0
  on the Pico 2 W: fits
ESP32_GENERIC-SPIRAM-20260824-v1.29.0.bin
  board ESP32_GENERIC, variant SPIRAM, version 1.29.0
  on the ESP32 WROVER: fits
ESP32_GENERIC-20260824-v1.29.0.app-bin
  board ESP32_GENERIC, variant -, version 1.29.0
  on the ESP32 WROOM: not the .bin file that the download page says to install
RPI_PICO_W-20261016-v1.30.0-preview.147.g9e348d293c.uf2
  board RPI_PICO_W, variant -, version 1.30.0
  on the Pico W: a preview build of the next release, not a release

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

The second case is the mistake of the opening: the RPI_PICO file on a Pico W runs, and fails at the first import network. The fifth is a trap of the ESP32 page, which lists an .app-bin file beside each .bin. The page says to download the .bin file, which is the combined image that the esp32 port’s README describes: a bootloader, a partition table and MicroPython itself.

Keep a record of what you installed: the file name and its SHA-256: A hash function that turns any file into a 256-bit fingerprint, written as 64 hexadecimal digits. Two files with the same SHA-256 have the same content, so comparing hashes shows which files changed. More in “mpremote: run, copy, mount and sync”, which you can compute without uploading the file anywhere. With them you can install the same firmware again on a replacement board, or tell two boards apart when one behaves differently.

Hash Generator Compute the SHA-256 of a downloaded firmware file in your browser, and keep it with the file name.

Checkpoint

Check: Choosing the firmware file

  1. Question 1 of 2 A Pico W was given the RPI_PICO file of 1.29.0. It starts and the prompt works. What happens when your code runs import network?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raises ImportError, because only the Pico W build includes the network module

    Firmware is built per board. Check _build in sys.implementation: 'RPI_PICO' on a Pico W means the wrong file, and copying the RPI_PICO_W file fixes it.

  2. Question 2 of 2 Your ESP32 board is built on a WROVER module, which has SPIRAM. Which file of the ESP32_GENERIC page should you install?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The SPIRAM variant

    The ESP32_GENERIC page lists the plain build and four variants, SPIRAM, D2WD, UNICORE and OTA, and says which board each one is for. A WROVER module has SPIRAM, so the SPIRAM variant is the one.

A Pico installs by copying a file; an ESP32 through esptool

Pico boards install firmware as a UF2 file: A firmware file made of 512-byte blocks that a board's bootloader writes to flash when the file is copied onto the USB drive it shows. Pico boards install MicroPython this way.. Their chip has a built-in bootloader: A small program that runs at reset, before the firmware, and can receive new firmware: the RP2040's USB drive mode and the ESP32's serial ROM bootloader are two. that shows the board to your computer as a USB drive, and copying a .uf2 file onto that drive installs it. Step through a first install:

Interactive · stage by stage Installing MicroPython on a Pico with a file copy Step through what happens from the BOOTSEL button to the first prompt. The same steps install a newer release later.
Installing MicroPython on a Pico with a file copy, step 6 of 6. Showing Hold BOOTSEL, plug in, A USB drive appears, Copy the .uf2 file, Flash written, MicroPython starts (filesystem mounted at /), The REPL on USB serial. In focus: The REPL on USB serial.Hold BOOTSEL, plug inor machine.bootloader()A USB drive appearsnamed RPI-RP2 or RP2350Copy the .uf2 filethe file for your boardFlash writtenthe board resets itselfMicroPython startsfilesystem mounted at /The REPL on USB seriala new serial port

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Bootloader mode. Hold the BOOTSEL button while you plug the USB cable in, then let go. A board that already runs MicroPython can do the same from its prompt with machine.bootloader().
  2. The drive. The chip's built-in bootloader shows the board to your computer as a USB drive, called RPI-RP2 on a Pico or Pico W and RP2350 on a Pico 2 or Pico 2 W. It is only there to receive a file.
  3. Copy the file. Copy the .uf2 file for your exact board onto the drive, by dragging it in a file manager or with a copy command. For a Pico W on 1.29.0, it is the file whose name starts with RPI_PICO_W and ends in v1.29.0.uf2.
  4. Flash written. The bootloader writes the firmware into the board's flash and resets the board by itself when it is done, so the drive disappears again.
  5. First start. MicroPython starts. Its frozen _boot.py looks for a filesystem in the flash, finds none on a new board, formats one and mounts it at /, without printing a word.
  6. The prompt. The board now appears as a USB serial port, /dev/ttyACM0 on Linux, /dev/cu.usbmodem followed by a number on a Mac and a COM port on Windows, and the REPL waits there for you.

The last step is the one to check. The prompt appears on a USB serial port, and once it does, two lines tell you exactly what you installed:

  • import sys and then sys.implementation show the version, (1, 29, 0, ''), and the build, _build='RPI_PICO_W' on a Pico W, as the previous lesson decoded.
  • If _build names another board than yours, install the right file again: the next copy simply replaces it.

An original ESP32 chip has no USB of its own, so it cannot appear as a drive: boards reach it through a USB-serial converter. Espressif’s esptool talks to the chip’s ROM bootloader over that serial connection, and the download page gives two commands, an erase and a write. Step through them, and through what happens when the erase is skipped:

Interactive · message by message Installing MicroPython on an ESP32 with esptool Step through the two esptool commands of the download page and the first start, then see what happens when the erase is skipped.
Installing MicroPython on an ESP32 with esptool, step 6 of 6. Messages so far: Your computer to ESP32 board: enter the bootloader; Your computer to ESP32 board: erase_flash; Your computer to ESP32 board: write_flash 0x1000 file.bin; ESP32 board to Your computer: Performing initial setup; ESP32 board to Your computer: >>> prompt, 115200 baud.Your computeresptoolESP32 boardfilesystem at /enter the bootloadererase_flashwrite_flash 0x1000 file.binPerforming initial setup>>> prompt, 115200 baudfix: erase_flash, then write_flash

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Bootloader mode. esptool puts the chip into its ROM bootloader over the USB-serial connection. If the board does not answer, hold BOOT, press and release RESET, then release BOOT.
  2. Erase everything. esptool.py erase_flash empties the whole flash, the old program and any old filesystem with it. The download page asks for this before a first install.
  3. Write at 0x1000. esptool.py --baud 460800 write_flash 0x1000 with the .bin file writes the image from address 0x1000 on, the address the download page gives for the original ESP32.
  4. First start. After a reset, MicroPython's _boot.py finds no filesystem, checks that its first block is empty, prints Performing initial setup, formats the flash and writes a boot.py.
  5. The prompt. The REPL now runs on the board's serial connection at 115200 baud, the speed the ESP32 tutorial gives. mpremote or any serial terminal can open it.
  6. If the erase was skipped. Without the erase, old data can sit where the filesystem goes. Then the first start prints that the filesystem appears to be corrupted, again every 3 seconds, until you erase the flash and write the image again.

The erase matters for a first install only. The esp32 port’s first-boot code checks that the block where the filesystem goes is empty before it formats it; on a board that held other firmware it may not be, and MicroPython then refuses to touch it. Updating a board that already runs MicroPython is different: the reset and boot documentation notes that installing new firmware usually keeps the filesystem, so your files survive an update.

Updating to fix a board that will not start

When a board that ran MicroPython stops starting after a change to your files, a new firmware file usually does not help, because the update keeps the filesystem and with it the file that breaks the start. The lesson on boot files shows how to get the board back; installing again only works after the flash is erased completely.

Checkpoint

Check: Installing on a Pico and an ESP32

  1. Question 1 of 2 At which address does the download page tell you to write the ESP32_GENERIC .bin file on an original ESP32?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0x1000

    The page gives two commands: esptool.py erase_flash, then esptool.py --baud 460800 write_flash 0x1000 with the .bin file. The image holds the bootloader, the partition table and MicroPython.

  2. Question 2 of 2 A Pico holds main.py and a lib folder. You install 1.29.0 over its older MicroPython with a .uf2 copy. What happens to the files?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: They are still there, because a firmware update usually keeps the filesystem

    The firmware and the filesystem are separate parts of the flash, and copying a .uf2 file replaces only the firmware. Erasing the whole flash clears the files as well, which is what the factory reset instructions do before installing again.

Three keys stop, restart and paste at the REPL

mpremote with no arguments, or any serial terminal, opens the prompt: it connects to the first USB serial device it finds, and Ctrl-] or Ctrl-X leaves it again. At the >>> prompt a few control keys do more than edit text. MicroPython reads them as commands only when the line is still empty, which the firmware’s line editor checks for each key:

Key At an empty prompt With text on the line
Ctrl-C A new prompt Cancels the line being typed
Ctrl-D A soft reset: A restart of the MicroPython interpreter without resetting the chip: Python objects, imports and most peripheral set-up are cleared, then the boot files run again. Ctrl-D at an empty prompt does one. More in “boot.py, main.py and a layout that survives resets”: variables and imports are cleared, then the boot files run again Ignored, or deletes a character on builds with Emacs-style keys
Ctrl-E paste mode: A REPL mode, opened with Ctrl-E at an empty prompt, that takes pasted text as it is, with no auto-indent, and compiles it as one block when you press Ctrl-D.: the prompt becomes ===, Ctrl-D runs the text, Ctrl-C cancels it Moves the cursor to the end of the line
Ctrl-A The raw REPL, for programs (next section) Moves the cursor to the start of the line
Ctrl-B Prints the banner again; in the raw REPL, returns to the friendly one Moves the cursor back one character on builds with Emacs-style keys
Tab Lists the names you have defined or imported so far Completes a name as far as it is unique; after machine. it lists what the module holds

While a program runs, the key that counts is Ctrl-C: it raises KeyboardInterrupt inside the program, which stops it and brings back the prompt unless the program catches that exception. Two habits follow from the table. Before you paste a block of code, press Ctrl-E on an empty line: in normal mode the REPL indents each new line after a colon by itself, so pasted code that is already indented gets indented twice and fails with IndentationError. And after an expression, the special variable _ holds its value, so x = _ keeps a result you forgot to name.

Pressing Ctrl-E with text on the line

Ctrl-E opens paste mode only at an empty prompt. With text on the line it moves the cursor to the end of it, and the paste that follows lands in the normal REPL with its auto-indent. Clear the line with Ctrl-C first.

Checkpoint

Check: The REPL's control keys

  1. Question 1 of 2 At an empty >>> prompt you press Ctrl-D. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A soft reset; variables and imports are cleared and the boot files run again

    A soft reset restarts the interpreter: every Python object is freed, and boot.py and main.py run as after a reset. The REPL docs show dir() before and after it, with the user's variables gone.

  2. Question 2 of 2 You paste a ten-line function into the normal REPL and get IndentationError, though the code is right. What fixes it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Press Ctrl-E at an empty prompt first, paste, then press Ctrl-D

    In normal mode the REPL indents the next line after a colon by itself, so pasted lines that are already indented get indented twice. Paste mode, with its === prompt, takes the text as it is and compiles it when you press Ctrl-D.

The raw REPL is the same prompt, framed for programs

The friendly REPL is made for people: it echoes keys, indents, and prints a prompt after each line. A program that sends code needs something stricter, and that is the raw REPL: The REPL's mode for programs, entered with Ctrl-A: it echoes nothing, runs the code it received when Ctrl-D arrives, and frames every reply with OK and two 0x04 bytes.. Ctrl-A switches to it, the board stops echoing, and each block of code ends with Ctrl-D. Every reply has the same frame: OK, the output, a 0x04 byte, the error output, a second 0x04 and the > prompt, so a tool can tell where output ends and a traceback starts. Step through one run of print(6 * 7):

Interactive · message by message One program through the raw REPL, byte by byte Step through what a tool such as mpremote sends and what the board answers, from the switch into raw mode to the next prompt.
One program through the raw REPL, byte by byte, step 9 of 9. Messages so far: The tool to The board: Enter, Ctrl-C; The tool to The board: Enter, Ctrl-A; The board to The tool: raw REPL; CTRL-B to exit >; The tool to The board: print(6 * 7); The tool to The board: Ctrl-D; The board to The tool: OK; The board to The tool: 42, then 0x04; The board to The tool: no error, then 0x04; The board to The tool: >.The toolmpremote or an editorThe boardwaiting in raw modeEnter, Ctrl-CEnter, Ctrl-Araw REPL; CTRL-B to exit >print(6 * 7)Ctrl-DOK42, then 0x04no error, then 0x04>

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. Stop what runs. The tool sends Enter and Ctrl-C, which stops a running program, and throws away whatever the board prints back. The board should now be waiting at a prompt.
  2. Switch to raw mode. Enter and Ctrl-A switch the board to the raw REPL. It answers with a fixed banner and a single > as its prompt, which the tool waits for before it goes on.
  3. The banner. The banner is the same on every board, raw REPL; CTRL-B to exit, so a tool can recognise it without knowing the board.
  4. The code, no echo. The tool sends the program as plain bytes, print(6 * 7). The raw REPL echoes nothing, so nothing comes back yet, and there is no auto-indent to spoil pasted code.
  5. Ctrl-D runs it. Ctrl-D marks the end of the code. The board answers OK at once, to say it has the code and is compiling and running it.
  6. OK. OK is the board's receipt. If the tool gets anything else, the board did not take the code, and mpremote stops with an error instead of waiting forever.
  7. The output, then Ctrl-D. The program prints 42, which arrives as 42 followed by a carriage return and a newline. A Ctrl-D byte, 0x04, ends the program's normal output.
  8. The error part. Next comes the error output, empty here because nothing failed, and a second 0x04. After a crash the traceback sits between the two 0x04 bytes instead.
  9. The next prompt. A > ends the reply, and the board waits for the next program in raw mode. Ctrl-B would take it back to the friendly REPL, where it prints its banner again.

The next program does what mpremote’s serial transport does: it switches the board into raw mode, sends two programs and splits each reply. Its board is fake_board.py, a stand-in written from the firmware’s own raw REPL loop that runs the code with your computer’s Python, so it shows the bytes of the protocol, not MicroPython’s own results:

fake_board.py: a stand-in for a board's REPL Python · raw-repl/fake_board.py
"""A stand-in for a MicroPython board's REPL, written from the firmware's own loop (shared/runtime/pyexec.c).

It answers the raw REPL the way the firmware does, and the few keys of the friendly REPL that a tool sends to reach
it. It runs the code it receives with the Python of this computer, so it shows the protocol, not MicroPython's own
results.
"""

import contextlib
import io

BANNER = b"raw REPL; CTRL-B to exit\r\n"


class FakeBoard:
    def __init__(self):
        self.raw = False          # False: the friendly REPL, with its >>> prompt
        self.line = bytearray()   # what the host has typed since the last Ctrl-D
        self.sent = bytearray()   # bytes the board has sent and the host has not read yet
        self.globals = {"__name__": "__main__"}

    def write(self, data):
        """Bytes from the host, one at a time, as they arrive over USB serial."""
        for byte in data:
            if self.raw:
                self._raw_key(byte)
            else:
                self._friendly_key(byte)

    def read(self):
        """Everything the board has sent since the last read."""
        data = bytes(self.sent)
        self.sent.clear()
        return data

    def _friendly_key(self, byte):
        if byte == 0x01:            # Ctrl-A at an empty prompt: switch to the raw REPL
            self.sent += b"\r\n" + BANNER + b">"
            self.raw = True
        elif byte in (0x0D, 0x03):  # Enter, or Ctrl-C, at an empty prompt: a new prompt
            self.sent += b"\r\n>>> "

    def _raw_key(self, byte):
        if byte == 0x01:            # Ctrl-A: start the raw REPL again
            self.line.clear()
            self.sent += BANNER + b">"
        elif byte == 0x02:          # Ctrl-B: back to the friendly REPL (a board then prints its banner)
            self.raw = False
            self.sent += b"\r\n"
        elif byte == 0x03:          # Ctrl-C: throw away the code typed so far
            self.line.clear()
        elif byte == 0x04:          # Ctrl-D: run what was typed
            self.sent += b"OK"
            self._run(bytes(self.line).decode())
            self.line.clear()
            self.sent += b">"
        else:                       # anything else is code, and nothing is echoed
            self.line.append(byte)

    def _run(self, source):
        out = io.StringIO()
        error = ""
        try:
            with contextlib.redirect_stdout(out):
                exec(compile(source, "<stdin>", "exec"), self.globals)
        except Exception as e:
            tb = e.__traceback__
            while tb.tb_next is not None:
                tb = tb.tb_next
            # The traceback in the form MicroPython prints it.
            error = (
                "Traceback (most recent call last):\n"
                f'  File "<stdin>", line {tb.tb_lineno}, in <module>\n'
                f"{type(e).__name__}: {e}\n"
            )
        self.sent += out.getvalue().replace("\n", "\r\n").encode()
        self.sent += b"\x04"
        self.sent += error.replace("\n", "\r\n").encode()
        self.sent += b"\x04"
Two programs through the raw REPL, with every byte shown Python · raw-repl/raw_session.py
"""Drive a board's raw REPL the way mpremote's serial transport does, and print every byte that crosses the cable.

The board here is fake_board.py, a stand-in that answers as the firmware does. mpremote sends the same bytes over a
real serial port, and reads until it sees each marker it expects: the raw REPL banner, OK, and the two Ctrl-D bytes.
"""

from fake_board import FakeBoard


def enter_raw_repl(board):
    board.write(b"\r\x03")   # Enter and Ctrl-C: stop whatever is running
    board.read()             # throw away the prompts that come back
    board.write(b"\r\x01")   # Enter and Ctrl-A: switch to the raw REPL
    reply = board.read()
    if not reply.endswith(b"raw REPL; CTRL-B to exit\r\n>"):
        raise RuntimeError(f"could not enter the raw REPL: {reply!r}")
    return reply


def exec_raw(board, code):
    """Send code, then Ctrl-D; return what it printed and its error output, as the board framed them."""
    board.write(code.encode() + b"\x04")
    reply = board.read()
    if not reply.startswith(b"OK"):
        raise RuntimeError(f"the board did not accept the code: {reply!r}")
    out, err, prompt = reply[2:].split(b"\x04")
    return reply, out, err


board = FakeBoard()
print("Ctrl-A     ->", enter_raw_repl(board))
for code in ["print(6 * 7)", "reading = None\nif reading is None:\n    raise ValueError('sensor not found')"]:
    reply, out, err = exec_raw(board, code)
    print("code       ->", code.encode() + b"\x04")
    print("reply      <-", reply)
    print("  output:  ", out)
    print("  error:   ", err)

Output

Ctrl-A     -> b'\r\n>>> \r\nraw REPL; CTRL-B to exit\r\n>'
code       -> b'print(6 * 7)\x04'
reply      <- b'OK42\r\n\x04\x04>'
  output:   b'42\r\n'
  error:    b''
code       -> b"reading = None\nif reading is None:\n    raise ValueError('sensor not found')\x04"
reply      <- b'OK\x04Traceback (most recent call last):\r\n  File "<stdin>", line 3, in <module>\r\nValueError: sensor not found\r\n\x04>'
  output:   b''
  error:    b'Traceback (most recent call last):\r\n  File "<stdin>", line 3, in <module>\r\nValueError: sensor not found\r\n'

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

The first reply is OK42\r\n\x04\x04>: the output, an empty error part between the two 0x04 bytes, and the prompt. The second program fails, so its output part is empty and the traceback, in the form MicroPython prints it, sits in the error part. That split is how mpremote exec can print your output and then exit with an error status when your code raised an exception.

Key idea

The raw REPL answers every block of code with the same frame, OK, the output, 0x04, the error output, 0x04 and the prompt, so a tool always knows where your output ends and an error begins.

mpremote and the project’s own pyboard.py tool drive boards this way, and since version 1.27 the unix port answers the raw REPL too, because its REPL now uses the same loop as the boards. mpremote first asks for raw-paste mode, a variant of the raw REPL with flow control: the board says how many bytes it can take at once, and the tool waits for room before it sends more, which makes long programs arrive safely.

Change the program

Press Edit on the second program and put a third piece of code in the list, such as print(sum(range(5))). Run it and find the 10 between OK and the first 0x04.

Checkpoint

Check: The raw REPL and its replies

  1. Question 1 of 2 A raw REPL reply reads b'OKhi\r\n\x04Traceback (most recent call last): ...\x04>'. What did the program do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It printed hi, then raised an exception

    Every reply is OK, the output, 0x04, the error output, 0x04 and >. Splitting at the two 0x04 bytes tells a tool what the program printed and whether it failed.

  2. Question 2 of 2 Why do tools such as mpremote send code through the raw REPL instead of typing it at the >>> prompt?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The raw REPL echoes nothing, adds no indentation and frames each reply, so a tool can tell output from errors

    Raw mode behaves like paste mode with echo turned off, and every reply comes back in the same frame. That makes it reliable for programs, while the friendly REPL is made for a person typing.

Practise: split a raw REPL reply

A tool that drives boards needs one small, careful function: the one that cuts a reply into output and error, and refuses a reply that is not complete. The sample tests include a traceback and output that contains > characters:

Exercise · Easy · Python

Split a raw REPL reply into output and error

After a tool sends code and Ctrl-D to a board's raw REPL, the board answers with one reply: OK, the program's output, a 0x04 byte, its error output, a second 0x04 byte, and the > prompt. Write split_raw_reply(data) in reply.py. data is that reply as bytes; return the pair (output, error), both bytes.

  • split_raw_reply(b"OK42\r\n\x04\x04>") returns (b"42\r\n", b"").
  • A reply whose program failed has the traceback as its error part, and an empty error part means the program ended normally.
  • Output may itself contain > characters: only the bytes after the second 0x04 are the prompt.

Raise ValueError when the reply is not a complete frame: it does not start with OK, it has fewer than two 0x04 bytes, or the second 0x04 is not followed by exactly >.

Starter code · reply.py

def split_raw_reply(data):
    """Return (output, error) of a raw REPL reply: b"OK" + output + b"\x04" + error + b"\x04" + b">"."""
    return (b"", b"")
The sample tests · test_reply.py
from reply import split_raw_reply


def raises_value_error(data):
    try:
        split_raw_reply(data)
    except ValueError:
        return True
    return False


def test_output():
    """splits a reply with output and no error"""
    assert split_raw_reply(b"OK42\r\n\x04\x04>") == (b"42\r\n", b"")


def test_traceback():
    """returns the traceback as the error part"""
    tb = b'Traceback (most recent call last):\r\n  File "<stdin>", line 1, in <module>\r\nValueError: no sensor\r\n'
    assert split_raw_reply(b"OKstart\r\n\x04" + tb + b"\x04>") == (b"start\r\n", tb)


def test_empty_program():
    """handles a program that printed nothing"""
    assert split_raw_reply(b"OK\x04\x04>") == (b"", b"")


def test_prompt_signs_in_output():
    """keeps > characters that the program printed"""
    assert split_raw_reply(b"OK>>> 3 > 2\r\n\x04\x04>") == (b">>> 3 > 2\r\n", b"")


def test_bad_frames():
    """refuses replies that are not complete frames"""
    assert raises_value_error(b"raw REPL; CTRL-B to exit\r\n>")
    assert raises_value_error(b"OK42\r\n\x04")
    assert raises_value_error(b"OK42\r\n\x04\x04")
    assert raises_value_error(b"OK42\r\n\x04\x04>>")
A hint

Check the start first: data.startswith(b"OK"). Then cut the rest at the 0x04 bytes with data[2:].split(b"\x04"): a complete frame gives exactly three pieces, the output, the error part and what follows the second 0x04, which must be b">".

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then review the four ideas of this lesson, from the firmware file to the raw REPL. The self-check starts where your answers so far are weakest, and it explains every choice the moment you make it:

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 12 Which file type do the Pico boards install?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: .uf2

    The Pico, Pico W, Pico 2 and Pico 2 W pages all install by copying a .uf2 file to the drive that appears in bootloader mode.

  2. Question 2 of 12 A board carries an ESP32 SOLO module, which has a single-core ESP32. Which build does the download page point you to?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: ESP32_GENERIC-UNICORE

    The ESP32_GENERIC page names a variant for each special case: SPIRAM, D2WD, UNICORE and OTA. Otherwise the plain build is the one.

  3. Question 3 of 12 A lab board breaks, and you must set up a replacement that behaves exactly like it. Which record from the day you installed it helps most?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: The firmware file name and its SHA-256, kept with a copy of the board's files

    The same version comes as different builds and variants. The exact file name pins the build, the SHA-256 pins the file itself, and a copy of the filesystem brings back everything that is not firmware.

  4. Question 4 of 12 How do you put a Pico into its bootloader, ready for a .uf2 file?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: Hold BOOTSEL while you plug it in, or run machine.bootloader() at its prompt

    In bootloader mode the board appears as a drive named RPI-RP2 (RP2350 on a Pico 2). Copying the .uf2 file onto it installs the firmware, and the board resets by itself.

  5. Question 5 of 12 You write MicroPython to an ESP32 that held other firmware, without erasing. It prints that the filesystem appears to be corrupted, again every few seconds. What fixes it?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: Erase the whole flash with esptool.py erase_flash, then write the image again

    The esp32 port's inisetup.py checks that the first block of the filesystem area is erased before it formats it. Old data there makes it print the corrupted-filesystem message in a loop, which is why the page asks for an erase before a first install.

  6. Question 6 of 12 After installing, how do you confirm from the board itself which build and version it runs?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Read sys.implementation at its prompt, its version and _build fields

    sys.implementation gives the version, such as (1, 29, 0, ''), and _build names the board build, such as 'RPI_PICO_W'. Together they say exactly which firmware runs.

  7. Question 7 of 12 A loop at the REPL prints numbers without end. Which key stops it?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Ctrl-C, which raises KeyboardInterrupt in the loop

    The REPL docs show a loop counting to a million, stopped by Ctrl-C with a KeyboardInterrupt traceback, and the prompt back.

  8. Question 8 of 12 At the REPL, 1 + 2 + 3 + 4 + 5 prints 15. You then type x = _. What is x?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: 15

    MicroPython stores the result of the previous expression in _, so x = _ keeps a value that you forgot to name.

  9. Question 9 of 12 You start typing impo, change your mind and press Ctrl-E to paste a script. What happens, and what should you have done?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: The cursor jumps to the end of the line; clear the line with Ctrl-C first, then press Ctrl-E

    MicroPython's line editor treats Ctrl-A to Ctrl-E as commands only when the line is empty. Otherwise they edit the line, so the paste would land in the normal REPL with its auto-indent.

  10. Question 10 of 12 Which key switches the board to the raw REPL, and which byte ends each block of code sent to it?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: Ctrl-A switches, and Ctrl-D (0x04) ends each block

    Ctrl-A, then the code, then Ctrl-D: the board answers OK and runs it. Ctrl-B returns to the friendly REPL.

  11. Question 11 of 12 Your tool reads the reply b'OK\x04\x04>'. What did the program do?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: It printed nothing and ended without an error

    A program such as x = 1 prints nothing and raises nothing, so its reply is the frame alone: OK, 0x04, 0x04 and the prompt.

  12. Question 12 of 12 mpremote exec printed your program's output, then exited with status 1. What does that tell you?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: The program raised an exception after printing, so the reply's error part was not empty

    The frame separates output from errors, so mpremote can pass your output through and still report the failure, with the traceback and a non-zero status that scripts can test.

Interview questions

Warm-up (fresher): At the MicroPython REPL, what do Ctrl-C and Ctrl-D do, and when does Ctrl-D not reset?

Ctrl-C interrupts: it raises KeyboardInterrupt in the running program, which stops it and returns to the prompt unless the program catches the exception, and at the prompt it cancels the line being typed. Ctrl-D at an empty prompt does a soft reset: the interpreter restarts, every variable and imported module is cleared, and the boot files run again, while the USB serial connection stays open. Ctrl-D does not reset when the line holds text, because the control keys of the REPL work as commands only on an empty line, and in paste mode, where it ends the paste and runs the code. In the raw REPL, Ctrl-D after code runs that code, and Ctrl-D with no code is the soft reset that mpremote uses before its first command.

Two longer interview questions of this module start from this lesson:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    After a repair, one Pico W of a fleet of 50 starts normally but fails at import network with ImportError. How do you establish which firmware it runs, fix it, and make sure no board of the fleet ends up with the wrong file again?

    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

    Your own Python script drives boards through the raw REPL over a serial port. On some runs it hangs forever after sending a program. Walk through the protocol, say where a host can wait forever, and explain how you would make the script robust.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

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

  1. What happens when a Pico W runs the plain RPI_PICO firmware?

    It starts and runs, but it has no network module and no Wi-Fi driver, so the first import network raises ImportError. Install the RPI_PICO_W file instead.

  2. Which ESP32_GENERIC download suits a WROVER module or any board with PSRAM?

    The SPIRAM variant, ESP32_GENERIC-SPIRAM, which the download page recommends for every board with SPIRAM. The plain build is for most boards with 4 MiB of flash or more.

  3. How is MicroPython installed on a Pico?

    Hold BOOTSEL while you plug the cable in: the chip's built-in bootloader appears as a USB drive. Copy the board's .uf2 file onto it, and the bootloader writes it to flash and resets the board by itself.

  4. Why erase an ESP32's flash before its first MicroPython install?

    At its first start MicroPython formats the filesystem's block only if that block is empty, and data left by other firmware makes it refuse. An update of a board that already runs MicroPython keeps the filesystem and your files.

  5. What does Ctrl-D do at an empty REPL prompt?

    A soft reset: variables and imports are cleared and the boot files run again, while the USB connection stays open. With text on the line it does not reset, because the control keys act only on an empty line.

  6. Why press Ctrl-E before pasting code into the REPL?

    Paste mode takes the text as it is. The normal REPL indents after every colon by itself, so code that is already indented gets indented twice and fails with IndentationError. Ctrl-D then runs the paste.

  7. How does the raw REPL frame every reply?

    OK, the output, a 0x04 byte, the error output, another 0x04, then the > prompt. Tools such as mpremote rely on it to tell where your output ends and a traceback starts.

  8. What does _ hold at the MicroPython REPL?

    The value of the last expression, so x = _ keeps a result you forgot to name.

Key takeaways

  • The firmware file must match the board: RPI_PICO runs on a Pico W but has no network module, and an ESP32 needs the variant for its RAM, flash and cores.
  • A Pico installs by copying a .uf2 file to the drive its bootloader shows; an original ESP32 installs with esptool, an erase and a write at 0x1000.
  • sys.implementation confirms what was installed, and a record of the file name and its SHA-256 lets you install the same firmware again.
  • At an empty prompt Ctrl-C gives a new prompt, Ctrl-D soft-resets and Ctrl-E opens paste mode; while a program runs, Ctrl-C stops it with KeyboardInterrupt.
  • The raw REPL frames every reply as OK, the output, 0x04, the error output, 0x04 and >, which lets mpremote and editors run code and tell output from errors.

Terms in this lesson

bootloader
A small program that runs at reset, before the firmware, and can receive new firmware: the RP2040's USB drive mode and the ESP32's serial ROM bootloader are two.
paste mode
A REPL mode, opened with Ctrl-E at an empty prompt, that takes pasted text as it is, with no auto-indent, and compiles it as one block when you press Ctrl-D.
raw REPL
The REPL's mode for programs, entered with Ctrl-A: it echoes nothing, runs the code it received when Ctrl-D arrives, and frames every reply with OK and two 0x04 bytes.
REPL
Read-eval-print loop: the interactive >>> prompt that MicroPython runs on a board's USB serial port or UART. Each line you type is compiled, run, and its result printed.
UF2 file
A firmware file made of 512-byte blocks that a board's bootloader writes to flash when the file is copied onto the USB drive it shows. Pico boards install MicroPython this way.

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.