Skip to main content

Ultrasonic Distance Sensor with Raspberry Pi Pico using Wokwi Simulator

Ultrasonic Distance Sensor with Raspberry Pi Pico & HC-SR04 | Wokwi MicroPython Tutorial
Beginner MicroPython Wokwi Simulator HC-SR04 I2C LCD

Ultrasonic Distance Sensor
with Raspberry Pi Pico

Measure 2–400 cm in real-time using HC-SR04 and MicroPython. Displays on a 16×2 I2C LCD with colour-coded LED alerts and buzzer. No hardware needed — simulate everything in Wokwi.

📅 February 2026 ⏱ ~20 min read 🎯 Wokwi Project #459620782326878209 🐍 MicroPython v1.0

This beginner-friendly project teaches you how to interface the popular HC-SR04 ultrasonic sensor with a Raspberry Pi Pico running MicroPython. You'll get real-time distance readings on a 16×2 I2C LCD, colour-coded LED proximity indicators, and an optional buzzer alert — all without needing physical hardware thanks to the Wokwi simulator.

Key Features

📏
2 cm – 400 cm Range Accurate to ±3 mm across the full measurement range
🖥️
Dual-Unit LCD Display Shows distance in both centimetres and inches, updating every 500 ms
🔴
RGB LED Indicators Red = Close (<30 cm), Yellow = Medium, Green = Safe, Blue = Clear
🔔
Buzzer Proximity Alert Configurable beep patterns for critical, warning, and caution zones
🛡️
Error Handling Timeout detection and out-of-range display for invalid readings
🖥️
Serial Logging Formatted data output to the serial monitor for debugging and data collection

Components Required

# Component Qty Type
1Raspberry Pi Pico1Hardware
2HC-SR04 Ultrasonic Sensor1Hardware
316×2 I2C LCD Display (0x27)1Hardware
4Red LED1Hardware
5Green LED1Hardware
6Blue LED1Hardware
7Active Buzzer1Hardware
8220 Ω Resistors3Passive
9lcd_api.pyLibrary
10pico_i2c_lcd.pyLibrary

diagram.json — Wokwi Circuit File

Copy the JSON below into your Wokwi project's diagram.json to instantly wire up all components. The simulator will render the circuit automatically.

📄 diagram.json
{
  "version": 1,
  "author": "Ultrasonic Distance Sensor",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-pi-pico", "id": "pico", "top": 0, "left": 0, "attrs": {} },
    {
      "type": "wokwi-hc-sr04",
      "id": "ultrasonic1",
      "top": -100,
      "left": 250,
      "attrs": { "distance": "100" }
    },
    { "type": "wokwi-lcd1602", "id": "lcd1", "top": 100, "left": 250, "attrs": { "pins": "i2c" } },
    {
      "type": "wokwi-led", "id": "led_red",
      "top": -80, "left": 450,
      "attrs": { "color": "red", "label": "Red" }
    },
    {
      "type": "wokwi-led", "id": "led_green",
      "top": -20, "left": 450,
      "attrs": { "color": "green", "label": "Green" }
    },
    {
      "type": "wokwi-led", "id": "led_blue",
      "top": 40, "left": 450,
      "attrs": { "color": "blue", "label": "Blue" }
    },
    { "type": "wokwi-buzzer", "id": "bz1", "top": 280.8, "left": 289.8, "attrs": {} },
    { "type": "wokwi-resistor", "id": "r1", "top": -80, "left": 500, "rotate": 90, "attrs": { "value": "220" } },
    { "type": "wokwi-resistor", "id": "r2", "top": -20, "left": 500, "rotate": 90, "attrs": { "value": "220" } },
    { "type": "wokwi-resistor", "id": "r3", "top": 40, "left": 500, "rotate": 90, "attrs": { "value": "220" } }
  ],
  "connections": [
    [ "pico:GP0", "lcd1:SDA", "green", [ "h0" ] ],
    [ "pico:GP1", "lcd1:SCL", "blue", [ "h0" ] ],
    [ "pico:3V3", "lcd1:VCC", "red", [ "h0" ] ],
    [ "pico:GND.1", "lcd1:GND", "black", [ "h0" ] ],
    [ "pico:GP2", "ultrasonic1:TRIG", "yellow", [ "h0" ] ],
    [ "pico:GP3", "ultrasonic1:ECHO", "orange", [ "h0" ] ],
    [ "pico:3V3", "ultrasonic1:VCC", "red", [ "h0" ] ],
    [ "pico:GND.2", "ultrasonic1:GND", "black", [ "h0" ] ],
    [ "pico:GP13", "r1:1", "red", [ "h0" ] ],
    [ "r1:2", "led_red:A", "red", [ "v0" ] ],
    [ "led_red:C", "pico:GND.3", "black", [ "h0" ] ],
    [ "pico:GP14", "r2:1", "green", [ "h0" ] ],
    [ "r2:2", "led_green:A", "green", [ "v0" ] ],
    [ "led_green:C", "pico:GND.3", "black", [ "h0" ] ],
    [ "pico:GP15", "r3:1", "blue", [ "h0" ] ],
    [ "r3:2", "led_blue:A", "blue", [ "v0" ] ],
    [ "led_blue:C", "pico:GND.3", "black", [ "h0" ] ],
    [ "pico:GP16", "bz1:1", "purple", [ "h0" ] ],
    [ "bz1:2", "pico:GND.4", "black", [ "h0" ] ]
  ],
  "dependencies": {}
}

