The 40-pin GPIO header on the Raspberry Pi 3 is a deceptively simple interface. While the physical layout is identical to the Pi 2 and Pi 4, the Pi 3 Model B and B+ have specific power delivery quirks—namely a strictly limited 3.3V LDO regulator and a 1.4A USB current limit—that dictate how you can safely load those pins. If you treat the Pi 3 header exactly like a Pi 4, you will eventually brownout your board or fry a sensor.
This guide cuts through the abstraction. We are targeting the Raspberry Pi 3 Model B+ (1GB RAM) running Raspberry Pi OS Bookworm. We will build a hardware-interrupted I2C environmental logger, map the exact pins, and systematically debug the most common I2C failure mode on this specific board.
The Raspberry Pi 3 Pins Decision Matrix
Before stripping wires, you need to lock in your pin numbering scheme and communication bus. Novices often mix BOARD (physical pin number) and BCM (Broadcom GPIO number) in the same script, resulting in short circuits or silent failures. Use this decision tree to lock in your configuration.
| Condition / Requirement | Option A | Option B | Verdict |
|---|---|---|---|
| Using modern Python libs (gpiozero, Adafruit Blinka)? | BCM Numbering | BOARD Numbering | Pick BCM. Libraries map directly to Broadcom chip pins. |
| Translating directly from a physical wiring diagram? | BCM Numbering | BOARD Numbering | Pick BOARD. Matches the physical 1-40 silkscreen on the board. |
| Sensor needs >1Mbps data rate or strict timing? | I2C Bus | SPI Bus | Pick SPI. I2C caps out around 400kHz on the Pi 3 without clock stretching issues. |
| Need to conserve GPIO pins for other peripherals? | I2C Bus | SPI Bus | Pick I2C. Uses 2 shared wires (SDA/SCL) vs SPI's 4+ wires. |
gpiozero and adafruit-circuitpython libraries used below.
Project Build: Hardware-Interrupted I2C Logger
We are building an environmental logger that sits idle until a physical button is pressed, triggering an I2C read from a BME280 sensor and blinking a status LED. This exercises I2C, digital output, and internal pull-up input pins.
Parts List & Exact Variants
- Compute: Raspberry Pi 3 Model B+ (1GB RAM) - ~$45 on secondary market in 2026
- Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) - $19.95. (Includes onboard 10k pull-ups and 3.3V regulator).
- Indicator: Standard 5mm Red LED + 330Ω carbon film resistor
- Input: 6x6mm Tactile pushbutton (SPST-NO)
- Wiring: Female-to-female Dupont jumper wires (20cm length to minimize I2C bus capacitance)
Pin Mapping Table
This table bridges the physical header (what you count with your fingers) and the BCM GPIO (what you type in Python). Reference pinout.xyz for the visual diagram.
| Physical Pin | BCM GPIO | Function | Connected To |
|---|---|---|---|
| Pin 1 | N/A (3.3V) | Power | BME280 VIN |
| Pin 3 | GPIO 2 | I2C SDA | BME280 SDI |
| Pin 5 | GPIO 3 | I2C SCL | BME280 SCK |
| Pin 6 | N/A (GND) | Ground | BME280 GND |
| Pin 11 | GPIO 17 | Digital Out | 330Ω Resistor -> LED Anode |
| Pin 31 | GPIO 6 | Digital In | Button Switch (Normally Open) |
| Pin 39 | N/A (GND) | Ground | Button Switch & LED Cathode |
Wiring the Pi 3 Model B+ Header
- De-energize the board. Unplug the Pi 3's micro-USB power supply. Never hot-plug I2C sensors on the Pi 3; the I2C bus lines can latch up if powered while floating.
- Wire the I2C Bus. Connect Pin 1 (3.3V) to BME280 VIN. Connect Pin 6 (GND) to BME280 GND. Connect Pin 3 (SDA) to BME280 SDI. Connect Pin 5 (SCL) to BME280 SCK.
- Wire the LED Output. Insert the 330Ω resistor into the breadboard. Connect Pin 11 (GPIO 17) to the resistor's anode side. Connect the resistor's cathode side to the LED's long leg (anode). Connect the LED's short leg (cathode) to Pin 39 (GND).
- Wire the Button Input. Place the tactile switch across the breadboard trench. Connect one side of the switch to Pin 31 (GPIO 6). Connect the opposite side of the switch to Pin 39 (GND). Note: We rely on the Pi's internal software pull-up resistor in the code; no external 10k pull-up is needed.
- Verify continuity. Use a multimeter in continuity mode. Check that Pin 6 and Pin 39 both beep to your common ground bus. Ensure SDA and SCL are not swapped.
Python Implementation with Robust Error Handling
This script targets the Pi 3 Model B+. It uses gpiozero for the digital I/O (standard in modern Pi OS) and Adafruit's Blinka/CircuitPython libraries for the BME280. Install dependencies first: sudo apt install python3-gpiozero i2c-tools && pip3 install adafruit-circuitpython-bme280.
import time
import board
import busio
import adafruit_bme280
from gpiozero import LED, Button
from signal import pause
# Target: Raspberry Pi 3 Model B+ (1GB RAM)
# Pin Definitions (BCM Numbering)
LED_PIN = 17 # Physical Pin 11
BUTTON_PIN = 6 # Physical Pin 31
# Initialize digital I/O
led = LED(LED_PIN)
# pull_up=True enables the internal 50k pull-up resistor on GPIO 6
button = Button(BUTTON_PIN, pull_up=True, bounce_time=0.05)
# Initialize I2C bus (Bus 1 is the default for Pi 3 header)
i2c = busio.I2C(board.SCL, board.SDA)
# Instantiate sensor globally to avoid bus lockups on repeated calls
# Adafruit BME280 (Product ID: 2652) defaults to I2C address 0x77
try:
bme280 = adafruit_bme280.basic.Adafruit_BME280_I2C(i2c, address=0x77)
bme280.sea_level_pressure = 1013.25
except ValueError as e:
print(f'FATAL BOOT ERROR: {e}. Check I2C address and wiring.')
raise
def log_sensor():
try:
temp_c = bme280.temperature
humidity = bme280.relative_humidity
pressure = bme280.pressure
print(f'[OK] Temp: {temp_c:.2f}C | Hum: {humidity:.1f}% | Press: {pressure:.1f}hPa')
led.blink(on_time=0.2, off_time=0.2, n=3, background=True)
except OSError as e:
# Exact error string catch for I2C bus drops
if e.errno == 121:
print('FAULT: OSError: [Errno 121] Remote I/O error. I2C bus dropped or NACK received.')
else:
print(f'FAULT: I2C Read OSError {e.errno}: {e}')
except Exception as e:
print(f'FAULT: Unexpected runtime error: {e}')
def on_button_press():
print('Interrupt triggered via GPIO 6...')
log_sensor()
# Bind hardware interrupt to function
button.when_pressed = on_button_press
print('System ready. Press button on GPIO 6 to log environmental data.')
pause() # Keeps script alive efficiently without polling
Debugging: Fixing 'OSError: [Errno 121] Remote I/O error'
If your script crashes or prints FAULT: OSError: [Errno 121] Remote I/O error, the Linux kernel's I2C driver is reporting a NACK (Not Acknowledged) from the slave device. The Pi 3 sent a clock pulse, but the BME280 didn't pull the SDA line low to respond. Here is the ranked cause list and the first three things to check.
The First 3 Things to Check
- Run
i2cdetect -y 1in the terminal. If you see a grid of dashes with no77(or76), the OS cannot see the hardware at all. If it hangs or throws an error, the I2C kernel module isn't loaded. - Verify the I2C interface is enabled in OS Bookworm. Unlike older OS versions where you edited
/boot/config.txt, Raspberry Pi OS Bookworm mounts the boot partition at/boot/firmware/. Open/boot/firmware/config.txtand ensure the linedtparam=i2c_arm=onis present and uncommented. Reboot after changing. - Measure the 3.3V rail under load. Put your multimeter probes on Physical Pin 1 (3.3V) and Pin 6 (GND). If it reads below 3.2V, the Pi 3's onboard LDO is browning out. The Pi 3 B+ is notorious for voltage sag if powered via a low-quality micro-USB cable.
Ranked Causes for Errno 121
| Rank | Cause | Fix / Verification |
|---|---|---|
| 1 | Missing or failed I2C pull-up resistors | The Pi 3 does not have onboard pull-ups for SDA/SCL. You must use a breakout board (like the Adafruit 2652) that includes them. Verify with a multimeter: SDA and SCL should read ~3.3V relative to GND when idle. |
| 2 | Incorrect I2C Address in Code | Generic clone BME280s often default to 0x76 instead of Adafruit's 0x77. Change the address=0x77 parameter in the Python script to 0x76 if i2cdetect shows 76. |
| 3 | Excessive I2C Bus Capacitance | If your Dupont wires are longer than 30cm, the capacitance ruins the 3.3V logic rise times. Shorten wires or drop the I2C baud rate by adding dtparam=i2c_arm_baudrate=50000 to config.txt. |
| 4 | Wire swapped (SDA to SCL) | Physically trace Pin 3 (SDA) and Pin 5 (SCL). Swapping them won't fry the Pi, but guarantees an Errno 121. |
Extending and Simplifying the Build
Once the baseline I2C read and GPIO interrupt are stable, you can scale the project up or strip it down based on your deployment needs.
How to Simplify (Stripping it Down)
If you don't need the physical button interrupt and just want a cron-jobbed data logger, remove the gpiozero button logic entirely. Replace the pause() and interrupt binding with a simple while True: loop containing log_sensor() and time.sleep(60). This reduces CPU wake-states and is ideal for headless Pi 3 deployments running on solar/battery setups where every milliamp counts.
How to Extend (Scaling Up)
To turn this into a proper IoT node, integrate the paho-mqtt library. Inside the log_sensor() try block, format the temperature and humidity into a JSON payload and publish it to an MQTT broker (like Mosquitto running on a local server). Because the Pi 3 Model B+ includes 2.4GHz 802.11n WiFi, you can push this data wirelessly without adding a USB Ethernet adapter. Just ensure you add a 10-second time.sleep() after connecting to the broker to allow the WiFi handshake to complete before the first sensor read, preventing socket timeout errors.
For comprehensive official documentation on the Pi 3 header limits, always refer to the Raspberry Pi Foundation GPIO Guide and the Adafruit BME280 Learn Guide for sensor-specific timing constraints.






