2WD robot car chassis kit with ESP32-S3, TB6612FNG motor driver, and 5-channel IR line sensor array following a black line

Intermediate

Build a Line-Following Robot

RoboticsSensorsMotion

The next step up from a basic robot car -- read the floor in real time and steer along a line automatically.

7 real parts included

Total for this kit: 1,684.00 ฿

Doesn't include any order discount you may get at checkout.

What you'll need

Project Goal

This project builds on the same two-wheel robot base used in a basic robot car, adding a downward-facing sensor array so the robot can read the floor beneath it and steer itself along a line automatically -- a real step up from manual or pre-programmed driving into closed-loop control.

What the Controller Does

The ESP32-S3 runs the control loop: read the line sensor array, decide how far off-center the robot currently is, calculate a steering correction, and send updated speed commands to each wheel through the motor driver. This loop repeats continuously and fast enough that the robot can react to the line in real time as it moves.

What the Chassis and Motors Do

The 2WD chassis kit provides the physical drivetrain -- two independently driven wheels, each turned by its own DC gear motor. Because the wheels are independent, the robot steers not by turning a wheel assembly but by running the two motors at different speeds -- exactly like a tank. Power for the motors comes from the 18650 battery, kept separate from the controller's own logic supply where the motor driver requires it.

What the Motor Driver Does

The TB6612FNG sits between the controller and the motors. The controller's own pins can't safely supply the current a DC motor draws, so the driver takes low-power logic signals from the controller and uses them to switch the higher-current motor supply on and off (and in which direction) -- effectively acting as a current amplifier and direction switch for both motors independently.

What the Line Sensor Array Does

The 5-channel IR line tracking array is mounted facing the floor. Each of its five sensors independently reports whether it currently sees a dark line or a lighter floor surface beneath it. Read together, the pattern across all five sensors -- which ones see the line and which don't -- tells the controller exactly how far off-center the robot currently is relative to the line, not just whether it's on or off it.

Proportional Steering

The core control concept in this project is proportional steering rather than simple bang-bang (full-left / full-right) steering. Instead of reacting the same way to any deviation, the controller calculates how far off-center the sensor pattern shows the robot to be and scales its correction to match: a small deviation gets a small, gentle correction, and a larger deviation gets a stronger one, easing off again as the robot re-centers on the line. This produces smoother, faster line-following than a simple all-or-nothing approach.

Software and Library Concepts

Structure your code around three clear stages each loop: read all five sensor values into an array, compute a single "error" number representing how far off-center that pattern is, then convert that error into individual left/right motor speed commands sent through the motor driver library. Keeping these three stages distinct in code makes it much easier to debug each one independently -- for instance, printing the raw sensor pattern and the computed error value before you even touch the motors.

Testing and Troubleshooting

  • Robot doesn't detect the line at all -- check the sensor array's mounting height and distance from the floor, and confirm the line has enough contrast against the floor color for the IR sensors to distinguish them.
  • Robot oscillates wildly side to side instead of smoothly tracking -- the steering correction is too aggressive for the sensor spacing and current speed; this needs tuning, typically by reducing how strongly the error value is scaled into a correction.
  • Robot consistently drifts to one side even on a straight section of line -- check for a miswired or underperforming motor on that side before assuming a sensor problem; a simple test is running both motors at the same fixed speed with no sensor input and watching whether the robot still drifts.
  • Robot loses the line entirely at a sharp turn -- this is often a sign the proportional correction is capped too low to react in time; confirm your code allows a strong enough maximum correction for large error values.

Optional Future Upgrade: Forward Obstacle Detection

Once line-following is reliable, see the Distance Warning System project for adding forward obstacle detection using the same HC-SR04-class ultrasonic distance sensor concept -- a natural addition that lets the robot stop or reroute rather than driving into something blocking the line.

Wiring

  • 18650 battery holder (2-slot, series wiring, ~7.4V nominal) → TB6612FNG VM, through the holder's onboard switch
  • Battery pack GND → TB6612FNG GND, and tied to a common ground with the ESP32-S3 and IR array
  • ESP32-S3 3.3V → TB6612FNG VCC — logic supply
  • ESP32-S3 GPIO9 → TB6612FNG STBY (`MOTOR_STBY_PIN`) — must be driven HIGH to enable the driver outputs
  • ESP32-S3 GPIO10 → TB6612FNG AIN1 (`AIN1_PIN`)
  • ESP32-S3 GPIO11 → TB6612FNG AIN2 (`AIN2_PIN`)
  • ESP32-S3 GPIO12 → TB6612FNG PWMA (`PWMA_PIN`)
  • ESP32-S3 GPIO13 → TB6612FNG BIN1 (`BIN1_PIN`)
  • ESP32-S3 GPIO14 → TB6612FNG BIN2 (`BIN2_PIN`)
  • ESP32-S3 GPIO21 → TB6612FNG PWMB (`PWMB_PIN`)
  • TB6612FNG A01/A02 → left DC motor terminals
  • TB6612FNG B01/B02 → right DC motor terminals
  • IR sensor array VCC → 5V, GND → GND (check your module — most 5-channel IR arrays want 5V for consistent emitter brightness, some run on 3.3V)
  • IR array 5 digital outputs → ESP32-S3 GPIO1, GPIO2, GPIO4, GPIO5, GPIO6 (`IR_PINS[0..4]`)