Circuit Connections

🔊 HC-SR04 Ultrasonic Sensor
VCC 3V3 (Pin 36)
GND GND
TRIG GP2
ECHO GP3
🖥️ 16×2 I2C LCD
SDA GP0 (I2C0 SDA)
SCL GP1 (I2C0 SCL)
VCC 3V3
GND GND
💡 LED Indicators
Red LED (+) GP13 → 220Ω
Green LED (+) GP14 → 220Ω
Blue LED (+) GP15 → 220Ω
All Cathodes GND
🔔 Buzzer (Optional)
Positive (+) GP16
Negative (−) GND

MicroPython Code

This project requires three files: main.py (main program), lcd_api.py (base LCD class), and pico_i2c_lcd.py (I2C driver). All three must be uploaded to your Pico or added to your Wokwi project.

main.py

main.py
MicroPython
"""
ULTRASONIC DISTANCE SENSOR WITH RASPBERRY PI PICO
Hardware: Pico · HC-SR04 · 16x2 I2C LCD · RGB LEDs · Buzzer
"""

from machine import Pin, I2C, PWM
import utime
from lcd_api import LcdApi
from pico_i2c_lcd import I2cLcd

# ── PIN CONFIGURATION ──────────────────────────────
TRIG_PIN  = 2     # HC-SR04 trigger
ECHO_PIN  = 3     # HC-SR04 echo
RGB_RED   = 13    # PWM red channel
RGB_GREEN = 14    # PWM green channel
RGB_BLUE  = 15    # PWM blue channel
BUZZER_PIN = 16   # Active buzzer
LCD_SDA   = 0     # I2C SDA
LCD_SCL   = 1     # I2C SCL

# ── DISTANCE THRESHOLDS (cm) ───────────────────────
VERY_CLOSE = 10
CLOSE      = 30
MEDIUM     = 100
FAR        = 200

# ── LCD CONFIGURATION ─────────────────────────────
I2C_ADDR     = 0x27
I2C_NUM_ROWS = 2
I2C_NUM_COLS = 16

# ── INITIALISE HARDWARE ───────────────────────────
trigger   = Pin(TRIG_PIN, Pin.OUT)
echo      = Pin(ECHO_PIN, Pin.IN)
red_led   = PWM(Pin(RGB_RED));   red_led.freq(1000)
green_led = PWM(Pin(RGB_GREEN)); green_led.freq(1000)
blue_led  = PWM(Pin(RGB_BLUE));  blue_led.freq(1000)
buzzer    = Pin(BUZZER_PIN, Pin.OUT)
i2c = I2C(0, sda=Pin(LCD_SDA), scl=Pin(LCD_SCL), freq=400000)
lcd = I2cLcd(i2c, I2C_ADDR, I2C_NUM_ROWS, I2C_NUM_COLS)

# ── HELPER: RGB LED ───────────────────────────────
def set_rgb_color(r, g, b):
    red_led.duty_u16(r)
    green_led.duty_u16(g)
    blue_led.duty_u16(b)

# ── HELPER: BUZZER ────────────────────────────────
def play_buzzer(beeps, duration=100):
    for _ in range(beeps):
        buzzer.value(1); utime.sleep_ms(duration)
        buzzer.value(0); utime.sleep_ms(duration)

