Python
on Hardware
Survival Guide

Carlos Pereira Atencio
PyCon ES 2026

  • Python
  • MicroPython
  • CircuitPython

Intro

Step 1: blink
Step 2: ???
Step 3: profit

"Look who's talking"

Carlos Pereira Atencio

  • Embedded Software in Aerospace, Telecoms and EdTech
  • Embedded Lead @ Micro:bit Educational Foundation
  • 10 years of BBC micro:bit + MicroPython microbit❤️🐍

What to expect from this talk?

1. Blink an LED

A MicroPython board on a breadboard, wired to one blinking LED 11551010151520202525303035354040abcdefghij−++−−++−

2. Build the rest of the owl

An owl-shaped PCB perched on a branch-shaped pin header, its eyes rings of LEDs round two round displays J1J2J3 USB-COWL-1REV A · PyCon ES 2026D1-D12D13-D24U1U2Y1U3R1-R6R7-R12
  • Resources
  • Drivers
  • Debugging
  • Tooling
  • AI

But there is a lot more!

The talk runs left to right along the horizontal slides; some have vertical slides below them The talk → Extra details ↓ resources   links  

Scan the QR to follow along during the talk,
or at your own pace. https://pycones2026.embeddedlog.com

Python
on Hardware

What is it?
How is it different than normal Python?

MicroPython, CircuitPython, all the Pythons

Boards placed by RAM and by flash on one log scale: microcontrollers have kilobytes to megabytes, single-board computers gigabytes RAM FLASH micro:bit V1 16 MHz micro:bit V2 64 MHz Pyboard D 216 MHz Pico 2 150 MHz Nano ESP32 240 MHz Pi Zero 2 W 1 GHz Raspberry Pi 5 2.4 GHz 10 KB 10 KB 100 KB 100 KB 1 MB 1 MB 10 MB 10 MB 100 MB 100 MB 1 GB 1 GB 10 GB 10 GB

MicroPython, CircuitPython, all the Pythons

MicroPython and CircuitPython

MicroPython

A lean Python 3 for microcontrollers, by Damien George.

The best of each chip. machine is the portable core, and each port adds modules for what only its chip can do.

CircuitPython

Adafruit's fork of MicroPython, for beginners and education.

The same code everywhere. One API on every board; a board that can't support a module leaves it out.

Blinka brings that API and Adafruit's drivers to CPython on a Raspberry Pi.

Sources: MicroPython libraries · CircuitPython core modules

Where they differ

MicroPython 1.29CircuitPython 10.3
Hardwaremachine, plus a module per portboard, digitalio, busio on every board
PinsThe port's ids: Pin(15), Pin("GP15")The board's labels: board.D5, board.LED
ConcurrencyPin interrupts, timers, _thread, asyncioNo interrupts or threads; asyncio
WorkflowCopy files with mpremote or ThonnyA USB drive: save code.py and it reruns
FilesYour code reads and writes themRead-only to code, unless boot.py remounts
Librariesmip, from micropython-libcircup, from a bundle of 300+ drivers

Sources: CircuitPython README, FAQ, storage · MicroPython docs

What's under your code

On your laptop

your_script.py
pip + site-packages, from PyPI
CPython
OS: processes, files, catches crashes
Kernel + drivers
Hardware

On a microcontroller

main.py
mip + micropython-lib, not PyPI
MicroPython VM + machine
No OS: crashes stop or reset the board
Vendor SDK
The chip, its flash as the disk

What happens at power-on

MicroPythonCircuitPython
1 · boot.pyRuns first, and should exitRuns once, before USB starts
2 · your codemain.py; it may loop forevercode.py in a fresh VM; reruns on save
3 · when it endsThe REPL, with main.py's variablesThe REPL, in another fresh VM
Ctrl-DSoft reset: boot.py, then main.pyReload: code.py again
BrownoutHard reset, back to the startSafe mode: your code is skipped

CircuitPython's status LED: 1 green blink, done · 2 red, an exception · 3 yellow, safe mode
Sources: MicroPython reset and boot · CircuitPython README

Three things you just lost

No OS

A micro:bit V2 has 128 KB of RAM. Total.

No safety net

5 V on a 3.3 V pin is out of spec. Interrupt handlers can't allocate.

No familiar tools

No step debugger in the firmware you flashed.

Resources

640 KB ought to be enough for anybody!

