IoTSimulator

Arduino Single-Axis Solar Tracker with Dual LDRs and Servo

Maximize solar panel energy harvest with an intelligent single-axis sun tracker that pivots toward the brightest light source in real time.
Muhammad Ichsanul Fadhil
IoTSimulator Team
October 25, 2026
Arduino Single-Axis Solar Tracker with Dual LDRs and Servo

Live project track

Interactive hardware & logic preview

Have you ever noticed how a sunflower in a garden turns its face toward the morning sunrise and slowly tracks the sun all the way to evening? In nature, plants do this naturally because facing the sun directly captures the most light possible.

Standard solar panels on house rooftops are bolted down at one fixed angle. When the morning sun rises or the evening sun sets, light hits the panel at a steep, glancing angle. Because of this, fixed panels lose up to 40% of the clean electricity they could make each day.

In this project, you will build a smart single-axis solar tracker with an Arduino Uno, two light-detecting photoresistors (LDRs), an SG90 micro servo motor, and an I2C LCD screen. The core concept to learn is simple: when you place a small wall between two light sensors, moving sunlight casts a shadow on one sensor. The Arduino measures that difference and steers the servo until both sensors receive equal light again.

The visual overview below compares the natural heliotropic behavior of sunflowers with our electronic dual-sensor tracking system:

Sunflower following the sun compared with dual sensor solar tracker
Sunflower following the sun compared with dual sensor solar tracker

Just as a growing sunflower uses auxin plant hormones to turn its petals toward the morning sun, our automated tracker uses the electrical voltage difference between two light sensors to aim the solar panel squarely at the brightest point in the sky. In the following section, we explore how this steering signal is generated.

How Sunlight Creates a Steering Signal

Think of balancing a seesaw on a playground. If two children of the same weight sit on both ends, the seesaw stays level. But if one child is heavier, that side drops down. To level the seesaw again, you have to push back up until both sides match.

Our solar tracker uses this exact balancing idea. Two Light Dependent Resistors (called LDRs for short) sit side by side on a small pivot frame. Between them sits a thin, opaque wall made of cardboard or plastic.

When the sun is straight overhead at noon, light shines evenly on both sensors. Both LDRs send nearly identical voltage readings to the Arduino. But when the sun shifts to the east or west, the center wall casts a dark shadow over one of the sensors.

Opaque center wall casting a shadow on one light sensor
Opaque center wall casting a shadow on one light sensor

An LDR changes its electrical resistance based on how much light hits its surface. When bright light shines on an LDR, electricity flows through it easily, producing a high analog voltage. When shade or darkness covers the LDR, its resistance shoots up, producing a lower voltage.

By comparing the left sensor reading with the right sensor reading, the Arduino knows instantly which way the sun has moved:

Sun PositionLeft LDR VoltageRight LDR VoltageDifference (Left - Right)Servo Motor Action
Sun to the Left (East)High (~ 850 counts)Shadowed (~ 200 counts)Positive (+650)Pivots Left toward sunrise
Directly Overhead (Noon)Balanced (~ 850 counts)Balanced (~ 850 counts)Near Zero (±15)Holds still (Motor rests)
Sun to the Right (West)Shadowed (~ 200 counts)High (~ 850 counts)Negative (-650)Pivots Right toward sunset
The opaque wall between the sensors is essential. Without a barrier to cast a shadow, both sensors would see similar ambient light, and the tracker would not know which way to steer.

The Arduino converts the analog voltage from each sensor into a digital number between 0 and 1023. Subtracting the right reading from the left reading gives a single number called diff. If diff is positive, the left side is brighter. If diff is negative, the right side is brighter.

Preventing Motor Jitter with a Deadband Zone

If you tell a servo motor to move on every single tiny number change, it will never stop buzzing. Natural sunlight is never 100% steady. Small tree leaves, moving clouds, and electronic noise cause sensor readings to flicker by a few numbers every millisecond.

If the motor tries to react to every flicker, the tiny plastic gears inside the SG90 servo will grind, heat up, and wear out quickly. It also wastes precious battery power that the solar panel is trying to collect.

To keep the motor quiet and healthy, we add a simple rule in the code called a deadband (or tolerance zone). We tell the Arduino: 'Do not move the motor unless the light difference is bigger than 35 counts.'

Deadband tolerance zone holding the motor still during minor flicker
Deadband tolerance zone holding the motor still during minor flicker

