If you are looking for a reliable Arduino light sensor example, skip the messy analog photoresistors (LDRs) and go straight to digital I2C. This guide walks through building a precision lux meter using the BH1750FVI digital ambient light sensor, specifically the widely available GY-302 breakout module. Unlike an LDR that outputs an arbitrary analog voltage requiring complex calibration, the BH1750 communicates directly in standard Lux units, features a spectral response that closely matches the human eye, and operates over a massive 1 to 65,535 lux range.

Target Board Variant: This code and wiring map directly to the Arduino Uno R3 and Arduino Nano v3 (both utilizing the ATmega328P microcontroller). If you are using an ESP32 or Raspberry Pi Pico, see the FAQ section at the bottom for I2C pin adjustments.

Difficulty Rating: ★★☆☆☆ (Beginner-Intermediate)
Time to Build: 15 minutes
Core Concepts: I2C protocol, hardware interrupts, raw Wire.h register mapping

Parts List & Specification Sheet

Before we wire anything up, verify you have the exact hardware variants listed below. The BH1750FVI is the actual silicon chip; the GY-302 is the breakout board that adds the necessary voltage regulation and pull-up resistors to make it breadboard-friendly.

Component Exact Model / Variant Key Specification Approx. Price (2026)
Microcontroller Arduino Uno R3 (or Nano v3) ATmega328P, 5V Logic, 16MHz $15.00 - $24.00
Light Sensor Module GY-302 (BH1750FVI chip) I2C Interface, 1-65535 Lux, 1 Lux Resolution $3.00 - $5.00
Wiring Dupont Jumper Wires (M-to-M) 24 AWG stranded, 20cm length $4.00 (pack)
Prototyping Half-size Solderless Breadboard 400 tie-points $5.00

Pin Mapping & Wiring Steps

The BH1750 communicates via the I2C bus. On the ATmega328P (Uno/Nano), the hardware I2C pins are hardcoded to A4 (SDA) and A5 (SCL). The GY-302 module features an onboard 3.3V LDO regulator, meaning you can safely power it from the Arduino's 5V pin, though feeding it 3.3V is technically cleaner for the I2C logic lines.

Pin Mapping Table:

GY-302 Module Pin Arduino Uno R3 Pin Function / Notes
VCC 5V (or 3.3V) Power input (module regulates down to 3.3V internally)
GND GND Common ground reference
SCL A5 I2C Clock Line
SDA A4 I2C Data Line
ADDR GND (or leave floating) Sets I2C address to 0x23. Tie to VCC for 0x5C.

Numbered Wiring Steps:

  1. Insert the GY-302 module into the breadboard, straddling the center trench.
  2. Connect the GND pin on the module to any GND rail on the Arduino.
  3. Connect the VCC pin to the Arduino 5V output.
  4. Run a jumper from the module's SCL to Arduino pin A5.
  5. Run a jumper from the module's SDA to Arduino pin A4.
  6. Ensure the ADDR pin is either unconnected or tied to GND. This locks the sensor's I2C address to 0x23.
Callout Tip: 5V Logic vs 3.3V Silicon
The BH1750FVI silicon is strictly a 3.3V device. While the GY-302 breakout handles the power regulation, the Arduino Uno pushes 5V logic on the SDA/SCL lines. For occasional bench testing, this is fine. For a permanent installation, use a bi-directional logic level converter (like the BSS138 module) between the Uno and the sensor to prevent long-term degradation of the sensor's I2C transceivers.

Complete Compilable Code (Raw I2C)

Most tutorials rely on third-party libraries that eventually break or cause dependency conflicts. To guarantee this Arduino light sensor example compiles cleanly on any machine, we are using raw I2C commands via the built-in Wire.h library. This directly queries the sensor's registers as defined in the Rohm Semiconductor BH1750FVI Datasheet.

#include <Wire.h>

// Pin definitions for documentation (Uno/Nano use hardware I2C on A4/A5)
const int PIN_SDA = A4;
const int PIN_SCL = A5;

// BH1750 I2C Address (ADDR pin tied to GND or floating)
const uint8_t BH1750_ADDRESS = 0x23;

// BH1750 Command Bytes (from Rohm Datasheet)
const uint8_t BH1750_POWER_ON = 0x01;
const uint8_t BH1750_CONTINUOUS_H_RES_MODE = 0x10; // 1 lux resolution, ~120ms measurement time

void setup() {
  Serial.begin(115200);
  while (!Serial) { 
    delay(10); // Wait for serial port to connect (needed for Leonardo/Micro)
  }

  Wire.begin();
  // Wire.setClock(400000); // Uncomment for 400kHz Fast Mode

  // Step 1: Power on the sensor
  Wire.beginTransmission(BH1750_ADDRESS);
  Wire.write(BH1750_POWER_ON);
  uint8_t error = Wire.endTransmission();

  // Error handling: Check if sensor acknowledges I2C address
  if (error != 0) {
    Serial.println("FATAL: Sensor not found on I2C bus at 0x23. Check wiring and ADDR pin.");
    while(1) { 
      delay(1000); // Halt execution to prevent bus spam
    }
  }

  // Step 2: Set measurement mode
  Wire.beginTransmission(BH1750_ADDRESS);
  Wire.write(BH1750_CONTINUOUS_H_RES_MODE);
  Wire.endTransmission();

  Serial.println("BH1750 Initialized. Reading continuous high-res mode...");
}

