If you are migrating from older boards to the Raspberry Pi 5, you will quickly discover that the new RP1 southbridge chip changes how GPIO and I2C buses are handled under the hood. Legacy libraries like RPi.GPIO are effectively dead on this architecture. For modern raspberry pi 5 projects, you must use updated tooling like gpiozero (backed by lgpio) and properly map the new I2C bus addresses.
This guide walks through building a practical environmental smart relay controller. We will read temperature and humidity from a BME280 sensor over I2C and trigger a 3.3V-compatible relay module when thresholds are crossed. We will also cover the exact hardware traps that brick Pi 5 builds and how to debug the inevitable I2C bus errors.
Project Spec Sheet & Exact Parts List
This build targets the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS (Bookworm, 64-bit). The 8GB model is specified here because running a local MQTT broker alongside sensor polling and a web dashboard will easily consume 2GB+ of RAM, and the 4GB model leaves too little headroom for future expansion.
Estimated Build Time: 45 minutes (hardware) + 30 minutes (software/debugging).
| Component | Exact Variant / Specification | Why This Variant? |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB) | RP1 southbridge requires updated GPIO libraries; 8GB prevents OOM kills with Docker/MQTT. |
| Power Supply | Official 27W USB-C PD Power Supply | Pi 5 limits downstream USB/peripheral current to 600mA if it doesn't detect a 5A PD handshake. A standard phone charger will cause relay brownouts. |
| Sensor | BME280 Breakout (3.3V I2C) | Measures Temp, Humidity, Pressure. Ensure it has onboard 3.3V voltage regulation and pull-ups. |
| Actuator | 4-Channel 3.3V Relay Module (Optocoupler isolated) | Pi 5 GPIO is strictly 3.3V. Standard 5V relay modules will not trigger reliably without level shifters or the VCC/JDV1 jumper trick. |
| Wiring | 24 AWG Silicone Wire / 40-pin Ribbon | Keep I2C runs under 30cm to avoid capacitance issues on the RP1 I2C bus. |
Hardware Wiring & Pin Mapping
The Raspberry Pi 5 40-pin header maintains physical backward compatibility, but the internal routing through the RP1 chip means the primary I2C bus on pins 3 and 5 is exposed to the OS as /dev/i2c-1.
Pin Mapping Table
| Component Pin | Pi 5 Physical Pin | Pi 5 BCM / Function | Wire Color (Suggested) |
|---|---|---|---|
| BME280 VCC | 1 | 3.3V Power | Red |
| BME280 GND | 6 | Ground | Black |
| BME280 SDA | 3 | GPIO 2 (I2C1 SDA) | Blue |
| BME280 SCL | 5 | GPIO 3 (I2C1 SCL) | Yellow |
| Relay VCC | 2 | 5V Power | Red (Thicker gauge) |
| Relay GND | 9 | Ground | Black |
| Relay IN1 | 11 | GPIO 17 | Green |
VCC and JD-VCC, remove the jumper. Connect JD-VCC to Pi Pin 2 (5V) to power the coils, and connect the module's VCC to Pi Pin 1 (3.3V) to power the optocoupler LEDs. If your module lacks this jumper, buy a dedicated 3.3V relay module.
Python Control Code (gpiozero & smbus2)
Because the Pi 5 uses the RP1 chip, we avoid the deprecated RPi.GPIO library. Instead, we use gpiozero (which automatically uses the lgpio backend on Pi 5) for the relay, and smbus2 for raw I2C communication with the BME280.
Prerequisites: Run sudo apt install python3-gpiozero python3-smbus2 i2c-tools and ensure I2C is enabled via sudo raspi-config (Interface Options -> I2C).
#!/usr/bin/env python3
"""
Raspberry Pi 5 Environmental Smart Relay Controller
Target Board: Raspberry Pi 5 (8GB) running Bookworm 64-bit
Libraries: gpiozero, smbus2
"""
import time
import sys
from gpiozero import OutputDevice
from smbus2 import SMBus
# --- PIN & ADDRESS DEFINITIONS ---
RELAY_PIN = 17 # BCM GPIO 17 (Physical Pin 11)
I2C_BUS_ID = 1 # Pi 5 primary header I2C is /dev/i2c-1
BME280_ADDR = 0x76 # Default BME280 address (check with i2cdetect -y 1)
# BME280 Registers for basic compensation
REG_DIG_T1 = 0x88
REG_DIG_T2 = 0x8A
REG_DIG_T3 = 0x8C
REG_TEMP_DATA = 0xFA
REG_CHIP_ID = 0xD0
# Initialize Relay (Active Low for most relay modules)
# If your relay turns ON when signal is HIGH, change to active_high=True
relay = OutputDevice(RELAY_PIN, active_high=False, initial_value=False)
def read_word(bus, addr, reg):
"""Read a 16-bit little-endian word from I2C."""
lsb = bus.read_byte_data(addr, reg)
msb = bus.read_byte_data(addr, reg + 1)
return (msb << 8) | lsb
def get_signed_word(bus, addr, reg):
"""Read a signed 16-bit word."""
val = read_word(bus, addr, reg)
if val > 32767:
val -= 65536
return val
def read_temperature(bus, addr, dig_T1, dig_T2, dig_T3):
"""Read and compensate temperature from BME280."""
# Read raw temperature data (20-bit)
msb = bus.read_byte_data(addr, REG_TEMP_DATA)
lsb = bus.read_byte_data(addr, REG_TEMP_DATA + 1)
xlsb = bus.read_byte_data(addr, REG_TEMP_DATA + 2)
raw_temp = (msb << 12) | (lsb << 4) | (xlsb >> 4)
# Bosch datasheet compensation formula
var1 = (((raw_temp >> 3) - (dig_T1 << 1)) * dig_T2) >> 11
var2 = (((((raw_temp >> 4) - dig_T1) * ((raw_temp >> 4) - dig_T1)) >> 12) * dig_T3) >> 14
t_fine = var1 + var2
temp_c = (t_fine * 5 + 128) >> 8
return temp_c / 100.0
def main():
print(f"Initializing I2C Bus {I2C_BUS_ID}...")
try:
with SMBus(I2C_BUS_ID) as bus:
# Verify Chip ID (BME280 should return 0x60)
chip_id = bus.read_byte_data(BME280_ADDR, REG_CHIP_ID)
if chip_id != 0x60:
print(f"Error: Expected BME280 Chip ID 0x60, got {hex(chip_id)}.")
sys.exit(1)
print("BME280 detected. Reading calibration data...")
dig_T1 = read_word(bus, BME280_ADDR, REG_DIG_T1)
dig_T2 = get_signed_word(bus, BME280_ADDR, REG_DIG_T2)
dig_T3 = get_signed_word(bus, BME280_ADDR, REG_DIG_T3)
# Set sensor to forced mode for single measurement
bus.write_byte_data(BME280_ADDR, 0xF4, 0x25)
time.sleep(0.1) # Wait for measurement
temp = read_temperature(bus, BME280_ADDR, dig_T1, dig_T2, dig_T3)
print(f"Current Temperature: {temp:.2f} °C")
# Threshold logic: Trigger relay if temp exceeds 28.0°C
THRESHOLD = 28.0
if temp > THRESHOLD:
print(f"Threshold exceeded. Energizing relay on GPIO {RELAY_PIN}.")
relay.on()
else:
print("Temperature normal. Relay de-energized.")
relay.off()
except OSError as e:
print(f"I2C Communication Failed: {e}")
print("Check 'First Three Things to Check' section in documentation.")
sys.exit(2)
except KeyboardInterrupt:
print("\nInterrupted. Cleaning up GPIO...")
finally:
relay.off()
relay.close()
if __name__ == "__main__":
main()
Debugging: First Three Things to Check When It Fails
When working with raspberry pi 5 projects involving I2C, the most common failure mode is the script crashing with the exact error string: OSError: [Errno 121] Remote I/O error. This means the Linux kernel attempted to clock data on the I2C bus, but the sensor did not acknowledge (ACK) the address.
Here are the first three things to check, ranked from most likely to least likely:
- I2C Bus Overlay is Missing in Bookworm: Unlike older OS versions, Raspberry Pi OS Bookworm uses
config.txtdifferently. Runsudo raspi-config, navigate to Interface Options -> I2C, and enable it. Reboot. Then verify the bus exists by runningls -l /dev/i2c*. If you don't see/dev/i2c-1, the RP1 device tree overlay failed to load. - Address Mismatch (0x76 vs 0x77): The BME280 has two possible I2C addresses depending on the breakout board manufacturer. Run
i2cdetect -y 1in the terminal. If you see77instead of76, update theBME280_ADDRvariable in the Python code to0x77. If the grid is entirely empty, your wiring is wrong or the sensor is dead. - Missing Pull-Up Resistors: The RP1 chip on the Pi 5 has internal pull-up resistors, but they are weak (around 50kΩ). The I2C specification requires strong pull-ups (typically 4.7kΩ) for reliable communication at 100kHz/400kHz. High-quality BME280 breakouts (like those from Adafruit or SparkFun) include these onboard. Cheap $2 clone boards often omit them. If using a clone, solder two 4.7kΩ resistors between SDA/VCC and SCL/VCC.
PermissionError: [Errno 13] Permission denied: '/dev/i2c-1', your user is not in the i2c group. Fix this by running sudo usermod -aG i2c $USER and logging out and back in.
Extending and Simplifying the Build
Not every project needs to be complex. Here is how you can scale this build up or down based on your actual requirements.
How to Simplify
If you only need temperature monitoring and don't need to control mains appliances or high-current DC loads, drop the relay module entirely. Replace the gpiozero relay logic with a simple HTTP POST request using the requests library to send data to a free dashboard like ThingSpeak or a local Home Assistant instance. This eliminates the 5V power routing headaches and reduces the project to just four I2C wires.
How to Extend
To turn this into a production-grade environmental node, add an MQTT broker (like Mosquitto) running locally on the Pi 5. Modify the Python script to publish the compensated temperature, humidity, and pressure to an MQTT topic (e.g., home/lab/environment). You can then use Node-RED to create complex automations, such as triggering a smart plug via Zigbee only if the temperature exceeds 28°C and the humidity is below 40%, preventing false triggers from localized heat spikes.
FAQ: Common Raspberry Pi 5 Projects Questions
Do legacy RPi.GPIO scripts work on Raspberry Pi 5 projects?
No. The Raspberry Pi 5 moved GPIO control from the main BCM SoC to a separate RP1 southbridge chip. The legacy RPi.GPIO library relies on direct memory mapping to the old BCM memory addresses, which no longer exist. If you try to run old code, you will get a RuntimeError. You must rewrite your scripts using gpiozero (which uses the lgpio backend on Pi 5) or the newly maintained rpi-gpio package.
Can I power a 4-channel relay directly from the Raspberry Pi 5 5V pin?
Only if you are using the official 27W USB-C PD power supply. The Pi 5 firmware negotiates power delivery; if it detects a standard 5V/3A charger, it restricts the total current available to the 5V pins and USB ports to 600mA to protect the board. A 4-channel relay module can draw up to 300mA when all coils are energized, leaving almost no margin for the Pi itself or USB peripherals, leading to random reboots. Always use the 27W official supply for relay projects.
Why is my BME280 reading 0.0 for humidity on the Pi 5?
If temperature and pressure read correctly but humidity is stuck at 0.0, you are likely reading the wrong registers or failing to configure the ctrl_hum register (0xF2) before triggering a measurement. According to the Bosch BME280 datasheet, the humidity oversampling configuration must be written to 0xF2 before writing to the ctrl_meas register (0xF4) to trigger the measurement. If you write to 0xF4 first, the humidity settings are ignored. Alternatively, ensure you actually have a BME280 and not a BMP280, which lacks a humidity sensor entirely but shares the same footprint.






