Raspberry Pi Python programming on the Raspberry Pi 5 requires a fundamental shift from older models. Because the Pi 5 uses the new RP1 southbridge chip, the legacy RPi.GPIO library is deprecated and will fail on modern Bookworm OS installations. The current 2026 standard is to use gpiozero backed by the lgpio pin factory, alongside smbus2 for robust I2C communication.
This guide walks through building a hardware-interfaced environmental dashboard. We will read temperature, humidity, and pressure from a BME280 sensor, render the data on an SSD1306 OLED display, and use a physical GPIO button to toggle the screen. More importantly, we will cover the exact debugging steps for the most common I2C failure mode you will encounter on the bench.
Project Overview & Hardware Spec Sheet
Estimated Time: 45 minutes (Hardware) + 30 minutes (Software/Debugging)
Target Board: Raspberry Pi 5 (4GB or 8GB variant) running Raspberry Pi OS (Bookworm or later)
Parts List & 2026 Pricing
| Component | Exact Variant / Model | Est. Cost |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 |
| Environmental Sensor | BME280 I2C Breakout (3.3V logic, Adafruit 2652 or equivalent) | $15.00 |
| Display | SSD1306 128x64 I2C OLED (Monochrome, 3.3V/5V tolerant) | $12.00 |
| Input | 6x6mm Tactile Pushbutton (Normally Open) | $0.50 |
| Wiring | Female-to-Female Jumper Wires (20cm, 28 AWG) | $5.00 |
| Passives | 2x 4.7kΩ Resistors (for I2C pull-ups, if not on breakout) | $1.00 |
Pin Mapping & Physical Wiring
The Raspberry Pi 5 40-pin header maintains the same physical pinout as the Pi 4, but the internal routing goes through the RP1 chip. Both the BME280 and SSD1306 will share the primary I2C bus (Bus 1).
| Pi 5 Pin (Physical) | GPIO / Function | Target Component | Component Pin |
|---|---|---|---|
| Pin 1 | 3V3 Power | BME280 & SSD1306 | VCC / VIN |
| Pin 3 | GPIO 2 (SDA1) | BME280 & SSD1306 | SDA |
| Pin 5 | GPIO 3 (SCL1) | BME280 & SSD1306 | SCL |
| Pin 6 | Ground | BME280, SSD1306, Button | GND |
| Pin 11 | GPIO 17 | Tactile Button | Leg 1 (Other leg to GND) |
The RP1 chip on the Pi 5 features internal pull-up resistors on the I2C lines, but they are roughly 50kΩ. This is too weak for reliable 400kHz (Fast Mode) I2C communication if your jumper wires exceed 10cm or you have multiple devices on the bus. If your sensor breakout board does not include 4.7kΩ external pull-ups on SDA and SCL, solder them between the 3V3 line and the SDA/SCL lines to prevent data corruption.
Environment Setup & Python Dependencies
Before writing code, we need to enable the I2C interface and install the modern Python libraries that support the Pi 5 architecture.
- Enable I2C: Open a terminal and run
sudo raspi-config. Navigate to Interface Options -> I2C and enable it. Reboot the Pi. - Verify Hardware: After reboot, run
sudo i2cdetect -y 1. You should see76(BME280) and3c(SSD1306) in the grid. If you see77instead of76, your BME280 breakout has the alternate address. - Install System Packages: The Pi 5 relies on the
lgpioC library for GPIO access. Install the system-level dependencies:sudo apt update sudo apt install python3-gpiozero python3-lgpio python3-smbus2 i2c-tools - Install Python Libraries: Create a virtual environment (best practice for Bookworm OS) and install the sensor and display drivers:
python3 -m venv ~/env-dashboard source ~/env-dashboard/bin/activate pip install RPi.bme280 luma.oled pillow
The Complete Python Script
This script initializes the I2C bus, loads the BME280 calibration parameters, and sets up a continuous loop. The GPIO button uses the gpiozero library, which automatically handles debouncing and uses the correct lgpio pin factory on the Pi 5.
import smbus2
import bme280
import time
import sys
from luma.core.interface.serial import i2c as luma_i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from gpiozero import Button
from signal import pause
# --- PIN & ADDRESS DEFINITIONS ---
I2C_PORT = 1
BME280_ADDR = 0x76 # Change to 0x77 if your breakout requires it
BUTTON_GPIO = 17
OLED_ADDR = 0x3C
# --- HARDWARE INITIALIZATION ---
try:
# Initialize I2C bus and BME280 calibration
bus = smbus2.SMBus(I2C_PORT)
calibration_params = bme280.load_calibration_params(bus, BME280_ADDR)
# Initialize OLED Display via luma.oled
serial = luma_i2c(port=I2C_PORT, address=OLED_ADDR)
device = ssd1306(serial)
# Initialize Button (Active low, pulled up internally by gpiozero)
toggle_btn = Button(BUTTON_GPIO, pull_up=True, bounce_time=0.05)
except Exception as e:
print(f'Hardware initialization failed: {e}')
sys.exit(1)
# --- STATE VARIABLES ---
display_on = True
def toggle_display():
global display_on
display_on = not display_on
if not display_on:
device.clear()
toggle_btn.when_pressed = toggle_display
# --- MAIN LOOP ---
print('Dashboard running. Press Ctrl+C to exit.')
try:
while True:
if display_on:
# Read sensor data
data = bme280.sample(bus, BME280_ADDR, calibration_params)
temp_c = data.temperature
humid = data.humidity
press = data.pressure
# Render to OLED
with canvas(device) as draw:
draw.text((0, 0), f'Temp: {temp_c:.1f} C', fill='white')
draw.text((0, 16), f'Hum: {humid:.1f} %', fill='white')
draw.text((0, 32), f'Pres: {press:.0f} hPa', fill='white')
draw.text((0, 48), 'Status: OK', fill='white')
time.sleep(2.0)
except KeyboardInterrupt:
print('\nShutting down gracefully...')
device.clear()
bus.close()
except OSError as e:
print(f'I2C Bus Error: {e}')
device.clear()
bus.close()
Debugging: Fixing "OSError: [Errno 121] Remote I/O error"
The most frequent point of failure in Raspberry Pi Python programming with I2C sensors is the dreaded OSError: [Errno 121] Remote I/O error. This error string is thrown by the underlying Linux I2C driver when the master (Pi) sends a clock pulse and address, but the slave (sensor) fails to pull the SDA line low to acknowledge (ACK).
The First Three Things to Check
- Run
i2cdetect -y 1: If the sensor address (e.g.,76) does not appear in the terminal grid, the Pi physically cannot see the chip. The issue is wiring, power, or a dead sensor. If it does appear but Python still throws Errno 121, you have a timing or logic-level issue. - Check Logic Levels: The Pi 5 I2C bus operates strictly at 3.3V. If you are using a 5V BME280 breakout without a logic level shifter, the 5V SDA line can back-feed into the RP1 chip, causing the Pi to clamp the bus and throw I/O errors. Always verify your sensor breakout is 3.3V native.
- Verify Pull-Up Strength: As mentioned in the wiring section, weak pull-ups cause the SDA line to rise too slowly, missing the ACK window at 400kHz. Add 4.7kΩ external pull-ups to 3.3V.
Ranked Causes for Errno 121
| Probability | Cause | Fix |
|---|---|---|
| High | Sensor disconnected or powered from 5V while Pi expects 3.3V. | Move VCC to Pin 1 (3V3). Reseat jumper wires. |
| Medium | Missing external 4.7kΩ pull-up resistors on SDA/SCL. | Solder 4.7kΩ resistors between 3V3 and SDA/SCL. |
| Low | I2C bus locked up due to a previous script crashing mid-transaction. | Reboot the Pi, or toggle the I2C kernel module: sudo rmmod i2c_dev && sudo modprobe i2c_dev. |
For deeper architectural context on how the Pi 5 handles peripheral routing differently than the Pi 4, refer to the official Raspberry Pi RP1 southbridge documentation.
Extending and Simplifying the Build
How to Simplify
If you don't have an OLED display, you can strip the luma.oled dependencies entirely and log the data to a local CSV file or push it to a cloud endpoint. Replace the canvas rendering block with a simple file-append operation:
import csv
with open('sensor_log.csv', 'a', newline='') as f:
writer = csv.writer(f)
writer.writerow([time.time(), temp_c, humid, press])
How to Extend
To turn this into a true IoT node, integrate the paho-mqtt library. You can publish the data.temperature and data.pressure payloads to a local Mosquitto broker on your network. This allows home automation systems like Home Assistant to ingest the Pi 5's sensor data without relying on proprietary cloud APIs. For standard MQTT integration patterns, the Eclipse Paho Python client documentation is the definitive reference.
Raspberry Pi Python Programming FAQ
Is legacy RPi.GPIO still viable for Raspberry Pi Python programming in 2026?
No. The RPi.GPIO library relied on direct memory mapping to the BCM2835/BCM2711 SoC registers. The Raspberry Pi 5 uses the RP1 southbridge, which routes GPIO over a PCIe link. RPi.GPIO cannot access these registers and will throw a RuntimeError on import. You must use gpiozero (which automatically uses the lgpio backend on Pi 5) or the raw lgpio Python bindings. See the gpiozero Pi 5 migration guide for exact syntax changes.
How do I auto-start my Raspberry Pi Python programming scripts on boot?
Do not use rc.local or .bashrc for production sensor scripts; they lack proper error logging and restart capabilities. Instead, create a systemd service. Create a file at /etc/systemd/system/dashboard.service:
[Unit]
Description=BME280 Dashboard
After=network.target
[Service]
ExecStart=/home/pi/env-dashboard/bin/python /home/pi/dashboard.py
WorkingDirectory=/home/pi
StandardOutput=append:/var/log/dashboard.log
StandardError=append:/var/log/dashboard_error.log
Restart=always
User=pi
[Install]
WantedBy=multi-user.target
Enable it with sudo systemctl enable dashboard.service and start it with sudo systemctl start dashboard.service.
Why does my Raspberry Pi Python programming I2C bus drop to 100kHz?
The Pi 5 defaults to 100kHz (Standard Mode) for I2C to ensure maximum compatibility with slow slave devices. If your BME280 and OLED both support 400kHz (Fast Mode), you can increase the bus speed by editing the boot configuration. Open /boot/firmware/config.txt and add the line dtparam=i2c_baudrate=400000. Reboot the Pi. Note that if you increase the baudrate without adding external 4.7kΩ pull-up resistors, you will likely trigger the Errno 121 I/O errors detailed in the debugging section above due to signal rise-time degradation.






