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.
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
Components Required
| # | Component | Qty | Type |
|---|---|---|---|
| 1 | Raspberry Pi Pico | 1 | Hardware |
| 2 | HC-SR04 Ultrasonic Sensor | 1 | Hardware |
| 3 | 16×2 I2C LCD Display (0x27) | 1 | Hardware |
| 4 | Red LED | 1 | Hardware |
| 5 | Green LED | 1 | Hardware |
| 6 | Blue LED | 1 | Hardware |
| 7 | Active Buzzer | 1 | Hardware |
| 8 | 220 Ω Resistors | 3 | Passive |
| 9 | lcd_api.py | — | Library |
| 10 | pico_i2c_lcd.py | — | Library |
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.
{
"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
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
""" 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 — 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
"""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.
Pico sends a 10 µs HIGH pulse to the TRIG pin to initiate a measurement cycle.
The HC-SR04 emits 8 pulses at 40 kHz, inaudible to humans but easily reflected by solid objects.
Sound waves travel outward, bounce off the nearest surface, and return to the sensor's microphone.
The ECHO pin stays HIGH for a duration equal to the total round-trip travel time of the sound wave.
Distance is derived from echo duration. The result is divided by 2 since sound makes a round trip.
Applications
FAQ
diagram.json, add the three Python files, and press Play — no physical components required.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.I2C_ADDR in main.py.
Comments
Post a Comment