When building a photo sensor Arduino project, you generally face a choice between cheap analog photoresistors (LDRs) and precise digital ambient light sensors (ALS). If you need raw threshold detection—like turning on a relay when the sun sets—a GL5528 Cadmium Sulfide (CdS) LDR costs pennies and works fine. But if you need actual lux measurements for plant grow-tents, smart blinds, or display dimming, the BH1750FVI digital I2C sensor is the undisputed benchmark for hobbyists. It features a built-in 16-bit ADC and a spectral response tuned to the human eye's luminosity function (V-lambda), outputting direct lux values without messy analog calibration.

This guide walks through a dual-sensor datalogger build targeting the Arduino Nano v3 (ATmega328P, 5V/16MHz). We will wire both sensors, write robust C++ code with hardware fault detection, and dive deep into debugging the most common I2C bus failures.

Difficulty: Intermediate | Time: 45 Minutes | Cost: ~$12 USD

Photo Sensor Technology Comparison & Spec Sheet

Before wiring the breadboard, it is critical to understand the physical limitations of your sensor. Analog LDRs suffer from slow response times and temperature drift, while digital sensors like the BH1750 handle AC mains flicker (50/60Hz) internally via their integration windows. Below is a data-dense spec sheet comparing the four most common photo sensors in the Arduino ecosystem.

Sensor Module Interface Lux Range / Resolution Spectral Peak Active Current Response / Integration Time
BH1750FVI (GY-302) I2C (0x23 / 0x5C) 1 - 65,535 lux (1 lx res) 550nm (Green/V-lambda) 190 µA (Active) 120ms (Default MTreg)
GL5528 (Bare LDR) Analog (Resistance) 10kΩ - 20kΩ @ 10 lux 540nm (Green) Passive (Depends on divider) ~20ms (Rise/Fall)
TSL2561 I2C / SMBus 0.1 - 40,000+ lux (16-bit) Broadband + IR channels 300 µA (Active) 13.7ms to 402ms
TEMT6000 Analog (Phototransistor) 0 - 1000 lux (Linear) 570nm (Yellow-Green) ~0.5mA (with 10kΩ load) 15µs (Microseconds)
Pro-Tip on BH1750 MTreg: The BH1750's Measurement Time register (MTreg) defaults to 69. If you are measuring very low light (e.g., moonlight or starlight), you can push the MTreg to 138 in software. This doubles the resolution but increases the measurement time to ~240ms.

Parts List & Pin Mapping for Arduino Nano v3

This build uses the Arduino Nano v3 because its compact footprint and 5V logic make it ideal for breadboard prototyping, while still exposing the hardware I2C pins on A4/A5. Note: If you are using an ESP32 DevKit v1, you will need a logic level shifter for the 5V I2C lines, or run the BH1750 strictly on 3.3V.

Exact Bill of Materials (BOM)

  • MCU: Arduino Nano v3 (ATmega328P, 16MHz, 5V logic) with USB-C or Mini-B header.
  • Digital Sensor: GY-302 BH1750FVI module (includes onboard 3.3V LDO and I2C pull-ups).
  • Analog Sensor: GL5528 Photoresistor (10-20kΩ at 10 lux).
  • Passives: 1x 10kΩ through-hole resistor (for LDR voltage divider), 1x 100nF ceramic capacitor (optional, for analog noise filtering).
  • Hardware: 400-point solderless breadboard, male-to-male jumper wires.

Pin Mapping Table

Component Module Pin Arduino Nano v3 Pin Notes / Constraints
BH1750 VCC 5V Module has onboard LDO; 5V is safe.
BH1750 GND GND Shared ground with Nano and LDR.
BH1750 SCL A5 Hardware I2C Clock.
BH1750 SDA A4 Hardware I2C Data.
BH1750 ADDR NC (Floating) Leave floating for I2C address 0x23.
GL5528 LDR Leg 1 5V Top of voltage divider.
GL5528 LDR Leg 2 (Node) A0 Mid-point to Analog In 0.
10kΩ Resistor Leg 1 A0 Shares node with LDR Leg 2.
10kΩ Resistor Leg 2 GND Bottom of voltage divider.

