Skip to content
Home » Embedded Systems » Projects » Automatic Light ON-OFF based on presence of people

Automatic Light ON-OFF based on presence of people

Complete circuit diagram for automatic light on-off system using two IR sensor modules connected to an Arduino UNO R3, with an LCD display for people count, BC547 transistor driving a 5V relay to switch 220VAC lamp and fan loads
Embedded Systems Learning Path
Part 63 of 129View Full Path →

KEY TAKEAWAYS

  • An LDR (Light Dependent Resistor) changes resistance based on ambient light: low resistance in bright light, high resistance in darkness.
  • A voltage divider with an LDR and a fixed resistor converts the resistance change into an analog voltage readable by the ADC.
  • Hysteresis (separate ON and OFF thresholds) prevents rapid flickering when ambient light is near the switching point.
  • PWM-based dimming provides gradual brightness control instead of simple ON/OFF switching, improving energy efficiency.
  • Use a MOSFET or relay driver (not a direct MCU pin) to switch loads, since MCU GPIO pins can only source a few milliamps.

Automatic light control systems save energy by turning lights on only when needed. This project uses a Light Dependent Resistor (LDR) to measure ambient brightness and a microcontroller to control a light through a relay or MOSFET. We will cover the sensor physics, complete circuit design, C firmware with hysteresis, PWM dimming, and practical enhancements for a production-quality system.

LDR Working Principle

An LDR (also called a photoresistor) is made from a semiconductor material, typically cadmium sulfide (CdS). When photons hit the material, they excite electrons from the valence band to the conduction band, creating free charge carriers and reducing resistance. In bright sunlight, an LDR typically has a resistance of 1-10 kilohms. In darkness, its resistance rises to 1-10 megohms.

The resistance vs light relationship is approximately logarithmic. Doubling the light intensity does not halve the resistance linearly; instead, the resistance follows a power law: R = R_ref * (Lux / Lux_ref) ^ (-gamma), where gamma is typically 0.6 to 0.9 depending on the specific LDR. This non-linear response means the LDR is most sensitive to changes in low-light conditions, which is exactly what we want for an automatic light controller.

LDRs have a slow response time (10-100 milliseconds to adjust to changes), which is fine for ambient light monitoring but too slow for applications like optical communication. They are also temperature-sensitive: resistance changes by 1-2% per degree Celsius, which can cause false switching on hot days if thresholds are not properly calibrated.

Circuit Design: Voltage Divider with LDR

To read the LDR with a microcontroller ADC, we form a voltage divider with the LDR and a fixed resistor. The arrangement determines whether the voltage increases or decreases with light:

LDR on top, fixed resistor on bottom (LDR between VCC and ADC pin, fixed resistor between ADC pin and GND): Voltage at ADC pin increases when it gets darker (higher LDR resistance means more voltage drop across the fixed resistor stays the same, and more voltage appears across the LDR, but actually the ADC pin sees the voltage across the fixed resistor, which decreases). Let me clarify: Vout = VCC * R_fixed / (R_LDR + R_fixed). In bright light, R_LDR is low, so Vout is high. In darkness, R_LDR is high, so Vout is low.

Fixed resistor on top, LDR on bottom: Vout = VCC * R_LDR / (R_fixed + R_LDR). In bright light, R_LDR is low, so Vout is low. In darkness, R_LDR is high, so Vout is high. Choose the arrangement that suits your logic: the first arrangement gives a high reading in daylight, the second gives a high reading in darkness.

Choose a fixed resistor value close to the LDR resistance at your desired switching point. For a switching point around dusk (approximately 50-100 lux), an LDR typically has a resistance of 10-50 kilohms, so a 10k or 47k fixed resistor works well. This maximizes the voltage swing around your threshold and provides the best ADC resolution where you need it most.

Complete C Code: ADC Read with Threshold Control

Here is a complete implementation for STM32 using HAL that reads the LDR, applies hysteresis to prevent flickering, and controls a relay via a GPIO pin:

/* Automatic Light Controller with LDR - STM32 HAL */
#include "stm32f4xx_hal.h"
#include <stdio.h>
#include <stdbool.h>

/* Pin definitions */
#define RELAY_PIN       GPIO_PIN_5
#define RELAY_PORT      GPIOA

/* ADC thresholds (12-bit ADC: 0-4095) */
/* Assuming LDR on top, fixed resistor on bottom:
   High ADC = bright (light OFF), Low ADC = dark (light ON) */
