Learning Goals 5 min
By the end of this lesson you will be able to:
- Explain what a light-dependent resistor (LDR) does — its resistance changes with brightness, high in the dark and low in light — and wire one with a fixed resistor as a voltage divider so the Arduino can measure the change.
- Use
analogRead(A0)to read a voltage from an analog input pin as a number from 0 to 1023, and print live values to the Serial Monitor to see brightness change in real time. - Compare the reading against a threshold with an
ifto drive an LED — your first sensor → decision → actuator loop, the basic shape of every reactive device.
Warm-Up 10 min
Until now your sketches have only been able to act on the world — switching LEDs, beeping the buzzer, printing text. You've taken simple input from buttons (HIGH or LOW — two states), but never anything continuous. Cluster F changes that. From this lesson on, the Arduino starts perceiving the world: brightness, temperature, distance, sound — all as numbers from a sensor.
Quick-fire puzzle
Walk past a streetlamp at dusk. As the sky darkens, it switches on automatically — no one flicked a switch. Same with the keyboard backlight on a laptop: it brightens when you're in a dim café and turns off when sunlight floods the table.
- What does the streetlamp need to know to decide when to switch on?
- Could you build that switching logic out of any of the components you've used so far (LEDs, resistors, buttons, buzzers)? Why not?
- What kind of new component would you add — and roughly what would its "interesting property" be?
Reveal the answer
- It needs to know how bright it is outside — and more specifically, whether the brightness is below some "switch on" level.
- No. Nothing you've used measures the environment. LEDs emit light, they don't sense it. Buttons are two-state human input. Resistors and buzzers have no input at all.
- You'd add a part whose electrical behaviour changes with light. That's exactly what a light-dependent resistor (LDR) is: a chip of light-sensitive material that conducts more easily when light hits it. Bright = low resistance, dark = high resistance. Today you wire one up and let the Arduino read its value as a number.
New Concept — the LDR, the voltage divider, and analogRead 15 min
The big idea — turning brightness into a number
Three things have to happen in sequence to get a brightness reading into your sketch:
- The LDR turns brightness into a resistance (in ohms).
- A voltage divider turns that resistance into a voltage (in volts, between 0 and 5).
analogReadturns the voltage into a number (between 0 and 1023) that your sketch can use.
You're already comfortable with steps 1 and 3 conceptually. Step 2 is new — and it's the trick that makes every analog sensor in this course (LDRs, thermistors, potentiometers, soil-moisture probes, force sensors) work the same way.
The LDR itself
A light-dependent resistor (also called a photoresistor) is a small disc — typically 5–10 mm across — with a wavy black pattern on top of a light-sensitive material called cadmium sulphide. It has two leads, no polarity (works in either direction). Typical values:
- Bright room: maybe 1–10 kΩ resistance
- Pitch dark (cover it with your finger): 100 kΩ to over 1 MΩ
So the resistance can swing by a factor of 100× or more between "lit" and "covered". That's a huge range — easy to detect, easy to act on.
The voltage divider — turning resistance into voltage
The Arduino can't read "resistance" directly. Its analog input only reads voltage. To convert one to the other, we pair the LDR with a fixed resistor (typically 10 kΩ) in a circuit called a voltage divider:
The intuition: imagine two pipes stacked vertically, water pressure (voltage) at the top. The narrower pipe loses more pressure across it. If the LDR is "narrow" (high resistance, dark), it eats most of the 5 V and the junction sits near 0 V. If the LDR is "wide" (low resistance, bright), the fixed 10 kΩ resistor eats most of the pressure and the junction sits near 5 V.
analogRead — turning voltage into a 0–1023 number
The Arduino Uno has six dedicated analog input pins, labelled A0 through A5. Each one feeds an internal chip called an analog-to-digital converter (ADC) that measures the pin's voltage and maps it to an integer:
int reading = analogRead(A0); // returns 0..1023- 0 V on the pin → reads
0. - 5 V on the pin → reads
1023. - 2.5 V (halfway) → reads about
511.
The number has 1024 possible values (0–1023, hence "10-bit" — 2¹⁰ = 1024). That's about a thousand brightness levels — plenty to distinguish "dark room", "lamp on", "sunlight through window", "torch shone right at it".
One detail: analogRead takes about 100 microseconds per call. Fine for hundreds of reads per second, but don't put it inside a really tight loop that's also doing other heavy work — Level 2 covers timing optimisation properly.
The sensor → decision → actuator loop
Now you can write the most fundamental shape in all of embedded code:
int light = analogRead(A0); // SENSE the world
if (light < 300) { // DECIDE based on the reading
digitalWrite(9, HIGH); // ACT on the decision
}
else {
digitalWrite(9, LOW);
}That's a nightlight in five lines. Every reactive device in your house — thermostat, smoke alarm, automatic door, robot vacuum, self-driving car — runs some version of this loop, thousands of times per second. Today's sketch is the smallest possible version.
Why it matters
This lesson is the gateway to everything. Every sensor in the rest of this syllabus (temperature, distance, sound, soil moisture, motion, pressure) plugs into analogRead in essentially the same way. Once you've wired and read one analog sensor, you can wire and read any of them. The component changes; the pattern doesn't.
Worked Example — wire it, read it, react 20 min
Step 1 — Wire the voltage divider on the breadboard
The LDR sits between the breadboard's + rail (which jumpers to 5V on the Arduino) and a free column. The 10 kΩ resistor sits between that same column and the − rail (which jumpers to GND). A single signal wire taps the junction and goes to A0. That's it — three components, four connections.
Step 2 — Print the live reading
Your first sketch: read A0 every 200 ms and print the value. Open the Serial Monitor, watch numbers stream past, and observe what happens as you cover and uncover the LDR with your hand.
// Live LDR reading — see brightness as numbers
void setup() {
Serial.begin(9600);
}
void loop() {
int light = analogRead(A0);
Serial.print("light = ");
Serial.println(light);
delay(200);
}Upload. Open the Serial Monitor at 9600 baud. You should see a stream of values like:
light = 743
light = 740
light = 738
light = 130 ← (covered the LDR with your hand)
light = 95
light = 85
light = 720 ← (uncovered)
light = 738The reading swings dramatically when you cover and uncover the LDR. Note your room's numbers — what's the "dark" floor, what's the "bright" ceiling? Pick a number in the middle of the swing as your threshold. For most indoor setups, somewhere around 300 is a reasonable "dark enough" cut-off.
Step 3 — Add an LED that reacts
Add one LED on D9 with a 220 Ω resistor (the L01-07 circuit). Now make it a nightlight: the LED turns on when the room is dark, off when it's bright.
// Nightlight — LED on when dark
const int LED_PIN = 9;
const int THRESHOLD = 300; // pick from your room's numbers!
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int light = analogRead(A0);
Serial.println(light);
if (light < THRESHOLD) {
digitalWrite(LED_PIN, HIGH);
}
else {
digitalWrite(LED_PIN, LOW);
}
delay(100);
}Upload. Cover the LDR with your hand: the LED lights up. Uncover it: the LED turns off. That's a sensor → decision → actuator loop — the most universal pattern in embedded code, written in 15 lines.
Trace one decision on paper
Fill in what the sketch does at each reading, assuming THRESHOLD = 300:
light value | Comparison light < 300 | Branch taken | LED state |
|---|---|---|---|
| 742 | false | else | off |
| 300 | ____ (careful — is it < or =?) | ____ | ____ |
| 299 | ____ | ____ | ____ |
| 85 | ____ | ____ | ____ |
The row at light = 300 is the trickiest — < is strict, so 300 itself doesn't count as "below". If you wanted 300 to switch the LED on, you'd use <=. Tiny operator choice, real boundary behaviour — worth knowing for every threshold you'll write from here on.
Try It Yourself — three brightness sketches 20 min
Goal: A brightness logger. Same wiring; print one reading every second; label each reading with how it compares to four bands: "very dark", "dim", "bright", "very bright". Pick cut-offs based on what you actually see in your room.
void setup() { Serial.begin(9600); }
void loop() {
int light = analogRead(A0);
Serial.print(light);
Serial.print(" — ");
if (light < 150) Serial.println("very dark");
else if (light < 400) Serial.println("dim");
else if (light < 700) Serial.println("bright");
else Serial.println("very bright");
delay(1000);
}Questions:
- Watch the Monitor for 30 seconds. Are the cut-off values right for your room, or are you stuck in only one or two bands? What numbers would split your room better? ____
- What's the highest reading you can produce by shining a phone torch directly at the LDR? Is it close to 1023 or much lower? ____
- Why do we use
else ifinstead of a chain of bareifs? ____ (Hint: L01-20.)
Goal: A brightness-mirroring LED. The LED on D9 gets brighter as the room gets darker — like a stage spotlight that compensates for ambient light. Use analogWrite (L01-31), not digitalWrite.
Plan: read the LDR (0–1023). Map that to an LED brightness 0–255, inverted: a high reading (bright room) should give a low PWM value (LED dim), and vice versa. Use the formula:
int brightness = 255 - (light / 4); // inverts and scales 1023→255
if (brightness < 0) brightness = 0; // safety clamp
analogWrite(LED_PIN, brightness);Why / 4? Because 1023 ÷ 4 is about 255 — the LDR's range gets squeezed into the PWM range with simple integer division.
Questions:
- What does the LED do when you cover the LDR completely? When you shine a torch on it? Describe the smoothness — is it gradual or stepped? ____
- Why is the safety clamp
if (brightness < 0)there? When couldlight / 4ever exceed 255? ____ (Hint: max possiblelightis 1023; 1023 ÷ 4 is 255 with no remainder. The clamp's mostly defensive.) - Arduino has a built-in
map(value, fromLow, fromHigh, toLow, toHigh)function. How would you rewrite the formula using it? ____
Goal: A self-calibrating nightlight. The threshold isn't hardcoded; the sketch watches the LDR for 5 seconds at startup, finds the highest and lowest readings, then uses the midpoint as the cut-off. After that, the nightlight works as in the worked example.
Plan: in setup(), after Serial.begin, run a 5-second calibration loop that updates minLight and maxLight on each reading. Print them when done. Compute the threshold as their average. Then loop() runs the normal nightlight logic using the discovered threshold.
const int LED_PIN = 9;
int threshold = 500; // will be overwritten in setup
void calibrate() {
int minLight = 1023;
int maxLight = 0;
unsigned long start = millis();
while (millis() - start < 5000) {
int v = analogRead(A0);
if (v < minLight) minLight = v;
if (v > maxLight) maxLight = v;
}
threshold = (minLight + maxLight) / 2;
Serial.print("min="); Serial.print(minLight);
Serial.print(" max="); Serial.print(maxLight);
Serial.print(" threshold="); Serial.println(threshold);
}
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
Serial.println("Calibrating — cover and uncover the LDR for 5 seconds...");
calibrate();
}
void loop() {
int light = analogRead(A0);
digitalWrite(LED_PIN, light < threshold ? HIGH : LOW);
delay(50);
}During the 5-second calibration, deliberately cover the LDR and shine your phone torch at it so both extremes get captured.
Questions:
- Why initialise
minLight = 1023andmaxLight = 0, not the other way round? ____ (Hint: the first reading should always replace both.) - This sketch survives moving to a different room without code changes. Why is that a big deal compared to the hardcoded-threshold version? ____
- What happens if the user doesn't cover the LDR during calibration — only the bright readings are captured? Does the nightlight still work? ____
Mini-Challenge — the ASCII brightness bar 10 min
"See the value as a bar, not just a number"
Build a sketch that prints the LDR reading as a horizontal bar of hashes in the Serial Monitor — like a tiny live brightness meter. A reading of 0 prints nothing; 1023 prints 40 hashes; values in between scale linearly.
light = 120 [### ]
light = 430 [################# ]
light = 742 [############################# ]
light = 995 [####################################### ]Your task:
- Read the LDR with
analogRead(A0). - Compute how many hashes to print:
int bar = light / 26;— gives 0 to ~39 hashes (because 1023 ÷ 26 ≈ 39). - Print the label
"light = "+ the value +" [". - Use a
forloop to printbarhash characters withSerial.print('#');. - Print a closing
"]"and a newline. delay(150)between readings — fast enough to feel live, slow enough to read.
It works if:
- The Monitor scrolls a steady stream of bars whose lengths track the LDR's brightness in real time.
- Covering the LDR shrinks the bar to almost nothing within a single reading.
- Shining a torch on it grows it to nearly full width.
Reveal one valid sketch
// ASCII brightness bar — see the value, not just the number
void setup() {
Serial.begin(9600);
}
void loop() {
int light = analogRead(A0);
int bar = light / 26; // 0..39 hashes
Serial.print("light = ");
Serial.print(light);
Serial.print(" [");
for (int i = 0; i < bar; i = i + 1) {
Serial.print('#');
}
for (int i = bar; i < 40; i = i + 1) {
Serial.print(' ');
}
Serial.println("]");
delay(150);
}The second for loop fills the rest of the bar with spaces so the closing ] always lines up in the same column — this is what makes the live update feel like a "meter" instead of jagged text. The trick of visualising a sensor value live in the Monitor is one of the most useful debugging moves you'll learn in this course; you'll reach for it on every analog sensor in the rest of the syllabus.
Recap 5 min
The Arduino can now sense the world. An LDR is a resistor that varies with brightness (low resistance when lit, high when dark). Paired with a fixed 10 kΩ resistor as a voltage divider, it turns brightness into a voltage at a chosen junction. analogRead(A0) measures that voltage and returns a number from 0 to 1023. Compare the number with a threshold using if, decide an action, drive an LED or buzzer — that's the sensor → decision → actuator loop, the universal shape of every reactive embedded device. Print live readings to the Monitor first; pick thresholds from your own data, not from a tutorial; never trust a hardcoded number until you've seen the actual values from your room.
- Sensor
- A component that turns a physical property of the world (brightness, temperature, distance, pressure, motion) into an electrical signal a microcontroller can read. The opposite of an actuator.
- Light-dependent resistor (LDR)
- A small light-sensitive resistor whose value drops as more light hits it. Cheap, no polarity, easy to wire — the perfect "first sensor".
- Voltage divider
- Two resistors in series between +5 V and GND, with the analog pin tapping the junction. The voltage at the junction depends on the ratio of the two resistances. The standard recipe for turning any variable resistor (LDR, thermistor, force sensor) into an analog-readable voltage.
- Analog input
- An Arduino pin that can read a continuous voltage from 0 to 5 V, as opposed to digital pins which only sense HIGH or LOW. On the Uno: A0–A5.
- Analog-to-digital converter (ADC)
- The chip inside the Arduino that converts an analog voltage to a 10-bit integer (0–1023). One call to
analogReadtriggers one conversion, which takes about 100 µs. - Sensor → decision → actuator loop
- The fundamental shape of reactive code: read a sensor, branch on the reading, drive an output. Every thermostat, smoke alarm, robot vacuum and self-driving car runs some version of this loop thousands of times per second.
- Threshold / calibration
- A cut-off value used to turn a continuous reading into a binary decision ("dark enough?"). Always pick thresholds from your own setup's readings; the same code in a different room can behave completely differently with the wrong threshold.
Homework 5 min
The brightness-driven mood lamp. Combine today's LDR + the L01-30 RGB LED into a "room ambience" device: when the room is bright, the RGB stays off; as the room dims, it glows a warm orange; in near-darkness, a calm blue. Three brightness bands → three colours.
- Reuse the L01-30 RGB wiring (D9/D10/D11). Add the today's LDR + 10 kΩ voltage divider on A0. The two circuits share GND through the − rail.
- Print the live LDR value to the Monitor so you can pick band boundaries for your room.
- Pick three colours, one per band:
- Bright (light > high cut-off): RGB off —
setColour(0, 0, 0). - Dim (mid cut-off < light < high cut-off): warm orange —
setColour(255, 100, 0). - Dark (light < mid cut-off): calm blue —
setColour(0, 80, 200).
- Bright (light > high cut-off): RGB off —
- Use the worked example's
setColour(r, g, b)helper and anif/else if/elsechain.
Also: a design reflection on paper.
- What two LDR readings did you pick as your "bright" and "dim" cut-offs, and how did you choose them? ____
- The lamp currently snaps between colours when the reading crosses a boundary. How could you make the transitions smooth instead, using ideas from L01-31? ____ (Hint: don't pick from three preset colours — interpolate between them based on the reading.)
- What real-world product behaves like this? (Two examples: phone display auto-brightness, hotel-room auto-dimming bedside lamp.) Pick one and describe how it probably uses its LDR. ____
- If you wanted the lamp to only change once it's been dim for 30 seconds (not flicker on every cloud passing the window), how would you do that with the tools you have? ____ (Hint:
millis()timing — debouncing in slow motion.)
Bring back next class:
- The saved
.inofile (call itbrightness-mood-lamp). - A 30-second phone video showing all three states (cover the LDR partially for dim, fully for dark, uncover for bright).
- Your four written reflection answers, in your notebook.
Heads up for next class: L01-37 "Analog vs Digital Pins" steps back to make today's leap explicit — the difference between a pin that's set to a state (HIGH/LOW) and a pin that measures a voltage (0–1023). With that clean in your head, the rest of Cluster F's sensors will all feel familiar.