In this tutorial, we’ll wire up a BMP280 sensor to a Raspberry Pi Pico over I2C, pull live temperature and pressure readings, and push them onto a 0.96″ SSD1306 OLED display — all in MicroPython. Along the way we’ll run into a couple of the most common roadblocks makers hit with this exact combo: an OSError: [Errno 5] ENODEV on the BMP280 side, and a blank or frozen OLED screen. We’ll fix both, step by step.
Table of Contents
Required Components (Bill of Materials)
| Component | Quantity | Notes |
|---|---|---|
| Raspberry Pi Pico (or Pico W) | 1 | Any RP2040-based Pico works |
| BMP280 temperature & pressure sensor breakout | 1 | I2C variant, usually 6-pin |
| 0.96″ SSD1306 OLED display (128×64) | 1 | I2C interface |
| Breadboard | 1 | Half-size or full-size |
| Male-to-male jumper wires | ~8 | For I2C bus + power rails |
| Micro USB cable | 1 | For programming and power |
Both the BMP280 and the SSD1306 sit on the same I2C bus in this project, so we only need four signal wires (SDA/SCL) shared between two devices, plus power and ground for each.
BMP280 Sensor — Quick Overview

The BMP280 is Bosch’s follow-up to the BMP180, and it measures both barometric pressure and temperature using a piezo-resistive MEMS element. Compared to its predecessor, it’s smaller, faster to respond, and pulls noticeably less current — which is why it shows up so often in weather stations, drones (for altitude hold), and indoor climate-monitoring builds.
It talks over either I2C or SPI, but we’ll stick to I2C here since it only needs two data lines and plays nicely alongside the OLED on the same bus.
Key specs:
- Interface: I2C (up to 3.4 MHz) or SPI
- Supply voltage: 1.71V – 3.6V
- Pressure range: 300–1100 hPa
- Pressure accuracy: ±1 hPa
- Temperature range: −40°C to +85°C
- Temperature accuracy: ±1°C
- Standby current: under 1 µA in sleep mode
Datasheet: Bosch BMP280 datasheet (PDF)
Wiring: BMP280 + SSD1306 OLED on the Pico’s I2C Bus
Connect all the required components as shown in the below circuit diagram.

The Raspberry Pi Pico has two hardware I2C peripherals (I2C0 and I2C1), and each can be mapped to several different GPIO pin pairs. For this build we’ll keep things simple and put both the sensor and the display on I2C0, using:
- GP0 → SDA
- GP1 → SCL
Wiring both devices:
| Pico Pin | BMP280 Pin | SSD1306 Pin |
|---|---|---|
| 3V3 (OUT) | VCC | VCC |
| GND | GND | GND |
| GP0 | SDA | SDA |
| GP1 | SCL | SCL |
Since I2C is a shared bus, both devices connect to the same SDA and SCL lines — we’re not wiring them separately to different pins. If your BMP280 breakout has extra pins like CSB and SDO, leave them unconnected. SDO is used when 2 BMP280 are used it changes i2c address and makes them master slave.
Writing the MicroPython Code
Before flashing anything, make sure MicroPython (not CircuitPython) is loaded onto your Pico, and that you’ve copied two driver files onto the board, you can do it by downloading from the below list or open the github code to copy and save with same filenames on our Raspberry pi pico board.
bmp280.py— a lightweight BMP280 MicroPython driver- Download heressd1306.py— the standard Micropython SSD1306 driver, Download here
With both drivers saved to the Pico’s filesystem (via Thonny’s file manager, for example), here’s the main working script:
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
from bmp280 import BMP280
import time
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
oled = SSD1306_I2C(128, 64, i2c)
bmp = BMP280(i2c)
while True:
temp = bmp.temperature
pressure = bmp.pressure / 100
oled.fill(0)
oled.text("BMP280 Reading", 0, 0)
oled.text("Temp: {:.1f} C".format(temp), 0, 20)
oled.text("Pres: {:.0f} hPa".format(pressure), 0, 35)
oled.show()
time.sleep(1)
Save this as main.py and run it.
Final Working Output
When you run it it initializes the OLED display, BMP280 and get data from the BMP280 and display it over the OLED display as shown in the below image: the OLED refreshed every second, showing live temperature in Celsius and pressure in hPa with no dropped frames or bus errors.

From here, it’s easy to extend the project — add an altitude readout using the standard barometric formula, log readings to a file on the Pico’s flash, or push values out over Wi-Fi if you’re using a Pico W.
Troubleshoot: Errors and Solutions
Error #1: OSError: [Errno 5] ENODEV
On the first run, the OLED lit up fine — but the moment the code tried to read the BMP280, it threw:
OSError: [Errno 5] ENODEV
This error means the Pico’s I2C bus scanned successfully, but nothing responded at the address the BMP280 driver was expecting. Almost always, this comes down to one of two things:
- The BMP280 breakout is actually sitting at
0x76, not the0x77default some driver libraries assume (or vice versa). - The
SDOpin is floating, which changes the address the sensor answers to.
Fix: run a quick I2C scanner script first, before touching the sensor driver at all:
from machine import Pin, I2C
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
devices = i2c.scan()
print("I2C devices found:", [hex(d) for d in devices])
On our board, this printed ['0x3c', '0x76'] — 0x3c being the OLED, and 0x76 being the BMP280. If your SDO pin is tied to VCC instead of GND or left floating, the sensor will instead answer at 0x77.
Once we had the real address, we passed it explicitly into the driver:
bmp = BMP280(i2c, address=0x76)
That alone cleared the ENODEV error completely.
Error #2: OLED Shows Nothing (or Freezes Mid-Update)
With the sensor fixed, the second snag showed up on the display itself: the OLED would either stay completely blank, or show the first frame and then lock up. Two culprits, both common with this display:
- Address collision / wrong OLED address — most SSD1306 boards default to
0x3c, but a few ship configured to0x3d. Ifoled.fill()throws silently or the screen never updates, re-run the scanner above and confirm the OLED’s actual address, then pass it in:SSD1306_I2C(128, 64, i2c, addr=0x3d). - I2C bus contention from insufficient pull-ups — many BMP280 and SSD1306 breakouts already include their own pull-up resistors on SDA/SCL. Wiring both to the same bus means you now have two sets of pull-ups in parallel, which can pull the resistance low enough to distort the signal on longer wires or breadboards with poor contacts. If the display flickers or hangs intermittently, try shortening your jumper wires or dropping the bus speed:
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=100000) # dropped from 400kHz to 100kHz
Dropping to standard-mode speed (100 kHz) resolved the intermittent freeze for us without needing to touch the resistors physically.
Video Tutorial
Conclusion
The two errors covered here — ENODEV from an address mismatch, and a stalled OLED from bus contention — account for the vast majority of issues people hit combining these two I2C devices on a Pico. Scanning the bus first, before touching either driver, is the fastest way to rule both out in under a minute.
If you run into a different error with this same setup, drop the exact message in the comments and we’ll help you track it down.