What's different to "normal" Python

  • Memory! KBs, maybe MBs, of RAM & ROM
  • Run out of RAM? There is no swap, just MemoryError
  • CPU frequencies in MHz, not GHz
  • Same language we love, just with tighter constraints
BBC micro:bit V2micro:bit V2128 KB
Adafruit Feather RP2040Feather RP2040264 KB
Raspberry Pi Pico 2Pico 2520 KB
Arduino Nano ESP32Nano ESP328 MB PSRAM
16 GB laptop
Espressif ESP32-S3-DevKitC-1

16 MB PSRAM

ESP32-S3-DevKitC-116 MB PSRAM

Common ways to run out of RAM

  • MemoryError when an allocation didn't find enough contiguous free memory.

Importing your modules

The traceback ends on an import line.

Large allocations

Allocating large lists or objects

Leaks and holes

Free memory goes down over time or gets fragmented.


  • import gc provides functions to check and manage memory.

Read your own heap

>>> import micropython
>>> micropython.mem_info(1)
GC: total: 64000, used: 9824, free: 54176
 No. of 1-blocks: 112, 2-blocks: 31, max blk sz: 64, max free sz: 3317
00000000: MDhhBTT=h===DhBBhSShhLh=hF.FF.S=..hhh=....Ah=======.....S=S=....
  • max free szthe largest hole: 3317 blocks × 16 = 53,072 bytes
  • the mapyour heap, one character per 16-byte block; . is free

If a MemoryError asks for fewer bytes than free but more than the largest hole, memory is in pieces.

Not in CircuitPython · micropython.mem_info() · py/gc.c

Your code lives in RAM too


import bme280    # a 10 KB .py driver
    
RAM used while importing bme280: as .py it climbs to 28 KB while parsing and compiling, then drops to 6.5 KB, and with only 20 KB free it fails on the way up. As .mpy it never climbs past 6.7 KB; from ROMFS it holds 4.1 KB and frozen 1.2 KB. 0 10 KB 20 KB 30 KB time → parse compile peak 28 KB stays 6.5 KB only 20 KB free ✕ MemoryError .mpy ROMFS 4.1 KB frozen 1.2 KB

Parse: the whole file becomes a tree of nodes, in RAM.

Compile: the tree becomes bytecode, while the tree is still there.

Then the tree is freed. The bytecode stays.

The import fails on the peak, not on what stays.

.mpy: file compiled on your machine via mpy-cross. The bytecode still loads into RAM.

ROMFS: bytecode saved into a special filesystem in flash.
Frozen: bytecode compiled into the MicroPython build.

* Peak and ends measured on the unix port

Which lines cost memory?

  • if, for, while, try
  • locals, small-int maths: n += 1
  • calling a function or a method
  • reading a pin: pin.value()
  • writing into a buffer: buf[i] = b
  • import, def, class
  • a new list, dict, str or bytes
  • s + t, f"{t}", data[4:20]
  • uart.read(64)
  • float maths, even n / 2*

   Control flow is free. Each new object takes at least one 16-byte block.

   From Damien George, PyCon AU 2018 | *MicroPython only

Tweaks to your common habits

uart.read(64) every loop→readinto(buf) with one buffer
packet[4:20]→a memoryview: no copy
float maths in a fast loop→use ints, increase units (e.g. millivolts)
magic numbers as globals→const()

data = uart.read(64)      # ✕ new bytes every loop
buf = bytearray(64)       # ✓ once, before the loop
n = uart.readinto(buf)    # ✓ in the loop: no allocation
    

header = packet[4:20]     # ✕ copies 16 bytes
mv = memoryview(packet)   # ✓ once
header = mv[4:20]         # ✓ a view into packet, no copy
    

raw = adc.read_u16()
volts = raw * 3.3 / 65535  # ✕ MicroPython: 2 new floats
mv = raw * 3300 // 65535   # ✓ millivolts, an int
    

REG_ID = 0xD0           # ✕ a global, looked up by name
from micropython import const
_REG_ID = const(0xD0)   # ✓ in the bytecode, no global
    

More tweaks: interrupts and big data

allocating in a hard ISR (Interrupt Service Routine)→micropython.schedule()
r.json() on a big reply→json.load(r.raw): parsed as it arrives
f.read() on a log file→for line in f:
1,000 floats in a list→array("f"): ~4 KB, not ~20 KB

