Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| Arduino Uno R3 (CH340 clone) | 1 | ₹450 | Buy Arduino Uno R3 (CH340 clone) |
| LDR light sensor module (4-pin, with AO) | 4 | ₹160 | Buy LDR light sensor module (4-pin, with AO) |
| SG90 micro servo (or MG90S metal gear) | 2 | ₹240 | Buy SG90 micro servo (or MG90S metal gear) |
| Pan-tilt bracket for SG90 servos | 1 | ₹90 | Buy Pan-tilt bracket for SG90 servos |
| Small solar panel, 6 V 1 W (or a dummy board) | 1 | ₹150 | Buy Small solar panel, 6 V 1 W (or a dummy board) |
| 470 µF 10 V capacitor | 2 | ₹10 | Buy 470 µF 10 V capacitor |
| 5 V 2 A adapter (servo supply) | 1 | ₹180 | Buy 5 V 2 A adapter (servo supply) |
| Black card or 3D-printed “+” shade | 1 | ₹20 | Buy Black card or 3D-printed “+” shade |
| Total | ₹1,300 |
Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.
Wiring
| Arduino Uno R3 | Part · pin | Wire | Notes |
|---|---|---|---|
| A0 | LDR light sensor module · AO | green | Top-left LDR |
| 5V | LDR light sensor module · VCC | red | |
| GND | LDR light sensor module · GND | black | |
| A1 | LDR light sensor module #2 · AO | yellow | Top-right LDR |
| 5V | LDR light sensor module #2 · VCC | red | |
| GND | LDR light sensor module #2 · GND | black | |
| A2 | LDR light sensor module #3 · AO | blue | Bottom-left LDR |
| 5V | LDR light sensor module #3 · VCC | red | |
| GND | LDR light sensor module #3 · GND | black | |
| A3 | LDR light sensor module #4 · AO | purple | Bottom-right LDR |
| 5V | LDR light sensor module #4 · VCC | red | |
| GND | LDR light sensor module #4 · GND | black | |
| D9 | SG90 micro servo (9 g) · SIG | orange | Pan (east–west) servo |
| GND | SG90 micro servo (9 g) · GND | black | Common ground with the servo adapter |
| 5 V 2 A adapter (servos) · +5V | SG90 micro servo (9 g) · VCC | red | Fit a 470 µF capacitor across each servo’s VCC and GND |
| 5 V 2 A adapter (servos) · GND | SG90 micro servo (9 g) · GND | black | |
| D10 | SG90 micro servo (9 g) #2 · SIG | white | Tilt (elevation) servo |
| GND | SG90 micro servo (9 g) #2 · GND | black | |
| 5 V 2 A adapter (servos) · +5V | SG90 micro servo (9 g) #2 · VCC | red | |
| 5 V 2 A adapter (servos) · GND | SG90 micro servo (9 g) #2 · GND | black |
Board quirks: This is a demonstrator: SG90s can turn a 1–5 W panel, not a rooftop module. Real trackers use worm-gear motors or linear actuators and a sun-position calculation as well as sensors. Most LDR modules read lower in brighter light; the sketch assumes that (<code>BRIGHT_IS_LOW</code>). Shine a torch on one LDR and watch Serial — if its number rises instead, set it to false. The servos take their own 5 V adapter: two SG90s moving together can pull close to 1 A, enough to reset the Uno if they share its 5 V pin. Join the grounds. The Servo library uses Timer1, so analogWrite stops working on D9 and D10 — which is why the servos sit on exactly those pins.
Code
// Dual-axis solar tracker — Arduino Uno + 4 LDR modules + 2 SG90 servos (pan and tilt)
// LDR modules (AO): top-left → A0, top-right → A1, bottom-left → A2, bottom-right → A3, all at 5V.
// Pan servo → D9, tilt servo → D10, both powered from a separate 5 V supply (common GND).
// The four LDRs sit in the corners of a "+" shaped shade. When the sun is off-centre, one side
// gets more light; the servos step towards the brighter side until all four read the same.
#include <Servo.h>
constexpr uint8_t LDR_TL = A0, LDR_TR = A1, LDR_BL = A2, LDR_BR = A3;
constexpr uint8_t PAN_PIN = 9, TILT_PIN = 10;
// Most LDR modules read LOWER in brighter light. Shine a torch on one and watch Serial; if the
// number goes up instead, set this to false.
constexpr bool BRIGHT_IS_LOW = true;
constexpr int TOLERANCE = 25; // ignore differences smaller than this (0–1023 scale)
constexpr int DARK_LEVEL = 60; // all four below this (as brightness) = night or heavy cloud
constexpr uint8_t PAN_MIN = 10, PAN_MAX = 170, TILT_MIN = 20, TILT_MAX = 160;
constexpr uint8_t PARK_PAN = 20, PARK_TILT = 60; // where to wait overnight: facing the sunrise
Servo pan, tilt;
int panPos = 90, tiltPos = 90;
unsigned long lastPrint = 0;
int brightness(uint8_t pin) {
int raw = analogRead(pin);
return BRIGHT_IS_LOW ? 1023 - raw : raw;
}
void sweepTest() { // power-on self test: shows both axes move freely before tracking starts
for (int a = 90; a >= 45; a--) pan.write(a), delay(10);
for (int a = 45; a <= 135; a++) pan.write(a), delay(10);
for (int a = 135; a >= 90; a--) pan.write(a), delay(10);
for (int a = 90; a <= 120; a++) tilt.write(a), delay(10);
for (int a = 120; a >= 90; a--) tilt.write(a), delay(10);
}
void setup() {
Serial.begin(115200);
pan.attach(PAN_PIN);
tilt.attach(TILT_PIN);
pan.write(panPos);
tilt.write(tiltPos);
delay(500);
sweepTest();
Serial.println(F("tl,tr,bl,br,pan,tilt"));
}
void loop() {
int tl = brightness(LDR_TL), tr = brightness(LDR_TR), bl = brightness(LDR_BL), br = brightness(LDR_BR);
int top = (tl + tr) / 2, bottom = (bl + br) / 2, left = (tl + bl) / 2, right = (tr + br) / 2;
bool dark = max(max(tl, tr), max(bl, br)) < DARK_LEVEL;
if (dark) {
// Night or a thick monsoon cloud: park facing east and wait.
if (panPos != PARK_PAN || tiltPos != PARK_TILT) {
panPos += panPos < PARK_PAN ? 1 : panPos > PARK_PAN ? -1 : 0;
tiltPos += tiltPos < PARK_TILT ? 1 : tiltPos > PARK_TILT ? -1 : 0;
}
} else {
// One degree per step towards the brighter side. Flip a sign if an axis runs away from the light.
if (top - bottom > TOLERANCE) tiltPos++;
else if (bottom - top > TOLERANCE) tiltPos--;
if (left - right > TOLERANCE) panPos++;
else if (right - left > TOLERANCE) panPos--;
}
panPos = constrain(panPos, PAN_MIN, PAN_MAX);
tiltPos = constrain(tiltPos, TILT_MIN, TILT_MAX);
pan.write(panPos);
tilt.write(tiltPos);
if (millis() - lastPrint > 1000) {
lastPrint = millis();
Serial.print(tl);
Serial.print(',');
Serial.print(tr);
Serial.print(',');
Serial.print(bl);
Serial.print(',');
Serial.print(br);
Serial.print(',');
Serial.print(panPos);
Serial.print(',');
Serial.print(tiltPos);
Serial.println(dark ? F(",parked") : F(""));
}
delay(40); // ~25 steps a second: smooth, and the servos don't overheat
}// Dual-axis solar tracker — Arduino Uno + 4 LDR modules + 2 SG90 servos (pan and tilt)
// LDR modules (AO): top-left → A0, top-right → A1, bottom-left → A2, bottom-right → A3, all at 5V.
// Pan servo → D9, tilt servo → D10, both powered from a separate 5 V supply (common GND).
// The four LDRs sit in the corners of a "+" shaped shade. When the sun is off-centre, one side
// gets more light; the servos step towards the brighter side until all four read the same.
#include <Servo.h>
constexpr uint8_t LDR_TL = A0, LDR_TR = A1, LDR_BL = A2, LDR_BR = A3;
constexpr uint8_t PAN_PIN = 9, TILT_PIN = 10;
// Most LDR modules read LOWER in brighter light. Shine a torch on one and watch Serial; if the
// number goes up instead, set this to false.
constexpr bool BRIGHT_IS_LOW = true;
constexpr int TOLERANCE = 25; // ignore differences smaller than this (0–1023 scale)
constexpr int DARK_LEVEL = 60; // all four below this (as brightness) = night or heavy cloud
constexpr uint8_t PAN_MIN = 10, PAN_MAX = 170, TILT_MIN = 20, TILT_MAX = 160;
constexpr uint8_t PARK_PAN = 20, PARK_TILT = 60; // where to wait overnight: facing the sunrise
Servo pan, tilt;
int panPos = 90, tiltPos = 90;
unsigned long lastPrint = 0;
int brightness(uint8_t pin) {
int raw = analogRead(pin);
return BRIGHT_IS_LOW ? 1023 - raw : raw;
}
void sweepTest() { // power-on self test: shows both axes move freely before tracking starts
for (int a = 90; a >= 45; a--) pan.write(a), delay(10);
for (int a = 45; a <= 135; a++) pan.write(a), delay(10);
for (int a = 135; a >= 90; a--) pan.write(a), delay(10);
for (int a = 90; a <= 120; a++) tilt.write(a), delay(10);
for (int a = 120; a >= 90; a--) tilt.write(a), delay(10);
}
void setup() {
Serial.begin(115200);
pan.attach(PAN_PIN);
tilt.attach(TILT_PIN);
pan.write(panPos);
tilt.write(tiltPos);
delay(500);
sweepTest();
Serial.println(F("tl,tr,bl,br,pan,tilt"));
}
void loop() {
int tl = brightness(LDR_TL), tr = brightness(LDR_TR), bl = brightness(LDR_BL), br = brightness(LDR_BR);
int top = (tl + tr) / 2, bottom = (bl + br) / 2, left = (tl + bl) / 2, right = (tr + br) / 2;
bool dark = max(max(tl, tr), max(bl, br)) < DARK_LEVEL;
if (dark) {
// Night or a thick monsoon cloud: park facing east and wait.
if (panPos != PARK_PAN || tiltPos != PARK_TILT) {
panPos += panPos < PARK_PAN ? 1 : panPos > PARK_PAN ? -1 : 0;
tiltPos += tiltPos < PARK_TILT ? 1 : tiltPos > PARK_TILT ? -1 : 0;
}
} else {
// One degree per step towards the brighter side. Flip a sign if an axis runs away from the light.
if (top - bottom > TOLERANCE) tiltPos++;
else if (bottom - top > TOLERANCE) tiltPos--;
if (left - right > TOLERANCE) panPos++;
else if (right - left > TOLERANCE) panPos--;
}
panPos = constrain(panPos, PAN_MIN, PAN_MAX);
tiltPos = constrain(tiltPos, TILT_MIN, TILT_MAX);
pan.write(panPos);
tilt.write(tiltPos);
if (millis() - lastPrint > 1000) {
lastPrint = millis();
Serial.print(tl);
Serial.print(',');
Serial.print(tr);
Serial.print(',');
Serial.print(bl);
Serial.print(',');
Serial.print(br);
Serial.print(',');
Serial.print(panPos);
Serial.print(',');
Serial.print(tiltPos);
Serial.println(dark ? F(",parked") : F(""));
}
delay(40); // ~25 steps a second: smooth, and the servos don't overheat
}Compiled for uno on avr 1.8.8 with Servo 1.3.0 · 3 Nov 2026
How it works
A fixed solar panel faces the sun squarely for only an hour or two a day. A tracker that follows the sun collects noticeably more — often quoted as a quarter to a third more for dual-axis tracking on clear days. This build shows the idea at desk scale: four light sensors, two servos, and a small panel that turns to face the sun.
How it finds the sun
The four LDRs sit in the four quarters of a “+”-shaped shade, a cross of black card a few centimetres tall. When the panel points straight at the sun, the shade casts no shadow on any LDR and all four read the same. When the sun moves west, the cross shades the eastern LDRs; the western pair reads brighter.
Every 40 ms the Uno reads all four on A0–A3 and compares averages:
- top vs bottom → tilt servo one degree towards the brighter side;
- left vs right → pan servo one degree towards the brighter side.
Differences smaller than TOLERANCE are ignored, so the servos rest once it is aligned instead of buzzing. When all four are dark (night, or a dark monsoon cloud) it parks facing the eastern morning sky, ready for sunrise. Serial prints all four readings and both angles once a second — handy for checking which LDR is which.
At power-on it does a short self-test sweep of both axes, so you can see the mechanics move freely before it starts tracking.
Building the sensor head
Glue the four LDR modules (or just their LDRs on short wires) to a small square of card, one per quarter, and stand a black “+” in the middle, 3–5 cm tall. Fix the head to the panel so it faces exactly where the panel faces. Taller arms make it more sensitive and slower to settle.
India notes
At Indian latitudes (roughly 8–35° N) the noon sun is high — nearly overhead in May–June in the south — and well to the south in December. Set the tilt range (TILT_MIN/TILT_MAX) so the bracket can reach both. Dust on the LDRs matters as much as dust on panels: wipe both.
Troubleshooting
One axis runs away from the light to its end stop
That pair of LDRs is swapped (or the servo is mounted the other way round). Swap the two AO wires of that axis, or flip the ++/−− in the sketch for it.
The panel hunts back and forth around the sun
Raise TOLERANCE (try 40), make the shade’s arms taller, and check all four LDRs read about the same under the same light.
The servos buzz and jitter while still
Supply noise: fit the 470 µF capacitors at the servos and use a proper 2 A adapter.
Upgrades
- Measure the panel’s output with an INA219 and compare fixed vs tracking on the same day.
- Add an RTC and calculate the sun’s position, using the LDRs only for fine correction.
- Replace the SG90s with a worm-gear motor for a panel that holds position in wind with no power.


