Machine Building Group 2¶
Project name¶
We named our solar tracker Areg (Արեգ), an Armenian male name that literally translates to Sun, perfectly reflecting the core purpose of our device
Slide and Video¶

Members¶
Students
-
Gevorg Malkhasyan - Sensor PCB development, 3D modeling, 3D printing, Assembling, Coding
-
Ani Petrosyan - 3D modeling, 3D printing, Assembling, Coding
Instructors
-
Onik Babajanyan - Project idea
-
Rudolf Igityan - Project implementation
Light Sensor Selection¶
Since a photoresistor (LDR) changes its resistance depending on the amount of light hitting it, it is well suited for our task.
Reading a Photoresistor with a Microcontroller¶
This raises the question: how do we read its value using a microcontroller?
The microcontroller supplies voltage to the photoresistor. However, if we read the voltage at the input side of the LDR, we always get 5V; if we read it at the output side (after the LDR, just before GND), we always get 0V — all the voltage has already dropped across the load. Neither reading is useful.
The solution is a voltage divider. By placing a fixed resistor in series with the photoresistor, we create two voltage drops — one across each component. If we connect the microcontroller’s analog pin to the midpoint between them, we get a reading that changes with the LDR’s resistance.
Since the microcontroller shares a common GND with the rest of the circuit, its analog pin effectively acts as a voltmeter — reading a value between 0 and 1023, where 0 corresponds to 0V and 1023 corresponds to 5V.
Circuit Schematic¶
5V
|
[R1] ← fixed resistor (1 kΩ or 10 kΩ)
|
+──────────→ Analog pin (A0–A3)
|
[LDR] ← photoresistor
|
GND
The fixed resistor R1 is placed first (closest to 5V), and the LDR second (closest to GND). This means the analog pin reads the voltage drop across the LDR.
Since the LDR’s resistance decreases in brighter light, the voltage across it also decreases, so lower analog values = more intense light. The relationship is inverse. Swapping the order of R1 and LDR would give a direct proportional relationship instead.
Choosing the Right Fixed Resistor¶
Rather than simply looking up the answer online, we decided to investigate the choice of resistor more deeply. Since most programming and calibration would take place indoors, we measured the LDR’s resistance in two conditions:
| Condition | LDR resistance |
|---|---|
| Room light | ~1 250 Ω |
| Covered (dark) | ~20 000 Ω |
We then considered two candidate resistors: 1 kΩ and 10 kΩ.
Calculations¶
For a series circuit with supply voltage 5V, by Ohm’s law the current through both components is the same:
The analog pin reads the voltage drop across the LDR only:
The Arduino ADC maps 0–5V linearly onto 0–1023. Combining both steps gives a single formula for the analog reading:
Case 1 — R1 = 1 kΩ, in light (R_LDR ≈ 1 250 Ω):
Case 2 — R1 = 1 kΩ, in darkness (R_LDR ≈ 20 000 Ω):
Case 3 — R1 = 10 kΩ, in light (R_LDR ≈ 1 250 Ω):
Case 4 — R1 = 10 kΩ, in darkness (R_LDR ≈ 20 000 Ω):
Results¶
| Condition | R1 = 1 kΩ | R1 = 10 kΩ |
|---|---|---|
| In light (LDR ≈ 1 250 Ω) | ~568 | ~114 |
| In darkness (LDR ≈ 20 000 Ω) | ~975 | ~682 |
| Spread (dark − light) | 407 | 568 |
Final Choice: 10 kΩ¶
We selected the 10 kΩ resistor. With this configuration, the analog values are immediately intuitive: values below approximately 500 indicate bright light, while values above 500 indicate darkness. The threshold sits near the midpoint of the 0–1023 scale, making the readings easy to interpret and calibrate.
The 10 kΩ resistor also gives a wider spread between light and dark conditions, making the sensor more sensitive and easier to work with in code.
Passive Sensor Design and Fabrication¶
The core idea behind our solar tracker is straightforward: instead of knowing the sun’s position mathematically, the device finds it by sensing light. To determine which direction the sun is in, the system compares light intensity from four sides simultaneously — top-left, top-right, bottom-left, and bottom-right. By knowing which sensor receives the most light, the microcontroller can decide which way to rotate.
For this comparison to be meaningful, each sensor must be fixed at a precise, known position and physically isolated from its neighbours — otherwise light from one direction could reach the wrong sensor and corrupt the reading. This is why a dedicated PCB was designed to hold all four photoresistor channels at consistent, symmetric positions, and why the 3D-printed enclosure includes divider walls between them.
Schematic Design¶
The schematic was created in KiCad and consists of four independent voltage divider channels. Each channel pairs a fixed 10 kΩ resistor (connected to 5V) with a photoresistor (connected to GND), and routes the midpoint voltage to one of four analog output pins.

All four channels share a common power connection through a pin header, which provides a single entry point for 5V and GND. The same header exposes four signal pins — one per channel — which carry the analog voltage readings to the microcontroller.

During routing, we encountered traces that could not be laid out without crossing each other. To resolve this, we added three 0 Ω resistors (R1, R10, R11) as jumpers, allowing traces to bridge over one another on a single-layer board without creating a short.

And the general shema turned out to be as follows:

PCB Design¶
With the schematic finalised, the layout was done in KiCad.
The four photoresistors were placed symmetrically. Notably, the photoresistors were placed on the back side of the board — this is intentional, as the PCB will be mounted to the enclosure from the back, positioning the sensors flush against the divider walls on the outside.
Analog signal traces were kept short to minimise noise, and power distribution — 5V and GND — was routed across the board for stable operation across all four channels. The layout also includes mounting holes at the corners for screw attachment to the enclosure:

The KiCad 3D viewer was used as a final visual check before export, allowing us to verify component placement and overall board geometry before committing to fabrication.

G-code Generation¶
Toolpaths were generated using mods.cba.mit.edu. Two separate SVG files were exported from KiCad — one for the copper traces, and one for the board outline and holes — and each was processed independently in mods to produce two NC files:
- NC file 1 — trace isolation milling, using a 0.4 mm end mill
- NC file 2 — outline cutting and hole drilling, using a 0.8 mm end mill

Milling on the Roland SRM-20¶
The two NC files were run sequentially on the Roland SRM-20 desktop CNC mill. The copper-clad FR1 board was fixed to the bed, the tool was zeroed, and the trace isolation pass was run first, followed by the outline and hole pass with the larger bit.
(PHOTO — Roland SRM-20 during milling)
Soldering¶
After milling, all components were soldered onto the board — except the photoresistors.
(PHOTO — board after soldering, without photoresistors)
The photoresistors were left unsoldered intentionally. They were only soldered after the board was mounted onto the enclosure, ensuring that each sensor sits in exactly the right position relative to the divider walls. This process is described in the Soldering the Photoresistors section of the Assembly chapter.
Servo Motor Selection¶
After completing the PCB design phase, we moved on to selecting the motors. We needed to provide both vertical and horizontal rotation for the system.
Comparing Specifications¶
We considered two different servo motors.
| Feature | HK-15138 Servo Motor | Corona DS-939MG Servo Motor |
|---|---|---|
| Image | ![]() |
![]() |
| Motor Type | Standard analog servo | Digital metal gear servo |
| Operating Voltage | 4.8V – 6V | 4.8V – 6V |
| Torque | ~4.3 kg.cm | ~2.5–2.7 kg.cm |
| Current | ~0.7–1 A (load dependent) | ~0.2–0.24 A (normal operation) |
| Speed | ~0.17 sec/60° | ~0.13–0.14 sec/60° |
| Gear Type | Plastic gears | Metal gears |
| Weight | ~38 g | ~12.5 g |
| Rotation Angle | ~180° | ~180° |
| Control Signal | PWM | PWM |
As an initial criterion, we looked at the torque value. In the case of the Corona DS-939MG Servo Motor, the rated torque is ~2.5–2.7 kg·cm. This figure seemed promising, as we intended to keep our device as lightweight as possible. It means the servo can rotate a 250-gram load at an arm length of 10 cm, which is well suited for our purposes.
Servo Motor Test¶
We didn’t stop there — it was important for us to observe the servo motor’s behavior under load firsthand.
To do this, we used a laser cutter to cut a circular disc from 3 mm plywood with a diameter of 150 mm, which allowed us to place weights on it.
We then attached the disc to the servo motor and connected it to an Arduino UNO.
Test Code¶
For this test we used the official Arduino example from the Arduino servo motor documentation:
#include <Servo.h>
Servo myservo; // create servo object to control a servo
// twelve servo objects can be created on most boards
int pos = 0; // variable to store the servo position
void setup() {
myservo.attach(9); // attaches the servo on pin 9 to the servo object
}
void loop() {
for (pos = 0; pos <= 180; pos += 1) { // goes from 0 degrees to 180 degrees
myservo.write(pos); // tell servo to go to position in variable 'pos'
delay(15); // waits 15ms for the servo to reach the position
}
for (pos = 180; pos >= 0; pos -= 1) { // goes from 180 degrees to 0 degrees
myservo.write(pos); // tell servo to go to position in variable 'pos'
delay(15); // waits 15ms for the servo to reach the position
}
}
This code sweeps the servo continuously from 0° to 180° and back, allowing us to observe its behavior during repeated motion cycles:
Performance Under Load¶
After verifying the basic motion, we began applying various weights to the servo to simulate real operating conditions. By gradually increasing the load, we were able to assess whether the motor could reliably handle the mechanical demands of our sun-tracking system:
After testing, we measured the weight of the assembly using a scale. The total weight of the upper structure came to approximately 805 grams:

