MicroPython for Microcontrollers Module 1 – Getting started: boards, firmware, the REPL and mpremote
boot.py, main.py and a layout that survives resets
The MicroPython boot sequence from _boot.py to the REPL, a thin main.py with the app in /lib, what soft and hard resets keep, and a main.py that recovers.
What you will learn
- Describe the boot sequence, _boot.py, boot.py, main.py and the REPL, and when main.py is skipped
- Organise a project with a thin main.py and its modules in /lib, and predict which file an import finds
- Distinguish what a soft reset and a hard reset keep, and which calls cause each
- Write a main.py that recovers from a crashing app without locking you out of the board
Before you start
What you’ll learn · Before you start
What you will learn
- Describe the boot sequence, _boot.py, boot.py, main.py and the REPL, and when main.py is skipped
- Organise a project with a thin main.py and its modules in /lib, and predict which file an import finds
- Distinguish what a soft reset and a hard reset keep, and which calls cause each
- Write a main.py that recovers from a crashing app without locking you out of the board
Before you start
On this page
Save a main.py that crashes on its first line and then resets the board, and the board starts again, crashes and
resets, over and over, dropping off your computer’s USB each time. Every MicroPython board starts the same way after a
reset, and two files you control, boot.py and main.py, decide whether that start leads to a working app, a prompt
or a loop.
This lesson follows the start of a board step by step, lays out a project so that a reset never runs stale code,
compares what soft and hard resets keep, and ends with a main.py that cannot lock you out, whatever the app does.
Every reset runs the same start: boot.py, main.py, then the REPL
Copy a main.py that prints hello to a Pico, open its REPL and press Ctrl-D: hello appears, and only then the
prompt. Whatever starts the board, a hard reset: A complete reset of the microcontroller, as at power-on: all hardware state is cleared, except that the RTC may keep its time. machine.reset(), the reset button and a watchdog cause one. such as power-on or 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. such as Ctrl-D, the firmware runs the same sequence. Step through it:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The reset. Power-on, the reset button, machine.reset(), a watchdog or waking from deep sleep cause a hard reset; Ctrl-D, machine.soft_reset() and sys.exit() a soft one. Both lead into the same sequence.
- _boot.py. First the firmware runs _boot.py, a script frozen into it. On the rp2 port it mounts the flash filesystem at /, and formats it first on a new board.
- boot.py. Then boot.py, if the filesystem has one, for one-off set-up such as the network. It must end, because the start goes on only when it does.
- USB starts late. On the rp2 port, as on every port with machine.USBDevice, USB starts only now, after boot.py, so what boot.py prints may not reach your terminal.
- Skipped main.py. If boot.py raised an exception, the firmware skips main.py and starts the REPL. A soft reset from the raw REPL skips it too, which is how mpremote reaches a board whose main.py never returns.
- main.py. Otherwise main.py runs, and every global that boot.py set is still set. It may loop forever; Ctrl-C on the serial port stops it with KeyboardInterrupt.
- The REPL. When main.py ends, or crashes with an exception it does not catch, the REPL starts, with every global of boot.py and main.py still set.
boot.py is for set-up that must happen before anything else and then finish, such as configuring hardware that
starts with a different setting. The documentation adds a warning for some ports, the rp2 port among them: USB starts
only after boot.py exits, so what it prints may not reach your terminal. Many projects need no boot.py at all and
put the set-up at the top of main.py. main.py is the app. It may loop forever, and both files share one set of
globals, which is easy to forget when a name in main.py seems to come from nowhere.
The next program models three starts of a board by the rules of the rp2 port’s main loop: after a normal reset,
after a boot.py that raises, and after a soft reset from 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. More in “Install firmware and use the REPL”, the way mpremote
resets a board:
"""The start of a board, three times, by the rules of the rp2 port's main loop (ports/rp2/main.c).
After the frozen _boot.py the firmware runs boot.py, then main.py, all in one set of globals. main.py runs only when
boot.py raised no error and the board is in the friendly REPL; a soft reset from the raw REPL skips it. Here the two
files are strings and exec() stands in for the firmware's runner.
"""
FILES = {
"boot.py": "wifi_ssid = 'lab-net'\nprint('boot.py ran')\n",
"main.py": "print('main.py ran; wifi_ssid is', wifi_ssid)\n",
}
def run_file(name, files, scope):
"""Run one file in the board's globals; return True if it raised no exception (a missing file counts as one)."""
if name not in files:
return True
try:
exec(files[name], scope)
return True
except Exception as e:
print(f" {name} raised {e!r}")
return False
def start(files, raw_repl=False):
scope = {"__name__": "__main__"}
boot_ok = run_file("boot.py", files, scope)
if boot_ok and not raw_repl:
run_file("main.py", files, scope)
else:
print(" main.py skipped")
shared = sorted(k for k in scope if not k.startswith("__"))
print(f" REPL starts, with the globals {shared}")
print("1. A reset:")
start(FILES)
print("2. A reset after a broken boot.py:")
start({**FILES, "boot.py": "raise OSError('SD card not found')\n"})
print("3. A soft reset from the raw REPL, as mpremote does:")
start(FILES, raw_repl=True) Output
1. A reset:
boot.py ran
main.py ran; wifi_ssid is lab-net
REPL starts, with the globals ['wifi_ssid']
2. A reset after a broken boot.py:
boot.py raised OSError('SD card not found')
main.py skipped
REPL starts, with the globals []
3. A soft reset from the raw REPL, as mpremote does:
boot.py ran
main.py skipped
REPL starts, with the globals ['wifi_ssid']
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 boot_sim.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The second start is the trap: an error in boot.py costs you main.py, without any message of its own beyond the
traceback. The third explains a puzzle from the previous lesson: mpremote soft-reset resets in raw mode, so your new
main.py does not start. To watch it start, reset with mpremote reset, or press Ctrl-D in mpremote repl, which
soft-resets from the friendly REPL.
A boot.py that never ends
A while True: loop in boot.py keeps the start from ever reaching main.py, and on the rp2 port USB never
starts either, so the board looks dead. Put loops in main.py, where Ctrl-C can stop them, and keep boot.py
short enough to finish.
Checkpoint
Check: The boot sequence
A thin main.py, with the app’s modules in /lib
The reset and boot page suggests a main.py that only imports the app and starts it, import my_app and
my_app.main(). The app itself lives in modules, and those belong in /lib. On the Pico, sys.path is
['', '.frozen', '/lib'], from MicroPython’s runtime and the rp2 port’s main loop: the current folder first, which is
/ after a normal start, then the frozen module: A module compiled into the firmware image when the firmware is built. On most ports its bytecode runs straight from flash, so it takes no RAM for the code. More in “What MicroPython is and where it runs” inside the firmware, then /lib.
In each folder name.py is tried before name.mpy, and the first match wins. The next program applies those rules
to a board with a tidy /lib and one stray file at the root:
"""Which file an import finds on a Pico, by the rules MicroPython documents.
sys.path on the rp2 port is ['', '.frozen', '/lib']: the current folder (/ after a normal boot), then the modules
frozen into the firmware, then /lib. In each folder name.py is tried before name.mpy, and the first match wins.
"""
SYS_PATH = ["", ".frozen", "/lib"]
# Frozen into the RPI_PICO firmware by its manifest (ports/rp2/boards/manifest.py).
FROZEN = {"asyncio", "dht", "ds18x20", "neopixel", "onewire"}
# The board's files in this example: a tidy /lib, plus a stray old sensor.py left at the root.
BOARD = {
"/main.py", "/sensor.py",
"/lib/app.py", "/lib/sensor.py", "/lib/neopixel.py",
"/lib/config.py", "/lib/config.mpy", "/lib/units.mpy",
}
def find(name, cwd="/"):
"""The file that `import name` loads, or None for an ImportError."""
for entry in SYS_PATH:
if entry == ".frozen":
if name in FROZEN:
return f"{name}, frozen into the firmware"
continue
folder = cwd if entry == "" else entry
for ext in (".py", ".mpy"):
path = folder.rstrip("/") + "/" + name + ext
if path in BOARD:
return path
return None
for name in ["app", "sensor", "neopixel", "config", "units", "logging"]:
print(f"import {name:<9} -> {find(name) or 'ImportError: no module named ' + repr(name)}") Output
import app -> /lib/app.py import sensor -> /sensor.py import neopixel -> neopixel, frozen into the firmware import config -> /lib/config.py import units -> /lib/units.mpy import logging -> ImportError: no module named 'logging'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 which_module.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Two results surprise. import sensor finds the old /sensor.py at the root, because the current folder comes before
/lib, so the new /lib/sensor.py is never used. And import neopixel never reaches /lib/neopixel.py, because the
Pico firmware freezes a neopixel module of its own, and .frozen comes before /lib.
Names that nothing else uses
Give your modules names that no built-in or frozen module has: help('modules') lists both. lab_neopixel.py is
found, while neopixel.py in /lib never is on a Pico.
A layout that avoids both traps puts nothing but the boot files at the root:
| Path on the board | What it holds |
|---|---|
/main.py |
A few lines: the recovery check of the last section, then the app’s start |
/boot.py |
Optional: set-up that must run first and finish |
/lib/app.py |
The app, with a main() function |
/lib/... |
Your other modules, and packages that mip installs there |
/data/ |
Files the app writes, such as logs and settings, kept apart from code |
With the root down to the two boot files and folders, nothing there can shadow /lib, and replacing the app means
replacing files in /lib while main.py stays the same.
Keep main.py itself as source. The reset and boot page mentions precompiled main.mpy and boot.mpy files, but
the main loops of the rp2 and esp32 ports in 1.29.0 look for boot.py and main.py by those names only, so a
main.mpy on the filesystem never runs at a reset. Precompile the modules in /lib that main.py imports instead.
Key idea
Keep the root down to boot.py and main.py and every module in /lib, under a name of its own: the current folder and the frozen modules come before /lib in sys.path.
Checkpoint
Check: A thin main.py, modules in /lib
Soft and hard resets keep different things
The two kinds of reset run the same sequence, but they start from different states. The reset and boot documentation lists what each leaves behind:
| What | After a soft reset | After a hard reset |
|---|---|---|
| Python variables, objects and imported modules | Cleared | Cleared |
Most peripherals set up through machine |
Reset, except pin modes on most ports; Pin.irq() always reset |
Reset |
| Open files and sockets | Closed; the filesystem is left valid | Gone with the reset |
| Wi-Fi and other network connections | Still up, with their IP address | Down; the app connects again |
| The USB serial REPL | Continues, as if nothing happened | Drops off and comes back |
| The RTC’s time | Kept | May be kept, depending on the port |
| What starts one | Ctrl-D, machine.soft_reset(), sys.exit() |
Power-on, a reset button, machine.reset(), a watchdog, waking from deep sleep |
machine.reset_cause() returns the cause of the last reset that the port can tell, for example
machine.PWRON_RESET or machine.WDT_RESET, so an app can log why it restarted. Ports tell different things: the
esp32 port returns machine.SOFT_RESET after a soft reset, while the rp2 port knows only those two causes, so after
Ctrl-D a Pico still reports the hard reset that came before.
The difference between the two resets matters in tests: a board that passes after a power cycle can fail after Ctrl-D, because a pin still drives the value the last run left on it, or because a network connection the app expects to open is already open.
So the kind of reset before a test decides what the test proves. A test that only ever follows a power cycle never meets a pin that the previous run left high, or a Wi-Fi connection that is already up. Run start-up checks after both kinds of reset, and after the raw-mode soft reset that mpremote does, before you rely on them.
sys.exit() resets since 1.24
Since MicroPython 1.24, an unhandled SystemExit on a board, which sys.exit() raises, ends the script with a soft
reset instead of leaving you at the REPL. On the unix and windows ports it still ends the program, as in CPython.
Checkpoint
Check: What soft and hard resets keep
A recovery path keeps a crashing app from locking you out
The reset and boot page shows a main.py that runs the app inside try, prints any exception with
sys.print_exception() and then calls machine.reset(). That is a good start, because a board in the field should
restart rather than sit at a prompt nobody reads. It has one weakness: an app that fails at every start makes the
board reset forever, and a hard reset takes USB down each time, so a terminal loses the board at every reset. The
next program models such a board: five starts under that main.py, and five under one that counts its restarts in a
small file:
"""Five boots of a board whose app always fails at start, under two versions of main.py.
The first is the pattern of MicroPython's reset and boot page: print the error, then machine.reset(). The second
counts its restarts in a small file that survives a reset, resets at most three times and then stays at the REPL.
Here the board is simulated: Reset stands for machine.reset(), and the dict `flash` for the file in its flash.
"""
class Reset(Exception):
"""Stands for machine.reset(): this boot ends, and the next one starts from the top."""
def app_main():
raise OSError("sensor not found on the I2C bus")
def main_reset_always(flash):
try:
app_main()
except Exception as e:
print(" Fatal error in main:", repr(e))
raise Reset
def main_bounded(flash, limit=3):
try:
app_main()
flash["restarts"] = 0
except Exception as e:
print(" crash:", repr(e))
if flash["restarts"] < limit:
flash["restarts"] += 1
print(f" restart {flash['restarts']} of {limit}")
raise Reset
print(" too many restarts: staying at the REPL")
for main in (main_reset_always, main_bounded):
print(main.__name__)
flash = {"restarts": 0}
for boot in range(1, 6):
try:
main(flash)
print(f"boot {boot}: main.py ended, the REPL starts")
break
except Reset:
print(f"boot {boot}: reset")
print() Output
main_reset_always
Fatal error in main: OSError('sensor not found on the I2C bus')
boot 1: reset
Fatal error in main: OSError('sensor not found on the I2C bus')
boot 2: reset
Fatal error in main: OSError('sensor not found on the I2C bus')
boot 3: reset
Fatal error in main: OSError('sensor not found on the I2C bus')
boot 4: reset
Fatal error in main: OSError('sensor not found on the I2C bus')
boot 5: reset
main_bounded
crash: OSError('sensor not found on the I2C bus')
restart 1 of 3
boot 1: reset
crash: OSError('sensor not found on the I2C bus')
restart 2 of 3
boot 2: reset
crash: OSError('sensor not found on the I2C bus')
restart 3 of 3
boot 3: reset
crash: OSError('sensor not found on the I2C bus')
too many restarts: staying at the REPL
boot 4: main.py ended, the REPL starts
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 crash_loop.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The first main.py never stops resetting. The second resets three times, which gives a passing fault, such as a
sensor that needed a moment, its chances, and then stays at the REPL with the error printed. Step through the
decisions of that second main.py, with a recovery button checked before anything else:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The button first. At every boot, main.py first reads a recovery button. Held down, it skips the app, and the board waits at the REPL, where you can fix or remove the app's files.
- Run the app. Not held, main.py runs app.main() inside try and except Exception. KeyboardInterrupt is not a subclass of Exception, so Ctrl-C still stops the app and reaches the REPL.
- The app fails. The app raises OSError, say a sensor missing from the I2C bus. main.py catches it and prints it with sys.print_exception, and can append it to a log file for later.
- Count the restarts. Then it reads how many restarts it has made in a row, a count kept in a small file in flash, which survives the reset that follows.
- Restart, while it may. With restarts left, it adds one to the count and calls machine.reset(). A passing fault, a sensor that needed a moment, gets another chance at the next boot.
- The next boot. The board starts again from the top, with the button check, and the count is still in its file.
- Stop, when none are left. After the third restart the count is used up, so main.py returns instead of resetting. The board stays at the REPL with the error printed, instead of resetting every few seconds.
The button is your own safe mode: A start that skips the app, and on some ports boot.py and main.py as well, and goes straight to the REPL, so that a board whose code fails at start can be repaired.. Some ports offer one of their own, a start that skips
boot.py and main.py, but the reset and boot page lists only port-specific instructions, and for the rp2 and esp32
ports those are factory resets that erase everything. A button you check yourself costs one pin and keeps your files.
If a board is already soft-bricked: A board that is not damaged but no longer reaches a usable prompt at start, for example because boot.py never ends or the code takes over the REPL's serial port., try the gentle ways first. mpremote stops a running
main.py with Ctrl-C and resets in raw mode, which skips it, so mpremote rm :main.py works on a board whose
main.py loops or hangs; in a reset loop it may take a few tries to catch the board between two resets. At a
prompt, import os and os.remove('main.py') do the same. Only when no prompt can be reached does the factory reset
remain: erase the whole flash, install the firmware again and copy your files back.
Catching everything in main.py
except: with nothing after it, or except BaseException, also catches KeyboardInterrupt, so Ctrl-C can no
longer stop the app and you lose the easiest way back to the prompt. Catch Exception: in Python’s exception
hierarchy KeyboardInterrupt is not one of its subclasses.
Checkpoint
Check: A main.py that recovers
Practise: write the supervisor
Write the function that the second main.py calls at every start: the button check, the app inside try, the log
line, and a restart count that stops after three. The sample tests use a fake board that records what it is asked to
do:
Exercise · Medium · Python
A main.py supervisor that cannot lock you out
Write supervise(app, log, reset, button_pressed, state, max_restarts=3) in supervisor.py, the function a main.py calls once per boot. app is the app's main function, log(text) records a line, reset() restarts the board (on a board, machine.reset()), button_pressed() tells whether the recovery button is held, and state is a dictionary with the key "restarts" that survives a reset (on a board, a small file). Return what happened:
- If
button_pressed()is true, do not start the app: log"safe mode: app not started"and return"safe". - Run
app(). If it returns, setstate["restarts"]to 0 and return"ended". - If it raises an
Exception, log"crash: <type>: <message>", for example"crash: OSError: no sensor". Then, ifstate["restarts"]is belowmax_restarts, add 1 to it, callreset()and return"reset"; otherwise log"too many restarts: staying at the REPL"and return"stopped"without callingreset(). - Let
KeyboardInterruptthrough, so that Ctrl-C still reaches the REPL: it is not anException.
Starter code · supervisor.py
def supervise(app, log, reset, button_pressed, state, max_restarts=3):
"""Run app once for this boot; return "safe", "ended", "reset" or "stopped"."""
app()
return "ended" The sample tests · test_supervisor.py
from supervisor import supervise
class Board:
"""Records what supervise asked the board to do."""
def __init__(self, held=False):
self.lines = []
self.resets = 0
self.held = held
def log(self, text):
self.lines.append(text)
def reset(self):
self.resets += 1
def button(self):
return self.held
def failing():
raise OSError("no sensor")
def test_safe_mode():
"""starts nothing while the button is held"""
board, ran = Board(held=True), []
result = supervise(lambda: ran.append(1), board.log, board.reset, board.button, {"restarts": 0})
assert result == "safe" and ran == [] and board.resets == 0
assert board.lines == ["safe mode: app not started"]
def test_app_ends():
"""clears the restart count when the app returns"""
board, state = Board(), {"restarts": 2}
assert supervise(lambda: None, board.log, board.reset, board.button, state) == "ended"
assert state["restarts"] == 0 and board.resets == 0
def test_crash_resets():
"""logs a crash and resets while restarts are left"""
board, state = Board(), {"restarts": 0}
assert supervise(failing, board.log, board.reset, board.button, state) == "reset"
assert board.lines == ["crash: OSError: no sensor"]
assert board.resets == 1 and state["restarts"] == 1
def test_stops_at_limit():
"""stays at the REPL once max_restarts is used up"""
board, state = Board(), {"restarts": 3}
assert supervise(failing, board.log, board.reset, board.button, state, max_restarts=3) == "stopped"
assert board.resets == 0 and state["restarts"] == 3
assert board.lines == ["crash: OSError: no sensor", "too many restarts: staying at the REPL"]
def test_ctrl_c_reaches_the_repl():
"""lets KeyboardInterrupt through"""
board = Board()
def interrupted():
raise KeyboardInterrupt
try:
supervise(interrupted, board.log, board.reset, board.button, {"restarts": 0})
except KeyboardInterrupt:
assert board.resets == 0
return
raise AssertionError("KeyboardInterrupt was swallowed") A hint
Check the button before anything else and return early. Then wrap only the call of app() in try/except Exception as e: KeyboardInterrupt is not a subclass of Exception, so it passes through by itself. The log line is "crash: " + type(e).__name__ + ": " + str(e).
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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 try the self-check on the start sequence, the layout, the two resets and recovery. It picks the topic where your answers have been weakest, and each answer you pick is followed by its reason:
Practice that adapts to you
Practise this lesson
14 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What belongs in boot.py, and what belongs in main.py?
boot.py runs first after every reset and is for set-up that must happen before anything else and then end: setting up a piece of hardware, or on some boards the network. It must not loop, because the start goes on only after it ends, and on ports such as rp2 even USB starts only after boot.py, so its prints may not be seen. An exception in boot.py also makes the firmware skip main.py. main.py is the app: it runs next, may loop forever, and shares its globals with boot.py. A good main.py stays short, imports the app from /lib and starts it inside a try block, and many projects need no boot.py at all.
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.
-
Fresher to mid
After an update, a board in the field restarts every few seconds and you cannot get a prompt on it. How do you recover it without losing its data, and how should its main.py have been written so that a failing app can never lock you out?
-
Mid to senior
Your hardware test passes after a power cycle but fails after Ctrl-D: a relay that must start off is on, and the app's Wi-Fi set-up, written for a fresh board, misbehaves because the board is still connected. Explain what a soft reset keeps and clears, and how you make the start behave the same after every kind of reset.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
In what order does a MicroPython board start after any reset?
The frozen _boot.py, then boot.py, then main.py, then the REPL. An error in boot.py, or a soft reset from the raw REPL as mpremote does it, skips main.py.
-
Why must boot.py never loop forever?
The start reaches main.py only after boot.py ends, and on the rp2 port USB starts only after boot.py exits, so the board looks dead. Loops belong in main.py, where Ctrl-C can stop them.
-
What is sys.path on a Pico, and which file wins an import?
['', '.frozen', '/lib']: the current folder, then the frozen modules, then /lib, with name.py tried before name.mpy in each. A stray file at the root, or a frozen module, with that name wins over your file in /lib.
-
Does a main.mpy run at reset on the rp2 and esp32 ports of 1.29.0?
No: their main loops look for boot.py and main.py by those names only. Keep main.py as source, and precompile the modules in /lib that it imports.
-
What does a soft reset keep that a hard reset loses?
Network connections with their IP address, the USB serial REPL, pin modes on most ports and the RTC's time. Both kinds clear Python objects and imported modules.
-
What does sys.exit() do on a board since MicroPython 1.24?
An unhandled SystemExit ends the script with a soft reset instead of leaving you at the REPL. On the unix and windows ports it still ends the program, as in CPython.
-
Why should main.py catch Exception rather than everything?
A bare except: or except BaseException also catches KeyboardInterrupt, so Ctrl-C can no longer stop the app. KeyboardInterrupt is not a subclass of Exception.
-
How do you remove a main.py that loops or hangs?
mpremote rm :main.py: mpremote stops the program with Ctrl-C and soft-resets in raw mode, which skips main.py. In a reset loop it may take a few tries to catch the board between two resets.
Key takeaways
- After every reset the firmware runs _boot.py, boot.py and main.py, then the REPL; an error in boot.py and a soft reset from the raw REPL both skip main.py.
- On the Pico, sys.path is the current folder, then .frozen, then /lib, so a stray root file or a frozen module of the same name wins over your file in /lib.
- A soft reset clears Python and most peripherals but keeps network connections, pin modes and the REPL; a hard reset clears almost everything, and machine.reset_cause() reports why the board last reset, as far as its port can tell.
- A main.py that checks a recovery button, logs crashes and limits its restarts with a count in flash can never lock you out of the board.
Terms in this lesson
- hard reset
- A complete reset of the microcontroller, as at power-on: all hardware state is cleared, except that the RTC may keep its time. machine.reset(), the reset button and a watchdog cause one.
- safe mode
- A start that skips the app, and on some ports boot.py and main.py as well, and goes straight to the REPL, so that a board whose code fails at start can be repaired.
- 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.
- soft-bricked
- A board that is not damaged but no longer reaches a usable prompt at start, for example because boot.py never ends or the code takes over the REPL's serial port.
References
- Reset and Boot Sequence (MicroPython project)
- The rp2 port's main loop (ports/rp2/main.c, v1.29.0) (MicroPython project)
- The esp32 port's main loop (ports/esp32/main.c, v1.29.0) (MicroPython project)
- The default sys.path (py/runtime.c, v1.29.0) (MicroPython project)
- The rp2 port's frozen modules (ports/rp2/boards/manifest.py, v1.29.0) (MicroPython project)
- sys – system specific functions (MicroPython project)
- MicroPython .mpy files (MicroPython project)
- machine – functions related to the hardware (MicroPython project)
- reset_cause() on the rp2 port (ports/rp2/modmachine.c, v1.29.0) (MicroPython project)
- reset_cause() on the esp32 port (ports/esp32/modmachine.c, v1.29.0) (MicroPython project)
- Working with filesystems (MicroPython project)
- MicroPython v1.24.0 release notes (sys.exit and soft reset) (MicroPython project)
- Factory reset (RP2xxx) (MicroPython project)
- Factory reset (ESP32) (MicroPython project)
- MicroPython remote control: mpremote (MicroPython project)
- Built-in Exceptions: exception hierarchy (Python Software Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress