Learning how to use the Raspberry Pi for hardware interfacing starts with mastering the I2C (Inter-Integrated Circuit) bus. To use the Raspberry Pi for hardware projects, you enable the I2C interface via raspi-config, wire the GPIO pins to your sensor's SDA/SCL lines, and poll the hardware registers using Python. This guide targets the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS (Bookworm or later), reading a Microchip MCP9808 high-accuracy I2C temperature sensor and logging the data to a CSV file.
Raspberry Pi 5 I2C Hardware and Sensor Specifications
The Raspberry Pi 5 uses the BCM2712 SoC, which features a different I2C controller implementation than the BCM2711 found on the Pi 4. Most notably, the Pi 5 handles I2C clock stretching differently, which can cause issues with slower sensors if the bus baudrate is too high. Below is a data-dense comparison of the hardware specifications you need to account for when designing your circuit.
| Specification | Raspberry Pi 4 (BCM2711) | Raspberry Pi 5 (BCM2712) | MCP9808 Sensor Spec |
|---|---|---|---|
| I2C Bus Logic Voltage | 3.3V | 3.3V | 1.71V to 5.5V (VDD) |
| Default I2C Bus | I2C1 (Pins 3, 5) | I2C1 (Pins 3, 5) | Addr: 0x18 (Default) |
| Internal Pull-up Resistors | ~1.8 kΩ | ~1.8 kΩ | N/A (Requires external) |
| Max Recommended Bus Speed | 400 kHz (Fast Mode) | 1 MHz (Fast+ Mode) | 400 kHz max |
| Temperature Resolution | N/A | N/A | 0.0625°C (13-bit) |
Parts List and Pin Mapping
Before wiring, gather the exact components listed below. Prices reflect typical 2026 retail costs for genuine hardware.
- Microcontroller: Raspberry Pi 5 (8GB RAM) — ~$80.00
- Power Supply: Official Raspberry Pi 27W USB-C PD Power Supply — ~$12.00
- Sensor: Adafruit MCP9808 Breakout Board (Product ID 1782) — ~$14.50
- Wiring: Female-to-Female Dupont Jumper Wires (20cm, 4-pin ribbon)
- Storage: 32GB MicroSD Card (Class A1 or better for logging)
GPIO Pin Mapping Table
The Raspberry Pi uses the Broadcom (BCM) GPIO numbering system in software, but physical pin numbers on the header. Always wire using the physical pin layout to avoid frying your board.
| Pi 5 Physical Pin | BCM GPIO | Function | MCP9808 Breakout Pin |
|---|---|---|---|
| 1 | 3.3V Power | VCC | VIN (or VDD) |
| 6 | GND | Ground | GND |
| 3 | GPIO 2 | I2C SDA (Data) | SDA |
| 5 | GPIO 3 | I2C SCL (Clock) | SCL |
Step-by-Step Wiring and OS Configuration
Follow these steps to configure the Raspberry Pi 5 OS and verify the I2C bus before writing any code.
- Wire the Breakout: Connect the 4 pins from the MCP9808 to the Pi 5 GPIO header exactly as mapped in the table above. Double-check that 3.3V goes to VIN and not 5V, though the Adafruit breakout has an onboard regulator, raw 5V into a 3.3V logic line can damage the Pi's BCM2712 SoC.
- Boot and Update: Power on the Pi 5. Open a terminal and run
sudo apt update && sudo apt upgrade -yto ensure your kernel and firmware are current. - Enable I2C Interface: Run
sudo raspi-config. Navigate to Interface Options > I2C and select Yes to enable the ARM I2C interface. - Cap the I2C Baudrate: Open the Pi 5 boot config file by typing
sudo nano /boot/firmware/config.txt. Add the following line at the bottom to force the bus to 400 kHz:dtparam=i2c_baudrate=400000
Save (Ctrl+O) and exit (Ctrl+X). - Install I2C Tools: Run
sudo apt install i2c-tools python3-smbus2 -y. - Verify Hardware Connection: Reboot the Pi (
sudo reboot), then runi2cdetect -y 1. You should see18in the grid output, confirming the sensor is present at address 0x18.
Complete Python I2C Logging Script
This script uses the smbus2 library to read raw I2C registers directly from the MCP9808. It includes pin definitions, bitwise parsing for the 13-bit temperature data, CSV logging, and robust error handling for I2C bus faults.
import smbus2
import time
import csv
import os
from datetime import datetime
# --- PIN & I2C DEFINITIONS ---
I2C_BUS = 1 # Pi 5 uses I2C1 on pins 3 & 5
MCP9808_ADDR = 0x18 # Default I2C address for MCP9808
TEMP_REG = 0x05 # Ambient Temperature Register
CONFIG_REG = 0x01 # Configuration Register
# --- FILE DEFINITIONS ---
LOG_FILE = "temp_log.csv"
POLL_INTERVAL_SEC = 5 # Read every 5 seconds
def setup_sensor(bus):
"""Wake up the sensor and set resolution to maximum (0.0625C)."""
try:
# Write 0x00 to config register to ensure it's in continuous mode
bus.write_word_data(MCP9808_ADDR, CONFIG_REG, 0x0000)
print("Sensor initialized successfully.")
except OSError as e:
print(f"Failed to initialize sensor: {e}")
raise
def read_temperature(bus):
"""Reads the 16-bit raw temperature register and parses to Celsius."""
# Read 2 bytes from the temperature register
raw_data = bus.read_i2c_block_data(MCP9808_ADDR, TEMP_REG, 2)
# Combine bytes into a 16-bit integer
raw_temp = (raw_data[0] << 8) | raw_data[1]
# Check if temperature is below 0°C (sign bit is bit 12)
if raw_temp & 0x1000:
# Negative temperature: clear sign bits and subtract from 256
raw_temp &= ~0x7000 # Clear flags (bits 15, 14, 13)
raw_temp &= 0x0FFF # Keep only the 12 data bits
temp_c = 256.0 - (raw_temp * 0.0625)
else:
# Positive temperature: clear flags and multiply by resolution
raw_temp &= 0x0FFF
temp_c = raw_temp * 0.0625
return temp_c
def main():
# Initialize I2C bus
bus = smbus2.SMBus(I2C_BUS)
setup_sensor(bus)
# Setup CSV logging
file_exists = os.path.isfile(LOG_FILE)
with open(LOG_FILE, mode='a', newline='') as file:
writer = csv.writer(file)
if not file_exists:
writer.writerow(["Timestamp", "Temperature_C", "Temperature_F"])
print(f"Logging to {LOG_FILE} every {POLL_INTERVAL_SEC} seconds. Press Ctrl+C to stop.")
try:
while True:
try:
temp_c = read_temperature(bus)
temp_f = (temp_c * 9.0 / 5.0) + 32.0
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Write to CSV
with open(LOG_FILE, mode='a', newline='') as file:
writer = csv.writer(file)
writer.writerow([timestamp, f"{temp_c:.4f}", f"{temp_f:.4f}"])
print(f"[{timestamp}] {temp_c:.2f}°C / {temp_f:.2f}°F")
time.sleep(POLL_INTERVAL_SEC)
except OSError as e:
# Catch I2C bus errors without crashing the main loop
print(f"I2C Read Error: {e}. Retrying in 5s...")
time.sleep(5)
except KeyboardInterrupt:
print("\nLogging stopped by user.")
finally:
bus.close()
if __name__ == "__main__":
main()
Debugging I2C Failures: "Remote I/O Error"
When working with raw I2C on the Pi 5, the most common point of failure is the bus dropping out or failing to acknowledge a transaction. If your script crashes, you will likely see this exact error string:
OSError: [Errno 121] Remote I/O error
This error means the Raspberry Pi sent a clock pulse and data, but the sensor did not pull the SDA line low to acknowledge (ACK) the byte. Here are the first three things to check when this happens:
- Run
i2cdetect -y 1: If the grid shows all--or throws an error, your hardware connection is broken. If it showsUU, the kernel driver has claimed the device and Python cannot access it directly. - Verify Ground Continuity: Use a multimeter in continuity mode. Place one probe on the Pi's Pin 6 (GND) and the other on the sensor's GND pin. It must read < 1 ohm. A floating ground will cause intermittent I/O errors.
- Check Logic Levels: Measure the voltage between the sensor's VCC and GND pins while the Pi is powered. It should read between 3.2V and 3.4V. If it reads 0V, your 3.3V rail is disconnected.
Ranked Causes for Errno 121
| Rank | Cause | Fix |
|---|---|---|
| 1 | I2C Baudrate too high (Clock Stretching failure) | Add dtparam=i2c_baudrate=100000 to /boot/firmware/config.txt and reboot. |
| 2 | Missing or weak Pull-up Resistors | The Pi's internal 1.8k pull-ups are weak. Add external 4.7kΩ resistors between SDA/SCL and 3.3V. |
| 3 | Address Collision or Mismatch | Check the A0, A1, A2 solder jumpers on the MCP9808. If bridged, the address shifts from 0x18. |
| 4 | Loose Dupont Wires / Breadboard Contact | Replace jumper wires. Breadboard contacts degrade over time, causing micro-disconnects during reads. |
Extending and Simplifying the Build
Depending on your project goals, you may want to abstract away the bitwise math or push the data to a network dashboard.
How to Simplify the Code
If you do not want to manage raw I2C registers and bitwise operations, you can simplify the build by using Adafruit's CircuitPython libraries. Install the Blinka environment and the specific sensor library:
pip3 install adafruit-circuitpython-mcp9808
This reduces the Python code to roughly five lines, abstracting the smbus2 register reads into a simple sensor.temperature property. The trade-off is a heavier dependency footprint and slightly slower execution time, which is negligible for a 5-second polling interval but matters if you are logging at 100Hz.
How to Extend to Home Assistant (MQTT)
To turn this local logger into a smart home node, extend the script using the paho-mqtt library. Instead of writing to a CSV, publish the parsed temp_c variable to an MQTT broker:
import paho.mqtt.client as mqtt
client = mqtt.Client("Pi5_Temp_Node")
client.connect("192.168.1.100", 1883, 60)
# Inside your main loop, replace the CSV write with:
client.publish("homeassistant/sensor/livingroom/temp", payload=f"{temp_c:.2f}", qos=1)
By combining the BCM2712's robust I2C controller with a high-precision sensor like the MCP9808, you bypass the ±2°C inaccuracy of the Pi's internal SoC temperature sensor, yielding lab-grade environmental data for your workshop or server rack.