Performance Under Vertical Load¶
We also tested an alternative load configuration, with the weight suspended vertically:
Final Selection¶
Based on the test results, it was clear that the servo motor could operate stably and provide the necessary range of motion even under load. These findings informed our final motor selection and confirmed its suitability for our sun-tracking system.
It is also worth noting that the Corona DS-939MG Servo Motor allows us to avoid the need for an external power supply. A single servo draws approximately 200 mA, and since we do not intend to run both motors simultaneously, the total current remains within the 500 mA that the Arduino UNO can supply through its 5V pin — more details on this can be found here.
Coding¶
After assembly, we moved on to programming.
Naming the Photoresistor Pins¶
To work with the data, we needed to name the 4 photoresistors. We moved our device to the position where both servo motors are at 0 degrees — 0° for the horizontal and 0° for the vertical — and began naming the pins according to the photoresistors:
#define topLeft A1
#define topRight A0
#define bottomLeft A3
#define bottomRight A2
Reading Sensor Data¶
Next, we needed to read data from the photoresistors. We created variables with short pin names, assigned the sensor readings to the corresponding variables, and printed them to the Serial Monitor:
#define topLeft A1
#define topRight A0
#define bottomLeft A3
#define bottomRight A2
void setup() {
Serial.begin(9600);
pinMode(topLeft, INPUT);
pinMode(topRight, INPUT);
pinMode(bottomLeft, INPUT);
pinMode(bottomRight, INPUT);
}
void loop() {
int tL = analogRead(topLeft);
int tR = analogRead(topRight);
int bL = analogRead(bottomLeft);
int bR = analogRead(bottomRight);
Serial.print("tL:");
Serial.println(tL);
Serial.print("tR:");
Serial.println(tR);
Serial.print("bL:");
Serial.println(bL);
Serial.print("bR:");
Serial.println(bR);
delay(1000);
}
We also added delay(1000); so that sensor values are read once per second.
We then verified the photoresistors were connected correctly by covering each one with a finger.
(PHOTO)
Mapping Sensor Pairs to Servo Axes¶
The next step was to determine which pairs of photoresistors control the horizontal servo motor, and which control the vertical servo motor. The layout is as follows:

