If you are searching for a practical beginner Raspberry Pi project that moves beyond blinking an LED, reading environmental data over I2C is the definitive next step. This guide walks through building a physical temperature, humidity, and pressure logger using a BME280 sensor and an SSD1306 OLED display, triggered by a physical push button.
Target Board Variant: This code and wiring diagram specifically target the Raspberry Pi 5 (4GB) running Raspberry Pi OS (64-bit). The hardware wiring and Python logic are 100% backward-compatible with the Raspberry Pi 4 Model B. We use the standard 40-pin header, relying on the primary I2C bus (Bus 1).
Project Spec Sheet & Parts List
Before stripping wire, verify you have the exact components. Using 5V-tolerant sensors on a Pi without a level shifter is the fastest way to brick your board's GPIO pins. The Pi's GPIO logic is strictly 3.3V.
| Component | Exact Model / Variant | Est. Price (2026) | Why this specific part? |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 | Current standard; RP1 chip offers better I2C stability than older Pi models. |
| Environment Sensor | Bosch BME280 (I2C variant, 3.3V) | $8.50 | Measures temp, humidity, and pressure. Avoid the BMP280 (no humidity) or DHT11 (unreliable 1-wire). |
| Display | 0.96" SSD1306 OLED (I2C, 128x64) | $7.00 | Low power draw, crisp text. Ensure it has 4 pins (VCC, GND, SCL, SDA), not SPI. |
| Trigger Input | 6x6mm Tactile Push Button | $0.10 | Standard through-hole breadboard switch. |
| Resistor | 10kΩ Pull-up Resistor (x2) | $0.05 | Required for stable I2C buses if your breakout boards lack them. |
| Consumables | Half-size breadboard, 22 AWG solid jumper wires | $6.00 | 22 AWG solid core grips breadboard terminals better than 24 AWG. |
Hardware Wiring & Pin Mapping
The I2C protocol uses a shared clock (SCL) and data (SDA) line, allowing multiple devices to share the same two GPIO pins as long as they have unique I2C addresses. The BME280 defaults to 0x76 (or 0x77), and the SSD1306 defaults to 0x3C.
Pin Mapping Table
| Component Pin | Pi 40-Pin Header | GPIO / Function | Wire Color (Standard) |
|---|---|---|---|
| BME280 VCC | Pin 1 | 3.3V Power | Red |
| BME280 GND | Pin 6 | Ground | Black |
| BME280 SDA | Pin 3 | GPIO 2 (SDA1) | Blue |
| BME280 SCL | Pin 5 | GPIO 3 (SCL1) | Yellow |
| OLED VCC | Pin 17 | 3.3V Power | Red |
| OLED GND | Pin 14 | Ground | Black |
| OLED SDA | Pin 3 | Shared SDA1 | Blue |
| OLED SCL | Pin 5 | Shared SCL1 | Yellow |
| Button Leg 1 | Pin 11 | GPIO 17 (Input) | Green |
| Button Leg 2 | Pin 9 | Ground | Black |
Python Code: I2C OLED & BME280 Logger
This script requires three libraries. Install them via your virtual environment or system pip: pip install smbus2 RPi.bme280 luma.oled RPi.GPIO. For a deeper understanding of the BME280's registers, refer to the Adafruit BME280 Guide.
The code below includes explicit pin definitions, robust I2C initialization, and hardware debouncing for the button.
import time
import sys
from smbus2 import SMBus
import bme280
import RPi.GPIO as GPIO
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import ImageFont
# --- PIN & ADDRESS DEFINITIONS ---
BUTTON_PIN = 17
I2C_PORT = 1
BME280_ADDR = 0x76 # Change to 0x77 if your board has the alternate address
OLED_ADDR = 0x3C
# --- HARDWARE SETUP ---
GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# Initialize I2C Bus
bus = SMBus(I2C_PORT)
# Load calibration parameters for BME280
calibration_params = bme280.load_calibration_params(bus, BME280_ADDR)
# Initialize OLED Display
serial_interface = i2c(port=I2C_PORT, address=OLED_ADDR)
display = ssd1306(serial_interface, width=128, height=64)
# Load a basic font (fallback to default if custom fails)
try:
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 14)
except IOError:
font = ImageFont.load_default()
def read_and_display():
"""Reads sensor data and renders it to the OLED."""
try:
data = bme280.sample(bus, BME280_ADDR, calibration_params)
temp_c = f"{data.temperature:.1f} C"
humidity = f"{data.humidity:.1f} %"
pressure = f"{data.pressure:.0f} hPa"
with canvas(display) as draw:
draw.text((0, 0), f"Temp: {temp_c}", font=font, fill='white')
draw.text((0, 20), f"Hum: {humidity}", font=font, fill='white')
draw.text((0, 40), f"Pres: {pressure}", font=font, fill='white')
print(f"[LOG] {temp_c} | {humidity} | {pressure}")
except OSError as e:
print(f"Sensor read failed: {e}")
display.clear()
def main():
print("System ready. Press the button to log data. Ctrl+C to exit.")
try:
while True:
# Button is active LOW due to internal pull-up
if GPIO.input(BUTTON_PIN) == GPIO.LOW:
read_and_display()
# Simple software debounce
time.sleep(0.3)
time.sleep(0.05)
except KeyboardInterrupt:
print("\nExiting gracefully...")
except Exception as e:
print(f"Fatal error: {e}")
finally:
GPIO.cleanup()
display.clear()
display.show()
if __name__ == '__main__':
main()
Debugging: Fixing 'OSError: [Errno 121] Remote I/O error'
If you run the script and immediately hit the following traceback, do not panic. This is the most common hurdle in any beginner Raspberry Pi I2C project.
Traceback (most recent call last):
File "main.py", line 24, in <module>
calibration_params = bme280.load_calibration_params(bus, BME280_ADDR)
OSError: [Errno 121] Remote I/O error
At the silicon level, Errno 121 means the Pi's I2C controller sent an address over the SDA line, but the target chip did not pull the line low to acknowledge it (a NACK condition). Here are the ranked causes and exact fixes:
- I2C Interface is Disabled in OS: Raspberry Pi OS ships with I2C disabled by default to save a microamp of power and free up pins.
- Fix: Open terminal, run
sudo raspi-config, navigate to Interface Options -> I2C, and enable it. Reboot. (See the official Raspberry Pi I2C documentation for visual steps).
- Fix: Open terminal, run
- Incorrect I2C Address: The BME280 address depends on a tiny jumper or pad on the back of the breakout board. If the pad is uncut, it's usually
0x76. If cut/soldered, it's0x77.- Fix: Run
i2cdetect -y 1in the terminal. Look for the hex number that appears in the grid. Update theBME280_ADDRvariable in the Python code to match.
- Fix: Run
- Missing Pull-Up Resistors or Loose Wires: I2C is an open-drain protocol. It requires pull-up resistors to pull the SDA/SCL lines high to 3.3V. While the Pi has internal 1.8kΩ pull-ups, long breadboard wires add capacitance, causing signal degradation.
- Fix: Check your breadboard seating. If using cheap clone sensors that omit onboard pull-ups, add external 4.7kΩ or 10kΩ resistors between the 3.3V rail and both the SDA and SCL lines.
1. Run
i2cdetect -y 1 to verify the hardware sees the chips.2. Use a multimeter to verify exactly 3.2V-3.3V at the breadboard power rails (not 5V).
3. Run
dmesg | grep i2c to check for kernel-level bus timeout errors indicating a short circuit.
Extending and Simplifying the Build
Once you have the baseline working, you can adapt this hardware to fit your exact skill level or project goals.
How to Simplify (Terminal Only)
If you are waiting for your OLED to ship in the mail, or you just want to log data to a file, strip out the luma.oled imports and the display initialization. Replace the canvas() block with a simple CSV append operation using Python's built-in csv module. This reduces the code footprint and eliminates I2C address conflicts.
How to Extend (Smart Home Integration)
To turn this into a smart home node, install the paho-mqtt library. Inside the read_and_display() function, format the sensor data into a JSON payload and publish it to an MQTT broker (like Mosquitto running on a Home Assistant server). You can then trigger automations—like turning on a dehumidifier relay—when the BME280 reports humidity above 60%.
Beginner Raspberry Pi FAQ
What is the best beginner Raspberry Pi board for GPIO projects in 2026?
The Raspberry Pi 5 (4GB) is the current sweet spot. It offers significantly faster CPU performance for compiling code and running local databases, and the RP1 chip provides more robust GPIO current sourcing (up to 20mA per pin safely, compared to the older 16mA limit on the Pi 4). However, if you are on a strict budget, a used Raspberry Pi 4 Model B (2GB) remains an exceptionally capable board for basic I2C and SPI sensor projects.
How do I fix I2C errors on a beginner Raspberry Pi breadboard setup?
Breadboards are notorious for intermittent connections, which cause I2C buses to drop packets and throw Errno 121. Always use 22 AWG solid-core wire instead of cheap, thin stranded jumper wires. If the bus drops out when you tap the table, your breadboard contacts are worn out. For permanent installations, move away from breadboards and solder the VCC, GND, SDA, and SCL lines directly to a perfboard or use JST connectors.
Should a beginner Raspberry Pi user use Python or C++ for GPIO?
Start with Python. The RPi.GPIO, gpiozero, and smbus2 libraries abstract away the complex memory-mapped register configurations required to toggle pins. Python allows you to focus on the logic of your project (e.g., "if temperature > 30, turn on fan") rather than fighting with compiler toolchains and C pointer arithmetic. Once you need microsecond-precise timing or are writing a high-frequency motor controller, transition to C++ using the lgpio library.
Can I power the BME280 and OLED from the Pi's 5V pin?
No. While the Pi's 5V pins (Pin 2 and Pin 4) can supply ample current, the GPIO data pins (SDA/SCL) operate at 3.3V. If you power a sensor at 5V, its logic HIGH threshold will be around 3.5V. The Pi's 3.3V output will not be recognized as a HIGH signal by the sensor, resulting in silent communication failures. Furthermore, if the sensor attempts to send a 5V HIGH signal back to the Pi's SDA pin, it will overvoltage and destroy the Pi's GPIO circuitry. Always use the 3.3V rail (Pin 1 or Pin 17) for I2C sensors.






