These days if you wanted to know the current temperature, pressure, or even the humidity levels you would probably just look it up on your phone or better still ask Alexa or Google about it. However if you are a maker, then Google, Alexa or your smart phone isn’t just going to cut it. A true maker would rather build their own weather station. So, here’s a project to challenge the maker in you. Instead of asking Google or Alexa the weather conditions how about building your own simple home weather monitoring project using a Raspberry Pi Pico and a BME 280 programmed with Micropython.
Raspberry Pi Pico – Raspberry Pi Pico is a tiny, fast, and versatile board built using RP2040, the flagship microcontroller chip designed by Raspberry Pi in the UK. From light displays and IoT devices to signage and manufacturing processes, Raspberry Pi Pico gives the maker the power to control countless home, hobby, and industrial projects. Programmable in C and MicroPython, the Pi Pico is adaptable to a vast range of applications and skill levels, and getting started is as easy as dragging and dropping a file. More experienced users can take advantage of Raspberry Pi Pico’s rich peripheral set, including SPI, I2C, and eight Programmable I/O (PIO) state machines for custom peripheral support.

Here’s the specifications (from Raspberry Pi Foundation website) of the Raspberry Pi Pico in case you are interested –
- 21 mm × 51 mm form factor
- RP2040 microcontroller chip designed by Raspberry Pi in the UK
- Dual-core Arm Cortex-M0+ processor, flexible clock running up to 133 MHz
- 264kB on-chip SRAM
- 2MB on-board QSPI flash
- 2.4GHz 802.11n wireless LAN (Raspberry Pi Pico W and WH only)
- 26 multifunction GPIO pins, including 3 analogue inputs
- 2 × UART, 2 × SPI controllers, 2 × I2C controllers, 16 × PWM channels
- 1 × USB 1.1 controller and PHY, with host and device support
- 8 × Programmable I/O (PIO) state machines for custom peripheral support
- Supported input power 1.8–5.5V DC
- Operating temperature -20°C to +85°C (Raspberry Pi Pico and Pico H); -20°C to +70°C (Raspberry Pi Pico W and Pico WH)
- Castellated module allows soldering direct to carrier boards (Raspberry Pi Pico and Pico W only)
- Drag-and-drop programming using mass storage over USB
- Low-power sleep and dormant modes
- Accurate on-chip clock
- Temperature sensor
- Accelerated integer and floating-point libraries on-chip
Pinout diagram for the Raspberry Pi Pico – To help you work out which pins are located where and what they are called you can refer to the pinout diagram for the Raspberry Pi Pico provided below.

PicoDev BME 280: The PiicoDev® Atmospheric Sensor is manufactured in Australia by Core Electronics, see details here – https://core-electronics.com.au/piicodev-atmospheric-sensor-bme280.html. The PiicoDev® Atmospheric Sensor allows you to easily measure barometric pressure, humidity, and temperature. Use the PiicoDev® Atmospheric Sensor to build your own weather station or perform experiments. Better still, you can connect to this sensor using PiicoDev adapters and cables – no soldering is required to connect the PiicoDev Atmospheric Sensor to other devices in the PiicoDev family.
The PiicoDev BME280 is a humidity sensor especially developed for mobile applications and wearables where size and low power consumption are key design parameters. The unit combines high linearity and high accuracy sensors and is perfectly feasible for low current consumption, long-term stability and high EMC robustness. The humidity sensor offers an extremely fast response time and therefore supports performance requirements for emerging applications such as context awareness, and high accuracy over a wide temperature range.
Here’s some of the features of the PiicoDev BME 280.
- Measure temperature, pressure and relative humidity
- PiicoDev connector, compatible with Qwiic and STEMMA QT (3.3V only)
- Address Switch quickly changes the device address without soldering.
- 2.54mm breakout for breadboarding/prototyping
- Manufactured and supported in Australia by Core Electronics

