Ultrasonic Distance Sensor
The ultrasonic distance sensor is directly connected to a digital input. This sensor requires a trigger signal to emit an ultrasonic pulse. The distance measurement works by analyzing the time-of-flight of the emitted signal:
- The sensor emits an ultrasonic pulse upon receiving a trigger signal.
- The pulse travels to a surface and reflects back to the sensor.
- The sensor outputs a signal with a pulse width ($ t_{\text{imp}}$) proportional to the travel time of the ultrasonic wave.
Since the pulse travels to the object and back, the distance to the object is calculated using the formula:
where:
- \(t_{\text{imp}} = time\) interval corresponding to the signal's round-trip
- \(v_{\text{sound}} = 332 \text{ m/s} \quad \text{at Normal Temperature and Pressure (NTP): 20°C, 101.325 kPa}\)
Since the sound wave travels the distance twice (to the object and back), the equation includes a division by 2.
Operating Principle of the Ultrasonic Distance Sensor
The sensor's operation is illustrated in the diagram from its datasheet. The key components of its functionality are as follows:
- Trigger Signal
- Connected to the microcontroller's GP0 pin.
- Initiates the ultrasonic pulse emission when activated.
- Echo Signal
- Connected to the microcontroller's GP1 pin.
- Receives the reflected ultrasonic signal and provides a pulse duration proportional to the distance.
- Ultrasonic Pulse Emission
- Represented in the middle section of the diagram.
- This is an internal process of the sensor and not accessible by the microcontroller.
Timing diagram of the ultrasonic sensor control

