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.
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.
| Component | Qty | Required / Optional | Notes |
|---|---|---|---|
| Chassis | 1 | Required | 3D printed (Step 1) in two parts, base plate and cover; PLA or ABS plastic |
| Sensor spacer | 1 per mounting hole | Required | 3D printed (Step 1), 7 mm wide, 13.5 mm high |
| Arduino spacer | 4 | Required | 3D printed (Step 1), 14 mm high, one per mounting hole of the Uno |
| DC motor with gearbox | 2 | Required | Yellow TT gearmotors, the common low-cost type for small robots |
| Wheels | 2 | Required | Must fit the TT motor shafts |
| Caster wheel | 1 | Required | Ball caster or swivel caster at the front |
| Arduino Uno | 1 | Required | Runs the sketch |
| Arduino Motor Shield Rev3 | 1 | Required | Plugs onto the Uno, drives both motors |
| TCS34725 RGB color sensor | 1 | Required | Reads the brightness of the track under the robot |
| Jumper wires | Several | Required | Male-to-female and male-to-male |
| M3 screws and threaded inserts | As needed | Required | Fix the motors and spacers to the chassis. Threaded inserts are metal sleeves with an inner thread that sit in the printed holes. |
| Battery | 1 | Required | 7.4 V 2-cell LiPo (lithium polymer); powers motors and Arduino through the shield |
| Power switch | 1 | Optional | Switches 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

Mounting the motors and wheels
- 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.
- Press the wheels onto the motor shafts and check that both spin freely without touching the chassis.
- Mount the caster wheel at the front. With the two drive wheels it gives three-point support.
Installing the electronics
- Fix the four Arduino spacers to the chassis and mount the Arduino Uno on them.
- 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.
- Mount the TCS34725 module on the sensor spacers at the front underside of the chassis, with the sensor facing the track.
- 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.

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.
| Motor | Shield terminal | Direction pin | PWM (speed) pin |
|---|---|---|---|
| Left motor (Motor A) | Motor A | Digital pin 12 (DIR_A) | Digital pin 3 (PWM_A) |
| Right motor (Motor B) | Motor B | Digital pin 13 (DIR_B) | Digital pin 11 (PWM_B) |
Sensor connections (I2C)
| TCS34725 pin | Arduino pin | Notes |
|---|---|---|
| VIN | 5 V | Power supply for the sensor |
| GND | GND | Common ground |
| SDA | A4 (SDA) | I2C data line |
| SCL | A5 (SCL) | I2C clock line |
Power connections
| Component | Connection | Notes |
|---|---|---|
| Battery (7.4 V LiPo) | External power input of the Motor Shield | Powers both motors and the Arduino through the shield |
| Power switch (optional) | In series with the positive battery lead | Switches 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).

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 surfacecis low and the error is positive: the sensor has drifted left off the tape. Over the white tapecis 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 (
blackValuehigher thanwhiteValue): 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

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
Kponly. SetKi = 0andKd = 0. RaiseKpstep by step from a low value such as 5 until the robot follows the line. If it overshoots and oscillates, lowerKpa little. - Add
Kicarefully. If the robot keeps drifting to one side or misses wide curves, raiseKiin small steps, for example 0.5. Too muchKicauses slow oscillation and instability. - Add
Kdonly if needed. If the robot still oscillates after tuningKp, a smallKd, for example 0.5 to 2.0, damps it. Too high aKdmakes the robot sluggish or jittery. - Keep
baseSpeedbetween 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
| Problem | Possible cause | Solution |
|---|---|---|
| Robot does not follow the line at all | Sensor not detected, wrong wiring or failed calibration | Check 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 line | Kp too high or Kd too low | Reduce Kp by 20 to 30 %. Add a small Kd (0.5 to 2.0) to damp the oscillation. |
| Robot reacts too slowly and misses curves | Kp too low or baseSpeed too high | Raise Kp step by step. Lower baseSpeed to give the controller more time. |
| Sensor readings unstable or noisy | Sensor too far from the surface, ambient light or loose wiring | Lower the sensor to 10 to 15 mm above the track. Shield the sensor from overhead lights. Check all wire connections. |
| Robot calibrates incorrectly | Wrong starting position or low contrast | Place 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
| Problem | Possible cause | Solution |
|---|---|---|
| Motors are weak or do not move | Low battery, PWM value too low or motor wires swapped | Charge 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 other | Differences between the motors or a wiring problem | Normal 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 line | One motor is wired in reverse | Swap the two wires of the motor that turns the wrong way. |
| Line contrast is insufficient | Tape too similar to the surface, or a glossy surface reflects light | Use matte white tape on a matte dark surface. Avoid glossy floors and metallic tape. |
| Behavior changes as the battery drains | Battery voltage drops under motor load | Use 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
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.
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.
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
- Building a Line-Following Robot with KY-033 Sensors for Beginners: the beginner version of this project, with infrared sensors and without PID control.
- Building the Bluetooth-Controlled Robot Car OmObi: a robot car controlled from a smartphone over Bluetooth.
- OmObiArm: Mobile Robot with Integrated Robotic Arm: a driving platform with a robotic arm.
- Control a Servo Motor with Joystick and OLED Display: Arduino basics with a servo, a joystick and a display.
- DIY 6-DOF Robotic Arm with Bluetooth Control: a robotic arm with six degrees of freedom.
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”