Line-following robot that actually follows the line: sensor spacing, proportional control and why yours weaves
- Build Logs
- 29 Jul, 2026
Every college lab builds a line follower. Most of them wobble down the track like they are drunk, and the usual fix — "reduce the speed" — just makes a slow drunk robot. The oscillation is not a speed problem. It is a control problem, and it takes about twenty lines of code to solve properly.
This build uses two IR sensors and a proportional controller. We will get it running with the naive bang-bang logic first, watch it fail, and then fix it — because the failure is the part worth understanding.
What you will need
| Part | Qty | Approx. |
|---|---|---|
| Arduino Uno R3 (compatible) | 1 | ₹418 |
| IR Sensor Module | 2 | ₹58 |
| L298N Motor Driver Module | 1 | ₹139 |
| 100 RPM Dual Shaft BO Motor with Wheel (4 pcs) | 1 set | ₹420 |
| 18650 Battery Holder × 2 + 18650 Cells | 2 | ₹152 |
Roughly ₹1,190 for the electronics. You will also want a chassis plate, a castor wheel and black insulation tape on white card for the track.
Why two sensors, and how far apart
An IR line sensor is a tiny reflectance meter: an infrared LED shines down, a phototransistor measures what bounces back. White card reflects, black tape absorbs. The module has a comparator and a trimpot, so you get a clean digital LOW over black and HIGH over white (some modules are inverted — check yours before you blame the code).
The spacing is the decision everyone gets wrong. Put the two sensors slightly narrower than the tape is wide. With 18 mm electrical tape, mount them about 15 mm apart, centre to centre.
- Too close together and both sensors sit on black almost all the time. The robot has no idea which way it is drifting until it has already left the line.
- Too far apart and both sit on white most of the time, so the robot only reacts once it is badly off course — which produces exactly the drunken weave everyone complains about.
Mount them 5–10 mm above the surface. Higher and the reflected signal gets weak and noisy; touching the floor and they catch on tape edges.
Wiring
| From | To |
|---|---|
| Left IR OUT | Uno D2 |
| Right IR OUT | Uno D3 |
| IR VCC / GND | Uno 5V / GND |
| L298N IN1, IN2 | Uno D5, D6 |
| L298N IN3, IN4 | Uno D9, D10 |
| L298N ENA, ENB | Uno D11, D12 (remove the jumpers) |
| L298N +12V / GND | Battery + / − |
| L298N GND | Uno GND (this one is not optional) |
Two things here trip people up constantly.
Remove the ENA and ENB jumpers. Out of the box the L298N has jumpers tying those enable pins to 5V, which locks both motors at full speed. Every PWM value you write is ignored, the robot runs flat out, and you conclude that PWM "does not work on this board". Pull the jumpers and wire ENA/ENB to PWM-capable pins.
Tie the grounds together. The motor battery and the Arduino must share a ground reference or the driver sees garbage on its input pins. Symptom: motors twitch randomly, or one channel refuses to run.
First attempt: bang-bang, and why it weaves
The obvious logic is: if the left sensor sees black, turn left; if the right sees black, turn right; otherwise go straight.
#define L_SENSOR 2
#define R_SENSOR 3
#define ENA 11
#define ENB 12
#define IN1 5
#define IN2 6
#define IN3 9
#define IN4 10
const int SPEED = 150;
void setup() {
pinMode(L_SENSOR, INPUT);
pinMode(R_SENSOR, INPUT);
pinMode(ENA, OUTPUT); pinMode(ENB, OUTPUT);
pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
// both motors forward, for the whole run
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
}
void loop() {
bool leftOnBlack = digitalRead(L_SENSOR) == LOW;
bool rightOnBlack = digitalRead(R_SENSOR) == LOW;
if (leftOnBlack && !rightOnBlack) { // drifted right
analogWrite(ENA, 0); analogWrite(ENB, SPEED);
} else if (!leftOnBlack && rightOnBlack) { // drifted left
analogWrite(ENA, SPEED); analogWrite(ENB, 0);
} else {
analogWrite(ENA, SPEED); analogWrite(ENB, SPEED);
}
}
This works, in the sense that the robot stays on the track. It also weaves, because the controller only has three outputs: hard left, hard right, straight. There is no such thing as a gentle correction. The robot overshoots the centre, the other sensor triggers, it overshoots back. That is the wobble, and no amount of lowering SPEED removes it — it just slows the oscillation down.
The fix: read the sensors as analogue
The digital output throws away everything useful. The phototransistor is producing a continuous value and the comparator is flattening it to one bit. Wire the sensor's analogue output into A0 and A1 instead, and you get a real error signal.
#define L_SENSOR A0
#define R_SENSOR A1
const int BASE_SPEED = 140;
const float KP = 0.35; // start here, then tune
int lWhite, lBlack, rWhite, rBlack; // calibration bounds
void calibrate() {
// Sweep the robot across the line by hand for ~3 seconds
lWhite = rWhite = 0;
lBlack = rBlack = 1023;
unsigned long end = millis() + 3000;
while (millis() < end) {
int l = analogRead(L_SENSOR), r = analogRead(R_SENSOR);
lWhite = max(lWhite, l); lBlack = min(lBlack, l);
rWhite = max(rWhite, r); rBlack = min(rBlack, r);
}
}
void loop() {
// Normalise each sensor to 0..100 using its OWN calibration.
int l = map(analogRead(L_SENSOR), lBlack, lWhite, 0, 100);
int r = map(analogRead(R_SENSOR), rBlack, rWhite, 0, 100);
l = constrain(l, 0, 100);
r = constrain(r, 0, 100);
int error = l - r; // 0 when centred
int correction = KP * error;
int leftSpeed = constrain(BASE_SPEED - correction, 0, 255);
int rightSpeed = constrain(BASE_SPEED + correction, 0, 255);
analogWrite(ENA, leftSpeed);
analogWrite(ENB, rightSpeed);
}
Now the correction is proportional to how far off centre the robot is. A 2 mm drift produces a small speed difference; a 10 mm drift produces a large one. The robot converges on the line instead of bouncing between two extremes.
Why per-sensor calibration matters
The two sensors will not agree, even from the same batch. LED brightness, phototransistor gain and how square you mounted them all differ. If you use one shared threshold, the robot will have a permanent bias to one side and you will spend an evening bending the sensor bracket trying to fix it mechanically.
Normalising each sensor against its own measured black and white values cancels all of that in software. Run calibrate() at power-on, sweeping the robot across the line by hand. It also makes the robot portable between the lab bench and a demo table under different lighting.
Tuning KP
- Still weaving — KP is too high. The robot is over-correcting. Halve it.
- Cuts corners or leaves the line on curves — KP is too low. Raise it by 50%.
-
Fine on straights, loses tight curves — lower
BASE_SPEED. There is a real physical limit to how fast the chassis can turn, and no gain value beats it.
Only add the D term of a PID once P alone is behaving. On a two-sensor robot at these speeds, P is usually enough, and a D term computed from a noisy 10-bit reading often makes things worse.
Things that go wrong
Robot runs full speed regardless of PWM. The ENA/ENB jumpers are still fitted. This is the single most common fault on this build.
One motor spins backwards. Swap that motor's two wires at the L298N screw terminal. It is not a code problem, and "fixing" it in software leaves you a trap for later.
Works on the bench, fails on the floor. Sensor height changed. Reflectance falls off fast with distance; 3 mm is enough to move both readings out of the calibrated range.
Arduino resets when the motors start. You are powering the Uno from the same battery through the L298N's 5V regulator, and motor inrush is dragging the rail down. Give the Arduino its own supply, or use a proper buck converter.
Sensors read inverted. Some IR modules output LOW on white. Check with the serial monitor before rewriting your logic.
Where to take it next
Two sensors can follow a line but cannot tell a sharp turn from a junction. Five sensors can — that is what a weighted-average position calculation gives you, and it is the jump from "follows a line" to "solves a maze".
Stuck on a step? Talk to one of our engineers — we would rather help you finish it than sell you a replacement board.