Pico: I2C in depth with MicroPython, scanners and bus recovery
Scan the I2C bus on a Pico for connected devices, recover from a locked-up SDA line, and understand the 7-bit vs 8-bit address gotcha. The pattern that turns 'my sensor is not responding' into a 30-second fix.
I2C is the protocol I reach for when I need to wire up a sensor and a microcontroller. Two wires, lots of devices on the same bus, simple addressing. It is also the protocol that produces the most confused emails I get, because I2C has three or four distinct failure modes that all look like “the sensor is not responding.”
This tutorial is the one I wish I had read the first five times I tried to use I2C. The patterns here (scan, recover, address gotcha, pull-ups) come up in every I2C project.
What you need
- Raspberry Pi Pico (or Pico W)
- An I2C device (a BME280, an MPU6050, an OLED display, an ADS1115, anything with SDA and SCL pins)
- 2 jumper wires (or 4, if the device has VCC and GND to wire too)
- USB cable
- MicroPython firmware installed (see the UART tutorial for the install steps)
For a “first I2C device,” I recommend the BME280 or the SSD1306 OLED. Both are well-documented, both work at 3.3V, and both have libraries in MicroPython.
What I2C is (SDA, SCL, addresses)
I2C is a two-wire protocol:
- SDA is the data line.
- SCL is the clock line.
Both lines need pull-up resistors to VCC. The Pico has internal pull-ups that are usually strong enough for one or two devices on a short bus. For more devices or longer wires, you need external pull-ups (typically 4.7k resistors from SDA and SCL to 3.3V).
Each device on the bus has a 7-bit address (0x00 to 0x7F). The master (the Pico) initiates every transaction. The slave (device) responds when its address is called. Multiple devices can share the bus as long as they have different addresses.
The bus speed is 100 kHz in standard mode, 400 kHz in fast mode, 1 MHz in fast mode plus, and 3.4 MHz in high speed mode. The Pico supports all of these. Most sensors only do 100 kHz or 400 kHz.
The code: scanning the bus
The first thing I run on any new I2C device is a bus scan. It tells you whether the device is responding and at what address:
from machine import I2C, Pin
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=100_000)
devices = i2c.scan()
print('Found devices at:', [hex(d) for d in devices])
If you have a BME280 wired correctly, this prints something like:
Found devices at: ['0x76']
If it prints an empty list, the wiring is wrong, the device has no power, or the device has a different default address. Most sensors have an address pin that lets you choose between two addresses (e.g. the BME280 is 0x76 by default, 0x77 if you tie the SDO pin to VCC). Try the other one.
The 7-bit vs 8-bit address gotcha
This is the part that has bitten me the most times. The I2C address is 7 bits. The 8th bit is the read/write bit.
When a datasheet says “the device address is 0x76,” that is the 7-bit address. When a datasheet says “the device address is 0x76 / 0x77 for write, 0x77 / 0x78 for read,” that is already shifted to include the R/W bit, and you have to right- shift by 1 to get the 7-bit address (0x76 / 0x77).
The rule: if the address in the datasheet is even, the author has already shifted. Divide by 2. If it is odd, the author is giving you the 7-bit address as-is. Use it directly. (This rule is approximate, and you should always check the actual datasheet.)
The MicroPython I2C.scan() returns the 7-bit address. Most
libraries (bme280, ssd1306, etc.) also take the 7-bit
address. If you are getting OSError: [Errno 5] EIO on every
transaction, you almost certainly have the wrong address.
Reading a sensor
Once the scan finds the device, reading data is straightforward:
from machine import I2C, Pin
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=100_000)
# raw read: 2 bytes from register 0x00
data = i2c.readfrom_mem(0x76, 0x00, 2)
print('Raw bytes:', data)
# write a byte to a register
i2c.writeto_mem(0x76, 0xF4, b'\x27')
For a real sensor, use a library. MicroPython has a BME280
library in the bme280_float module (one of the
community-maintained ones). The driver handles the register
reads and the math:
import bme280_float as bme280
bme = bme280.BME280(i2c=i2c)
print(bme.values)
# ('23.45C', '45.67%', '1013.25hPa')
The MicroPython library ecosystem for I2C sensors is smaller than the Arduino one, but the common ones (BME280, MPU6050, SSD1306, ADS1115, AHT20) all have working drivers.
Pull-up resistors: internal vs external
I2C requires pull-up resistors on SDA and SCL. The Pico’s internal pull-ups are about 50k, which is too weak for a bus with more than one device or any meaningful wire length.
For one device on a short bus (under 10 cm), internal pull-ups usually work. For more devices or longer wires, add external pull-ups:
Pico 3.3V --[ 4.7k ]-- SDA
Pico 3.3V --[ 4.7k ]-- SCL
Smaller resistor values (2.2k, 1k) give stronger pull-ups and support faster speeds or longer buses. The trade-off is more current draw when the bus is low.
You can also enable internal pull-ups in code, but those are
too weak for most cases. The I2C bus constructor accepts
sda=Pin(0, Pin.IN, Pin.PULL_UP) if you want to be explicit.
The bus lockup problem (SDA stuck LOW) and how to recover
This is the failure mode that confuses everyone. The Pico is the master, the sensor is the slave, the sensor is in the middle of a transaction, and then… silence. The Pico says “SDA is stuck low.” Every subsequent read times out.
The cause: the slave device was interrupted (power loss, glitch on the line, software bug in the slave) while it was holding SDA low. The slave is now waiting for the master to clock out the rest of the transaction, but the master has already given up. Deadlock.
The fix: manually clock out the rest of the transaction. The master toggles SCL up and down 9 times while watching SDA. The slave releases SDA on the next clock pulse.
from machine import Pin
def i2c_recover(scl_pin_num, sda_pin_num):
scl = Pin(scl_pin_num, Pin.OPEN_DRAIN, value=1)
sda = Pin(sda_pin_num, Pin.OPEN_DRAIN, value=1)
# Toggle SCL up to 9 times to release a stuck slave
for _ in range(9):
scl.value(0)
scl.value(1)
if sda.value() == 1:
break
# Send a STOP (SDA low while SCL high, then SDA high)
sda.value(0)
scl.value(1)
sda.value(1)
# usage:
i2c_recover(scl_pin_num=1, sda_pin_num=0)
This is the I2C bus recovery procedure from the I2C spec. Every embedded engineer eventually writes this function. Keep it in your toolbox.
After recovery, re-run i2c.scan() to confirm the device is
back.
The multi-master I2C gotcha
I2C supports multiple masters. The Pico can be one master, a Raspberry Pi can be another, and they share the bus.
This sounds convenient. It is a recipe for hangs.
The problem: two masters both think the bus is free, both start a transaction at the same time, and the bus state becomes incoherent. The I2C spec has arbitration for this, but in practice it is fragile.
The fix: pick one master. The Pico is the master, the Raspberry Pi is the master, not both. Use a logic level shifter or a different bus for the second master.
For most projects, you have one master (the microcontroller) and many slaves (sensors). That works. Multi-master is for specific cases (e.g. a hot-swap controller board) and is not worth the complexity otherwise.
When to use I2C vs SPI
Both are serial protocols for inter-chip communication. The short version:
- I2C: 2 wires, many devices, 400 kHz typical, address conflicts are possible. Use for: sensors, GPIO expanders, low-bandwidth peripherals.
- SPI: 4+ wires, one device per CS pin, 10+ MHz, no addressing. Use for: displays with high refresh, SD cards, sensors that need fast reads.
The decision: if you need speed and have CS pins to spare, SPI. If you need to share a bus and can live with 400 kHz, I2C.
The I2C bus on the Pico can do 1 MHz if the sensors support it, but most stop at 400 kHz. For a sensor that needs 1 MHz or more, SPI is the right choice.
When something breaks
OSError: [Errno 5] EIOon every read. The address is wrong, the device is unpowered, or SDA/SCL are swapped. Re-run the scanner. Check the wiring.OSError: [Errno 110] ETIMEDOUT. The bus is locked up. Run the recovery procedure above.- The scanner finds the device but reads return garbage. The baud rate is too high for the wire length. Drop to 100 kHz. Or the pull-ups are too weak. Add 4.7k external pull-ups.
- Works on the breadboard, fails on the soldered board. Cold solder joint, usually on SDA or SCL. Reflow.
- Two devices with the same address. I2C addresses are 7 bits, so 128 possible. With more than a few devices on the bus, address conflicts are common. Most sensor breakouts have an address pin to choose between two addresses. For three or more devices, use a TCA9548A I2C multiplexer ($3) to split the bus into 8 sub-buses.
What to build next
- A multi-sensor weather station (BME280 + BH1750 light sensor
- soil moisture on the same I2C bus).
- An OLED display that shows live sensor readings.
- A TCA9548A multiplexer to break an address conflict.
- An I2C slave: configure a second Pico as a slave and communicate master-to-master.
The OLED display is the natural next project. The book Pico MicroPython has a chapter on building a sensor hub that reads a dozen I2C devices and exposes the data over a simple UART stream, including the bus recovery function as a defensive measure.