Line Following Car
// assign pins
const int L2 = 10;
const int L1 = 12;
const int middle = 4;
const int R1 = 8;
const int R2 = 7;
const int sensorPins[5] = {L2, L1, middle, R1, R2};
const int outerSensors[4] = {L2, L1, R1, R2}; // sensors to the left and right of the middle sensor
const int obstaclePin = 13;
const int wheel_left = 11;
const int wheel_right = 9;
const int wheelPins[2] = {wheel_left, wheel_right};
unsigned long lastHighTime = 0; // timestamp for when a sensor last read HIGH
const unsigned long timeoutDuration = 2000; // 2 seconds
int k_p = 15; // error constant (constant * deviation)
int k_d = 15; // motor derivative (added to error constant)
int k_i = 15; // ~not needed
//forward declarations for helper functions
int obstacleReading;
int readingL2;
int readingL1;
int readingMid;
int readingR1;
int readingR2;
int lineSensorReadings[5] = {readingL2, readingL1, readingMid, readingR1, readingR2};
int lastHighTime;
int speedLeft;
int speedRight;
int correction;
bool anyHigh = false; // for tracking state of line sensors
bool detectObstacle(obstacleReading);
void readLineSensors();
void checkLineSensorsActiveStatus(lastHighTime);
void brake();
//------------------------------------------------------------------------
void setup(){
Serial.begin(9600); //serial on port 9600
for (int i = 0; i < 5; i++){ //init sensors
pinMode(sensorPins[i], INPUT);
}
lastHighTime = millis(); // initialize to current time
speed = 196; // initial speed. will vary after corrections
pinmode(obstaclePin, INPUT);
pinmode(wheel_left, OUTPUT);
pinmode(wheel_right, OUTPUT);
}
//------------------------------------------------------------------------
void loop(){
obstacleReading = digitalRead(obstaclePin);
detectObstacle(obstacleReading);
bool isLeft = false; // for when the line is toward the left of the car
bool isRight = false; // for when the line is toward the right of the car
bool onTrack = false; // for when middle sensor reads HIGH
analogWrite(wheel_left, speedLeft);
analogWrite(wheel_right, speedRight); //~3.8V input into 6V motors
for (int i = 0; i < 4; i++) { // why is this in a loop?
checkLastSensorsActiveStatus(lastHighTime); //input param is previous state
}
readLineSensors();
for (int i = 0; i < 5; i++) {
if (lineSensorReadings[i] == HIGH) {
anyHigh = true;
lastHighTime = millis(); //lastHighTime is a global var
break; // Exit early when one HIGH reading is found
}
} // make this a new helper function or integrate it into readLineSensors()
if (anyHigh){ // if any of the line sensors is HIGH--
if (readingR1 == HIGH) or (readingR2 == HIGH) {
isLeft = true; // assign direction of deviation.
}
else if (readingL1 == HIGH) or (readingL2 == HIGH) {
isRight = true;
}
else if middle = HIGH; {
onTrack = true;
}
} // turn into a function
if (onTrack == false) { // determine the extent of deviation
if (readingL1 == HIGH) or (readingR1 == HIGH) {
error = 1;
}
else if (readingL2 == HIGH) or (readingR2 == HIGH) {
error = 2;
}
} // turn into a function
// adjusts motor speeds
Pout = error * k_p;
Dout = (error - previous_error) * k_d;
correction = (Pout + Dout); // may need to be multiplied by some value
if (isLeft == true) {
speedLeft += correction;
speedright -= correction;
analogWrite(wheel_left, speedLeft);
analogWrite(wheel_right, speedRight);
}
else if (isRight == true) {
speedLeft -= correction;
speedright += correction;
analogWrite(wheel_left, speedLeft);
analogWrite(wheel_right, speedRight);
}
}
//------------------------------------------------------------------------
//------------------------------------------------------------------------
/**
* function to detect where the line is
*/
void readLineSensors() {
for (int i = 0; i < 5; i++) {
lineSensorReadings[i] = digitalRead(sensorPins[i]);
}
}
//------------------------------------------------------------------------
/**
* function to detect and handle if we've hit an obstacle
* @param obstacleReading HIGH or LOW reading from sensor at pin 13
*/
void detectObstacle(obstacleReading) {
if(obstacleReading == HIGH) { // early guarding against obstacle
Serial.println("Obstacle detected. Stopping car.");
brake();
}
}
//------------------------------------------------------------------------
/**
* checking if any of our line detecting sensors are valid, if not we brake
* @param lastHighTime time in ms since last active sensor detection
*/
void checkLineSensorsActiveStatus(lastHighTime) {
if(millis() - lastHighTime >= timeoutDuration) {
Serial.println("No sensor activity for 2 seconds. Stopping car.");
brake();
}
}
//------------------------------------------------------------------------
/**
* force brake by writing 0 to all wheel pins
*/
void brake() {
Serial.println("Braking...");
for (int i = 0; i < 2; i++){
analogWrite(wheelPins[i], 0);
}
}
Summary:
Building the line-following car taught me how mechanical design, electronics, and software must work together for a system to function reliably. Rather than focusing on a single component, this project required thinking at the system level—how the chassis design, sensor placement, motor control, and code logic all influenced performance. Small changes in one area often had noticeable effects elsewhere, which emphasized the importance of iteration and testing.
From a mechanical perspective, I learned how design decisions impact stability, weight distribution, and assembly. Designing and fabricating the chassis required balancing rigidity with simplicity while ensuring proper mounting for motors, sensors, and electronics. I also gained experience translating CAD designs into physical components and identifying discrepancies between the digital model and the real-world build.
On the electronics side, integrating sensors, motors, and the microcontroller helped me understand practical wiring constraints, power management, and signal reliability. Proper sensor placement and alignment turned out to be critical for consistent line detection, and I learned how noise and surface variation could affect sensor readings. Troubleshooting hardware issues reinforced the value of clear wiring layouts and incremental testing.
Programming the control logic was one of the most challenging and rewarding aspects of the project. Writing and tuning the line-following algorithm required experimenting with sensor thresholds, motor speeds, and control logic to achieve smooth and stable motion. Debugging the code taught me how to approach problems methodically, test assumptions, and refine logic based on real performance rather than ideal behavior.
Overall, this project strengthened my ability to approach open-ended engineering problems that do not have a single “correct” solution. It taught me how to iterate through design, build, test, and refine cycles while balancing mechanical, electrical, and software constraints. The experience helped bridge the gap between theoretical concepts and hands-on engineering, and increased my confidence in designing and debugging integrated electromechanical systems.