The LDR Light Sensor: Sensing Principle and Output Types

A Light Dependent Resistor (LDR), or cadmium sulfide (CdS) photoresistor, is a passive semiconductor component. When photons strike the CdS material, they excite electrons into the conduction band, which drops the electrical resistance. In total darkness, a standard 5mm LDR like the GL5528 exhibits a "dark resistance" of roughly 1MΩ. Under bright sunlight (approx. 10,000 lux), that resistance plummets to 1kΩ–3kΩ. The response is highly non-linear and logarithmic, meaning a change from 10 to 100 lux causes a massive resistance drop, while a change from 10,000 to 10,100 lux is barely measurable.

Crucially, the raw LDR light sensor does not output a voltage or a digital signal; it is strictly a variable resistor. To interface it with a microcontroller, you must convert that resistance change into a voltage using a voltage divider circuit. Most commercial breakout boards (like the ubiquitous 3-pin or 4-pin blue modules) include a fixed 10kΩ resistor to form this divider, and often an LM393 op-amp comparator to provide a secondary digital threshold output. You must treat the analog and digital outputs as entirely separate signal paths.

Wiring Guide and Pinout Specifications

The standard 4-pin LDR module operates on a supply range of 3.3V to 5V. If you are using an ESP32, power the module from the 3.3V pin to keep the analog output within the ESP32's safe ADC input range (0–3.1V). Feeding a 5V analog signal into an ESP32 GPIO will permanently damage the silicon.

Module Pin Function ESP32 DevKit V1 Connection Arduino Uno Connection Notes
VCC Power Supply (3.3V - 5V) 3V3 5V Match VCC to your MCU's logic level for accurate ADC scaling.
GND Circuit Ground GND GND Ensure a shared ground with the MCU.
AO Analog Voltage Output GPIO 34 (ADC1_CH6) A0 Outputs a divided voltage between 0V and VCC.
DO Digital Threshold Output Any GPIO (e.g., GPIO 15) D2 Outputs HIGH/LOW based on the LM393 trim pot setting.
Callout Tip: ESP32 ADC Pin Selection
Always use ADC1 pins (GPIO 32-39) on the ESP32 for analog sensors. ADC2 pins (GPIO 0, 2, 4, 12-15, 25-27) are shared with the WiFi radio and will return garbage data or fail to read entirely when WiFi is active.

The Math: Converting Raw ADC Readings to Lux

Getting a raw ADC number is easy; converting it to a physical unit (Lux) requires reversing the voltage divider math and applying a logarithmic curve fit. Here is the exact signal chain.

Step 1: Calculate the LDR Resistance
Assuming a 10kΩ fixed resistor ($R_{fixed}$) and a 3.3V supply ($V_{cc}$), the voltage at the analog pin ($V_{out}$) is:

V_out = V_cc * (R_fixed / (R_LDR + R_fixed))

Rearranging to solve for the LDR's resistance:

R_LDR = R_fixed * ((V_cc / V_out) - 1)

Step 2: Convert Resistance to Lux
The GL5528 datasheet specifies a resistance of ~10kΩ at 10 Lux, with a log-log slope (gamma) of roughly -0.7. The empirical formula to approximate Lux from resistance (in kΩ) is:

Lux = 10 ^ (1.7 - 0.7 * log10(R_LDR_kOhm))

Here is the complete, compilable C++ implementation for the ESP32, utilizing the modern ESP-IDF ADC calibration API to fix the ESP32's notorious hardware non-linearity near 0V and 3.1V.


#include <Arduino.h>
#include <esp_adc/adc_oneshot.h>
#include <esp_adc/adc_cali.h>
#include <math.h>

#define LDR_PIN ADC_CHANNEL_6 // GPIO 34
#define R_FIXED 10000.0       // 10k Ohm fixed resistor
#define V_CC 3300.0           // 3.3V in millivolts

adc_oneshot_unit_handle_t adc_handle;
adc_cali_handle_t cali_handle;

void setup() {
  Serial.begin(115200);
  adc_oneshot_unit_init_cfg_t init_config = {.unit_id = ADC_UNIT_1};
  adc_oneshot_new_unit(&init_config, &adc_handle);
  
  adc_oneshot_chan_cfg_t config = {.bitwidth = ADC_BITWIDTH_DEFAULT, .atten = ADC_ATTEN_DB_11};
  adc_oneshot_config_channel(adc_handle, LDR_PIN, &config);
  
  adc_cali_curve_fitting_config_t cali_config = {
    .unit_id = ADC_UNIT_1, .chan = LDR_PIN, .atten = ADC_ATTEN_DB_11, .bitwidth = ADC_BITWIDTH_DEFAULT
  };
  adc_cali_create_scheme_curve_fitting(&cali_config, &cali_handle);
}

