Learn

MicroPython on the Raspberry Pi Pico, with nothing to install

Beginner

Or see every lesson.

  • 455parts on the bench
  • 43boards running now
  • 1.00xreal time, on every board
Raspberry Pi Pico: MicroPython dims an LED from a knoblive0.000 s 0.00x
Click to open it in the editor
Press Run, then turn the knob. MicroPython reads GP26 with ADC and sets the PWM duty on GP15.

Mokxi runs MicroPython on its Raspberry Pi Pico. Put a Pico on the canvas, choose MicroPython (main.py) in its Firmware panel, write main.py and press Run. What runs is the official MicroPython v1.29.0 firmware for the Pico, the same file you would drag onto a real one, on Mokxi’s simulated RP2040, instruction by instruction.

When you press Run the board boots that firmware, and the computer on the end of its simulated USB cable copies your .py files onto the board’s filesystem the way Thonny does, then soft-reboots it. MicroPython prints MPY: soft reboot and runs main.py. Everything print() says shows in the Serial Monitor, and when main.py ends the REPL answers: type 6*7 and it says 42.

The circuit above is a potentiometer on GP26 and an LED on GP15. Turn the knob and the LED dims and brightens, and the Serial Monitor shows the knob’s position in percent.

The program: ADC in, PWM out

ADC(26) is the RP2040’s analog input on GP26, and read_u16() gives 0 at 0 V and 65535 at the 3.3 V reference. The converter itself has 12 bits, so the value moves in steps of about 16. PWM(Pin(15)) is the PWM slice behind GP15, and duty_u16() takes the same 0 to 65535 range, so the knob’s reading can go straight in as the duty.

With the wiper at 1.1 V, a third of the way up, read_u16() printed 21845 every time here: 1365 counts of 4095, which MicroPython stretches to 16 bits by shifting it up four bits and repeating its top four bits underneath, as on a real Pico. At a duty of 32768 the LED pin measured high for 50.0% of each 1 ms period.

main.py: the knob dimmer
# MicroPython on a Raspberry Pi Pico: a knob dims an LED.
# The potentiometer's wiper is on GP26 (ADC0), the LED on GP15 is driven by PWM.
from machine import ADC, Pin, PWM
import time

knob = ADC(26)            # read_u16(): 0 at 0 V, 65535 at 3.3 V
led = PWM(Pin(15))
led.freq(1000)            # 1 kHz, far too fast to see it flicker

last = -1
while True:
    value = knob.read_u16()
    led.duty_u16(value)   # the knob's position is the LED's duty cycle
    percent = value * 100 // 65535
    if percent != last:
        print("knob", percent, "%")
        last = percent
    time.sleep_ms(50)

Blink with machine.Pin, and how long a sleep really takes

The on-board LED of a Pico is GP25. The program below toggles it every 100 ms. Measured on the board’s pin, every period here was 200 ms to within a millisecond, because time.sleep_ms() waits on the RP2040’s own microsecond timer.

utime.ticks_us() reads that timer too. Four sleep_ms(250) calls in a row measured within half a millisecond of 1000000 microseconds every time here. machine.freq() says 125000000, the Pico’s 125 MHz.

main.py: blink the on-board LED
from machine import Pin
import time

led = Pin(25, Pin.OUT)   # the Pico's green LED

while True:
    led.toggle()
    time.sleep_ms(100)

A button with an interrupt

Pin.irq() runs a function when a pin changes, without the main loop watching it. Here a button from GP14 to ground uses the pin’s internal pull-up, so the pin reads 1 until the button is pressed, and IRQ_FALLING runs the handler once per press. Keep a handler short: add to a counter, set a flag, and let the main loop do the printing.

main.py: count presses with Pin.irq()
from machine import Pin
import time

presses = 0

def pressed(pin):
    global presses
    presses += 1

button = Pin(14, Pin.IN, Pin.PULL_UP)
button.irq(trigger=Pin.IRQ_FALLING, handler=pressed)

last = 0
while True:
    if presses != last:
        last = presses
        print("presses", presses)
    time.sleep_ms(5)

Porting an Arduino sketch to MicroPython

Most of an Arduino sketch has a one-line MicroPython twin. There is no setup() and loop(): the top of main.py runs once, and a while True: loop at the end runs for ever. Pins are objects made from the machine module, and times come from the time module.

Two things change size. analogRead() on an Uno gives 0 to 1023, and read_u16() gives 0 to 65535 whatever the converter’s own resolution. And a time difference is taken with time.ticks_diff(), which stays right when the counter wraps, rather than by subtracting.

Below is the Arduino AnalogReadSerial example, ported, on the knob dimmer’s circuit. With the knob halfway it printed 0 ms 32743 1.65 V, then the same reading at 500 ms, 1000 ms and on: 32743 is 2046 of 4095 counts, stretched to 16 bits.

Arduino calls and their MicroPython equivalents on the Pico
ArduinoMicroPython
pinMode(15, OUTPUT); digitalWrite(15, HIGH);led = Pin(15, Pin.OUT); led.value(1)
pinMode(14, INPUT_PULLUP); digitalRead(14)button = Pin(14, Pin.IN, Pin.PULL_UP); button.value()
analogRead(A0), 0 to 1023ADC(26).read_u16(), 0 to 65535
analogWrite(15, 128), 0 to 255PWM(Pin(15)).duty_u16(32768), 0 to 65535
delay(500)time.sleep_ms(500)
millis()time.ticks_ms()
Serial.println(x)print(x)
attachInterrupt(..., FALLING)Pin.irq(trigger=Pin.IRQ_FALLING, handler=...)
main.py: AnalogReadSerial, ported
# Arduino's AnalogReadSerial, ported to MicroPython
from machine import ADC
import time

