A robust headless raspberry pi computer setup for embedded control requires Raspberry Pi OS Lite (64-bit), SSH enabled via the Imager's advanced settings, and the I2C bus activated in the config.txt boot file. Unlike a desktop environment, an embedded Pi must boot directly into a minimal shell, automatically execute sensor polling scripts via systemd, and gracefully handle I2C bus faults without a monitor attached.
This guide walks through provisioning a Raspberry Pi 5 as a dedicated I2C telemetry node, reading a BME280 environmental sensor, and publishing data over MQTT. We will also address the specific I2C clock-stretching hardware quirks introduced with the Pi 5's BCM2712 SoC.
Hardware BOM & Embedded Spec Comparison
Before flashing the OS, verify your hardware. The code and pin mappings in this guide explicitly target the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Bookworm (64-bit Lite). If you are using a Pi 4 or Zero 2 W, the GPIO pinout remains identical for I2C Bus 1, but the I2C baudrate configurations in the boot file will differ.
- Compute: Raspberry Pi 5 (8GB RAM) with 27W USB-C PD Power Supply
- Sensor: Adafruit BME280 I2C Breakout (Product ID: 2652)
- Wiring: 4x Silicone stranded jumper wires (26 AWG)
- Storage: 32GB SanDisk High Endurance microSD (U3/V30 rated for continuous logging)
- Optional: Active Cooler (mandatory for Pi 5 in enclosed embedded housings)
Board Variant Comparison for Embedded Nodes
| Board Variant | I2C Default Clock | Max GPIO Sink/Source | Boot Time (OS Lite) | Approx 2026 Price |
|---|---|---|---|---|
| Raspberry Pi 5 (8GB) | 100 kHz (BCM2712) | 16 mA per pin / 50mA total | ~12 seconds | $80 USD |
| Raspberry Pi 4B (4GB) | 100 kHz (BCM2711) | 16 mA per pin / 50mA total | ~18 seconds | $55 USD |
| Raspberry Pi Zero 2 W | 100 kHz (BCM2710A1) | 16 mA per pin / 50mA total | ~24 seconds | $15 USD |
| Compute Module 4 (Lite) | 100 kHz (BCM2711) | 16 mA per pin / 50mA total | ~15 seconds | $65 USD |
Pi 5 I2C Pin Mapping
| Pi 5 Physical Pin | BCM GPIO | Function | Wire Color | BME280 Breakout Pin |
|---|---|---|---|---|
| 1 | N/A | 3V3 Power | Red | VIN |
| 6 | N/A | Ground | Black | GND |
| 3 | GPIO 2 | SDA1 (Data) | Blue | SDI / SDA |
| 5 | GPIO 3 | SCL1 (Clock) | Yellow | SCK / SCL |
Headless OS Provisioning & I2C Bus Configuration
Do not use the desktop version of Raspberry Pi OS for embedded nodes. The GUI consumes 300MB+ of RAM and introduces unnecessary background services that can cause latency spikes in sensor polling. Use Raspberry Pi OS Lite (64-bit).
- Flash the OS: Open the official Raspberry Pi Imager. Select your Pi 5 and choose Raspberry Pi OS (other) -> Raspberry Pi OS Lite (64-bit).
- Advanced Headless Settings: Click the gear icon (or press
Ctrl+Shift+X). Check Enable SSH (select "Use password authentication" for initial setup, then migrate to key-based). Set a strict hostname likeenv-node-01.local. Configure your WiFi SSID and password—note that Bookworm uses NetworkManager under the hood, so the oldwpa_supplicant.confdrop-in method is deprecated. - Boot and SSH: Insert the microSD, power the Pi 5, and wait 30 seconds. Connect via
ssh yourusername@env-node-01.local. - Enable I2C: Run
sudo raspi-config, navigate to Interface Options -> I2C, and enable it. Reboot. - Patch the Pi 5 I2C Clock Bug: The BCM2712 chip on the Pi 5 has a known hardware quirk where it drops I2C transactions if a sensor stretches the clock signal too long. Open the boot config:
sudo nano /boot/firmware/config.txt. Add the following line to the bottom to force the I2C baudrate down to a stable 10kHz:dtparam=i2c_arm_baudrate=10000
Save and reboot. This prevents silent data corruption with BME280 and SHT3x sensors. - Verify the Bus: Install the I2C tools (
sudo apt install i2c-tools) and runi2cdetect -y 1. You should see76or77in the grid output.
Python I2C Telemetry Code & Error Handling
This script polls the BME280 every 10 seconds and publishes a JSON payload to an MQTT broker. It uses the smbus2 and RPi.bme280 libraries for direct I2C register access, which is significantly lighter than loading the full Adafruit Blinka/CircuitPython stack on a headless Lite OS.
Install dependencies first:
sudo apt install python3-pip python3-venv
python3 -m venv env_sensors && source env_sensors/bin/activate
pip install smbus2 RPi.bme280 paho-mqtt
import smbus2
import bme280
import paho.mqtt.client as mqtt
import time
import json
import logging
# --- PIN & ADDRESS DEFINITIONS ---
I2C_BUS_ID = 1 # Physical pins 3 (SDA) and 5 (SCL) map to I2C Bus 1
BME280_I2C_ADDR = 0x76 # Default Adafruit BME280 address. Use 0x77 if SDO is tied high.
MQTT_BROKER_IP = "192.168.1.50"
MQTT_PORT = 1883
MQTT_TOPIC = "telemetry/lab/env_node_01"
# Setup basic logging for systemd journal integration
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Initialize I2C Bus
try:
bus = smbus2.SMBus(I2C_BUS_ID)
calibration_params = bme280.load_calibration_params(bus, BME280_I2C_ADDR)
logging.info(f"Successfully loaded BME280 calibration params from address {hex(BME280_I2C_ADDR)}")
except Exception as init_err:
logging.critical(f"Failed to initialize I2C bus or sensor: {init_err}")
exit(1)
# MQTT Callback
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
logging.info("Connected to MQTT Broker successfully.")
else:
logging.error(f"MQTT Connection failed with reason code: {reason_code}")
# Initialize MQTT Client (Paho MQTT v2.0 API)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
try:
client.connect(MQTT_BROKER_IP, MQTT_PORT, 60)
client.loop_start()
except Exception as mqtt_err:
logging.warning(f"MQTT Broker unreachable at startup: {mqtt_err}. Will retry in loop.")
# Main Telemetry Loop
try:
while True:
try:
# Read sensor data
data = bme280.sample(bus, BME280_I2C_ADDR, calibration_params)
payload = {
"temp_c": round(data.temperature, 2),
"humidity_pct": round(data.humidity, 2),
"pressure_hpa": round(data.pressure, 2),
"timestamp": int(time.time())
}
# Publish to MQTT
result = client.publish(MQTT_TOPIC, json.dumps(payload))
if result.rc == mqtt.MQTT_ERR_SUCCESS:
logging.debug(f"Published: {payload}")
else:
logging.warning(f"MQTT publish failed with code: {result.rc}")
time.sleep(10)
except OSError as io_err:
# Catch I2C hardware faults without crashing the entire script
logging.error(f"I2C Read Fault: {io_err}. Attempting bus reset in 5s.")
time.sleep(5)
# Re-initialize bus on failure
bus = smbus2.SMBus(I2C_BUS_ID)
calibration_params = bme280.load_calibration_params(bus, BME280_I2C_ADDR)
except KeyboardInterrupt:
logging.info("Telemetry script terminated by user.")
finally:
client.loop_stop()
client.disconnect()
bus.close()
logging.info("Clean shutdown complete.")
Debugging "[Errno 121] Remote I/O error"
If your script crashes or logs an I2C fault, you will almost certainly encounter this exact Python exception string: OSError: [Errno 121] Remote I/O error. In the smbus2 library, this is raised when the Linux I2C driver sends a read/write request but receives no ACK (acknowledge) bit from the sensor on the 9th clock cycle.
When this error strikes, here are the first three things to check, ranked from most likely to least likely:
- Verify the Address with i2cdetect: Run
i2cdetect -y 1. If the grid shows--at0x76but shows77, your sensor's SDO pin is pulled high. UpdateBME280_I2C_ADDR = 0x77in the Python script. If the grid is entirely empty, you have a physical wiring break or missing power. - Check for Pull-Up Resistors: The I2C protocol requires pull-up resistors on both SDA and SCL lines to 3.3V. The Pi 5's internal pull-ups are roughly 50kΩ, which is too weak for reliable communication at 100kHz over wires longer than 10cm. Ensure your BME280 breakout board has 4.7kΩ or 10kΩ pull-ups physically populated on the PCB. If using a raw sensor chip, add external 4.7kΩ resistors.
- Mitigate Clock Stretching Timeouts: As mentioned in the setup phase, the BCM2712 SoC on the Pi 5 has a strict timeout for I2C clock stretching. If the BME280 takes too long to process an internal ADC conversion, it holds the SCL line low. The Pi 5 gives up and throws
[Errno 121]. If you haven't already addeddtparam=i2c_arm_baudrate=10000to/boot/firmware/config.txt, do it now and reboot.
Scaling the Build: Simplify or Extend
Once your headless Raspberry Pi computer setup is stable, you will inevitably need to adapt the hardware to the deployment environment. Here is how to modify the architecture based on your constraints.
Simplifying for Low-Power / Remote Deployments
The Raspberry Pi 5 draws roughly 2.5W at idle, which makes it unsuitable for battery-backed or solar-powered remote weather stations. If you need to deploy this exact codebase on a low-power budget, downgrade to the Raspberry Pi Zero 2 W.
- Code Compatibility: The Python script above requires zero modifications. The I2C Bus 1 pinout is identical on the Zero 2 W.
- Power Draw: The Zero 2 W idles at ~0.7W. Combined with a 12V-to-5V buck converter and a 20W solar panel, it can run indefinitely off a 3Ah 18650 Li-ion pack.
- OS Tweak: Add
dtoverlay=disable-wifitoconfig.txtif you only need to log data to the local microSD card, saving an additional 150mA of current draw.
Extending for Industrial Environments (Modbus RTU)
If you are moving this setup from a hobbyist workbench to an industrial control panel, raw I2C is unacceptable due to its lack of noise immunity and strict distance limits. To extend this build for factory floors, add an isolated RS485 HAT (such as the Waveshare RS485 CAN HAT+).
- Hardware Shift: The HAT connects to the Pi's UART pins (GPIO 14/15) and handles the differential signaling required for RS485. You will replace the BME280 with an industrial Modbus RTU temperature transmitter (e.g., a PT100 probe with a 4-20mA/RS485 module).
- Software Shift: Swap the
smbus2library forpymodbus. You will send a Modbus function code 03 (Read Holding Registers) over/dev/serial0at 9600 baud. - Isolation: The HAT provides 2.5kV galvanic isolation, protecting the Pi's 3.3V logic from ground loops and inductive voltage spikes common near heavy machinery and VFDs (Variable Frequency Drives).
By treating the Raspberry Pi not as a desktop computer, but as a headless embedded Linux controller, you unlock a highly flexible telemetry platform that bridges the gap between hobbyist sensors and industrial data acquisition.