def on_edge(pin):                      # hard IRQ, heap locked
    events.append(pin.value())         # ✕ MemoryError
    micropython.schedule(record, pin)  # ✓ may allocate
    

data = r.json()           # ✕ whole body in RAM first
data = json.load(r.raw)   # ✓ parsed as it arrives
    

lines = f.read().split("\n")   # ✕ whole file, then a copy
for line in f:                 # ✓ one line at a time
    handle(line)
    

temps.append(sensor.temperature)  # ✕ ~20 KB of floats
temps = array("f", [0] * 1000)    # ✓ raw floats: ~4 KB
temps[i] = sensor.temperature
    

Fragmentation: free memory in pieces


readings = []
for _ in range(9):
    readings.append(read())        # kept: a float, 1 block
    msg = "T=%.1f" % readings[-1]  # dropped next time: a str, 2 blocks
big_array = bytearray(64)          # 64 bytes: 4 blocks in a row
  

1 square = 1 block = 16 bytes

...........................
    
FS=FS=FS=FS=FS=FS=FS=FS=FS=
    
F..F..F..F..F..F..F..F..FS=
    

27 blocks, all free

the loop fills it: each float kept, each string dropped

no room for big_array, so the collector frees the strings:
16 blocks × 16 B = 256 B free, but the biggest hole is 2 blocks, 32 B

A===
    

✕ MemoryError: memory allocation failed, allocating 64 bytes

A big object needs one hole that big.

Allocate once, at the start


big_array = bytearray(64)          # first, while the heap is empty
readings = []
for _ in range(9):
    readings.append(read())
    msg = "T=%.1f" % readings[-1]
  
A===.......................
    
A===FS=FF.FS=F..F..F..FS=F.
    

big_array takes 4 blocks in a row while there are plenty

✓ the loop fills and frees the space after it; big_array keeps its room

Imports, then gc.collect(),
then the big buffers. Then reuse them.

Bound growing data


readings = []
while True:
    readings.append(read())    # every reading, forever
    

from collections import deque
readings = deque((), 6)        # the last 6, never more
while True:
    readings.append(read())
    
FFF........................
    
FFFFFFFFFFFFFF.............
    
FFFFFFFFFFFFFFFFFFFFFFFFFFF
    
FFFFFF.....................
    

after a minute: 3 readings

after an hour: every reading is still in the list

after a day: ✕ MemoryError. The collector can't free what you still hold.

readings: 21.1 21.3 21.4 21.2 21.0 20.9

after a day: 21.1 21.3 21.4 21.2 21.0 20.9 21.5 · still 6

The newest pushes the oldest out.

Make the hot loop fast


def checksum(buf):
    s = 0
    for b in buf:
        s += b
    return s & 0xFFFF
      

@micropython.native
def checksum(buf):
    s = 0
    for b in buf:
        s += b
    return s & 0xFFFF
      

@micropython.viper
def checksum(buf) -> int:
    p = ptr8(buf)
    s = 0
    for i in range(int(len(buf))):
        s += p[i]
    return s & 0xFFFF
      
Adafruit's fixed-point Mandelbrot on a Metro RP2040: bytecode 8,335 ms, native 4,778 ms, viper 423 ms. bytecode 8,335 ms native 4,778 ms · 1.7× viper 423 ms · 19.7×

Mandelbrot, Metro RP2040 · Adafruit

  • nativeyour Python compiled to machine code, not bytecode
  • viperraw ints and pointers: no floats, no overflow checks
  • Turbonative & viper for CircuitPython, built on a computer

Keep it running

  • Watch free memory
  • Collect at a quiet point
  • Prove a loop allocates nothing
  • Out of memory anyway? Log it, reset

gc.collect()
print(gc.mem_free())    # once a minute
          
Free memory over hours: a flat line is fine, a falling line is a leak. hours → flat: fine falling: a leak

while True:
    read_sensor()
    update_display()
    gc.collect()        # here, never mid-task
    time.sleep_ms(100)
        

micropython.heap_lock()    # MicroPython only
handle(packet)             # allocates? MemoryError
micropython.heap_unlock()
        

try:
    app.run()
except MemoryError as e:
    log_crash(e)      # append to crash.log
    machine.reset()   # start on a fresh heap
        

Rules of thumb

