Project Overview & Difficulty Rating

Difficulty: 2/5 | Time: 30 Minutes | Target Board: Arduino Uno R3 (ATmega328P) & Arduino Nano v3

To make a light sensor with Arduino, you must convert light intensity into an electrical signal the microcontroller can read. You have two primary paths: an analog Photoresistor (LDR) using a voltage divider, or a digital I2C sensor like the BH1750. This guide covers both, allowing you to read precise lux values via I2C while simultaneously reading relative ambient light via the analog-to-digital converter (ADC).

The code provided targets the Arduino Uno R3 and Arduino Nano v3 (both ATmega328P-based). If you are using an ESP32 or Mega2560, the I2C pins and ADC resolutions will differ, which we address in the debugging and FAQ sections.

Choosing Your Sensor: Photoresistor (LDR) vs BH1750

Before wiring anything, understand the trade-offs. A standard GL5528 LDR is cheap and simple but highly non-linear and temperature-dependent. The Rohm BH1750 is a digital ambient light sensor that outputs calibrated lux values directly over I2C, bypassing the Arduino's ADC entirely.

SpecificationGL5528 Photoresistor (LDR)GY-302 BH1750 Module
InterfaceAnalog (Voltage Divider)Digital (I2C)
Output MetricRelative Resistance (0-1023)Calibrated Lux (1 - 65535)
AccuracyLow (±20% variance)High (±20% max, but linear)
Typical Cost$0.10 per unit$1.50 - $3.00 per module
Best Use CaseDay/Night threshold detectionScreen brightness, greenhouse monitoring

For a robust project, we wire both. The LDR acts as a fast, low-overhead analog trigger, while the BH1750 provides the exact lux measurement for serial logging or display.

Parts List & Pin Mapping

Here is the exact bill of materials for this dual-sensor build. Do not substitute the 10kΩ resistor for the LDR without recalculating the voltage divider curve.

  • Microcontroller: Arduino Uno R3 or Nano v3 (ATmega328P)
  • Digital Sensor: GY-302 BH1750 I2C Light Sensor Module
  • Analog Sensor: 5mm GL5528 Photoresistor (LDR)
  • Resistor: 1x 10kΩ (1/4W) for LDR voltage divider
  • Hardware: Half-size breadboard, male-to-male jumper wires

Pin Mapping Table

ComponentModule PinArduino Uno / Nano PinNotes
BH1750VCC5V (or 3.3V)Module has onboard regulator
BH1750GNDGNDCommon ground required
BH1750SCLA5I2C Clock (Uno/Nano)
BH1750SDAA4I2C Data (Uno/Nano)
BH1750ADDRNot ConnectedFloats low (I2C Addr: 0x23)
LDRLeg 15VVoltage divider top
LDRLeg 2A0Analog read + 10kΩ tie-in
10kΩ ResistorLeg 1A0Shared with LDR Leg 2
10kΩ ResistorLeg 2GNDVoltage divider bottom

Step-by-Step Wiring & Assembly

Bench Tip: The GY-302 module includes 4.7kΩ I2C pull-up resistors. If you are using a raw BH1750FVI chip without a breakout board, you must add external 4.7kΩ pull-ups to SDA and SCL, or the I2C bus will hang.
  1. Prepare the I2C Bus: Connect the BH1750 VCC to 5V and GND to GND. Wire SDA to A4 and SCL to A5. Leave the ADDR pin unconnected to default to the primary I2C address (0x23).
  2. Build the LDR Voltage Divider: Insert the LDR into the breadboard. Connect one leg to 5V. Connect the other leg to Arduino pin A0.
  3. Add the Pull-Down Resistor: Insert the 10kΩ resistor so one leg shares the A0/LDR junction, and the other leg connects to GND. This creates a voltage divider where the voltage at A0 changes as the LDR's resistance shifts with light.
  4. Verify Connections: Use a multimeter in continuity mode to ensure A4/A5 are not shorted to power, and that the 10kΩ resistor is firmly seated in GND.

