If you need to measure ambient light with a microcontroller, you have two main paths: a $0.50 analog Light Dependent Resistor (LDR) or a $2.50 digital I2C sensor like the BH1750FVI. For a reliable Arduino light detector that outputs actual lux values rather than arbitrary 0-1023 analog readings, the BH1750 is the undisputed winner. However, pairing it with an LDR gives you the best of both worlds—calibrated digital precision for logging, and a fast, cheap analog trigger for immediate hardware interrupts.

This guide walks through building a dual-sensor light detector targeting the Arduino Uno R3 (ATmega328P). We will cover the hardware selection, exact pin mappings, raw I2C implementation (no third-party libraries required), and how to debug the most common I2C and analog failure modes.

Sensor Selection: BH1750 vs. Analog LDR vs. TSL2561

Before wiring the breadboard, it is critical to understand the trade-offs between common light sensors. The table below compares the three most frequent choices for embedded light detection based on real-world bench performance.

Feature BH1750FVI (GY-302) GL5528 Analog LDR TSL2561
Interface I2C (Digital) Analog (Resistance) I2C (Digital)
Measurement Range 1 to 65,535 lux Non-linear (approx. 10-10k lux) 0.1 to 40,000+ lux
Resolution 1 lux (H-Res mode) Depends on ADC & voltage divider 0.1 lux (with scaling)
IR Rejection Built-in optical filter Poor (reacts to IR heat sources) Excellent (Dual diode IR subtraction)
Typical Price (2026) $2.00 - $3.50 (Breakout) $0.10 - $0.50 (Bare component) $4.50 - $7.00 (Breakout)
Calibration Required Factory calibrated Requires manual curve fitting Factory calibrated

The Verdict: Use the BH1750 for 90% of projects. It requires no complex math to convert raw data to lux, and its built-in optical filter ignores infrared radiation from incandescent bulbs or sunlight heat, which heavily skews raw LDR readings. Keep the LDR only if you need a sub-millisecond response time for a hardware comparator circuit.

Hardware BOM and Pin Mapping

This build assumes you are using the widely available GY-302 breakout board for the BH1750. This specific breakout is crucial because it includes the necessary 4.7kΩ I2C pull-up resistors and a voltage regulator, allowing you to safely connect it to the 5V logic of an Arduino Uno R3 without frying the sensor's 3.3V I2C lines.

Difficulty Rating: Beginner/Intermediate | Time to Build: 25 minutes | Cost: ~$18

Bill of Materials

  • Microcontroller: Arduino Uno R3 (ATmega328P) or compatible clone
  • Digital Sensor: BH1750FVI on GY-302 breakout board
  • Analog Sensor: 5mm GL5528 LDR (Light Dependent Resistor)
  • Resistor: 10kΩ (1/4W, 5% tolerance) for LDR voltage divider
  • Wiring: 22 AWG solid-core jumper wires
  • Prototyping: Half-size 400-point breadboard

Pin Mapping Table

Component Module Pin Arduino Uno R3 Pin Notes
BH1750 (GY-302) VCC 5V Breakout regulator handles 5V to 3.3V step-down
BH1750 (GY-302) GND GND Common ground required for I2C
BH1750 (GY-302) SCL A5 I2C Clock (Hardware I2C)
BH1750 (GY-302) SDA A4 I2C Data (Hardware I2C)
BH1750 (GY-302) ADDR Not Connected Leave floating or tie to GND for address 0x23
LDR (GL5528) Leg 1 5V Connected to 5V rail
LDR (GL5528) Leg 2 A0 Analog input (Voltage divider midpoint)
10kΩ Resistor Leg 1 A0 Shares node with LDR Leg 2
10kΩ Resistor Leg 2 GND Pulls voltage down to create divider

Step-by-Step Wiring and Assembly

  1. Prep the Power Rails: Connect the Arduino 5V pin to the red breadboard rail and the GND pin to the blue breadboard rail using 22 AWG solid wire.
  2. Wire the BH1750 I2C Bus: Connect the GY-302 VCC to 5V and GND to the blue rail. Route SDA to A4 and SCL to A5. Do not cross SDA and SCL; while I2C won't physically short, the hardware peripheral will fail to initialize.
  3. Configure the I2C Address: Leave the ADDR pin on the GY-302 unconnected. By default, the internal pull-down sets the I2C address to 0x23. If you tie it to 5V, the address shifts to 0x5C.
  4. Build the LDR Voltage Divider: Insert the LDR and the 10kΩ resistor so they share a common center node on the breadboard. Connect the top of the LDR to 5V. Connect the bottom of the 10kΩ resistor to GND.
  5. Route the Analog Signal: Run a jumper wire from the shared center node of the LDR/Resistor pair to the Arduino A0 pin. This midpoint voltage will vary from near 0V (dark) to near 5V (bright light).
  6. Verify Connections: Use a multimeter in continuity mode to verify that GND is common across the Arduino, the sensor breakout, and the bottom of the 10kΩ resistor. A floating ground is the #1 cause of erratic analog readings.

Calibrated Lux Measurement Code

The following code targets the Arduino Uno R3 (ATmega328P). Instead of relying on third-party libraries that may break or require specific versions, this implementation uses raw I2C commands via the built-in Arduino Wire library. This guarantees compilation out-of-the-box and demonstrates exactly how the BH1750FVI datasheet protocols work under the hood.

#include <Wire.h>

// --- Pin Definitions ---
const int LDR_PIN = A0;

