The Core Decision: Interactive vs. Non-Interactive Config Execution
When deploying embedded projects on a Raspberry Pi, enabling hardware interfaces like I2C, SPI, or UART requires modifying the bootloader configuration. The raspberry pi config command (officially raspi-config) is the standard tool for this, but makers frequently hit a wall when transitioning from a bench monitor to a headless SSH deployment. The tool operates in two distinct modes, and choosing the wrong one leads to silent failures in automated scripts.
| Criteria | Interactive (sudo raspi-config) | Non-Interactive (sudo raspi-config nonint) |
|---|---|---|
| Fleet Size | Single board on the bench | Multiple boards or automated CI/CD |
| SSH Availability | Direct monitor/keyboard attached | Headless SSH only |
| Error Feedback | Visual prompts and confirmations | Silent exit codes (0 for success) |
| Parameter Logic | Standard Yes/No menus | Backwards boolean (0=Enable, 1=Disable) |
1 actually disables the interface. Reserve nonint strictly for bash provisioning scripts.
Hardware Spec Sheet & I2C Pin Mapping
To demonstrate the debugging process, we will wire a Bosch BME280 environmental sensor to the primary I2C bus. This setup targets the current standard for embedded Linux projects.
| Component | Exact Variant / Model | Approx. Cost (2026) |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB RAM) | $80.00 |
| Sensor | Adafruit BME280 I2C/SPI Breakout (Product #2652) | $14.95 |
| Wiring | Silicone female-to-female jumper wires (26 AWG) | $6.00 |
| OS | Raspberry Pi OS (Debian Bookworm, 64-bit Lite) | Free |
The Raspberry Pi 5 retains the standard 40-pin header layout for primary I2C, but the underlying device tree overlays have shifted slightly in the Bookworm release. Always wire to the dedicated hardware I2C pins rather than attempting software bit-banging, which introduces unacceptable jitter for sensor polling.
| Pi 5 Pin # | GPIO / Function | BME280 Breakout Pin | Wire Color |
|---|---|---|---|
| 1 | 3V3 Power | VIN | Red |
| 6 | Ground | GND | Black |
| 3 | GPIO 2 (SDA1) | SDA | Blue |
| 5 | GPIO 3 (SCL1) | SCL | Yellow |
Executing the Config Command: Step-by-Step Setup
With the hardware wired, the I2C bus is disabled by default on a fresh Raspberry Pi OS Lite image. You must execute the config command to load the i2c_dev kernel module and apply the device tree overlay.
- SSH into your Pi 5:
ssh pi@raspberrypi.local - Launch the interactive tool:
sudo raspi-config - Use the arrow keys to navigate to 3 Interface Options and press Enter.
- Select I4 I2C and press Enter.
- When prompted 'Would you like the ARM I2C interface to be enabled?', select <Yes>.
- Exit the tool and apply the reboot prompt.
sudo raspi-config nonint do_i2c 0. Notice the 0. In the raspi-config non-interactive API, 0 means ON and 1 means OFF. Passing a 1 will silently disable the bus and break your deployment. See the official Raspberry Pi configuration documentation for the full non-integer parameter list.
Debugging I2C Failures: Exact Errors and Ranked Causes
When your Python script fails to read the sensor, do not guess. Follow this strict diagnostic path. These are the first three things to check when the bus fails to initialize.
Check 1: Is the kernel module loaded?
Run lsmod | grep i2c_dev. If it returns blank, the config command failed to persist across reboot. Re-run raspi-config.
Check 2: Is the overlay active in the bootloader?
Run cat /boot/firmware/config.txt | grep dtparam=i2c. You must see dtparam=i2c_arm=on. (Note: On Bookworm, the path is /boot/firmware/, not the legacy /boot/).
Check 3: Is the device physically responding?
Install the tools (sudo apt install i2c-tools) and run i2cdetect -y 1. The BME280 should appear at 0x76 or 0x77. If the grid is empty, you have a physical wiring fault.
When your Python code executes, it will throw specific OSError exceptions. Here is the ranked cause list for the exact error strings you will encounter:
| Exact Error String | Rank | Root Cause & Fix |
|---|---|---|
OSError: [Errno 2] No such file or directory | 1 | The /dev/i2c-1 device node does not exist. The raspi-config command was not run, or the Pi was not rebooted after running it. |
OSError: [Errno 121] Remote I/O error | 1 | The I2C bus exists, but the sensor did not ACK the address. Causes: SDA/SCL swapped, missing 3.3V power to the breakout, or using I2C bus 0 instead of 1. |
OSError: [Errno 16] Device or resource busy | 2 | Another process holds the file handle. Usually caused by leaving an i2cdetect or htop process running in another SSH tab. |
ValueError: Invalid I2C address | 3 | Code is targeting 0x76 but the physical breakout has the SDO pin pulled high (address 0x77). Check the Adafruit BME280 breakout docs for jumper pad settings. |
Python Implementation with Robust Error Handling
The following script uses the smbus2 library to read the BME280 Chip ID register. This is the ultimate embedded debugging step: if you can read the hardcoded Chip ID (0x60), your I2C bus, config command, and physical wiring are 100% verified, regardless of higher-level library compensation math.
import smbus2
import sys
import time
# =========================================================
# TARGET BOARD: Raspberry Pi 5 (8GB) - Debian Bookworm
# I2C BUS: 1
# PIN DEFINITIONS:
# SDA -> GPIO 2 (Physical Pin 3)
# SCL -> GPIO 3 (Physical Pin 5)
# SENSOR: Bosch BME280 (Adafruit #2652)
# Default I2C Address: 0x76 (SDO to GND)
# =========================================================
I2C_BUS = 1
BME280_ADDR = 0x76
CHIP_ID_REG = 0xD0
EXPECTED_CHIP_ID = 0x60
def verify_i2c_connection():
"""Attempts to read the BME280 Chip ID to verify bus integrity."""
try:
# Initialize the SMBus on I2C Bus 1
bus = smbus2.SMBus(I2C_BUS)
# Read 1 byte from the Chip ID register
chip_id = bus.read_byte_data(BME280_ADDR, CHIP_ID_REG)
if chip_id == EXPECTED_CHIP_ID:
print(f"[SUCCESS] I2C Bus verified. BME280 Chip ID: 0x{chip_id:02X}")
return True
else:
print(f"[WARNING] Device responded, but Chip ID is 0x{chip_id:02X} (Expected 0x60).")
print("Check if you are accidentally communicating with a BMP280 or BME680.")
return False
except FileNotFoundError as e:
# Catches: OSError: [Errno 2] No such file or directory
print(f"[FATAL] {e}")
print("Fix: Run 'sudo raspi-config' to enable I2C, then reboot.")
sys.exit(1)
except OSError as e:
if e.errno == 121:
# Catches: OSError: [Errno 121] Remote I/O error
print(f"[FATAL] {e}")
print("Fix: Check physical wiring. Run 'i2cdetect -y 1' to verify address.")
sys.exit(1)
elif e.errno == 16:
# Catches: OSError: [Errno 16] Device or resource busy
print(f"[FATAL] {e}")
print("Fix: Close other terminal sessions running i2c-tools.")
sys.exit(1)
else:
print(f"[FATAL] Unhandled OS Error: {e}")
sys.exit(1)
except Exception as e:
print(f"[FATAL] Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
print("Starting I2C bus verification sequence...")
time.sleep(1) # Allow bus to settle after boot
verify_i2c_connection()
Extending and Simplifying the Build
Once the raspi-config command has successfully opened the I2C bus and the raw register read verifies the hardware, you have a decision to make regarding the software stack.
How to Simplify:
If you do not need to write custom low-level I2C scripts and just want temperature/humidity data for a dashboard, abandon smbus2. Install the Adafruit Blinka ecosystem (pip3 install adafruit-circuitpython-bme280). This abstracts the I2C bus handling and includes the complex Bosch compensation algorithms for humidity and pressure out of the box. The config command setup remains identical.
How to Extend:
To add a high-speed device like an SPI TFT display alongside your I2C sensor, you must return to the config command to enable the SPI bus. Use the interactive tool to enable I5 SPI. Physically wire the display to the Pi 5's SPI0 pins (MOSI on Pin 19, MISO on Pin 21, SCLK on Pin 23, CE0 on Pin 24). Because SPI and I2C operate on entirely different hardware peripherals within the BCM2712 SoC, enabling both via raspi-config will not cause bus contention, provided you respect the distinct chip-select lines.