# ── MEASURE DISTANCE ──────────────────────────────
def measure_distance():
    trigger.low();  utime.sleep_us(2)
    trigger.high(); utime.sleep_us(10)
    trigger.low()
    t0 = utime.ticks_us()
    while echo.value() == 0:
        signal_off = utime.ticks_us()
        if utime.ticks_diff(signal_off, t0) > 30000: return -1
    t0 = utime.ticks_us()
    while echo.value() == 1:
        signal_on = utime.ticks_us()
        if utime.ticks_diff(signal_on, t0) > 30000: return -1
    return (utime.ticks_diff(signal_on, signal_off) * 0.0343) / 2

# ── UPDATE LCD ────────────────────────────────────
def update_lcd(cm, inch):
    lcd.clear()
    if cm < 0:
        lcd.putstr("  SENSOR ERROR  "); lcd.move_to(0,1); lcd.putstr("  Out of Range  ")
    elif cm > 400:
        lcd.putstr("Distance: OUT   "); lcd.move_to(0,1); lcd.putstr("OF RANGE (>4m)  ")
    else:
        lcd.putstr(f"Dist: {cm:.1f} cm")
        lcd.move_to(0,1); lcd.putstr(f"      {inch:.1f} in")

# ── PROCESS ALERTS ────────────────────────────────
def process_alerts(d):
    if   d < 0:          set_rgb_color(65535, 0, 0);      play_buzzer(3, 50)
    elif d < VERY_CLOSE: set_rgb_color(65535, 0, 0);      play_buzzer(3, 100)
    elif d < CLOSE:      set_rgb_color(65535, 16384, 0);  play_buzzer(2, 150)
    elif d < MEDIUM:     set_rgb_color(65535, 65535, 0);  play_buzzer(1, 100)
    elif d < FAR:        set_rgb_color(0, 65535, 0)
    else:                set_rgb_color(0, 0, 65535)

# ── MAIN LOOP ─────────────────────────────────────
def main():
    count = 0
    while True:
        try:
            cm = measure_distance()
            inch = cm / 2.54
            count += 1
            update_lcd(cm, inch)
            print(f"#{count:04d} | {cm:6.2f} cm | {inch:6.2f} in")
            process_alerts(cm)
            utime.sleep_ms(500)
        except KeyboardInterrupt:
            lcd.clear(); lcd.putstr("   STOPPED!   ")
            set_rgb_color(0, 0, 0); buzzer.value(0); break

if __name__ == "__main__": main()

lcd_api.py

lcd_api.py
MicroPython
"""LCD API — Base class for HD44780-compatible displays."""

class LcdApi:
    LCD_CLR = 0x01; LCD_HOME = 0x02
    LCD_ENTRY_MODE = 0x04; LCD_ENTRY_INC = 0x02
    LCD_ON_CTRL = 0x08; LCD_ON_DISPLAY = 0x04
    LCD_ON_CURSOR = 0x02; LCD_ON_BLINK = 0x01
    LCD_FUNCTION = 0x20; LCD_FUNCTION_8BIT = 0x10
    LCD_FUNCTION_2LINES = 0x08; LCD_CGRAM = 0x40; LCD_DDRAM = 0x80

    def __init__(self, num_lines, num_columns):
        self.num_lines = num_lines; self.num_columns = num_columns
        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 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, x, y):
        self.cursor_x = x; self.cursor_y = y
        addr = x & 0x3f
        if y & 1: addr += 0x40
        if y & 2: addr += self.num_columns
        self.hal_write_command(self.LCD_DDRAM | addr)

    def putchar(self, char):
        if char != '\n':
            self.hal_write_data(ord(char)); self.cursor_x += 1
        if self.cursor_x >= self.num_columns or char == '\n':
            self.cursor_x = 0; self.cursor_y += 1
            if self.cursor_y >= self.num_lines: self.cursor_y = 0
            self.move_to(self.cursor_x, self.cursor_y)

    def putstr(self, string):
        for ch in string: self.putchar(ch)

    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
    def hal_sleep_us(self, usecs): raise NotImplementedError

pico_i2c_lcd.py

pico_i2c_lcd.py
MicroPython
"""I2C LCD Driver — PCF8574 adapter for Raspberry Pi Pico."""

import utime
from lcd_api import LcdApi

MASK_RS = 0x01; MASK_RW = 0x02; MASK_E = 0x04
MASK_BACKLIGHT = 0x08; SHIFT_DATA = 4

