Learning Goals 5 min
- Re-wire the LDR voltage divider from L01-36 and add a five-second auto-calibration in
setup()so it learns this room's darkest and brightest values. - Use
map()+constrain()to convert the calibrated raw range into a friendly 0–100 "light percentage", plus a four-band label: dark / dim / bright / glare. - Discuss why your sketch reports relative brightness, not real-world lux — and what it would take to make it a real lux meter (spoiler: a much pricier sensor).
Warm-Up 10 min
You first met the LDR — light-dependent resistor — back in L01-36. Bright light = low resistance; dark = high resistance. In a divider with 10 kΩ, the A0 voltage swings between roughly 0.5 V (covered with your hand) and 4.5 V (phone torch shining on it).
Today we promote it from "raw number" to a calibrated, labelled, useful reading. The trick is L02-09's setup-time calibration: at startup the sketch records the min and max it sees over the first five seconds, and from then on it scales every reading against those limits. Move the project to a different room and the calibration adapts automatically.
Quick estimate
If your LDR sketch printed raw = 200 in a normal classroom, and you covered the LDR with your finger and saw raw = 30 — which value means "dark", and which means "bright"? (Hint: think about which way the divider is wired.)
Reveal
It depends on how you wired the divider — that's why every datasheet draws it one way and every classroom build does it another. In the wiring we use this lesson (LDR between A0 and +5 V, 10 kΩ between A0 and GND): bright light → low LDR resistance → MORE current flows → A0 sits closer to 5 V → higher raw number. So 200 = brighter than 30. Flip the LDR and 10 kΩ around and the relationship inverts. The calibration takes care of either case.
New Concept · From a raw number to a meaningful one 20 min
The wiring (refresher)
| From | Via | To |
|---|---|---|
| +5 V rail | LDR | A0 column (midpoint) |
| A0 column (midpoint) | 10 kΩ resistor | GND rail |
Same divider as L01-36. No polarity to worry about — the LDR is symmetric, and 10 kΩ is a passive resistor.
The five-second calibration
From L02-09: during the first 5 000 ms after power-on we keep checking the LDR's reading and update two variables — darkRaw and brightRaw — as we see lower and higher values. You're meant to cup your hand over the LDR for ~2 s, then shine a phone torch on it for ~2 s, all within those five seconds. The sketch remembers what it saw.
void calibrate() {
int darkRaw = 1023, brightRaw = 0;
unsigned long t0 = millis();
while (millis() - t0 < 5000) {
int r = analogRead(LDR);
if (r < darkRaw) darkRaw = r;
if (r > brightRaw) brightRaw = r;
}
// ... store darkRaw and brightRaw in globals ...
}This way the same sketch works in your bedroom (where "bright" might top out at raw = 600) and on a sunny veranda (where "dark" under your palm might still read raw = 200).
map() + constrain() — the polishing step
Once we have darkRaw and brightRaw, every later reading gets pushed through:
int rawClipped = constrain(rawNow, darkRaw, brightRaw);
int pct = map(rawClipped, darkRaw, brightRaw, 0, 100);constrain() clips the raw value so it never goes outside our calibrated range (otherwise map() would extrapolate to negative numbers or above 100). map() rescales linearly. Output: a tidy 0–100 percent number.
From percent to a label
Same pattern as the personal thermometer:
const char* labelFor(int pct) {
if (pct < 20) return "dark";
if (pct < 50) return "dim";
if (pct < 80) return "bright";
return "glare";
}Four bands, tuned to feel right. You can re-tune later — "dim" might mean "cinema screen okay to read"; "bright" might mean "ceiling light on".
Worked Example · Self-calibrating light meter 20 min
Step 1 — wiring
LDR voltage divider on A0 as above. No LEDs yet — we want to focus on the calibration logic and the Serial output.
Step 2 — the sketch
Save as light-meter.ino:
// L02-15: Self-calibrating LDR light meter
const int LDR = A0;
int darkRaw = 1023; // updated during calibration
int brightRaw = 0;
void calibrate() {
Serial.println("Calibrating — cover then expose the LDR (5 s)...");
unsigned long t0 = millis();
while (millis() - t0 < 5000) {
int r = analogRead(LDR);
if (r < darkRaw) darkRaw = r;
if (r > brightRaw) brightRaw = r;
}
// Safety: if the user didn't move enough, force a usable range
if (brightRaw - darkRaw < 50) {
darkRaw = 0;
brightRaw = 1023;
Serial.println("(no clear range — using 0..1023 fallback)");
}
Serial.print("dark="); Serial.print(darkRaw);
Serial.print(" bright="); Serial.println(brightRaw);
}
const char* labelFor(int pct) {
if (pct < 20) return "dark";
if (pct < 50) return "dim";
if (pct < 80) return "bright";
return "glare";
}
void setup() {
Serial.begin(9600);
delay(500); // let the Serial Monitor wake
calibrate();
}
void loop() {
int raw = analogRead(LDR);
int clipped = constrain(raw, darkRaw, brightRaw);
int pct = map(clipped, darkRaw, brightRaw, 0, 100);
Serial.print("raw: "); Serial.print(raw);
Serial.print(" pct: "); Serial.print(pct);
Serial.print("% "); Serial.println(labelFor(pct));
delay(250);
}Step 3 — upload, calibrate, read
Open Serial Monitor. You'll see Calibrating — cover then expose the LDR (5 s).... Quickly cup your hand over the LDR for ~2 s, then point a phone torch at it for ~2 s. After five seconds the sketch prints the dark/bright values it learned, then settles into a flowing read:
Calibrating — cover then expose the LDR (5 s)... dark=42 bright=910 raw: 235 pct: 22% dim raw: 236 pct: 22% dim raw: 350 pct: 35% dim raw: 612 pct: 65% bright raw: 880 pct: 96% glare
Step 4 — sanity test
- Hand cupped → should read 0–10%, label dark.
- Normal classroom lighting → should read 20–60%, label dim or bright.
- Phone torch at 5 cm → should read 90–100%, label glare.
If the labels feel wrong, re-press the reset button and redo calibration with a more dramatic dark and a stronger bright.
Step 5 — what the fallback does
The if (brightRaw - darkRaw < 50) check is a safety net for when the student forgets to do the cover-and-shine routine. Without it, calibration might fix darkRaw = 410 and brightRaw = 420 — and every reading would clip to 0% or 100% with no in-between. The fallback gracefully degrades to the full 0–1023 range so the sketch still works (just less precisely).
Try It Yourself 20 min
Goal: Add an LED on D9. Have it light up whenever the label is "dark" — your first automatic night-light idea. (The polished version is the L01-43 night-light project; this is a Level-2 calibrated revisit.)
Hint
In setup: pinMode(9, OUTPUT); In loop, after computing pct:
digitalWrite(9, (pct < 20) ? HIGH : LOW);Goal: Use the percent value to set LED brightness via PWM, not just on/off. Brighter room → dimmer LED, darker room → brighter LED. This is a true night-light that fades on as the room dims.
Hint
Use analogWrite() on a PWM pin (3, 5, 6, 9, 10 or 11). Invert the percent so high light = low LED brightness:
int led = map(pct, 0, 100, 255, 0); // dark → 255, bright → 0
analogWrite(9, led);Notice the map() call swaps its output range — it can map "forwards" or "backwards" freely. Useful trick.
Goal: Add a re-calibrate button on D2 (with INPUT_PULLUP, button between D2 and GND). When pressed, the sketch should reset darkRaw and brightRaw and re-run the 5-second calibration. Lets you re-calibrate without un-plugging.
Hint
Read the button in loop:
if (digitalRead(2) == LOW) {
darkRaw = 1023;
brightRaw = 0;
calibrate();
}This is a Level-1 reflex now: pull-up button reads LOW when pressed. The trick is calling the existing calibrate() function rather than re-implementing it.
Mini-Challenge · Smart-shade prototype 15 min
You're a sleepy student who hates harsh morning sun. Build the dumb-but-useful prototype of an "auto-curtain alarm":
- LDR on A0.
- Red LED on D9 — "sun is too bright, close the curtain".
- Buzzer on D8 — chirps every 5 seconds while "glare" lasts (warning).
Behaviour:
- 5-second calibration on boot (point a torch at it, cover it).
- While the room is "dark", "dim" or "bright" — nothing happens.
- The moment it becomes "glare", the red LED comes on and the buzzer chirps every 5 s.
- Cover the LDR (the "curtain is now closed" simulation) → LED off, buzzer stops.
It's done when:
- Pointing a torch at the LDR triggers the LED + chirp within 1 s.
- Covering the LDR clears the alert within 1 s.
- The buzzer never fires more than once every 5 s.
- The
loop()is non-blocking: nodelay(5000).
Reveal one valid sketch
const int LDR = A0;
const int LED = 9;
const int BUZZ = 8;
int darkRaw = 1023, brightRaw = 0;
unsigned long lastBeep = 0;
void calibrate() {
Serial.println("Calibrating...");
unsigned long t0 = millis();
while (millis() - t0 < 5000) {
int r = analogRead(LDR);
if (r < darkRaw) darkRaw = r;
if (r > brightRaw) brightRaw = r;
}
if (brightRaw - darkRaw < 50) { darkRaw = 0; brightRaw = 1023; }
}
void setup() {
pinMode(LED, OUTPUT);
pinMode(BUZZ, OUTPUT);
Serial.begin(9600);
calibrate();
}
void loop() {
int raw = analogRead(LDR);
int clipped = constrain(raw, darkRaw, brightRaw);
int pct = map(clipped, darkRaw, brightRaw, 0, 100);
bool glare = (pct >= 80);
digitalWrite(LED, glare ? HIGH : LOW);
if (glare && millis() - lastBeep >= 5000) {
tone(BUZZ, 1500, 80);
lastBeep = millis();
}
Serial.print(pct); Serial.print("% ");
Serial.println(glare ? "GLARE" : "ok");
delay(100);
}Same non-blocking-beep pattern as the personal thermometer (L02-12). Pattern: a state flag, a millis() gate, and the alarm fires only when both are true.
Recap 5 min
Today you upgraded the LDR from L01-36's "raw number" to a calibrated, labelled, percent-style light meter. The two new ingredients were the 5-second setup-time calibration (L02-09) and the constrain() + map() chain (L02-10). Together they turn an environment-dependent sensor into a tidy 0–100 percent reading with a friendly four-band label, ready to feed into LEDs, buzzers, or a project comparison print. The recurring pattern — read · smooth · convert · classify · act — is now operating on its third sensor in three lessons.
- LDR (photoresistor)
- A resistor whose value drops as light intensity rises. Cheap, slow, non-linear — good for "dark / dim / bright" relative readings, not for accurate lux.
- Lux
- The SI unit for illuminance — light hitting a surface. A real lux meter uses a calibrated photodiode and a colour filter; the LDR can only approximate.
- Auto-calibration
- A short setup phase where the sketch records the min and max values it sees, then uses those as the reference range for all later readings. Makes one sketch portable across rooms.
constrain(x, lo, hi)- Clips
xso it's never belowloor abovehi. Essential beforemap()when the input could exceed the calibration range. - Relative vs absolute reading
- Relative: "47% as bright as the brightest thing I've seen". Absolute: "347 lux". This sketch is relative; lux meters are absolute.
- Fallback range
- The default 0..1023 we drop back to if calibration didn't pick up a wide enough span — keeps the sketch useful even when the user skips the calibration ritual.
Homework 5 min
Build a brightness map of your home. Take the light-meter sketch to four spots:
- Your bedroom desk with the room light on.
- Same desk with only your monitor as a light source.
- The bathroom (small bulb? big window?).
- An outdoor spot in the shade (don't put the LDR in direct sun — easy to peg).
At each spot: reset the Arduino, do the cover-then-shine calibration, then record three percent readings over ~30 seconds. Compute the average for each spot.
On paper:
- Rank the four spots from darkest to brightest by average percent.
- Were any spots harder to calibrate? Why?
- If you were designing an auto-night-light for your bedroom, what percent threshold would trigger it ON? OFF? (Hint: pick a threshold that puts a real-world dim-but-not-pitch-black bedroom in the ON band.)
Bring back next class:
- Your four-spot percent table.
- Your three written answers.
- Your
hw-l02-15.inosketch — tomorrow we swap the LDR for a soil-moisture probe and reuse exactly the same calibration template.