This parts list has no separate buck converter for the ESP32-S3's own logic power — many S3 dev boards have an onboard regulator on their 5V/VIN pin rated for higher input, so you may be able to feed the raw battery voltage there if your specific board's regulator supports it (check its datasheet first); otherwise power the S3 from USB-C via a small power bank while testing untethered. GPIO0 and GPIO3 are strapping pins on the ESP32-S3 and are intentionally avoided above; if your specific dev board reserves other GPIOs for onboard flash/PSRAM, remap the `#define`s to whatever's actually broken out. Each IR sensor module usually has its own onboard potentiometer to tune light/dark threshold — do that over your actual track surface before trusting the digital HIGH/LOW output, and always confirm STBY is HIGH before expecting any motor response.

Wiring Diagram

VM GND 3.3V → VCC GPIO9 → STBY GPIO10 → AIN1 GPIO11 → AIN2 GPIO12 → PWMA GPIO13 → BIN1 GPIO14 → BIN2 GPIO21 → PWMB A01/A02 B01/B02 VCC GND GPIO1 ESP32-S3 TB6612FNG IR array 5 digital outputs 18650 battery holder Battery pack left DC motor terminals right DC motor terminals 5V IR sensor array GND

Starter Code

// Line-following robot: 5-channel IR array + TB6612FNG dual motor driver

#define MOTOR_STBY_PIN 9
#define AIN1_PIN 10
#define AIN2_PIN 11
#define PWMA_PIN 12
#define BIN1_PIN 13
#define BIN2_PIN 14
#define PWMB_PIN 21

const int IR_PINS[5] = {1, 2, 4, 5, 6}; // far-left, left, center, right, far-right

const int BASE_SPEED = 150;   // 0-255
const int MAX_SPEED = 255;
const int TURN_GAIN = 60;     // how hard to correct per unit of error

void setMotor(int in1Pin, int in2Pin, int pwmPin, int speed) {
  speed = constrain(speed, -MAX_SPEED, MAX_SPEED);
  if (speed >= 0) {
    digitalWrite(in1Pin, HIGH);
    digitalWrite(in2Pin, LOW);
  } else {
    digitalWrite(in1Pin, LOW);
    digitalWrite(in2Pin, HIGH);
    speed = -speed;
  }
  analogWrite(pwmPin, speed); // Arduino-ESP32 core 3.x supports analogWrite() directly on PWM-capable pins
}

void setup() {
  Serial.begin(115200);

  pinMode(MOTOR_STBY_PIN, OUTPUT);
  pinMode(AIN1_PIN, OUTPUT);
  pinMode(AIN2_PIN, OUTPUT);
  pinMode(PWMA_PIN, OUTPUT);
  pinMode(BIN1_PIN, OUTPUT);
  pinMode(BIN2_PIN, OUTPUT);
  pinMode(PWMB_PIN, OUTPUT);

  for (int i = 0; i < 5; i++) {
    pinMode(IR_PINS[i], INPUT);
  }

  digitalWrite(MOTOR_STBY_PIN, HIGH); // enable driver outputs

  Serial.println("Line follower ready.");
}

void loop() {
  int sensorVal[5];
  for (int i = 0; i < 5; i++) {
    sensorVal[i] = digitalRead(IR_PINS[i]); // assume HIGH = line detected; flip if your array is inverted
  }

  // Weighted position error: -2 (far left) ... 0 (centered) ... +2 (far right)
  int weights[5] = {-2, -1, 0, 1, 2};
  int weightedSum = 0;
  int activeCount = 0;

  for (int i = 0; i < 5; i++) {
    if (sensorVal[i] == HIGH) {
      weightedSum += weights[i];
      activeCount++;
    }
  }

  if (activeCount == 0) {
    // Line lost — stop rather than guess
    setMotor(AIN1_PIN, AIN2_PIN, PWMA_PIN, 0);
    setMotor(BIN1_PIN, BIN2_PIN, PWMB_PIN, 0);
    return;
  }

  int error = weightedSum; // simple proportional error, no averaging needed for 5 discrete sensors
  int correction = error * TURN_GAIN;

  int leftSpeed = BASE_SPEED + correction;
  int rightSpeed = BASE_SPEED - correction;

  setMotor(AIN1_PIN, AIN2_PIN, PWMA_PIN, leftSpeed);
  setMotor(BIN1_PIN, BIN2_PIN, PWMB_PIN, rightSpeed);
}

Not sure about a step? Ask us