WhenDo
Before you change anythingMeasure: gc.collect(), then gc.mem_free(), before and after
At start-upImports, biggest first; gc.collect(); then the big buffers
Code you don't edit.mpy with mpy-cross; ROMFS or frozen when tight
Big dataStream it; array and bytearray, not lists of floats
LoopsReuse buffers: readinto(), memoryview, no s += …
Anything that growsA limit: deque((), N), a ring buffer, batches to flash
Hard interruptsAllocate nothing: set a flag, micropython.schedule()
Slow loopsTime it with ticks_us(), then @micropython.native or viper

Read: MicroPython on microcontrollers · Adafruit memory tips

Drivers

Every light, motor and sensor needs one.

Your Python controlling the real world

A lit NeoPixel LED ring
light · NeoPixel ring
A micro servo
move · micro servo
A small OLED screen
show · OLED screen
A BME280 sensor breakout
sense · temperature
An e-ink display
show · e-ink screen
A small stepper motor and its driver board
move · stepper motor
An ultrasonic distance sensor
sense · distance
A PIR motion sensor
sense · motion
A small camera module
see · camera
SparkFun's LIS3DH accelerometer breakout
sense · acceleration
Adafruit's I2C rotary encoder board, with a knob and a lit LED
control · rotary knob

Every part needs its own signals, protocol, and timing.

A driver abstracts all of that for you.

First step, find an existing driver

  1. Read the official docs. Vendors might provide all you need.
  2. Find the chip name. Board name might not be the same as the chip (e.g. BME280).
  3. Start with Google, but also use GitHub's search with filters.
  4. Check micropython-lib, awesome-micropython, awesome-circuitpython, popular vendor's repos (Adafruit, SparkFun, Pimoroni, Arduino).
  5. Check it's for your Python. E.g. machine.I2C is MicroPython; busio is CircuitPython.
Adafruit's BME280 breakout, with BME280 printed on it

Getting the driver onto your board

MicroPython

$ pip install mpremote
$ mpremote mip install \
    github:robert-hh/BME280

import bme280_float as bme280
bme = bme280.BME280(i2c=i2c)
        

By hand: mpremote cp bme280_float.py :

CircuitPython

$ pip install circup
$ circup install adafruit_bme280

from adafruit_bme280 import basic as bme280
bme = bme280.Adafruit_BME280_I2C(i2c)
        

By hand: drag the .mpy into CIRCUITPY/lib

Other ways to install a driver

MicroPython

  • Board with Wi-Fi (ESP32, Pico W): import mip, then mip.install("ssd1306")
  • Any repo with a package.json: mpremote mip install github:org/repo
  • By hand: mpremote cp bme280_float.py :, where : is the board

CircuitPython

  • By hand: copy the .mpy and its dependencies into CIRCUITPY/lib
  • Match the bundle to your firmware: 10.x for CircuitPython 10
  • circup install --auto installs what code.py imports

MicroPython packages · mpremote · circup · CircuitPython libraries

No driver? You can write it in Python!

The layers under one sensor read: your code, the driver, MicroPython and the silicon vendor SDK in software; the microcontroller and its I2C, SPI, UART, PWM and GPIO pins in hardware, wired to a BME280, an OLED, an SD card, a GPS, a servo and a NeoPixel ring SOFTWARE ↑ HARDWARE ↓ YOUR CODEmain.py DRIVERbme280.py MICROPYTHONmachine.I2C SILICON VENDOR SDK MICROCONTROLLERI²CSPIUARTPWMGPIO BME280SSD1306 OLEDSD cardGPSSG90 servoNeoPixel ring its I²C block drives the wires

from machine import I2C
from bme280 import BME280

sensor = BME280(I2C(0))
print(sensor.temperature)
        

Python user code, requests temperature, driver handles the details


class BME280:
    def __init__(self, i2c, addr=0x76):
        chip_id = i2c.readfrom_mem(addr, 0xD0, 1)
        if chip_id != b"\x60":
            raise OSError("BME280 not found")
        

Driver in Python code, initialises and controls the sensor

Written in C, provides Python API to control the Microcontroller

Usually C code, from the chip vendor, provides low-level access to Microcontroller features.

Which bus?

  • A bus is the signals between chips & the rules for sending bytes (the protocol).
  • Microcontrollers have built-in hardware for common ones like: UART, I²C and SPI.
  • You rarely choose: the part's datasheet or product page specifies the bus.
  • You still need to know it: it decides which pins you wire, and what bus you hand the driver.

