If you are asking what can you do on Raspberry Pi hardware beyond running a Pi-hole ad blocker or a RetroPie emulation station, you are looking at the wrong layer of the stack. Underneath the desktop environment, the Raspberry Pi 5 is a quad-core ARM Cortex-A76 embedded Linux powerhouse capable of handling high-speed I/O, real-time telemetry routing, and edge computing. Instead of treating it like a miniature desktop PC, we are going to treat it like an industrial edge gateway.
In this guide, we will answer the question by building a robust, multi-sensor I2C environmental telemetry node. It will read temperature, humidity, and barometric pressure, package the data into JSON, and publish it to an MQTT broker over WiFi. We will cover the exact hardware specs, the wiring, the Python 3.11+ code (using the modern Paho MQTT v2 API), and how to debug the most notorious I2C error you will face on the bench.
The Embedded Capability Matrix: Pi 5 vs Pi 4 vs Zero 2 W
Before we wire up the sensors, it helps to understand the raw I/O capabilities of the current Raspberry Pi lineup. This table dictates which board you should choose based on your embedded project's bus speed and power constraints.
| Feature / Spec | Raspberry Pi 5 (8GB) | Raspberry Pi 4 Model B (4GB) | Raspberry Pi Zero 2 W |
|---|---|---|---|
| Primary I2C Bus | I2C1 (GPIO 2/3) + I2C0 (GPIO 0/1) | I2C1 (GPIO 2/3) | I2C1 (GPIO 2/3) |
| Internal I2C Pull-ups | 1.8kΩ (Strong, limits bus capacitance) | 1.8kΩ (Configurable via EEPROM) | 1.8kΩ |
| SPI Max Practical Clock | 50 MHz (Stable) / 125 MHz (Theoretical) | 30 MHz (Stable) | 30 MHz (Stable) |
| Hardware UART Ports | 4x PL011 UARTs (via RP1 chip) | 2x (1x PL011, 1x Mini UART) | 2x (1x PL011, 1x Mini UART) |
| Idle Power Draw (5V) | ~2.3W (Requires active cooling) | ~1.5W | ~0.7W |
| Best Embedded Use Case | Multi-bus edge gateway, computer vision | Standard IoT hub, local dashboard | Battery/solar remote sensor node |
Note: The Raspberry Pi 5 routes its peripherals through the RP1 southbridge chip. This vastly improves SPI and UART performance but changes how device tree overlays are applied in the /boot/firmware/config.txt file compared to the Pi 4. For full details on the RP1 architecture, refer to the official Raspberry Pi 5 documentation.
Project Build: Parts List and Pin Mapping
This build targets the Raspberry Pi 5 8GB running Raspberry Pi OS (Bookworm, 64-bit). We are using the STEMMA QT / Qwiic ecosystem to eliminate soldering and ensure solid I2C connections.
Bill of Materials (BOM)
- Compute: Raspberry Pi 5 8GB with official Active Cooler ($85 total)
- Sensor 1: Adafruit BME280 I2C/SPI Temperature Humidity Pressure Sensor (STEMMA QT) - Product ID 2652 ($12)
- Sensor 2: Adafruit SGP40 Air Quality Sensor (STEMMA QT) - Product ID 4829 ($15)
- Wiring: STEMMA QT to Raspberry Pi GPIO Ribbon Cable (Product ID 4463) ($5)
- Power: 27W USB-C PD Power Supply (Official Pi 5)
GPIO Pin Mapping Table
The primary I2C bus on the Pi 5 is exposed on the standard 40-pin header. We are using I2C1.
| Pi 5 GPIO Pin | BCM Number | Function | STEMMA QT Wire Color |
|---|---|---|---|
| Pin 1 | 3.3V | Power (VCC) | Red |
| Pin 3 | GPIO 2 | I2C1 SDA | Blue |
| Pin 5 | GPIO 3 | I2C1 SCL | Yellow |
| Pin 6 | GND | Ground | Black |
Wiring and Hardware Setup
- De-energize the board: Unplug the USB-C power supply from the Pi 5. Never hot-plug I2C sensors on the 3.3V rail; a slipped wire can feed 5V into the SDA line and fry the RP1 southbridge.
- Connect the ribbon: Plug the STEMMA QT to GPIO ribbon cable into the Pi's 40-pin header, ensuring the red wire aligns with Pin 1 (3.3V, closest to the USB-C port).
- Daisy-chain the sensors: Plug one end of a short STEMMA QT cable into the Pi ribbon adapter, and the other into the BME280. Use a second cable to chain the SGP40 from the BME280's secondary port.
- Verify I2C addresses: Power up the Pi, SSH in, and run
sudo i2cdetect -y 1. You should see77(BME280 default) and59(SGP40 default) in the grid.
Complete Python Telemetry Code
This script targets Python 3.11+ on Bookworm. It uses smbus2 and the bme280 package for the sensor, and paho-mqtt for telemetry. Crucial for 2026: Paho MQTT v2.0 introduced breaking API changes regarding callbacks. This code uses the modern CallbackAPIVersion.VERSION2 to prevent runtime errors. See the Paho MQTT v2 migration guide for details.
Install dependencies first:
sudo apt install python3-smbus2 python3-pip
pip3 install bme280 paho-mqtt --break-system-packages
#!/usr/bin/env python3
import time
import json
import smbus2
import bme280
import paho.mqtt.client as mqtt
from paho.mqtt.enums import CallbackAPIVersion
# --- Configuration ---
I2C_BUS = 1
BME280_ADDR = 0x77
MQTT_BROKER = '192.168.1.50'
MQTT_PORT = 1883
MQTT_TOPIC = 'workbench/env/node01'
POLL_INTERVAL = 10 # seconds
# --- Hardware Initialization ---
bus = smbus2.SMBus(I2C_BUS)
calibration_params = bme280.load_calibration_params(bus, BME280_ADDR)
# --- MQTT Callbacks (Paho v2 API) ---
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
print(f'Connected to MQTT broker with result code {reason_code}')
else:
print(f'Failed to connect, reason code: {reason_code}')
def on_publish(client, userdata, mid, reason_code, properties):
# Optional: verify QoS delivery
pass
# Initialize MQTT Client with v2 API
client = mqtt.Client(
callback_api_version=CallbackAPIVersion.VERSION2,
client_id='pi5_env_node'
)
client.on_connect = on_connect
client.on_publish = on_publish
try:
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start()
except Exception as e:
print(f'MQTT Connection Failed: {e}')
exit(1)
# --- Main Telemetry Loop ---
try:
while True:
try:
# Read Sensor
data = bme280.sample(bus, BME280_ADDR, calibration_params)
# Build Payload
payload = {
'temp_c': round(data.temperature, 2),
'humidity': round(data.humidity, 2),
'pressure_hpa': round(data.pressure, 2),
'timestamp': int(time.time())
}
# Publish to MQTT
result = client.publish(MQTT_TOPIC, json.dumps(payload), qos=1)
if result.rc == mqtt.MQTT_ERR_SUCCESS:
print(f'Published: {payload}')
else:
print(f'Publish failed with code: {result.rc}')
except OSError as e:
print(f'I2C Hardware Error: {e}')
except Exception as e:
print(f'Unexpected error: {e}')
time.sleep(POLL_INTERVAL)
except KeyboardInterrupt:
print('Shutting down...')
client.loop_stop()
client.disconnect()
bus.close()
Debugging: Fixing 'OSError: [Errno 121] Remote I/O error'
If you run the script and immediately hit OSError: [Errno 121] Remote I/O error, do not panic. This is the most common embedded I2C fault on the Raspberry Pi. It means the Linux kernel sent a clock pulse and address byte, but the sensor failed to pull the SDA line low to acknowledge (ACK) it.
The First Three Things to Check
- Run
i2cdetect -y 1: If the grid shows allUUor blank spaces, the bus is dead or locked. If it shows the wrong address (e.g.,76instead of77), your code is pointing to the wrong hex address. Adafruit boards default to0x77, while generic Amazon BME280 clones often default to0x76. - Verify Physical Seating and Pinout: STEMMA QT cables can be inserted upside down. Ensure the red wire is on the 3.3V pin, not the 5V pin. Use a multimeter to verify you have exactly 3.2V to 3.4V between the red and black wires at the sensor breakout.
- Check for Clock Stretching Collisions: The BME280 uses clock stretching during ADC conversion. If you have a logic analyzer, verify the SCL line is actually being held low by the sensor. If the Pi's I2C driver times out before the sensor finishes converting, it throws Errno 121.
Ranked Causes for Errno 121
| Probability | Cause | Fix |
|---|---|---|
| High (60%) | Loose STEMMA QT connection or swapped SDA/SCL wires on custom breakout boards. | Reseat cables; verify continuity from Pi GPIO 2 to sensor SDA pin. |
| Medium (25%) | Wrong I2C address hardcoded in Python script. | Update BME280_ADDR variable to match i2cdetect output. |
| Low (10%) | Bus capacitance too high (cables > 1 meter), causing slow rise times. | Lower I2C baudrate to 100kHz in /boot/firmware/config.txt using dtparam=i2c_baudrate=100000. |
| Rare (5%) | Sensor brownout due to Pi 5 3.3V rail droop under heavy CPU load. | Ensure official 27W PSU is used; add a 100µF decoupling capacitor across sensor VCC/GND. |
Extending and Simplifying the Build
Once the baseline node is stable, you can adapt the architecture to fit your specific deployment environment.
How to Simplify (No Network Required)
If you are deploying this in an off-grid cabin or a Faraday cage where MQTT is unavailable, strip out the paho-mqtt library. Instead, import sqlite3 and write the JSON payloads directly to a local database file on the Pi's microSD card. Use a simple cron job to rotate the database file weekly, preventing SD card wear-leveling issues from constant small writes.
How to Extend (Scaling to 8+ Sensors)
The BME280 only has two selectable I2C addresses (0x76 and 0x77). If you want to map the thermal gradient across a large server rack or greenhouse, you cannot just daisy-chain 10 of them.
To extend this, wire a TCA9548A I2C Multiplexer to the Pi's primary I2C bus. The mux acts as a switchboard, allowing you to connect up to 8 identical BME280 sensors (all set to 0x77) to 8 isolated channels. You simply send an I2C command to the mux to open channel 1, read the sensor, close channel 1, and open channel 2. For detailed wiring on the Adafruit BME280 breakout, refer to the Adafruit BME280 learning guide.
By treating the Raspberry Pi 5 as a dedicated embedded edge node rather than a desktop toy, you unlock industrial-grade telemetry capabilities for under $120 in hardware. Wire it cleanly, handle your I2C exceptions gracefully, and let the RP1 southbridge do the heavy lifting.