Here are the components you will need to build your home weather station –
Bill Of Material – Here’s the Bill Of Material for this tutorial.
- 1 x Raspberry Pi Pico
- 1 x PicoDev BME 280
- 10 x Male to Male Dupont cables
- 1 x 800 point breadboard
- 1 x Breadboard power module
Circuit Diagram – Here’s the circuit you will need to put together for this tutorial.
- GND on the PiicoDev BME 280 connects to Ground pin on the Raspberry Pi Pico
- 3.3V on the PiicoDev BME 280 connects to 3V3 on the Raspberry Pi Pico
- SDA connects to GPIO26 on the Raspberry Pi Pico
- SCL connects to GPIO27 on the Raspberry Pi Pico

Code for the tutorial – Open up your favorite Micropython editor and type in the following code. There are a number of editors out there for you to choose from. The two most commonly used editors for Micropython are Thonny and Mu. We tend to use Thonny a lot in class with our kids.
- Please save this file as “main.py” on the Raspberry Pi Pico. [Base Code provided by Core Electronics, modified by Emil Jowett]
from PiicoDev_BME280 import PiicoDev_BME280
from PiicoDev_Unified import sleep_ms # cross-platform compatible sleep function
from machine import Pin, RTC
import time
# Setting up the pin which we'll use to blink the onboard LED:
led = Pin(25, Pin.OUT)
# Initialise the BME280 sensor using the bus and pins we are connecting it to.
sensor = PiicoDev_BME280(1, 100000, Pin(26), Pin(27))
# Take an initial altitude reading
zeroAlt = sensor.altitude()
# Create a Real Time Clock object.
# Note, the date and time will be set by Thonny if connected to a computer.
# When the Pi Pico is run disconnected from Thonny, the date at time will start at 01/01/2021 12:00AM.
rtc = RTC()
# Open/create a log.csv file to add data to.
logFile = open("hab_log.csv", "a")
# Write the a CSV header line
logFile.write("Date time, Temperature, Pressure, Humidity, Altitude" + "\n")
#Close the file to avoid corruption.
logFile.close()
while True:
# Get the current Date and Time from the Real Time Clock object.
timestamp=rtc.datetime()
# Format the Date and Time into an easy to read string.
timestring="%04d-%02d-%02d %02d:%02d:%02d"%(timestamp[0:3] + timestamp[4:7])
temperature, pressure, humidity = sensor.values() # read data from the BME280 sensor
altitude = sensor.altitude() - zeroAlt
pressure = pressure / 100 # convert air pressure Pascals -> hPa (or mbar, if you prefer)
# Testing out variables by printing them out on the screen
print("Temperature: ", temperature)
print("Pressure is: ", pressure)
print("Humidity is: ", humidity)
print("Altitude is: ", altitude)
# Create a string that contains our Date and Time, Temperature and Humidity, each separated by a comma.
csvLine = timestring + "," + str(temperature) + "," + str(pressure) + "," + str(humidity) + "," + str(altitude)
# Blink LED so we know its still running when not connected to Thonny.
led.on()
time.sleep(.2)
led.off()
time.sleep(.2)
# Open/create a log.csv file to add data to.
logFile = open("hab_log.csv", "a")
# Write the data to the file including adding "\n" to the end to end the line.
logFile.write(csvLine + "\n")
#Close the file to avoid corruption.
logFile.close()
time.sleep(10) #delay of 10s
2. Please save the following file as “PiicoDev_BME280.py” on the Raspberry Pi Pico. [Base Code provided by Core Electronics, modified by Emil Jowett]
# A MicroPython class for the Core Electronics PiicoDev Atmospheric Sensor BME280
# Ported by Michael Ruppe at Core Electronics
# MAR 2021
# Original repo https://bit.ly/2yJwysL
from PiicoDev_Unified import *
compat_str = '\nUnified PiicoDev library out of date. Get the latest module: https://piico.dev/unified \n'
class PiicoDev_BME280:
def __init__(self, bus=None, freq=None, sda=None, scl=None, t_mode=2, p_mode=5, h_mode=1, iir=1, address=0x77):
try:
if compat_ind >= 1:
pass
else:
print(compat_str)
except:
print(compat_str)
self.i2c = create_unified_i2c(bus=bus, freq=freq, sda=sda, scl=scl)
self.t_mode = t_mode
self.p_mode = p_mode
self.h_mode = h_mode
self.iir = iir
self.addr = address
self._t_fine = 0
try:
self._T1 = self._read16(0x88)
except Exception as e:
print(i2c_err_str.format(self.addr))
raise e
self._T2 = self._short(self._read16(0x8A))
self._T3 = self._short(self._read16(0x8C))
self._P1 = self._read16(0x8E)
self._P2 = self._short(self._read16(0x90))
self._P3 = self._short(self._read16(0x92))
self._P4 = self._short(self._read16(0x94))
self._P5 = self._short(self._read16(0x96))
self._P6 = self._short(self._read16(0x98))
self._P7 = self._short(self._read16(0x9A))
self._P8 = self._short(self._read16(0x9C))
self._P9 = self._short(self._read16(0x9E))
self._H1 = self._read8(0xA1)
self._H2 = self._short(self._read16(0xE1))
self._H3 = self._read8(0xE3)
a = self._read8(0xE5)
self._H4 = (self._read8(0xE4)<<4)+(a%16)
self._H5 = (self._read8(0xE6)<<4)+(a>>4)
self._H6 = self._read8(0xE7)
if self._H6 > 127:
self._H6 -= 256
self._write8(0xF2, self.h_mode)
sleep_ms(2)
self._write8(0xF4, 0x24)
sleep_ms(2)
self._write8(0xF5, self.iir<<2)
def _read8(self, reg):
t = self.i2c.readfrom_mem(self.addr, reg, 1)
return t[0]
def _read16(self, reg):
t = self.i2c.readfrom_mem(self.addr, reg, 2)
return t[0]+t[1]*256
def _write8(self, reg, dat):
self.i2c.write8(self.addr, bytes([reg]), bytes([dat]))
def _short(self, dat):
if dat > 32767:
return dat - 65536
else:
return dat
def read_raw_data(self):
self._write8(0xF4, (self.p_mode << 5 | self.t_mode << 2 | 1))
sleep_time = 1250
if self.t_mode in [1, 2, 3, 4, 5]:
sleep_time += 2300*(1<< self.t_mode)
if self.p_mode in [1, 2, 3, 4, 5]:
sleep_time += 575+(2300*(1<<self.p_mode))
if self.h_mode in [1, 2, 3, 4, 5]:
sleep_time += 575+(2300*(1<<self.h_mode))
sleep_ms(1+sleep_time//1000)
while(self._read16(0xF3) & 0x08):
sleep_ms(1)
raw_p = ((self._read8(0xF7)<<16)|(self._read8(0xF8)<<8)|self._read8(0xF9))>>4
raw_t = ((self._read8(0xFA)<<16)|(self._read8(0xFB)<<8)|self._read8(0xFC))>>4
raw_h = (self._read8(0xFD) << 8)| self._read8(0xFE)
return (raw_t, raw_p, raw_h)
def read_compensated_data(self):
try:
raw_t, raw_p, raw_h = self.read_raw_data()
except:
print(i2c_err_str.format(self.addr))
return (float('NaN'), float('NaN'), float('NaN'))
var1 = ((raw_t>>3)-(self._T1<<1))*(self._T2>>11)
var2 = (raw_t >> 4)-self._T1
var2 = var2*((raw_t>>4)-self._T1)
var2 = ((var2>>12)*self._T3)>>14
self._t_fine = var1+var2
temp = (self._t_fine*5+128)>>8
var1 = self._t_fine-128000
var2 = var1*var1*self._P6
var2 = var2+((var1*self._P5)<<17)
var2 = var2+(self._P4<<35)
var1 = (((var1*var1*self._P3)>>8)+
((var1*self._P2)<<12))
var1 = (((1<<47)+var1)*self._P1)>>33
if var1 == 0:
pres = 0
else:
p = ((((1048576-raw_p)<<31)-var2)*3125)//var1
var1 = (self._P9*(p>>13)*(p >> 13))>>25
var2 = (self._P8*p)>>19
pres = ((p+var1+var2)>>8)+(self._P7<<4)
h = self._t_fine-76800
h = (((((raw_h<<14)-(self._H4<<20)-
(self._H5*h))+16384)
>>15)*(((((((h*self._H6)>>10)*
(((h*self._H3)>>11)+32768))>>10)+
2097152)*self._H2+8192)>>14))
h = h-(((((h>>15)*(h>>15))>>7)*self._H1)>>4)
h = 0 if h < 0 else h
h = 419430400 if h>419430400 else h
humi = h>>12
return (temp, pres, humi)
def values(self):
temp, pres, humi = self.read_compensated_data()
return (temp/100, pres/256, humi/1024)
def pressure_precision(self):
p = self.read_compensated_data()[1]
pi = float(p // 256)
pd = (p % 256)/256
return (pi, pd)
def altitude(self, pressure_sea_level=1013.25):
pi, pd = self.pressure_precision()
return 44330*(1-((float(pi+pd)/100)/pressure_sea_level)**(1/5.255))
3. Please save the following code as “PiicoDev_Unified.py” on the Raspberry Pi. [Base Code provided by Core Electronics, modified by Emil Jowett]
'''
PiicoDev.py: Unifies I2C drivers for different builds of MicroPython
Changelog:
- 2022-10-13 P.Johnston Add helptext to run i2csetup script on Raspberry Pi
- 2022-10-14 M.Ruppe Explicitly set default I2C initialisation parameters for machine-class (Raspberry Pi Pico + W)
'''
import os
_SYSNAME = os.uname().sysname
compat_ind = 1
i2c_err_str = 'PiicoDev could not communicate with module at address 0x{:02X}, check wiring'
setupi2c_str = ', run "sudo curl -L https://piico.dev/i2csetup | bash". Suppress this warning by setting suppress_warnings=True'
if _SYSNAME == 'microbit':
from microbit import i2c
from utime import sleep_ms
elif _SYSNAME == 'Linux':
from smbus2 import SMBus, i2c_msg
from time import sleep
from math import ceil
def sleep_ms(t):
sleep(t/1000)
else:
from machine import I2C, Pin
from utime import sleep_ms
class I2CBase:
def writeto_mem(self, addr, memaddr, buf, *, addrsize=8):
raise NotImplementedError('writeto_mem')
def readfrom_mem(self, addr, memaddr, nbytes, *, addrsize=8):
raise NotImplementedError('readfrom_mem')
def write8(self, addr, buf, stop=True):
raise NotImplementedError('write')
def read16(self, addr, nbytes, stop=True):
raise NotImplementedError('read')
def __init__(self, bus=None, freq=None, sda=None, scl=None):
raise NotImplementedError('__init__')
class I2CUnifiedMachine(I2CBase):
def __init__(self, bus=None, freq=None, sda=None, scl=None):
if bus is not None and freq is not None and sda is not None and scl is not None:
print('Using supplied freq, sda and scl to create machine I2C')
self.i2c = I2C(bus, freq=freq, sda=sda, scl=scl)
else:
self.i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=100000)
self.writeto_mem = self.i2c.writeto_mem
self.readfrom_mem = self.i2c.readfrom_mem
def write8(self, addr, reg, data):
if reg is None:
self.i2c.writeto(addr, data)
else:
self.i2c.writeto(addr, reg + data)
def read16(self, addr, reg):
self.i2c.writeto(addr, reg, False)
return self.i2c.readfrom(addr, 2)
class I2CUnifiedMicroBit(I2CBase):
def __init__(self, freq=None):
if freq is not None:
print('Initialising I2C freq to {}'.format(freq))
microbit.i2c.init(freq=freq)
def writeto_mem(self, addr, memaddr, buf, *, addrsize=8):
ad = memaddr.to_bytes(addrsize // 8, 'big') # pad address for eg. 16 bit
i2c.write(addr, ad + buf)
def readfrom_mem(self, addr, memaddr, nbytes, *, addrsize=8):
ad = memaddr.to_bytes(addrsize // 8, 'big') # pad address for eg. 16 bit
i2c.write(addr, ad, repeat=True)
return i2c.read(addr, nbytes)
def write8(self, addr, reg, data):
if reg is None:
i2c.write(addr, data)
else:
i2c.write(addr, reg + data)
def read16(self, addr, reg):
i2c.write(addr, reg, repeat=True)
return i2c.read(addr, 2)
class I2CUnifiedLinux(I2CBase):
def __init__(self, bus=None, suppress_warnings=True):
if suppress_warnings == False:
with open('/boot/config.txt') as config_file:
if 'dtparam=i2c_arm=on' in config_file.read():
pass
else:
print('I2C is not enabled. To enable' + setupi2c_str)
config_file.close()
with open('/boot/config.txt') as config_file:
if 'dtparam=i2c_arm_baudrate=400000' in config_file.read():
pass
else:
print('Slow baudrate detected. If glitching occurs' + setupi2c_str)
config_file.close()
if bus is None:
bus = 1
self.i2c = SMBus(bus)
def readfrom_mem(self, addr, memaddr, nbytes, *, addrsize=8):
data = [None] * nbytes # initialise empty list
self.smbus_i2c_read(addr, memaddr, data, nbytes, addrsize=addrsize)
return data
def writeto_mem(self, addr, memaddr, buf, *, addrsize=8):
self.smbus_i2c_write(addr, memaddr, buf, len(buf), addrsize=addrsize)
def smbus_i2c_write(self, address, reg, data_p, length, addrsize=8):
ret_val = 0
data = []
for index in range(length):
data.append(data_p[index])
if addrsize == 8:
msg_w = i2c_msg.write(address, [reg] + data)
elif addrsize == 16:
msg_w = i2c_msg.write(address, [reg >> 8, reg & 0xff] + data)
else:
raise Exception('address must be 8 or 16 bits long only')
self.i2c.i2c_rdwr(msg_w)
return ret_val
def smbus_i2c_read(self, address, reg, data_p, length, addrsize=8):
ret_val = 0
if addrsize == 8:
msg_w = i2c_msg.write(address, [reg]) # warning this is set up for 16-bit addresses
elif addrsize == 16:
msg_w = i2c_msg.write(address, [reg >> 8, reg & 0xff]) # warning this is set up for 16-bit addresses
else:
raise Exception('address must be 8 or 16 bits long only')
msg_r = i2c_msg.read(address, length)
self.i2c.i2c_rdwr(msg_w, msg_r)
if ret_val == 0:
for index in range(length):
data_p[index] = ord(msg_r.buf[index])
return ret_val
def write8(self, addr, reg, data):
if reg is None:
d = int.from_bytes(data, 'big')
self.i2c.write_byte(addr, d)
else:
r = int.from_bytes(reg, 'big')
d = int.from_bytes(data, 'big')
self.i2c.write_byte_data(addr, r, d)
def read16(self, addr, reg):
regInt = int.from_bytes(reg, 'big')
return self.i2c.read_word_data(addr, regInt).to_bytes(2, byteorder='little', signed=False)
def create_unified_i2c(bus=None, freq=None, sda=None, scl=None, suppress_warnings=True):
if _SYSNAME == 'microbit':
i2c = I2CUnifiedMicroBit(freq=freq)
elif _SYSNAME == 'Linux':
i2c = I2CUnifiedLinux(bus=bus, suppress_warnings=suppress_warnings)
else:
i2c = I2CUnifiedMachine(bus=bus, freq=freq, sda=sda, scl=scl)
return i2c
You can also download all of the above code within one zip file from – Posted in MicroPython, Pi Pico, STEM Education