To reliably log temperature, humidity, and pressure on a Raspberry Pi Pico, use the Bosch BME280 sensor over the I2C bus at 400kHz, paired with 4.7kΩ external pull-up resistors. This combination avoids the most common I2C bus capacitance issues and ensures stable data reads in MicroPython.
(Search note: If you arrived here searching for 'raspverry pi pico', you are in the right place—this guide covers the official Raspberry Pi Pico and Pico W families.)*
Hardware Decision Tree: Which Pico Variant to Choose
The Raspberry Pi ecosystem now includes several board variants. Choosing the wrong one for a simple wired sensor node wastes money and complicates your firmware. Use this decision matrix to select the right board.
| Board Variant | Microcontroller | Key Features | Best Use Case |
|---|---|---|---|
| Raspberry Pi Pico | RP2040 | No headers, no wireless | Custom PCB soldering |
| Raspberry Pi Pico H | RP2040 | Pre-soldered headers, castellated pads | Breadboard prototyping |
| Raspberry Pi Pico W | RP2040 + CYW43439 | WiFi/Bluetooth, higher power draw | IoT cloud logging |
| Raspberry Pi Pico 2 | RP2350 | Higher clock, more RAM, RISC-V/ARM | Heavy DSP or complex RTOS |
Exact Parts List & Pin Mapping
Before wiring, verify your sensor module. Many cheap breakouts sell the BMP280 (temperature and pressure only) disguised as the BME280 (adds humidity). Check the silver chip casing: it must read 'BME' or 'HUM' to support humidity reads.
Bill of Materials (BOM)
- Microcontroller: Raspberry Pi Pico H (RP2040) with MicroPython v1.22+ firmware installed.
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) or SparkFun BME280 (SEN-13676). Avoid unbranded $2 clones unless you verify the I2C pull-up resistors.
- Resistors: Two 4.7kΩ through-hole resistors (for I2C pull-ups).
- Wiring: 22 AWG solid core jumper wires (keep I2C traces under 30cm).
Pin Mapping Table
The RP2040 allows I2C on multiple pins, but we will use the default I2C0 bus mapping for standard library compatibility.
| Pico H Pin (GP) | Physical Pin # | BME280 Breakout Pin | Function |
|---|---|---|---|
| GP4 | 6 | SDI / SDA | I2C Data Line |
| GP5 | 7 | SCK / SCL | I2C Clock Line |
| 3V3 (OUT) | 36 | VIN / VCC | 3.3V Power |
| GND | 38 | GND | Common Ground |
Wiring Procedure & I2C Pull-up Strategy
I2C is an open-drain bus. It requires pull-up resistors to pull the SDA and SCL lines high to the logic voltage (3.3V). The RP2040 datasheet specifies that GPIO pins have internal pull-ups, but they are typically ~50kΩ to ~60kΩ—far too weak for reliable 400kHz communication over wires longer than 5cm.
- Place the Pico H across the center trench of your breadboard.
- Insert the BME280 on the same side, leaving a 4-hole gap between the SDA/SCL pins and the Pico GPIOs.
- Wire Power and Ground: Connect Pico Pin 36 (3V3) to the BME280 VIN. Connect Pico Pin 38 (GND) to the BME280 GND.
- Install External Pull-ups: Insert one leg of a 4.7kΩ resistor into the 3V3 rail, and the other leg into the SDA line (GP4). Repeat with the second 4.7kΩ resistor from 3V3 to the SCL line (GP5).
- Wire Data Lines: Connect Pico GP4 to BME280 SDA, and Pico GP5 to BME280 SCL.
The BME280 has an SDO (Serial Data Out) pin that dictates the I2C address. If SDO is tied to GND, the address is
0x76. If tied to 3V3, it is 0x77. Most Adafruit/SparkFun boards default to 0x77, while cheap generic clones often default to 0x76. We will handle both dynamically in the code below.
Complete MicroPython Code with Error Handling
This code targets the Raspberry Pi Pico H (RP2040). It initializes the I2C bus, scans for the sensor, reads the Bosch chip ID to verify communication, and catches specific I2C bus errors. Copy and paste this directly into your main.py or Thonny IDE shell.
import machine
import time
# --- Pin Definitions & I2C Setup ---
I2C_SCL_PIN = 5
I2C_SDA_PIN = 4
I2C_FREQ = 400000 # 400kHz Fast Mode
# Initialize I2C0 bus
i2c = machine.I2C(0, scl=machine.Pin(I2C_SCL_PIN), sda=machine.Pin(I2C_SDA_PIN), freq=I2C_FREQ)
BME280_CHIP_ID = 0x60
BME280_REG_ID = 0xD0
def find_bme280_address(i2c_bus):
"""Scans I2C bus and returns the BME280 address, or None."""
devices = i2c_bus.scan()
for addr in [0x76, 0x77]:
if addr in devices:
return addr
return None
def read_chip_id(i2c_bus, addr):
"""Reads the Chip ID register to verify sensor presence."""
# Write the register pointer, then read 1 byte
i2c_bus.writeto(addr, bytes([BME280_REG_ID]))
chip_id = i2c_bus.readfrom(addr, 1)[0]
return chip_id
def main():
print('Starting I2C Bus Scan...')
addr = find_bme280_address(i2c)
if addr is None:
print('FATAL: BME280 not found at 0x76 or 0x77. Check wiring and SDO pin.')
return
print(f'Found device at I2C address: {hex(addr)}')
while True:
try:
chip_id = read_chip_id(i2c, addr)
if chip_id == BME280_CHIP_ID:
print(f'Success: Read valid BME280 Chip ID ({hex(chip_id)})')
else:
print(f'Warning: Device responded, but Chip ID is {hex(chip_id)} (Expected 0x60). Might be a BMP280.')
# In a full implementation, you would read compensation params and
# raw ADC registers here, or import a dedicated bme280 library.
except OSError as e:
# Catch specific I2C hardware errors
err_code = e.args[0] if e.args else 0
if err_code == 121:
print('ERROR: OSError [Errno 121] EIO - Remote I/O error. NACK received on bus.')
elif err_code == 19:
print('ERROR: OSError [Errno 19] ENODEV - No such device. Device disconnected mid-read.')
else:
print(f'ERROR: Unexpected I2C OSError: {e}')
except Exception as e:
print(f'Unexpected software error: {e}')
time.sleep(2)
if __name__ == '__main__':
main()
Debugging: 'OSError: [Errno 121] EIO' and I2C Failures
When working with I2C on the RP2040, the MicroPython machine.I2C documentation notes that hardware timeouts and bus lockups manifest as specific OS errors. If your script crashes, do not guess—follow this diagnostic path.
The First Three Things to Check When It Fails
- Run a raw bus scan: Open the REPL and type
i2c.scan(). If it returns an empty list[], your issue is physical (power, ground, or wrong address). If it returns[118]or[119](decimal for 0x76/0x77), your wiring is fine, and the issue is in your register read logic. - Measure pull-up voltage: Set your multimeter to DC Voltage. Probe the SDA line relative to GND. It should read ~3.2V to 3.3V when idle. If it reads < 2.5V, your pull-up resistors are too weak, or a device is holding the bus low.
- Check logic level mismatch: The Pico is strictly a 3.3V logic device. If you are using a 5V BME280 breakout board without a bi-directional logic level shifter, you risk damaging the RP2040 GPIO pins, resulting in a dead I2C peripheral.
Ranked Causes for Exact Error Strings
| Exact Error String | Meaning | Most Likely Cause (Ranked) | Fix |
|---|---|---|---|
OSError: [Errno 121] EIO |
Remote I/O Error (NACK on bus) | 1. Missing/weak pull-up resistors. 2. I2C frequency too high for wire capacitance. 3. Sensor is in sleep/fault state. |
Add 4.7kΩ pull-ups. Drop freq to 100000. Power cycle the sensor. |
OSError: [Errno 19] ENODEV |
No such device (Address not found) | 1. SDO pin configured to opposite address. 2. SDA/SCL wires swapped. 3. Sensor is completely unpowered. |
Check SDO trace on PCB. Swap SDA/SCL. Verify 3.3V at VIN pin. |
OSError: [Errno 110] ETIMEDOUT |
Bus timeout (SCL held low) | 1. Bus capacitance > 400pF. 2. Electrical noise spike locked the bus. |
Shorten wires. Add a 100nF decoupling capacitor across sensor VIN/GND. |
Extending and Simplifying the Build
Once you have stable Chip ID reads, you will want to adapt this setup for your specific environment. Here is how to scale the project up or down without rewriting your hardware abstraction layer.
How to Simplify (For Desktop/Short-Range Use)
If your sensor is mounted directly next to the Pico (wires under 10cm) and you are out of resistors, you can enable the RP2040's internal pull-ups via MicroPython and drop the bus speed.
Change your I2C initialization to:
i2c = machine.I2C(0, scl=machine.Pin(5, machine.Pin.PULL_UP), sda=machine.Pin(4, machine.Pin.PULL_UP), freq=100000)
This forces 100kHz Standard Mode, which is much more forgiving of the weak ~50kΩ internal pull-ups. Do not use this for wires longer than 15cm.
How to Extend (For Field Data Logging)
To turn this into an offline environmental logger, add a microSD card breakout board using the SPI0 bus. Wire the SD module to GP16 (MISO), GP17 (CS), GP18 (SCK), and GP19 (MOSI). Because SPI and I2C use different hardware peripherals on the RP2040, they will not interfere with each other. Wrap your sensor read loop in a machine.lightsleep() cycle, waking every 60 seconds via an RTC alarm to log data to the FAT32 filesystem on the SD card.






