The most common mistake makers make with an Arduino photoresistor circuit is treating the light-dependent resistor (LDR) like a standard digital switch. A photoresistor is a variable resistor, meaning it cannot be read directly by a microcontroller's GPIO pin. You must convert its changing resistance into a changing voltage using a voltage divider, then read that voltage with the Arduino's Analog-to-Digital Converter (ADC).
Below is the exact hardware selection, wiring procedure, and production-ready code to build a stable light-sensing circuit that filters out 50/60Hz mains lighting flicker.
The Decision Path: Choosing Your Fixed Resistor
A photoresistor requires a paired fixed resistor to create a voltage divider. The value of this fixed resistor dictates the sensitivity range of your circuit. Use this decision tree to select the right resistor for your specific lighting environment.
| Target Environment | Ambient Light Level | LDR Resistance (GL5528) | Optimal Fixed Resistor |
|---|---|---|---|
| Bright Sunlight / Direct Grow Lights | 10,000+ lux | ~1kΩ - 3kΩ | 1kΩ |
| Standard Indoor Room / Office | 100 - 500 lux | ~8kΩ - 15kΩ | 10kΩ |
| Twilight / Streetlamp Detection | 10 - 50 lux | ~20kΩ - 50kΩ | 33kΩ |
| Pitch Dark / Moonlight Only | < 1 lux | ~1MΩ+ | 100kΩ |
Parts List and Pin Mapping
This build targets the Arduino Uno R3 (ATmega328P, 5V logic, 10-bit ADC). If you are using a 3.3V board like the Arduino Nano 33 IoT or an ESP32, you must scale the voltage divider to avoid exceeding the 3.3V ADC maximum, and adjust the code's maximum ADC value from 1023 to 4095.
Required Components
- Microcontroller: Arduino Uno R3 (or compatible ATmega328P clone)
- Sensor: GL5528 CdS Photoresistor (Peak wavelength 540nm, 10-20kΩ @ 10 lux)
- Fixed Resistor: 10kΩ 1/4W (Color code: Brown-Black-Orange-Gold)
- Hardware: Half-size breadboard, male-to-male jumper wires
Pin Mapping Table
| Component | Component Leg / Pin | Arduino Uno R3 Pin | Wire Color (Suggested) |
|---|---|---|---|
| GL5528 LDR | Leg 1 | 5V | Red |
| GL5528 LDR | Leg 2 | Analog A0 (via junction) | Yellow |
| 10kΩ Resistor | Leg 1 | Analog A0 (via junction) | Yellow |
| 10kΩ Resistor | Leg 2 | GND | Black |
Step-by-Step Wiring Procedure
- Insert the LDR: Place the GL5528 photoresistor across the center trench of the breadboard. Polarity does not matter; CdS cells are non-polarized.
- Insert the Fixed Resistor: Place one leg of the 10kΩ resistor in the same row as one leg of the LDR. Place the other leg in an empty row on the negative (GND) rail side of the trench.
- Create the Analog Tap: Use a yellow jumper wire to connect the shared row (where the LDR and 10kΩ resistor meet) to the Arduino's A0 pin. This is your signal output.
- Connect Power: Run a red jumper from the Arduino 5V pin to the positive rail, and connect it to the free leg of the LDR.
- Connect Ground: Run a black jumper from the Arduino GND pin to the negative rail, and connect it to the free leg of the 10kΩ resistor.
Physics Check: When light hits the LDR, its resistance drops. This allows more current to flow through the LDR and the 10kΩ resistor to ground. According to Ohm's Law, the voltage drop across the fixed 10kΩ resistor increases, pushing the voltage at pin A0 closer to 5V (ADC value ~1023). In the dark, the LDR's resistance spikes, starving the circuit of current, and the voltage at A0 drops toward 0V (ADC value ~0).
Complete Arduino Code with Error Handling
Beginner tutorials often use a raw analogRead() inside the main loop. In the real world, this causes two problems: serial monitor spam, and relay flutter caused by 50Hz/60Hz AC mains flicker from overhead fluorescent or LED room lighting. The code below implements a rolling average array to smooth out AC flicker and includes bounds-checking to prevent phantom triggers.
// Target Board: Arduino Uno R3 (ATmega328P)
// Sensor: GL5528 Photoresistor with 10k pull-down
#define LDR_PIN A0
#define SAMPLE_SIZE 20 // Number of samples to average (filters 50/60Hz flicker)
#define SAMPLE_DELAY 2 // Milliseconds between samples
#define DARK_THRESHOLD 300 // ADC value below which we consider it "dark"
#define LIGHT_THRESHOLD 700 // ADC value above which we consider it "light"
int samples[SAMPLE_SIZE];
int sampleIndex = 0;
long totalValue = 0;
void setup() {
Serial.begin(115200);
pinMode(LDR_PIN, INPUT);
// Pre-fill the array to prevent initial calculation errors
int initialRead = analogRead(LDR_PIN);
for (int i = 0; i < SAMPLE_SIZE; i++) {
samples[i] = initialRead;
totalValue += initialRead;
}
Serial.println("Arduino Photoresistor System Initialized.");
}
void loop() {
// 1. Subtract the oldest sample from the running total
totalValue -= samples[sampleIndex];
// 2. Read the new value and store it in the array
int currentRead = analogRead(LDR_PIN);
// Error handling: Check for disconnected/floating pin
// A completely dark room rarely drops below 10 on a 5V Uno with a 10k divider
// A floating pin will often jitter wildly or stick to extreme rails
if (currentRead < 0 || currentRead > 1023) {
Serial.println("ERROR: ADC Out of Bounds. Check wiring.");
delay(1000);
return;
}
samples[sampleIndex] = currentRead;
totalValue += currentRead;
// 3. Advance the index, wrapping around if necessary
sampleIndex = (sampleIndex + 1) % SAMPLE_SIZE;
// 4. Calculate the smoothed average
int smoothedValue = totalValue / SAMPLE_SIZE;
// 5. Hysteresis logic to prevent relay flutter at the threshold boundary
static bool isDark = false;
if (!isDark && smoothedValue < DARK_THRESHOLD) {
isDark = true;
Serial.print("STATE CHANGE: DARK (ADC: ");
Serial.print(smoothedValue);
Serial.println(") - Triggering Nightlight");
}
else if (isDark && smoothedValue > LIGHT_THRESHOLD) {
isDark = false;
Serial.print("STATE CHANGE: LIGHT (ADC: ");
Serial.print(smoothedValue);
Serial.println(") - Turning Off Nightlight");
}
// Optional: Print raw vs smoothed for debugging (throttled)
if (millis() % 500 == 0) {
Serial.print("Raw: "); Serial.print(currentRead);
Serial.print(" | Smoothed: "); Serial.println(smoothedValue);
}
delay(SAMPLE_DELAY);
}
Source Reference: For more on how the Arduino ADC maps voltages, consult the official Arduino analogRead() documentation.
Debugging: When analogRead() Returns 0 or 1023
If your serial monitor outputs a flat line of 1023 or 0 regardless of how much light you shine on the sensor, your circuit has a hard fault. Do not rewrite your code; the issue is physical.
The First Three Things to Check
- Measure the Fixed Resistor: Pull the 10kΩ resistor out of the breadboard and measure it with a multimeter. Breadboard contacts can bend resistor legs, or you may have accidentally grabbed a 100kΩ or 10Ω resistor from your kit.
- Verify the Analog Tap: Ensure the jumper wire to A0 is in the exact same breadboard row as the junction where the LDR and fixed resistor meet. If it is one row off, you are reading a floating pin.
- Check for Rail Shorts: Use your multimeter's continuity mode to check if the A0 row is accidentally shorted to the 5V or GND rail via a stray wire strand inside the breadboard.
Ranked Causes for Stuck ADC Values
| Symptom String | Most Likely Cause | The Fix |
|---|---|---|
Smoothed: 1023 (Stuck High) |
Fixed resistor is missing, broken, or not connected to GND. The LDR is pulling A0 directly to 5V. | Reseat the 10kΩ resistor. Verify continuity from the resistor's ground leg to the Arduino GND pin. |
Smoothed: 0 (Stuck Low) |
LDR is missing, broken, or not connected to 5V. The 10kΩ resistor is pulling A0 directly to GND. | Reseat the LDR. Verify 5V is reaching the top breadboard rail and the LDR leg. |
Smoothed: 340 (Stuck Mid-Rail) |
Breadboard internal short, or reading from the wrong analog pin (e.g., A1 instead of A0). | Move the circuit to a completely different section of the breadboard. Verify #define LDR_PIN A0 matches the physical wire. |
For a deeper dive into how voltage dividers behave under fault conditions, review the All About Circuits guide on voltage divider circuits.
Scaling Up: Extending or Simplifying the Build
Once you have the baseline Arduino photoresistor circuit working, you will eventually need to adapt it for specific project constraints. Here is how to pivot based on your end goal.
How to Simplify (No Code Required)
If you only need a digital HIGH/LOW signal to trigger a relay when the sun goes down, drop the Arduino entirely. Purchase an LM393 Light Sensor Module (typically $2-$4). These modules feature a built-in photoresistor, a fixed resistor, and an LM393 comparator chip with a trimpot. You simply turn the trimpot with a small screwdriver to set your exact lux threshold, and the module outputs a clean 5V or 0V digital signal that can drive a transistor or logic gate directly.
How to Extend (IoT and Displays)
If you need to log light levels over time or view them without a PC:
- Add an I2C Display: Wire an SSD1306 128x64 OLED display to the I2C pins (A4/A5 on the Uno). Use the
Adafruit_SSD1306library to render a real-time bar graph of the smoothed ADC value. - Port to ESP32 for MQTT: If moving this to an ESP32 for home automation, remember that the ESP32's ADC is non-linear and operates at 3.3V. You must change the fixed resistor to 4.7kΩ to keep the voltage divider output safely below 3.3V in bright sunlight, and update the code's maximum ADC constant from 1023 to 4095. You can then publish the smoothed lux value to an MQTT broker like Mosquitto for integration with Home Assistant.
For more advanced sensor linearization techniques and alternative LDR circuit topologies, the Electronics Tutorials photoresistor guide provides excellent schematics for op-amp buffering when driving heavy loads.






