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.
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.
Binary ON/OFF steering
Oscillation, lost on corners
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
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.
// --- 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:
| Sensor | Position | Weight | Purpose |
|---|---|---|---|
| S0 (Far Left) | A0 | -2000 | Hard left correction |
| S1 (Left) | A1 | -1000 | Gentle left correction |
| S2 (Center) | A2 | 0 | On the line (no error) |
| S3 (Right) | A3 | +1000 | Gentle right correction |
| S4 (Far Right) | A4 | +2000 | Hard 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.
// --- 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:
- 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).
- Calculate initial gains from the Ziegler-Nichols table:
Kp = 0.6 × KuKi = 2 × Kp / TuKd = Kp × Tu / 8
- Fine-tune on the competition track. Increase Kd if corners cause overshoot. Reduce Ki if the robot drifts on straights.
Our Tuning Journey
| Attempt | Kp | Ki | Kd | Behavior |
|---|---|---|---|---|
| 1 (P-only) | 15 | 0 | 0 | Heavy oscillation, goes off on corners |
| 2 (Ku found) | 17 | 0 | 0 | Sustained oscillation → Ku = 17, Tu ≈ 0.3s |
| 3 (Z-N formula) | 10.2 | 68 | 0.38 | Better, slight drift on straights |
| 4 (Manual adjustment) | 10 | 0.05 | 28 | ✅ 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 Type | Our Robot | Winner (8-sensor) | Gap |
|---|---|---|---|
| Straight Line (3m) | 2.5s | 2.2s | 0.3s |
| 90° Turns (4 corners) | 8.1s | 7.8s | 0.3s |
| Complex Track (S-curves + hairpins) | 14.2s | 12.9s | 1.3s |
| Total Time | 24.8s | 22.9s | 1.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.
About Shubham Kulkarni
I am a Robotics & AI Engineer passionate about building systems that bridge the physical and digital worlds. View my Portfolio.