Skip to main content

3.1 Measuring distance

The goal of this example is to measure distance with the kit ultrasonic sensor. The sensor sends out a short burst of sound, listens for the echo bouncing back off whatever is in front of it, and works out how far away that object is. The result is printed to the console.

This is also the first example that uses a driver and a Qwiic module, so the wiring is the easiest of the whole kit: one cable, and you are done.

In this documentation you will learn:

  • How an ultrasonic sensor measures distance by timing an echo.
  • How to connect a Qwiic module with a single cable and no jumper wires.
  • How to set up an I2C connection, and why MicroPython makes you name the pins.
  • What a driver is, and how to get one onto the board.
  • Why this sensor needs two function calls to give you one reading.
  • How to spot a reading that is not a real measurement.

Hardware required:

  • 1x Soldered NULA MINI board
  • 1x Breadboard
  • 1x Ultrasonic distance sensor (HC-SR04)
  • 1x Qwiic cable
  • 1x USB-C cable
ℹ️
No resistors and no jumper wires this time. Every example so far has needed you to build a circuit hole by hole. This one does not: the sensor is a Qwiic module, so a single cable carries power and data at once, and it only fits one way round.

Putting the components together

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.

NULA MINI board seated on the breadboard
Step 1: the board seated on the breadboard
ℹ️
The breadboard is not doing any electrical work in this example, because nothing connects to the pins of the board. It is here only to hold the board flat and steady while you plug the cable in. Example 3.2 adds an LED to this same build, and then the breadboard starts earning its place.

2. Plug the Qwiic cable into the board

The NULA MINI has one Qwiic connector, on the edge of the board between the USER and RST buttons. It is the white connector marked qwiic on the silkscreen. Push the cable in until it clicks.

Qwiic cable plugged into the NULA MINI board, with the other end still loose
Step 2: the cable in the Qwiic connector of the board, other end still free
⚠️
Do not confuse it with the smaller two-contact connector on the opposite edge of the board, marked - and +. That one is the battery connector, and a Qwiic cable will not fit it.
ℹ️
The USB-C cable is already plugged in in these photos, and the purple PWR light is on. It makes no difference when you connect it. Qwiic modules are safe to plug in either way round and either way about.

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

The NULA MINI board with its Qwiic connector highlighted
The Qwiic connector on the NULA MINI, highlighted in blue

3. Plug the other end into the sensor

The sensor is two boards bolted back to back: the blue ultrasonic module carrying the two silver transducers, and a purple Soldered board carrying the electronics that do the talking. The purple board is silkscreened ULTRASONIC SENSOR QWIIC, and it has two Qwiic connectors, one at each end.

Plug the free end of the cable into either one. They are wired together, so it makes no difference which you pick. The second one is there so you can chain a further module onwards.

The completed connection: Qwiic cable running from the NULA MINI to the ultrasonic sensor
Step 3: the cable joining the board to the sensor. That is the entire circuit
ℹ️
Leave the small orange ADDR switch block exactly as it came, with all three sliders pushed away from the ON marking. It sets the address the sensor answers to, and the code expects the factory setting. You would only ever change it to put two of these sensors on the same cable.

4. Point the sensor at open space

Turn the sensor over so the two silver transducers face out into the room, and keep the space in front of them clear. They are the parts that emit and hear the sound, so anything resting against them (the desk, the cable, your hand) is the only thing the sensor will ever find.

The sensor held up with its two transducers facing out into the room
Step 4: the transducers facing out, with clear space in front of them

How the sensor measures distance

Sound travels at a known, steady speed through air: about 343 metres per second, which works out to 0.0343 centimetres every microsecond. That fixed speed is the whole trick. If you know how long a sound took to get somewhere, you know how far it went.

So the sensor emits a very short chirp at about 40 kHz, far too high for a human to hear, though a bat would have no trouble, and starts a stopwatch. The chirp travels out, hits something, and part of it bounces straight back. The moment the sensor hears it return, it stops the stopwatch.

Diagram of an ultrasonic sensor emitting a pulse that reflects off an object and returns
The pulse leaves the transmitter, bounces off the object, and comes back to the receiver

Turning that time into a distance takes one small correction:

Distance = (Time × 0.0343) ÷ 2

ℹ️
The division by two is the part people forget. The sound had to travel to the object and then all the way back, so the stopwatch measured a round trip. The object is only half that far away.

On this module you never have to do that sum yourself. The purple adapter board has its own small microcontroller which runs the stopwatch, does the arithmetic, and keeps the answer ready for you to collect.


Setting up the I2C connection

A Qwiic cable is not four separate wires you route wherever you like. It is a fixed bundle of four: 3.3 V, ground, and the two signal lines of a bus called I2C. On the NULA MINI those two signal lines are wired to IO6 and IO7. They are not even brought out to the pin headers, so there is nothing to choose.

