Learning Goals 5 min
- Explain why two LDRs (or two pots, or two thermistors) sitting next to each other give different raw readings — manufacturing tolerance, room lighting, supply voltage drift.
- Write a five-second calibration sweep inside
setup()that records a sensor's min and max while the student waves their hand over it. - Use the calibrated min and max afterwards to interpret "0% light" and "100% light" against that specific sensor in that specific room.
Warm-Up 10 min
You wrote a sketch in L02-07 that reads the LDR and prints the value. You sat at your desk and saw, say, 420 in normal light and 80 with your hand over it. Brilliant.
Now take the sketch home. Your bedroom is darker than the classroom. Sit at your desk and the "normal" reading is 180, "hand over" is 30. The same code; different numbers.
Quick puzzle
If you hard-code "LED on when value < 100" (because that worked at school), what happens at home where the always-on reading is 180?
Reveal
The LED never turns on at home — the home reading never gets below 100. The sketch is "correct" but useless because the calibration is baked in. Today's lesson: don't bake in. Measure when you boot.
New Concept · Self-calibrating in setup() 20 min
The big idea
Right after the Arduino boots, spend a few seconds sampling the sensor while the user shows it the extremes — "darkest light you'll ever see" and "brightest light you'll ever see". Record the min and max. Use those for every later reading.
The five-second sweep
Use millis() from L01-XX (well — we haven't formally taught it yet; here's the gist): millis() returns how many milliseconds the Arduino has been running. Subtract a start time and you have an "elapsed" counter.
int sensorMin = 1023; // initialise to the WORST POSSIBLE min
int sensorMax = 0; // initialise to the WORST POSSIBLE max
void setup() {
Serial.begin(9600);
Serial.println("Calibrating — show the sensor its extremes for 5 seconds.");
unsigned long startTime = millis();
while (millis() - startTime < 5000) {
int reading = analogRead(A0);
if (reading < sensorMin) sensorMin = reading;
if (reading > sensorMax) sensorMax = reading;
}
Serial.print("Done. min = "); Serial.print(sensorMin);
Serial.print(", max = "); Serial.println(sensorMax);
}The two "worst possible" initial values force the first real reading to replace them, no matter what it is. sensorMin = 1023 (any reading will be less) — the first reading replaces it. Same for sensorMax = 0 (any reading will be greater). This is the standard idiom for finding min/max in a stream.
Using the calibration afterwards
Once calibration is done, the loop() uses sensorMin and sensorMax to interpret each new reading:
void loop() {
int raw = analogRead(A0);
long normalised = (long)(raw - sensorMin) * 100 / (sensorMax - sensorMin);
Serial.println(normalised);
delay(100);
}That formula gives 0% at the calibrated min and 100% at the calibrated max — regardless of room, sensor batch or voltage. Outside the calibrated range you can get negative numbers or >100, which is fine.
Why unsigned long for the time?
millis() returns an unsigned 32-bit number. unsigned long matches it. After ~50 days the number wraps around back to 0 — for a 5-second sweep right after boot, you're safe forever.
The user feedback pattern
Calibration sweeps work best when the user knows they need to wiggle the sensor. Always:
- Print a clear "Calibrating — do X for Y seconds" message at the start.
- (Optional) Blink an on-board LED while it's sampling so users see "something is happening".
- Print a "Done" message with the captured min and max, so they can verify it worked.
Worked Example · Self-calibrating LDR light meter 20 min
Step 1 — wiring
LDR + 10 kΩ pull-down on A1 (same as L02-08). Built-in LED on pin 13 we'll use as a "calibrating now" indicator.
Step 2 — the sketch
Save as ldr-calibrated.ino:
// L02-09: self-calibrating LDR meter
const int LDR = A1;
const int LED = 13;
const unsigned long CAL_MS = 5000;
int sensorMin = 1023;
int sensorMax = 0;
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(9600);
Serial.println("Calibration starting — show the sensor:");
Serial.println(" 1) brightest light you'll see (e.g. phone torch)");
Serial.println(" 2) darkest light (cover with your hand)");
Serial.println(" 3) sweep between them for 5 seconds.");
digitalWrite(LED, HIGH); // "calibrating" indicator
unsigned long startTime = millis();
while (millis() - startTime < CAL_MS) {
int reading = analogRead(LDR);
if (reading < sensorMin) sensorMin = reading;
if (reading > sensorMax) sensorMax = reading;
}
digitalWrite(LED, LOW);
// sanity check
if (sensorMax - sensorMin < 50) {
Serial.println("WARN: sensor barely moved — using defaults.");
sensorMin = 100;
sensorMax = 900;
}
Serial.print("Calibrated. min = "); Serial.print(sensorMin);
Serial.print(", max = "); Serial.println(sensorMax);
}
void loop() {
int raw = analogRead(LDR);
long pct = (long)(raw - sensorMin) * 100 / (sensorMax - sensorMin);
Serial.print("raw: "); Serial.print(raw);
Serial.print(" "); Serial.print(pct);
Serial.println("%");
delay(100);
}Step 3 — upload & calibrate
Open Serial Monitor. You should see:
Calibration starting — show the sensor: 1) brightest light you'll see (e.g. phone torch) 2) darkest light (cover with your hand) 3) sweep between them for 5 seconds.
The on-board pin-13 LED lights up. Quickly: shine your phone torch directly at the LDR for ~2 seconds, then cup your hand completely over it for the remaining ~3 seconds. The LED turns off after 5 seconds, and you'll see something like:
Calibrated. min = 35, max = 985 raw: 412 42% raw: 410 42% raw: 96 7% raw: 920 93%
Step 4 — verify it works at different desk lights
Run the sketch in a bright room → re-do step 3 with that room's light. The min/max are now calibrated for there. The percentage readings should still go 0–100 across your hand-cover-vs-torch range. Move to a darker room, hit reset → re-calibrate. Same percentages still mean the same thing relative to the new room's extremes.
Try It Yourself 20 min
Goal: Shrink the calibration window to 3 seconds. Will it still work reliably?
Reveal
Change const unsigned long CAL_MS = 5000; to 3000. It works if you can hit both extremes within 3 seconds — but most people fumble. 5 seconds gives breathing room. Sensor calibration is a UX problem: as the developer, give your users enough time to do the thing you're asking.
Goal: Add a button on pin 2 (INPUT_PULLUP) that, when pressed any time, restarts the calibration. Useful if the room lights change after boot.
Hint
Pull the calibration out into its own function calibrate(). Call it once from setup(), then again whenever the button reads LOW inside loop(). Reset sensorMin = 1023 and sensorMax = 0 at the start of calibrate().
Goal: Drive the LED on pin ~9 from the normalised percentage. Brightness = pct / 100 × 255. The LED should glow more in bright light. If the room gets dark, the LED dims. After re-calibration, the LED's response covers the full range again.
Hint
long pct = (long)(raw - sensorMin) * 100 / (sensorMax - sensorMin);
pct = constrain(pct, 0, 100); // clip out-of-range values (preview of L02-10)
analogWrite(LED, pct * 255 / 100);The constrain() built-in clips values to a range — exactly the topic of L02-10. We'll use it properly tomorrow; here it's a small preview.
Mini-Challenge · The clap-detector 15 min
Use the piezo as a knock sensor (it's a tiny voltage generator when squeezed; connect both legs across A2 and GND, with a 1 MΩ resistor in parallel). Calibrate for 5 seconds in setup() — the user is supposed to tap the piezo to generate spikes. After calibration, every time the reading exceeds the calibrated max, blink the on-board LED.
It works if:
- During the 5-second sweep, the user taps the piezo a few times to register what "loud" looks like.
- After calibration, tapping the piezo at a similar intensity triggers an LED blink.
- Gentle hand-shaking or ambient vibration does NOT trigger the LED — because that's below the calibrated max.
Reveal one valid sketch
const int PIEZO = A2;
const int LED = 13;
int threshold = 0; // set during calibration
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(9600);
Serial.println("Tap the piezo a few times in the next 5 s...");
digitalWrite(LED, HIGH);
unsigned long start = millis();
while (millis() - start < 5000) {
int r = analogRead(PIEZO);
if (r > threshold) threshold = r;
}
digitalWrite(LED, LOW);
threshold = threshold - 50; // trigger just under the loudest cal tap
if (threshold < 100) threshold = 100;
Serial.print("Threshold: "); Serial.println(threshold);
}
void loop() {
int r = analogRead(PIEZO);
if (r > threshold) {
digitalWrite(LED, HIGH);
delay(100);
digitalWrite(LED, LOW);
}
}The threshold - 50 trick adds a small buffer so the trigger fires reliably even on a slightly weaker tap. The if (threshold < 100) guard handles the case where calibration totally failed.
Recap 5 min
Sensors vary, rooms vary, supplies drift. Hard-coded thresholds break the moment your sketch leaves the workbench. The fix is to spend a few seconds at boot measuring the actual extremes the sensor will see, and use those as the calibration. The pattern: print instructions → loop over millis() for N seconds → record min and max → sanity-check → use them. Add a button to re-calibrate on demand if conditions change. Tomorrow we make the conversion from raw to "useful" even cleaner with map() and constrain().
- Calibration
- Measuring a sensor's real-world min and max, in its current environment, before you start using its readings.
millis()- Built-in that returns the number of milliseconds since the Arduino booted. Returns
unsigned long. Wraps at ~50 days. - Initial extremes
sensorMin = 1023andsensorMax = 0— the "worst possible" starting values so the first real reading replaces them.- Sanity check
- A guard that catches silly inputs (calibration sweep where nothing moved, max less than min) and falls back to sensible defaults.
- Normalisation
- Converting a raw value into a 0–100% (or 0–1) value relative to the calibrated range.
(raw - min) / (max - min). - UX of calibration
- Tell the user what to do, indicate when sampling is happening (LED), and print the result so they can verify.
Homework 5 min
Three rooms, three calibrations. Take your Arduino + LDR to three different lighting situations:
- Bright (sunny room, lamp on).
- Normal (regular room light).
- Dim (curtains drawn, no lamp).
In each, run the worked-example sketch, do the calibration sweep, and write down the captured min and max. Then in each room: cover the LDR fully → what percentage do you see? Shine your torch → what percentage?
Question for your notebook: "Are the cover/torch percentages similar across all three rooms, or do they drift? Why?"
Bring back next class:
- Your three-room table with min, max, cover%, torch%.
- Your answer to the drift question.
- The sketch saved as
hw-l02-09.ino.