Building a Line Following Robot with TCS34725 RGB Sensor

This post contains affiliate links, marked with *. As an Amazon Associate I earn from qualifying purchases, at no extra cost to you. It helps keep the tutorials free.

The video loads from YouTube (Google), which may set cookies. Privacy policy

Quick Summary

This line-following robot uses an Arduino Uno, a TCS34725 RGB color sensor and the Arduino Motor Shield Rev3. At every start the robot measures the white tape and the dark surface itself. A PID controller then steers it along the edge of the tape and corrects the steering in proportion to how far the sensor is off the edge. The post includes the wiring diagram, the full Arduino code and the PID tuning steps.

This project builds on the KY-033 line-following robot. My KY-033 robot followed the line, but it never drove smoothly and swung from side to side the whole time. This version replaces the KY-033 infrared sensors with one TCS34725 sensor and the on/off steering with a PID controller. The new robot follows the line more smoothly, also through tighter curves.

How the robot works

The Arduino Uno reads the sensor, runs the PID calculation and sends a speed command to each motor. The Arduino Motor Shield Rev3, an expansion board that plugs onto the Uno, drives the two DC (direct current) motors. The TCS34725 sensor sits on the underside of the chassis and faces the track. It measures light in four channels (red, green, blue and clear). The sketch (the Arduino program) uses only the clear channel, a brightness value that separates the white tape from the dark surface. The robot runs a closed control loop: the sensor reading goes to the PID controller, the controller sets the motor speeds, and the robot’s new position changes the next reading.

Components needed (bill of materials)

If you already built the KY-033 robot, you can reuse the chassis, the printed spacers, the motors, wheels, Arduino and motor shield. I mounted the TCS34725 on that same robot, so the module is the only new part.

ComponentQtyRequired / OptionalNotes
Chassis1Required3D printed (Step 1) in two parts, base plate and cover; PLA or ABS plastic
Sensor spacer1 per mounting holeRequired3D printed (Step 1), 7 mm wide, 13.5 mm high
Arduino spacer4Required3D printed (Step 1), 14 mm high, one per mounting hole of the Uno
DC motor with gearbox2RequiredYellow TT gearmotors, the common low-cost type for small robots
Wheels2RequiredMust fit the TT motor shafts
Caster wheel1RequiredBall caster or swivel caster at the front
Arduino Uno1RequiredRuns the sketch
Arduino Motor Shield Rev31RequiredPlugs onto the Uno, drives both motors
TCS34725 RGB color sensor1RequiredReads the brightness of the track under the robot
Jumper wiresSeveralRequiredMale-to-female and male-to-male
M3 screws and threaded insertsAs neededRequiredFix the motors and spacers to the chassis. Threaded inserts are metal sleeves with an inner thread that sit in the printed holes.
Battery1Required7.4 V 2-cell LiPo (lithium polymer); powers motors and Arduino through the shield
Power switch1OptionalSwitches the robot off between tests

Step 1: Designing the robot in Autodesk Inventor

I designed the parts in the 3D design program Autodesk Inventor: the chassis (a base plate and a cover), the sensor spacers and the Arduino spacers. The chassis carries the motors, the sensor, the Arduino with its shield and the battery. Each sensor spacer is 7 mm wide and 13.5 mm high. The sensor spacers hold the sensor at the front underside, 10 to 15 mm above the ground; calibration and troubleshooting assume this height. The four Arduino spacers are 14 mm high and carry the Uno.

Step 2: 3D printing the components

I printed the parts on a Creality Ender 3. Any FDM printer (fused deposition modeling) works; it builds the parts layer by layer from melted plastic filament. After printing, remove the supports, sand rough edges and check that the screw holes and slots fit before assembly.

Step 3: Assembling the robot

3D rendering of the robot: blue body with an orange top and bottom, the power switch and the OmArTronics lettering on top, two wheels with orange rims.

Mounting the motors and wheels

  1. Fix both DC motors to the chassis with the M3 screws and threaded inserts, motor shafts facing outward. I heated the inserts beforehand and pressed them into the printed holes.
  2. Press the wheels onto the motor shafts and check that both spin freely without touching the chassis.
  3. Mount the caster wheel at the front. With the two drive wheels it gives three-point support.

Installing the electronics

  1. Fix the four Arduino spacers to the chassis and mount the Arduino Uno on them.
  2. Plug the Arduino Motor Shield Rev3 onto the Uno and check that all header pins (the pins on the underside of the shield) line up with the sockets of the Uno.
  3. Mount the TCS34725 module on the sensor spacers at the front underside of the chassis, with the sensor facing the track.
  4. Fix the battery on the chassis; I used cable ties. Connect it only after the wiring in Step 4 is complete. Secure all parts so nothing comes loose while the robot drives.