void loop() {
  int raw_adc;
  int voltage_mv;
  adc_oneshot_read(adc_handle, LDR_PIN, &raw_adc);
  adc_cali_raw_to_voltage(cali_handle, raw_adc, &voltage_mv);
  
  float v_out = voltage_mv;
  float r_ldr = R_FIXED * ((V_CC / v_out) - 1.0);
  float r_ldr_k = r_ldr / 1000.0;
  
  // Clamp to prevent math domain errors in total darkness
  if (r_ldr_k < 0.1) r_ldr_k = 0.1; 
  if (r_ldr_k > 2000) r_ldr_k = 2000;
  
  float lux = pow(10.0, (1.7 - 0.7 * log10(r_ldr_k)));
  
  Serial.printf('Voltage: %d mV | R_LDR: %.1f Ohm | Lux: %.1f\n', voltage_mv, r_ldr, lux);
  delay(500);
}

Calibration, Interference, and Real-World Gotchas

If your Lux readings look wrong on the serial monitor, you are likely hitting one of three physical interference sources inherent to CdS cells.

  • Spectral Mismatch (The IR Problem): CdS photoresistors peak in sensitivity around 540nm (green light), but they remain highly sensitive into the near-infrared (IR) spectrum up to 900nm. If you calibrate your LDR under a cool-white LED desk lamp, and then test it in direct sunlight or under an incandescent bulb, the reading will spike artificially high because the LDR is "seeing" the IR heat radiation that human eyes cannot. Fix: Place a visible-pass IR-blocking filter (like a piece of exposed photographic film or specialized optical gel) over the sensor if you need human-eye-matched readings.
  • Mains Flicker (100/120Hz Ripple): AC-powered room lighting turns on and off 100 or 120 times a second. An LDR is fast enough to catch this ripple, causing your ADC readings to jitter wildly. Fix: Solder a 100nF ceramic capacitor directly across the LDR legs on the module to create a low-pass hardware filter, or average 20 sequential ADC reads in software.
  • Temperature Drift: CdS resistance shifts with ambient temperature. A sensor calibrated at 20°C will read roughly 10% darker at 40°C. For outdoor enclosures, this means your "sunset trigger" might fire an hour early on a hot summer day.

Decision Matrix: LDR vs. BH1750 vs. Photodiode

Do not default to an LDR just because it is cheap. Choose your sensor based on the physical requirement of your project. Use this decision path to lock in your bill of materials.

Project Requirement LDR (GL5528) BH1750 (I2C Digital) BPW34 Photodiode
Primary Use Case Simple dark/light threshold switching Accurate ambient lux logging / smart home High-speed optical comms / laser tripwires
Output Type Analog Voltage (via divider) Digital I2C (Direct Lux value) Current (requires transimpedance amp)
IR Rejection Poor (Sees IR as visible light) Excellent (IR filtered on silicon) Poor (Broad spectrum, needs external filter)
Response Time Slow (~20-50ms) Medium (~120ms integration) Ultra-fast (<1µs)
Typical Cost (2026) ~$1.20 (Module) ~$2.50 (GY-302 Breakout) ~$1.50 (Raw component)
The Final Verdict: What to Buy
If your project is a simple automated chicken coop door or a solar tracker that just needs to know "is the sun up or down?", buy the 4-pin GL5528 LDR module with the LM393 comparator. Set the trim pot once, read the digital DO pin, and ignore the analog math entirely.

However, if you are building a circadian lighting controller, a greenhouse automation system, or an ESPHome dashboard that requires repeatable, accurate Lux values regardless of temperature or IR interference, abandon the LDR. Buy the GY-302 BH1750 I2C breakout board. It handles the ADC math internally, rejects IR natively, and outputs a calibrated 16-bit Lux value directly over I2C, saving you hours of software debugging.

For deeper technical specifications on ESP32 ADC hardware calibration, refer to the Espressif ADC Calibration API documentation. For I2C digital alternatives, review the SparkFun BH1750 Hookup Guide to compare wiring complexity.