If you wire an arduino photocell (Light Dependent Resistor, or LDR) directly to an analog pin and call analogRead(), you will get a raw integer between 0 and 1023. That number is practically useless on its own. LDRs are highly non-linear components; their resistance changes logarithmically with light intensity, and their baseline resistance varies wildly from batch to batch.

To get usable, repeatable data from a photocell, you must build a hardware voltage divider with a precision fixed resistor, add a hardware low-pass filter to kill ADC noise, and apply a logarithmic curve fit in your firmware to estimate actual Lux. This guide walks through the exact hardware topology, the math required to translate resistance to Lux, and the specific debugging steps when your readings flatline or drift.

LDR Spec Sheet: Choosing the Right Photocell for Your Lux Range

Not all photocells are created equal. The most common hobbyist LDR is the GL5528, but it is optimized for indoor room lighting. If you are building an outdoor streetlight controller or a low-light astronomy trigger, you need a different chemical formulation. The table below maps the four most common cadmium sulfide (CdS) photocell variants to their real-world electrical characteristics.

Model Max Dark Resistance (MΩ) Resistance at 10 Lux (kΩ) Gamma (γ) Peak Wavelength (nm) Best Use Case
GL5528 1.0 10 - 20 0.6 540 Indoor ambient lighting, desk lamps
GL5516 0.5 5 - 10 0.6 540 Bright indoor, shaded outdoor areas
GL5539 3.0 30 - 90 0.8 540 Low-light detection, nightlights
GL5549 5.0 100 - 200 0.9 540 Ultra-low light, starlight triggers

Note: Gamma (γ) defines the slope of the resistance-to-light curve. A higher gamma means the sensor is more sensitive to small changes in low light but saturates faster in bright light. Data derived from standard CdS photoresistor characteristic sheets.

Hardware Build: Parts, Pinout, and the Voltage Divider

An Arduino cannot measure resistance directly; it measures voltage. To convert the LDR's variable resistance into a variable voltage, we use a voltage divider. We also add a ceramic capacitor to act as a hardware low-pass filter, which is critical for stabilizing the 10-bit ADC against 50/60Hz mains flicker and USB power rail noise.

Parts List

  • Microcontroller: Arduino Uno R3 (ATmega328P, 5V logic, 10-bit ADC)
  • Sensor: GL5528 CdS Photocell
  • Fixed Resistor: 10kΩ 1/4W Metal Film Resistor (1% tolerance — do not use 5% carbon film, as the baseline error will ruin your Lux math)
  • Filter Capacitor: 100nF (0.1µF) Ceramic Capacitor
  • Wiring: Half-size breadboard, 22 AWG solid core jumper wires

Pin Mapping Table

Component Arduino Pin Electrical Notes
LDR Leg 1 5V Connected to VCC rail
LDR Leg 2 / 10kΩ Resistor A0 Voltage divider midpoint (Analog Input)
10kΩ Resistor Leg 2 GND Pull-down to ground
100nF Capacitor A0 & GND Wired in parallel with the 10kΩ resistor
Pro-Tip: The 100nF Hardware Filter
Many tutorials skip the capacitor. Without it, the high impedance of the voltage divider (which can exceed 100kΩ in the dark) makes the A0 pin act like an antenna, picking up electromagnetic interference and 60Hz AC mains flicker. The 100nF capacitor creates an RC low-pass filter that smooths the voltage before it hits the ATmega328P's sample-and-hold circuit.

Compilable Code: Noise Filtering and Lux Estimation

The code below targets the Arduino Uno R3. It reads the analog pin, applies a 10-sample moving average to kill residual software noise, calculates the actual resistance of the LDR, and then uses the logarithmic gamma equation to estimate Lux. It includes explicit error handling to prevent divide-by-zero crashes when the room is pitch black or fully saturated.

// Target Board: Arduino Uno R3 (ATmega328P)
// Sensor: GL5528 Photocell in a 5V voltage divider

#define LDR_PIN A0
#define SERIES_RESISTOR 10000.0  // 10k ohm fixed resistor (use 1% tolerance)
#define ADC_MAX 1023.0           // 10-bit ADC resolution
#define VCC 5.0                  // Nominal VCC (measure with DMM for better accuracy)

#define SAMPLE_SIZE 10           // Moving average window

// GL5528 typical datasheet values for curve fitting
#define R_AT_10_LUX 15000.0      // Resistance at 10 Lux (midpoint of 10-20k range)
#define GAMMA 0.6                // Logarithmic slope

int samples[SAMPLE_SIZE];
int sampleIndex = 0;

void setup() {
  Serial.begin(115200);
  analogReference(DEFAULT); // Use 5V VCC as ADC reference on Uno R3
  
  // Initialize sample array
  for(int i = 0; i < SAMPLE_SIZE; i++) {
    samples[i] = 0;
  }
  
  Serial.println("Arduino Photocell Lux Meter Initialized.");
}

