Arduino Line Following Robot for the Science Olympics :: Member Project

Need an Arduino line following robot to win a race?

So did Mario Kart – or should I say the Mario Kinetic Autonomous Racing Team!   This group of 4 high school students built and programmed a robot using Arduino, DC motors, a servo, ultrasonic and infrared radiation sensors, and more – then they raced it in a competition.

Read below to learn more about their project, journey, and make sure to check out the code at the bottom of the post!

(Editor’s Note: The robot is called Tim)

Why the heck did you build a Line following robot?

Most of the members of our APEGA (The Association of Professional Engineers and Geoscientists of Alberta) Science Olympics team have been together for five years, completing challenges together since grade seven (we’re now in grade eleven).

This year, our task was to use a microcontroller to create an autonomous, line-following robot that could move along a black line of electrical tape (while maximizing the speed at which it could do this – it was a race!)

Arduino robot

How does your project work?

Our project uses an Arduino UNO for its brain, along with two DC motors for forward thrust, a steering servo, two infrared radiation sensors (to go on either side of the black line) and three ultrasonic sensors (which were used to detect any cars in front of ours, stopping a crash).

The code was written to prioritize steering over everything else (we used interrupts to make sure that Tim would never miss the states of the infrared sensors changing and thus missing a cue to turn with the line).

With the interrupt service routines triggering every so often, Tim is also programmed to trigger the ultrasonic sensors to send a signal, measure the time between when the signal was sent and received, and then use that time to calculate the distance between Tim and an approaching object.

These distance readings are then used to determine how fast Tim should be going, which is adjusted using pulse width modulation to the DC thrust motors.

Arduino Robot

 

What was your biggest struggle as you worked through this project?

At first, we had troubles getting motors that could move the load that we had in mind.

Cheap DC motors you can get off of Amazon or from the dollar store were not even close to being strong enough, as we needed to move the entire chassis, all the sensors and electrical components, along with the batteries (these were the biggest source of weight in our design).

Arduino layout with sensors

At last, we had to go to a local hobby shop to buy some better quality motors at a higher price, but at least they worked.

Our biggest struggle with the project was trying to put our code theory into practice. We had to get to know each sensor that we used to get a feel for how they worked and how they would communicate with the Arduino.

We had a lot of trouble trying to account for the time Tim would take to accelerate (in its relation to what speed Tim should be going at at any given time to ensure a controlled steer) as well as the rate at which the batteries were discharging.

Throughout our testing, we were left confused time after time when the batteries were changed, because of the change in available power to the thrust motors – we tried to compensate by increasing/decreasing the duty cycle in the PWM signal, but as the batteries discharged, the value we entered no longer became appropriate and we had to readjust the value.

Arduino Robot

Did the project end up as you expected?

Unfortunately, no. Tim totally failed on the day of the competition, despite our endless testing and problem-solving at home.

First we had troubles with the battery pack (it had gotten so hot that the electrical connections lost contact in places) and then Tim couldn’t turn in the right direction anymore (something went very wrong with the interrupts, because there was inconsistency in the direction the steering servo turned when a line was detected for a certain side, if it ever decided to turn at all).

However, in the end, we were able to explain our design process to the judges, who gave us about 60 of a possible 100 points for our efforts.

Along with us almost acing the other two mystery challenges (which we weren’t allowed to prepare for), we were placed in the gold tier of the competition. 🙂    (Editor’s note: Yay!)

Looking back on this project, what can you say you have learned about programming and/or electronics through the creation process?

Although we were all in different places of understanding (some of us had no experience at all with Arduino, some had taken computer science in school; some had very limited knowledge about circuitry, while others were more experienced at it), we were able to use each other’s strengths to come to our result.

We all learned more about many different functions (for(), digital/analogRead/Write(), etc.), libraries (namely the servo library), and concepts (workings of an ultrasonic sensor, interrupts, etc.), allowing each of us to achieve a greater understanding of how logical computers have to process information, opening our eyes to the many different sensing and processing mechanisms that we subconsciously use everyday.

For all of us, programming and electronics continues to bring fascination into our lives.  (Editor’s note: Me too!)

Arduino robot

Was the training at Programming Electronics Academy able to help you build your skill?

After I got my first Arduino at the end of 2018, I was having quite a bit of trouble with it. I noticed that many internet explanations already assumed that you had a bunch of knowledge already, so I felt lost most of the time.

Then, in the summer of 2019 (time really flew as I had many school commitments), I came across a video made by the Programming Electronics Academy on Youtube, which went through everything step-by-step and assumed you knew nothing about electronics (which I enjoy, because I’ve found that it’s hard to grasp a difficult concept by jumping right in – you’ve got to learn the basics first).

It was then I found PEA, and over the rest of the summer, I completed the Arduino Course for Absolute Beginners 2nd Edition, the Basic Electronics course, and well as the Internet of Things course.

If it wasn’t for PEA, I would not have been able to contribute in the way I did to our group’s Science Olympics project. Thank you!

About Mario KART (Mario Kinetic Autonomous Racing Team)

Mario Kinetic Autonomous Racing Team

I am a high school student (along with the rest of my team Jason, Ryan & Daisy) .

I’ve been interested in electronics for as long as I can remember. When I was in elementary school, I worked on more mechanical projects, and longed to be able to understand and apply electronics, and I can still remember my grandpa (a retired construction teacher) sitting me down and teaching me about concepts such as the series and parallel circuit.

Once I got into junior high, I gradually gained the powerful tool of research, and was able to learn about electronics on my own.

After a while of taking a look at other people’s projects online and hearing that they used something called an Arduino, I finally decided that I needed to know why so many people knew about this technology.

I watched one video, and was instantly hooked. I received my first Arduino for Christmas in 2018 and it has been one of my prominent passions since then.

The other members of our team had not been familiar with Arduino at all, but had some experience with other programming prior to working on this project.

A couple of them learned about coding in junior high (and continue to take the course in high school) while another member was with me on our junior high school’s FIRST Lego League robotics team for all three years. Another of our members is currently on a FIRST Tech Challenge team with our local Telus World of Science.  (This is a main reason for why we are submitting this – many of us have many great memories participating in FIRST’s robotics competitions.)

The Arduino Code:

// Mario KART (Kinetic Autonomous Racing Team)'s code for the division four APEGA Science Olympics (February 22, 2020) classroom challenge!

// We need this library to properly communicate with servos:
#include <Servo.h>

// Create instances/objects of the "Servo" class:
Servo steeringServo;
Servo brakingServo;

// Servo pins:
byte steeringServoPin = A4;
byte brakingServoPin = A5;

// INPUTS:
byte irR = 2; // NO CHANGING!!! Interrupts must be on these two pins.
byte irL = 3; // "    "    "    "
byte usHighEcho = 4;
byte usLowEcho = 5;
byte systemStatusButton = 6;
byte usMidEcho = 7;

// OUTPUTS:
byte usHighTrig = 8;
byte usLowTrig = 9;
byte thrustStatus1LED = 10;
int backThrust = 11;
byte thrustStatus2LED = 12;
byte thrustStatus3LED = 13;
byte usStatusLED = 14; // AKA A0
byte usMidTrig = 15; // AKA A1
byte startupLED = 16; // AKA A2
byte systemRunStatusLED = 17; // AKA A3

// Servo information:
byte straightAhead = 88; // adjust this number to calibrate the steering servo.
byte steerMagnitude = 27; // adjust this number to calibrate the amount steered in each direction when steering is needed.
byte brakingPosition = 90;
byte unbrakingPosition = 0;
// Declared "volatile" because these values are being changed in an interrupt service routine!
volatile boolean lineDetectedRight = false;
volatile boolean lineDetectedLeft = false;
unsigned long timeAtLastSteer = 0;