Step 4: Wiring the components

The sensor connects over I2C, a two-wire bus: SDA carries the data and SCL the clock signal. The motors run through the Motor Shield. The wiring diagram shows the complete circuit with a 9 V battery; my robot runs on a 7.4 V LiPo instead.

Wiring diagram: Motor Shield Rev3 on the Arduino Uno, two DC motors on the motor terminals, a 9 V battery and a power switch on the power input, and the TCS34725 sensor on 5 V, GND, A4 and A5.

Motor connections

The DC motors connect to the motor terminals of the shield. The shield sets each motor’s direction with a direction pin and its speed with a PWM (pulse-width modulation) pin. PWM switches the pin on and off rapidly; the share of on-time sets the speed.

MotorShield terminalDirection pinPWM (speed) pin
Left motor (Motor A)Motor ADigital pin 12 (DIR_A)Digital pin 3 (PWM_A)
Right motor (Motor B)Motor BDigital pin 13 (DIR_B)Digital pin 11 (PWM_B)

Sensor connections (I2C)

TCS34725 pinArduino pinNotes
VIN5 VPower supply for the sensor
GNDGNDCommon ground
SDAA4 (SDA)I2C data line
SCLA5 (SCL)I2C clock line

Power connections

ComponentConnectionNotes
Battery (7.4 V LiPo)External power input of the Motor ShieldPowers both motors and the Arduino through the shield
Power switch (optional)In series with the positive battery leadSwitches the robot off without unplugging the battery

Step 5: Understanding PID control for this robot

PID stands for Proportional-Integral-Derivative. The KY-033 robot kept the tape between its two sensors. It used bang-bang control (straight ahead or a fixed turn almost on the spot, nothing in between), so it zig-zagged along the line. This robot uses the clear-channel value and steers in proportion to the distance between the reading and the threshold (middleValue).

PID control loop of the robot: setpoint (middleValue), error, PID correction, motors, and the sensor reading fed back.

What error, setpoint, and correction mean in this robot

  • Setpoint: the target value of the controller. Here it is middleValue, the average of the two calibration readings, which marks the boundary between the white tape and the dark surface.
  • Process variable: the measured value, here the current clear-channel reading c.
  • Error: middleValue - c. Over the dark surface c is low and the error is positive: the sensor has drifted left off the tape. Over the white tape c is high and the error is negative. The correction steers the robot back toward the boundary, so it follows the left edge of the tape.
  • Correction: the PID output. It is added to the left motor speed and subtracted from the right motor speed. This is differential steering: the robot turns because one wheel runs faster than the other.

The three PID components

Proportional (P): reacts to the current error. The term multiplies the current error by the gain Kp. If the sensor drifts off the tape edge, the P term produces a correction in proportion to the distance. A larger Kp reacts faster but overshoots and oscillates if set too high. Equation: P = Kp * error.

Integral (I): sums the error over time. The term adds up the error of every loop and multiplies the sum by Ki. The integral removes a constant offset, for example from one slower motor or an asymmetric track. The code limits this sum to ±4 so it cannot grow so large that it keeps steering after the error is gone (integral windup). Equation: I = Ki * sumError.

Derivative (D): reacts to how fast the error changes. The term multiplies the change of the error since the last loop by Kd. It damps the movement: if the robot swings toward the tape quickly, the D term reduces the correction and limits the overshoot. Step 8 shows when to raise Kd. Equation: D = Kd * (error - previousError).

The combined PID equation

The sketch adds the three terms: correction = Kp * error + Ki * sumError + Kd * (error - previousError). The correction then changes the motor speeds: speedLeft = baseSpeed + correction and speedRight = baseSpeed - correction.

When the error is zero (the sensor is right on the threshold), both motors run at baseSpeed and the robot drives straight. When the error is positive or negative, one motor runs faster and the other slower, and the robot steers back toward the tape edge.

With my readings from Step 7 (about 2 over the dark surface and 8 over the white tape), middleValue is 5 and the error runs from -3 to +3. The P term then reaches up to Kp × 3 = 24 × 3 = 72. The I term stays within ±4 (Ki = 1.0 times the limit of sumError), and the D term within ±6. The P term does most of the steering.

Step 6: Programming the robot (Arduino code)

Upload the sketch below to the Uno with the Arduino IDE, the Arduino programming software. Before you upload it, install the Adafruit TCS34725 library (ready-made code that talks to the sensor): open the Library Manager in the Arduino IDE and search for “Adafruit TCS34725”. The same code is in the GitHub repository.

Complete Arduino sketch

/**
 * Author: Omar Draidrya
 * Date: 2024/12/06
 * Line-Following Robot with TCS34725.
 */
#include <Wire.h>
#include <Adafruit_TCS34725.h>
// Initialize TCS34725 sensor
Adafruit_TCS34725 tcs = Adafruit_TCS34725(TCS34725_INTEGRATIONTIME_2_4MS, TCS34725_GAIN_4X);
// Motor control pins
#define DIR_A 12
#define DIR_B 13
#define PWM_A 3
#define PWM_B 11
// PID parameters
float Kp = 24.0;  // Proportional gain
float Ki = 1.0;   // Integral gain
float Kd = 1.0;   // Derivative gain
int baseSpeed = 60;  // Base speed
// Middle value for black/white
int middleValue = 3; // Adjusted based on measured values
// Global variables for PID
float error = 0, previousError = 0, sumError = 0;
void setup() {
  Serial.begin(9600);
  // Initialize TCS34725 sensor
  if (!tcs.begin()) {
    Serial.println("Error: TCS34725 not detected. Check wiring!");
    while (1);
  }
  Serial.println("TCS34725 sensor detected!");
  // Configure motor control pins
  pinMode(DIR_A, OUTPUT);
  pinMode(DIR_B, OUTPUT);
  pinMode(PWM_A, OUTPUT);
  pinMode(PWM_B, OUTPUT);
  // Perform sensor calibration
  calibrateSensor();
}
void loop() {
  uint16_t r, g, b, c;
  tcs.getRawData(&r, &g, &b, &c);
  // Calculate error
  error = middleValue - (float)c ;
  // PID calculation
  sumError += error;
  sumError = constrain(sumError, -4, 4); // Limit integral component
  if (abs(error) < 2.0) {
    sumError = 0; // Reset for small errors
  }
  float differential = error - previousError;
  float correction = Kp * error + Ki * sumError + Kd * differential;
  previousError = error;
  // Adjust motor speeds
  int speedLeft = baseSpeed + (int)correction;
  int speedRight = baseSpeed - (int)correction;
  speedLeft = constrain(speedLeft, 20, 255);
  speedRight = constrain(speedRight, 20, 255);
  controlMotors(speedLeft, speedRight);
}
void calibrateSensor() {
  uint16_t r, g, b, c;
  // Measure black value
  Serial.println("Calibrating: Measuring black value...");
  controlMotors(-85, 85); // Slightly turn left
  delay(350);
   // Stop motors after calibration
  controlMotors(0, 0);
  delay(50);
  tcs.getRawData(&r, &g, &b, &c);
  int blackValue = c;
  Serial.print("Black Value: ");
  Serial.println(blackValue);
  // Measure white value
  Serial.println("Calibrating: Measuring white value...");
  controlMotors(85, -85); // Slightly turn right
  delay(500);
    // Stop motors after calibration
  controlMotors(0, 0);
  delay(50);
  tcs.getRawData(&r, &g, &b, &c);
  int whiteValue = c;
  Serial.print("White Value: ");
  Serial.println(whiteValue);
  // Calculate middle value
  middleValue = (blackValue + whiteValue) / 2;
  Serial.print("Middle Value: ");
  Serial.println(middleValue);
  // Stop motors after calibration
  controlMotors(0, 0);
}
void controlMotors(int speedLeft, int speedRight) {
  if (speedLeft >= 0) {
    digitalWrite(DIR_A, HIGH);
  } else {
    digitalWrite(DIR_A, LOW);
    speedLeft = abs(speedLeft);
  }
  if (speedRight >= 0) {
    digitalWrite(DIR_B, HIGH);
  } else {
    digitalWrite(DIR_B, LOW);
    speedRight = abs(speedRight);
  }
  analogWrite(PWM_A, speedLeft);
  analogWrite(PWM_B, speedRight);
}

Sensor initialization

Adafruit_TCS34725 tcs = Adafruit_TCS34725(TCS34725_INTEGRATIONTIME_2_4MS, TCS34725_GAIN_4X);

The integration time is how long the sensor collects light for one reading. At 2.4 ms the sensor delivers a new value every few milliseconds, so the robot reacts quickly to the line. The gain of 4× amplifies the signal without driving the sensor to its maximum value (saturation) on the white tape. If your readings are too low or too high, try another gain setting (1×, 4×, 16× or 60×).

Motor pin definitions

#define DIR_A 12
#define DIR_B 13
#define PWM_A 3
#define PWM_B 11

