Robotics & IoT

Line Follower Robot: PID Control & Arduino

A detailed engineering guide to building a precision autonomous robot using PID logic, Arduino Mega, and IR sensor arrays.

Shubham Kulkarni
Shubham Kulkarni Robotics Engineer
Updated
Line Follower Robot

Building a robot that can follow a line is the "Hello World" of robotics — every tutorial makes it look trivial. But building one that navigates a competition track at 1.2 m/s without oscillating off sharp corners? That's a control systems problem. The difference between a ₹500 science fair toy and a competition-winning machine isn't the hardware — it's the algorithm.

This article goes beyond simple "if-else" logic. By implementing a Proportional-Integral-Derivative (PID) controller with proper sensor calibration and Ziegler-Nichols tuning, we eliminated the "jerky wobbling" typical of amateur robots and achieved smooth, railing-like cornering performance that won 2nd place in the Tech Fest Robotics competition.

5ms Control Loop Time
1.2 m/s Max Speed
2nd Competition Rank

1. Why PID Matters: The If-Else Trap

The Naive Approach (Don't Do This)

Every beginner's first instinct is: "If left sensor sees line → turn left. If right sensor → turn right." This causes violent oscillation because the robot overcorrects on every cycle, creating a zig-zag path.

❌ If-Else

Binary ON/OFF steering

Oscillation, lost on corners

✅ PID

Proportional correction

Smooth, predictive steering

2. Hardware Stack

The brain of the operation is the Arduino Mega 2560 — chosen not because of its processing power, but because of its 6 hardware interrupt pins and multiple ADC channels, which are critical for time-sensitive sensor reads. The motors are driven by an L293D H-Bridge driver with PWM speed control.

  • Microcontroller: Arduino Mega 2560 — 16 MHz ATmega2560, 8KB SRAM, 6 hardware interrupts
  • Sensors: 5-Channel IR Array (TCRT5000 reflective sensors, analog output)
  • Motor Driver: L293D Dual H-Bridge (600mA per channel, PWM-controlled)
  • Motors: N20 300 RPM Bo Motors (3V–12V, high torque gearbox)
  • Power: 12V 2200mAh Li-Po battery (runtime: ~45 minutes at full load)
  • Chassis: Custom laser-cut 3mm acrylic (weight: 380g total)

System Diagram

flowchart TD subgraph Sensors ["👁️ Perception"] IR["5× IR Sensor Array\nTCRT5000"] end subgraph Brain ["🧠 Processing"] Arduino["Arduino Mega 2560"] PID["PID Controller\nKp·e + Ki·∫e + Kd·de/dt"] end subgraph Drive ["⚡ Actuation"] L293D["L293D H-Bridge"] LM["🔄 Left Motor\n300 RPM"] RM["🔄 Right Motor\n300 RPM"] end Battery["🔋 12V Li-Po"] --> L293D Battery --> Arduino IR -->|Analog Readings| Arduino Arduino --> PID PID -->|PWM Signal| L293D L293D -->|Voltage| LM L293D -->|Voltage| RM

3. Sensor Calibration: The Step Everyone Skips

Raw IR sensor readings are meaningless without calibration. The TCRT5000 output varies wildly between surfaces, lighting conditions, and even sensor aging. A "black line" might read 850 on a clean track and 650 on a dusty one.

At boot, we run a 30-second calibration routine where the robot slowly rotates over the line, recording the min and max readings from each sensor. This gives us normalized 0–1000 values regardless of environment.

calibration.ino
// --- Sensor Calibration Routine ---
// Run at boot: robot slowly rotates to capture min/max for each sensor

#define NUM_SENSORS 5
int sensorPins[NUM_SENSORS] = {A0, A1, A2, A3, A4};
int sensorMin[NUM_SENSORS], sensorMax[NUM_SENSORS];

void calibrateSensors() {
    // Initialize min/max arrays
    for (int i = 0; i < NUM_SENSORS; i++) {
        sensorMin[i] = 1023;  // Start high (10-bit ADC max)
        sensorMax[i] = 0;     // Start low
    }
    
    // Spin slowly for 30 seconds, sampling continuously
    unsigned long startTime = millis();
    while (millis() - startTime < 30000) {
        // Rotate robot slowly to sweep sensors over line
        analogWrite(LEFT_PWM, 80);
        analogWrite(RIGHT_PWM, 80);
        digitalWrite(LEFT_DIR, HIGH);
        digitalWrite(RIGHT_DIR, LOW);  // Opposite → spin in place
        
        for (int i = 0; i < NUM_SENSORS; i++) {
            int val = analogRead(sensorPins[i]);
            if (val < sensorMin[i]) sensorMin[i] = val;
            if (val > sensorMax[i]) sensorMax[i] = val;
        }
        delay(5);
    }
    stopMotors();
}

// Normalize raw reading to 0-1000 range using calibration data
int normalizeReading(int sensorIndex, int rawValue) {
    int range = sensorMax[sensorIndex] - sensorMin[sensorIndex];
    if (range < 50) return 0;  // Guard: sensor dead or disconnected
    int normalized = map(rawValue, sensorMin[sensorIndex],
                         sensorMax[sensorIndex], 0, 1000);
    return constrain(normalized, 0, 1000);
}
Sensor Weight Array

Each sensor is assigned a weight based on its distance from center. The weighted average gives us a continuous error signal:

SensorPositionWeightPurpose
S0 (Far Left)A0-2000Hard left correction
S1 (Left)A1-1000Gentle left correction
S2 (Center)A20On the line (no error)
S3 (Right)A3+1000Gentle right correction
S4 (Far Right)A4+2000Hard right correction

4. Understanding PID Control

PID is a closed-loop feedback controller that calculates an error value as the difference between a desired setpoint (center of line) and the measured process variable (sensor position). Each of the three terms contributes differently:

P — Proportional

P = Kp × error

Steers proportionally to how far off-center the robot is. High Kp = aggressive correction, but can cause oscillation.

I — Integral

I += error × dt

Accumulates past errors. Fixes steady-state offset (robot slightly off-center on straights). Too much → "wind-up" drift.

D — Derivative

D = Kd × (error - prev_error)

Predicts the future by measuring rate of change. Acts as a "brake" to prevent overshoot on corners.

The Combined Formula

Correction = Kp·e(t) + Ki·∫e(t)dt + Kd·de(t)/dt

5. Arduino Implementation (Full Code)

Below is the complete PID loop including sensor reading, error calculation, integral wind-up protection, and motor speed clamping. The loop runs every 5ms (200Hz), fast enough for smooth control at 1.2 m/s.

line_follower.ino
// --- PID Line Follower (Competition-Ready) ---
// Hardware: Arduino Mega, 5× TCRT5000, L293D, N20 Motors

// PID Gains (tuned via Ziegler-Nichols method)
float Kp = 10.0;   // Proportional gain
float Ki = 0.05;    // Integral gain (kept low to avoid wind-up)
float Kd = 28.0;    // Derivative gain (aggressive damping)

// State variables
float error = 0, previous_error = 0;
float P = 0, I = 0, D = 0, PID_value = 0;
int base_speed = 180;  // PWM: 0-255 (70% of max)

// Sensor weights: far-left(-2000) to far-right(+2000)
int weights[5] = {-2000, -1000, 0, 1000, 2000};

void loop() {
    // 1. Read calibrated sensor values
    int position = 0, totalWeight = 0;
    bool onLine = false;
    
    for (int i = 0; i < 5; i++) {
        int val = normalizeReading(i, analogRead(sensorPins[i]));
        if (val > 200) onLine = true;  // At least one sensor sees line
        position += val * weights[i];
        totalWeight += val;
    }
    
    // 2. Calculate error (0 = centered on line)
    if (totalWeight > 0) {
        error = (float)position / totalWeight;
    }
    // If line lost: use last known direction (overshoot recovery)
    if (!onLine) {
        error = (previous_error > 0) ? 2500 : -2500;
    }
    
    // 3. PID Calculation
    P = error;
    I = I + error;
    D = error - previous_error;
    
    // Anti-wind-up: clamp integral to prevent accumulation
    I = constrain(I, -10000, 10000);
    
    PID_value = (Kp * P) + (Ki * I) + (Kd * D);
    previous_error = error;
    
    // 4. Apply correction to motors (differential steering)
    int left_speed  = base_speed - PID_value;
    int right_speed = base_speed + PID_value;
    
    // Clamp to valid PWM range
    left_speed  = constrain(left_speed, 0, 255);
    right_speed = constrain(right_speed, 0, 255);
    
    setMotorSpeed(left_speed, right_speed);
    
    delay(5);  // 5ms loop → 200 Hz control frequency
}

6. Tuning Methodology (Ziegler-Nichols)

Tuning a PID controller is part science, part art. We used the Ziegler-Nichols method, a systematic approach that saves hours of trial-and-error:

  1. Set Ki = 0, Kd = 0. Increase Kp until the robot follows the line but oscillates consistently. This is the Ultimate Gain (Ku). Record the oscillation period (Tu).
  2. Calculate initial gains from the Ziegler-Nichols table:
    • Kp = 0.6 × Ku
    • Ki = 2 × Kp / Tu
    • Kd = Kp × Tu / 8
  3. Fine-tune on the competition track. Increase Kd if corners cause overshoot. Reduce Ki if the robot drifts on straights.
Our Tuning Journey
AttemptKpKiKdBehavior
1 (P-only)1500Heavy oscillation, goes off on corners
2 (Ku found)1700Sustained oscillation → Ku = 17, Tu ≈ 0.3s
3 (Z-N formula)10.2680.38Better, slight drift on straights
4 (Manual adjustment)100.0528✅ Smooth! Competition-ready

Key Insight: The Ziegler-Nichols Ki value (68) was way too high for our system and caused integral wind-up. We reduced it to 0.05 and increased Kd dramatically. This is common in fast-response systems — the derivative term matters much more than the integral.

7. Competition Results & Performance

We tested the robot on three different track types during the Tech Fest competition. Here are the results compared to the winning team (Team Alpha) who used an 8-sensor array:

Competition Performance
Track TypeOur RobotWinner (8-sensor)Gap
Straight Line (3m)2.5s2.2s0.3s
90° Turns (4 corners)8.1s7.8s0.3s
Complex Track (S-curves + hairpins)14.2s12.9s1.3s
Total Time24.8s22.9s1.9s (2nd Place)

The 1.9-second gap was primarily due to the S-curve section. With only 5 sensors, our bot lost the line momentarily on tight curves. The winning team's 8-sensor array gave them wider coverage. That said, we matched them on straights and 90° turns — proving that algorithm tuning matters more than sensor count for common track geometries.

8. Future Enhancements

Building a PID line follower is just the beginning. Here's the roadmap for Version 2:

  • Camera-Based Navigation: Replace IR sensors with a Raspberry Pi Camera and OpenCV to detect line position via image processing — enabling intersection handling and color-coded routes.
  • 8+ Sensor Array: Upgrade to an 8-sensor array for better coverage on tight curves. The math stays the same; only the weight array changes.
  • Adaptive Kp: Implement dynamic Kp based on speed — higher Kp at low speeds for tight corners, lower Kp at high speeds for stability.
  • Bluetooth Tuning App: Build an Android app to adjust Kp/Ki/Kd in real-time via Bluetooth, eliminating the upload-test-repeat cycle.
  • EEPROM Storage: Save tuned PID values to EEPROM so the robot remembers its calibration between power cycles.

Key Takeaways

  • Calibrate before you ride. Raw sensor readings are unreliable. A 30-second calibration routine is the single biggest improvement you can make.
  • Kd > Ki for fast systems. In a robot moving at 1.2 m/s, the derivative term (prediction) matters far more than the integral (history).
  • Anti-wind-up is essential. Without integral clamping, the I term accumulates on corners and causes the robot to overshoot on the straight after.
  • The Ziegler-Nichols method gives you a starting point, not the answer. Always fine-tune manually on the actual competition track.
  • 5 sensors are enough for 90% of tracks. Algorithm tuning compensates for sensor limitations on all but the tightest curves.

Shubham Kulkarni
About Shubham Kulkarni

I am a Robotics & AI Engineer passionate about building systems that bridge the physical and digital worlds. View my Portfolio.