MicroPython still makes you say so out loud:

i2c = I2C(0, scl=Pin(7), sda=Pin(6))

Read that as: use I2C peripheral 0, with the clock line on IO7 and the data line on IO6. The 0 is which of the hardware I2C blocks inside the chip to use, scl is the clock and sda is the data.

⚠️
These two pin numbers are not optional and not interchangeable. MicroPython does not assume which pins your Qwiic connector uses, so leaving them out or swapping them gives you an I2C bus pointing at the wrong pins. The sensor then never answers and the script fails with OSError: [Errno 19] ENODEV, which looks like a broken sensor but is only a mistyped pin number. scl=Pin(7), sda=Pin(6), every time.
ℹ️
This is a real difference from Arduino. There, the library calls Wire.begin() with no arguments and the pins come from whichever board you picked in the IDE, so a sketch that names no pins still works, as long as the right board is selected. MicroPython has no board selection, so the pins live in your code instead. Being explicit is more typing but one less thing to get silently wrong.

The sensor object is then created with the I2C connection, and no pin numbers of its own:

sensor = UltrasonicSensor(i2c)

What the driver does need to know is which module on the cable it is talking to, because I2C lets several modules share one pair of wires. Each one answers to its own number, called an address, and the address of this sensor is 0x30. The driver already knows that, which is why you do not pass it.

ℹ️
This is the pattern for every Qwiic module in the kit. The LCD in section 4 and the temperature sensor in section 5 are created the same way, from the same i2c object, because they sit on the same two wires at different addresses.

Getting the driver onto the board

A driver is code somebody else wrote that you borrow. Without one you would have to time the echo pulse and do the arithmetic yourself; with one you call getDistance() and get a number.

This example needs UltrasonicSensor.py, which lives in the lib folder of the examples repository together with Qwiic.py, the shared helper it is built on. Unlike Arduino, where the Library Manager installs into the IDE on your computer, a MicroPython driver has to be copied onto the board itself. The board is what runs the code, and it can only import what it can find in its own /lib folder.

If you followed Setting up MicroPython you already have all four drivers in place. If not, the quickest way is with mip:

mpremote mip install github:SolderedElectronics/Soldered-NULA-Beginner-kit-MicroPython-project-examples/lib
⚠️
If the script stops with ImportError: no module named 'UltrasonicSensor', the driver is not on the board, or it landed somewhere other than /lib. Nothing is wrong with the wiring or the sensor. Copy the lib folder across and run it again.

Reading the sensor takes two steps

The script does not simply ask for a distance. It does this instead:

sensor.takeMeasure()
time.sleep_ms(MEASURE_WAIT_MS)
distance = sensor.getDistance()

Three lines where you might have expected one, and the reason is that measuring takes real time. takeMeasure() tells the sensor to go and do the work: send the chirp, listen, run the stopwatch, store the answer. That request comes back immediately. The board has only sent an instruction, not waited for a result.

The sensor then needs a moment. It gives up listening after about 38 milliseconds if no echo ever arrives, so a measurement can take that long in the worst case. The script waits 50 ms, comfortably longer, before getDistance() collects the finished answer.

ℹ️
Ask too early and you get the previous reading, or nothing sensible at all. This split into "start the work" and "fetch the result" is very common with sensors, so it is worth recognising.

begin() is called once before the loop starts. For a Qwiic sensor that opens the conversation on the address of the module, and a sensor should always be initialized before you read from it.

Not every number is a real measurement

This sensor works from about 2 centimetres out to about 4 metres. Past that the sound comes back too faint to recognise, and the sensor has no way of telling you so. It has no "not found" answer. It simply hands over a number anyway.

In practice that number comes out very large, somewhere close to 1000 cm, and it drifts about by a few centimetres from reading to reading. Ten metres is well outside anything this sensor can genuinely hear, so treat a reading of a few hundred centimetres or more as "nothing found" rather than as a distant object.

The same thing happens even with an object well within range, if that object happens to be soft, or angled steeply away from the sensor. Cushions, curtains and jumpers absorb the sound; a hard flat surface facing the sensor square-on reflects it best.

The script also checks for a reading of exactly 0 and prints No echo received, nothing in range. instead of a distance. Zero is what the sensor answers when it heard no usable echo at all, which also happens when something is closer than roughly 3 cm: the echo arrives before the outgoing chirp has finished, so the sensor never recognises it.


Code

# I2C is a way for several devices to talk to the board over just two wires, and it is what the Qwiic connector
# carries. We need it here because the sensor is a Qwiic module.
from machine import I2C, Pin