When the light difference is small (between -35 and +35), the Arduino leaves the servo in peace. The motor shuts off its driving pulse and stays quiet. Only when the sun moves far enough across the sky to create a clear shadow does the Arduino step the motor 2 degrees toward the light.

  • Difference larger than +35: The left sensor has clear sunlight. The servo takes a small step to the left.
  • Difference between -35 and +35: The tracker is facing the sun. The servo holds its position silently.
  • Difference smaller than -35: The right sensor has clear sunlight. The servo takes a small step to the right.

Steering the Servo Across a 180-Degree Arc

An SG90 micro servo is a motor that can turn to an exact angle between 0 degrees and 180 degrees. Unlike a toy DC motor that spins continuously, a servo motor uses an internal potentiometer and control circuit to hold whatever position you tell it to hold.

We map the 180-degree sweep of the servo directly to the sun's path across the sky:

Servo sweep angles mapped from sunrise to sunset
Servo sweep angles mapped from sunrise to sunset

When the system turns on in the morning, the servo starts at 90 degrees, pointing straight up. As the morning sun rises in the east, the left sensor gets bright, and the code steps the angle down toward 10 degrees. By midday, the sun moves overhead, bringing the servo back to 90 degrees. In late afternoon, the right sensor catches the sunset, nudging the servo toward 170 degrees.

We also add physical angle limits in the code. We never tell the servo to go below 10 degrees or above 170 degrees. This prevents the motor horn from hitting the workbench frame or straining its plastic gears.

Building the Solar Tracker Circuit

Every electronic project needs a clear power plan. The Arduino Uno board gets 5V power from your computer's USB cable. It then shares that power with all parts through its 5V and GND pins.

5V is the positive power supply pin. Think of it like the positive terminal of a battery where clean electrical current comes from. GND stands for Ground. It is the return path that electricity needs to complete its journey back to the board. Every sensor, display, and motor must share this common ground so they all agree on what zero volts means.

Here is how the four main parts connect to the Arduino Uno:

Pin Connection Map
Left LDR Sensor Module
VCC
→
Arduino Uno
5V
Explanation
Supplies positive power to the light sensor circuit.
Left LDR Sensor Module
GND
→
Arduino Uno
GND.1
Explanation
Completes the ground return circuit.
Left LDR Sensor Module
AO (Analog Out)
→
Arduino Uno
A0
Explanation
Sends a continuous 0V to 5V voltage signal based on morning light.
Pin Connection Map
Right LDR Sensor Module
VCC
→
Arduino Uno
5V
Explanation
Supplies positive power to the light sensor circuit.
Right LDR Sensor Module
GND
→
Arduino Uno
GND.1
Explanation
Shared ground return line.
Right LDR Sensor Module
AO (Analog Out)
→
Arduino Uno
A1
Explanation
Sends a continuous 0V to 5V voltage signal based on afternoon light.
Pin Connection Map
I2C LCD1602 Display
VCC
→
Arduino Uno
5V
Explanation
Powers the LCD screen controller and blue backlight.
I2C LCD1602 Display
GND
→
Arduino Uno
GND.1
Explanation
Returns display electrical current to ground.
I2C LCD1602 Display
SDA (Serial Data)
→
Arduino Uno
A4
Explanation
Carries text data packets using the two-wire I2C protocol.
I2C LCD1602 Display
SCL (Serial Clock)
→
Arduino Uno
A5
Explanation
Carries clock pulses so the LCD and Arduino stay in sync.
Pin Connection Map
SG90 Micro Servo Motor
V+ (Red Wire)
→
Arduino Uno
5V
Explanation
Powers the small DC motor and gears inside the servo.
SG90 Micro Servo Motor
GND (Brown Wire)
→
Arduino Uno
GND.2
Explanation
Completes the servo motor power loop.
SG90 Micro Servo Motor
PWM (Orange Wire)
→
Arduino Uno
Pin 9
Explanation
Carries timed pulses that command the servo arm angle.
If you build this with loose LDR components instead of pre-assembled sensor modules, you must add a 10k ohm resistor in series with each LDR to form a voltage divider. Pre-built sensor boards already have this resistor on board.

The Complete Solar Tracker Sketch

Here is the full Arduino sketch. It reads both light sensors, computes the difference, updates the LCD screen, and steps the servo motor toward the light:

C++ Source
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Servo.h>

const int ldrLeftPin = A0;
const int ldrRightPin = A1;
const int servoPin = 9;

LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo trackerServo;