// Thrust information:
byte reducedSpeed = 160; // Must be a number between 0 and 255, inclusive
int fullSpeed = 255; // "    "    "    "
// Both of the below are needed as thrust may need to be adjusted based on both steering and obstacle situations.
byte obstacleThrustStatus; // 0 : brake | 1 : power is cut | 2 : reduced speed | 3 : full speed
byte steeringThrustStatus; // "     "     "     "

// Boolean variables for flow structure:
boolean startupSequenceRan = false;
boolean systemRunStatus = false;
boolean startupSequenceNecessary = false;

// Other program timers
// Main loop() counter to keep steering a priority, opposed to checking ultrasonic sensors unnecessarily...
byte supervisoryLoopRunCycles = 0;
byte supervisoryLoopRunCycleLimit = 17;
unsigned long systemRunStartTime;

//-------------------------------------------------------------------------------------------------------------------------

void setup() {

  Serial.begin(9600);

  steeringServo.attach(steeringServoPin);
  brakingServo.attach(brakingServoPin);

  steeringServo.write(straightAhead);

  // Using a for() loop to initialize all necessary pins as INPUTs or OUTPUTs.
  for (byte i = 2; i <= 7; i++) {
    pinMode(i, INPUT);
  }
  for (byte i = 8; i <= 17; i++) {
    pinMode(i, OUTPUT);
  }

  if (startupSequenceNecessary == true) {
    startupSequence();
  }

  attachInterrupt(digitalPinToInterrupt(irR), lineDetectedRightISR, RISING); // the sensors report a change from 0 to 1...
  attachInterrupt(digitalPinToInterrupt(irL), lineDetectedLeftISR, RISING); // ...when the black line is detected. "HIGH" mode option not available for the UNO, so code has to accomodate this.
}

//-------------------------------------------------------------------------------------------------------------------------

void loop() {

  //Serial.println("Main loop initialized.");

  if ((digitalRead(systemStatusButton) == true)) {
    //Serial.println("systemStatusButton read!");
    systemRunStatus = true;
    digitalWrite(systemRunStatusLED, HIGH);
    systemRunStartTime = millis();
  }
  
  //Serial.println (systemRunStartTime);

  if (systemRunStatus == false) {
    steeringServo.write(straightAhead);
  }

  // This is the "supervisory loop" - it looks after everything by calling each user-defined function:
  while (systemRunStatus == true && supervisoryLoopRunCycles <= supervisoryLoopRunCycleLimit) {
    //Serial.println(supervisoryLoopRunCycles);
    if (supervisoryLoopRunCycles == supervisoryLoopRunCycleLimit) {
      //Serial.println("crashDetection() begin");
      crashDetection();
    }
    //Serial.println("steer() begin");
    steer();
    //Serial.println("thrust() begin");
    thrust();
    supervisoryLoopRunCycles++;
  }
  supervisoryLoopRunCycles = 0; // Reset the run cyles once the while() is exited.
}

//-------------------------------------------------------------------------------------------------------------------------

void startupSequence() {

  while ((digitalRead(systemStatusButton) == HIGH) && (startupSequenceRan == 0)) {

    //Serial.println("startupSequence() begin");

    digitalWrite(startupLED, HIGH);

    // Thrust Test
    digitalWrite(backThrust, true);
    delay(200);
    digitalWrite(backThrust, false);
    delay(3000);
    digitalWrite(backThrust, true);
    delay(2000);
    digitalWrite(backThrust, false);

    for (int i = 0; i <= 255; i = i + 20) {
      analogWrite(backThrust, i);
      delay(200);
    }

    digitalWrite(backThrust, true);
    delay(1500);
    digitalWrite(backThrust, false);

    // Steering Test
    for (byte pos = straightAhead - 45; pos <= straightAhead + 45; pos += 1) { // goes from 45 degrees to 135 degrees
      // in steps of 1 degree
      steeringServo.write(pos);              // tell servo to go to position in variable 'pos'
      delay(15);                       // waits 15ms for the servo to reach the position
    }
    for (byte pos = straightAhead + 45; pos >= straightAhead - 45; pos -= 1) { // goes from 45 degrees to 135 degrees
      steeringServo.write(pos);              // tell servo to go to position in variable 'pos'
      delay(15);                       // waits 15ms for the servo to reach the position
    }

    steeringServo.write(straightAhead);

    startupSequenceRan = startupSequenceRan + 1;
    digitalWrite(startupLED, LOW);
  }
  if (startupSequenceRan == 0) {
    startupSequence();
    //Serial.println("startupSequence() called again due to no button press");
  }
}

