Why I Started Using This Tool
My proximity alarm could blink, print and beep, but it had no idea whether anything was actually close. An alarm needs a sense, and the cheapest, friendliest distance sensor around is the HC-SR04: a little board with two silver "eyes" that works like a bat. It shouts a sound too high for us to hear and times how long the echo takes to come back.
Then I read the label: 5 V. The Pi's GPIO pins are 3.3 V only, with no tolerance for 5 V. Powering the sensor from 5 V is fine; the Pi has 5 V pins. The problem is the Echo signal coming back. When it's high, it's 5 V, and that would go straight into a GPIO pin. Plenty of tutorials skip this, and plenty of Pis have dead pins because of it. It might even seem to work for a while... until the pin dies.
The fix costs two resistors and one line of math: a voltage divider. Once you understand it, you'll spot it in circuits everywhere. Like the buzzer part, there's no C code today: just wiring, math, and a test from the console that measures a real distance before we write a single line.
Missed the buzzer wiring? Start with Part 8: Giving My Raspberry Pi a Voice: A Buzzer, a 2N3904 and One 1 kΩ Resistor (video).
Here's the video walkthrough of this post, if you'd rather watch it step by step:
What It Does
The HC-SR04 measures distance with sound:
- I pulse Trig high for at least 10 µs.
- The sensor sends 8 pulses of 40 kHz ultrasound.
- Echo goes high and stays high until the echo comes back.
- The width of that pulse is the round trip:
distance_cm = t_us / 58.
The wiring:
- VCC → 5 V (pin 2), GND → GND (pin 14).
- Trig → GPIO23 (pin 16), direct. 3.3 V from the Pi is a clean "high" for the sensor.
- Echo → 1 kΩ → GPIO24 (pin 18), with 2 kΩ from GPIO24 to GND.
Those two resistors are a voltage divider: 5 V × 2000 / (1000 + 2000) = 3.33 V.
Exactly what the Pi wants, and only about 1.7 mA of load on the sensor.
Before connecting the divider to the Pi, I checked it with a multimeter: top of R1 on 5 V, middle read 3.3 V.
Then I fired the sensor over SSH with pinctrl set 23 dh; pinctrl set 23 dl while
gpiomon -c gpiochip0 24 printed the rising and falling edges with timestamps in another window.
2917 µs between them, divided by 58: about 50 cm. The ruler agreed.
Step by Step
1. Recap: which pins?
Back in Part 3 we made a pin table for the whole project. The sensor rows were:
| Function | BCM GPIO | Physical pin | Notes |
|---|---|---|---|
| HC-SR04 TRIG | GPIO23 | 16 | Output: starts a measurement |
| HC-SR04 ECHO | GPIO24 | 18 | Input: must go through a voltage divider |
| 5 V power | — | 2, 4 | For the sensor and the LCD |
| Ground | — | 6, 9, 14, 20, … | Any GND pin works |
And the rule we promised to deal with "in the sensor part": the Pi's GPIO pins are 3.3 V only, not 5 V tolerant. Today's the day.
Handy detail: pins 14 (GND), 16 (GPIO23) and 18 (GPIO24) sit next to each other on the inner row of the header. Three neighbours, so the wiring stays tidy.
2. How the HC-SR04 measures distance
The sensor has two round "eyes". One is a tiny speaker (transmitter) and the other is a tiny microphone (receiver). They work at 40 kHz, far above human hearing (we top out around 20 kHz).
One measurement goes like this:
- We pull TRIG high for at least 10 µs, then low again. That's "please measure now".
- The sensor fires a burst of 8 pulses at 40 kHz out of the transmitter.
- The sensor pulls ECHO high and keeps it high...
- ...until the sound bounces off an object and comes back to the receiver. Then ECHO goes low.
So the width of the Echo pulse is the round-trip time of the sound. Our job is just to time it.
TRIG ___|‾‾|_____________________________________
10 µs
burst ______|||||||||_____________________________ (8 × 40 kHz, inside the sensor)
ECHO _______________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|_________
|<---- time t ------>|
sound goes out and comes back
From time to distance. Sound travels at about 343 m/s in room-temperature air, which is 0.0343 cm per microsecond. The echo travels there and back, so we divide by 2:
distance_cm = t_us × 0.0343 / 2
= t_us / 58 (near enough)
Some quick examples:
| Echo pulse | Distance |
|---|---|
| 580 µs | 10 cm |
| 2.9 ms | 50 cm |
| 5.8 ms | 100 cm |
| ~23 ms | ~400 cm (the sensor's practical maximum) |
A few more numbers worth knowing:
- Range: about 2 cm to 400 cm. Closer than 2 cm, the echo comes back before the sensor has finished shouting.
- No echo? If nothing reflects, Echo stays high until the sensor gives up, often around 38 ms (cheap clones vary).
- Wait between measurements: at least 60 ms, so the last echo has died away and doesn't confuse the next one.
- Soft or angled things (curtains, a cushion, a wall at a steep angle) can absorb or deflect the sound and give odd readings. Flat, hard objects facing the sensor are best.
Our shared frame from Part 6 already has a home for the result:
UINT16 distanceCm; /* last HC-SR04 reading */
BOOLEAN distanceValid; /* FALSE until a good reading */
Today we build the hardware that will fill those two fields.
3. The four pins
Flip the sensor over. The pins are labelled on the board:
| Sensor pin | Connects to | Why |
|---|---|---|
| VCC | Pi 5 V (pin 2) | The sensor is a 5 V part. On 3.3 V many modules don't work at all, or give weak, random readings. |
| Trig | GPIO23 (pin 16), direct | Pi → sensor. Safe, see below. |
| Echo | GPIO24 (pin 18), through the divider | Sensor → Pi. 5 V! Needs the divider. |
| GND | Pi GND (pin 14) | Common ground, so both sides agree what 0 V means. |
Some newer boards sold as "HC-SR04" (often labelled HC-SR04P or "3–5.5 V") also run on 3.3 V. If you power one of those from 3.3 V, its Echo is 3.3 V and needs no divider. Our setup uses the classic 5 V module, so we build the divider, and it does no harm on a 3.3 V board either.
4. Why TRIG can go straight to the Pi
Look at the direction of each signal. Trig flows from the Pi to the sensor. The Pi drives it at 3.3 V.
The sensor's input reads anything above roughly 2 V as "high" (standard 5 V TTL logic). 3.3 V is comfortably above that, so the sensor sees the trigger just fine. And a 3.3 V signal can't hurt a 5 V part. Trig: direct wire, no parts.
5. Why ECHO must NOT go straight to the Pi
Echo flows from the sensor to the Pi. When it's high, the sensor drives it to its own supply: 5 V.
Inside every Pi GPIO pin are tiny protection diodes that clamp the voltage to the 3.3 V supply. Push 5 V into the pin and those diodes start conducting, dumping current into the Pi's 3.3 V rail through a part that was only designed for small, brief accidents. Do that thousands of times while the alarm runs, and the pin, or the whole chip, can be damaged. Sometimes it fails right away. Sometimes it dies weeks later. Either way, there's no "undo".
So the rule from Part 3 becomes concrete: any signal coming into the Pi must be 3.3 V or less.
6. The voltage divider
A voltage divider is just two resistors in series between a voltage and ground. The point in the middle sits at a fraction of the input:
Vout = Vin × R2 / (R1 + R2)
- R1 is the "top" resistor, from the 5 V signal to the middle.
- R2 is the "bottom" resistor, from the middle to GND.
- Vout is the middle point, which goes to the Pi.
We want about 3.3 V out of 5 V. That's a ratio of 2/3, so R2 should be twice R1. Our choice: R1 = 1 kΩ, R2 = 2 kΩ.
Vout = 5 V × 2000 / (1000 + 2000)
= 5 V × 2/3
= 3.33 V ✔ right at the Pi's 3.3 V
No 2 kΩ in your kit? Put two 1 kΩ resistors in series for R2. That's 2 kΩ. Same result.
What does the Pi need to see? The Pi reads a pin as "high" once it's well above about half its supply (the exact threshold depends on the chip; check the BCM2837 documentation if you're curious). 3.33 V is a clean, solid high. 0 V is a clean low. Perfect.
Current through the divider when Echo is high: 5 V / 3000 Ω ≈ 1.7 mA. A tiny load
for the sensor's output.
Bonus protection: if a bug in our program ever turns GPIO24 into an output, the 1 kΩ in series limits the current between the Pi and the sensor. One resistor, two jobs.
Other pairs you'll see online:
| R1 (top) | R2 (bottom) | Vout from 5 V | Verdict |
|---|---|---|---|
| 1 kΩ | 2 kΩ | 3.33 V | Our choice: spot on |
| 1 kΩ | 1.8 kΩ | 3.21 V | Great, slightly safer margin |
| 2.2 kΩ | 4.7 kΩ | 3.41 V | Fine, very common in tutorials |
| 330 Ω | 470 Ω | 2.94 V | Works |
| 1 kΩ | 1 kΩ | 2.50 V | Usually works, but less margin |
| 100 Ω | 200 Ω | 3.33 V | Right voltage, but draws ~17 mA from the sensor's output. Too much. |
| 1 MΩ | 2 MΩ | 3.33 V | Right voltage, but so weak it picks up noise and edges get slow |
| 2 kΩ | 1 kΩ | 1.67 V | Swapped by mistake! Too low, the Pi may never see "high" |
That last row is the classic mistake. The bigger resistor goes to GND. Remember: "the big one sits at the bottom".
7. The circuit
HC-SR04
┌──────────────────┐
│ VCC Trig Echo GND│
└──┬───┬────┬───┬──┘
│ │ │ │
Pi pin 2 (5 V) ────┘ │ │ └──────────────────── Pi pin 14 (GND)
│ │ │
Pi pin 16 (GPIO23) ────┘ │ │
│ │
R1 1 kΩ │
│ │
Pi pin 18 (GPIO24) ─────────● ← divider middle: 3.3 V │
│ │
R2 2 kΩ │
│ │
└──────────────────────────────┘
Follow each signal:
- Power: 5 V (pin 2) → VCC. GND → GND (pin 14).
- Trigger: GPIO23 (pin 16) → Trig. Direct, 3.3 V, safe.
- Echo: Echo (5 V) → 1 kΩ → middle point → GPIO24 (pin 18). Middle point → 2 kΩ → GND.
The Pi only ever sees the middle point. The full 5 V never reaches pin 18.
8. Build it on the breadboard
| Part | Qty | Notes |
|---|---|---|
| HC-SR04 ultrasonic sensor | 1 | Classic 5 V module |
| 1 kΩ resistor | 1 | R1: brown, black, red |
| 2 kΩ resistor | 1 | R2: red, black, red (or two 1 kΩ in series) |
| Jumper wires (female-to-male) | 5 | Pi pins 2, 14, 16, 18 + one spare for GND |
| Breadboard | 1 | Shared with the buzzer circuit from Part 8 |
Power the Pi off first. Then:
- Push the HC-SR04 into the breadboard so its four pins land in four separate rows. Eyes facing out, toward the room.
- Jumper: Pi pin 2 (5 V) → the VCC row.
- Jumper: Pi pin 14 (GND) → the GND row. (If your breadboard has a GND rail from the buzzer build, connect the sensor's GND row to that rail too: one common ground for everything.)
- Jumper: Pi pin 16 (GPIO23) → the Trig row.
- R1 (1 kΩ): from the Echo row to a free row. That free row is the divider middle.
- R2 (2 kΩ): from the divider middle row to the GND row (or GND rail).
- Don't connect pin 18 yet. We'll test the divider first.
Trace it with your finger: 5 V → VCC. GND → GND. GPIO23 → Trig. Echo → 1 kΩ → middle → 2 kΩ → GND. No wire from Echo straight to the Pi.
9. Test it from the console, before any code
Same philosophy as Parts 3 and 8: prove the hardware with simple tools, so any bug later is in the code.
Test 1: check the divider with a multimeter (before it touches the Pi). Power the Pi on. Pin 18 is still unconnected. Set the multimeter to DC volts, black probe on GND.
| Measure (red probe) | Expect | Meaning |
|---|---|---|
| Sensor VCC | ~5 V | Sensor is powered |
| Divider middle (Echo idle) | ~0 V | Echo is low while no measurement is running |
Now let's check the divider ratio itself. Unplug the Echo end of R1 from the Echo row, and touch it to 5 V for a moment instead:
| Measure (red probe) | Expect |
|---|---|
| Divider middle, R1 top on 5 V | ~3.3 V |
You see ~3.3 V? Your divider works, and R1 and R2 are the right way around. (See ~1.7 V? They're swapped.) Put R1 back on the Echo row, power off, and now connect Pi pin 18 (GPIO24) to the divider middle. Power back on.
Test 2: fire the sensor and watch the echo. SSH in twice, two terminal windows.
In the first one, start watching GPIO24 for edges with gpiomon from libgpiod (from Part 3,
sudo apt install gpiod if you haven't yet):
ssh au@au.local
gpiomon -c gpiochip0 24 # libgpiod v2: print every rising/falling edge on GPIO24
# gpiomon gpiochip0 24 # libgpiod v1 (Bookworm) syntax
In the second window, make GPIO23 an output and give it a pulse:
ssh au@au.local
pinctrl set 23 op dl # output, start low
pinctrl set 23 dh; pinctrl set 23 dl # one trigger pulse
Our shell pulse is much longer than 10 µs (each pinctrl call is a whole program starting up, so it
lasts milliseconds), but that's fine: the datasheet asks for at least 10 µs, and the sensor starts
measuring once Trig goes low again.
Back in the first window you'll see two events, something like:
5123.401220481 rising gpiochip0 24
5123.404137925 falling gpiochip0 24
Subtract the timestamps: 5123.404137925 − 5123.401220481 = 0.002917 s = 2917 µs. Then
2917 / 58 ≈ 50 cm. Grab a ruler: put your hand or a book at 50 cm and try it. Move it to 20 cm and
fire again. You just measured distance with a Linux console and two resistors.
The timestamps are taken by the kernel when it handles each edge, so they can be off by some tens of microseconds. That's a centimetre or so of error. Good enough for a test, and in the code part we'll see how to get it tighter.
Press Ctrl+C in the first window to stop gpiomon.
Leave it safe: put the Trig pin back the way it started:
pinctrl set 23 ip pd # input with pull-down: no more triggers
10. When things go wrong
| Problem | What it means | Fix |
|---|---|---|
gpiomon shows nothing at all | No trigger, or no power to the sensor | Check VCC is on 5 V (pin 2), GND is connected, Trig is on pin 16 |
| Only a rising edge, falling comes ~38 ms later | No echo came back (the sensor timed out) | Point it at a flat, hard object within 4 m; nothing within 2 cm |
| Divider middle reads ~1.7 V with 5 V on top | R1 and R2 swapped | The big resistor (2 kΩ) goes to GND |
| Divider middle reads ~5 V | R2 is missing or not on GND | Disconnect pin 18 now and fix R2 before going on |
| Readings jump around | Soft or angled target, loose breadboard wire, or two sensors pinging at once | Use a book or wall facing the sensor; press wires in firmly |
| Readings always short or always the same | Something in front of the sensor (breadboard edge, a cable) | Make sure the eyes have a clear view |
| Works on 5 V but you powered it from 3.3 V | Classic HC-SR04 doesn't like 3.3 V | Move VCC to pin 2 |
gpiomon: ... Device or resource busy | Another program already holds GPIO24 | gpioinfo to see who; stop it (or our alarm app via systemctl) |
gpiomon: unrecognized option '-c' | You have libgpiod v1 | gpiomon gpiochip0 24 |
pinctrl: Permission denied | Your user can't access the GPIO | sudo pinctrl ... |
11. What's next
The alarm can now sense. Next we write the distance task in C: a new thread that pulses
GPIO23, times the Echo pulse on GPIO24, turns microseconds into centimetres, and writes distanceCm
and distanceValid into the shared frame. Then the buzzer finally knows when to shout.
Key Takeaways
- The HC-SR04 is a bat on a chip: a 10 µs pulse on Trig fires 40 kHz sound, and Echo stays high for the round-trip time.
distance_cm = t_us / 58. - Power it from 5 V (pin 2), with GND to the Pi's GND. The classic module doesn't work reliably on 3.3 V.
- Trig goes straight to GPIO23: the Pi's 3.3 V is a clean "high" for the sensor, and it can't hurt it.
- Echo outputs 5 V and must NOT go straight to the Pi. A 1 kΩ / 2 kΩ voltage divider brings it to
5 × 2/3 = 3.33 Vfor GPIO24. The big resistor goes to GND. - Test before code: multimeter the divider before it touches pin 18, then fire with
pinctrland time the echo withgpiomon. Finish withpinctrl set 23 ip pd.
Final Verdict
The HC-SR04 is a fantastic beginner sensor: cheap, simple, and surprisingly accurate on flat, hard targets from 2 cm to about 4 m. And the voltage divider is one of those tiny circuits that you'll use forever. Any time a 5 V signal has to talk to a 3.3 V chip, this is the first thing to reach for.
Gotchas I hit, so you don't have to:
- Never wire Echo straight to the Pi. It's 5 V. The pin might survive a while, then quietly die.
- The big resistor goes to GND. Swap 1 kΩ and 2 kΩ and you get 1.7 V, which the Pi may not read as high.
- Test the divider before it touches the Pi. 5 V on top, ~3.3 V in the middle, then connect pin 18.
- Power the classic module from 5 V. On 3.3 V it often doesn't work at all. (The newer "HC-SR04P" runs on 3.3 V, if you have one.)
- Common ground. The sensor's GND must connect to the Pi's GND.
- Soft or angled targets give odd readings. A book or a wall facing the sensor gives the cleanest results.
- Wait at least 60 ms between pings, or the last echo can confuse the next measurement.
Next up: the C thread that turns those microseconds into distanceCm in the shared frame, and finally
tells the buzzer when to shout.