Learning Goals 5 min
- Wire an HC-SR04 (4 pins) to your UNO, fire a 10 µs trigger pulse, and use
pulseIn()to measure how long the ECHO pin stays HIGH. - Understand
pulseIn(pin, state, timeout)in detail: what it returns, what it returns on timeout (zero), and why the optional timeout matters when nothing is in front of the sensor. - Print raw microseconds — not yet centimetres — and use the L02-21 conversion in your head to sanity-check the readings.
Warm-Up 10 min
Yesterday was physics: the HC-SR04 raises the ECHO pin for as long as a round-trip ultrasonic ping takes, and 58 microseconds = 1 cm round-trip. Today we use a built-in Arduino function — pulseIn() — to measure that ECHO pulse width and print it. Tomorrow we wrap it all in a clean distance helper.
Why a special function?
You could measure a pulse manually with micros():
while (digitalRead(ECHO) == LOW) ; // wait for it to go HIGH
unsigned long t0 = micros();
while (digitalRead(ECHO) == HIGH) ; // wait for it to go LOW
unsigned long width = micros() - t0;That works — but it's 4 lines, easy to get wrong, and lacks a timeout (an infinite loop if the pulse never arrives). pulseIn() does the same thing in one well-tested line.
New Concept · pulseIn(), step by step 20 min
The signature
unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout = 1000000);Three arguments, two of them often defaulted:
pin— the digital pin to watch (e.g.ECHO).state— what level to time (HIGHfor "time the HIGH pulse",LOWfor "time the LOW pulse"). For the HC-SR04:HIGH.timeout— the maximum time to wait, in microseconds. Default = 1 000 000 (1 second). If no pulse arrives within timeout,pulseInreturns 0.
Return value: the pulse width in microseconds. Or 0 on timeout. unsigned long means values up to 4 billion — way more than we need (we cap at ~25 000 µs for distance work).
The HC-SR04 measurement recipe
The same six lines you'll write every time you read this sensor:
// 1. Ensure TRIG starts LOW
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
// 2. Send the 10 µs trigger pulse
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
// 3. Measure the resulting ECHO pulse width
unsigned long width = pulseIn(ECHO, HIGH, 25000); // timeout 25 ms ≈ 4.3 mThree small ideas:
delayMicroseconds()— likedelay()but in microseconds. Accurate for short waits up to ~16 ms.- The 2 µs LOW before the trigger is "just in case" — some boards leave TRIG floating; setting it LOW first guarantees a clean rising edge.
- The 25 000 µs timeout covers slightly more than the sensor's 4 m maximum (which is ~23 000 µs round-trip). Anything longer is a timeout, returning 0. Without a timeout,
pulseIndefaults to 1 000 000 µs = 1 second — your loop freezes for a full second every time nothing is in front of the sensor. Bad.
What 0 means
If width == 0, the chip never reported an echo within the timeout. Three possible causes:
- Nothing in front of the sensor (most common reason).
- Target is more than 4 m away.
- Target absorbs the ping (curtain, foam, cat).
In a real sketch we treat 0 as "no reading" and either skip the iteration or print a special label. Never feed 0 into the distance formula — you'd compute 0 cm and trigger any "something is very close" alarms.
How pulseIn actually works (peek under the hood)
Inside the Arduino core, pulseIn is a small assembly loop that polls the pin's register at high speed. It can resolve pulses down to ~10 microseconds reliably. Below that, the function's own loop overhead becomes a significant fraction of the measurement. For HC-SR04 distances (174 µs at minimum range), this resolution is plenty.
Worked Example · Raw microseconds reader 20 min
Step 1 — wiring
| HC-SR04 pin | UNO pin |
|---|---|
| VCC | +5 V |
| TRIG | D9 |
| ECHO | D10 |
| GND | GND |
Point the sensor at a clear bit of wall about 50 cm away. Open space behind it.
Step 2 — the sketch
Save as echo-microseconds.ino:
// L02-22: HC-SR04 — print raw ECHO pulse widths
const int TRIG = 9;
const int ECHO = 10;
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
Serial.begin(9600);
}
void loop() {
// Send the 10 µs trigger pulse
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
// Measure the echo
unsigned long width = pulseIn(ECHO, HIGH, 25000);
if (width == 0) {
Serial.println("(no echo)");
} else {
Serial.print("ECHO: ");
Serial.print(width);
Serial.println(" µs");
}
delay(100); // 100 ms between measurements — safely above the 60 ms minimum
}Step 3 — upload and observe
Open Serial Monitor at 9600. Pointed at a wall 50 cm away you should see something like:
ECHO: 2918 µs ECHO: 2922 µs ECHO: 2914 µs ECHO: 2920 µs
Divide by 58: 2918 / 58 ≈ 50.3 cm. Matches your measuring tape ±1 cm. The sketch is working.
Step 4 — wave your hand
Move your hand slowly toward the sensor from 50 cm down to 5 cm. The numbers should drop smoothly: 2900 → 2300 → 1800 → 1200 → 600 → 300. Each step roughly corresponds to centimetres (divide by 58 if you want exact).
Step 5 — point at empty space
Aim the sensor at the ceiling (assuming a high ceiling) or out a window. You should start seeing (no echo) lines once nothing is within range. That's the timeout-returning-zero path. Notice that those lines come out about every 125 ms (100 ms delay + 25 ms timeout) — the loop is still responsive, just slower.
Step 6 — try an absorbent target
Hold a cushion or a folded sweater 30 cm in front of the sensor. Expected reading: 30 × 58 = 1740 µs. Actual reading: often (no echo), or intermittently a number. That's acoustic absorption from L02-21 — the cushion swallows the ping. The sensor isn't broken; soft targets just don't reflect ultrasound well.
Basic 7 min
Goal: Count timeouts. Add a counter that tracks how many of the last 50 measurements were timeouts (echo = 0), and print the count next to each reading. A reading is "reliable" when this count is low; suspicious when it's high.
Challenge 1 7 min
Goal: Apply L02-08's running-average smoothing (N = 5) to the pulse widths. Print the smoothed value alongside the raw one. Smooth readings are dramatically more usable for downstream logic.
Challenge 2 6 min
Goal: Manually re-implement pulseIn using micros() and a busy-wait. Compare your version to the library version: do they agree on a 50 cm target?
Challenge 3 · The "close warning" tone 15 min
Combine the HC-SR04 with a piezo buzzer on D8. The sketch should:
- Measure the ECHO pulse every 100 ms.
- Stay completely silent for any reading > 1740 µs (i.e. > 30 cm) or any timeout.
- For widths between 580–1740 µs (10–30 cm range), play a single 100 ms beep at 1500 Hz — once per measurement.
- For widths below 580 µs (closer than 10 cm), play a continuous 2500 Hz tone (high-pitch "too close" warning).
It's done when:
- Pointing at empty air → silent.
- Slow approach with your hand → beep starts at ~30 cm, gets more urgent as you close in.
- Below ~10 cm → continuous high-pitched tone.
- Pulling away → tone stops, beeps fade away.
- You can name the three audio states clearly in the Serial Monitor ("silent / beep / TOO CLOSE") for debugging.
Recap 5 min
pulseIn(pin, state, timeout) is the right tool for measuring a pulse width in microseconds. For the HC-SR04 the recipe is always the same: LOW-HIGH-LOW the TRIG pin for 10 µs, then call pulseIn(ECHO, HIGH, 25000). The function returns the round-trip time in microseconds, or 0 on timeout. Always set the timeout — without it your loop can stall for a full second when nothing is in front of the sensor. Today you printed raw microseconds and verified with a tape measure that ÷58 gives sensible centimetres. Tomorrow we promote that division into a clean readDistanceCm() helper and write the whole thing up as the standard distance pattern.
pulseIn(pin, state, timeout)- Built-in Arduino function that measures how long a pin stays at the given state, returning the duration in microseconds — or 0 if the pulse doesn't finish within timeout.
delayMicroseconds(n)- Like
delay()but in microseconds. Accurate for short pauses up to ~16 ms. Used to make the 10 µs trigger pulse. - Timeout
- The maximum time
pulseInwill wait for a pulse. Defaults to 1 second; for HC-SR04 we set it to 25 000 µs to bound our loop's worst case. - Blocking call
- A function that doesn't return until it's done.
pulseInis blocking — your loop is paused until the pulse ends or the timeout fires. - Trigger pulse
- The 10 µs HIGH-then-LOW signal on TRIG that tells the HC-SR04 to start a measurement.
- Echo pulse
- The HIGH pulse on ECHO whose width equals the round-trip time of the ultrasonic ping.
- Inter-measurement delay
- The minimum time between consecutive HC-SR04 measurements — datasheet says 60 ms, we use 100 ms for safety. Lets the previous ping's echo die out before the next ping starts.
Extra Mission 5 min
Part 1 — Design a "something is there" detector
Pick a spot where you want to know if something is present — a seat, a shelf, a toy car's parking space. On paper, design a detector that only uses the echo width in µs.
Your design must include:
- A name and a one-sentence job for the detector.
- The echo width that means "something is there", and the one that means "empty".
- What a timeout (width 0) means for your detector, and what it should do.
- How often it measures, and how it shows the result — an LED or the buzzer.
Part 2 — Make it
Build it with the HC-SR04 on D9 and D10. Use pulseIn() with a 25000 µs timeout. Print the width and your decision every 100 ms. Count timeouts as in Basic.
Bring back next class: the uploaded sketch and a Serial Monitor screenshot showing both states and a timeout.