If you are building a weather station, the direct answer for 95% of makers is the Bosch BME280 (specifically the Adafruit I2C/SPI Breakout, Product ID 2652). It costs around $11, communicates over a hardware I2C bus, and measures temperature, humidity, and barometric pressure. While the DHT22 is cheaper, its 1-Wire-style timing protocol frequently fails on the Raspberry Pi because Linux is not a real-time operating system; kernel interrupts routinely cause missed microsecond pulses, resulting in dropped readings.

This guide cuts through the guesswork. We will run a decision matrix to prove why the BME280 wins, wire it to a Raspberry Pi 4 Model B, write robust Python code with exact error handling, and cover the specific I2C debugging steps you need when the bus locks up.

The Verdict: Which Weather Sensor Should You Buy?

Use this decision tree to select the right sensor for your specific environment and data requirements.

If your project needs... Then choose this sensor Why it wins (or fails)
Basic indoor temp/humidity on a strict <$5 budget DHT22 (AM2302) Cheap, but relies on bit-banged timing. High failure rate on Pi due to OS thread jitter.
Reliable temp, humidity, AND barometric pressure BME280 (Concrete Pick) Hardware I2C handles timing in the background. Includes pressure for altitude/weather forecasting.
Indoor air quality (VOCs) alongside weather BME680 Adds a gas sensor, but requires complex burn-in calibration and costs ~$16.
High-precision outdoor temp/humidity only AHT20 Excellent I2C stability and accuracy, but lacks the pressure sensor of the BME280.
Default Recommendation: Buy the BME280. The barometric pressure data allows you to calculate approximate altitude and predict short-term weather changes (falling pressure indicates incoming storms), making it vastly more useful than a simple temp/humidity module.

Hardware Spec Sheet & Parts List

This build targets the Raspberry Pi 4 Model B (4GB RAM). The code and wiring are 100% compatible with the Pi 3B+ and Pi 5, as the 40-pin GPIO I2C layout remains unchanged across these generations.

  • Microcontroller: Raspberry Pi 4 Model B (4GB) — ~$55
  • Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID 2652) — ~$11. (Includes onboard 10kΩ pull-up resistors and a 3.3V voltage regulator).
  • Wiring: 4x Female-to-Female jumper wires (or a STEMMA QT / Qwiic cable if using a compatible Pi shim).
  • Software Stack: Raspberry Pi OS (Bookworm or newer), Python 3.9+, adafruit-circuitpython-bme280 library.
Warning on I2C Capacitance: The Raspberry Pi's I2C bus has 1.8kΩ internal pull-up resistors. If you use jumper wires longer than 30cm (12 inches), the parasitic capacitance of the wire will degrade the signal edges, causing I2C bus lockups. For outdoor runs, use a shielded CAT5e cable and keep the I2C run under 1 meter, or switch to an RS485 transceiver for long-distance runs.

Wiring the BME280 to Raspberry Pi 4B

The BME280 supports both SPI and I2C. We are using I2C because it only requires two data pins and allows you to daisy-chain other sensors later. The Adafruit breakout defaults to I2C address 0x77.

Raspberry Pi 4B Pin (Physical) GPIO / Function BME280 Breakout Pin Wire Color (Standard)
Pin 1 3.3V Power VIN (or 3Vo) Red
Pin 6 Ground GND Black
Pin 3 GPIO 2 (SDA.1) SDI (SDA) Blue
Pin 5 GPIO 3 (SCL.1) SCK (SCL) Yellow

Crucial Safety Note: Never connect the BME280 VIN pin to the Pi's 5V pins (Pin 2 or 4) unless your specific breakout board explicitly states it has a 5V-tolerant voltage regulator. The Pi's I2C pins (SDA/SCL) are strictly 3.3V tolerant. Feeding 5V back into the SDA line will permanently destroy the BCM2711 SoC.

Python Build: Polling I2C Weather Data

Before running the code, enable the I2C interface on your Pi by running sudo raspi-config, navigating to Interface Options -> I2C -> Enable, and rebooting. Next, install the required CircuitPython library:

pip3 install adafruit-circuitpython-bme280

The following script initializes the I2C bus, handles the specific hardware exceptions that occur when a sensor is missing, and polls the data every 5 seconds.

import time
import board
import busio
import adafruit_bme280

# Define the I2C bus using the Pi's hardware SCL and SDA pins
i2c = busio.I2C(board.SCL, board.SDA)

# Attempt to initialize the sensor with error handling
def init_sensor():
    try:
        # Adafruit breakouts default to 0x77. Generic clones often use 0x76.
        sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x77)
        print("BME280 initialized successfully at 0x77.")
        return sensor
    except ValueError as e:
        # Catches chip ID mismatch or incorrect address
        print(f"[FATAL] Sensor not found or wrong address. Error: {e}")
        print("Action: Run 'i2cdetect -y 1' to find the correct hex address.")
        exit(1)
    except RuntimeError as e:
        # Catches I2C bus lockups or OS-level permission denied errors
        print(f"[FATAL] I2C Bus Error. Error: {e}")
        print("Action: Ensure I2C is enabled in raspi-config and you have i2c permissions.")
        exit(1)

