The Core Challenge: Reading Analog Light with an LDR and Arduino

To successfully interface an LDR and Arduino, you must solve a fundamental hardware mismatch: the Arduino’s Analog-to-Digital Converter (ADC) reads voltage (0V to 5V), but a Light Dependent Resistor (LDR) only changes resistance. You cannot wire an LDR directly to an analog pin and expect a reading. Instead, you must build a voltage divider circuit to translate the changing resistance into a proportional voltage.

The most common hobbyist LDR is the GL5528. It is a cadmium sulfide (CdS) photoresistor with a spectral peak of 540nm, meaning it closely mimics human eye sensitivity to visible light and is largely blind to infrared. However, its resistance curve is highly non-linear and logarithmic. Understanding this curve is the difference between a project that reliably turns on your lights at dusk and one that triggers randomly when a cloud passes.

GL5528 Calibration: Lux vs. Expected ADC Values

Before writing a single line of code, you need to know what numbers the Arduino will actually spit out. The table below maps real-world illuminance (Lux) to the expected 10-bit ADC values (0-1023) when using a standard 5V Arduino Uno R3 and a 10kΩ pull-down resistor. This data is derived from the GL5528 datasheet and calculated using the standard voltage divider formula: Vout = Vin * (R2 / (R1 + R2)).

Environment Illuminance (Lux) GL5528 Resistance Voltage at A0 Expected ADC (10-bit)
Pitch Black (Covered) < 1 Lux > 1,000,000 Ω ~0.00V 0 - 2
Dim Living Room 10 Lux ~15,000 Ω 2.00V ~409
Office / Retail Space 100 Lux ~2,500 Ω 1.00V ~204
Overcast Daylight 1,000 Lux ~400 Ω 0.19V ~39
Direct Sunlight 10,000+ Lux ~50 Ω 0.025V ~5

Why a 10kΩ resistor? In a voltage divider, you get the maximum voltage swing (resolution) when the fixed resistor matches the mid-range resistance of the sensor. Since the GL5528 hovers around 5kΩ to 15kΩ in typical indoor lighting, a 10kΩ fixed resistor provides the steepest voltage change exactly where you need it for indoor automation. If you are building an outdoor sun-tracker, you would drop the fixed resistor to 1kΩ to shift the sensitive range into the higher Lux values.

Hardware Build: Parts, Pinout, and Wiring

This build targets the Arduino Uno R3 (ATmega328P) running at 5V logic. If you are using a 3.3V board like the Arduino Nano 33 IoT, the math changes, and you must use a 3.3V reference in your code.

Exact Parts List

  • Microcontroller: Arduino Uno R3 (ATmega328P, DIP or SMD variant)
  • Sensor: GL5528 Photoresistor (10mm epoxy package, 500-02 lead spacing)
  • Resistor: 10kΩ 1/4W Carbon Film (Color bands: Brown-Black-Orange-Gold)
  • Wiring: 22 AWG solid core jumper wires
  • Prototyping: Standard 830-point solderless breadboard

Pin Mapping Table

Component Component Pin Arduino Pin Notes
GL5528 LDR Leg 1 5V Polarity does not matter on resistors
GL5528 LDR Leg 2 A0 (Shared Node) This is the analog measurement point
10kΩ Resistor Leg 1 A0 (Shared Node) Connects to LDR Leg 2 in the same row
10kΩ Resistor Leg 2 GND Pulls the node to 0V when LDR is high resistance
Callout Tip: LDRs have a "memory effect" known as photoconductive lag. If you move the sensor from a bright room to a dark room, the resistance will take several seconds to fully climb to its dark-resistance maximum. Do not expect instant 0ms reaction times in total darkness.

Complete Arduino Code with Smoothing and Bounds Handling

Raw ADC reads from an LDR are notoriously noisy due to electromagnetic interference (EMI) on the breadboard and the high impedance of the voltage divider. The code below implements an Exponential Moving Average (EMA) filter to smooth the data without the memory overhead of a large array. It also includes bounds checking to flag hardware faults.

/*
 * LDR and Arduino Voltage Divider Reader
 * Target Board: Arduino Uno R3 (ATmega328P, 5V/10-bit ADC)
 * Author: ElectricalFlux
 */

// Pin Definitions
#define PIN_LDR A0

// Smoothing Configuration (Alpha = 1/4)
// Higher weight to historical data reduces noise but slows response
#define ALPHA_SHIFT 2 

// Thresholds for hardware fault detection
#define ADC_SATURATION_HIGH 1020
#define ADC_SATURATION_LOW  5

int smoothed_adc = 0;
bool is_initialized = false;

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; } // Wait for serial port (native USB boards)
  
  // Set analog reference to default (5V on Uno)
  analogReference(DEFAULT);
  
  // Prime the ADC capacitor with a dummy read
  analogRead(PIN_LDR);
  delay(10);
}