int currentAngle = 90;
const int tolerance = 35;
unsigned long lastTelemetryTime = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("[TRACKER]: 1-Axis Solar Tracker Initializing...");

  trackerServo.attach(servoPin);
  trackerServo.write(currentAngle);

  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print(" SOLAR TRACKER  ");
  lcd.setCursor(0, 1);
  lcd.print(" SYSTEM ACTIVE  ");
  delay(80);
  lcd.clear();

  Serial.println("[TRACKER]: Ready. Tracking Optimal Light Angle.");
}

void loop() {
  unsigned long currentMillis = millis();

  int leftVal = analogRead(ldrLeftPin);
  int rightVal = analogRead(ldrRightPin);
  int diff = leftVal - rightVal;

  // Kemudikan servo menuju intensitas cahaya terkuat
  if (diff > tolerance && currentAngle > 10) {
    currentAngle -= 2;
    trackerServo.write(currentAngle);
  } else if (diff < -tolerance && currentAngle < 170) {
    currentAngle += 2;
    trackerServo.write(currentAngle);
  }

  // Update tampilan LCD secara responsif
  lcd.setCursor(0, 0);
  lcd.print("L:");
  lcd.print(leftVal);
  lcd.print(" R:");
  lcd.print(rightVal);
  lcd.print("       ");

  lcd.setCursor(0, 1);
  lcd.print("SERVO: ");
  lcd.print(currentAngle);
  lcd.print((char)223); // Simbol derajat
  if (diff > tolerance) {
    lcd.print(" [LEFT] ");
  } else if (diff < -tolerance) {
    lcd.print(" [RIGHT]");
  } else {
    lcd.print(" [LOCK] ");
  }

  // Stream Telemetry
  if (currentMillis - lastTelemetryTime >= 1000) {
    lastTelemetryTime = currentMillis;
    Serial.print("[Telemetry]: LDR_Left=");
    Serial.print(leftVal);
    Serial.print(" | LDR_Right=");
    Serial.print(rightVal);
    Serial.print(" | Diff=");
    Serial.print(diff);
    Serial.print(" | ServoAngle=");
    Serial.print(currentAngle);
    Serial.print(" | Tracking=");
    Serial.println(abs(diff) > tolerance ? "ACTIVE" : "LOCKED");
  }

  delay(50);
}

How the Code Works, Step by Step

Let us look at each part of the sketch so you understand exactly what the code is doing.

Including Libraries and Naming Pins

At the very top of the sketch, we include three libraries that give the Arduino extra capabilities:

C++ Source
1
2
3
4
5
6
7
8
9
10
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Servo.h>

const int ldrLeftPin = A0;
const int ldrRightPin = A1;
const int servoPin = 9;

LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo trackerServo;

Wire.h handles the I2C communication protocol. LiquidCrystal_I2C.h lets us talk to the LCD screen using just two wires instead of sixteen. Servo.h generates the microsecond timing pulses that control the servo motor.

We also give friendly names to the pins. ldrLeftPin is pin A0, ldrRightPin is pin A1, and servoPin is digital pin 9. Giving pins names makes the sketch much easier to read than using raw numbers throughout the code.

Setting Starting Values and Tolerance

Next, we declare the variables that store the tracker's status:

C++ Source
1
2
3
int currentAngle = 90;
const int tolerance = 35;
unsigned long lastTelemetryTime = 0;

currentAngle = 90 starts the tracker pointing straight up toward the sky. tolerance = 35 sets our deadband buffer so the motor only moves when sunlight is noticeably brighter on one side.

lastTelemetryTime stores a timestamp using the Arduino's built-in millisecond clock, allowing us to send diagnostic text to the computer once per second without freezing the rest of the sketch.

Setup Prepares Hardware on Startup

The setup() function runs once as soon as the Arduino receives power:

C++ Source
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void setup() {
  Serial.begin(9600);
  trackerServo.attach(servoPin);
  trackerServo.write(currentAngle);

  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print(" SOLAR TRACKER  ");
  lcd.setCursor(0, 1);
  lcd.print(" SYSTEM ACTIVE  ");
  delay(80);
  lcd.clear();
}

trackerServo.attach(servoPin) tells the Arduino to output servo signals on pin 9. trackerServo.write(currentAngle) moves the servo arm to its neutral 90-degree position right away.

Next, lcd.init() wakes up the LCD screen controller, and lcd.backlight() turns on its blue LED backlight so you can read the screen clearly. A brief greeting confirms that the system is ready to track light.

Reading Sensors and Calculating Light Difference

The loop() function runs over and over again, hundreds of times every second:

C++ Source
1
2
3
int leftVal = analogRead(ldrLeftPin);
int rightVal = analogRead(ldrRightPin);
int diff = leftVal - rightVal;

