Difficulty: Beginner | Time: 20 minutes | Cost: ~$8 (assuming you own the Arduino)

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Ω
The Default Pick: If you are building a general-purpose day/night detector or an automatic indoor nightlight, use a 10kΩ 1/4W carbon film resistor. The GL5528 LDR hovers around 10kΩ at 10 lux (typical dusk/dawn), meaning a 10kΩ fixed resistor will yield the widest, most linear voltage swing across the Arduino's 0-5V ADC range during the transition periods that actually matter.

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

  1. Insert the LDR: Place the GL5528 photoresistor across the center trench of the breadboard. Polarity does not matter; CdS cells are non-polarized.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. 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.
  2. 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.
  3. 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_SSD1306 library 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.