Learning Goals 5 min
- Take yesterday's raw µs reader and wrap it into a clean
float readDistanceCm()helper — the standard pattern you'll re-use in every distance project. - Handle the three things the function has to deal with: a real reading, a no-echo timeout (return a sentinel like
-1), and out-of-range readings (clip or reject). - Build a Serial-Monitor distance display that's actually pleasant to look at — readings every 100 ms, with units, with a label, with a clear "out of range" line when the sensor sees nothing.
Warm-Up 10 min
Two lessons in we know everything about the HC-SR04: the physics (L02-21), the timing (L02-22). Today we get to enjoy the result — a single helper function that takes no arguments and returns a clean distance in centimetres. Every future project — parking sensor, smart bin, range alarm — will call this one function.
Quick recap of the conversion
From two days ago: round-trip ECHO microseconds ÷ 58 = centimetres. The /58 is the magic number. In code:
float cm = width / 58.0;One caveat: width / 58 (integer division) gives integer cm, which loses fractions. width / 58.0 (the .0 matters!) gives float cm, which is what we want.
New Concept · A reusable distance helper 20 min
The shape of the function
Here's the goal — one function, no arguments, returns a float:
float readDistanceCm();Every project we write from this point on will start with this single line in loop():
float d = readDistanceCm();The function itself encapsulates the trigger pulse, the pulseIn, the /58, the timeout handling, and the range check. The rest of the sketch doesn't need to know any of those details.
Sentinel value for "no reading"
What should the function return when there's no echo? We need a special value distinct from any real reading. Two clean options:
- Return -1 — works because no real distance is negative. Caller checks:
if (d < 0) { ... }. - Return NaN (using
NAN) — fits with the DHT11's convention. Caller checks:if (isnan(d)) { ... }.
For distance projects, -1 is the more common convention. We'll go with that.
Range clipping
From L02-21 we know the sensor is reliable from ~2 to ~400 cm. Values outside that range — even if the pulseIn returned something — should be treated as junk. Two choices:
- Clip: return 2 cm for any reading < 2, and 400 cm for any reading > 400. Hides bad data inside the function — caller sees a smooth-but-clipped value.
- Reject: return -1 for out-of-range. Caller can decide what to do.
For an "is something close?" alarm, clipping is fine. For a measurement tool, rejection is better. Our default helper rejects.
The full helper
const int TRIG = 9;
const int ECHO = 10;
float readDistanceCm() {
// 10 µs trigger
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long width = pulseIn(ECHO, HIGH, 25000);
if (width == 0) return -1; // timeout / out of range
float cm = width / 58.0;
if (cm < 2 || cm > 400) return -1; // outside reliable range
return cm;
}Six effective lines, one early-return for the timeout, one for the range check. Anywhere else in your sketch, distance is now just one function call away.
Using the helper
The caller is now extremely short:
void loop() {
float d = readDistanceCm();
if (d < 0) Serial.println("(out of range)");
else { Serial.print(d, 1); Serial.println(" cm"); }
delay(100);
}This is the "separation of concerns" idea: the sensor function deals with the sensor; the loop deals with what to do with the value. Same pattern as the personal thermometer (L02-12), DHT logger (L02-14), Weather Station (L02-20).
Worked Example · Polished distance display 20 min
Step 1 — wiring
Same as L02-22. HC-SR04 on D9 (TRIG) and D10 (ECHO), plus +5 V and GND. Point at a clear area.
Step 2 — the sketch
Save as distance-display.ino:
// L02-23: clean distance helper + pleasant display
const int TRIG = 9;
const int ECHO = 10;
float readDistanceCm() {
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long width = pulseIn(ECHO, HIGH, 25000);
if (width == 0) return -1;
float cm = width / 58.0;
if (cm < 2 || cm > 400) return -1;
return cm;
}
const char* zoneFor(float d) {
if (d < 0) return "no echo";
if (d < 10) return "VERY CLOSE";
if (d < 30) return "close";
if (d < 100) return "near";
return "far";
}
unsigned long lastRead = 0;
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
Serial.begin(9600);
}
void loop() {
if (millis() - lastRead < 100) return;
lastRead = millis();
float d = readDistanceCm();
if (d < 0) {
Serial.println("------ cm (no echo)");
} else {
Serial.print(d, 1);
Serial.print(" cm ");
Serial.println(zoneFor(d));
}
}Step 3 — upload and observe
Open Serial Monitor at 9600. Move your hand toward the sensor from far away:
------ cm (no echo) 145.2 cm far 98.7 cm near 54.1 cm near 32.3 cm near 20.6 cm close 12.4 cm close 7.8 cm VERY CLOSE
The zone label changes smoothly. The (no echo) line happens when your hand is out of range, and as soon as it's detectable, real distances start streaming.
Step 4 — visualise with the Plotter
The Arduino IDE has a built-in Serial Plotter (Tools → Serial Plotter, same shortcut as Serial Monitor but for graphs). It plots any number it sees on each line. Change the print to just the number:
// inside loop, replace the print block with:
Serial.println(d < 0 ? 0 : d); // 0 means "no echo" in the plotOpen the Plotter. Wave your hand toward and away from the sensor — you'll see a wobbly curve that mirrors your hand's movement. Your first real-time graph. Useful for tuning thresholds visually before committing them to code.
Step 5 — the helper, reusable
Copy the readDistanceCm function into a new blank sketch and write a one-line loop:
void loop() { Serial.println(readDistanceCm()); delay(200); }It still works. That's the test of a good helper: it's portable. Tomorrow we drop this same helper into a range-alarm; next lesson into a parking sensor; the lesson after into a smart-bin servo. Same six lines, dozens of products.
Try It Yourself 20 min
Goal: Print both centimetres and inches on the same line. 1 inch = 2.54 cm.
Hint
float in = d / 2.54;
Serial.print(d, 1); Serial.print(" cm ");
Serial.print(in, 1); Serial.println(" in");Goal: Apply L02-08 smoothing inside the helper itself. The function should average the last 3 reliable measurements (skipping any -1 timeouts) and return the smoothed distance. The caller doesn't need to know smoothing is happening.
Hint
float readDistanceCm() {
static float buffer[3] = {-1, -1, -1};
static int idx = 0;
// ... existing measurement code ...
if (width == 0 || cm < 2 || cm > 400) return -1;
buffer[idx] = cm;
idx = (idx + 1) % 3;
float sum = 0; int n = 0;
for (int i = 0; i < 3; i++) if (buffer[i] > 0) { sum += buffer[i]; n++; }
return n > 0 ? sum / n : -1;
}The static keyword inside a function makes the buffer survive between calls — same lifetime as a global, but only visible inside this function. Cleaner than a global for "function-private memory".
Goal: Temperature-compensated distance. Add a readDistanceCm(float tempC) overload that uses the temperature to compute a more accurate speed of sound. Use it with the DHT11 from L02-14: feed yesterday's temperature into today's distance helper.
Hint
float readDistanceCm(float tempC) {
// speed of sound (m/s) = 331.4 + 0.6 × T
// microseconds per cm round-trip = 20 000 000 / v (cm/s)
float vCmPerS = (331.4 + 0.6 * tempC) * 100.0;
float usPerCm = 2.0 * 1e6 / vCmPerS;
// ... existing trigger + pulseIn ...
if (width == 0) return -1;
float cm = width / usPerCm;
// ... existing range check, return cm ...
}For most indoor uses the difference is tiny (~1%). For a refrigerator gauge or an outdoor build in extreme weather, it's worth it.
Mini-Challenge · Three-light bargraph 15 min
Build a visual distance display with three LEDs that act as a coarse bargraph:
- Green LED on D9 — always on when distance > 30 cm (far & safe).
- Yellow LED on D10 — additionally on when distance is between 10–30 cm (getting close).
- Red LED on D11 — additionally on when distance < 10 cm (DANGER).
So three LEDs lit means you're very close; two means close-ish; one means safe; none means no echo at all.
It's done when:
- Pointing at empty air → all three LEDs off.
- Hand at 50 cm → only green on.
- Hand at 20 cm → green + yellow on.
- Hand at 5 cm → all three on.
- The transitions are clean (no flicker) — try moving your hand slowly across the threshold lines.
Reveal one valid sketch
const int TRIG = 9;
const int ECHO = 10;
const int GREEN = 6; // moved to free up 9-10 for HC-SR04
const int YELLOW = 5;
const int RED = 3;
float readDistanceCm() {
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long w = pulseIn(ECHO, HIGH, 25000);
if (w == 0) return -1;
float cm = w / 58.0;
if (cm < 2 || cm > 400) return -1;
return cm;
}
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
pinMode(GREEN, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(RED, OUTPUT);
Serial.begin(9600);
}
void loop() {
float d = readDistanceCm();
bool hasReading = (d >= 0);
bool green = hasReading; // any valid reading lights green
bool yellow = hasReading && (d < 30);
bool red = hasReading && (d < 10);
digitalWrite(GREEN, green);
digitalWrite(YELLOW, yellow);
digitalWrite(RED, red);
if (hasReading) {
Serial.print(d, 1); Serial.println(" cm");
} else {
Serial.println("(no echo)");
}
delay(100);
}The clever bit: each LED is governed by its own simple boolean — "hasReading AND distance below threshold". No nested if/elses, no exclusive-zone logic — they naturally compose. Want a fourth LED for the < 5 cm zone? Add one more boolean line. The pattern scales.
Recap 5 min
The three-lesson HC-SR04 arc is complete. L02-21 gave you the physics; L02-22 the timing; L02-23 the helper. readDistanceCm() is now a reusable building block: six effective lines, no caller-visible details, return -1 on no-reading or out-of-range. The same separation pattern — sensor function returns a clean value; calling code decides what to do with it — drives every future sensor project, from the parking sensor tomorrow to the smart bin lid two days from now.
- Helper function
- A small, single-purpose function that hides implementation detail behind a clean signature —
readDistanceCm(),readTempC(),setColour(r,g,b), etc. The building blocks of clean projects. - Sentinel value
- A specific return value (here -1, in DHT11 NaN) that means "no valid reading". Callers must check for it before using the value in maths.
- Range clip / range reject
- Two strategies for handling out-of-spec readings: clip = clamp to the boundary, reject = return the sentinel. Pick based on whether downstream code can tolerate clamped values.
- Separation of concerns
- The design principle that each function should do one thing.
readDistanceCmreads;zoneForclassifies; the main loop combines them. Each is testable on its own. static(inside a function)- A variable inside a function declared
staticpersists across calls — same lifetime as a global, but only visible inside the function. Used for function-private state like a smoothing buffer. - Serial Plotter
- The Arduino IDE's built-in graph view (Tools → Serial Plotter). Plots any number it sees on each Serial line — useful for tuning thresholds visually.
Homework 5 min
Write your "distance toolkit" one-page reference. By now you have a working readDistanceCm() helper. Write a one-page reference (typed or hand-drawn) that you could share with a classmate who's starting Cluster D fresh. It should include:
- The wiring diagram (TRIG, ECHO, VCC, GND).
- The 6-line helper function, copyable.
- A sentence explaining the -1 return and how to check for it.
- The range limits (2–400 cm) and beam angle.
- One "gotcha" — something you wish you'd known before you started.
Then build one small sketch — your choice — that uses the helper in an interesting way. Suggestions:
- A "height of liquid in a cup" gauge (point sensor down into a tall cup).
- A "is the door open" detector (point at a door frame).
- An "air-violin" — distance controls the pitch of a continuous buzzer tone.
Bring back next class:
- Your one-page distance-toolkit reference.
- Your
hw-l02-23.inosketch demonstrating one creative use ofreadDistanceCm. - Tomorrow we use the helper for the canonical "reverse parking" range alarm.