MicroPython on ESP32 and Raspberry Pi Pico W: The Complete Guide
MicroPython lets you program supported microcontrollers with a Python-like language while keeping direct access to GPIO, I²C, SPI, PWM, timers, and networking. The ESP32 family and Raspberry Pi Pico W are both popular for connected electronics, but they use different chips, firmware builds, pin layouts, and wireless capabilities. A script that works on one board may need small but important changes on the other.
This guide helps you choose a board, install the correct firmware, run a first program, connect sensors, use Wi-Fi where available, and troubleshoot common errors. It separates portable MicroPython concepts from board-specific details so you can avoid assuming that every API or pin assignment is identical.
1. ESP32 or Pico W?
ESP32 is a family of microcontrollers with several variants. Many classic ESP32 development boards include 2.4 GHz Wi-Fi and Bluetooth, while wireless features differ on variants such as ESP32-C3 and ESP32-S3. The Raspberry Pi Pico W uses the RP2040 microcontroller with a wireless chip for Wi-Fi. The original Raspberry Pi Pico without the W does not include onboard Wi-Fi. Confirm the exact product before choosing firmware or following a wireless tutorial.
Choose ESP32 when you want a widely available Wi-Fi microcontroller, many board and module options, and a large ecosystem of sensors and projects. Choose Pico W when you prefer the RP2040 platform, Raspberry Pi's board ecosystem, and MicroPython as a first-class learning path. Neither is universally better: memory, GPIO availability, radio features, USB behaviour, price, and peripheral requirements should guide the decision.
For both platforms, read the exact board pinout and voltage limits. GPIO is generally 3.3 V logic and should not be treated as 5 V tolerant. Never drive motors, relay coils, or other high-current loads directly from a GPIO. Use suitable driver circuitry and a separate supply where required.
2. Install the right MicroPython firmware
Download firmware for the exact board and chip variant from the official MicroPython downloads and documentation. ESP32 firmware differs by chip family and sometimes by flash size or build options. Pico W needs firmware intended for Pico W, not the image for an original non-wireless Pico. Do not select a firmware image merely because the board has a similar name.
Before flashing, save any scripts or configuration files stored on the board. Firmware installation methods can erase the filesystem. For a Pico W, the common bootloader workflow uses the BOOTSEL button while connecting USB and copying the correct UF2 file to the mounted drive. Follow the current official instructions for your board revision. ESP32 installation commonly uses a serial flashing tool or an IDE workflow that writes the appropriate firmware image; exact steps vary by board and operating system.
Use a known data-capable USB cable and a stable USB port. If a board does not appear, try another cable and port before assuming the board is damaged. Close other programs that may have the serial port open. After flashing, select the correct interpreter and serial device in Thonny or your preferred editor, then confirm that the REPL responds.
3. First REPL test and file layout
The REPL is an interactive prompt where you can run commands and inspect results. Start with a harmless test:
import sys
print(sys.implementation)
print("MicroPython is running")
If the command prints implementation details, the firmware is running and the editor is communicating with the board. Save a script as main.py only when you want it to run automatically at startup. While learning, keep a separate development file on your computer and copy it to the device after testing. An error in main.py can repeatedly restart or block the application, making it harder to recover.
MicroPython's available modules depend on the port and firmware build. Before relying on a package or API, check the documentation for the installed version. A desktop Python package may not fit the microcontroller's memory or runtime. Some drivers are pure Python and can be copied to the device; others rely on native features and must be supported by the firmware.
4. Blink an LED safely
The simplest hardware test is an LED with a current-limiting resistor. Select a GPIO documented as safe for output on your exact board. The example uses a placeholder pin number; replace it with a valid GPIO from your board's pinout. On a Pico W, physical header labels such as GP0 and GP1 refer to GPIO numbers; they are not the same as physical header positions. ESP32 development boards also print labels that may differ from physical pin numbering.
from machine import Pin
from time import sleep
LED_PIN = 2 # example only: verify for your board
led = Pin(LED_PIN, Pin.OUT)
while True:
led.value(1)
sleep(0.5)
led.value(0)
sleep(0.5)
Wire the external LED in series with a suitable resistor, with its cathode returning to GND. Verify polarity and resistor placement before powering the board. Some boards have an onboard LED connected to a special pin or controlled through different circuitry; if the example does not work, use an external LED or consult the board documentation. Stop the loop from the REPL before changing wiring.
5. Understand pins and peripherals
The machine.Pin API is common across MicroPython ports, but the available GPIO numbers and their restrictions are board-specific. Some pins are connected to flash, wireless circuitry, boot selection, onboard LEDs, or other functions. Always check the pinout and avoid using a pin just because it exists in an example. For input buttons, internal pull-ups or pull-downs may be available, but confirm the electrical design and active state before writing application logic.
A simple button input can use an internal pull-up when supported:
from machine import Pin
button = Pin(14, Pin.IN, Pin.PULL_UP) # example pin only
print(button.value()) # 1 released, 0 pressed for this wiring
Connect the button between the chosen GPIO and GND for this configuration. Real switches can bounce, producing several fast transitions from one press. For reliable event counting, add software debounce or appropriate hardware conditioning. Do not connect an input to a voltage above its specified maximum.
For I²C, SPI, and UART, verify both the peripheral's voltage and the board's signal pins. I²C usually needs pull-ups, and the pull-up voltage must be safe for the MCU. SPI requires the correct clock, data, chip-select, and sometimes reset lines. UART TX and RX cross between devices, and both devices need a common reference ground unless an appropriate isolated interface is used.
6. Read a sensor and handle errors
Start with a sensor whose exact model and driver are documented for your board. For example, a DHT-family sensor can be read using the built-in dht module on supported MicroPython builds. The DHT22 requires a suitable sampling interval; do not poll it rapidly in a tight loop. Check the module's supply, data-line pull-up requirements, pin assignment, and cable length before blaming the code.
import dht
from machine import Pin
from time import sleep
sensor = dht.DHT22(Pin(15)) # example GPIO; verify board pinout
while True:
try:
sensor.measure()
temperature_c = sensor.temperature()
humidity_pct = sensor.humidity()
print(temperature_c, humidity_pct)
except OSError as exc:
print("Sensor read failed:", exc)
sleep(2.5)
The example illustrates a pattern rather than a universal pin assignment. Some sensor modules include a pull-up resistor; bare sensors may need one according to the datasheet. Keep the polling interval at or above the device's specified minimum. Catch expected I/O errors, but do not silently replace failed readings with believable-looking values. Report the failure so a user can distinguish a missing sample from a real measurement.
When troubleshooting, isolate one variable at a time: run the smallest driver example, verify wiring, and compare the sensor model against the library documentation. A driver for one sensor family may not support a visually similar part.
7. Wi-Fi differences and safe testing
Wi-Fi support depends on the exact board and firmware. ESP32 boards with radio support and Raspberry Pi Pico W can generally use the MicroPython network API, while the original Pico does not have onboard Wi-Fi. Check the board-specific documentation before following a networking tutorial. For connection tests, use placeholder network details in examples and keep actual credentials in a private local copy. Do not publish credentials in source files or screenshots.
Use a bounded connection wait so a missing access point or incorrect password does not trap the application forever. Print the current network configuration only for local debugging, and remember that the address may change after a restart. A phone or computer testing a local server usually needs to be on the same trusted network; guest-network isolation can prevent devices from communicating even when both have internet access. Keep a simple demo server off the public internet.
8. PWM for LED brightness and servo signals
Pulse-width modulation (PWM) switches an output quickly to control its average effect. It can dim a suitable LED or provide a control signal to a servo driver. API details vary by MicroPython version and port; check whether your installed firmware supports duty_u16 and the chosen frequency.
from machine import Pin, PWM
from time import sleep
pwm = PWM(Pin(16)) # example GPIO only
pwm.freq(1000)
for duty in range(0, 65536, 4096):
pwm.duty_u16(duty)
sleep(0.05)
pwm.deinit()
Use a resistor with an external LED. Some devices require different frequencies or duty ranges. A servo control signal does not mean the GPIO can power the servo motor: use a suitable supply, connect grounds correctly, and avoid drawing motor current from the MCU pin. Servos can create supply dips that reset the board, so separate power and decoupling may be needed. If the code raises an attribute error for duty_u16, check the firmware API before assuming the board is defective.
9. Choose drivers and manage files
Many MicroPython projects need a driver for an OLED, environmental sensor, RTC, or other peripheral. First identify the exact component and interface. A module sold as an OLED may use SSD1306 or SH1106, and a breakout may expose I²C or SPI. Read the board labels and datasheet rather than choosing a library based only on appearance. Check the driver's supported MicroPython port and whether it requires extra files.
A pure-Python driver is often installed by copying its .py file to the device using the editor. Keep a copy on your computer and note the version or source. Avoid copying a large package collection onto a small filesystem without checking memory and storage. If an import fails, verify the filename, module path, spelling, and whether the file was saved to the device rather than only to the computer.
Keep application logic separated from hardware setup where practical. A small configuration section for pins and sensor addresses makes it easier to move a project between boards. Add comments wherever a value is board-specific. Use meaningful exceptions and printed diagnostics during development; once the project is stable, replace excessive debug output with concise status messages. Do not hide sensor failures by substituting plausible values.
10. Common errors and recovery
No serial port appears: try a known data cable, another USB port, and close any other program using the serial device. The REPL is unresponsive: stop a running loop or reset the board; if a startup script immediately locks up the device, follow the board's recovery steps and temporarily remove or rename the problematic startup file if possible. ImportError appears: verify that the module exists in the selected firmware or that its driver file is saved on the device. A GPIO error occurs: check whether the number is valid on that board and whether the pin is reserved.
Wi-Fi does not connect: verify the board supports Wi-Fi, the firmware matches the board, the network name and password are correct, and the access point supports the required band and security settings. Sensor values are unstable: check power, ground, pull-ups, wiring length, sampling interval, and driver compatibility. The board resets when an actuator starts: disconnect the load and test the MCU separately; motors and servos often need a supply designed for their current and appropriate decoupling.
Change one variable at a time and record the result. A useful debug note includes the board model, firmware version, pin mapping, driver version, error message, and a minimal reproduction script. This is more effective than repeatedly reflashing firmware without identifying the failure.