Step-by-Step Wiring & Assembly

  1. Seat the Nano: Press the Arduino Nano v3 into the breadboard, ensuring pins on both sides align with the center trench.
  2. Wire Power Rails: Connect Nano 5V to the red breadboard rail and GND to the blue rail. Do this on both sides if your board has split rails.
  3. Connect the BH1750: Route jumper wires from the Nano's A4 (SDA) and A5 (SCL) to the GY-302 module. Connect VCC to 5V and GND to GND. Leave the ADDR pin unconnected.
  4. Build the LDR Voltage Divider: Insert the GL5528 LDR into the breadboard. Connect one leg to the 5V rail. Insert the 10kΩ resistor so one leg shares the same breadboard row as the LDR's second leg. Connect the other leg of the 10kΩ resistor to GND.
  5. Route the Analog Signal: Run a jumper wire from the shared LDR/Resistor node to the Nano's A0 pin.
  6. Verify Connections: Use a multimeter in continuity mode to verify that the GND rail is continuous and that SDA/SCL are not shorted to VCC before plugging in the USB cable.

Complete Compilable C++ Code with Error Handling

The following code targets the Arduino Nano v3. It relies on the standard Arduino Wire library and the widely used claws/BH1750 library (install via Arduino Library Manager). It includes hardware fault detection: if the I2C bus hangs or the sensor NACKs, the code falls back to the analog LDR and flags a warning via the onboard LED and Serial monitor.


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

// --- Pin Definitions ---
const int LDR_ANALOG_PIN = A0;
const int STATUS_LED_PIN = 13; // Nano onboard LED

// --- Global Objects ---
BH1750 lightMeter;
bool bh1750_ok = false;

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 2000) { delay(10); } // Wait for serial monitor
  
  pinMode(STATUS_LED_PIN, OUTPUT);
  digitalWrite(STATUS_LED_PIN, LOW);

  // Initialize I2C bus
  Wire.begin();
  
  // Attempt BH1750 initialization with error handling
  if (lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
    Serial.println(F("[OK] BH1750 initialized on I2C (0x23)."));
    bh1750_ok = true;
  } else {
    Serial.println(F("[ERROR] BH1750 not found! Check SDA/SCL wiring and pull-ups."));
    Serial.println(F("[WARN] Falling back to analog GL5528 LDR only."));
    bh1750_ok = false;
    // Blink LED to indicate hardware fault
    for(int i=0; i<5; i++) {
      digitalWrite(STATUS_LED_PIN, HIGH); delay(100);
      digitalWrite(STATUS_LED_PIN, LOW); delay(100);
    }
  }
}

void loop() {
  float lux_digital = -1.0;
  int raw_analog = 0;
  float voltage_analog = 0.0;
  
  // 1. Read Digital Sensor (if healthy)
  if (bh1750_ok) {
    lux_digital = lightMeter.readLightLevel();
    // Check for I2C read timeout/failure (library returns negative on fail)
    if (lux_digital < 0) {
      Serial.println(F("[ERROR] I2C read failed mid-operation. Bus locked?"));
      bh1750_ok = false; // Disable further reads to prevent loop hangs
    }
  }
  
  // 2. Read Analog LDR (Always active as fallback)
  raw_analog = analogRead(LDR_ANALOG_PIN);
  voltage_analog = (raw_analog * 5.0) / 1023.0;
  
  // 3. Format and Print Telemetry
  Serial.print(F("Time: ")); Serial.print(millis() / 1000); Serial.print(F("s | "));
  
  if (lux_digital >= 0) {
    Serial.print(F("BH1750: ")); Serial.print(lux_digital, 1); Serial.print(F(" lx | "));
  } else {
    Serial.print(F("BH1750: OFFLINE | "));
  }
  
  Serial.print(F("LDR Raw: ")); Serial.print(raw_analog);
  Serial.print(F(" (")); Serial.print(voltage_analog, 2); Serial.println(F("V)"));
  
  // 4. Simple Threshold Logic (Turn on LED if dark)
  if (raw_analog < 300) { // Approx < 5 lux depending on divider
    digitalWrite(STATUS_LED_PIN, HIGH);
  } else {
    digitalWrite(STATUS_LED_PIN, LOW);
  }
  
  delay(1000); // 1Hz sampling rate
}