void loop() {
  // 1. Read raw ADC and store in circular buffer
  int raw = analogRead(LDR_PIN);
  samples[sampleIndex] = raw;
  sampleIndex = (sampleIndex + 1) % SAMPLE_SIZE;

  // 2. Calculate moving average
  long sum = 0;
  for(int i = 0; i < SAMPLE_SIZE; i++) {
    sum += samples[i];
  }
  float avgADC = (float)sum / SAMPLE_SIZE;

  // 3. Error Handling: Prevent divide-by-zero and infinity
  if (avgADC <= 0) avgADC = 1; 
  if (avgADC >= ADC_MAX) avgADC = ADC_MAX - 1; 

  // 4. Calculate LDR Resistance
  // Formula: R_LDR = R_series * ((V_in / V_out) - 1)
  // Where V_in/V_out is equivalent to ADC_MAX / avgADC
  float rLDR = SERIES_RESISTOR * ((ADC_MAX / avgADC) - 1.0);

  // 5. Calculate Estimated Lux using Logarithmic Curve Fit
  // Formula derived from: log(R) = log(A) - gamma * log(Lux)
  float lux = pow(10, (log10(R_AT_10_LUX) - log10(rLDR)) / GAMMA) * 10.0;

  // 6. Output Data
  Serial.print("Avg ADC: "); Serial.print(avgADC, 1);
  Serial.print(" | R_LDR: "); Serial.print(rLDR, 0);
  Serial.print(" ohms | Est Lux: "); Serial.println(lux, 1);

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

Debugging: Why is my analogRead() Stuck or Noisy?

When a photocell circuit misbehaves, it almost always manifests as one of three specific serial output symptoms. Before rewriting your code, check the physical layer.

The First Three Things to Check

  1. DMM Continuity on the Divider: Set your multimeter to resistance mode. Probe the A0 pin and GND. You should read exactly ~10kΩ (the value of your fixed resistor). If it reads infinite (OL), your pull-down resistor is unseated. If it reads 0Ω, your A0 pin is shorted to ground.
  2. Measure the 5V Rail: The Arduino Uno's USB 5V rail often sags to 4.7V or 4.8V under load. Because analogReference(DEFAULT) uses VCC as the baseline, a sagging VCC artificially inflates your Lux reading. Measure the 5V pin with a DMM and update the #define VCC in your code to the real measured value.
  3. Breadboard Power Rail Continuity: Cheap breadboards often have split ground rails. Ensure the GND pin of your 10kΩ resistor and the GND pin of your 100nF capacitor are on the exact same continuous metal strip.

Symptom 1: "analogRead() stuck at 1023"

The Cause: The A0 pin is being pulled hard to 5V. This happens when the LDR is wired to 5V, but the 10kΩ pull-down resistor is missing, broken, or not connected to GND. The ATmega328P's internal ADC impedance is roughly 100MΩ; without the 10kΩ path to ground, the pin floats high.

The Fix: Verify the 10kΩ resistor is physically bridging the A0 midpoint and the GND rail. Check for cold solder joints if using a perfboard.

Symptom 2: "analogRead() stuck at 0"

The Cause: The A0 pin is shorted to GND, or the LDR is completely missing from the 5V side of the divider. It can also happen if you accidentally wired the LDR to GND and the resistor to 5V (reversing the divider logic), and the room is pitch black (LDR resistance > 1MΩ).

The Fix: Trace the 5V path. Ensure the LDR is physically connected between the 5V rail and the A0 midpoint.

Symptom 3: "Readings fluctuate by ±20 points in stable light"

The Cause: 50/60Hz AC mains flicker from overhead fluorescent/LED bulbs, or high-frequency switching noise from the Arduino's onboard USB voltage regulator. The 100nF capacitor is either missing or too small for the specific impedance of your dark-state divider.

The Fix: Ensure the 100nF capacitor is installed. If the noise persists in very dark environments (where LDR resistance > 500kΩ), increase the capacitor to 1µF to lower the RC filter's cutoff frequency, or increase the SAMPLE_SIZE in the code to 50.

Extending and Simplifying the Build

The analog LDR is a fantastic learning tool for understanding voltage dividers and ADC math, but it has hard physical limits. Cadmium sulfide cells suffer from "memory effect" (hysteresis) and degrade over years of UV exposure. Depending on your end goal, you should either simplify the hardware or upgrade the sensor entirely.

Simplify: The LM393 Digital Comparator

If you only need to know "Is it dark enough to turn on the porch light?", you do not need an Arduino or an ADC at all. Buy an LM393 LDR module (typically $1.50 USD). These modules include the voltage divider and an LM393 op-amp configured as a comparator. You turn a small trimpot to set your Lux threshold, and the module outputs a clean 5V HIGH or 0V LOW digital signal. You wire this directly to a digital GPIO pin or even directly to a 5V relay module.

Extend: I2C Calibrated Lux Sensors

If you are building a greenhouse monitor, a photography light meter, or a smart home automation node that requires linear, repeatable Lux data, abandon the photocell. Upgrade to a BH1750FVI digital ambient light sensor (~$3.00 USD). The BH1750 uses an I2C interface, contains an internal ADC, and outputs a calibrated, linear Lux value directly. It completely bypasses the need for voltage divider math, gamma curve fitting, and moving average filters. For professional embedded applications, the analogRead() function and LDRs should be reserved for simple threshold triggers, not precision metrology.