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.

Decision Matrix: Which Config Mode to Use
CriteriaInteractive (sudo raspi-config)Non-Interactive (sudo raspi-config nonint)
Fleet SizeSingle board on the benchMultiple boards or automated CI/CD
SSH AvailabilityDirect monitor/keyboard attachedHeadless SSH only
Error FeedbackVisual prompts and confirmationsSilent exit codes (0 for success)
Parameter LogicStandard Yes/No menusBackwards boolean (0=Enable, 1=Disable)
The Concrete Pick: If you are actively debugging a sensor failure, use the Interactive Mode. The visual confirmation prevents the notorious 'backwards boolean' trap of the non-interactive command, where passing a 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.

Project Bill of Materials
ComponentExact Variant / ModelApprox. Cost (2026)
MicrocontrollerRaspberry Pi 5 (8GB RAM)$80.00
SensorAdafruit BME280 I2C/SPI Breakout (Product #2652)$14.95
WiringSilicone female-to-female jumper wires (26 AWG)$6.00
OSRaspberry 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 to BME280 I2C Pin Mapping
Pi 5 Pin #GPIO / FunctionBME280 Breakout PinWire Color
13V3 PowerVINRed
6GroundGNDBlack
3GPIO 2 (SDA1)SDABlue
5GPIO 3 (SCL1)SCLYellow

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.

  1. SSH into your Pi 5: ssh pi@raspberrypi.local
  2. Launch the interactive tool: sudo raspi-config
  3. Use the arrow keys to navigate to 3 Interface Options and press Enter.
  4. Select I4 I2C and press Enter.
  5. When prompted 'Would you like the ARM I2C interface to be enabled?', select <Yes>.
  6. Exit the tool and apply the reboot prompt.
Headless Scripting Trap: If you must use the non-interactive command in a bash script, the syntax to enable I2C is 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:

I2C Python Error Strings & Ranked Causes
Exact Error StringRankRoot Cause & Fix
OSError: [Errno 2] No such file or directory1The /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 error1The 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 busy2Another process holds the file handle. Usually caused by leaving an i2cdetect or htop process running in another SSH tab.
ValueError: Invalid I2C address3Code 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.