Info
Ultrasonic sensor datasheet HC-SR04 Datasheet
⚡Hands-on tasks
✅ Task 1 - Measuring Pulse Duration on a Pin
The fundamental step in ultrasonic distance measurement is to precisely measure the duration of an echo pulse to calculate distance, as discussed above. To achieve this, we need to create a program that measures the duration of a pulse, which is essentially the time difference between a LOW-to-HIGH and HIGH-to-LOW transition.
First, let's create a program that serves as an example of how to measure the time of level changes on an input - in this case on the previously used GP9 - providing the necessary foundation for later implementing ultrasonic sensor measurements.
To pull GP9 to the GND use the jumper as we did at the GPIOs.
from machine import Pin
from time import sleep, ticks_us, ticks_diff
# --- Configuration ---
# Configure GP9 as input with internal pull-up (default HIGH)
input_pin = Pin(9, Pin.IN, Pin.PULL_UP)
print("System Startup")
print("Monitoring GP9 for low-to-high transitions...")
while True:
# Wait for GP9 to be pulled LOW
while input_pin.value() == 1:
pass
start_time = ticks_us()
print("> GP9 pulled LOW - timing started.")
# Wait for GP9 to be released back to HIGH
while input_pin.value() == 0:
pass
end_time = ticks_us()
# Use ticks_diff() to handle timer overflows correctly
elapsed_time = ticks_diff(end_time, start_time)
print("> GP9 released HIGH - timing stopped.")
print("Elapsed time: {} milliseconds".format(elapsed_time/1000))
# Short delay before next measurement
sleep(0.5)
Why Are We Doing This?
This code helps us understand how to measure the time difference between two events on a GPIO pin. It serves as a preparatory step for working with ultrasonic sensors, where we need to measure the time a signal takes to travel and return.
What Does ticks_us() Do?
ticks_us()is a function in MicroPython that returns the current time in microseconds (µs).- It provides a continuously increasing counter that we can use for precise time measurements.
- Unlike
time.time(), which works with seconds,ticks_us()is much finer, making it suitable for fast event timing, like measuring sensor pulses.
How Do We Calculate the Time Difference?
-
Record the timestamp when the event starts (pin goes LOW)
-
Record the timestamp when the event stops (pin goes HIGH)
-
**Calculate the elapsed time by subtracting start from end would be fine but ticks_diff() calculates correctly even ticks_us value overflow **
# use ticks_diff(t1, t2) calculates t1 - t2 correctly
elapsed_time = ticks_diff(end_time, start_time)
This gives us the time difference in microseconds.
Why Use ticks_diff() instead of subtraction > end_time - start_time?
In MicroPython, functions like ticks_us(), ticks_ms(), and ticks_cpu() return unsigned integer timestamps that wrap around (overflow) after reaching their maximum value. This means that using simple subtraction to calculate elapsed time may produce incorrect results when an overflow occurs.
The Problem: Overflow in Timer Values
Let's consider a 32-bit timer (ticks_us()), which increases over time. Since it has a fixed range from 0 to 2^32 - 1 (4,294,967,295), it overflows (resets to 0) after reaching its maximum value. This happens roughly every 71.6 minutes.
Imagine the following scenario:
If you simply subtract two ticks_us() values:
time1 = ticks_us()
# Some delay...
time2 = ticks_us()
diff = time2 - time1 # What happens if an overflow occurs?
What Goes Wrong?
If time2 > time1, the subtraction works fine. However, if an overflow happens between time1 and time2, we might get an incorrect negative value.
The Solution: ticks_diff()
MicroPython provides ticks_diff() to correctly handle overflow:
from machine import ticks_us, ticks_diff
time1 = ticks_us()
# Some delay...
time2 = ticks_us()
diff = ticks_diff(time2, time1) # Correct, even if an overflow happens
ticks_diff() Works
According to the MicroPython documentation:
-
ticks_diff(ticks1, ticks2)works like subtraction (ticks1 - ticks2), but it handles wraparounds using modular arithmetic. -
It returns a signed integer in the range:
[-\text{TICKS_PERIOD}/2, \text{TICKS_PERIOD}/2 - 1]
This follows the standard two's-complement integer behavior.
-
If the result is negative, it means
ticks1happened earlier thanticks2. -
If the result is positive, it means
ticks1happened afterticks2.This function is reliable as long as the two timestamps are not more than half of the wraparound period apart.
How C Handles This with Unsigned Integers
In C, we can safely use unsigned subtraction to achieve the same behavior:
Why Unsigned Subtraction Works (Modular Arithmetic Explanation)#include <stdint.h> uint8_t time1 = 250; uint8_t time2 = 5; uint8_t diff = time2 - time1; // Correct even if overflow occursIn fixed-width unsigned arithmetic (such as 8-bit arithmetic), numbers are represented modulo 2n2^n2n. This means that when you perform operations, the results are always within the range 000 to 2n−12^n - 12n−1 by wrapping around if necessary. For an 8-bit system, numbers wrap around modulo 256.
Unsigned Subtraction with Overflow
When performing unsigned subtraction, the result follows the rules of modular arithmetic.
Let's consider an example with 8-bit numbers:
\(7−250\)
At first glance, this seems like it would produce a negative result, but in an unsigned 8-bit system, we actually compute:
\(7 - 250 \equiv 7 + (256 - 250) \pmod{256}\)
Since:
\(256−250=6\)
We get:
\(7 + 6 = 13 \pmod{256}\)
Thus, in unsigned 8-bit arithmetic:
\(7−250=13\)
which might seem unexpected, but it makes sense when considering how numbers behave in a circular manner.
Visualizing This on a Circular Scale
Imagine a circular number scale from 0 to 255 (like a clock with 256 positions). There are two ways to measure the difference between 250 and 7:
-
Direct Subtraction:
- The absolute difference is: \(250−7=243\)
- But since 243 is greater than 127 (half of 256), the result would be considered wrapped around.
-
Wrapping Around the End of the Range:
- Instead of counting backward from 250 to 7, we count forward from 250 to 256 (which wraps to 0) and then to 7.
- This gives: \(256−243=13\)
- This is exactly the result we got from modular arithmetic.
In Other Words
- Unsigned subtraction result: The difference is calculated modulo \(2^n\) (in our case, modulo 256), which accounts for the wraparound effect.
- Distance on a circle: If you think of a clock with 256 positions, starting at 250 and moving forward 13 steps lands you at 7.
- So while the absolute difference is 243, in unsigned arithmetic, subtraction gives the correct result in the context of circular (modular) arithmetic.
Why Does This Matter for
ticks_diff()?In MicroPython, timer values from
ticks_us()are unsigned 32-bit numbers, meaning they wrap around just like our 8-bit example, but at \(2^{32} = 4,294,967,296\).Using normal subtraction, you might get incorrect negative results when a timer overflows. However,
ticks_diff()correctly computes the time difference using modular arithmetic, just like how unsigned subtraction in C works. -
Info
Further information about time methods can be found in the documentation
✅ Task 2 - Measuring Ultrasonic Sensor Pulse Duration
Based on the timing diagram Create a program that measures the pulse duration of an ultrasonic sensor's echo signal. This duration is proportional to the distance between the sensor and an object, but in this step, we will focus only on measuring and displaying the pulse duration in microseconds.
The measured value should be displayed on the terminal every 0.5 seconds. This program builds on the previous time measurement exercise.
To test the code, move your hand or any object closer to or farther from the ultrasonic sensor. Observe how the measured pulse duration changes in response to the distance.
How to measure and display distance with an ultrasonic sensor?
An ultrasonic sensor works by sending a trigger pulse, which generates an ultrasonic wave. When this wave hits an object, it reflects back, and the sensor records the time taken for the echo to return.
from machine import Pin
from time import sleep, ticks_us, ticks_diff
# --- Configuration ---
TRIGGER_PIN_NUM = 0 # Trigger pin of the ultrasonic sensor
ECHO_PIN_NUM = 1 # Echo pin of the ultrasonic sensor
# Initialize pins
trigger_pin = Pin(TRIGGER_PIN_NUM, Pin.OUT)
echo_pin = Pin(ECHO_PIN_NUM, Pin.IN)
def measure_pulse_duration_us():
"""
Triggers the ultrasonic sensor and measures the duration of the echo pulse.
Returns:
int: The duration of the echo pulse in microseconds.
"""
# Ensure trigger pin is LOW before sending pulse
trigger_pin.value(0)
sleep(0.00001) # wait 10µs for ensure low voltage level
# Send a 10µs trigger pulse
trigger_pin.value(1)
sleep(0.00001)
trigger_pin.value(0)
# Wait for echo pin to go HIGH (start of pulse)
while echo_pin.value() == 0:
pass
start_time = ticks_us()
# Wait for echo pin to go LOW (end of pulse)
while echo_pin.value() == 1:
pass
end_time = ticks_us()
# Calculate elapsed time
return ticks_diff(end_time, start_time)
print("Start measuring pulse durations...")
while True:
pulse_duration_us = measure_pulse_duration_us()
print("Pulse duration: {} microseconds".format(pulse_duration_us))
sleep(0.5)
Task 3 - Measuring Distance with the Ultrasonic Sensor
Create a Python program that calculates the distance to an object based on the pulse duration of an ultrasonic sensor. Implement a function that takes the pulse duration as an argument and returns the corresponding distance and display in the terminal.
Define a constant for the speed of sound in air:
Implement a function calculate_distance(pulse_duration) that:
- Takes the pulse duration (in seconds) as an argument.
- Computes the distance using the formula:
Warning
Don't forget to use the correct unit prefixes! Ensure you properly convert between meters (m), centimeters (cm), and millimeters (mm) when calculating distances.
Info
Check the MicroPython documentation for time related built-in methods.
📌 Note: Sensor Accuracy Testing
To test the accuracy of the sensor, you can use an A4 sheet of paper (297 mm long). Position one end of the paper against a flat surface (such as a wall or a book), and place the sensor at the other end.
✅ Extra Task 4: Formatting Measured Distance for Display
Modify the program to display the measured value in meters, rounded to three decimal places.
Info
You can control decimal precision for printing floating point numbers in Python using the print module. For more details, refer to the official Python documentation on print.
✅ Extra Task 5: Conditional Display of Measured Distance
Modify the program to print the measured distance only if the change from the previous value exceeds 20%. The displayed value should be in centimeters with two decimal places.
Tip
Displaying values based on their changes can improve efficiency. In this case, sleep can be eliminated.