Raspberry Pi Pico HC-SR04 Ultrasonic Distance Sensor in MicroPython

The HC-SR04 measures distance the way a bat does: it sends a short burst of ultrasound and times how long the echo takes to come back. It is the standard sensor for obstacle-avoiding robots, water-tank level meters and parking aids. There is one catch with the Pico: the module runs on 5 V and its echo output is a 5 V signal, which is too high for the Pico's 3.3 V pins. This tutorial shows the safe wiring, the timing maths, reliable MicroPython code with filtering, and a simple parking sensor that blinks faster as an object gets closer.

How the HC-SR04 Works

  1. The microcontroller holds the TRIG pin high for at least 10 microseconds.
  2. The module transmits a burst of eight 40 kHz ultrasonic pulses.
  3. It sets the ECHO pin high and keeps it high until the reflected sound returns, or until it gives up if nothing comes back.
  4. The width of the ECHO pulse equals the round-trip travel time of the sound.

Sound travels about 343 metres per second in air at 20 degrees C, which is 0.0343 cm per microsecond. Because the pulse measures the trip there and back, divide by two.

distance (cm) = echo_time (us) x 0.0343 / 2
             = echo_time (us) / 58.3

Example: echo pulse = 1166 us
distance = 1166 x 0.0343 / 2 = 20.0 cm

Speed of sound vs temperature:
c (m/s) = 331.3 + 0.606 x T (degrees C)
At 35 C: c = 352.5 m/s, about 2.8% faster than at 20 C

The module's datasheet quotes a range of 2 cm to 400 cm. In practice, reliable readings depend on the target: a flat wall facing the sensor reflects well, while soft cloth, angled surfaces or thin objects may return no echo at all.

Safe Wiring with a Voltage Divider

Power the HC-SR04 from VBUS (5 V when the Pico is on USB). The TRIG input accepts the Pico's 3.3 V high level directly. The ECHO output, however, swings to 5 V, so pass it through a resistor divider made from a 1 k and a 2 k resistor (or 1 k and 2.2 k).

ECHO ----[ 1 k ]----+---- GP15 (pin 20)
                    |
                 [ 2 k ]
                    |
                   GND

V_GP15 = 5 V x 2 k / (1 k + 2 k) = 3.33 V
HC-SR04 pinConnect toPhysical pin
VCCVBUS (5 V from USB)40
TRIGGP14 directly19
ECHOGP15 through the 1 k / 2 k divider20
GNDGND38

The Code

MicroPython's machine.time_pulse_us(pin, level, timeout_us) waits for the pin to reach level and then measures how long it stays there, in microseconds. It returns -2 if the pulse never started and -1 if the pulse lasted longer than the timeout. The program below wraps this in a median filter and drives a parking indicator on the onboard LED.

hcsr04_parking.py - HC-SR04 on GP14/GP15 with temperature compensation, median filter and blinking LED
# hcsr04_parking.py - HC-SR04 distance sensor with parking indicator
from machine import Pin, time_pulse_us
import time

TRIG = Pin(14, Pin.OUT, value=0)
ECHO = Pin(15, Pin.IN)            # through the 1 k / 2 k divider
led = Pin('LED', Pin.OUT)

AIR_TEMP_C = 27.0                 # set to your room temperature
SOUND_CM_PER_US = (331.3 + 0.606 * AIR_TEMP_C) / 10000   # m/s -> cm/us
TIMEOUT_US = 30000                # echo from about 5 m, beyond the rated range

def read_cm():
    # Send a clean 10 us trigger pulse
    TRIG.value(0)
    time.sleep_us(5)
    TRIG.value(1)
    time.sleep_us(10)
    TRIG.value(0)
    # Measure how long ECHO stays high
    t = time_pulse_us(ECHO, 1, TIMEOUT_US)
    if t < 0:
        return None               # -1 or -2 means no valid echo
    return t * SOUND_CM_PER_US / 2

def read_median(n=5):
    values = []
    for _ in range(n):
        d = read_cm()
        if d is not None:
            values.append(d)
        time.sleep_ms(60)         # let echoes die out between pings
    if not values:
        return None
    values.sort()
    return values[len(values) // 2]

next_toggle = time.ticks_ms()
blink_ms = 0

while True:
    d = read_median()
    if d is None:
        print('out of range')
        blink_ms = 0              # LED off: nothing detected
    else:
        print('{:.1f} cm'.format(d))
        if d < 10:
            blink_ms = -1         # solid on: too close
        elif d < 100:
            blink_ms = int(d * 5) # 10 cm -> 50 ms, 100 cm -> 500 ms
        else:
            blink_ms = 0

    if blink_ms == -1:
        led.on()
    elif blink_ms == 0:
        led.off()
    elif time.ticks_diff(time.ticks_ms(), next_toggle) >= 0:
        led.toggle()
        next_toggle = time.ticks_add(time.ticks_ms(), blink_ms)

How the Code Works

  • SOUND_CM_PER_US uses the temperature formula and converts metres per second to centimetres per microsecond by dividing by 10,000.
  • The 60 ms gap between pings prevents a late echo from the previous ping being mistaken for the current one.
  • The median of five readings rejects single wild values far better than an average, which a single bad echo would drag off.
  • Each loop takes about 300 ms (five pings at 60 ms), so the LED toggles at most once per loop. For a faster indicator, take fewer samples or move the LED to a Timer callback.
  • ticks_add and ticks_diff schedule the next toggle safely across counter wrap-around.
  • A 30,000 microsecond timeout corresponds to an echo from roughly 5 m, so the code never waits long for a reflection that cannot arrive within the rated 400 cm range.

Troubleshooting

  • Always out of range: the module has no 5 V, TRIG and ECHO are swapped, or the divider is wired backwards so GP15 never sees a high level.
  • Readings stuck at about 2 to 3 cm: the sensor is too close to a surface or sees the edge of the breadboard. Mount it at the board edge facing open space.
  • Distances 2 to 3 percent off: set AIR_TEMP_C to the real temperature.
  • Random large values: soft or angled targets, or two ultrasonic sensors pinging at the same time. Fire multiple sensors one after another.

Summary

The HC-SR04 converts distance into an echo pulse width. On the Pico you trigger it with a 10 microsecond pulse, protect GP15 from the 5 V echo with a 1 k / 2 k divider, measure the pulse with time_pulse_us, and convert using the temperature-corrected speed of sound. Median filtering makes readings stable enough for a robot or a parking aid.