//-------------------------------------------------------------------------------------------------------------------------

void lineDetectedRightISR() {
  lineDetectedRight = true;
  steeringServo.write(straightAhead - steerMagnitude);
  //noInterrupts();
}
void lineDetectedLeftISR() {
  lineDetectedLeft = true;
  steeringServo.write(straightAhead + steerMagnitude);
  //noInterrupts();
}

//-------------------------------------------------------------------------------------------------------------------------

void crashDetection() {

  // These are arrays to store the data collected from the ultrasonic sensors.
  // Three sensors report three values + the null character at the end = 4 components
  float waveTravelDurationsUs[4];
  float distancesToObstacleCm[4];
  float closestDistanceCm;

  // Sending and reading signal on the lower US sensor
  digitalWrite(usLowTrig, HIGH);
  delayMicroseconds(10);
  digitalWrite(usLowTrig, LOW);
  waveTravelDurationsUs[0] = pulseIn (usLowEcho, HIGH);

  // Sending and reading signal on the middle US sensor
  digitalWrite(usMidTrig, HIGH);
  delayMicroseconds(10);
  digitalWrite(usMidTrig, LOW);
  waveTravelDurationsUs[1] = pulseIn (usMidEcho, HIGH);

  // Sending and reading signal on the higher US sensor
  digitalWrite(usHighTrig, HIGH);
  delayMicroseconds(10);
  digitalWrite(usHighTrig, LOW);
  waveTravelDurationsUs[2] = pulseIn (usHighEcho, HIGH);

  // Using the travel times of the waves, calcuate the distances they traveled:
  distancesToObstacleCm[0] = (waveTravelDurationsUs[0] * 0.034) / 2;
  distancesToObstacleCm[1] = (waveTravelDurationsUs[1] * 0.034) / 2;
  distancesToObstacleCm[2] = (waveTravelDurationsUs[2] * 0.034) / 2;

  // Compare the distances each sensor reported; whichever reports a closer object is the one we want to use to consider what to do.
  if (distancesToObstacleCm[0] < distancesToObstacleCm[1] && distancesToObstacleCm[0] < distancesToObstacleCm[2]) {
    closestDistanceCm = distancesToObstacleCm[0];
  }
  else if (distancesToObstacleCm[1] < distancesToObstacleCm[0] && distancesToObstacleCm[1] < distancesToObstacleCm[2]) {
    closestDistanceCm = distancesToObstacleCm[1];
  }
  else {
    closestDistanceCm = distancesToObstacleCm[2];
  }

  //Serial.print("closestDistanceCm: ");
  //Serial.println(closestDistanceCm);

  // Depending on each case of clostestDistanceCm, manipulate the global variable obstacleThrustStatus to an appropriate level.
  if (closestDistanceCm <= 10) {
    obstacleThrustStatus = 0; // If an object is less than or equal to 10cm away, apply the brake.
    digitalWrite(usStatusLED, LOW);
  }
  else if (closestDistanceCm > 10 && closestDistanceCm <= 30) {
    obstacleThrustStatus = 1; // If an object is between 10 and 30 cm away (including 30cm), cut forward thrust power.
    analogWrite(usStatusLED, 100);
  }
  else if (closestDistanceCm > 30 && closestDistanceCm <= 60) {
    obstacleThrustStatus = 2; // If an object is between 30 and 60 cm away (including 60cm), continue at a lower speed.
    analogWrite(usStatusLED, 170);
  }
  else {
    obstacleThrustStatus = 3; // If the closest distance reported is more than 60cm away, go full speed ahead.
    digitalWrite(usStatusLED, HIGH);

    //Serial.println(distancesToObstacleCm[0]);
    //Serial.println(distancesToObstacleCm[1]);
    //Serial.println(distancesToObstacleCm[2]);
  }
}

