Project Spec Sheet & Bill of Materials
The Raspberry Pi Zero W (and its modern successor, the Zero 2 W) packs a full 40-pin GPIO header into a board smaller than a stick of gum. While the original Zero W is largely discontinued in 2026, the Raspberry Pi Zero 2 W shares the exact same GPIO pinout and form factor, making it the default choice for new embedded builds. This project builds an environmental logger with a hardware interrupt button, moving away from the deprecated RPi.GPIO library to the modern gpiozero and smbus2 stack required by Raspberry Pi OS Bookworm and later.
| Component | Exact Variant / Model | Est. Price (2026) | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi Zero 2 W (512MB) | $15.00 | Header pre-soldered or solder it yourself. |
| Env. Sensor | Adafruit BME280 I2C (PID 2652) | $9.95 | Includes onboard 4.7kΩ pull-ups and 3.3V LDO. |
| Pushbutton | 6x6mm Tactile Switch (4-pin) | $0.10 | Standard breadboard-friendly SPST-NO. |
| Status LED | 5mm Diffused Green LED | $0.05 | Forward voltage ~2.2V @ 20mA. |
| Resistor | 330Ω 1/4W Carbon Film | $0.02 | Current limiting for the LED. |
Raspberry Pi Zero W GPIO Pin Mapping
Before wiring, understand that the Pi's GPIO pins operate at 3.3V logic. Feeding 5V into any GPIO pin (like BCM 17 or 27) will permanently destroy the SoC's peripheral block. The BME280 breakout handles its own 3.3V regulation, but the button and LED must be wired strictly to the 3.3V rail.
| Physical Pin | BCM GPIO | Function in this Build | Connected To | Wire Color |
|---|---|---|---|---|
| 1 | 3V3 Power | VCC Supply | BME280 VIN, Button Common | Red |
| 3 | GPIO 2 (SDA1) | I2C Data | BME280 SDI/SDA | Yellow |
| 5 | GPIO 3 (SCL1) | I2C Clock | BME280 SCK/SCL | Orange |
| 6 | GND | Ground Return | BME280 GND, LED Cathode | Black |
| 11 | GPIO 17 | Digital Output | 330Ω Resistor -> LED Anode | Green |
| 13 | GPIO 27 | Digital Input (Pull-down) | Tactile Switch NO Pin | Blue |
Wiring Procedure & OS Configuration
Raspberry Pi OS Bookworm (the standard for 2026) disabled the legacy I2C kernel module by default and deprecated the RPi.GPIO Python library. Follow these steps to configure the environment correctly.
- Physical Wiring: Power down the Pi completely. Connect the BME280 to pins 1, 3, 5, and 6. Wire the LED anode to GPIO 17 via the 330Ω resistor, and the cathode to GND. Wire one side of the button to GPIO 27 and the other to 3V3.
- Enable I2C: Boot the Pi, open a terminal, and run
sudo raspi-config. Navigate to Interface Options -> I2C and enable it. Reboot. - Install Dependencies: We use
gpiozerofor hardware abstraction andsmbus2for raw I2C byte manipulation.sudo apt update sudo apt install python3-gpiozero python3-smbus2 i2c-tools pip3 install RPi.bme280 --break-system-packages - Verify Hardware: Run
i2cdetect -y 1. You should see76or77in the grid. If the grid is empty, check your physical SDA/SCL connections.
Complete Python Control Script
This script targets the Raspberry Pi Zero 2 W (and original Zero W) running Python 3.11+. It initializes the BME280, sets up a hardware interrupt on the button, and toggles the LED while logging sensor data.
#!/usr/bin/env python3
import time
import sys
import bme280
from gpiozero import Button, LED
from smbus2 import SMBus
# --- Pin Definitions (BCM Numbering) ---
PIN_LED_STATUS = 17
PIN_BUTTON_INT = 27
# --- I2C Configuration ---
I2C_BUS_ID = 1
BME280_ADDR = 0x76 # Change to 0x77 if your breakout uses the alternate address
def setup_hardware():
"""Initialize GPIO and I2C peripherals with error handling."""
try:
# gpiozero handles pull-downs automatically for Button when wired to 3V3
button = Button(PIN_BUTTON_INT, bounce_time=0.05, pull_up=False)
led = LED(PIN_LED_STATUS)
bus = SMBus(I2C_BUS_ID)
calibration_params = bme280.load_calibration_params(bus, BME280_ADDR)
return button, led, bus, calibration_params
except PermissionError:
print('ERROR: Permission denied. Ensure your user is in the i2c group.')
sys.exit(1)
except FileNotFoundError:
print('ERROR: I2C bus not found. Did you enable I2C in raspi-config?')
sys.exit(1)
def log_sensor_data(bus, params):
"""Read and print BME280 data."""
try:
data = bme280.sample(bus, BME280_ADDR, params)
print(f'[{time.strftime("%H:%M:%S")}] Temp: {data.temperature:.2f}C | '
f'Hum: {data.humidity:.1f}% | Press: {data.pressure:.1f}hPa')
except OSError as e:
print(f'I2C Read Fault: {e}')
def main():
button, led, bus, params = setup_hardware()
print('System ready. Press the button to toggle logging. Ctrl+C to exit.')
logging_active = False
# Hardware interrupt callback
def toggle_logging():
nonlocal logging_active
logging_active = not logging_active
led.value = logging_active
state = 'STARTED' if logging_active else 'STOPPED'
print(f'\n--- Logging {state} ---')
button.when_pressed = toggle_logging
led.off()
try:
while True:
if logging_active:
log_sensor_data(bus, params)
time.sleep(1.0)
except KeyboardInterrupt:
print('\nGraceful shutdown triggered.')
finally:
led.off()
bus.close()
print('Pins reset and I2C bus closed.')
if __name__ == '__main__':
main()
Debugging: "OSError: [Errno 121] Remote I/O error"
When working with the Raspberry Pi Zero W GPIO and I2C bus, you will inevitably encounter the dreaded OSError: [Errno 121] Remote I/O error. This is a kernel-level rejection indicating the Pi sent a clock pulse but received no ACK (acknowledge) bit from the slave device.
The First Three Things to Check
- Verify the Address with i2cdetect: Run
i2cdetect -y 1. If your BME280 shows up as77but your code says0x76, the script will throw Errno 121. Update theBME280_ADDRvariable. - Multimeter Continuity Test: Power down. Set your multimeter to continuity. Probe Physical Pin 3 (SDA) to the SDA pad on the BME280. Probe Physical Pin 5 (SCL) to the SCL pad. A single loose Dupont wire crimp causes 90% of these errors.
- Check for Missing Pull-ups: Measure the voltage on the SDA and SCL lines while the Pi is idle. They should sit at ~3.3V. If they read 0V or float randomly, your breakout board lacks pull-up resistors and the bus is dead.
Ranked Causes for Persistent Errno 121
| Rank | Cause | Fix / Action |
|---|---|---|
| 1 | Loose breadboard contact | Squeeze Dupont pins with pliers or solder direct. |
| 2 | Incorrect I2C Address | Check i2cdetect; change code variable. |
| 3 | Bus Capacitance Overload | Shorten wires. I2C max capacitance is 400pF (~30cm). |
| 4 | Sensor in Sleep/Bricked | Power cycle the Pi completely (unplug USB). |
Extending and Simplifying the Build
The beauty of the Raspberry Pi Zero W GPIO layout is its scalability. Depending on your deployment environment, you can easily adapt this circuit.
How to Simplify (The Minimalist Node)
If you are building a battery-powered remote sensor node, strip away the LED and button. Rely on a cron job to run a headless version of the script every 15 minutes. To maximize battery life, use the BME280's 'forced' sleep mode (available in advanced configuration of the bme280 library), which drops the sensor's quiescent current from 3.6mA down to roughly 0.1μA between reads. Power the Pi via a LiFePO4 pack and a USB-C buck converter set to exactly 5.1V.
How to Extend (The Multi-Sensor Array)
The I2C bus supports up to 127 devices, but you are limited by address collisions. To add a second BME280, you must use a board with an address jumper (like the Adafruit 2652) and cut the default trace to switch it to 0x77. For mixing protocols, utilize the Pi's SPI0 bus (Physical pins 19, 21, 23, 24, 26) to add an RFM95W LoRa transceiver, turning this Zero W build into a long-range, off-grid environmental telemetry node. When mixing SPI and I2C, ensure your SPI chip select (CE0/CE1) lines have 10kΩ pull-ups to prevent false triggers during Pi boot sequences.
For deeper dives into the gpiozero pin factories and advanced interrupt handling, refer to the official gpiozero documentation. For OS-level I2C troubleshooting, the Raspberry Pi OS manual remains the definitive reference.






