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.
Try It Yourself 20 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.
Hint
int total = 0, fails = 0;
// in loop, after measuring width:
total++;
if (width == 0) fails++;
if (total >= 50) { total = 0; fails = 0; }
Serial.print("width: "); Serial.print(width);
Serial.print(" µs fails (50): "); Serial.println(fails);If fails > 20 out of 50, you're aimed at an absorbent target or empty space.
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.
Hint
Same template as the smoothed pot/thermistor — only the source changes:
const int N = 5;
unsigned long samples[N] = {0};
int idx = 0;
unsigned long total = 0;
unsigned long smoothed(unsigned long fresh) {
total -= samples[idx];
samples[idx] = fresh;
total += samples[idx];
idx = (idx + 1) % N;
return total / N;
}Skip the running-average entirely on timeout readings (don't let a 0 corrupt the buffer) — pass through the previous smoothed value instead.
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?
Hint
unsigned long myPulseIn(int pin, int state, unsigned long timeoutUs) {
unsigned long start = micros();
while (digitalRead(pin) != state) {
if (micros() - start > timeoutUs) return 0;
}
unsigned long pulseStart = micros();
while (digitalRead(pin) == state) {
if (micros() - pulseStart > timeoutUs) return 0;
}
return micros() - pulseStart;
}Two timeouts (one for the pulse to start, one for it to end), both relative. Run side by side with the built-in pulseIn — your version is usually within a few microseconds, sometimes ahead, sometimes behind, depending on where in your loop the pulse happens to arrive. Useful exercise for understanding what the library does internally.
Mini-Challenge · 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.
Reveal one valid sketch
const int TRIG = 9;
const int ECHO = 10;
const int BUZZ = 8;
const unsigned long BEEP_THRESHOLD = 1740; // 30 cm
const unsigned long ALARM_THRESHOLD = 580; // 10 cm
unsigned long lastMeasure = 0;
bool alarmActive = false;
unsigned long measureEcho() {
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
return pulseIn(ECHO, HIGH, 25000);
}
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
pinMode(BUZZ, OUTPUT);
Serial.begin(9600);
}
void loop() {
if (millis() - lastMeasure < 100) return;
lastMeasure = millis();
unsigned long w = measureEcho();
if (w == 0 || w > BEEP_THRESHOLD) {
if (alarmActive) { noTone(BUZZ); alarmActive = false; }
Serial.println("silent");
} else if (w > ALARM_THRESHOLD) {
if (alarmActive) { noTone(BUZZ); alarmActive = false; }
tone(BUZZ, 1500, 80);
Serial.print("beep width="); Serial.println(w);
} else {
if (!alarmActive) { tone(BUZZ, 2500); alarmActive = true; }
Serial.print("TOO CLOSE width="); Serial.println(w);
}
}Notice two flavours of tone(): with a duration (one-off beep, non-blocking) for the "getting closer" pulses; without a duration (continuous tone, has to be cancelled with noTone) for the "too close" alarm. The alarmActive flag prevents tone from being re-started every loop iteration, which would cause audible glitches.
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.
Homework 5 min
Calibration session. Wire the HC-SR04 + Serial output. Tape a tape measure on the bench, mount the sensor at the 0 cm mark. Place a hardback book flat as a target. For each of these actual distances, record the measured ECHO microseconds (take 5 readings, average them):
| Actual cm | Expected µs (× 58) | Measured µs (avg of 5) | Implied cm (÷ 58) |
|---|---|---|---|
| 5 | ? | ? | ? |
| 10 | ? | ? | ? |
| 25 | ? | ? | ? |
| 50 | ? | ? | ? |
| 100 | ? | ? | ? |
Fill in all 4 columns. Then:
- Which row had the smallest error?
- Which row had the largest? Why might that be?
- If you had to use a non-58 number to convert µs → cm in your classroom, what would it be? (Hint: divide measured µs by actual cm for the 50 cm row.)
Bring back next class:
- Your filled-in calibration table.
- Your three written answers.
- Your
hw-l02-22.inosketch — tomorrow we wrap this all in a clean distance function.