7.3 Parking Sensor
This project builds the reverse parking sensor you already know from cars. The ultrasonic sensor watches the space in front of it, and the closer something gets, the faster the buzzer beeps. Come close enough and the beeping collapses into one unbroken tone and a warning light comes on.
It is the first project that puts a Qwiic module and a breadboard circuit on the board at the same time. The sensor arrives on a single cable, exactly as in section 3.1, while the buzzer from section 2.4 and the LED from section 3.2 are wired by hand. Two outputs, one sensor, and a small piece of timing logic joining them.
In this documentation you will learn:
- How to drive two different outputs from a single sensor reading.
- How to make something happen at a chosen speed without pausing the program, so measuring never stops.
- Why the unit the sensor answers in has to be checked before comparing it to anything.
- What a sensor blind zone is, and why getting too close makes this project fall silent.
- How a chain of
if/elifturns one number into five different behaviours.
Hardware required:
- 1x Soldered NULA MINI board
- 1x Breadboard
- 1x Ultrasonic distance sensor (Qwiic)
- 1x Qwiic cable
- 1x Passive buzzer
- 1x LED (any colour)
- 1x 330 Ω resistor (orange-orange-brown)
- 5x Jumper wires
- 1x USB-C cable
Putting the components together
Both pins this project uses are on the f–j side of the board: IO2 in row 25 for the buzzer, and IO5 in row 28 for the LED. The board body covers columns f–i, so j25, j28 and j30 are the holes you can actually get a wire into. Everything else happens further along the same f–j side, well clear of the board.
IO5 on the f–j side and IO18 on the a–e side, and row 25 is IO2 on the f–j side but TX on the a–e side. Count the rows on the same side you are plugging into.1. Insert the NULA MINI board on the breadboard
Push the board into one end of the breadboard so that its two rows of pins sit on either side of the centre channel, with the chip facing down.

2. Place the buzzer and wire it to IO2
Three things happen in this step, because they belong together.
First run a jumper from j30 (GND) across to the blue − rail. That rail becomes the shared ground line for everything else in the project.
Then push the buzzer into the f–j side with its two legs in row 15 and row 14. Look at the top of its case before you do: next to the HWDZ moulding there is a small + inside a circle, and the leg on that side is the positive one. That leg goes into row 15.
Finally, one jumper from j15 to j25 (IO2) to carry the sound, and one from j14 to the blue − rail to complete the loop.

3. Run IO5 out to the LED and add the resistor
One jumper from j28 (IO5) along to j8. This is the wire that will switch the warning light.
Then the 330 Ω resistor from i8 to i6, bridging the two rows. Its bands read orange-orange-brown.

4. Add the LED
The LED goes in with its long leg in row 6, the row the resistor feeds, and its short leg in row 5.

5. Connect the LED to ground
One last jumper, from j5 (the row holding the short leg of the LED) across to the blue − rail.

The chain is now closed end to end: IO5 → resistor → LED → GND.
6. Plug in the ultrasonic sensor
The sensor needs no breadboard holes at all. It is a Qwiic module, so one cable carries its power and its data together, and it only fits one way round.
Plug one end into the Qwiic connector of the board, the white one on the edge between the USER and RST buttons, and the other into either connector on the purple ULTRASONIC SENSOR QWIIC board. It has two, one at each end, wired together, so it makes no difference which you pick.

If you would like to see exactly which connector this is on a bare board, it is highlighted here:

ON marking. It sets the address the sensor answers to, and the code expects the factory setting.7. Connect the board to your computer
Turn the sensor so its two silver transducers face out into the room, keep the space in front of them clear, and plug in the USB-C cable.

