This raspberry pi pico project guides you through building a WiFi-connected environmental data logger. Using the Raspberry Pi Pico W, a BME280 sensor, and an SSD1306 OLED display, the system reads temperature, humidity, and barometric pressure, renders the data locally on the screen, and publishes it to an MQTT broker for remote dashboarding (like Home Assistant or Node-RED). We will cover the exact hardware variants, provide a fault-tolerant MicroPython script, and break down the specific I2C and network errors that commonly stall Pico W builds.
Project Specs & Target Hardware
The RP2040 dual-core Arm Cortex-M0+ running at 133MHz is more than capable of handling simultaneous I2C polling and WiFi TCP keep-alives. However, the Pico W's WiFi chip (CYW43439) is connected via SPI to the RP2040, not directly to the USB bus. This architectural quirk means WiFi operations consume specific GPIO pins internally (GPIO 23, 24, 25, and 29), which you must avoid using for your external peripherals.
Hardware Spec Sheet & Parts List
Procure these exact components to ensure pin compatibility and I2C voltage matching. The Pico W operates at 3.3V logic; using 5V I2C sensors without a level shifter will degrade the RP2040's GPIO pads over time.
| Component | Exact Model / Part Number | Qty | Est. Price (2026) |
|---|---|---|---|
| Microcontroller | Raspberry Pi Pico W (with pre-soldered headers) | 1 | $6.00 |
| Environment Sensor | Adafruit BME280 I2C Breakout (Product ID: 2652) | 1 | $9.95 |
| Display | SSD1306 128x64 I2C OLED (Monochrome, 3.3V tolerant) | 1 | $7.50 |
| Prototyping | Half-size solderless breadboard (400 tie-points) | 1 | $4.00 |
| Wiring | 24 AWG solid core jumper wires (Male-to-Male) | 1 pack | $5.00 |
Pin Mapping Table
We are using I2C0 for the sensor and I2C1 for the display to prevent address conflicts and bus capacitance issues. Do not share the same I2C bus for both if you are running high polling rates.
| Pico W Pin | Function | Connects To |
|---|---|---|
| Pin 1 (GP0) | I2C0 SDA | BME280 SDI |
| Pin 2 (GP1) | I2C0 SCL | BME280 SCK |
| Pin 4 (GP2) | I2C1 SDA | OLED SDA |
| Pin 5 (GP3) | I2C1 SCL | OLED SCL |
| Pin 36 (3V3 OUT) | Power (3.3V) | BME280 VIN & OLED VCC |
| Pin 38 (GND) | Ground | BME280 GND & OLED GND |
Step-by-Step Wiring & Assembly
- Place the Pico W: Insert the Pico W into the center trench of the breadboard. Ensure the USB port faces the edge for cable clearance.
- Wire the Power Rails: Connect Pin 36 (3V3) to the red power rail and Pin 38 (GND) to the blue ground rail. Safety check: Do not connect 5V (Pin 40) to the 3.3V rail, or you will instantly destroy the BME280 and OLED.
- Connect the BME280 (I2C0): Route GP0 to the sensor's SDA and GP1 to SCL. Connect power and ground. The Adafruit breakout includes onboard 10kΩ pull-up resistors, so no external resistors are needed on this bus.
- Connect the OLED (I2C1): Route GP2 to the OLED SDA and GP3 to SCL. Connect power and ground. If your generic SSD1306 module lacks pull-up resistors (common on cheap 4-pin variants), add 4.7kΩ resistors between SDA/SCL and 3.3V.
- Verify with Multimeter: Before plugging in USB, use a multimeter in continuity mode to verify there is no short between the 3.3V rail and GND.
Complete MicroPython Firmware & Code
This script targets the Raspberry Pi Pico W. It includes robust error handling for I2C initialization and network drops. You must upload the ssd1306.py and bme280.py driver libraries to your Pico's root directory via Thonny IDE before running this main script.
import machine
import network
import time
import ubinascii
from umqtt.simple import MQTTClient
import ssd1306
# --- PIN DEFINITIONS ---
I2C0_SDA = machine.Pin(0)
I2C0_SCL = machine.Pin(1)
I2C1_SDA = machine.Pin(2)
I2C1_SCL = machine.Pin(3)
# --- CONFIGURATION ---
WIFI_SSID = 'YourNetworkSSID'
WIFI_PASS = 'YourNetworkPassword'
MQTT_BROKER = '192.168.1.100'
MQTT_PORT = 1883
CLIENT_ID = ubinascii.hexlify(machine.unique_id())
MQTT_TOPIC = b'pico/environment/data'
# --- HARDWARE INITIALIZATION ---
try:
i2c_sensor = machine.I2C(0, sda=I2C0_SDA, scl=I2C0_SCL, freq=400000)
i2c_display = machine.I2C(1, sda=I2C1_SDA, scl=I2C1_SCL, freq=400000)
oled = ssd1306.SSD1306_I2C(128, 64, i2c_display)
print('Displays and I2C buses initialized.')
except Exception as e:
print(f'Hardware Init Failed: {e}')
machine.reset()
# --- SENSOR FALLBACK ---
try:
import bme280
sensor = bme280.BME280(i2c=i2c_sensor)
USE_BME = True
except ImportError:
print('bme280.py not found. Falling back to internal RP2040 temp sensor.')
USE_BME = False
adc_temp = machine.ADC(4) # Internal temp sensor on ADC4
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASS)
oled.fill(0)
oled.text('Connecting WiFi', 0, 0)
oled.show()
max_wait = 15
while max_wait > 0:
if wlan.status() < 0 or wlan.status() >= 3:
break
max_wait -= 1
time.sleep(1)
if wlan.status() != 3:
raise RuntimeError('WiFi connection failed')
ip = wlan.ifconfig()[0]
oled.fill(0)
oled.text(f'IP: {ip}', 0, 0)
oled.show()
return ip
def read_sensors():
if USE_BME:
temp = float(sensor.temperature[:-1])
hum = float(sensor.humidity[:-1])
pres = float(sensor.pressure[:-3])
else:
reading = adc_temp.read_u16() * 3.3 / (65535)
temp = 27 - (reading - 0.706)/0.001721
hum = 0.0
pres = 0.0
return temp, hum, pres
# --- MAIN LOOP ---
try:
ip_addr = connect_wifi()
client = MQTTClient(CLIENT_ID, MQTT_BROKER, MQTT_PORT)
client.connect()
while True:
temp, hum, pres = read_sensors()
# Update OLED
oled.fill(0)
oled.text(f'Temp: {temp:.1f} C', 0, 10)
oled.text(f'Hum: {hum:.1f} %', 0, 25)
oled.text(f'Pres: {pres:.0f} hPa', 0, 40)
oled.show()
# Publish MQTT
payload = f'{{"temp":{temp:.1f},"hum":{hum:.1f},"pres":{pres:.0f}}}'
client.publish(MQTT_TOPIC, payload)
time.sleep(10)
except OSError as e:
print(f'Network/I2C OS Error: {e}')
machine.reset()
except Exception as e:
print(f'Unhandled Exception: {e}')
machine.reset()
Debugging: Exact Errors & The First 3 Things to Check
When a raspberry pi pico project fails, it rarely fails silently. The RP2040 throws specific OS errors. Before rewriting code, check these three physical and configuration layers:
- I2C Pull-ups and Swapped Pins: SDA and SCL are frequently swapped on generic OLEDs. Verify continuity from the Pico GP pins to the exact sensor pads.
- Thonny Interpreter Lock: Ensure Thonny is set to MicroPython (Raspberry Pi Pico) at the bottom right, not standard Python. If the port is greyed out, another process (like a serial monitor) holds the COM port.
- WiFi Band Compatibility: The Pico W's CYW43439 chip is strictly 2.4GHz. If your router uses a unified SSID for 2.4GHz and 5GHz with aggressive band steering, the Pico will fail to associate. Create a dedicated 2.4GHz IoT SSID.
Error: OSError: [Errno 121] EIO
Meaning: I/O Error. The RP2040 sent an I2C address but received no ACKnowledge (NACK) from the slave device.
Ranked Causes:
- 1. Wrong I2C Address: The BME280 defaults to
0x77, but some clones use0x76. Run an I2C scanner script to verify the hex address. - 2. Missing Pull-up Resistors: The I2C bus is open-drain. Without 4.7kΩ pull-ups to 3.3V, the signal lines float, causing the Pico to read garbage or time out.
- 3. 5V Logic Damage: If you previously wired the sensor to 5V, the BME280's internal logic level translator may be fried.
Error: OSError: [Errno 116] ETIMEDOUT
Meaning: The WiFi associated, but the TCP handshake to the MQTT broker timed out.
Ranked Causes:
- 1. Broker Unreachable: The MQTT broker IP is wrong, or the broker service (e.g., Mosquitto) is stopped.
- 2. Firewall Blocking Port 1883: Your local network firewall is dropping unencrypted MQTT traffic.
- 3. DNS Resolution Failure: If using a hostname instead of an IP for the broker, MicroPython's DNS resolver can occasionally fail on local LAN names. Use static IPs for local brokers.
Extending and Simplifying the Build
umqtt import and the client.publish() lines. The Pico W will still connect to WiFi (useful for NTP time syncing later) and function as a standalone local display. You can also drop the WiFi entirely and use the standard Pico (non-W) by removing the network module calls.
How to Extend: To make this a battery-powered remote node, you need to implement deep sleep. However, the Pico W has a known hardware quirk: the CYW43 WiFi chip does not automatically shut down during the RP2040's native machine.deepsleep(), drawing roughly 20mA in the background. To properly extend this for battery use, you must initialize the WiFi chip, perform the transmission, and then explicitly command the CYW43 to sleep using the undocumented network.WLAN(network.STA_IF).active(False) followed by a custom register write to the PMU (Power Management Unit) before triggering the RP2040 dormant state. For most hobbyists, adding a hardware timer (like a TPL5110) to physically cut power to the Pico W between reads is a much more reliable low-power extension.
Frequently Asked Questions
Can I use the standard Raspberry Pi Pico (non-W) for this project?
Yes, but you must remove the WiFi and MQTT code blocks. The standard Pico lacks the CYW43439 chip, so the network.WLAN calls will throw an AttributeError. The I2C sensor and OLED code will work perfectly on the standard Pico, making it an excellent, lower-cost ($4) offline data logger.
Why does my BME280 read 0x76 instead of 0x77 on the I2C bus?
The Bosch BME280 datasheet specifies that the I2C address is determined by the SDO (Serial Data Out) pin. If SDO is tied to GND, the address is 0x76. If tied to VCC, it is 0x77. Adafruit's official breakout pulls it high by default (0x77), but many generic eBay/AliExpress clones pull it low (0x76). Update the address parameter in your bme280.py initialization if you are using a clone.
How do I fix the 'Backend not connected' error in Thonny IDE?
This error occurs when Thonny's serial interface cannot establish a REPL session with the Pico. First, unplug the Pico and plug it back in while holding the BOOTSEL button to force it into USB mass storage mode, then release and click 'Stop' in Thonny. If it persists, check your OS device manager to ensure the Pico is enumerated as a COM port and not stuck in a generic USB hub state. Finally, ensure no other software (like PuTTY, Arduino IDE serial monitor, or a background MQTT script) is holding the serial port open.
What is the actual current draw of the Pico W when transmitting over WiFi?
During active WiFi transmission (TCP/MQTT publish), the Pico W spikes to approximately 110mA - 130mA for a few milliseconds. At idle with WiFi associated but not transmitting, it draws about 40mA - 50mA. This is significantly higher than the ESP32-C3 or ESP8266 in deep sleep, which is why the Pico W is generally avoided for ultra-low-power coin-cell applications unless paired with an external power-gating MOSFET.