i2c = I2C(0, scl=Pin(5), sda=Pin(4))
bme = bme280.BME280(i2c=i2c)
    
  • Not every part needs a bus: buttons use a plain pin (GPIO), servos use pulses (PWM).

UART, I²C and SPI side by side

UARTI²CSPI
Used forGPS, radios, another boardsensors, clocks, small OLEDscolour screens, SD cards
WiresTX → RX, crossedSDA, SCLSCK, COPI, CIPO + CS each
Devicestwo, one at each endmany, each at an addressmany, one CS pin each
Speed9,600–115,200 baud100 or 400 kHz1–10+ MHz
Extra partsnonepull-upsoften pull-ups on CS
Anyone there?no way to knowscan() lists themno: you read 0x00 or 0xFF
Classic mistakewrong baud: garbage, no errortwo of the same chip clashwrong mode: wrong bits, no error
UART: two devices, each one's TX wired to the other's RX, and a shared ground board GPS TX RX GND TX RX GND I2C: one controller and three targets, all on the same two wires, SDA and SCL controller SDA SCL target target target SPI: one controller and two peripherals sharing SCK, COPI and CIPO, with a chip-select wire to each controller SCK COPI CIPO CS CS peripheral peripheral

Connect GND between boards · 5 V can kill a 3.3 V pin
Not every pin can do every bus · keep wires short

UART: no clock, so agree in advance

UART sends bytes one bit at a time from TX to the other end's RX. There is no clock wire, so both ends agree on the speed, the baud rate, in advance.

'A' = 0x41 = 0100 0001 · sent least significant bit (LSB) first

UART frame for the letter A: start bit, 1 0 0 0 0 0 1 0, stop bit TX idle START 10000010D0D1D2D3D4D5D6D7 STOPidle

Idle: the line sits HIGH while nothing is sent.

START: the line drops for one bit. The receiver starts timing from this edge.

Eight data bits, least significant first: 'A' goes out backwards.

STOP: back to HIGH for at least one bit, ready for the next byte.

The receiver samples the middle of each bit at its baud rate. Send at 9600, read at 19200: garbage, and no exception.

machine.UART · SparkFun: Serial Communication

I²C: reading the chip ID, bit by bit

SCL is the clock, SDA the data, both shared. The microcontroller (MCU) reads the BME280's ID:

i2c.readfrom_mem(0x76, 0xD0, 1) → b'\x60'

I2C waveform of a BME280 chip-ID read, with who sends each field: the MCU sends S, 0x76 and W, the BME280 ACKs, the MCU sends 0xD0, the BME280 ACKs, the MCU sends Sr, 0x76 and R, the BME280 ACKs and sends 0x60, the MCU sends NACK and P SCL SDA MCU BME280 SCL SDA MCU BME280 11101100 0 110100000 111011010 01100000 1 S 0x76 W A✓ 0xD0 register A✓ Sr 0x76 R A✓ 0x60 chip id N P MCU BME280 Idle: both linespulled HIGH START: SDA fallswhile SCL is HIGH Address 0x76+ write bit Sensor pulls SDALOW: ACK Register 0xD0,then ACK Repeated START Address 0x76+ read bit, ACK Sensor sends0x60 NACK: that isall I need STOP: SDA riseswhile SCL is HIGH

SPI: to read, you must write

SPI has a clock, a wire each way and a chip-select (CS) per device. Every tick moves one bit each way.

cs(0); spi.write_readinto(b'\xD0\x00', buf); cs(1) → buf[1] == 0x60

SPI mode 0: controller sends 0xD0 then a dummy byte while the BME280 returns 0x60 CS SCK COPI CIPO 110100000xD0 · bit 7 = read don't care 011000000x60 · chip id

Idle: CS is HIGH, so the sensor ignores the bus.

cs(0): CS goes LOW and selects the BME280.

Byte 1: the microcontroller sends 0xD0, the ID register (bit 7 = read). The reply is ignored.

Byte 2: the microcontroller sends a dummy 0x00. Its 8 clocks shift 0x60 back on CIPO.

cs(1): done. No ACK, so no scan(): reading a known ID register is SPI's scan().

COPI and CIPO were MOSI and MISO in older docs · machine.SPI · BME280 datasheet

A driver is a small class


from micropython import const
_ID = const(0xD0)
_CTRL = const(0xF4)
_TEMP = const(0xFA)