# Importing a module gives us access to ready-made functions that do the hard work for us. Here we import the
# Soldered driver for the Ultrasonic Distance Sensor, so we don't have to time the echo ourselves.
# This driver lives in the lib folder of the examples repository. Copy the whole lib folder onto your board,
# otherwise MicroPython will not be able to find it.
from UltrasonicSensor import UltrasonicSensor
import time

# Here we set up the I2C connection. The two pins are fixed by the board: 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.
# We always name them ourselves, because the board does not assume which pins the connector uses.
i2c = I2C(0, scl=Pin(7), sda=Pin(6))

# Here we create our sensor object, which we named "sensor", and hand it the I2C connection. An object is our way
# of talking to the sensor: every function we call on it, we call through this name.
# Notice that we pass no pin numbers for the sensor itself. Every Qwiic module shares the same two I2C pins, which
# is why you can chain several of them together, and this sensor answers on address 0x30.
sensor = UltrasonicSensor(i2c)

# This is how long we wait after asking for a measurement, in milliseconds. The sensor gives up after 38
# milliseconds if no echo comes back, so we wait a little longer than that.
MEASURE_WAIT_MS = 50

# begin() prepares the sensor for use. A sensor should always be initialized before we start reading from it.
sensor.begin()

# Print out the initial message so we know that the program started successfully.
print("Ultrasonic Measuring Distance Example started!")
print("Move an object in front of the sensor to see the distance change.")

# A while True loop repeats forever, so the code inside it keeps running until we stop the program.
while True:

# Reading a Qwiic sensor takes two steps. takeMeasure() is the first one: it asks the sensor to send out a
# pulse and time the echo. The sensor does that work on its own and remembers the answer, which is why we
# then have to wait before collecting it.
sensor.takeMeasure()
time.sleep_ms(MEASURE_WAIT_MS)

# getDistance() is the second step: it fetches the answer the sensor worked out and stored, already
# converted into centimeters.
distance = sensor.getDistance()

# The sensor answers with 0 when it heard no echo at all, which happens when nothing is in range or when the
# surface in front of it scatters the sound away. That is not a real measurement, so we say so rather than
# reporting a distance of zero, which would suggest an object touching the sensor.
if distance == 0:
print("No echo received, nothing in range.")
else:
print("Distance: {} cm".format(distance))

# time.sleep() starts a pause in the code, given in seconds. Without this pause the readings would scroll by
# far too quickly to read, and it also lets the echoes of the last pulse die out before the next one.
time.sleep(0.5)
ℹ️
"Distance: {} cm".format(distance) builds one string out of text and a number. The {} is a placeholder, and format() drops the value into it. It is the MicroPython equivalent of the several Serial.print() calls an Arduino sketch needs to build the same line.

What you should see

Press Run. After the two startup lines you get a fresh reading twice a second.

Point the sensor across the room and wave your hand about in front of it, and the numbers follow whatever is nearest:

Ultrasonic Measuring Distance Example started!
Move an object in front of the sensor to see the distance change.
Distance: 52 cm
Distance: 52 cm
Distance: 160 cm
Distance: 96 cm
Distance: 39 cm

The first two readings sit at 52 cm, which is where the nearest object was standing still. The values that jump (160 cm, 96 cm, 39 cm) are the moments when something moved through the field of view of the sensor, or when the sound found something further away instead.

ℹ️
The readings are whole centimetres, and they wobble by a few even with everything held perfectly still. That is normal. It is the honest precision of an inexpensive ultrasonic sensor, not a fault.

Now aim the sensor at empty space, or straight up at the ceiling, and the picture changes completely. Nearly every line now reads somewhere between 996 and 1000 cm:

Distance: 997 cm
Distance: 999 cm
Distance: 996 cm
Distance: 1000 cm

That is the way this sensor reports finding nothing at all, as described above, not an object ten metres away. Seeing this cluster is a useful thing to recognise, because it tells you the board and the sensor are talking to each other perfectly well and the problem is only where the sensor is pointed.

⚠️
If the script stops immediately with OSError: [Errno 19] ENODEV, the board cannot find the sensor on the bus. Check the two Pin numbers in the I2C() line first, then that the Qwiic cable is pushed fully into both of its connectors.

Try it yourself: change the time.sleep(0.5) at the end of the loop to time.sleep(0.1) and run it again. Readings now arrive about six times a second instead of twice, and following a moving hand becomes much smoother. Shorten it much further, though, and each new chirp goes out while the echoes of the last one are still bouncing around the room, which makes the numbers jump about.


Full example

Check out the full example code on the link below:

3.1_Measuring_Distance.py

Example that reads the Qwiic ultrasonic distance sensor and prints the distance to the console.