void loop() {
  // 1. Read raw ADC value
  int raw_adc = analogRead(PIN_LDR);
  
  // 2. Hardware Error Handling & Bounds Checking
  if (raw_adc >= ADC_SATURATION_HIGH) {
    Serial.println("ERROR: ADC Saturated High (1023). Check if LDR is shorted to 5V or fixed resistor is missing/open.");
  } 
  else if (raw_adc <= ADC_SATURATION_LOW && is_initialized) {
    Serial.println("WARN: ADC Saturated Low (0). Check if LDR is exposed to extreme direct light or shorted to GND.");
  }

  // 3. Exponential Moving Average (EMA) Smoothing
  if (!is_initialized) {
    smoothed_adc = raw_adc; // Seed the filter on first run
    is_initialized = true;
  } else {
    // Integer math EMA: smoothed = smoothed + (raw - smoothed) / 4
    smoothed_adc = smoothed_adc + ((raw_adc - smoothed_adc) >> ALPHA_SHIFT);
  }

  // 4. Map to approximate percentage (0% = Dark, 100% = Bright)
  // Note: Inverted because higher light = lower resistance = lower voltage
  int light_percent = map(smoothed_adc, 0, 1023, 100, 0);
  light_percent = constrain(light_percent, 0, 100);

  // 5. Output
  Serial.print("Raw: ");
  Serial.print(raw_adc);
  Serial.print(" | Smoothed: ");
  Serial.print(smoothed_adc);
  Serial.print(" | Light Level: ");
  Serial.print(light_percent);
  Serial.println("%");

  delay(100); // 10Hz sample rate
}

Debugging: The First Three Things to Check When It Fails

When your serial monitor outputs garbage, flatlines, or throws IDE errors, follow this ranked troubleshooting sequence.

1. The Compile Error: 'A0' was not declared in this scope

The Cause: You are likely compiling for a generic ATtiny core, an ESP32 board where analog pins are labeled differently (e.g., GPIO36), or you have a typo in your pin definition.
The Fix: Ensure your Arduino IDE Tools > Board is set to "Arduino Uno". If porting this code to an ESP32, change #define PIN_LDR A0 to #define PIN_LDR 36 (ADC1_CH0) and remember that the ESP32 uses a 12-bit ADC (0-4095), requiring you to update the map() function parameters.

2. The Hardware Fault: Readings Stuck at 1023

The Cause: The voltage at pin A0 is sitting at a solid 5V. This happens when the pull-down resistor is disconnected (floating node pulled high by the LDR), or the LDR is completely shorted.
The Fix: Unplug the Arduino. Use a multimeter in continuity mode. Place one probe on the A0 pin and the other on the GND pin. You should read exactly 10kΩ (the value of your fixed resistor). If you read "OL" (Open Loop), your 10kΩ resistor is not making contact in the breadboard.

3. The Hardware Fault: Readings Stuck at 0

The Cause: The voltage at A0 is 0V. This means the LDR is acting as a dead short, or the 5V rail is not reaching the LDR.
The Fix: Cover the LDR completely with your finger. If the reading jumps up, your room is simply too bright for the 10kΩ divider (the LDR resistance has dropped below 50Ω). If it stays at 0, check your 5V rail with a multimeter. A common beginner mistake is plugging the LDR into the VIN pin instead of 5V; if you are powering the Uno via USB, VIN outputs nothing.

Extending and Simplifying the Build

Depending on your end goal, you may want to strip this project down to its bare essentials or scale it up into an IoT network.

How to Simplify: Use a Pre-Built Module

If you do not want to calculate voltage divider math or deal with breadboard noise, purchase an Analog Light Sensor Module (often sold in packs of 5 for under $8). These modules include the LDR, a fixed resistor, and an LM393 comparator with a trimpot. They output both an Analog signal (AO) and a Digital signal (DO) that flips high/low at a specific Lux threshold you set with a Phillips screwdriver. This eliminates the need for software debouncing if you only need a simple "day/night" trigger.

How to Extend: Porting to ESP32 and Adding I2C

To make this an IoT node, port the circuit to an ESP32-WROOM-32 DevKit v1. However, you must account for the ESP32's ADC non-linearity. According to Espressif's official documentation, the ESP32 ADC struggles to differentiate voltages near 0V and 3.3V. To fix this, use the ESP32's analogReadMilliVolts() function instead of analogRead(), which applies factory-stored eFuse calibration data to correct the curve.

For local display, wire an SSD1306 128x64 I2C OLED to pins A4 (SDA) and A5 (SCL) on the Uno (or GPIO 21/22 on the ESP32). Use the Adafruit_SSD1306 library to render a real-time bar graph of the smoothed ADC values, giving you a visual oscilloscope to verify your EMA filter is working correctly before deploying the sensor into an enclosure.

Summary: Building a reliable LDR and Arduino circuit hinges on matching your pull-down resistor to your target lighting environment, filtering the noisy ADC signal in software, and verifying the physical voltage divider with a multimeter before trusting the serial monitor.