Complete Arduino Code with Error Handling

This sketch requires the BH1750 library by Christopher Laws (available via the Arduino Library Manager). It includes explicit I2C bus scanning to catch wiring errors before the main loop hangs, and handles the 10-bit ADC mapping for the LDR.

#include <Wire.h>
#include <BH1750.h>

// --- Pin Definitions ---
#define PIN_LDR A0
#define PIN_STATUS_LED 13
#define BH1750_I2C_ADDR 0x23

// --- Sensor Objects ---
BH1750 lightMeter;

void setup() {
  Serial.begin(115200);
  pinMode(PIN_STATUS_LED, OUTPUT);
  
  // Initialize I2C bus
  Wire.begin();
  
  // Hardware I2C check before library init
  Wire.beginTransmission(BH1750_I2C_ADDR);
  byte i2cError = Wire.endTransmission();
  
  if (i2cError == 0) {
    Serial.println(F("I2C device found at 0x23."));
    lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
  } else {
    Serial.println(F("Error: BH1750 sensor not found on I2C bus!"));
    Serial.println(F("Check SDA/SCL wiring. Falling back to LDR only."));
  }
}

void loop() {
  float lux = -1.0;
  
  // Read Digital Sensor (if present)
  if (Wire.endTransmission() == 0 || true) { // Simplified check, library handles timeout
    lux = lightMeter.readLightLevel();
    if (lux < 0 || isnan(lux)) {
      Serial.println(F("Warning: BH1750 read timeout or NaN."));
    }
  }
  
  // Read Analog LDR (Voltage Divider)
  int ldrRaw = analogRead(PIN_LDR);
  // Map 0-1023 to a rough relative percentage (0 = dark, 100 = bright)
  int ldrPercent = map(ldrRaw, 0, 1023, 0, 100);
  
  // Serial Output
  Serial.print(F("BH1750 Lux: "));
  if (lux >= 0) Serial.print(lux, 1);
  else Serial.print(F("N/A"));
  
  Serial.print(F(" | LDR Raw: "));
  Serial.print(ldrRaw);
  Serial.print(F(" ("));
  Serial.print(ldrPercent);
  Serial.println(F("%)"));
  
  // Threshold logic: Turn on LED if room is dark (< 15 lux)
  if (lux >= 0 && lux < 15.0) {
    digitalWrite(PIN_STATUS_LED, HIGH);
  } else if (lux < 0 && ldrPercent < 20) { // Fallback if BH1750 fails
    digitalWrite(PIN_STATUS_LED, HIGH);
  } else {
    digitalWrite(PIN_STATUS_LED, LOW);
  }
  
  delay(500);
}

Debugging: First 3 Things to Check When It Fails

When your serial monitor spits out garbage or the sensor refuses to initialize, do not rewrite your code. 95% of I2C and ADC failures on the bench are hardware faults. Here are the first three things to check.

1. Exact Error: "Error: BH1750 sensor not found on I2C bus!"

Ranked Causes:

  1. Swapped SDA/SCL Pins: On the Uno/Nano, SDA is A4 and SCL is A5. They are not interchangeable. Swap them and reset.
  2. Missing Ground: The I2C bus requires a common ground reference. If the GND wire from the sensor to the Arduino is loose, the pull-up resistors cannot establish a logic HIGH.
  3. Wrong I2C Address: If the ADDR pin on the GY-302 is accidentally pulled HIGH, the address shifts to 0x5C. Check your solder bridges or jumper wires.

2. Symptom: LDR Reading Stuck at 1023 or 0

Ranked Causes:

  1. Missing Pull-Down Resistor: If you wired the LDR between 5V and A0 but forgot the 10kΩ resistor to GND, the A0 pin is floating when the LDR resistance is high (dark). It will read erratic noise or peg at 1023.
  2. Short to Ground: If the reading is permanently 0, your A0 wire is likely shorted to GND, or your 10kΩ resistor is shorting the circuit.

