In this blog post I will show how you can build a Digital Lux Meter with Arduino designed to measure light intensity. It uses an LDR (Light Dependent Resistor) as the primary sensor, an LM358 Op-Amp for signal conditioning, and an Arduino Uno to process the data and display it on an OLED screen.
Applications
The Digital Lux Meter is a versatile tool used across various industries to ensure optimal lighting conditions, ranging from agricultural monitoring, where it helps maintain the specific light intensity required for greenhouse crops, to workplace ergonomics, ensuring offices and schools meet safety standards to prevent eye strain. In the field of photography and cinematography, it allows creators to balance exposure and achieve consistent scene lighting, while in energy management, it can be integrated into "smart home" systems to automate lighting based on natural ambient levels, significantly reducing electricity consumption.
Furthermore, for electronics hobbyists and developers, this project serves as a practical application of signal conditioning using op-amps, providing a high-precision method for testing LED brightness or calibrating light-sensitive automated systems.
Hardware and Circuit Schematic
The Arduino based Lux meter circuit diagram is as shown below:
Now I will explain how this lux meter circuit works.
How the Circuit Works
1. The Sensor Stage (LDR + Voltage Divider)
The LDR1 and the potentiometer RV2 form a variable voltage divider.
LDR1: Its resistance decreases as light intensity increases.
RV2: This acts as a sensitivity adjustment. By changing its resistance, you change the "baseline" voltage that goes into the Op-Amp.
2. The Signal Conditioning (LM358 Op-Amp)
The LM358 is configured as a Non-Inverting Amplifier.
Input: The voltage from the LDR divider enters Pin 3 (Non-inverting input).
Gain Control: RV1 potentiometer is connected in the feedback loop. By adjusting RV1, you control the Gain (amplification factor) of the circuit. This allows you to stretch or shrink the voltage range that the Arduino sees at pin
A0.
3. The Processing & Display (Arduino + OLED)
The Arduino reads the analog voltage from the Op-Amp output (Pin 1) via pin A0.
The code converts this 0–5V analog signal into a digital value (0–1023) and then maps it to a "Illuminance" value (0–1000 lm).
The SSD1306 OLED displays the calculated Luminous Flux and the raw Reference Voltage (Vref).
How to Tune RV1 and RV2 for the Proper Range
Tuning this circuit is about balancing Sensitivity (RV2) and Scale (RV1).
Step 1: Set the Dark Baseline (RV2)
Action: Cover the LDR so it is in total darkness.
Tuning: Adjust RV2 until the OLED shows a value very close to 0 lm and the Vref is at its lowest point (around 0.0V or 0.1V).
Purpose: This ensures your meter starts at zero when there is no light.
Step 2: Set the Maximum Brightness (RV1)
Action: Shine your brightest light source (or the "Torch") directly onto the LDR.
Tuning: Adjust RV1 (the Gain pot) until the OLED displays your desired maximum value (e.g., 900–1000 lm). You want the
Vrefto be near4.5Vto4.8Vat maximum light.Note: If Vref hits 5.0V too early, turn RV1 down. If the value is too low even in bright light, turn RV1 up to increase the gain.
Step 3: Verify the Mid-Range
Move the light source to a medium distance. If the reading drops too fast or stays too high, slightly tweak RV1 again.
Final Calibration: Once you are happy with the physical tuning, update your Arduino code's
mapfunction to match your results:
Summary of Potentiometer Roles
| Component | Function | Effect |
| RV2 | Sensitivity | Shifts the whole range up or down. Controls the "Zero" point. |
| RV1 | Gain (Range) | Expands or contracts the scale. Controls how high the "Max" goes. |
Programming & Code
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 OLEDLCD(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, 400000, 400000, OLED_RESET);
const int sensorPin = A0;
void setup() {
if(!OLEDLCD.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
for(;;);
}
OLEDLCD.clearDisplay();
OLEDLCD.setTextColor(SSD1306_WHITE);
OLEDLCD.setTextSize(2);
OLEDLCD.setCursor(10, 0);
OLEDLCD.print(F("LUX METER"));
OLEDLCD.setTextSize(1);
OLEDLCD.setCursor(28, 22);
OLEDLCD.print(F("ee-diary.net"));
OLEDLCD.display();
delay(3000);
}
void loop() {
int rawValue = analogRead(sensorPin);
float voltage = rawValue * (5.0 / 1023.0);
int luxValue = map(rawValue, 11, 719, 0, 1000);
luxValue = constrain(luxValue, 0, 1000);
OLEDLCD.clearDisplay();
// --- HEADER ---
OLEDLCD.setTextSize(1);
OLEDLCD.setCursor(0, 0);
OLEDLCD.print(F("Illuminance:"));
// --- MAIN READING (BIG) ---
OLEDLCD.setTextSize(2);
OLEDLCD.setCursor(0, 14);
OLEDLCD.print(luxValue);
// --- DYNAMIC UNIT (SMALL) ---
OLEDLCD.setTextSize(1);
int unitX;
// Large digits (size 2) are 12 pixels wide
if (luxValue >= 1000) unitX = 48;
else if (luxValue >= 100) unitX = 36;
else if (luxValue >= 10) unitX = 24;
else unitX = 12;
// Setting Y to 20 so the small "lx" aligns with the bottom of the big numbers
OLEDLCD.setCursor(unitX + 2, 20);
OLEDLCD.print(F("lx"));
// --- FORMATTED Vref (SMALL) ---
OLEDLCD.setCursor(75, 22);
OLEDLCD.print(F("Vref:"));
OLEDLCD.print(voltage, 1);
OLEDLCD.print(F("V"));
OLEDLCD.display();
delay(250);
}
Here is the breakdown of how the program works section by section:
1. Libraries and Definitions
#include <Wire.h> // Handles I2C communication
#include <Adafruit_GFX.h> // Core graphics library
#include <Adafruit_SSD1306.h> // Specific library for the OLED driver
Wire: Essential because the OLED uses only two wires (SDA/SCL) to talk to the Arduino.
SSD1306: This manages the specific pixels on your 128x32 display.
2. The Setup Function (Initialization)
void setup() {
OLEDLCD.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
// Startup screen logic...
}
OLEDLCD.begin(): Wakes up the display. If the address (0x3C) is wrong, the screen stays black.Startup Screen: We show the "LUX METER" title and your website URL for 3 seconds. This gives the LDR and Op-Amp time to stabilize their voltages before the first reading.
3. Data Acquisition (The loop)
int rawValue = analogRead(sensorPin);
float voltage = rawValue * (5.0 / 1023.0);
analogRead: Converts the voltage on Pin A0 (0V to 5V) into a number between 0 and 1023.Voltage Calculation: This math converts that 0–1023 number back into a human-readable voltage (e.g.,
0.9V) so you can see exactly what the Op-Amp is outputting.
4. Calibration & Mapping
int luxValue = map(rawValue, 11, 719, 0, 1000);
luxValue = constrain(luxValue, 0, 1000);
map: This is the heart of the "Meter." It takes your specific circuit range (where 11 is "dark" and 719 is "bright") and scales it to a clean 0–1000 Lux scale.constrain: This prevents the display from showing weird negative numbers or values over 1000 if the light gets unexpectedly bright or dark.
5. Dynamic Display Logic
This is the most complex part of your specific code:
if (luxValue >= 1000) unitX = 48;
else if (luxValue >= 100) unitX = 36;
// ... and so on
The "Sticky" Unit: Since
setTextSize(2)characters are 12 pixels wide, we calculate the width of the digits.unitX: This moves the cursor for the "lx" unit so it always stays exactly next to the numbers, whether the reading is5or555.
6. Rendering the Vref
OLEDLCD.setCursor(75, 22);
OLEDLCD.print(F("Vref:"));
OLEDLCD.print(voltage, 1);
Placement: By setting the cursor to
(75, 22), we push the technical info to the bottom-right corner.voltage, 1: The,1tells the Arduino to only show one digit after the decimal point (e.g.,0.9), which keeps the text short and prevents it from overlapping with your Lux reading.
7. The Final Push
OLEDLCD.display();
Crucial Step: All the
printcommands only draw to the Arduino's internal memory (the buffer). Thedisplay()command is what actually sends all that data over the I2C wires to make the pixels light up on the physical screen.
