To read an LDR (Light Dependent Resistor) with an Arduino, wire it in a voltage divider circuit using a 10kΩ pull-down resistor and read the midpoint voltage on an analog pin (like A0). The Arduino's 10-bit ADC will convert this voltage into a value between 0 and 1023, which you can map to light intensity or use to trigger a relay. Because analog sensors are prone to noise and threshold chatter, your code must include a moving average filter and hysteresis logic to ensure stable operation.
LDR Specifications and Voltage Divider Math
The most common photoresistor for hobbyist projects is the GL5528. It is a cadmium sulfide (CdS) cell that drops in resistance as ambient light increases. To interface this variable resistance with the Arduino's analog-to-digital converter (ADC), we must convert the resistance change into a voltage change using a voltage divider.
We pair the LDR with a fixed 10kΩ resistor. Why 10kΩ? The GL5528 has a dark resistance of ~1MΩ and a light resistance of ~1kΩ. Its midpoint resistance under typical indoor lighting (around 10-50 lux) is roughly 10kΩ. Matching the pull-down resistor to the sensor's midpoint resistance yields the widest voltage swing across the ADC's readable range.
GL5528 Photoresistor Data Sheet Characteristics
| Parameter | Test Condition | Min Value | Typical Value | Max Value |
|---|---|---|---|---|
| Dark Resistance | 0 Lux (Covered) | 1.0 MΩ | 3.0 MΩ | 10.0 MΩ |
| Light Resistance (10 Lux) | 10 Lux (Dim Room) | 8.0 kΩ | 14.0 kΩ | 20.0 kΩ |
| Light Resistance (100 Lux) | 100 Lux (Office) | 1.5 kΩ | 2.5 kΩ | 3.5 kΩ |
| Peak Spectral Response | Wavelength | - | 540 nm | - |
| Response Time | Rise / Fall | - | 20ms / 30ms | - |
Worked Numeric Example:
Let's calculate the ADC reading under standard office lighting (100 Lux). At 100 Lux, the GL5528 resistance ($R_{LDR}$) is approximately 2.5kΩ. Our pull-down resistor ($R_{pull}$) is 10kΩ. Supply voltage ($V_{in}$) is 5.0V.
The voltage at the analog pin ($V_{out}$) is calculated as:
$V_{out} = V_{in} \times \frac{R_{pull}}{R_{LDR} + R_{pull}}$
$V_{out} = 5.0 \times \frac{10000}{2500 + 10000} = 5.0 \times 0.8 = 4.0V$
The Arduino Uno R3 uses a 10-bit ADC (1024 steps). The digital reading is:
$Reading = \frac{4.0V}{5.0V} \times 1023 = 818$
For a deeper dive into the math behind this circuit, review the voltage divider circuits guide on All About Circuits.
Parts List and Pin Mapping
This build targets the Arduino Uno R3 (ATmega328P). If you are using the newer Uno R4 Minima, note that it features a 14-bit ADC (16384 steps), which will require adjusting the math in the code block below.
Required Components
- Microcontroller: Arduino Uno R3 (or compatible clone with ATmega328P)
- Sensor: GL5528 Photoresistor (approx. $0.15/ea in bulk)
- Resistor: 10kΩ 1/4W carbon film pull-down resistor
- Calibration (Optional): 10kΩ linear taper trimpot (B103) to replace the fixed resistor for physical threshold tuning
- Output: 5V relay module or standard LED with 220Ω current-limiting resistor
- Hardware: Half-size breadboard, male-to-male jumper wires
Pin Mapping Table
| Component Pin | Arduino Uno R3 Pin | Function |
|---|---|---|
| LDR Leg 1 | 5V | Sensor excitation voltage |
| LDR Leg 2 | A0 | Analog signal input (Voltage Divider Midpoint) |
| LDR Leg 2 | Shared with A0 | Connected to Pull-down Resistor Leg 1 |
| 10kΩ Resistor Leg 2 | GND | Circuit ground reference |
| Relay IN / LED Anode | D8 | Digital output control |
Step-by-Step Wiring Procedure
- Place the LDR: Insert the two legs of the GL5528 into adjacent rows on the breadboard (e.g., Row 10, Columns A and B).
- Wire the Excitation: Run a jumper from the Arduino 5V pin to Row 10, Column A (LDR Leg 1).
- Create the Midpoint: Insert one leg of the 10kΩ resistor into Row 10, Column B (sharing the hole with LDR Leg 2). Insert the other leg of the resistor into an empty row (e.g., Row 15, Column B).
- Wire the Analog Input: Run a jumper from Row 10, Column B to the Arduino A0 pin.
- Complete the Ground: Run a jumper from Row 15, Column B (Resistor Leg 2) to the Arduino GND pin.
- Connect the Output: Wire your relay module VCC to 5V, GND to GND, and the IN pin to Arduino D8.
Compilable Arduino Code with Hysteresis
Raw analogRead() values fluctuate by ±5 counts due to internal ADC noise and power supply ripple. If you use a simple if (val > 500) statement to trigger a relay at dusk, the relay will chatter rapidly as the light level hovers around 500. The code below implements a moving average filter to smooth the noise and hysteresis to create dead-bands for switching. For more on reading analog signals cleanly, see the official Arduino analogRead() documentation.
// Target Board: Arduino Uno R3 (ATmega328P)
// LDR Arduino Hysteresis and Smoothing Example
#define LDR_PIN A0
#define RELAY_PIN 8
// Thresholds for hysteresis (0-1023 scale)
#define THRESHOLD_DARK 400 // Turn ON relay when it gets darker than this
#define THRESHOLD_LIGHT 450 // Turn OFF relay when it gets lighter than this
// Moving average filter parameters
const int NUM_READINGS = 10;
int readings[NUM_READINGS];
int readIndex = 0;
long total = 0;
int average = 0;
bool relayState = false;
void setup() {
Serial.begin(9600);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Ensure relay is off at boot
// Initialize the readings array to 0
for (int i = 0; i < NUM_READINGS; i++) {
readings[i] = 0;
}
}
void loop() {
// 1. Read the sensor and apply moving average filter
total = total - readings[readIndex];
int rawValue = analogRead(LDR_PIN);
// Error handling: Check for disconnected sensor (stuck at 0 or 1023)
if (rawValue <= 2 || rawValue >= 1021) {
Serial.print("WARNING: Sensor Value: ");
Serial.print(rawValue);
Serial.println(" (Possible open/short circuit. Check wiring.)");
}
readings[readIndex] = rawValue;
total = total + readings[readIndex];
readIndex = (readIndex + 1) % NUM_READINGS;
average = total / NUM_READINGS;
// 2. Apply Hysteresis Logic
// Note: Higher analogRead values mean MORE light in this specific wiring topology
// because the LDR is on top and the pull-down is on the bottom.
// Wait, let's verify topology: LDR to 5V, Resistor to GND.
// Vout = 5V * (R_pull / (R_LDR + R_pull)).
// As light increases, R_LDR drops, Vout INCREASES.
// So High Value = Bright, Low Value = Dark.
if (average < THRESHOLD_DARK && relayState == false) {
// It is dark, turn on the light/relay
digitalWrite(RELAY_PIN, HIGH);
relayState = true;
Serial.println("STATE CHANGE: Relay ON (Dark)");
}
else if (average > THRESHOLD_LIGHT && relayState == true) {
// It is bright, turn off the light/relay
digitalWrite(RELAY_PIN, LOW);
relayState = false;
Serial.println("STATE CHANGE: Relay OFF (Light)");
}
// 3. Output telemetry for Serial Plotter
Serial.print("Raw:");
Serial.print(rawValue);
Serial.print(" | Avg:");
Serial.print(average);
Serial.print(" | Relay:");
Serial.println(relayState ? "ON" : "OFF");
delay(50); // 50ms loop delay for stability
}
Debugging: "Sensor Value: 1023 or 0 (Stuck)"
When testing LDR circuits, the most common failure mode is a locked ADC reading. If your serial monitor repeatedly outputs the exact string WARNING: Sensor Value: 1023 (Possible open/short circuit. Check wiring.) or WARNING: Sensor Value: 0, your microcontroller is not seeing the voltage divider midpoint.
The First Three Things to Check
- Breadboard Rail Continuity: Use a digital multimeter (DMM) in continuity mode. Place one probe on the Arduino 5V pin and the other on the breadboard row where the LDR leg is inserted. A lack of a beep indicates a broken internal breadboard clip or a split power rail.
- Pull-Down Resistor Verification: Remove the 10kΩ resistor from the board and measure it with your DMM. Color-code bands can be misread (e.g., confusing a 10kΩ with a 100kΩ or 1kΩ). If you accidentally used a 1MΩ resistor, the ADC will read near 0 in normal lighting.
- Analog Pin Physical Connection: Jumper wires frequently suffer from internal strand breakages right at the crimp. Swap the jumper wire connecting the midpoint to A0 with a known-good wire.
Ranked Causes for Stuck ADC Readings
| Symptom (Serial Output) | Most Likely Cause | Secondary Cause | Fix / Measurement Threshold |
|---|---|---|---|
Stuck at 1023 |
Pull-down resistor is missing, disconnected, or blown open. | GND wire is disconnected; A0 pin is shorted to 5V. | Measure resistance from midpoint node to GND. Must read ~10kΩ. |
Stuck at 0 |
LDR is missing, or 5V excitation wire is disconnected. | Midpoint node is shorted directly to GND. | Measure voltage at LDR Leg 1. Must read 4.8V - 5.2V. |
| Erratic jumps (e.g., 100 to 900) | Floating analog pin (loose jumper wire at A0). | Heavy EMI from a nearby motor or unfiltered switching power supply. | Add a 0.1µF ceramic capacitor between A0 and GND to filter high-frequency noise. |
Extending and Simplifying the Build
How to Simplify the Circuit
If you do not need granular light measurements and only need a simple "day/night" binary switch, replace the bare GL5528 and resistor with a pre-packaged Digital LDR Module (often sold as an LM393 photoresistor module for ~$1.50). These modules include a built-in trimpot and comparator. You wire the module's D0 (Digital Out) pin directly to an Arduino digital pin (e.g., D2) and use digitalRead(). This eliminates the need for ADC math, moving averages, and software hysteresis entirely, though you sacrifice the ability to measure actual lux levels.
How to Extend the Project
For advanced data logging, migrate the code to an ESP32 DevKit V1. The ESP32 allows you to push the LDR readings to an MQTT broker over WiFi for integration with Home Assistant. However, be aware of the ESP32's ADC limitations: it uses a 12-bit ADC (0-4095 range), but it is notoriously non-linear at the extreme high and low ends of the voltage scale. Furthermore, the ESP32 GPIO pins are 3.3V tolerant. You must change your voltage divider to ensure the maximum output voltage does not exceed 3.3V, or you risk permanently damaging the ESP32's internal ADC circuitry. Swap the 10kΩ pull-down for a 6.8kΩ resistor to safely shift the voltage curve downward.
Finally, consider adding an I2C OLED display (SSD1306, 128x64) to visually output the calculated Lux value in real-time, turning your breadboard prototype into a standalone benchtop light meter.