3. Symptom: "Warning: BH1750 read timeout or NaN"

Ranked Causes:

  1. I2C Bus Capacitance: If your jumper wires are excessively long (over 1 meter), the bus capacitance exceeds the I2C spec, causing signal degradation. Keep I2C wires under 30cm.
  2. Power Brownout: The BH1750 draws roughly 190µA during measurement. If your Arduino 5V rail is sagging due to other high-draw components (like a motor shield), the sensor will brownout and fail to respond.
Pro Tip: For deeper I2C debugging, flash the standard Arduino I2C_Scanner example sketch. It will brute-force all 127 addresses and tell you exactly where your sensor is hiding.

Extending and Simplifying the Build

Depending on your end goal, you may not need both sensors. Here is how to adapt the circuit.

How to Simplify

If you only need to know if a room is "light" or "dark" (e.g., triggering a closet light), drop the BH1750 entirely. The LDR and 10kΩ resistor cost pennies and require no external libraries. Simply delete the I2C code blocks and rely on the ldrPercent threshold.

How to Extend

  • Add an OLED Display: Wire an SSD1306 128x64 I2C OLED to the same A4/A5 bus (I2C supports multiple devices). Use the Adafruit_SSD1306 library to print real-time lux values.
  • Upgrade to ESP32 for MQTT: Swap the Uno for an ESP32 DevKit v1. Note that ESP32 I2C pins are GPIO 21 (SDA) and GPIO 22 (SCL). Use the PubSubClient library to push lux data to a Home Assistant MQTT broker for smart home automation.
  • Smooth the Analog Signal: If your LDR readings fluctuate due to 50/60Hz AC mains flicker from room lighting, add a 10µF electrolytic capacitor in parallel with the 10kΩ resistor to act as a low-pass hardware filter.

Frequently Asked Questions

How to make a light sensor with Arduino without a resistor?

You cannot use a standard analog LDR without a resistor; it requires a voltage divider to create a readable voltage change. However, you can use the Arduino's internal pull-up resistor (approx. 20kΩ-50kΩ) by wiring the LDR between A0 and GND, and enabling pinMode(A0, INPUT_PULLUP) in your code. The readings will be inverted and less precise, but it eliminates the external 10kΩ resistor. Alternatively, use the digital BH1750, which requires no resistors on the analog pins.

How to make a light sensor with Arduino to turn on an LED automatically?

Wire an LED (with a 220Ω current-limiting resistor) to a digital pin (e.g., Pin 8). In your loop(), read the sensor value. If the lux drops below your desired threshold (e.g., if (lux < 20.0)), use digitalWrite(8, HIGH) to illuminate the LED. For high-power lighting (like a 12V LED strip), use the Arduino pin to trigger a logic-level MOSFET (like an IRLZ44N) or a 5V relay module instead of driving the load directly from the GPIO.

How to make a light sensor with Arduino using an ESP32 instead?

The wiring and code logic remain nearly identical, but the hardware mapping changes. On a standard 30-pin ESP32 DevKit v1, the default I2C pins are GPIO 21 (SDA) and GPIO 22 (SCL). Furthermore, the ESP32 ADC is 12-bit (0-4095) and notoriously non-linear at the extremes. If using an LDR with an ESP32, map the analog read from 0-4095 instead of 0-1023, and consider using the analogReadMilliVolts() function for better accuracy.

Why is my Arduino light sensor reading fluctuating wildly?

Wild fluctuations (jumping 50+ lux per second) are usually caused by AC flicker from fluorescent or cheap LED room lighting, which pulses at 100Hz or 120Hz. The BH1750's default integration time is roughly 120ms, which usually averages this out, but if you are using the continuous low-res mode, it samples faster and catches the flicker. Switch the library to BH1750::CONTINUOUS_HIGH_RES_MODE_2 for maximum averaging, or add a software moving-average filter to your code.