class BME280:
    def __init__(self, i2c, addr=0x76):
        self.i2c, self.addr = i2c, addr
        self._buf = bytearray(3)
        if i2c.readfrom_mem(addr, _ID, 1) != b"\x60":
            raise OSError("BME280 not found")
        i2c.writeto_mem(addr, _CTRL, b"\x27")  # wake

    def raw_temperature(self):
        b = self._buf
        self.i2c.readfrom_mem_into(self.addr, _TEMP, b)
        return b[0] << 12 | b[1] << 4 | b[2] >> 4
    
  1. Registers as const()
  2. Take the bus, don't make it: I2C, SoftI2C or a fake
  3. Check the ID, wake it up
  4. One buffer, reused

Before you blame the driver

For every board and power supply. Signals need the common ground.

Check the part's supply range and its logic level.

Long wires add capacitance & might need stronger pull-ups (↓ resistance). Remember boards/modules might already have pulls!

Servos and LED strips brown out the board. Keep GND shared.

Shift I²C with a MOSFET. A BSS138-style shifter works on I²C. A TXB0108 doesn't.

Boot, input-only, LED and button pins: read the pinout.

When scan() lies to you

i2c.scan()Usual cause
[]no power or GND, SDA/SCL swapped, wrong pins
every address, or hangsSDA held LOW: no pull-ups, stuck target
yours is missing8-bit address (0xEC ≠ 0x76), collision
found, reads failweak pull-ups, long wires
found, on 5 Vits pull-ups put 5 V on 3.3 V pins

Why weak pull-ups fail

The pull-up resistor lifts the line. The weaker it is, the slower the rise: at 400 kHz, 300 ns at most.

RC rise of SDA with a 4.7 kilo-ohm and a 50 kilo-ohm pull-up 0.7 VDD 0.3 VDD 300 ns limit (400 kHz) ━ 4.7 kΩ ━ 50 kΩ (Pico internal) time (µs) · 50 pF bus

Rise time: 4.7 kΩ ~0.2 µs ✓ · 50 kΩ ~2.1 µs ✕ · five 4.7 kΩ in parallel = 940 Ω, under the 967 Ω minimum

The Pico's internal pull-ups are 50–80 kΩ: fine on the desk, flaky on wires · NXP UM10204, the I²C specification

I²C: resistors pull HIGH, chips pull LOW

  • Resistors "pull-ups" the SDA (data) and SCL (clock) signals to 3.3 V.
  • The microcontroller and I2C device pull the lines LOW to communicate.
Three breakouts on one bus, each with a 4.7 kilo-ohm pull-up from 3V3 to SDA; together they act as 1.6 kilo-ohms 3V3 SDA board 1 4.7 kΩ board 2 4.7 kΩ board 3 4.7 kΩ Together, in parallel they reduce resistance: 1.6 kΩ
  • Some breakouts/modules bring their own resistors.
  • Pull too strong (low resistance), under ~1 kΩ at 3.3 V: chips can't pull LOW. Disable pull-ups on some boards.
  • Pull too weak (or long wires): signal takes too long to rise to 3V3. Reduce total resistance, or reduce bus frequency.

Reset the board mid-read and the sensor can keep holding SDA LOW, still waiting to finish its byte.

scan() fails until you unplug it.

Fix: drive GPIO to pulse SCL up to nine times, until SDA goes HIGH, then start I²C again. Or power-cycle the sensor.

Power budget

  • Every part draws current, normally in milliamps (mA).
  • Board power supply only provides only so much; microcontroller pins are more limited.
Pico, Pico 2under 300 mA
micro:bit V1 · V290 · 190 mA
Adafruit Feather500 mA peak
USB 2.0 port, in total500 mA

On an ESP32, Wi-Fi alone can take 240 mA of it.

RP2040/2350 · Pico 1 & 24 mA, up to 12
ESP3220 mA, up to 40
micro:bit V21–4 mA

Fine for an LED and a 330 Ω resistor, about 4 mA.
Not enough for a motor.

  • Add up the datasheets: BME280 under 1 mA · OLED ~20 mA · NeoPixel up to 60 mA · servo, hundreds.
  • Measure with a USB power meter, or a multimeter in series.

Debugging

Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod.

Lorem ipsum dolor

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Tooling

Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod.

Lorem ipsum dolor

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

AI

Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod.

Lorem ipsum dolor

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Conclusion

Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod.

Lorem ipsum dolor

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Thank you

Questions?