//-------------------------------------------------------------------------------------------------------------------------

void thrust() {
  byte thrustStatus;

  //Serial.print("obstacleThrustStatus: ");
  //Serial.println(obstacleThrustStatus);
  //Serial.print("steeringThrustStatus: ");
  //Serial.println(steeringThrustStatus);

  if (obstacleThrustStatus < steeringThrustStatus) {
    thrustStatus = obstacleThrustStatus;
  }
  else {
    thrustStatus = steeringThrustStatus;
  }

  if (thrustStatus == 0) {
    digitalWrite(backThrust, LOW);
    brakingServo.write(brakingPosition);
    digitalWrite(thrustStatus1LED, LOW);
    digitalWrite(thrustStatus2LED, LOW);
    digitalWrite(thrustStatus3LED, LOW);
  }
  else if (thrustStatus == 1) {
    brakingServo.write(unbrakingPosition);
    digitalWrite(backThrust, LOW);
    digitalWrite(thrustStatus1LED, HIGH);
    digitalWrite(thrustStatus2LED, LOW);
    digitalWrite(thrustStatus3LED, LOW);
  }
  else if (thrustStatus == 2) {
    brakingServo.write(unbrakingPosition);
    analogWrite(backThrust, reducedSpeed);
    digitalWrite(thrustStatus1LED, HIGH);
    digitalWrite(thrustStatus2LED, HIGH);
    digitalWrite(thrustStatus3LED, LOW);
  }
  else if (thrustStatus == 3) {

    brakingServo.write(unbrakingPosition);
    analogWrite(backThrust, fullSpeed);
    digitalWrite(thrustStatus1LED, HIGH);
    digitalWrite(thrustStatus2LED, HIGH);
    digitalWrite(thrustStatus3LED, HIGH);
  }
}

void steer() {
  unsigned long timeSinceLastSteer;
  
  if (lineDetectedRight == true) {
    while (digitalRead(irR) == true) {
      steeringServo.write(straightAhead - steerMagnitude);
    }
    timeAtLastSteer = millis();
    lineDetectedRight = false;
    //interrupts();
  }
  else if (lineDetectedLeft == true) {
          
    while (digitalRead(irL) == true) {
      steeringServo.write(straightAhead + steerMagnitude);
    }
    timeAtLastSteer = millis();
    lineDetectedLeft = false;
    //interrupts();
  }
  else { //((digitalRead(irR) == LOW) && (digitalRead(irL) == LOW))
    //Serial.println("Turning straight.");
    steeringServo.write(straightAhead);
    steeringThrustStatus = 3;
  }
  timeSinceLastSteer = millis() - timeAtLastSteer;
  //Serial.println (timeSinceLastSteer);
  //Serial.println (timeAtLastSteer);

  if (timeSinceLastSteer > 4500 && (millis() - systemRunStartTime) > 10000) {
    steeringThrustStatus = 0;
  }
}

installing Arduino libraries

Installing Arduino Libraries | Beginners Guide

IoT sewage project

Pumping poo! An IoT sewage project

ESP32 webOTA updates

How to update ESP32 firmware using web OTA [Guide + Code]

error message Brackets Thumbnail V1

expected declaration before ‘}’ token [SOLVED]

Compilation SOLVED | 1

Compilation error: expected ‘;’ before [SOLVED]

Learn how to structure your code

1 Comment

  1. […] post Arduino Line Following Robot for the Science Olympics :: Member Project appeared first on Programming Electronics […]