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.
Basic 7 min
Goal: Shrink the calibration window to 3 seconds. Will it still work reliably?
Challenge 1 7 min
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.
Challenge 2 6 min
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.
Challenge 3 · 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.
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.
Extra Mission 5 min
Part 1 — Design a gadget that works in any room
A sensor reads differently in a sunny room and a dark one. Pick a gadget that must work anywhere — a night-light, a "curtains open" detector, a shadow counter. On paper, design how it calibrates itself.
Your design must include:
- A name and a one-sentence job for the gadget.
- What the user does during the calibration window, and how long it lasts.
- How the gadget shows that calibration is happening — an LED, a printed message, or both.
- The rule after calibration, as a percentage — for example "LED on below 20%".
Part 2 — Make it
Build it with the LDR from today's worked example. Calibrate in setup() as today, then apply your percentage rule in loop(). Print the captured min and max, then the live percentage.
Bring back next class: the uploaded sketch and a Serial Monitor screenshot showing the calibrated min and max.