Horizontal Servo Control¶
First, we developed code for the horizontal servo motor only.
We imported the Servo library:
#include <Servo.h>
Then created a servo object:
Servo horizontalservo;
And a global variable to track its position:
int horizontalpos = 90;
We initialized the variable with a value because the servo model we are using has no way of reporting its current position. For our task, we need to know the starting position of the servo — more on this shortly.
In the setup() function, we attached the servo to pin 8 and moved it to the initial position horizontalpos:
horizontalservo.attach(8);
horizontalservo.write(horizontalpos);
We chose tL and tR as the comparison pair.
Since a photoresistor works such that lower resistance corresponds to a lower analog reading, if tL < tR — meaning the Top Left sensor receives more light (and is oriented toward the 0° end of the servo’s range), while the Top Right sensor is oriented toward 180° — then we need to decrease horizontalpos by 1 degree.
Only 1 degree at a time, since after each movement we need to re-check whether the condition tL < tR still holds.
In code:
if(tL < tR) {
horizontalpos = horizontalpos - 1;
horizontalservo.write(horizontalpos);
delay(100);
}
And the opposite condition:
if(tR < tL) {
horizontalpos = horizontalpos + 1;
horizontalservo.write(horizontalpos);
delay(100);
}
Since we were working indoors, we tested the code using a flashlight.
(PHOTO)
Adding Boundary Limits¶
The code had one issue: servo position is bounded between [0, 180], but our code placed no limits on the value. So for the first condition, we added a check that horizontalpos > 0 — meaning the motor will only move further if its current position is above 0. If it has already reached 0, it stays there even if the light intensity on the tL side is higher than on tR.
We added the same constraint for the opposite direction:
if(tL < tR && horizontalpos > 0) {
horizontalpos = horizontalpos - 1;
horizontalservo.write(horizontalpos);
delay(100);
}
if(tR < tL && horizontalpos < 180) {
horizontalpos = horizontalpos + 1;
horizontalservo.write(horizontalpos);
delay(100);
}
Preventing Oscillation with a Threshold¶
This fixed the boundary issue, but there was still another problem. If both sensor values are close to each other — say 112 and 116 — the code treats that as a meaningful difference and rotates 1 degree. After rotating, the values might become 111 and 109, which triggers a rotation back, and so on indefinitely. The device would keep oscillating and never settle.
To prevent this, we added a third condition: the motor only moves if the percentage difference between the two sensors exceeds a configurable threshold, stored in a global variable:
int threshold = 5;
This value can be adjusted to tune the sensitivity of the device.
With this added, the relevant code became:
if(tL < tR && horizontalpos > 0 && (tR - tL)*100/tR > threshold) { // Added percentage threshold condition
horizontalpos = horizontalpos - 1;
horizontalservo.write(horizontalpos);
delay(100);
}
if(tR < tL && horizontalpos < 180 && (tL - tR)*100/tL > threshold) { // Added percentage threshold condition
horizontalpos = horizontalpos + 1;
horizontalservo.write(horizontalpos);
delay(100);
}
The device now worked correctly.
(VIDEO)
Refactoring: A Universal Function for Both Axes¶
The next step was to add the same logic for the vertical servo motor. Rather than duplicating the rotation logic, it made more sense to create a single universal function — which we did:
void sensdetect(int start, int finish, Servo &s, int &position ) {
if(start < finish && position > 0 && (finish - start)*100/finish > threshold) {
position = position - 1;
s.write(position);
delay(25);
}
if(finish < start && position < 180 && (start - finish)*100/start > threshold) {
position = position + 1;
s.write(position);
delay(25);
}
}
Where:
- start — the sensor value corresponding to the 0° end of the servo’s range
- finish — the sensor value corresponding to the 180° end
- s — the servo motor (horizontal or vertical)
- position — the current position of the corresponding servo
Final Code¶
With this single function handling both axes, the final code is as follows:
#include <Servo.h>
#define topLeft A1
#define topRight A0
#define bottomLeft A3
#define bottomRight A2
int horizontalpos = 90;
int verticalpos = 90;
int threshold = 5;
Servo horizontalservo;
Servo verticalservo;
void setup() {
Serial.begin(9600);
pinMode(topLeft, INPUT);
pinMode(topRight, INPUT);
pinMode(bottomLeft, INPUT);
pinMode(bottomRight, INPUT);
horizontalservo.attach(8);
verticalservo.attach(10);
horizontalservo.write(horizontalpos);
delay(500);
verticalservo.write(verticalpos);
delay(500);
}
void loop() {
int tL = analogRead(topLeft);
int tR = analogRead(topRight);
int bL = analogRead(bottomLeft);
int bR = analogRead(bottomRight);
sensdetect(tL, tR, horizontalservo, horizontalpos);
sensdetect(bL, tL, verticalservo, verticalpos);
}
void sensdetect(int start, int finish, Servo &s, int &position ) {
if(start < finish && position > 0 && (finish - start)*100/finish > threshold) {
position = position - 1;
s.write(position);
delay(25);
Serial.print("position:");
Serial.println(position);
}
if(finish < start && position < 180 && (start - finish)*100/start > threshold) {
position = position + 1;
s.write(position);
delay(25);
Serial.print("position:");
Serial.println(position);
}
}
Possible Improvements¶
The system is functional, but there is room for further development.
-
The code works, but requires additional logic. For example, if the device’s head is at position (0°, 0°) and the light is most intense from the left side, there is currently no logic to make it rotate all the way to the opposite position (179°, 180°). The device would simply stop at the boundary and remain there. Additionally, the current code compares sensors in only two pairs: top-left vs. top-right to drive the horizontal servo, and bottom-left vs. top-left to drive the vertical servo. For more accurate tracking, comparisons between other sensor pairs should also be incorporated.
-
It is also worth noting that our device locates the position of maximum light intensity, but it is not designed to actually rotate a solar panel. A real-world solar panel tracker would require a much more robust mechanical construction. Furthermore, for energy-efficient operation, the motor drive system should use worm gears. A worm gear prevents back-rotation when the motors are powered off — an important property for a tracker that needs to hold its position without continuously drawing power. A similar mechanism was used by our lab instructor Babken Chugaszyan in his final project.
-
Additionally, placing capacitors (100–470 µF) across the power supply of each servo motor would help stabilize voltage, reducing noise and preventing unexpected resets of the microcontroller caused by current spikes during motor movement.