// --- I2C Configuration ---
// BH1750 address when ADDR pin is LOW or floating
#define BH1750_ADDRESS 0x23 

// BH1750 Operation Codes (from Rohm Datasheet)
#define BH1750_CONT_H_RES_MODE 0x10 // Continuously H-Resolution Mode (1 lx resolution, 120ms measurement time)
#define BH1750_POWER_ON 0x01

// --- Variables ---
float currentLux = 0.0;
int rawAnalogLight = 0;

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial port to connect (needed for native USB boards, safe on Uno)
  }
  
  Wire.begin();
  
  // Initialize BH1750
  Wire.beginTransmission(BH1750_ADDRESS);
  Wire.write(BH1750_POWER_ON);
  byte error = Wire.endTransmission();
  
  if (error != 0) {
    Serial.println("Fatal: BH1750 not responding at 0x23. Halting.");
    while (1) {
      // Blink LED to indicate fatal I2C error
      pinMode(LED_BUILTIN, OUTPUT);
      digitalWrite(LED_BUILTIN, HIGH);
      delay(100);
      digitalWrite(LED_BUILTIN, LOW);
      delay(100);
    }
  }
  
  // Set measurement mode
  Wire.beginTransmission(BH1750_ADDRESS);
  Wire.write(BH1750_CONT_H_RES_MODE);
  Wire.endTransmission();
  
  Serial.println("Sensors initialized. Logging...");
}

void loop() {
  // 1. Read Digital Lux from BH1750
  Wire.requestFrom(BH1750_ADDRESS, 2);
  if (Wire.available() == 2) {
    uint16_t rawLux = Wire.read();
    rawLux <<= 8;
    rawLux |= Wire.read();
    
    // Datasheet formula: Raw Value / 1.2 = Lux
    currentLux = rawLux / 1.2; 
  } else {
    currentLux = -1.0; // Error flag
  }
  
  // 2. Read Analog LDR
  rawAnalogLight = analogRead(LDR_PIN);
  
  // 3. Output Data (CSV format for serial plotter or datalogging)
  Serial.print("Lux_BH1750:");
  Serial.print(currentLux, 1);
  Serial.print(", LDR_ADC:");
  Serial.println(rawAnalogLight);
  
  // BH1750 H-Res mode takes ~120ms per measurement. 
  // Delaying 250ms prevents reading the same I2C register twice.
  delay(250); 
}

Debugging: I2C Failures and Analog Drift

When working with mixed-signal sensor boards, things will go wrong. If your serial monitor outputs the exact error string "Fatal: BH1750 not responding at 0x23. Halting.", or if your LDR readings are bouncing wildly, follow this ranked decision path.

The First Three Things to Check for I2C Failure

  1. SDA and SCL Swapped: This is the most common bench mistake. On the Uno R3, A4 is strictly SDA and A5 is strictly SCL. Unlike software I2C, the hardware TWI peripheral will silently fail if these are reversed. Swap them and reset.
  2. ADDR Pin State Conflict: If you are using a custom breakout board (not the GY-302) and the ADDR pin is tied HIGH, the sensor is listening on 0x5C, not 0x23. Change the #define BH1750_ADDRESS 0x23 to 0x5C in the code, or physically tie the ADDR pin to GND.
  3. Missing Pull-Up Resistors: The I2C specification requires pull-up resistors on SDA and SCL. The GY-302 breakout includes 4.7kΩ pull-ups. If you wired a bare BH1750FVI chip directly to the Arduino without external 4.7kΩ resistors to 3.3V, the bus will float and fail initialization. Refer to the NXP I2C Bus Specification for pull-up calculations.

Fixing Analog LDR Drift and Noise

If the BH1750 works but your LDR_ADC values are jumping by ±50 counts in stable lighting, you have a high-impedance node picking up electromagnetic interference (EMI).
The Fix: Ensure your 10kΩ pulldown resistor is physically located as close to the Arduino A0 pin as possible on the breadboard. If the noise persists, add a 0.1µF ceramic capacitor in parallel with the 10kΩ resistor to create a low-pass hardware filter, or implement a software moving-average filter in the loop().

Extending and Simplifying the Build

Depending on your end goal, you can strip this project down to its bare essentials or scale it up into an IoT environmental monitor.

How to Simplify

If you only need to know if a room is "light" or "dark" to trigger a relay (like a closet light), drop the BH1750 entirely. Use only the LDR and an LM393 comparator IC. Set the comparator's reference voltage with a potentiometer, and feed the digital output to an Arduino interrupt pin. This removes I2C overhead and frees up the microcontroller to sleep, drawing microamps instead of milliamps.

How to Extend

To turn this into a long-term datalogger, swap the Arduino Uno R3 for an ESP32-DevKitC V4. The ESP32 operates at 3.3V logic, meaning you can wire a bare BH1750FVI directly without a level-shifting breakout board.
Extension Steps:

  • Update the I2C pins in code: Wire.begin(21, 22); (Default ESP32 I2C pins).
  • Add the WiFi.h and HTTPClient.h libraries.
  • Push the CSV data via HTTP POST to an InfluxDB instance or a free MQTT broker like HiveMQ every 60 seconds.
  • Utilize the ESP32's deep sleep capabilities, waking only via an RTC timer to sample the light, dropping average power consumption below 50µA for battery-powered outdoor deployments.

By understanding the raw I2C protocol and the analog voltage divider physics, you can adapt this Arduino light detector to any environment, from a simple bedroom night-light trigger to a precision greenhouse lux monitor.