pot = ADC(26)                     # analogRead(A0) becomes ADC(26)
start = time.ticks_ms()           # millis() becomes time.ticks_ms()

while True:
    value = pot.read_u16()        # 0 to 65535, not 0 to 1023
    volts = value * 3.3 / 65535
    print(time.ticks_diff(time.ticks_ms(), start), "ms", value, "%.2f V" % volts)
    time.sleep_ms(500)            # delay(500) becomes time.sleep_ms(500)

I2C: an OLED and a BME280 with machine.I2C

machine.I2C(0) and machine.I2C(1) are the RP2040’s two I2C blocks, and the parts on a bus answer at their addresses: here the SSD1306 OLED is on I2C0 (GP4 and GP5) and scan() finds it at 0x3C (60), and the BME280 is on I2C1 (GP6 and GP7) at 0x76 (118). Both can share one bus too, as they have different addresses. Add the drivers as tabs, ssd1306.py from micropython-lib and a BME280 driver, and they are copied onto the board with main.py, as Thonny copies them.

The bus runs on the simulated clock at the speed the block really makes: 400 kHz asked is 376 kHz on the wire, and oled.show(), 1 KB of picture, takes 25.4 ms. Ask an address nobody answers and the call raises OSError: [Errno 5] EIO, as on a real Pico. machine.SPI works the same way, DMA and all, and SoftI2C and SoftSPI work on any pins.

main.py: a BME280 on an SSD1306
# MicroPython on a Raspberry Pi Pico: a BME280 and an SSD1306 OLED on I2C.
# The OLED is on I2C0: SDA on GP4 (pin 6), SCL on GP5 (pin 7).
# The BME280 is on I2C1: SDA on GP6 (pin 9), SCL on GP7 (pin 10).
# Both run from 3V3 (pin 36). ssd1306.py and bme280.py are in their own tabs.
from machine import I2C, Pin
import time
import ssd1306
import bme280

i2c0 = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
i2c1 = I2C(1, scl=Pin(7), sda=Pin(6), freq=400000)
print("I2C0:", [hex(a) for a in i2c0.scan()])   # 0x3c, the OLED
print("I2C1:", [hex(a) for a in i2c1.scan()])   # 0x76, the BME280

oled = ssd1306.SSD1306_I2C(128, 64, i2c0)
sensor = bme280.BME280(i2c=i2c1)

while True:
    t, p, h = sensor.read_compensated_data()   # C, Pa, %RH
    oled.fill(0)
    oled.text("Pico weather", 0, 0)
    oled.text("%.1f C" % t, 0, 20)
    oled.text("%.1f hPa" % (p / 100), 0, 34)
    oled.text("%.1f %%RH" % h, 0, 48)
    oled.show()
    print("%.2f C  %.2f hPa  %.2f %%" % (t, p / 100, h))
    time.sleep(2)

When the program stops with an error

MicroPython stops a program that goes wrong and prints a traceback: the file, the line, and the error. Mokxi reads it off the serial port, marks the line in the editor and says what the error usually means. A NameError is a name used before it is given a value or imported (Python names are case-sensitive); a SyntaxError is a line that is not valid Python, often a missing colon at the end of an if, for, while or def line; an ImportError for RPi.GPIO or gpiozero means code written for a Raspberry Pi computer, not the Pico.

After the traceback the REPL answers, so you can try the line again by hand before you change main.py.

What is simulated, and what is not

Simulated: the RP2040’s Cortex-M0+ core and its boot ROM, the second-stage bootloader, the flash chip and the littlefs filesystem on it, the clocks, the USB port, the GPIO pins with their pull-ups and interrupts, PWM, the ADC with the temperature sensor, the I2C and SPI blocks and the DMA, the timer, the RTC and the watchdog.

Not simulated yet: the second core, so _thread stops the board with a message saying so; the I2C block as a target (machine.I2CTarget); and the CPU clock, which stays at 125 MHz whatever machine.freq() is set to, though the timer and every time measurement stay right. The PIO blocks run, so rp2.PIO and rp2.StateMachine drive their pins. The Pico W runs its own MicroPython build, with its CYW43439 modeled from its pins: Pin("LED") lights the board’s LED and network.WLAN joins Mokxi’s simulated network.

Questions

Is this real MicroPython or an imitation of it?

Real. The board runs the official MicroPython v1.29.0 firmware for the Raspberry Pi Pico, the same .uf2 file micropython.org publishes, on a simulated RP2040. Nothing in the web page interprets your Python: the firmware compiles and runs it on the simulated chip.

How does main.py get onto the board?

The way Thonny and mpremote do it. At Run, the simulated computer on the Pico’s USB port opens its serial port, enters the raw REPL, writes each .py tab into the board’s littlefs filesystem with a short script, and soft-reboots the board, which then runs main.py.

Can I add my own modules or a library?

Yes. Add a tab with the module’s .py file, such as ssd1306.py, and import it from main.py. Every .py tab is copied onto the board with main.py.

Does it download anything?

The first Run of a MicroPython project downloads the firmware, 229 KB, and the browser keeps it for every later Run. An Arduino project never downloads it.

Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.