Similar to how bats echo-locate and how submarines use sonar, the PiicoDev® Ultrasonic Rangefinder uses sound waves to measure the distance to an object. It sends out a high-pitched sound wave, which bounces off of the object and comes back to the sensor. The time it takes for the sound wave to go out and come back is used to calculate the distance to the object. Think of it like shouting and listening for an echo! By knowing how long it takes for the echo to come back, we can calculate how far away the object is.
Distance data can be used in all kinds of creative ways! Plenty of consumer devices use distance data to determine the presence of humans or obstacles - things like non-contact faucets which can control the flow of water if hands are present in a wash sink; or vehicle detectors in parking garages that indicate if a parking space is vacant.
This guide will walk through connecting a PiicoDev Ultrasonic Rangefinder to a dev. board and provide some example code to get started taking distance measurements. There's even some project inspiration further down. Let's jump in!
Contents
- Hardware and Connections
- Setup Thonny
- Download / Install PiicoDev Modules
- Measure Distance
- Code Remix - Proximity Alert
- Multiple Rangefinders on the same PiicoDev Bus
- Conclusion
- Resources
- Project Ideas
Hardware and Connections
To follow along you'll need:
- A Raspberry Pi Pico (with Soldered Headers) or Pico W (with Soldered Headers)
- A PiicoDev Ultrasonic Rangefinder
- A PiicoDev Expansion Board for Raspberry Pi Pico
- A PiicoDev Cable
First, make sure your PiicoDev Ultrasonic Rangefinder is assembled correctly. Plug the ultrasonic ranger into the 4-pin female header on the PiicoDev module such that it points outwards. Be careful here - If you connect the ranger incorrectly both it and the PiicoDev module may become damaged.
Make sure all the ID switches are OFF before proceeding.
Plug your Pico into the Expansion Board. Make sure it is installed in the correct orientation - Pin 0 on the expansion board should be adjacent to the Pico's USB connector.
Connect your PiicoDev Ultrasonic Rangefinder to the expansion board with a PiicoDev Cable.
To follow along you'll need:
- A Raspberry Pi single board computer (Pictured: Raspberry Pi 4 Model B)
- A PiicoDev Ultrasonic Rangefinder
- A PiicoDev Adapter for Raspberry Pi
- A PiicoDev Cable (100mm or longer is best for Raspberry Pi)
- (Optional) A PiicoDev Platform will keep everything mounted securely.
First, make sure your PiicoDev Ultrasonic Rangefinder is assembled correctly. Plug the ultrasonic ranger into the 4-pin female header on the PiicoDev module such that it points outwards. Be careful here - If you connect the ranger incorrectly both it and the PiicoDev module may become damaged.
Make sure all the ID switches are OFF before proceeding.
Mount the Adapter onto your Pi's GPIO. Make sure it is plugged in the correct orientation - An arrow on the Adapter will point to the Pi's Ethernet connector (on a Pi 3 the Ethernet connector and USB ports are swapped.)
Connect your PiicoDev Ultrasonic Rangefinder to the Adapter with a PiicoDev Cable.
To follow along you'll need:
- A micro:bit v2
- APiicoDev Ultrasonic Rangefinder
- A PiicoDev Adapter for micro:bit
- A PiicoDev Cable
- (Optional) A PiicoDev Platform will keep everything mounted securely.
First, make sure your PiicoDev Ultrasonic Rangefinder is assembled correctly. Plug the ultrasonic ranger into the 4-pin female header on the PiicoDev module such that it points outwards. Be careful here - If you connect the ranger incorrectly both it and the PiicoDev module may become damaged.
Make sure all the ID switches are OFF before proceeding.
Plug your micro:bit into the Adapter, making sure the buttons on the micro:bit are facing up.
Connect your PiicoDev Ultrasonic Rangefinder to the Adapter with a PiicoDev Cable.
Setup Thonny
Download / Install PiicoDev Modules
To work with PiicoDev hardware, we need to download some drivers. The drivers provide all the functions to easily connect and communicate with PiicoDev hardware. Select your dev board from the options above.
We need these files to easily drive the PiicoDev Ultrasonic Rangefinder:
- Save the following files to your preferred coding directory - In this tutorial, we save to My Documents > PiicoDev.
- Download the PiicoDev Unified Library: PiicoDev_Unified.py (right-click, "save link as").
- Download the device module: PiicoDev_Ultrasonic.py (right-click, "save link as").
- Upload the files to your Pico. This process was covered in the Setup Thonny section.
The PiicoDev Unified Library is responsible for communicating with PiicoDev hardware, and the device module contains functions for driving specific PiicoDev devices.
We need these files to easily drive the PiicoDev Ultrasonic Rangefinder:
- Save the following files to your preferred coding directory - In this tutorial, we save to My Documents > PiicoDev.
- Download the PiicoDev Unified Library: PiicoDev_Unified.py (right-click, "save link as").
- Download the device module: PiicoDev_Ultrasonic.py (right-click, "save link as").
- Upload the files to your micro:bit. This process was covered in the Setup Thonny section.
The PiicoDev Unified Library is responsible for communicating with PiicoDev hardware, and the device module contains functions for driving specific PiicoDev devices.
Measure Distance
The PiicoDev Ultrasonic Rangefinder samples distance continuously at a constant rate. You can read the latest sample from the .distance_mm
attribute (or .distance_inch
if you prefer imperial).
The following code is a minimal example for reading data from the Rangefinder, and controlling its onboard LED.
# Read distance from the PiicoDev Ultrasonic Rangefinder from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Unified import sleep_ms ranger = PiicoDev_Ultrasonic() # Initialise the rangefinder while True: print(ranger.distance_mm) ranger.led = not ranger.led # blink the LED every loop sleep_ms(100)
Code Remix - Proximity Alert
Let's do a quick code remix - we can modify the above example so that the Rangefinder's LED is only illuminated when the measured range is less than 100mm.
The .led
attribute can be set to either True
or False
. Rather than toggle the LED every loop like the previous example, we can set .led
to the result of an expression:
(ranger.distance_mm < 100)
will evaluate as True
whenever the measured distance is less than 100mm, else it will evaluate as False
. This is a pretty compact way of including the functionality we desire without having to create a new variable or using an if
statement.
# Turn on the LED when range is less than 100mm from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Unified import sleep_ms ranger = PiicoDev_Ultrasonic() # Initialise the rangefinder while True: print(ranger.distance_mm) ranger.led = (ranger.distance_mm < 100) sleep_ms(100)
Let's do a quick code remix - we can modify the above example so that the Rangefinder's LED is only illuminated when the measured range is less than 100mm.
The .led
attribute can be set to either True
or False
. Rather than toggle the LED every loop like the previous example, we can set .led
to the result of an expression:
(ranger.distance_mm < 100)
will evaluate as True
whenever the measured distance is less than 100mm, else it will evaluate as False
. This is a pretty compact way of including the functionality we desire without having to create a new variable or using an if
statement.
# Turn on the LED when range is less than 100mm from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Unified import sleep_ms ranger = PiicoDev_Ultrasonic() # Initialise the rangefinder while True: print(ranger.distance_mm) ranger.led = (ranger.distance_mm < 100) sleep_ms(100)
Let's do a quick code remix - we can modify the above example to create an audible alert whenever the range is less than 100mm.
To do this we can import the music
package and use the .pitch
method to play a tone.
# Make an audible proximity alert import music from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Unified import sleep_ms ranger = PiicoDev_Ultrasonic() # Initialise the rangefinder while True: print(ranger.distance_mm) if ranger.distance_mm < 100: music.pitch(880, 100) # play a 880Hz tone for 100ms sleep_ms(100)
Multiple Rangefinders on the same PiicoDev Bus
It's possible to connect multiple PiicoDev Ultrasonic Rangefinders on the same PiicoDev bus, though beware! These sensors are quite capable of interfering with each other if pointed together, or at the same object - so consider the path reflected sound may take.
Begin by setting a unique ID for each module. The ID switches are located in the centre of the module. Use a fine pen or similar instrument to set the ID switches as shown in the image below.
In the initialisation code, we will use the id
argument to differentiate the Rangefinders. id
is a list that encodes the ID switch positions; 1=ON and 0=OFF.
- The first rangefinder (range_a) will have all its ID switches off, and will be initialised with the argument
id=[0,0,0,0]
- The second rangefinder (range_b) will have ID 1,3 ON and the rest off, and will be initialised with the argument
id=[1,0,1,0]
The following example shows how to sample two ranges independently.
# Read distance from two PiicoDev Ultrasonic Rangefinders independently from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Unified import sleep_ms range_a = PiicoDev_Ultrasonic(id=[0,0,0,0]) range_b = PiicoDev_Ultrasonic(id=[1,0,1,0]) # id argument must match ID switch positions while True: print(range_a.distance_mm, range_b.distance_mm) sleep_ms(100)
Note: ID switches are how PiicoDev abstracts away I2C Addressing - you don't have to remember the address if you can just read the switch positions.
If you prefer to work with actual addresses instead, the following table shows the relationship between ID Switches and I2C Addresses.
Sample Period
The sample period (milliseconds) can be updated by setting the .period_ms
attribute which accepts integers between 0 (disabled) and 65335. The default value is 20.
Most users won't need to change the default sample period, but the option is available and useful if you also wish to enable/disable the sensor.
ranger.period_ms = 1000 # sample once per second
ranger.period_ms = 0 # disable the sensor entirely
print(ranger.period_ms) # query and display the sample period
For longer sample periods, you may want to poll the Rangefinder status and only read new ranges when they become available (instead of reading out the same data multiple times). For this, you can check the .new_sample_available attribute
. The following example sets the sample period to 1000ms, and checks for a new sample every 100ms. Each sample is printed exactly once.
# Only read the Rangefinder when a new sample is available from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Unified import sleep_ms ranger = PiicoDev_Ultrasonic() # Initialise the rangefinder ranger.period_ms = 1000 # set a slow sample period while True: if ranger.new_sample_available: print(ranger.distance_mm) sleep_ms(100)
Conclusion
It's pretty amazing to be able to measure distance so easily! Check out the project ideas featured below if you'd like some inspiration of what useful and creative projects you can complete with an Ultrasonic Rangefinder. If you have any questions or need some help, start the conversation below. We're full-time makers and happy to help.
Happy making!
Resources
This guide has taken every care to document the PiicoDev Ultrasonic Rangefinder to be useful for most makers. If you really want to look under the hood and explore the hardware and software, we've provided these additional resources.
- Hardware Repo for the PiicoDev Ultrasonic Rangefinder PCB
- Schematic
- MicroPython/Python Library and API documentation
Project Ideas
This invisible tripwire project is a flexible starter project with loads of applications. You could set it up to range across a doorway - when somebody passes through the door a message is displayed. This is a useful starter project for other range-alert style projects - for example, you could create an invisible limbo-bar that tells you when somebody didn't limbo low enough!
The project is self-calibrating, meaning you don't have to manually set any range thresholds. The tripwire will adapt to a steady-state range, and if it detects any sudden, large errors in the range measurement a trip event is reported. This is handled by continuously updating an average range (using exponential smoothing). A trip event occurs when any one range sample deviates too far from the average.
# A self-calibrating tripwire that looks for gross changes in distance # Slowly calibrates for new distances - no manual tuning required. from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Unified import sleep_ms ranger = PiicoDev_Ultrasonic() # Initialise the rangefinder tuning = 0.1 # a tuning parameter that affects how quickly new distances are calibrated sensitivity = 50 # [mm] any change greater than this amount will count as a trip average = ranger.distance_mm # Initialise the moving average with a range sample sleep_ms(1000) while True: distance = ranger.distance_mm print(distance, average) # check if the wire has been tripped if abs(average - distance) > sensitivity: print("Trip!") ranger.led = True # Indicate a trip else: ranger.led = False # update the moving average https://en.wikipedia.org/wiki/Exponential_smoothing average = tuning * distance + (1 - tuning) * average sleep_ms(100)
A theremin is a musical instrument controlled without physical contact. Real theremins use changing electric fields to control the sound output - here we can use the hands proximity to the Ultrasonic Rangefinder. Hold your hand closer to produce lower notes, and farther away to produce higher notes.
This project requires a PiicoDev Buzzer, and the PiicoDev_Buzzer.py module.
# A non-contact musical instrument. Hold your hand near the rangefinder and move # closer or farther to play different notes. from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Buzzer import PiicoDev_Buzzer from PiicoDev_Unified import sleep_ms ranger = PiicoDev_Ultrasonic() # Initialise the rangefinder buzzer = PiicoDev_Buzzer() def get_note_from_distance(distance): """Converts a distance [mm] to a note [Hz] from a supplied scale""" scale_Hz = [523, 587, 659, 698, 784, 880, 988, 1047] # Cmajor scale note_number = round( (distance - 100) / 50 ) # spaces notes 50mm apart, starting 100mm away from the rangefinder # make sure the note stays within bounds if 0 <= note_number < len(scale_Hz): return scale_Hz[note_number] else: return 0 # silence if not being actively played note_duration = 250 # [ms] while True: distance = ranger.distance_mm note = get_note_from_distance(distance) buzzer.tone(note, note_duration) print(distance, note) sleep_ms(note_duration)
This project uses the plotter in Thonny to draw a simple obstacle-avoiding, 2D-scrolling game - similar to Flappy Bird or the timeless classic Helicopter Game. The player flies through a winding cave and must avoid crashing into the walls. The player position is controlled by the Ultrasonic Rangefinder. The cave is procedurally generated - the frame counter drives a generate_cave()
function which uses simple trig functions (sin, cos) to create an infinite cave.
This project requires a PiicoDev Buzzer, and the PiicoDev_Buzzer.py module.
# Cave explorer game. Use the ultrasonic rangefinder to control the position of the player as you # fly through a narrow, winding cave. Don't crash into the walls! import math from PiicoDev_Ultrasonic import PiicoDev_Ultrasonic from PiicoDev_Buzzer import PiicoDev_Buzzer from PiicoDev_Unified import sleep_ms ranger = PiicoDev_Ultrasonic() # Initialise the rangefinder buzzer = PiicoDev_Buzzer() # Initialise the buzzer def generate_cave(): """generate a new cave floor and ceiling based of the frame number. Changing these constants will affect game difficulty""" floor = range + \ range/2 * math.sin(2*math.pi * frame/200) + \ range/4 * math.cos(2*math.pi * frame/88) + \ range/4 * math.sin(2*math.pi * frame/33) ceiling = floor + cave_size return floor, ceiling def player_in_bounds(): return player > cave_floor and player < cave_ceiling def play_start_sound(): buzzer.tone(1200,300) def play_crash_sound(): buzzer.tone(1200,300) sleep_ms(300) buzzer.tone(600,300) ################# # Game Variables ################# cave_size = 150 # height of the cave cave_floor = 150 # starting floor height range = 150 # how much room the player needs to move around frame = 0 # frame counter is used to generate the cave, and keep score bound_counter = 0 # how many frames the player was out of bounds. glitches happen, so we look for consecutive out-of-bounds to trigger a crash state = 'prestart' print("Game starts when player is in bounds") sleep_ms(1000) while True: # Update the game cave_floor, cave_ceiling = generate_cave() player = ranger.distance_mm # Check for out-of-bounds if player_in_bounds(): bound_counter = 0 if state == 'prestart': state = 'play' play_start_sound() else: bound_counter += 1 # Plot the game state print(f"{player} {cave_ceiling} {cave_floor} 0 {2*range + cave_size}") # Crash when out of bounds for consecutive frames if bound_counter > 2 and state == 'play': print(f"You crashed! Score: {frame}") play_crash_sound() break; frame += 1 sleep_ms(100)