How one number becomes five behaviours
Everything this project does comes from a single measurement, passed down a chain of if and elif:
| Distance | Buzzer | LED |
|---|---|---|
| more than 100 cm | silent | off |
| 60–100 cm | slow beeping, 800 ms apart | off |
| 30–60 cm | medium beeping, 400 ms apart | off |
| 10–30 cm | fast beeping, 150 ms apart | off |
| less than 10 cm | one continuous tone | on |
The board checks these ranges from the widest down and stops at the first one that matches, which is why each test only needs a lower bound. By the time it reaches elif distance > 30, everything above 60 has already been dealt with.
Beeping without stopping the program
The obvious way to make a beep is: sound on, sleep, sound off, sleep. This script does not do that, and the reason is worth understanding, because it is one of the most useful ideas in the whole kit.
time.sleep() does not mean "wait in the background". It means stop. During time.sleep(0.8) the board does nothing else at all. It cannot measure, it cannot notice you moving closer, it cannot react. A parking sensor that stops looking for eight tenths of a second at a time is not much of a parking sensor.
So instead the script keeps a note of the time:
last_beep = 0
beep_interval = 0
buzzer_on = False
time.ticks_ms() returns how many milliseconds the board has been running. Every pass through the loop, the script compares the moment of the last change with the moment now, and only acts when enough time has gone by:
if beep_interval > 0:
now = time.ticks_ms()
if time.ticks_diff(now, last_beep) >= beep_interval:
last_beep = now
buzzer_on = not buzzer_on
if buzzer_on:
buzzer.duty_u16(SOUND_ON)
else:
buzzer.duty_u16(SOUND_OFF)
The not operator flips a True to False and back again, so each time the interval elapses the buzzer switches to the opposite of whatever it was. On, off, on, off: doing that over and over is what you hear as beeping. A smaller beep_interval means less waiting between switches, which is faster beeping.
beep_interval = 0 is used as a flag meaning "do not beep at all". Both the silent band and the continuous-tone band set it to zero, because in neither case does the buzzer need switching: one leaves the sound off, the other leaves it on.Reading the sensor, and reading it correctly
Three details in the code deserve a closer look.
It takes two calls, not one
sensor.takeMeasure()
time.sleep_ms(MEASURE_WAIT_MS)
reading = sensor.getDistance()
takeMeasure() only asks the sensor to go and do the work. The sensor then needs a moment (it gives up listening after about 38 milliseconds if no echo arrives), so the script waits 50 ms before getDistance() collects the finished answer. This is explained in full in 3.1 Measuring distance.
That 50 ms wait is also the only pause in the whole loop, which is why there is no sleep at the bottom of it. The measurement sets the pace: about twenty readings every second.
The answer arrives in millimetres
distance = reading // 10
The sensor reports the distance in millimetres, so a reading of 250 means 25 centimetres. Every threshold further down the script is written in centimetres, because that is how a person would describe a distance, so the reading is converted once here and the converted value is what the if chain compares against. // is integer division: it discards the remainder and keeps the value a whole number, so 253 millimetres becomes 25 centimetres.
distance > 100 would mean 10 cm rather than 1 m, and the whole parking sensor would end up working only inside a 10 cm bubble. Whenever you use a new sensor, check what unit it answers in before comparing it to anything.Zero is not a distance
if reading == 0:
print("No echo received, nothing in range.")
buzzer.duty_u16(SOUND_OFF)
buzzer_on = False
led.value(0)
continue
The sensor answers 0 whenever it heard no echo it could use, and that happens in two opposite situations: when nothing is in range at all, and when something is closer than about 3 centimetres.
The second one surprises people. Every ultrasonic sensor has a blind zone right in front of it. The sensor sends out a burst of sound and then listens for it coming back, but from very close range the echo returns before the burst has finished, so it arrives while the sensor is still talking and is never heard.
Neither case is a real measurement. If the script treated 0 as a distance it would read as "zero centimetres away" and set off the alarm at full blast whenever the way was clear, so instead it switches everything off and waits for the next reading.
buzzer_on before using continue. Skipping the rest of the loop means none of the code below runs, so anything left switched on would stay on. The branch has to tidy up after itself.Code
# The Soldered driver for the Ultrasonic Distance Sensor, found in the lib folder of the examples repository.
from UltrasonicSensor import UltrasonicSensor
# I2C is what the Qwiic connector carries, PWM drives the buzzer, and a plain Pin is enough for the warning LED.
from machine import I2C, Pin, PWM
import time
# This is a variable to which we pass the number of pin that we had connected the buzzer to.
# The NULA board has a pin naming logic as follows: IO2, where 2 is the number that we give to the variable.
BUZZER_PIN = 2
# This is a variable to which we pass the number of pin that we had connected the warning LED to.
# Remember that the LED needs a 330 Ohm resistor in series with it.
LED_PIN = 5
# This is the frequency of the warning sound, in Hertz. Small buzzers like this one are loudest somewhere between
# 2 and 4 kHz, so feel free to experiment with this value until it sounds best to you.
TONE_FREQ = 2700
# This is how long we wait after asking for a measurement, in milliseconds. The sensor gives up listening for an echo
# after 38 milliseconds, so waiting a little longer than that means the answer is always ready when we ask for it.
# This wait also sets the pace of the whole loop, which is why there is no pause at the bottom of it.
MEASURE_WAIT_MS = 50
# Here we set up the I2C connection. On the NULA MINI, I2C uses IO6 for the data line (SDA) and IO7 for the clock
# line (SCL), which are exactly the pins the Qwiic connector is wired to.
i2c = I2C(0, scl=Pin(7), sda=Pin(6))
# Here we create our sensor object and hand it the I2C connection. We pass no pin numbers for the sensor itself:
# every Qwiic module shares those same two I2C pins, and this sensor answers on address 0x30.
sensor = UltrasonicSensor(i2c)
# Here we create our PWM object for the buzzer and set its pitch once, since it never changes in this project.
# duty_u16() sets what fraction of the time the pin stays on, and half of the full range gives the clearest tone.
buzzer = PWM(Pin(BUZZER_PIN))
buzzer.freq(TONE_FREQ)
SOUND_ON = 32768
SOUND_OFF = 0
buzzer.duty_u16(SOUND_OFF)
# Here we create our Pin object for the LED, and write 0 to it so the warning light starts out switched off.
led = Pin(LED_PIN, Pin.OUT)
led.value(0)
# These three variables are what lets us beep at different speeds without ever stopping the program.
# "last_beep" remembers the moment the buzzer was last switched on or off, "beep_interval" holds how long we want to
# wait between those switches, and "buzzer_on" remembers whether the buzzer is currently sounding or silent.
last_beep = 0
beep_interval = 0
buzzer_on = False
# begin() prepares the sensor for use, before we start reading from it.
sensor.begin()
# Print out the initial message so we know that the program started successfully.
print("Ultrasonic buzzer + LED reverse sensor started")
while True:
# Reading a Qwiic sensor takes two steps. takeMeasure() asks the sensor to send out a pulse and time the echo,
# and the wait afterwards gives it the moment it needs to finish that work and store the answer.
sensor.takeMeasure()
time.sleep_ms(MEASURE_WAIT_MS)
# getDistance() then fetches the stored answer. Note the unit: the sensor reports the distance in MILLIMETERS,
# so a reading of 250 means the obstacle is 25 centimeters away. We keep it in a variable named "reading" so that
# the name reminds us it is not yet the unit we want to think in.
reading = sensor.getDistance()
# The sensor answers with 0 whenever it heard no echo it could use. That happens in two opposite situations: when
# nothing is in range at all, and when an object is closer than about 3 centimeters, because then the echo comes
# back before the sensor has finished sending and it never hears it. Neither is a real measurement, and treating
# one as a distance would set off the alarm whenever the way is clear, so instead we fall silent and wait for the
# next one. continue jumps straight back to the top of the loop.
if reading == 0:
print("No echo received, nothing in range.")
buzzer.duty_u16(SOUND_OFF)
buzzer_on = False
led.value(0)
continue
# Now we turn the reading into centimeters, which is how we would naturally describe a distance to a person, and
# how every number further down this script is written. Ten millimeters make one centimeter, so we divide by ten.
# // is whole-number division, so 253 millimeters becomes 25 centimeters, and that is all the accuracy this
# sensor can honestly offer anyway.
distance = reading // 10
# Print the measured distance to the console so we can follow along while testing.
print("Distance from obstacle: {} cm".format(distance))
# We switch the LED off at the start of every pass through the loop. Only the closest range switches it back on
# again a few lines below, so this one line saves us from having to turn it off in every other case.
led.value(0)
# This chain of if statements is the heart of the project: it turns a distance into a beeping speed. Each range
# gets a different value of beep_interval, and a smaller interval means less waiting between beeps, which we hear
# as faster beeping. The board checks the ranges from the widest down and stops at the first one that matches.
if distance > 100:
# Nothing in range. An interval of 0 keeps the buzzer silent, and here we make sure it really is silent.
beep_interval = 0
buzzer.duty_u16(SOUND_OFF)
buzzer_on = False
elif distance > 60:
# Between 60 and 100 cm, beep slowly.
beep_interval = 800
elif distance > 30:
# Between 30 and 60 cm, beep at a medium speed.
beep_interval = 400
elif distance > 10:
# Between 10 and 30 cm, beep quickly.
beep_interval = 150
else:
# Closer than 10 cm. Here we leave the sound switched on instead of beeping, which makes one continuous tone,
# and we light up the LED as a final warning.
buzzer.duty_u16(SOUND_ON)
buzzer_on = True
beep_interval = 0
led.value(1)
# This is where the beeping itself happens. By comparing time.ticks_ms() against the moment of the last switch we
# can wait the right amount of time without pausing the program, which would stop us from measuring.
# Every time the interval has passed we flip buzzer_on to its opposite value with the "not" operator, and then
# either start the sound or stop it. Doing that over and over is what produces a beep.
if beep_interval > 0:
now = time.ticks_ms()
if time.ticks_diff(now, last_beep) >= beep_interval:
last_beep = now
buzzer_on = not buzzer_on
if buzzer_on:
buzzer.duty_u16(SOUND_ON)
else:
buzzer.duty_u16(SOUND_OFF)
What you should see
Press Run. The script announces itself:
Ultrasonic buzzer + LED reverse sensor started
From then on you get a line about twenty times a second. Move your hand slowly towards the sensor and watch the numbers come down:
Distance from obstacle: 26 cm
Distance from obstacle: 21 cm
Distance from obstacle: 12 cm
Distance from obstacle: 25 cm
Distance from obstacle: 10 cm
Distance from obstacle: 7 cm
Distance from obstacle: 4 cm
12 cm to 25 cm and straight back to 10 cm. That stray reading is normal. An ultrasonic sensor sometimes catches an echo off your sleeve, the desk, or the edge of your hand instead of the flat of it. It does not matter here, because another measurement arrives 50 milliseconds later. This is worth remembering whenever you use a distance sensor: judge it on a run of readings, never on one.That single run walks through the end of the project. The readings start around 26 cm, which is fast beeping. They cross 10 cm about a third of the way down, and from there the beeping stops and holds one continuous tone while the warning light comes on:

Bring your hand in more slowly than this and you hear all three beep speeds on the way down: slow from 1 m, medium from 60 cm, fast from 30 cm.
When nothing is in front of the sensor, or when something is pressed right up against it, you get this instead:
No echo received, nothing in range.
If nothing beeps at all, check the two legs of the buzzer are in two different rows, and that the jumper from IO2 lands in the same row as the + leg.
If the tone never goes solid, you are either not close enough, or you are too close. Remember the blind zone: an object touching the transducers makes the sensor stop answering, and everything falls silent. About 5 cm is the sweet spot.
If the tone goes solid but the LED stays dark, the code is doing its job and the fault is in the circuit. Check the LED is the right way round first: the long leg belongs in row 6, facing the resistor.
If the script stops with IndexError: index out of range pointing inside UltrasonicSensor.py, the board is not reaching the sensor at all. The driver catches the I2C error itself and hands back an empty answer, so what you see is the line that tries to read that empty answer rather than a complaint about the bus. Check the scl=Pin(7), sda=Pin(6) in the I2C() line, then reseat both ends of the Qwiic cable.
Full example
Check out the full example code on the link below:
7.3_Parking_sensor.py
Project that uses an ultrasonic sensor, buzzer, and LED to simulate a reverse-parking warning system.