void loop() {
  // Request 2 bytes of data from the sensor
  uint8_t bytesReceived = Wire.requestFrom(BH1750_ADDRESS, (uint8_t)2);

  // Error handling: Verify we actually got 2 bytes back
  if (bytesReceived == 2) {
    uint8_t highByte = Wire.read();
    uint8_t lowByte = Wire.read();

    // Combine bytes and divide by 1.2 per datasheet formula
    float lux = ((highByte << 8) | lowByte) / 1.2;

    Serial.print("Illuminance: ");
    Serial.print(lux, 1);
    Serial.println(" lx");
  } else {
    Serial.println("ERROR: I2C Read Failed - Sensor returned < 2 bytes.");
  }

  delay(500); // Measurement time is ~120ms; 500ms loop is safe and readable
}

Debugging: First 3 Things to Check When It Fails

When working with I2C sensors on the bench, things occasionally go wrong. If your serial monitor is throwing errors, do not immediately rewrite the code. Follow this diagnostic sequence.

The First 3 Things to Check

  1. Verify the ADDR Pin State: The BH1750 has two possible I2C addresses. If the ADDR pin is Low (GND) or floating, the address is 0x23. If it is High (VCC), the address shifts to 0x5C. If your code is looking for 0x23 but the pin is accidentally touching the VCC rail, the sensor will ignore the microcontroller.
  2. Check for Pull-Up Resistors: I2C requires pull-up resistors on the SDA and SCL lines. The GY-302 breakout board includes 4.7kΩ surface-mount pull-ups. However, if you are wiring a raw BH1750FVI chip directly without the breakout board, the bus will float, and you will get garbage data. Add 4.7kΩ resistors from SDA to VCC and SCL to VCC.
  3. Run an I2C Scanner: Before blaming the sensor, upload the standard Arduino I2C Scanner sketch. If the scanner returns No I2C devices found, you have a physical wiring fault or a dead power rail. If it returns 0x5C, your ADDR pin is pulled high.

Exact Error Strings & Ranked Causes

If you encounter these specific errors in the Arduino IDE or Serial Monitor, here is how to fix them:

Error 1: fatal error: Wire.h: No such file or directory

  • Cause 1 (Most Likely): You have selected a generic or unsupported board in Tools > Board that lacks the standard AVR core libraries. Ensure "Arduino Uno" is selected.
  • Cause 2: Your Arduino IDE installation is corrupted. Reinstall the IDE or update the AVR board package via the Boards Manager.

Error 2: ERROR: I2C Read Failed - Sensor returned < 2 bytes. (Serial Output)

  • Cause 1 (Most Likely): I2C bus lockup. The sensor missed a clock pulse and is holding the SDA line low. Power cycle the Arduino and the sensor completely.
  • Cause 2: Voltage brownout. If you are powering the Arduino via USB from an unpowered hub, the 5V rail may be dipping below 4.5V under load, causing the sensor's internal ADC to stall.
  • Cause 3: SDA and SCL wires are swapped. Double-check that A4 is Data and A5 is Clock.

Scaling the Build: Simplify or Extend

Depending on your end goal, you might not need a $5 digital I2C sensor, or you might need far more data than just ambient lux. Here is how to pivot this build.

How to Simplify (The Analog LDR Route):
If you only need to detect "Is it dark or light?" (e.g., triggering a nightlight), strip out the BH1750. Use a standard GL5528 Photoresistor ($0.50). Wire one leg to 5V, the other leg to a 10kΩ pulldown resistor connected to GND. Read the junction between the LDR and the 10kΩ resistor using analogRead(A0). You will get a raw 0-1023 value. It is non-linear and temperature-dependent, but perfectly adequate for basic threshold triggering.

How to Extend (Multi-Sensor Environmental Logging):
Because I2C is a bus protocol, you can daisy-chain multiple sensors. Add a BME280 (Temperature/Humidity/Pressure) to the exact same A4/A5 pins. The BME280 uses address 0x76 or 0x77, so it will not collide with the BH1750's 0x23. You can log combined environmental data to an SD card module or push it via MQTT using an ESP32 upgrade.

Frequently Asked Questions

Can I use this Arduino light sensor example with an ESP32?

Yes, but you must adjust the I2C pin definitions. The ESP32 does not use A4/A5 for hardware I2C. On a standard 30-pin ESP32 DevKit V1, the default I2C pins are GPIO 21 (SDA) and GPIO 22 (SCL). Because the ESP32 is a native 3.3V device, you can safely connect the BH1750 directly to the 3.3V pin without worrying about 5V logic degradation. In the code above, simply change the initialization to Wire.begin(21, 22);.

Why is my analog LDR giving random values compared to this BH1750 example?

Analog LDRs (like the GL5516 or GL5528) are made of Cadmium Sulfide (CdS). Their resistance changes based on light, but their spectral response is heavily skewed toward green/yellow light and they suffer from "memory effects" (hysteresis) where they take seconds to recover from a bright flash. The BH1750 uses a specialized photodiode array with an integrated ADC and a filter that mimics the human eye's photopic vision curve. If you need consistent, repeatable Lux measurements for plant grow tents or photography lighting, the BH1750 is mandatory; the LDR is useless for absolute metrics.

How do I change the I2C address to use two light sensors on the same bus?

The BH1750 supports exactly two addresses: 0x23 and 0x5C. To use two sensors simultaneously, wire the first sensor's ADDR pin to GND (address 0x23) and the second sensor's ADDR pin to VCC (address 0x5C). You will need to instantiate two separate measurement sequences in your loop(), alternating between Wire.requestFrom(0x23, 2) and Wire.requestFrom(0x5C, 2). Note that if you need more than two sensors, you will need to introduce an I2C Multiplexer like the TCA9548A.