Raspberry Pi Pico
Digital Weather Station
Monitor real-time temperature and humidity with a DHT22 sensor and 16×2 LCD display — all in MicroPython, fully testable in the free Wokwi browser simulator!
Overview
What You'll Build
A fully functional environmental monitor that reads real sensor data and displays it live on an LCD — in MicroPython!
Components
What You Need
All components are available virtually in Wokwi — nothing to purchase to get started!
main.py (your logic), lcd_api.py (HD44780 base class), and pico_i2c_lcd.py (I2C driver). All three are provided below — just upload them together.Wiring
Circuit Connections
The DHT22 uses a single data wire, while the LCD connects via I2C with just 2 wires.
Full Connection Table
| Component | Pin | Pico GPIO | Wire |
|---|---|---|---|
| DHT22 VCC | VCC | 3.3V (Pin 36) | 🔴 Red |
| DHT22 GND | GND | GND (Pin 3) | ⬛ Black |
| DHT22 DATA | SDA/Data | GP15 (Pin 20) | 🟡 Yellow |
| 10kΩ Resistor | Between DATA & VCC | Pull-up to 3.3V | — |
| LCD SDA | SDA | GP0 (Pin 1) | 🟢 Green |
| LCD SCL | SCL | GP1 (Pin 2) | 🔵 Blue |
| LCD VCC | VCC | 3.3V | 🔴 Red |
| LCD GND | GND | GND | ⬛ Black |
Instructions
Step-by-Step Guide
8 steps to get your weather station reading live data in Wokwi.
-
Open the Wokwi Simulation
Go to wokwi.com/projects/459537616532034561 or click ▶ Run Free Simulation above. Select Raspberry Pi Pico as the board and set language to MicroPython.
-
Paste the diagram.json
Click the diagram.json tab and replace all content with the JSON from the Diagram JSON section below. This auto-places and wires the Pico, DHT22, LCD, and pull-up resistor.
-
Create the lcd_api.py file
In Wokwi click + New File → name it
lcd_api.py→ paste the lcd_api.py code from the Code section below. This is the HD44780 base library. -
Create the pico_i2c_lcd.py file
Click + New File again → name it
pico_i2c_lcd.py→ paste the I2C driver code. This handles the PCF8574 I2C-to-parallel translation chip used on most I2C LCD backpacks. -
Paste main.py code
Open the main.py tab and paste the main application code. This reads the DHT22 sensor every 2 seconds and updates both lines of the LCD.
-
Click ▶ Run Simulation
Press the green play button. The LCD will show the startup message "MakeMindz / Temp & Humidity" for 2 seconds, then switch to live sensor readings.
Expected LCD output: Line 1 showsTemp:24.5°C, Line 2 showsHumid:65.2%— updating every 2 seconds. -
Check the Serial Console
Open Wokwi's Serial Monitor tab to see console output:
Temperature: 24.5°C (76.1°F)andHumidity: 65.2%printed every 2 seconds. -
Adjust virtual sensor values
Click the DHT22 component in Wokwi and drag its temperature and humidity sliders to test different readings. Watch the LCD update in real time — perfect for testing error handling too!
Diagram JSON
Wokwi diagram.json
Paste this into the diagram.json tab in Wokwi. It auto-places and wires all 4 components — Pico, DHT22, LCD (I2C mode), and the 10kΩ pull-up resistor.
diagram.json tab in Wokwi2. Select all text (Ctrl+A) and delete it
3. Paste this JSON — all wiring appears automatically
4. Note: DHT22 is pre-set to
24°C / 65% humidity — drag its sliders to change!
{
"version": 1,
"author": "MakeMindz",
"editor": "wokwi",
"parts": [
{
"type": "wokwi-pi-pico",
"id": "pico",
"top": 0, "left": 0,
"attrs": {}
},
{
"type": "wokwi-dht22",
"id": "dht22",
"top": -38.4, "left": 220.8,
"attrs": {
"temperature": "24", // editable in simulator
"humidity": "65"
}
},
{
"type": "wokwi-lcd1602",
"id": "lcd",
"top": -144, "left": -192,
"attrs": { "pins": "i2c" } // I2C mode — only 2 data wires needed
},
{
"type": "wokwi-resistor",
"id": "r1",
"top": -19.2, "left": 268.8,
"attrs": { "value": "10000" } // 10kΩ pull-up for DHT22 data line
}
],
"connections": [
// LCD I2C connections
[ "pico:GP0", "lcd:SDA", "green", ["h0"] ],
[ "pico:GP1", "lcd:SCL", "blue", ["h0"] ],
[ "pico:3V3", "lcd:VCC", "red", ["h0"] ],
[ "pico:GND.8", "lcd:GND", "black", ["h0"] ],
// DHT22 sensor connections
[ "dht22:VCC", "pico:3V3", "red", ["v0"] ],
[ "dht22:GND", "pico:GND.3", "black", ["v0"] ],
[ "dht22:SDA", "pico:GP15", "yellow", ["v0"] ],
// 10kΩ pull-up resistor between DHT22 data and VCC
[ "r1:1", "dht22:SDA", "", ["v0"] ],
[ "r1:2", "dht22:VCC", "", ["v0"] ]
],
"dependencies": {}
}
📋 Connection Reference
| Connection | Purpose |
|---|---|
| pico:GP0 → lcd:SDA | I2C data line to LCD — carries all display commands and characters |
| pico:GP1 → lcd:SCL | I2C clock line — synchronises data transfer at 400kHz |
| pico:3V3 → lcd:VCC | 3.3V power for the LCD and its PCF8574 backpack |
| pico:GND.8 → lcd:GND | Ground for LCD circuit |
| dht22:VCC → pico:3V3 | 3.3V power for DHT22 sensor |
| dht22:GND → pico:GND.3 | Ground for DHT22 |
| dht22:SDA → pico:GP15 | Single-wire data signal — sends temperature & humidity readings |
| r1: between DATA & VCC | 10kΩ pull-up keeps data line HIGH when idle — required for DHT22 |
Source Code
MicroPython Files
Three files are needed. Create each one in Wokwi using + New File and paste the code.
""" Temperature and Humidity Monitor — Raspberry Pi Pico Hardware: DHT22 Sensor + 16x2 I2C LCD Display Platform: Wokwi Simulator | MakeMindz Summer Class """ from machine import Pin, I2C from time import sleep import dht from lcd_api import LcdApi from pico_i2c_lcd import I2cLcd # ── LCD Configuration ── I2C_ADDR = 0x27 # Default I2C address for most LCD backpacks I2C_NUM_ROWS = 2 I2C_NUM_COLS = 16 # ── Pin Configuration ── DHT_PIN = 15 # DHT22 data pin → GPIO15 I2C_SDA_PIN = 0 # I2C SDA → GPIO0 I2C_SCL_PIN = 1 # I2C SCL → GPIO1 # ── Initialize I2C and LCD ── i2c = I2C(0, sda=Pin(I2C_SDA_PIN), scl=Pin(I2C_SCL_PIN), freq=400000) lcd = I2cLcd(i2c, I2C_ADDR, I2C_NUM_ROWS, I2C_NUM_COLS) # ── Initialize DHT22 sensor ── sensor = dht.DHT22(Pin(DHT_PIN)) # ── Startup splash screen ── lcd.clear() lcd.putstr("MakeMindz") lcd.move_to(0, 1) lcd.putstr("Temp & Humidity") sleep(2) print("Temperature and Humidity Monitor Started!") print("=" * 40) def celsius_to_fahrenheit(celsius): """Convert Celsius to Fahrenheit.""" return (celsius * 9/5) + 32 def read_sensor(): """Read temperature and humidity from DHT22.""" try: sensor.measure() temp_c = sensor.temperature() humidity = sensor.humidity() return temp_c, humidity except OSError as e: print(f"Failed to read sensor: {e}") return None, None def display_readings(temp_c, humidity): """Display temperature and humidity on LCD and console.""" if temp_c is not None and humidity is not None: temp_f = celsius_to_fahrenheit(temp_c) lcd.clear() lcd.putstr(f"Temp:{temp_c:.1f}C") # Line 1 lcd.move_to(0, 1) lcd.putstr(f"Humid:{humidity:.1f}%") # Line 2 print(f"Temperature: {temp_c:.1f}°C ({temp_f:.1f}°F)") print(f"Humidity: {humidity:.1f}%") print("-" * 40) else: lcd.clear() lcd.putstr("Sensor Error!") lcd.move_to(0, 1) lcd.putstr("Check Wiring") print("Error: Could not read sensor data") # ── Main loop ── while True: try: temperature, humidity = read_sensor() display_readings(temperature, humidity) sleep(2) except KeyboardInterrupt: print("\nProgram stopped by user") lcd.clear() lcd.putstr("Program Stopped") break except Exception as e: print(f"Error in main loop: {e}") lcd.clear() lcd.putstr("System Error!") sleep(2)
""" LCD API Library for MicroPython Provides base API for HD44780 compatible character LCDs. """ import time class LcdApi: """Base class for HD44780 compatible LCD displays.""" LCD_CLR = 0x01 LCD_HOME = 0x02 LCD_ENTRY_MODE = 0x04 LCD_ENTRY_INC = 0x02 LCD_ENTRY_SHIFT = 0x01 LCD_ON_CTRL = 0x08 LCD_ON_DISPLAY = 0x04 LCD_ON_CURSOR = 0x02 LCD_ON_BLINK = 0x01 LCD_MOVE = 0x10 LCD_MOVE_DISP = 0x08 LCD_MOVE_RIGHT = 0x04 LCD_FUNCTION = 0x20 LCD_FUNCTION_8BIT = 0x10 LCD_FUNCTION_2LINES= 0x08 LCD_FUNCTION_10DOTS= 0x04 LCD_CGRAM = 0x40 LCD_DDRAM = 0x80 LCD_RS_CMD = 0 LCD_RS_DATA = 1 LCD_RW_WRITE = 0 LCD_RW_READ = 1 def __init__(self, num_lines, num_columns): self.num_lines = min(num_lines, 4) self.num_columns = min(num_columns, 40) self.cursor_x = 0 self.cursor_y = 0 self.backlight = True self.display_off() self.backlight_on() self.clear() self.hal_write_command(self.LCD_ENTRY_MODE | self.LCD_ENTRY_INC) self.hide_cursor() self.display_on() def clear(self): self.hal_write_command(self.LCD_CLR) self.hal_write_command(self.LCD_HOME) self.cursor_x = 0 self.cursor_y = 0 def show_cursor(self): self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY | self.LCD_ON_CURSOR) def hide_cursor(self): self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY) def display_on(self): self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY) def display_off(self): self.hal_write_command(self.LCD_ON_CTRL) def backlight_on(self): self.backlight = True self.hal_backlight_on() def backlight_off(self): self.backlight = False self.hal_backlight_off() def move_to(self, cursor_x, cursor_y): self.cursor_x = cursor_x self.cursor_y = cursor_y addr = cursor_x & 0x3f if cursor_y & 1: addr += 0x40 if cursor_y & 2: addr += self.num_columns self.hal_write_command(self.LCD_DDRAM | addr) def putchar(self, char): if char == '\n': if self.cursor_y < self.num_lines - 1: self.cursor_y += 1 self.cursor_x = 0 self.move_to(self.cursor_x, self.cursor_y) else: self.hal_write_data(ord(char)) self.cursor_x += 1 if self.cursor_x >= self.num_columns: self.cursor_x = 0 if self.cursor_y < self.num_lines - 1: self.cursor_y += 1 self.move_to(self.cursor_x, self.cursor_y) def putstr(self, string): for char in string: self.putchar(char) def hal_backlight_on(self): pass def hal_backlight_off(self): pass def hal_write_command(self, cmd): raise NotImplementedError def hal_write_data(self, data): raise NotImplementedError
""" I2C LCD Driver for Raspberry Pi Pico (MicroPython) Controls HD44780 LCD via PCF8574 I2C expander chip. """ import time from lcd_api import LcdApi # PCF8574 pin bit masks MASK_RS = 0x01 # P0 — Register Select MASK_RW = 0x02 # P1 — Read/Write (always 0 = write) MASK_E = 0x04 # P2 — Enable strobe MASK_BACKLIGHT= 0x08 # P3 — Backlight control SHIFT_DATA = 4 # P4–P7 carry 4-bit data nibble class I2cLcd(LcdApi): """HD44780 LCD via PCF8574 I2C backpack.""" def __init__(self, i2c, i2c_addr, num_lines, num_columns): self.i2c = i2c self.i2c_addr = i2c_addr self.i2c.writeto(self.i2c_addr, bytes([0])) time.sleep_ms(20) # 8-bit reset sequence (3×) self.hal_write_init_nibble(self.LCD_FUNCTION | self.LCD_FUNCTION_8BIT) time.sleep_ms(5) self.hal_write_init_nibble(self.LCD_FUNCTION | self.LCD_FUNCTION_8BIT) time.sleep_ms(1) self.hal_write_init_nibble(self.LCD_FUNCTION | self.LCD_FUNCTION_8BIT) time.sleep_ms(1) # Switch to 4-bit mode self.hal_write_init_nibble(self.LCD_FUNCTION) time.sleep_ms(1) LcdApi.__init__(self, num_lines, num_columns) cmd = self.LCD_FUNCTION if num_lines > 1: cmd |= self.LCD_FUNCTION_2LINES self.hal_write_command(cmd) def hal_write_init_nibble(self, nibble): byte = ((nibble >> 4) & 0x0f) << SHIFT_DATA self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) time.sleep_ms(1) self.i2c.writeto(self.i2c_addr, bytes([byte])) time.sleep_ms(1) def hal_backlight_on(self): self.i2c.writeto(self.i2c_addr, bytes([1 << 3])) def hal_backlight_off(self): self.i2c.writeto(self.i2c_addr, bytes([0])) def hal_write_command(self, cmd): byte = ((self.backlight << 3) | (((cmd >> 4) & 0x0f) << SHIFT_DATA)) self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) time.sleep_ms(1) self.i2c.writeto(self.i2c_addr, bytes([byte])) byte = ((self.backlight << 3) | ((cmd & 0x0f) << SHIFT_DATA)) self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) time.sleep_ms(1) self.i2c.writeto(self.i2c_addr, bytes([byte])) if cmd <= 3: time.sleep_ms(5) def hal_write_data(self, data): byte = (MASK_RS | (self.backlight << 3) | (((data >> 4) & 0x0f) << SHIFT_DATA)) self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) time.sleep_ms(1) self.i2c.writeto(self.i2c_addr, bytes([byte])) byte = (MASK_RS | (self.backlight << 3) | ((data & 0x0f) << SHIFT_DATA)) self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) time.sleep_ms(1) self.i2c.writeto(self.i2c_addr, bytes([byte]))
Explanation
How It Works
Three systems work together: sensor reading, I2C LCD display, and error handling.
DHT22 Single-Wire Protocol
The DHT22 uses a proprietary single-wire protocol. When the Pico pulls the data line LOW for ~18ms, the sensor responds with 40 bits of data — 16 bits temperature, 16 bits humidity, 8 bits checksum. MicroPython's built-in dht module handles all the timing automatically. You just call sensor.measure() then read sensor.temperature() and sensor.humidity().
I2C LCD Communication
The LCD uses an I2C backpack module (PCF8574 chip) that translates 2-wire I2C into the 8-bit parallel interface of the HD44780 controller. The Pico sends data to address 0x27 at 400kHz. The pico_i2c_lcd.py driver handles the nibble-splitting (4-bit mode) and the Enable pulse toggling needed by the HD44780 protocol.
Error Handling
The read_sensor() function wraps the DHT22 read in a try/except OSError block. If the sensor fails to respond (loose wire, timing issue), instead of crashing, it returns None, None. The display_readings() function checks for this and shows Sensor Error! / Check Wiring on the LCD — great practice for production embedded code.
Education
What You'll Learn
Use Cases
Comments
Post a Comment