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.
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) |
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
- Seat the Nano: Press the Arduino Nano v3 into the breadboard, ensuring pins on both sides align with the center trench.
- Wire Power Rails: Connect Nano
5Vto the red breadboard rail andGNDto the blue rail. Do this on both sides if your board has split rails. - Connect the BH1750: Route jumper wires from the Nano's
A4(SDA) andA5(SCL) to the GY-302 module. Connect VCC to 5V and GND to GND. Leave the ADDR pin unconnected. - 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.
- Route the Analog Signal: Run a jumper wire from the shared LDR/Resistor node to the Nano's
A0pin. - 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.
- Verify the I2C Address: The GY-302 module defaults to
0x23. If the ADDR pin is accidentally bridged to VCC, it shifts to0x5C. Run an I2C Scanner sketch to see what the bus actually sees. - 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.
- 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
SdFatlibrary for lower RAM overhead than the defaultSDlibrary. - 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
PubSubClientlibrary to publish thelux_digitalpayload 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.