These pins match the Arduino Motor Shield Rev3. DIR_A and DIR_B set the direction of each motor (HIGH forward, LOW reverse). PWM_A and PWM_B set the speed, from 0 (stopped) to 255 (full speed). The PID correction changes these two PWM values separately, which steers the robot.

Calibration routine

void calibrateSensor() {
  // Measures the light intensity for black and white areas.
  // Calculates the middle value (threshold) as the average of the two.
}

calibrateSensor() runs once at startup. It measures the dark surface and the white tape and sets middleValue to the average of both readings. Step 7 covers the starting position and each calibration step.

Main loop and error calculation

void loop() {
  uint16_t r, g, b, c;
  tcs.getRawData(&r, &g, &b, &c);
  // Reads the light intensity from the sensor.
  // Calculates error and adjusts motor speeds using PID control.
}

Each loop reads all four channels but uses only the clear value c. The error is middleValue - c; Step 5 explains its sign. This signed error feeds the PID calculation.

PID correction calculation

float correction = Kp * error + Ki * sumError + Kd * differential;

This line is the core of the controller. It combines the three terms into one correction value. The variable differential holds the change of the error since the last loop (error - previousError). sumError is limited to ±4 (Step 5) and reset to zero when the error lies between -2.0 and +2.0, so no correction builds up while the robot is already on the tape edge.

Speed limiting

After the correction, both motor speeds are limited to the range 20 to 255 with constrain(). Below 20 the motors may not turn at all because of friction. 255 is the PWM maximum.

Motor control function

void controlMotors(int speedLeft, int speedRight) {
  // Adjusts motor direction and speed based on the calculated correction.
}

controlMotors() takes a signed speed for each motor. Positive values drive forward; negative values drive in reverse, which the calibration turns use. The function sets the direction pin (HIGH or LOW) and writes the absolute speed to the PWM pin. The same function handles driving, steering and the calibration rotation.

Step 7: Calibration on the track

Absolute sensor readings change with the lighting, the sensor height, the surface color and the tape material. Instead of a fixed threshold, the robot measures the actual track at every start and calculates the threshold from that. The start value middleValue = 3 in the sketch is only a placeholder. calibrateSensor() replaces it with the measured average. When I first tested the sensor on its own, the clear channel read about 2 over the dark surface and about 8 over the white tape. The threshold then comes out at (2 + 8) / 2 = 5. The short integration time of 2.4 ms keeps the readings this low.

Starting position

Place the robot before you switch it on. Put the sensor at the left edge of the white tape, with the dark surface to the left and the white tape to the right. The robot then reads the dark surface when it turns left and the white tape when it turns right. In the wrong position the calibration values are wrong and the robot does not follow the line.

What the calibration routine does

The variable names come from the sketch: blackValue is the reading of the dark surface, whiteValue the reading of the white tape.

  • Measure the dark surface (blackValue): the robot turns left for 350 ms, stops and stores the clear-channel reading.
  • Measure the white tape (whiteValue): the robot turns right for 500 ms, a little longer so the sensor crosses onto the tape, stops and stores the reading.
  • Calculate the threshold: middleValue = (blackValue + whiteValue) / 2.
  • Print all three values to the Serial Monitor, the Arduino IDE window that shows the text the board sends over USB.

If calibration gives poor values

  • Values close together: the sensor cannot tell the tape from the surface. Check the sensor height (10 to 15 mm), check the contrast of the track and compare the three printed values.
  • Values reversed (blackValue higher than whiteValue): the robot started in the wrong position, or the rotation times need adjustment.
  • Both values at zero: check the I2C wiring (SDA to A4, SCL to A5) and the detection message at startup.

Step 8: Testing and tuning

Program flow of the sketch (calibration, then reading the sensor, calculating the error and PID control in a loop) and the control loop with middleValue 5, sensor reading 2 and error 3.

Setting up the test track

  • Use 50 mm wide white adhesive tape on a dark, matte surface. The white LED on the module lights the track under the sensor. Leave it on: the LED pin stays unconnected, as in the wiring diagram.
  • Start with an oval or wide curves. Avoid sharp 90-degree turns until the PID is tuned.
  • Keep the track evenly lit. Shadows and bright spotlights change the readings.

Tuning the PID parameters

