Recalibrate
Five mental-model shifts
If you internalize these, everything else in embedded Python is just details.
The board has its own filesystem
A small flash drive inside the chip. Your code runs there, not on your laptop —
you edit locally, then copy files over USB. pip and .venv
never touch the board.
main.py runs at power-on
No terminal, no python main.py. Whatever is named main.py on the
board's filesystem runs automatically every time power arrives. That's what makes it an
appliance.
The REPL is a live wire into the chip
Over the same USB cable you get a Python prompt running on the board. Poke pins, test ideas, read tracebacks — interactively.
Two kinds of reset
Soft reset (Ctrl+D in the REPL) restarts your program in place. Hard reset (the RESET button, or re-plugging) restarts the whole chip. You'll use both, constantly.
import searches the board
Not your project folder. If main.py needs a driver or a font, that file must
already be on the board's flash — which is why deploys copy the whole
src/ directory.
Batteries (mostly) included
MicroPython ships hardware modules like machine (pins, SPI),
network (Wi-Fi) and ntptime — plus familiar ones like
time and json, slimmed down.
Kit
Two tools, installed once
esptool
Espressif's utility that writes firmware images into the chip's flash. You'll use it
exactly twice: erase_flash, then write_flash.
mpremote
MicroPython's official Swiss-army CLI: opens the REPL, copies files, mounts folders, resets the board. Every day-to-day command in this project wraps it.
$ uv tool install esptool
$ uv tool install mpremote
One-time surgery
Flash MicroPython onto the board
New boards ship with CircuitPython (Adafruit's cousin of MicroPython). This project uses MicroPython, so the first job is replacing the firmware. Do it once and forget it.
Download the firmware
Grab the latest Standard .bin from the
ESP32_GENERIC_S3 download page.
Enter bootloader mode
Hold D0, tap RESET, release D0. The screen goes dark and the board re-appears as a bare ROM bootloader, ready to accept firmware. (This is why D0 is also labeled BOOT.)
Erase, then write
Find your serial port, wipe the old firmware, and write the new one at address
0:
# macOS: ls /dev/cu.usbmodem* · Linux: ls /dev/ttyACM*
$ esptool.py --chip esp32s3 --port /dev/cu.usbmodem1101 erase_flash
$ esptool.py --chip esp32s3 --port /dev/cu.usbmodem1101 --baud 460800 \
write_flash 0 ESP32_GENERIC_S3-*.bin
Press RESET
The board reboots into MicroPython. Nothing shows on screen yet — there's no program installed. The proof of life is in the REPL, next.
Don't download the “Octal-SPIRAM” build — this board has quad PSRAM and the octal
build won't boot. And note the write address for the ESP32-S3 is 0, not the
0x1000 you'll see in older ESP32 tutorials.
First contact
Say hello over the REPL
Connect to the board and you're typing Python into the chip. Light the NeoPixel — note how the power-gate pin from chapter 01 shows up immediately:
$ mpremote
Connected to MicroPython at /dev/cu.usbmodem1101
>>> print("hello from the board")
hello from the board
>>> import machine, neopixel
>>> machine.Pin(21, machine.Pin.OUT, value=1) # switch on the NeoPixel's power rail
>>> np = neopixel.NeoPixel(machine.Pin(33), 1)
>>> np[0] = (0, 40, 0); np.write() # and it glows green
Three keystrokes run the whole show in the REPL:
Interrupt the running program and drop to the prompt — your escape hatch from any loop.
Soft reset: re-runs boot.py
then main.py without cutting power.
Exit mpremote and hand the
serial port back to your terminal.
Only one program at a time can hold the serial port. If mpremote says the
port is busy or “could not enter raw repl”, close Thonny / other REPLs and retry.
Rhythm
The dev loop
Two shortcuts collapse the loop even further while you iterate:
mpremote run file.py
Pipe one script straight into RAM and run it — nothing written to the board's flash. Great for quick experiments.
mpremote mount src
Make the board see your laptop folder as its filesystem. Edit locally, run
import main in the REPL, repeat — zero copying while you iterate.
Go deeper