analogRead() checks the voltage on the sensor pin and turns it into a number between 0 (completely dark) and 1023 (very bright). We store both numbers in leftVal and rightVal.

Subtracting right from left produces diff. If both sensors see equal light (for example, 850 on the left and 845 on the right), diff is only 5. Since 5 is smaller than our tolerance of 35, the code knows the panel is already well-aligned.

Steering the Servo Motor

When the light difference is bigger than the tolerance, the motor takes action:

C++ Source
1
2
3
4
5
6
7
if (diff > tolerance && currentAngle > 10) {
  currentAngle -= 2;
  trackerServo.write(currentAngle);
} else if (diff < -tolerance && currentAngle < 170) {
  currentAngle += 2;
  trackerServo.write(currentAngle);
}

If diff > tolerance, the left side is brighter. The code subtracts 2 degrees from currentAngle, nudging the panel to the left. The condition currentAngle > 10 acts as a safety stop so the motor never twists past its physical limit.

If diff < -tolerance, the right side is brighter. The code adds 2 degrees to currentAngle, turning the panel to the right. Taking small 2-degree steps ensures smooth, gentle movement that looks realistic and prevents mechanical strain.

Updating the LCD Dashboard

The LCD screen provides continuous visual feedback without needing a computer attached:

C++ Source
1
2
3
4
5
6
7
8
9
10
lcd.setCursor(0, 0);
lcd.print("L:");
lcd.print(leftVal);
lcd.print(" R:");
lcd.print(rightVal);

lcd.setCursor(0, 1);
lcd.print("SERVO: ");
lcd.print(currentAngle);
lcd.print((char)223); // Degree symbol

Row 0 displays both live sensor values side by side. Row 1 shows the current servo angle followed by a small degree symbol (character code 223).

Next to the angle, the screen prints a clear status tag: [LEFT] when steering left, [RIGHT] when steering right, and [LOCK] when sunlight is balanced and the tracker is sitting happily on target.

Testing Your Tracker in the Simulator

You can test this entire project inside the IoTSimulator workspace right in your browser:

  • Step 1: Click the green Run button in the workspace toolbar. You will see the LCD light up with the boot message and the servo arm move straight up to 90 degrees.
  • Step 2: Look at the top of the canvas for the Sunlight Actor. Drag the sun slider toward the left side. Notice the left LDR value climb while the right value drops.
  • Step 3: Watch the LCD switch from [LOCK] to [LEFT]. The servo motor will smoothly step downward toward 10 degrees to follow the light.
  • Step 4: Now drag the sun over to the right side. The LCD shows [RIGHT], and the servo pivots all the way across to the right until both values match.

Troubleshooting Common Mistakes

If you build this project and the tracker behaves strangely, check these common issues first:

  • Motor turns away from the light instead of toward it: Your sensor wires are swapped. Either swap the A0 and A1 wires on the board, or swap ldrLeftPin and ldrRightPin in the code.
  • Motor twitches and never stops humming: Your tolerance value is too low. Increase tolerance from 35 to 50 in the sketch to give the sensors a wider resting window.
  • LCD lights up blue but shows no text: The I2C address might be different on your physical module. Most modules use address 0x27, but some use 0x3F. You can scan for the address with a quick I2C scanner sketch.
  • Servo resets or the board disconnects: A servo motor draws a burst of electrical current when it starts moving. If your USB port cannot supply enough current, add a 100uF or 470uF electrolytic capacitor across the servo's 5V and GND pins to smooth out power dips.

Taking Your Tracker Further

Once you have mastered this single-axis tracker, here are three exciting ways to expand your project:

  • Upgrade to Dual-Axis Tracking: Add two more LDRs (Top and Bottom) and a second servo motor. Your tracker will now follow the sun both horizontally across the horizon and vertically up into the summer sky.
  • Add a Solar Panel Voltage Sensor: Connect a real small solar cell to analog pin A2 through a voltage divider. Show both the servo angle and the real generated milliwatts on the LCD so you can measure how much extra energy tracking gives you.
  • Nighttime Return to Dawn: Add an if condition that detects when both sensors read deep darkness (night). The tracker can automatically pivot all the way back to 10 degrees (East) so it is already waiting for tomorrow's sunrise.
Keywords
#Arduino #Solar Tracker #LDR Sensor #Servo Motor #Single Axis Solar Tracker #Renewable Energy
Total word count: 2208 words

More in Sunlight Actor

Continue exploring this hardware category

Project discussion

Questions, feedback, and community insights
No discussions yet. Be the first to start!