How to Program a Line Following Robot

In this article, I’d like to cover the two main ways that people choose to program a line following robot and compare the two. I’ll be comparing, in detail, the “Simple Line Follow” and the “PID Line Follow”. Take a look at these videos, at low speeds a simple line follow algorithm is perfectly acceptable, as the speed increases the simple line follow algorithm can’t come close to the power of PID control algorithms.

Simple Line Follow

First off, we have the simple line follow, the general premise behind this type of line follow is that you have anywhere from one to a few sensors and you hard code a motor response based on which sensor sees the line. This is generally the first method people learn when line following because it’s simple to understand and simple to program. Let’s first review the typical sensor options.

One Sensor

The general idea behind one sensor line following is that you set one motor to run at a slightly decrease or increase the speed of a motor so that the robot favors one direction (the direction toward the line). When the sensor sees the line, you would speed up the motor closest to the line briefly to keep if from crossing.

Prossingle_sensor_line_follow_path

  • Simple circuit
  • Easy to program
  • Does the job at a minimal level
  • Not a bad solution for straight lines

Cons

  • If you overshoot the line once it can be disastrous
  • Only works well in a smooth line (no abrupt turns)
  • Time is wasted traveling the extra distance (as opposed to driving straight)
  • Cannot go very fast at all
Example Code

The following should give you an idea of what this code would look like, I have not tested this code so proceed with caution.

#define L_MOTOR         9
#define R_MOTOR         10
#define MAX_SPEED       200
#define SET_SPEED       150
#define MIN_SPEED       135
#define MIDDLE_IR       A1
#define TAPE_THRESHOLD  350   // Anything less is a Black reflectance

void setup() {
  // Intialize pins
  pinMode(MIDDLE_IR, INPUT);
}

void loop() {
  
  if (analogRead(MIDDLE_IR) < TAPE_THRESHOLD) {
    // We are seeing the line under the middle sensor
    analogWrite(L_MOTOR, SET_SPEED);
    analogWrite(R_MOTOR, MAX_SPEED);
    
  } else {
    // Drive slightly right to hug the line
    analogWrite(L_MOTOR, SET_SPEED);
    analogWrite(R_MOTOR, MIN_SPEED);
  }
}

 

Two Sensors

The general idea behind two sensor line following is that when one sensor sees the line, you slow down or stop the motor on the opposite side of the sensor that saw the line. The sensors work in tandem and keep the robot on track (pun intended). If you use this method, try to keep your sensors close together, the closer your sensors are the less wobble you’ll get in your robot when it drives which will give you a more straight driving appearance and waste less time as you are traveling less distance from your goal before adjusting.

Prostwo_sensor_line_follow_path

  • Simple circuit
  • Easy to program

Cons

  • If you lose the line, it can be difficult to recover
  • Time is wasted traveling side to side
  • Not good for high speeds
Example Code
#define L_MOTOR         9
#define R_MOTOR         10
#define MAX_SPEED       250
#define SET_SPEED       200
#define MIN_SPEED       50
#define LEFT_IR         A0
#define RIGHT_IR        A2
#define TAPE_THRESHOLD  350   // Anything less is a Black reflectance

void setup() {
  // Intialize pins
  pinMode(LEFT_IR, INPUT);
  pinMode(RIGHT_IR, INPUT);
}

void loop() {
  
  if (analogRead(LEFT_IR) < TAPE_THRESHOLD) {
    // We are seeing the line under the left sensor, turn left
    analogWrite(L_MOTOR, MIN_SPEED);
    analogWrite(R_MOTOR, SET_SPEED);

  } else if (analogRead(RIGHT_IR) < TAPE_THRESHOLD) {
    // We are seeing the line under the right sensor, turn right
    analogWrite(L_MOTOR, SET_SPEED);
    analogWrite(R_MOTOR, MIN_SPEED);
    
  }
}

 

Three or more sensors

The idea behind three sensor line following is that it makes the two sensor a little more robust. Typically you put the third sensor in the middle and in addition to adjusting like you would with a two sensor line follower, you add a third and even a fourth case. The third case would say, if the line is under the middle sensor, set both motors to the same speed in an attempt to maintain a straight path. The optional fourth case would say, if none of the sensors see the line, either turn the motors off or maybe spin until it finds the line again.

In addition, if you add even more sensors you could add tiers of adjustments. lets say you had four sensors, if the left most sensor sees the line then you could turn the right motor off while if the second sensor from the left sees the line you could simply slow the right motor, this way it would react more abruptly to sharp corners or if it moves farther off the line for some reason.

Pros

  • Most robust simple line follow method
  • Able to detect if the line is lost
  • Gradual adjustment versus strictly on/off or high/low speed
  • Easy to program

Cons

  • Still not a true analog response to error (robot often jitters while line following)
  • Not good for high speeds
Example Code
#define L_MOTOR         9
#define R_MOTOR         10
#define MAX_SPEED       200
#define SET_SPEED       200
#define MIN_SPEED       50
#define STOP_SPEED      0
#define LEFT_IR         A0
#define MIDDLE_IR       A1
#define RIGHT_IR        A2
#define TAPE_THRESHOLD  350   // Anything less is a Black reflectance

void setup() {
  // Intialize pins
  pinMode(LEFT_IR, INPUT);
  pinMode(MIDDLE_IR, INPUT);
  pinMode(RIGHT_IR, INPUT);
}

void loop() {
  
  if (analogRead(LEFT_IR) < TAPE_THRESHOLD) {
    // We are seeing the line under the left sensor, turn left
    analogWrite(L_MOTOR, MIN_SPEED);
    analogWrite(R_MOTOR, SET_SPEED);

  } else if (analogRead(MIDDLE_IR) < TAPE_THRESHOLD) {
    // We are seeing the line under the middle sensor, go straight
    analogWrite(L_MOTOR, SET_SPEED);
    analogWrite(R_MOTOR, SET_SPEED);
    
  } else if (analogRead(RIGHT_IR) < TAPE_THRESHOLD) {
    // We are seeing the line under the right sensor, turn right
    analogWrite(L_MOTOR, SET_SPEED);
    analogWrite(R_MOTOR, MIN_SPEED);
    
  } else {
    // We don't see the line at all, let's stop
    analogWrite(L_MOTOR, STOP_SPEED);
    analogWrite(R_MOTOR, STOP_SPEED);
  }
}

PID Line Follow

Now lets go over a slightly more complex but much more accurate line following algorithm, PID line following. I notice that many beginners are put off by the complexity of PID line following and tend to rely on a library to do their PID calculations. I recommend you start with your own simple PID algorithm so you understand what’s going on. Sure, you can get away with using a PID library but then you’re not really learning how it works so it’s difficult to fine tune your values and know exactly how that is going to affect the outcome.

Pros

  • Very smooth and accurate line following
  • Can typically follow the line much faster than the “Simple” method
  • Greater flexibility if you want to follow along various positions (follow on center, to the left slightly, to the right slightly)

Cons

  • Requires tuning which can take time
  • Can seem overwhelming without guidance (hopefully this article remedies that)
  • More sensors will take more time to capture readings from

What is PID?

First of all, let’s discuss a little about what PID is. PID is an acronym for Proportional Integral Derivative but don’t let that scare you, it’s very easy and there is no calculus required going forward. The idea behind PID is that you have an actual value and a goal value or a setpoint. You make some adjustment to the system based on the difference between your goal and your actual values.

P

The first aspect of that adjustment is the P, which is simply a coefficient… that is it’s a number that you multiply your error by to then apply that as an adjustment. Let’s say you are driving a car and you are trying to keep a constant speed of 55MPH, if you read your speedometer and you see it’s currently 55, you know that you are five MPH too low so you need to push down on the throttle to bring the speed back up. Let’s say you check your speedometer once ever second and make an adjustment, most likely you will slowly release off the throttle. So, with a P value we need to determine a value to convert how far off we are to how much we need to adjust the throttle.

Let’s say, for example, one inch of throttle movement will typically increase the speed by one MPH over the course of one second. Well, doing some simple math, we can come up with a pretty straight forward algorithm for this. If we would multiply our error by one MPH we would then have a P value of 1.

adjustment = P*error

adjustment = 1*error

Voila, if the speed is off six MPH, our adjustment will be (1*6) or 6 inches, if it’s twelve MPH off, (1*12) or 12 inches to move the throttle. Likewise, if it is 8 MPH over we will adjust by -8 inches. The point is, now we have an adjustment that is based off of some knowledge and in the case of our motors, we would make a smaller adjustment if the robot was only a small amount off of the line.

Anyone who has ever driven a vehicle knows that there isn’t a linear relationship between the distance you move the throttle and the amount of increased speed over one second of time. This is where the other elements of PID come into play.

I

One caveat with I is that I don’t typically use this term in a line follower. The I term is the most complicated term to use for beginners so unless you have experience, here is where I would use a library. But again, I don’t typically use I so I’ll show you an example that doesn’t use this term but still gives you great line following.

The P value is great, but it’s not a perfect system you see? The truth is, when the vehicle is moving slower your acceleration will be different than when it is moving faster. Also, if you have your foot at a cool 6 inches of throttle, what happens when you hit a hill, that same throttle position won’t maintain your speed, this is where the I term is going to come into play. The thing about the I term is it keeps track of your errors and accounts for external factors. If your P value doesn’t seem to be making the appropriate changes at decent rate, the I term is going to slowly grow to force your adjustment to be larger because it’s adding up every error and applying that to your adjustment. So, a PI controller algorithm would look like this:

adjustment = P*error + I*sumOfErrors

Where the sumOfErrors is the adding of errors each time you compute your adjustment. So, going back to the uphill issue I mentioned, if you’re able to generally cruise along without moving your throttle, then you hit a hill, your actual speed vs your goal is going to grow giving you an error, each time you have an error your I term is going to tell you to push more on the throttle until eventually you get up to speed. Once you get to the top of the hill though, you have now added more throttle but the hill is gone, you’re going to need to start releasing the throttle, generally this is how the I term would level back out, you would be overshooting your goal so your error would be negative thus subtracting from your sum of I values. The issue with I is you need to know when to reset it or possibly let it settle on its own. This is the main reason I don’t use I, it’s difficult for beginners to grasp and it can drastically affect your outcome without really providing much of a benefit in the case of a line following robot.

With a line following robot, the part where the I term would be very helpful is if you have a motor that is failing so your robot is constantly driving off to one side. Your I term would be summing all of the errors and eventually that sum would settle to a point where it would make up for that weak motor and constantly apply an extra adjustment in that direction.

D

A proportional control algorithm would be great by itself except for the fact that it is so erratic, it never tends to settle very well on the goal value. This is because it’s just a simple coefficient that is applied to range of errors when in fact, we wish that coefficient could possibly make itself less effective when we are at our goal. This is where the D value comes in, the D value computes the difference between the last error and the current error. So, as an example, if the first reading was an error of 5 and the new reading is an error of 1 then the derivative adjustment would be the D coefficient multiplied by the difference in error (1-5) or -4. If we add the D term to our PI controller, it would look like this:

adjustment = P*error + I*sumOfErrors + D*(error – lastError)

Or, if we simply had a PD control algorithm

adjustment = P*error + D*(lastError – error)

So, if we try to analyze this calculation as shown with PD we can see that our P portion, as we’ve learned already, applies a multiplicative factor based on the amount of error. The D value however, applies a multiplicative factor to the difference in error, so what does this mean? As we approach our goal, our D value is going to dampen the effect of P so we don’t overshoot the goal. If you noticed, our example had a negative value, so this will effectively lessen the adjustment as we approach an error of 0.

line_with_errorsYou can also think of it this way, if the error is growing, then P isn’t doing a good enough job, in this case the new error will be greater than the old error because we are moving away from the goal. A larger number minus a smaller number will result in a positive value which is going to add to the P portion to help get back on track. On the other end, if we are correctly approaching the goal we will have a current error which is smaller than the previous error and this would subtract from the P portion to make sure we don’t overshoot our goal. See the image on the right to get a better visual of the error in relation to the line.

PID Line Following Algorithm

Now that we have a basic understanding of what PID is and how it works, let’s get back to talking about how to implement this in a line following robot.

Flow Chart

Let’s start by going over a flow chart which covers the basic aspects of PID line follow. Hopefully the flow chart helps you understand the algorithm as it relates to code, if you’re better at understanding code by reading the code itself, I have some example code following the flow chart.

Example Code

A few things I need to mention in advance here, this code is based on the use of the Pololu QTR-8RC Sensor array. You could easily modify this for the QTR-8A array or even for use with individual QTR-1RC or QTR-1A IR sensors, I’ve used as few as four IR sensors on a PID line follower with great success. Just adjust the NUM_SENSORS and the pin numbers that are passed into the QTRSensorsRC object. Also, if you have a different number of sensors, if you want to follow on center, your goal will be different. To calculate center, use the following algorithm: (NUM_SENSORS – 1)*500. Of course, you don’t have to follow on center, you can follow anywhere within the range of sensors.

You’ll need to have the QTR Sensors library which can be found here, the link is a pointer to my personal version of the library. I found a bug that I suggested they fix but they haven’t yet, it can cause some wacky values so I suggest you use my modified library with the fix.

The readLine(sensorValues) function returns a value between 0 and (NUM_SENSORS – 1)*1000. If you have 8 sensors, the reading will be 0-7000, 6 sensors will be 0-5000. In each case, 0 represents the left most sensor and each increment of 1000 after that represents another sensor. 0 == first sensor, 1000 == second sensor and there are ranges in between as well. 500 means the line is between the first and second sensors, 1200 means it’s between the second and third sensors but closer to the second… you get the idea right?

Lastly, my KP and KD values will most likely not work with your robot as every robot is different. These values are dependent on motor speed, robot weight, wheel size, the position of the sensor with respect to the wheels, etc.

#include <QTRSensors.h>

#define SETPOINT    3500  // The goal for readLine (center)
#define KP          0.2   // The P value in PID
#define KD          1     // The D value in PID
#define L_MOTOR     9     // Left motor pin
#define R_MOTOR     10    // Right motor pin
#define MAX_SPEED   200   // The max speed to set motors to
#define SET_SPEED   200   // The goal speed to set motors to
#define MIN_SPEED   0     // The min speed to set motors to
#define NUM_SENSORS 8     // The number of QTR sensors
#define TIMEOUT     2500  // Timeout for the QTR sensors to go low
#define EMITTER_PIN 2     // Emitter pin for QTR sensor emitters

// PID **************************************
int lastError = 0;  // For storing PID error

// SENSORS **********************************
// sensors 0 through 7 are connected to digital pins 3 through 10, respectively
QTRSensorsRC qtrSensors((unsigned char[]) {3, 4, 5, 6, 7, 8, 9, 10}, NUM_SENSORS, TIMEOUT, EMITTER_PIN);
unsigned int sensorValues[NUM_SENSORS];   // For sensor values of readLine()

void setup() {
  // Initialize Pins
  pinMode(L_MOTOR, OUTPUT);
  pinMode(R_MOTOR, OUTPUT);
}

void loop() {
  // Take a reading
  unsigned int linePos = qtrSensors.readLine(sensorValues);

  // Compute the error
  int error = SETPOINT - linePos;

  // Compute the motor adjustment
  int adjust = error*KP + KD*(error - lastError);

  // Record the current error for the next iteration
  lastError = error;

  // Adjust motors, one negatively and one positively
  analogWrite(L_MOTOR, constrain(SET_SPEED - adjust, MIN_SPEED, MAX_SPEED));
  analogWrite(R_MOTOR, constrain(SET_SPEED + adjust, MIN_SPEED, MAX_SPEED));
}

PID Tuning

Tuning the P and D values of this PD control algorithm are outside of the scope of this article. As mentioned, these KP and KD values will likely not work with your robot as every robot is different. PID Tuning is an important aspect of the algorithm, every robot needs to be tuned based on a series of parameters specific to that robot. I will include an article in the coming days and provide a link when that is ready. As a tip until that article comes out, set KD to 0 (which will give you a P controller only) and try to get a KP value that follows the line, don’t worry about KD until you have KP working. Also, try tuning with your motors set to at least half of your desired speed, PID is hard to tune at full speed unless your full speed is already slow.

UPDATE: The PID tuning article can be found here: http://robotresearchlab.com/2019/02/16/pid-line-follower-tuning/

9 Comments

  1. vinay

    i actually wanted to make a pid line follower in such way. Im not able to get the sensor readings properly. its actually that they have updated the library and changed the classes. once check my code and say what is wrong

    1. Vinay, I looked through your code (I removed it from the comment) and there are a few things missing, it doesn’t compile. I’m aware that Pololu has updated their library but from my students’ experience, they have only had problems with the new library so I still recommend that they use my library which is just a slightly modified copy of Pololu’s old library before they added the new QTR HD and MD sensors.

      I would expect that their example code would work for the library though, you couldn’t get the example code to work (just printing sensor values)? If that wasn’t working it may be your circuit rather than your code.

      You could also try to use my library https://github.com/gberl001/qtr-sensors-arduino which is still written by Pololu but i’ve fixed an overflow problem their library has.

    1. I believe I briefly cover how to modify the code to use QTR-8A in the video but basically you’d change the array object from QTRSensorsRC to QTRSensorsAnalog and instead of TIMEOUT you would use a variable to hold the number of times to read each sensor (like NUM_SAMPLES_PER_SENSOR) which in the Analog example is defaulted to 4 (I’ve gone as low as 2 though without much issue).

      As for a tutorial on how to use the library, there isn’t one. However, this library has some of the best documentation I’ve seen. There is this link which tells you how to use the board and then this one which explains in great detail, what each function does.

      Let me know if you’ve got and more specific questions, you can also hit up Pololu’s forums, they’re pretty good about getting back to people.

  2. Avinash Kumar

    Hi Sir,
    Again thanks for lot’s of explanation on PID.
    I would like to ask one question >> I am using TB26612FNG motor driver.
    So, what can i change in code ???

    &

    If i am using L293D motor driver then :

    #include
    #define SETPOINT 2500
    #define KP 0.2
    #define KD 1

    #define L_MOTOR_P 9
    #define R_MOTOR_P 10
    #define L_MOTOR_M 6
    #define R_MOTOR_M 11

    #define MAX_SPEED 200
    #define SET_SPEED 200
    #define MIN_SPEED 0

    #define NUM_SENSORS 5
    #define TIMEOUT 2500
    #define EMITTER_PIN 2

    // PID **************************************
    int lastError = 0; // For storing PID error

    // SENSORS **********************************
    // sensors 0 through 7 are connected to digital pins 3 through 10, respectively
    QTRSensorsRC qtrSensors((unsigned char[]) {2, 3, 4, 5, 7}, NUM_SENSORS, TIMEOUT, EMITTER_PIN);
    unsigned int sensorValues[NUM_SENSORS]; // For sensor values of readLine()

    void setup() {
    // Initialize Pins
    pinMode(L_MOTOR_P, OUTPUT);
    pinMode(R_MOTOR_P, OUTPUT);
    pinMode(L_MOTOR_M, OUTPUT);
    pinMode(R_MOTOR_M, OUTPUT);
    }

    void loop() {
    // Take a reading
    unsigned int linePos = qtrSensors.readLine(sensorValues);

    // Compute the error
    int error = SETPOINT – linePos;

    // Compute the motor adjustment
    int adjust = error*KP + KD*(error – lastError);

    // Record the current error for the next iteration
    lastError = error;

    // Adjust motors, one negatively and one positively
    analogWrite(L_MOTOR_P, constrain(SET_SPEED – adjust, MIN_SPEED, MAX_SPEED));
    analogWrite(L_MOTOR_M, 0);
    analogWrite(R_MOTOR_P, constrain(SET_SPEED + adjust, MIN_SPEED, MAX_SPEED));
    analogWrite(R_MOTOR_M, 0);

    }

    1. Hi Avinash, sorry for the delay, my notifications haven’t been working. The TB6612FNG appears to have the same circuitry and internals as the TB67H420FTG so you should be able to use the Dynamic Motor Driver library which can be found here https://github.com/mcc-robotics/Dynamic_Motor_Driver. Just use the example for the TB67H420FTG and change the pins to what you have in your circuit to test if that library will work.

      Then you could change the last four lines in the loop() code to this
      setMotorAPower(constrain(SET_SPEED – adjust, MIN_SPEED, MAX_SPEED));
      setMotorBPower(constrain(SET_SPEED + adjust, MIN_SPEED, MAX_SPEED));

      And at the top, you’ll need to create the motor driver object
      TB67H420FTG driver(INA1, INA2, PWMA, INB1, INB2, PWMB);

      Finally, don’t forget to init the driver in the setup()
      driver.init();

      You may need to swap INA1 and INA2 or INB1 and INB2 if the motor is going in the wrong direction but that should be all you need to change. Let me know how you make out.

  3. MED

    Hi !!
    I have done everything the same and download the same library u provided. However, I got this error when doing compilation . What’s wrong ?? could u please help me

    ‘QTRSensorsRC’ does not name a type; did you mean ‘QTRSensors’?

    1. I’m not sure, it would depend on your environment setup. The code in here was copy/pasted straight from my working code so it’s definitely functional with the right libraries. My guess would be that you somehow downloaded the newer Pololu QTR library, I think they removed QTRSensorsRC and now refer to all sensors with the same code.

      The library I am using is one of my own, it’s Pololu’s library but slightly modified, it hasn’t been updated since Pololu added their new line of QTR sensors but it still works for the older QTR-RC and QTR-A sensors.

      The library I am using can be found here https://github.com/gberl001/qtr-sensors-arduino (Be sure to remove any existing QTR libraries you already have as they will cause a conflict.

      Sorry for the delay, my notifications stopped working again

Leave a Reply

Your email address will not be published. Required fields are marked *