Back

Activity 2.13: Ultrasonic Distance

Motion & Autonomy — Day 3 of 4

divider

Activity 2.13

Ultrasonic Distance

Key Concepts

Timing a Pulse

Unit Conversion

Out-of-Range Values

Your Robot Moves. It Cannot See.

Last session it drove wherever you told it to, straight into whatever was there. Today it gets a way to find out.

Not with a camera. By shouting and timing the echo, exactly like a bat.

Send a Pulse, Time What Comes Back

One pin gives the sensor a brief nudge — ten microseconds is enough. The sensor emits a chirp too high to hear, then holds a second pin HIGH for exactly as long as the echo takes to return.

So the distance is not measured. It is timed.

You Have Timed Something Before

2.7: ticks_ms() milliseconds, for human reflexes
2.13: time_pulse_us() microseconds, for the speed of sound

A reaction time was a couple of hundred milliseconds. An echo from across the room takes about three thousand microseconds — a thousand times finer, so it needs a finer clock.

Microseconds Into Centimeters

Terminal window
sound travels 0.0343 cm every microsecond
the echo goes out AND back, so divide by 2
cm = microseconds / 58

The same shape as Activity 2.9: a raw number that means nothing until you convert it. There the datasheet gave you the rule. Here physics does.

Sometimes Nothing Comes Back

duration = time_pulse_us(echo, 1, TIMEOUT_US)
if duration < 0:
return -1 # nothing came back

Too far away, or angled so the echo bounces off sideways. The sensor simply never answers, and the timer gives up.

A negative sentinel again — the same trick as the false start in Activity 2.7, because a real distance is never negative.

Today's Objectives

  • Wiring the HC-SR04 and sending a trigger pulse
  • Timing the echo with time_pulse_us()
  • Converting a pulse duration into centimeters
  • Handling readings where nothing comes back

Key Terms

Ultrasonic
Sound above the range of human hearing — what the sensor emits.
Trigger and Echo
Two pins: one tells the sensor to chirp, the other reports how long the echo took.
Microsecond
A millionth of a second. A thousand of them make one millisecond.
Timeout
Giving up on a measurement that never arrives, rather than waiting forever.

'F' → Fullscreen

divider

Build

Keep the motor driver wired — session 16 needs both. Create a new MicroPython script named 2-13-distance.


Task 1: Time the Echo

  • Wire the sensor: VCC to 3V3, GND to GND, Trig to GPIO 22, Echo to GPIO 20.
  • Send a 10-microsecond pulse on Trig, then time how long Echo stays HIGH.
  • Print the raw number of microseconds and move your hand towards and away from the sensor.

Power it from 3V3 rather than 5V. These GPIO pins expect 3.3V signals, and the Echo pin answers at whatever voltage the sensor runs on.

Hookup diagram for the HC-SR04 ultrasonic sensor showing VCC to 3V3, Trig to GPIO 22, Echo to GPIO 20, and GND to ground
[PHOTO PLACEHOLDER — Task 1: Ultrasonic Sensor Mounted Facing Forward]
Ultrasonic Distance
from machine import Pin, time_pulse_us
import time
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
while True:
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, 30000)
print("echo lasted", duration, "microseconds")
time.sleep(0.5)

Task 2: Into Centimeters

  • Divide the duration by 58 and print centimeters instead.
  • Check it against a ruler. Hold something at 20cm and see what you get.

Work out where 58 comes from before you accept it. Sound covers 0.0343cm per microsecond, and the echo travels the distance twice.

Ultrasonic Distance
from machine import Pin, time_pulse_us
import time
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
while True:
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, 30000)
print(duration // 58, "cm")
time.sleep(0.5)

Task 3: When Nothing Comes Back

  • Point the sensor at the ceiling. The reading will be nonsense or the program will stall.
  • Move the whole measurement into read_distance(), and return -1 when the timer gives up.
  • Print a readable message instead of a meaningless number.
Ultrasonic Distance
from machine import Pin, time_pulse_us
import time
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
TIMEOUT_US = 30000
def read_distance():
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, TIMEOUT_US)
if duration < 0:
return -1
return duration // 58
while True:
cm = read_distance()
if cm < 0:
print("nothing in range")
else:
print(cm, "cm")
time.sleep(0.5)

Task 4: Steady the Reading

  • Write read_average_distance(samples) that takes several readings and averages the good ones.
  • Skip the failures rather than letting a -1 drag the average down.
  • Return -1 only if every reading failed.

Same idea as the temperature sensor in 2.9, with one new wrinkle: here some readings are not merely noisy but entirely absent, so you have to count how many were usable.

Ultrasonic Distance
from machine import Pin, time_pulse_us
import time
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
TIMEOUT_US = 30000
def read_distance():
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, TIMEOUT_US)
if duration < 0:
return -1
return duration // 58
def read_average_distance(samples):
total = 0
good = 0
for i in range(samples):
cm = read_distance()
if cm > 0:
total = total + cm
good = good + 1
time.sleep(0.02)
if good == 0:
return -1
return total // good
while True:
cm = read_average_distance(5)
if cm < 0:
print("nothing in range")
else:
print(cm, "cm")
time.sleep(0.3)

Task 5: Put It on the Screen

  • Bring the OLED and show() back from Activity 2.11.
  • Display the distance on the glass so you can read it while walking around with the robot.
Ultrasonic Distance
from machine import Pin, time_pulse_us
import qwiic_large_oled
import time
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
TIMEOUT_US = 30000
oled = qwiic_large_oled.QwiicLargeOled()
oled.begin()
def read_distance():
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, TIMEOUT_US)
if duration < 0:
return -1
return duration // 58
def read_average_distance(samples):
total = 0
good = 0
for i in range(samples):
cm = read_distance()
if cm > 0:
total = total + cm
good = good + 1
time.sleep(0.02)
if good == 0:
return -1
return total // good
def show(oled, line1, line2):
oled.clear(oled.PAGE)
oled.print(line1)
oled.print(line2)
oled.display()
while True:
cm = read_average_distance(5)
if cm < 0:
show(oled, "Distance", "out of range")
else:
show(oled, "Distance", str(cm) + " cm")
time.sleep(0.3)
[PHOTO PLACEHOLDER — Task 5: OLED Showing a Live Distance Reading]

Challenge (Optional): Find the Blind Spots

  • Work out the closest and furthest distances your sensor reports reliably.
  • Try a soft object like a jumper, and a hard flat one like a book. Try holding the book at an angle.
  • Write down what fools it. The Final Project will thank you.
divider

Checkpoint

Moving your hand towards and away from the sensor should print something like:

Terminal window
24 cm
23 cm
nothing in range
41 cm
  • A book held at 20cm reads within a couple of centimeters of 20
  • Moving your hand closer makes the number fall smoothly
  • Pointing at the ceiling reports out of range rather than a wild number
  • The program never stalls, whatever you point it at
divider

Reflection

Answer the following questions before submitting your work.

  1. Explain where the number 58 comes from, using the speed of sound and the path the echo takes.
  2. Activity 2.7 timed a reaction in milliseconds; this activity times an echo in microseconds. Explain why the finer unit is necessary here and was not there.
  3. read_average_distance() counts how many readings were usable instead of just dividing by samples. Explain what would go wrong with the simpler version.
divider

Submit

Submit the required files to the appropriate dropbox.

Activity Complete