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
- The microcontroller holds the TRIG pin high for at least 10 microseconds.
- The module transmits a burst of eight 40 kHz ultrasonic pulses.
- It sets the ECHO pin high and keeps it high until the reflected sound returns, or until it gives up if nothing comes back.
- 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 CThe 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 pin | Connect to | Physical pin |
|---|---|---|
| VCC | VBUS (5 V from USB) | 40 |
| TRIG | GP14 directly | 19 |
| ECHO | GP15 through the 1 k / 2 k divider | 20 |
| GND | GND | 38 |
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 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_USuses 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
Timercallback. ticks_addandticks_diffschedule 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_Cto 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.