#define THRESHOLD_ON    1500   /* Turn light ON below this (getting dark) */
#define THRESHOLD_OFF   2000   /* Turn light OFF above this (getting bright) */
/* The gap between ON and OFF thresholds is the hysteresis band */

extern ADC_HandleTypeDef hadc1;

static bool light_is_on = false;

uint16_t read_ldr_adc(void) {
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 10);
    uint16_t adc_val = HAL_ADC_GetValue(&hadc1);
    HAL_ADC_Stop(&hadc1);
    return adc_val;
}

/* Average multiple readings to filter noise */
uint16_t read_ldr_filtered(uint8_t samples) {
    uint32_t sum = 0;
    for (uint8_t i = 0; i < samples; i++) {
        sum += read_ldr_adc();
        HAL_Delay(5);
    }
    return (uint16_t)(sum / samples);
}

void relay_on(void) {
    HAL_GPIO_WritePin(RELAY_PORT, RELAY_PIN, GPIO_PIN_SET);
    light_is_on = true;
}

void relay_off(void) {
    HAL_GPIO_WritePin(RELAY_PORT, RELAY_PIN, GPIO_PIN_RESET);
    light_is_on = false;
}

void automatic_light_control(void) {
    uint16_t adc_val = read_ldr_filtered(8);

    /* Hysteresis logic: different thresholds for ON and OFF */
    if (!light_is_on && adc_val < THRESHOLD_ON) {
        /* It is dark enough - turn light ON */
        relay_on();
        printf("Light ON  (ADC: %d)rn", adc_val);
    }
    else if (light_is_on && adc_val > THRESHOLD_OFF) {
        /* It is bright enough - turn light OFF */
        relay_off();
        printf("Light OFF (ADC: %d)rn", adc_val);
    }
    /* If ADC is between THRESHOLD_ON and THRESHOLD_OFF,
       maintain current state (hysteresis prevents flickering) */
}

/* Main loop */
void light_control_main(void) {
    while (1) {
        automatic_light_control();
        HAL_Delay(1000);  /* Check every 1 second */
    }
}

Hysteresis: Preventing Flickering

Without hysteresis, when ambient light is near the threshold, small fluctuations cause the light to rapidly toggle ON and OFF. This is annoying, wastes energy, and can damage relay contacts. Hysteresis introduces a dead band between the ON threshold and the OFF threshold. The light turns ON when brightness drops below the lower threshold and does not turn OFF until brightness rises above the higher threshold. The gap between the two thresholds should be 10-20% of the ADC range for reliable operation.

Think of it like a thermostat: a heater turns ON at 18 degrees and OFF at 22 degrees, not at the same temperature. The 4-degree gap is the hysteresis.

PWM-Based Dimming Control

Instead of simple ON/OFF control, you can use PWM (Pulse Width Modulation) to gradually adjust the light brightness based on the ambient light level. This provides a smoother user experience and saves more energy by matching the artificial light output to the actual need.

/* PWM Dimming based on LDR reading */
extern TIM_HandleTypeDef htim3;  /* Timer configured for PWM on Channel 1 */

#define PWM_MAX  999   /* Timer period (auto-reload value) */

/* Map ADC reading to PWM duty cycle */
/* Dark = high PWM (bright light), Bright = low PWM (dim or off) */
void update_pwm_brightness(uint16_t adc_val) {
    uint16_t duty;

    if (adc_val > 3000) {
        duty = 0;           /* Bright ambient: LED fully off */
    } else if (adc_val < 500) {
        duty = PWM_MAX;     /* Very dark: LED fully on */
    } else {
        /* Linear mapping from ADC range [500..3000] to PWM [MAX..0] */
        duty = (uint16_t)((3000 - adc_val) * (uint32_t)PWM_MAX / 2500);
    }

    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);
}

/* Smooth transition: ramp to target instead of jumping */
void smooth_dimming(uint16_t target_duty) {
    static uint16_t current_duty = 0;

    while (current_duty != target_duty) {
        if (current_duty < target_duty) current_duty++;
        else current_duty--;
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, current_duty);
        HAL_Delay(2);  /* 2ms per step = ~2 second full sweep */
    }
}

Calibration Procedure

