PID Control Explained with a Line Follower Robot: Code and Tuning

The three-sensor line follower from the earlier tutorial makes on-off decisions: go straight, turn gently or turn sharply. At low speed this works, but as you go faster the robot zig-zags and eventually leaves the track. Fast competition line followers instead measure how far the line is from the centre as a number and correct in proportion to that error. The standard method is the PID controller.

In this tutorial you will compute a continuous line position from five analog IR sensors, understand what each of the P, I and D terms does, implement a PID loop on an Arduino and follow a practical tuning procedure. The same controller structure is used later in this series for motor speed and for balancing.

The PID Equation

A PID controller takes an error, the difference between where you want to be (the setpoint) and where you are, and produces a correction. For a line follower the setpoint is line position zero, meaning the line is under the centre sensor.

error      e(t) = setpoint - position
correction u(t) = Kp*e(t) + Ki*sum(e)*dt + Kd*(e(t) - e(t-1))/dt

left motor  = base speed - u
right motor = base speed + u   (sign depends on how position is defined)
TermResponds toEffect on the robot
P (proportional)Present errorSteers harder the further the line is from centre; too much causes oscillation
I (integral)Accumulated past errorRemoves a steady offset, for example from unequal motors; too much causes slow, large swings
D (derivative)Rate of change of errorDamps the motion by resisting fast changes; too much makes the robot twitchy with noise

For most line followers a PD controller is enough, because the error keeps changing sign along a track and the integral rarely helps. Start with Ki set to zero and add it only if the robot consistently runs off-centre on long straights.

Measuring Line Position with Five Sensors

Use five TCRT5000 sensors read through their analog outputs on A0 to A4, or a ready-made five-channel analog sensor bar. Each reading is first scaled to 0 to 1000 using minimum and maximum values recorded during a calibration sweep, so that all sensors behave alike. Then a weighted average gives the position.

Sensor index:     0      1      2      3      4
Weight:        -2000  -1000     0    1000   2000

position = sum(weight_i * value_i) / sum(value_i)

Example calibrated values: 0, 200, 900, 300, 0
position = (-1000*200 + 0*900 + 1000*300) / (200+900+300)
         = 100000 / 1400 = +71  (line slightly right of centre)

Because neighbouring sensors both see part of the line, the position changes smoothly between sensors rather than jumping. Calibrated values here are scaled so that 1000 means black, so check the direction for your sensor and invert the scaling if needed.

PID Line Follower Code

Motor wiring is the TB6612FNG layout from the 2WD chassis tutorial. When powered, the robot runs a five-second calibration: slide it slowly left and right across the line by hand so every sensor sees both black and white.

Five-sensor analog PD/PID line follower with calibration
// Five-sensor PID line follower: Arduino Uno + TB6612FNG
const int NUM = 5;
const int SENS[NUM] = {A0, A1, A2, A3, A4};
const int PWMA = 5, AIN1 = 7, AIN2 = 8;
const int PWMB = 6, BIN1 = 9, BIN2 = 10;
const int STBY = 4;

int calMin[NUM], calMax[NUM];

// Gains: start with Kp only, then add Kd. Values depend on your robot.
float Kp = 0.08;
float Ki = 0.0;
float Kd = 0.6;
const int BASE = 150;

float integral = 0;
int lastError = 0;
int lastPosition = 0;

void motor(int pwmPin, int in1, int in2, int s) {
  s = constrain(s, -255, 255);
  digitalWrite(in1, s > 0 ? HIGH : LOW);
  digitalWrite(in2, s < 0 ? HIGH : LOW);
  analogWrite(pwmPin, abs(s));
}
void drive(int l, int r) { motor(PWMA, AIN1, AIN2, l); motor(PWMB, BIN1, BIN2, r); }

void calibrate() {
  for (int i = 0; i < NUM; i++) { calMin[i] = 1023; calMax[i] = 0; }
  unsigned long start = millis();
  while (millis() - start < 5000) {        // move robot over the line by hand
    for (int i = 0; i < NUM; i++) {
      int v = analogRead(SENS[i]);
      if (v < calMin[i]) calMin[i] = v;
      if (v > calMax[i]) calMax[i] = v;
    }
  }
}

// Returns line position -2000..2000 (0 = centred)
int readPosition() {
  long weighted = 0, total = 0;
  for (int i = 0; i < NUM; i++) {
    int range = max(calMax[i] - calMin[i], 1);
    long v = (long)(analogRead(SENS[i]) - calMin[i]) * 1000 / range;
    v = constrain(v, 0, 1000);
    // If your sensor gives LOWER readings on black, use: v = 1000 - v;
    if (v < 50) v = 0;                     // ignore noise on white
    weighted += v * (i - 2) * 1000L;
    total += v;
  }
  if (total == 0) {                        // line lost: hold extreme of last side
    return lastPosition > 0 ? 2000 : -2000;
  }
  lastPosition = weighted / total;
  return lastPosition;
}

void setup() {
  int pins[] = {PWMA, AIN1, AIN2, PWMB, BIN1, BIN2, STBY};
  for (int p : pins) pinMode(p, OUTPUT);
  digitalWrite(STBY, HIGH);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);         // LED on during calibration
  calibrate();
  digitalWrite(LED_BUILTIN, LOW);
  delay(2000);                             // place robot on the line
}

void loop() {
  int position = readPosition();
  int error = 0 - position;                // setpoint is 0

  integral += error;
  integral = constrain(integral, -20000, 20000);  // anti-windup clamp
  int derivative = error - lastError;
  lastError = error;

  float u = Kp * error + Ki * integral + Kd * derivative;

  // Positive position = line to the right = positive error is negative,
  // so u < 0 slows the right wheel and speeds the left: robot turns right.
  drive(BASE - (int)u, BASE + (int)u);
  delay(2);                                // fixed, short loop period
}

Tuning Procedure

  1. Set Ki and Kd to zero and BASE to a slow speed such as 100.
  2. Increase Kp until the robot follows the line but oscillates steadily from side to side. Then reduce Kp by about a third.
  3. Increase Kd in small steps until the oscillation dies out quickly after a curve. If the motors start to buzz or twitch, Kd is too high.
  4. Raise BASE gradually. Each speed increase usually needs a little more Kd.
  5. Only if the robot holds a constant offset on straights, add a very small Ki.

The gain values in the code are starting guesses for this particular scaling, not universal numbers. Because the loop runs with a fixed short delay, the derivative is computed per loop instead of per second; if you change the loop timing, retune Kd. Record each set of gains and the result in a notebook so you can return to a good setting.

Common Problems

  • Robot steers away from the line: the sign of the correction is reversed. Swap BASE - u and BASE + u.
  • Position stuck near zero: the sensors were not swept over both colours during calibration.
  • Smooth on straights but leaves sharp turns: BASE too high for the track, or the sensor bar is too narrow to see the line on the turn.
  • Wild oscillation that grows: Kp is far too high, or Ki is accumulating; check the anti-windup clamp.

Summary

PID replaces on-off decisions with a correction proportional to the error, its history and its rate of change. A calibrated weighted average turns five analog sensors into a smooth position, and a PD controller with careful step-by-step tuning lets the robot follow lines much faster without zig-zagging. The same idea controls motor speed with encoders in the next tutorial.