The sketch uses Kp = 24.0, Ki = 1.0, Kd = 1.0 and baseSpeed = 60. I kept changing the gains on my test track until the robot followed the edge, and these are the values I ended with. Your motors, battery and track may need different values.

  • Start with Kp only. Set Ki = 0 and Kd = 0. Raise Kp step by step from a low value such as 5 until the robot follows the line. If it overshoots and oscillates, lower Kp a little.
  • Add Ki carefully. If the robot keeps drifting to one side or misses wide curves, raise Ki in small steps, for example 0.5. Too much Ki causes slow oscillation and instability.
  • Add Kd only if needed. If the robot still oscillates after tuning Kp, a small Kd, for example 0.5 to 2.0, damps it. Too high a Kd makes the robot sluggish or jittery.
  • Keep baseSpeed between 40 and 60 during early tests (the sketch uses 60). A lower speed gives the PID controller more time to react. Raise it step by step once the robot follows the line reliably.

Open the Serial Monitor at 9600 baud, the data rate set by Serial.begin(9600) in setup(). After calibration, the sketch prints the black, white and middle values. To watch the error while tuning, add Serial.println(error); at the end of loop(). The extra output slows the loop, so remove the line once the gains are set.

Troubleshooting common issues

ProblemPossible causeSolution
Robot does not follow the line at allSensor not detected, wrong wiring or failed calibrationCheck the I2C wiring (SDA to A4, SCL to A5). Open the Serial Monitor and check the detection message and the three calibration values. Reposition the robot and restart.
Robot oscillates heavily around the lineKp too high or Kd too lowReduce Kp by 20 to 30 %. Add a small Kd (0.5 to 2.0) to damp the oscillation.
Robot reacts too slowly and misses curvesKp too low or baseSpeed too highRaise Kp step by step. Lower baseSpeed to give the controller more time.
Sensor readings unstable or noisySensor too far from the surface, ambient light or loose wiringLower the sensor to 10 to 15 mm above the track. Shield the sensor from overhead lights. Check all wire connections.
Robot calibrates incorrectlyWrong starting position or low contrastPlace the robot at the left edge of the white tape on the dark surface. Check the contrast: matte white tape on a matte dark surface.

Motor, power, and track issues

ProblemPossible causeSolution
Motors are weak or do not moveLow battery, PWM value too low or motor wires swappedCharge or replace the battery. Check that the minimum speed of 20 moves your motors. Swap the motor wires if the direction is wrong.
One motor runs faster than the otherDifferences between the motors or a wiring problemNormal for low-cost TT motors. The P term evens out small differences. The I term adds at most ±4 here (Step 5), so if the robot still drifts to one side, raise Ki in small steps.
Robot moves forward and back instead of turning on the spot during calibration, or spins in circles while following the lineOne motor is wired in reverseSwap the two wires of the motor that turns the wrong way.
Line contrast is insufficientTape too similar to the surface, or a glossy surface reflects lightUse matte white tape on a matte dark surface. Avoid glossy floors and metallic tape.
Behavior changes as the battery drainsBattery voltage drops under motor loadUse a fully charged 7.4 V LiPo. A 9 V PP3 battery (the small rectangular type) loses voltage quickly under motor load. Consider a battery voltage monitor.

Frequently asked questions

Can I use a different Arduino board instead of the Uno?

Yes, with limits. The Motor Shield Rev3 also fits an Arduino Mega, but the Mega has its I2C pins on 20 (SDA) and 21 (SCL), so connect the sensor there instead of A4/A5. A Nano does not take the shield; you need a separate motor driver wired by hand.

Can I use multiple TCS34725 sensors on this robot?

The TCS34725 has a fixed I2C address (the number the Arduino uses to select a device on the bus), so two sensors cannot share the bus directly. You need an I2C multiplexer such as the TCA9548A, which switches between several devices with the same address. One sensor is enough for the tracks described here.

Can I modify this project to follow a colored line instead of white tape on a dark surface?

Yes. The sensor returns red, green, blue and clear values, so you can use a color channel instead of the clear channel. To follow a red line, compare the red channel with the green and blue channels. This needs changes to the error calculation.

Resources and downloads

  • GitHub repository: the full Arduino code on GitHub.
  • STL files: the chassis and the spacers in the OmArTronics Shop.
  • Adafruit TCS34725 library: Arduino IDE, Library Manager, search for “Adafruit TCS34725”.

Related OmArTronics tutorials

Next steps

From here you can raise baseSpeed, lay out tighter curves, follow a colored line with the RGB channels or add more sensors with an I2C multiplexer. The sketch keeps both wheels at 20 or more: at the largest error of +3, the P term alone gives the inner wheel 60 – 72 = -12, and constrain() raises it to 20. The robot never turns on the spot. For sharp 90-degree corners, set the lower limit in constrain() below zero so the inner wheel can turn backward.

1 thought on “Building a Line Following Robot with TCS34725 RGB Sensor”

Leave a Comment