To set the correct thresholds for your installation, follow this calibration procedure:

  • Step 1: Connect the LDR circuit and enable serial debug output to print ADC values continuously.
  • Step 2: At the desired “lights ON” darkness level (e.g., dusk), note the ADC reading. This becomes your THRESHOLD_ON value.
  • Step 3: At the desired “lights OFF” brightness level (e.g., dawn), note the ADC reading. This becomes your THRESHOLD_OFF value.
  • Step 4: Verify that THRESHOLD_OFF is at least 300-500 ADC counts above THRESHOLD_ON to ensure stable hysteresis.
  • Step 5: Test over several day-night cycles and adjust if needed. Shadows from trees or buildings may require wider hysteresis.

Power Considerations: Relay vs MOSFET vs Transistor

Relay: Best for switching AC mains loads (light bulbs, fans). Provides galvanic isolation between the MCU circuit and the mains voltage. Requires a driver transistor (BC547 or 2N2222) because the relay coil draws 40-80mA, far more than an MCU GPIO can supply (10-20mA max). Add a flyback diode (1N4007) across the relay coil to protect the transistor from back-EMF spikes when the relay de-energizes. Relays have limited lifespan due to mechanical contacts (typically 100K-1M switching cycles).

MOSFET: Best for DC loads (LED strips, DC lamps). No mechanical parts, so unlimited switching lifetime. Logic-level MOSFETs (like IRLZ44N) can be driven directly from a 3.3V or 5V GPIO pin. They can switch at PWM frequencies (1-100kHz) for dimming control, which relays cannot do. Use a gate resistor (100 ohm) and a pull-down resistor (10k) to prevent floating gate during MCU reset.

BJT Transistor (BC547, 2N2222): Suitable for small DC loads under 500mA. Simple to use but less efficient than MOSFETs for higher currents due to the voltage drop across the collector-emitter junction (0.2-0.7V). Typically used as a relay driver rather than directly switching the load.

Enhancements for a Production System

Time delay: Add a configurable delay before turning the light OFF. This prevents the light from turning off during brief bright moments (like a car headlight sweeping past the sensor at night). A 30-60 second delay is typical for outdoor lighting.

Manual override: Include a physical switch or button that forces the light ON or OFF regardless of the LDR reading. Implement three modes: AUTO (LDR controls), FORCE ON, and FORCE OFF. Store the current mode in EEPROM so it persists across power cycles.

Multiple zones: Use multiple LDR sensors in different locations with separate relay outputs. A hallway might need different threshold values than a parking area. Each zone can have independent thresholds and timing parameters.

Motion integration: Combine the LDR with a PIR motion sensor (see our Motion and Proximity Sensors guide). The light turns ON only when it is dark AND someone is present, maximizing energy savings. This is the most common configuration for stairwell and hallway lighting in commercial buildings.

Original Arduino Visitor Counter Project

The original version of this project used an Arduino Uno with two IR sensors to count people entering and leaving a room, displaying the count on a 16×2 LCD. When the count reached zero (no one in the room), the relay turned off the light. Here is the circuit and code for reference:

/* Arduino Visitor Counter with Automatic Light */
#include <LiquidCrystal.h>

LiquidCrystal lcd(13, 12, 11, 10, 9, 8);

#define IN_SENSOR  14
#define OUT_SENSOR 19
#define RELAY_PIN  2

int count = 0;

void personIn() {
    count++;
    lcd.clear();
    lcd.print("Person In Room:");
    lcd.setCursor(0, 1);
    lcd.print(count);
    delay(1000);
}

void personOut() {
    count--;
    lcd.clear();
    lcd.print("Person In Room:");
    lcd.setCursor(0, 1);
    lcd.print(count);
    delay(1000);
}

void setup() {
    lcd.begin(16, 2);
    lcd.print("Visitor Counter");
    delay(2000);
    pinMode(IN_SENSOR, INPUT);
    pinMode(OUT_SENSOR, INPUT);
    pinMode(RELAY_PIN, OUTPUT);
    lcd.clear();
    lcd.print("Person In Room:");
    lcd.setCursor(0, 1);
    lcd.print(count);
}

void loop() {
    if (digitalRead(IN_SENSOR))
        personIn();
    if (digitalRead(OUT_SENSOR))
        personOut();

    if (count <= 0) {
        count = 0;  /* Prevent negative count */
        digitalWrite(RELAY_PIN, LOW);
        lcd.clear();
        lcd.print("Nobody In Room");
        lcd.setCursor(0, 1);
        lcd.print("Light Is Off");
        delay(200);
    } else {
        digitalWrite(RELAY_PIN, HIGH);
    }
}

📖 Related: PWM (Pulse Width Modulation) in Embedded Systems

Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *