Built-in project · Arduino Uno R3

Line-following cart on an Arduino Uno

A five-channel line tracker, a TB6612FNG and two gearbox motors on a 2S 18650 pack: the cart steers to keep the tape under its middle sensor and stops at the finish bar. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 40 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $39. On Mokxi they're free.

Step by step, with the wiring and the common mistakes: Arduino Line Follower Robot: Code and Circuit

The circuit runs here once the page has loaded.

What you'll learn

  • Finding a line's position from several sensors
  • Steering with proportional and derivative gains
  • Driving two motors through a TB6612FNG
  • Limiting speed to protect motors on a higher voltage
  • Handling lost-line and finish cases
  • Difficulty: Intermediate
  • Time: About 40 minutes
  • Board: Arduino Uno R3
  • Parts: 6

How it works

A five-channel line tracker on A4 to A0 reports which sensors are over the black line. Each sensor is 15 mm from the next, so the sketch averages the positions of the ones that see the line. That gives an error in millimeters: 0 means the line is under the middle sensor.

The error steers the cart. The sketch slows the wheel on the side the line is on and speeds up the other. It uses KP for the error and KD for how fast the error changes, which stops the cart from weaving. The TB6612FNG driver runs the two motors from the battery pack.

If no sensor sees the line, the cart turns toward where it last saw it. If all five see black at once, that is the finish bar and the cart stops.

Parts list

6 parts, plus the jumper wires. They retail for about $39; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Motor driver, TB6612FNG pin GND1Arduino Uno R3 pin GND
Motor driver, TB6612FNG pin STBYArduino Uno R3 pin 9
Motor driver, TB6612FNG pin BIN2Arduino Uno R3 pin 8
Motor driver, TB6612FNG pin BIN1Arduino Uno R3 pin 7
Motor driver, TB6612FNG pin PWMBArduino Uno R3 pin 6
Motor driver, TB6612FNG pin PWMAArduino Uno R3 pin 5
Motor driver, TB6612FNG pin AIN2Arduino Uno R3 pin 4
Motor driver, TB6612FNG pin AIN1Arduino Uno R3 pin 2
Line tracker, 5 channel pin VCCArduino Uno R3 pin IOREF
Motor driver, TB6612FNG pin VCCArduino Uno R3 pin 5V
Line tracker, 5 channel pin GNDArduino Uno R3 pin GND
Line tracker, 5 channel pin S5Arduino Uno R3 pin A0
Line tracker, 5 channel pin S4Arduino Uno R3 pin A1
Line tracker, 5 channel pin S3Arduino Uno R3 pin A2
Line tracker, 5 channel pin S2Arduino Uno R3 pin A3
Line tracker, 5 channel pin S1Arduino Uno R3 pin A4
Battery: AA, coin cell, LiPo pin V+Motor driver, TB6612FNG pin VM
DC motor (1) pin 1Motor driver, TB6612FNG pin AO1
DC motor (1) pin 2Motor driver, TB6612FNG pin AO2
DC motor (2) pin 2Motor driver, TB6612FNG pin BO2
DC motor (2) pin 1Motor driver, TB6612FNG pin BO1
Battery: AA, coin cell, LiPo pin V-Motor driver, TB6612FNG pin GND3

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Line-following cart: a five-channel line tracker across the front, two
// gearbox motors on a TB6612FNG, and two 18650 cells for the motors.
//
//   A4..A0  the tracker's S1..S5, left to right (high over the black line):
//           the board sits turned around under the Uno, so S1 lands on A4
//   pin 5   PWMA, the left motor's speed      pin 6  PWMB, the right motor's
//   pin 2   AIN1    pin 4  AIN2               pin 7  BIN1    pin 8  BIN2
//   pin 9   STBY
//   VM      the 2S pack's +, 8.4 V full; VCC the Uno's 5 V; all grounds joined
//
// # Where the line is
//
// The sensors are 15 mm apart. Each one over the tape counts its place, -2 at
// S1 to +2 at S5, and the line's position is the average of the ones that see
// it, in millimeters: 0 under S3, +7 between S3 and S4, +30 under S5.
//
// # Steering
//
// The error is that position. The cart slows the wheel on the side the line
// is on and speeds up the other, by KP x error plus KD x how fast the error
// is changing (the damping that stops it weaving). With the line to the right
// the left wheel runs faster, and the cart turns right, back over it.
//
// Lose the line and it keeps turning the way it last saw it. All five black
// at once is the finish bar across the track: it stops.
//
// # Speed and the battery
//
// Two full 18650s are 8.4 V and these are 6 V gearbox motors, so the sketch
// never asks for more than 180 of 255: about 5.9 V on average.

#include <SparkFun_TB6612.h>

// S1 (left) to S5 (right).
const int SENSORS[5] = {A4, A3, A2, A1, A0};

const int AIN1 = 2;
const int AIN2 = 4;
const int PWMA = 5;
const int BIN1 = 7;
const int BIN2 = 8;
const int PWMB = 6;
const int STBY = 9;

const int BASE = 120;
const int MAX_SPEED = 180;
const int SEARCH = 110;
// Steering gains: counts of PWM per mm of error, and per mm of change.
const int KP = 3;
const int KD = 6;

Motor leftMotor = Motor(AIN1, AIN2, PWMA, 1, STBY);
Motor rightMotor = Motor(BIN1, BIN2, PWMB, 1, STBY);

int lastError = 0;
bool finished = false;
unsigned long lastReport = 0;

int clampSpeed(int v) {
  if (v > MAX_SPEED) return MAX_SPEED;
  if (v < -MAX_SPEED) return -MAX_SPEED;
  return v;
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Arduino Uno: line-following cart");
  for (int i = 0; i < 5; i++) pinMode(SENSORS[i], INPUT);
}

void report(const char *what, int error, int left, int right) {
  if (millis() - lastReport < 200) return;
  lastReport = millis();
  Serial.print(what);
  Serial.print(" ");
  Serial.print(error);
  Serial.print(" mm   left ");
  Serial.print(left);
  Serial.print("  right ");
  Serial.println(right);
}

void loop() {
  if (finished) return;

  int seen = 0;
  int sum = 0;
  for (int i = 0; i < 5; i++) {
    if (digitalRead(SENSORS[i]) == HIGH) {
      seen++;
      sum += (i - 2) * 15;
    }
  }

  if (seen == 5) {
    brake(leftMotor, rightMotor);
    finished = true;
    Serial.println("finish bar: stopped");
    return;
  }

  int left;
  int right;
  if (seen == 0) {
    // Lost it: turn toward where it was last.
    left = lastError >= 0 ? SEARCH : -SEARCH;
    right = -left;
    leftMotor.drive(left);
    rightMotor.drive(right);
    report("searching, last", lastError, left, right);
    delay(10);
    return;
  }

  int error = sum / seen;
  int turn = KP * error + KD * (error - lastError);
  lastError = error;
  left = clampSpeed(BASE + turn);
  right = clampSpeed(BASE - turn);
  leftMotor.drive(left);
  rightMotor.drive(right);
  report("line", error, left, right);
  delay(10);
}

Code walkthrough

The key functions of the sketch, and what each one does.

clampSpeed sketch.ino

Keeps a motor command between -MAX_SPEED and MAX_SPEED, which is 180 out of 255. The battery pack is 8.4 V when full and these are 6 V motors, so the cap keeps the average voltage near 5.9 V.

int clampSpeed(int v) {
  if (v > MAX_SPEED) return MAX_SPEED;
  if (v < -MAX_SPEED) return -MAX_SPEED;
  return v;
}

setup sketch.ino

Starts the Serial Monitor at 115200 and sets the five sensor pins as inputs. The motor pins are set up by the Motor objects.

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Arduino Uno: line-following cart");
  for (int i = 0; i < 5; i++) pinMode(SENSORS[i], INPUT);
}

report sketch.ino

Prints the error and both motor speeds, but no more than once every 200 ms. This keeps the monitor readable while the loop runs about every 10 ms.

void report(const char *what, int error, int left, int right) {
  if (millis() - lastReport < 200) return;
  lastReport = millis();
  Serial.print(what);
  Serial.print(" ");
  Serial.print(error);
  Serial.print(" mm   left ");
  Serial.print(left);
  Serial.print("  right ");
  Serial.println(right);
}

loop sketch.ino

Counts sensors that read HIGH and sums their positions. Five means finish: brake and stop. Zero means search. Otherwise it computes the error, adds KP times the error and KD times its change to get a turn value, and drives the left and right motors at BASE plus or minus that.

void loop() {
  if (finished) return;

  int seen = 0;
  int sum = 0;
  for (int i = 0; i < 5; i++) {
    if (digitalRead(SENSORS[i]) == HIGH) {
      seen++;
      sum += (i - 2) * 15;
    }
  }

  if (seen == 5) {
    brake(leftMotor, rightMotor);
    finished = true;
    Serial.println("finish bar: stopped");
    return;
  }

  int left;
  int right;
  if (seen == 0) {
    // Lost it: turn toward where it was last.
    left = lastError >= 0 ? SEARCH : -SEARCH;
    right = -left;
    leftMotor.drive(left);
    rightMotor.drive(right);
    report("searching, last", lastError, left, right);
  // ... 14 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The cart turns away from the line This project
    The sensors may be reversed, or a motor's wires swapped. Check that S1 goes to A4 and S5 goes to A0.
  • The cart weaves back and forth This project
    Lower KP or raise KD.
  • Motors do nothing This project
    Check STBY on pin 9 and that the battery pack's ground is joined to the Uno's ground.
  • The robot cannot tell the line from the floor. Line tracker
    Keep the sensors about 2 mm to 10 mm above the surface. Test each channel on black and white, and adjust the trimmer. Shade the sensors from room light.
  • The robot follows the wrong way. Line tracker
    Each output is HIGH over the black line. Check which channel is the left one and map it to the correct motor. Flip the steering in code if the car turns away from the line.
  • The motors do not move. Motor driver
    Set STBY HIGH. Power the motors at VM (about 4.5 to 13.5 V) and logic at VCC. Join all grounds. Then set the direction pins and a PWM level on the PWM pin.
  • The motor stops hard instead of coasting. Motor driver
    Both input pins HIGH, or PWM at 0, brakes the motor. To let it coast, set both inputs LOW with PWM high. Per channel the chip handles about 1.2 A continuous.
  • The motor does not spin, or the pin gets hot or the board resets when it is wired straight to a GPIO pin. DC motor
    A pin gives about 20 mA and a motor needs far more. Drive it through a transistor or motor driver, with its own supply and a shared ground.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.