Debugging: I2C NACKs, Address Conflicts & Analog Noise

Embedded hardware rarely works perfectly on the first power-up. If your serial monitor is throwing errors, follow this diagnostic tree. According to TI's I2C Troubleshooting Guidelines, bus lockups and NACKs are almost always physical layer issues, not software bugs.

The First Three Things to Check When It Fails:
  1. Verify the I2C Address: The GY-302 module defaults to 0x23. If the ADDR pin is accidentally bridged to VCC, it shifts to 0x5C. Run an I2C Scanner sketch to see what the bus actually sees.
  2. Check for Pull-Up Resistors: The I2C spec requires open-drain lines pulled HIGH. While genuine GY-302 modules have 4.7kΩ onboard pull-ups, cheap clone boards often omit them. Measure SDA/SCL to VCC with a multimeter; if it reads 0V or floats, add external 4.7kΩ resistors.
  3. Inspect Logic Level Voltages: The BH1750 chip is strictly 3.3V. The GY-302 module handles 5V via an LDO, but if you are wiring a raw BH1750 chip to a 5V Nano, you will fry the silicon or cause silent NACKs. Use a logic level shifter (like the BSS138 bidirectional module) if bypassing the carrier board.

Ranked Causes for Exact Error Strings

Exact Error String / Symptom Root Cause Hardware / Software Fix
Wire.endTransmission() returned 2 Received NACK on transmit of address. The Nano sent 0x23, but no device acknowledged. Check SDA/SCL continuity. Verify ADDR pin is floating. Ensure module has 3.3V power at the chip.
Wire.endTransmission() returned 3 Received NACK on transmit of data. Device acknowledged address, but rejected the config register byte. Usually a timing issue. Add Wire.setClock(100000); in setup to force standard 100kHz mode.
[ERROR] I2C read failed mid-operation Bus locked up. SDA line is being held LOW by the sensor (clock stretching failure). Power cycle the sensor. In code, implement a Wire bus reset by toggling SCL manually 9 times.
Analog LDR value jumps erratically (e.g., 400 to 800) 60Hz AC mains flicker from room lighting hitting the CdS sensor, or a floating breadboard node. Add a 100nF capacitor in parallel with the 10kΩ pull-down resistor. Average 16 analog reads in software.

Extending and Simplifying the Build

Once you have baseline telemetry streaming to the Serial monitor, you can adapt this circuit to fit your specific project constraints.

How to Simplify (For Basic Threshold Switching)

If you only need to trigger a relay when it gets dark (e.g., automatic chicken coop door or porch light), drop the BH1750 entirely. The GL5528 LDR combined with a 10kΩ resistor and a simple if (analogRead(A0) < threshold) statement is vastly cheaper, requires no I2C library overhead, and is immune to I2C bus lockups. You can even replace the Arduino entirely with a 555 timer or an LM393 comparator module for a purely analog, zero-code solution.

How to Extend (For IoT & Datalogging)

  • Add an SD Card Module: Wire a MicroSD card adapter via SPI (Pins 10-13 on the Nano) to log lux values every 5 minutes for a month. Use the SdFat library for lower RAM overhead than the default SD library.
  • Upgrade to ESP32 for MQTT: Swap the Nano for an ESP32-WROOM-32 DevKit v1. The code above compiles identically (just change the I2C pins to GPIO 21/22), but you gain WiFi. Use the PubSubClient library to publish the lux_digital payload to a Home Assistant MQTT broker for automated smart-blind triggers.
  • Implement High-Resolution Mode: For indoor plant monitoring where light levels are low, change the initialization in the code to BH1750::CONTINUOUS_HIGH_RES_MODE_2. This yields 0.5 lux resolution, allowing you to detect the difference between a single grow-light bulb and dual-bulb configurations.