class I2cLcd(LcdApi):
    def __init__(self, i2c, i2c_addr, num_lines, num_columns):
        self.i2c = i2c; self.i2c_addr = i2c_addr
        self.i2c.writeto(self.i2c_addr, bytearray([0]))
        utime.sleep_ms(20)
        self.hal_write_init_nibble(self.LCD_FUNCTION | self.LCD_FUNCTION_8BIT)
        utime.sleep_ms(5)
        self.hal_write_init_nibble(self.LCD_FUNCTION | self.LCD_FUNCTION_8BIT)
        utime.sleep_ms(1)
        self.hal_write_init_nibble(self.LCD_FUNCTION | self.LCD_FUNCTION_8BIT)
        utime.sleep_ms(1)
        self.hal_write_init_nibble(self.LCD_FUNCTION)
        utime.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, bytearray([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytearray([byte]))

    def hal_backlight_on(self):
        self.i2c.writeto(self.i2c_addr, bytearray([1 << 3]))

    def hal_backlight_off(self):
        self.i2c.writeto(self.i2c_addr, bytearray([0]))

    def hal_write_command(self, cmd):
        byte = ((self.backlight << 3) | (((cmd >> 4) & 0x0f) << SHIFT_DATA))
        self.i2c.writeto(self.i2c_addr, bytearray([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytearray([byte]))
        byte = ((self.backlight << 3) | ((cmd & 0x0f) << SHIFT_DATA))
        self.i2c.writeto(self.i2c_addr, bytearray([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytearray([byte]))
        if cmd <= 3: utime.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, bytearray([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytearray([byte]))
        byte = (MASK_RS | (self.backlight << 3) | ((data & 0x0f) << SHIFT_DATA))
        self.i2c.writeto(self.i2c_addr, bytearray([byte | MASK_E]))
        self.i2c.writeto(self.i2c_addr, bytearray([byte]))

    def hal_sleep_us(self, usecs): utime.sleep_us(usecs)

How It Works

The HC-SR04 uses the speed of sound to calculate distance. The Pico controls the timing with microsecond precision.

01
Trigger Phase

Pico sends a 10 µs HIGH pulse to the TRIG pin to initiate a measurement cycle.

02
Ultrasonic Burst

The HC-SR04 emits 8 pulses at 40 kHz, inaudible to humans but easily reflected by solid objects.

03
Echo Wait

Sound waves travel outward, bounce off the nearest surface, and return to the sensor's microphone.

04
Time Measurement

The ECHO pin stays HIGH for a duration equal to the total round-trip travel time of the sound wave.

05
Distance Calculation

Distance is derived from echo duration. The result is divided by 2 since sound makes a round trip.

Distance (cm) = (Echo Time µs × 0.0343) ÷ 2 Speed of sound ≈ 343 m/s = 0.0343 cm/µs · divided by 2 for round-trip

Applications

🚗
Parking Assist
Warn drivers of obstacles while reversing
🤖
Obstacle Avoidance
Robot navigation and collision prevention
💧
Liquid Level
Non-contact tank fill-level monitoring
🗑️
Smart Bin
Detect when waste bins need emptying
🔐
Security System
Intruder detection via presence sensing
🚪
Automatic Doors
Trigger door opening when people approach
🏭
Industrial Automation
Object detection on conveyor belts
🌱
Smart Agriculture
Measure crop height or reservoir levels

FAQ

Can I test this without buying hardware? +
Yes! The entire project runs in the Wokwi simulator. Open the free simulation link at the top of this article, paste the diagram.json, add the three Python files, and press Play — no physical components required.
What is the range and accuracy of the HC-SR04? +
The HC-SR04 measures from 2 cm to 400 cm with an accuracy of approximately ±3 mm under ideal conditions. Performance degrades with soft, angled, or very small targets.
Which programming language is used? +
MicroPython — a lean Python 3 implementation designed specifically for microcontrollers. It provides easy access to hardware peripherals like GPIO, I2C, and PWM with clean, readable syntax.
Can I change the distance thresholds? +
Absolutely. In main.py, edit the four constants at the top: VERY_CLOSE, CLOSE, MEDIUM, and FAR. These values control when each LED colour and buzzer pattern activates.
My LCD isn't displaying anything — what should I check? +
First confirm the I2C address. Most PCF8574-based LCD backpacks use 0x27, but some use 0x3F. Run an I2C scan on the Pico to detect the correct address, then update I2C_ADDR in main.py.

© 2026 MakeMindz · Raspberry Pi Pico MicroPython Tutorials

Made for learners, makers, and IoT enthusiasts everywhere.

Comments

try for free