sensor = init_sensor()

# Configure the sensor for indoor weather station use
# Higher oversampling reduces noise but increases read time
sensor.oversampling_humidity = adafruit_bme280.OVERSAMPLING_X2
sensor.oversampling_temperature = adafruit_bme280.OVERSAMPLING_X2
sensor.oversampling_pressure = adafruit_bme280.OVERSAMPLING_X2
sensor.mode = adafruit_bme280.MODE_NORMAL
sensor.standby_period = adafruit_bme280.STANDBY_TC_500

print("Polling weather data... (Press Ctrl+C to stop)")

try:
    while True:
        temp_c = sensor.temperature
        humidity = sensor.relative_humidity
        pressure_hpa = sensor.pressure
        
        # Convert to Fahrenheit and inHg for US users
        temp_f = (temp_c * 9/5) + 32
        pressure_inhg = pressure_hpa * 0.02953
        
        print(f"Temp: {temp_c:.1f}°C ({temp_f:.1f}°F) | "
              f"Humidity: {humidity:.1f}% | "
              f"Pressure: {pressure_hpa:.1f} hPa ({pressure_inhg:.2f} inHg)")
        
        time.sleep(5)

except KeyboardInterrupt:
    print("\nPolling stopped by user.")
except OSError as e:
    print(f"\n[ERROR] I2C Bus dropped during read: {e}")
    print("Action: Check for loose jumper wires or excessive cable capacitance.")

Debugging: Exact Errors & The First 3 Checks

When working with I2C on the Raspberry Pi, things will eventually go wrong. If your script crashes, do not guess. Follow this exact diagnostic path.

The First 3 Things to Check When It Fails

  1. Verify I2C is enabled and detected: Run sudo i2cdetect -y 1 in the terminal. You should see a grid with 77 (or 76) highlighted. If the grid is entirely empty, your wiring is wrong or I2C is disabled in the OS.
  2. Check for 5V on the I2C line: Use a multimeter to measure the voltage between the Pi's GND (Pin 6) and the SDA wire. It must read ~3.3V. If it reads 5V, you have a short or a faulty breakout board that is back-feeding voltage.
  3. Verify User Permissions: If running without sudo, ensure your user is in the i2c group by running sudo usermod -aG i2c $USER and rebooting.

Ranked Causes for Exact Error Strings

Exact Error String Rank Root Cause & Fix
ValueError: No I2C device at address: 0x77 1st Cause: You are using a generic clone BME280 that defaults to 0x76.
Fix: Change address=0x77 to address=0x76 in the Python script.
OSError: [Errno 121] Remote I/O error 2nd Cause: I2C bus lockup due to parasitic capacitance on long wires, or a loose SDA connection.
Fix: Shorten wires to <30cm, or add external 4.7kΩ pull-up resistors to 3.3V.
ValueError: Bad chip ID... expected 0x60 3rd Cause: You bought a BMP280 (temp/pressure only) mislabeled as a BME280 on Amazon/AliExpress. The BMP280 chip ID is 0x58.
Fix: Use the adafruit-circuitpython-bmp280 library instead.

Extending the Build: Wind, Rain, and Lightning

The BME280 handles the ambient air perfectly, but a complete outdoor weather station needs mechanical sensors. Here is how you extend the system without overloading the Pi's GPIO.

  • To Simplify (Indoor Only): If you only care about indoor climate control, stop here. Mount the BME280 inside a slotted 3D-printed Stevenson screen to prevent direct sunlight from skewing the temperature readings, and pipe the data to Home Assistant via MQTT.
  • To Extend (Wind & Rain): Do not connect mechanical anemometers (wind speed) and rain gauges (tipping buckets) directly to the Pi's GPIO. They use reed switches that cause contact bounce, and running long wires to mechanical switches acts as an antenna for ESD (Electrostatic Discharge), which will fry the Pi. Instead, use an Arduino Nano or ESP32 as a dedicated node to debounce the reed switches and count interrupts, then send the aggregated data to the Pi via UART or ESP-NOW.
  • To Extend (Lightning): Add the SparkFun AS3935 Lightning Detector (Product ID: SEN-15441). It uses SPI or I2C. Note that if you share the I2C bus with the BME280, the AS3935's internal oscillator can introduce noise. Keep the lightning detector at least 5cm away from the BME280 on your breadboard to prevent barometric pressure jitter.

By anchoring your build on the BME280's hardware I2C protocol, you eliminate the timing nightmares of cheaper sensors and build a foundation stable enough to support a full-scale meteorological array.