Project, a Line Following Robot
| Difficulty | Advanced |
|---|---|
| Category | Projects |
| Supported boards | Arduino Uno R3 and compatible ATmega328P boards |
| Time needed | 180 minutes |
| Prerequisites | A Four-Way Line Tracking Sensor |
| Tested board | Not yet verified |
| Tested on | Not yet verified |
| Arduino IDE version | Not yet verified |
| Library | Not yet verified |
What you will build
A robot that follows a line of black tape across the floor. Four sensors looking down, two geared motors, and a sketch that turns “the line is slightly to my left” into “slow the left wheel”.
It is the first project in this library that is a closed loop: the thing it measures is changed by the thing it does, continuously, and that changes how you think about all of it. Everything in the two component pages that was a problem — two motors that will not run at the same speed, a sensor with no analog output, a trim that is only right at one speed — stops mattering here, because the robot corrects its own heading several hundred times a second.
That is the lesson, and it is worth more than the robot.
Before you begin
Parts
| Part | Siratek product | Its own tutorial |
|---|---|---|
| Arduino Uno | 2219 | Getting Started with Arduino Uno |
| Four-way line tracking sensor | 2943 | A Four-Way Line Tracking Sensor |
| L298N motor driver | 2533 | Driving DC Motors with an L298N |
| Geared motor and wheel, two of them | 7406 | Geared Motors and Wheels for a Robot |
| Push button | 2353 | Read a Push Button |
| Breadboard and jumper wires | Using a Breadboard |
Also needed: a chassis or something to screw the motors to, a castor or ball wheel for the front, a battery pack of four to six AA cells for the motors, a soldering iron, black electrical tape and a large sheet of white paper or card.
Siratek product 7406 is one motor and one wheel. A driving robot needs two of them. That is the single most common ordering mistake on this project.
The motor supply, which is not optional
Each geared motor draws roughly 150 to 200 mA running free and close to 1 A stalled. Two stalled motors is near 2 A, which is far beyond USB and more than a tired battery pack will hold up under.
A robot that resets when it bumps into something is this, not a software fault. Separate motor supply, grounds joined, as the L298N page sets out. There is no sketch that fixes a sagging supply.
Why the motor mismatch stops mattering
Geared Motors and Wheels for a Robot is blunt about it: two identical motors given the same voltage make a robot curve, every time, often by ten percent or more, and the three honest responses are to trim it, to fit encoders, or to build something that corrects its own heading.
This project is the third one. The line tells the robot where it is several hundred times a second, and a wheel that runs a little fast simply gets a little less of the correction. You never have to measure the trim at all.
Which is why a line follower is the right first robot, rather than the hard one it looks like.
Six pins for the driver, four for the sensor, and the collision between them
The L298N takes six Arduino pins, and the page assigns them D5 to D10 because three of those are PWM-capable, which the enable pins need.
The tracking sensor’s own page uses D4 to D7. Those overlap. Something has to move, and it must not be the enable pins, because PWM is not available everywhere.
So the sensor goes on A0 to A3, used as digital pins. The analog inputs on an Uno are ordinary digital pins as well — A0 is D14, A1 is D15, and so on — and digitalRead(A0) works exactly as digitalRead(4) does. Nothing is lost here, because the sensor has no analog output for us to give up.
This is a small thing that catches people out on every project with more than two modules in it: pins are a budget, and the ones with special abilities get spent last.
Set the trimmers before you write any logic
The tracking sensor has four trimmers and four indicator LEDs, and nothing works properly until they are set. Lay out your line, hold the sensors at their mounted height over the white card, and turn each trimmer until its LED just changes, then back it off slightly.
Height matters more than anything else. Five to fifteen millimetres is the usual working range, all four heads must sit at the same height, and a head two millimetres low behaves like a differently adjusted one. Fix the height mechanically; do not try to fix it in software.
The full procedure is on the sensor’s page. Do it with the robot on a stand, before the motors are ever enabled.
Build the line before you build the robot
Black electrical tape on white card, at least two metres of it, with gentle curves. Make the first one easy: wide radius, no crossings, no sharp corners.
A tight corner is not a harder version of the same problem — it is a different problem, because the line leaves the sensor array completely and the robot has to search for it. Get the easy course working first, then make it harder deliberately.
How it works
One loop, running as fast as it can, doing three things:
| Step | What happens |
|---|---|
| Read | Four digitalRead() calls become one position number |
| Decide | The position becomes a left speed and a right speed |
| Act | Two analogWrite() calls |
There is no delay() anywhere, and that is not a style preference. A delay of fifty milliseconds at walking pace is several centimetres of travel with its eyes shut.
The position number
From the sensor’s own page: each head reads light or dark, the dark ones are weighted -3, -1, +1, +3 from left to right, and averaging the weights of the heads that see the line gives a single number saying how far off-centre it is. Negative is left, positive is right, zero is centred, and NO_LINE is a separate answer that is not zero.
That last distinction is the one that keeps the robot on the table. A sketch that treats “no line” as “centred” drives confidently straight on.
Proportional steering
The whole controller is one idea: steer in proportion to how far off you are.
left = base + gain * position
right = base - gain * position
A little off-centre, a little correction. Far off, a big one. The robot slows the wheel on the side the line is on, which turns it that way.
gain is the number you tune, and it has two failure modes you will meet in this order:
| Gain | Behaviour |
|---|---|
| Too low | Drifts off the line on curves; corrects too lazily |
| About right | Follows the line with a gentle weave |
| Too high | Oscillates violently, left-right-left, and often loses the line entirely |
Start low, raise it until the weave appears, then back off slightly. That is the whole tuning procedure, and doing it by hand once teaches more about control than reading about it does.
This is proportional control only, the P of PID. Adding the other two terms is a real improvement at speed and a project of its own; at the speed a first robot should run, P alone follows a reasonable line well.
Speed is the hidden variable
Everything above depends on how fast the robot is going. A gain tuned at a crawl oscillates at speed, because the same correction has longer to act.
So tune slowly first. BASE_SPEED of about 120 is a sensible start. Raising the speed later means re-tuning the gain, and that relationship — faster needs gentler, or needs a better controller — is worth noticing because it is true of every control loop you will ever write.
Losing the line, which is normal
Especially on corners. The behaviour you choose defines how the robot feels, and the simplest useful rule is: remember which way you were last turning, and keep turning that way. If the line vanished to the left, it is probably still to the left.
With a time limit on it. Searching for a second is persistence; searching forever is a robot spinning in the corner of the room until its batteries go flat.
All four dark
Four sensors reading dark at once is not a line. It is a junction, the end of the course marked with a wide band, or the sensors lifted off the surface. The sketch reports it separately and stops, which is the honest default — deciding what a junction means is a choice about your course, not about the robot.
Build it in stages
Each stage ends with something working. Do not skip to the end; a robot that drives before its sensors are trusted is a robot that drives off the bench.
Stage 1: the mechanics
Solder leads to both motors before mounting them — afterwards the tabs are awkward to reach and they break off easily. A dab of hot glue over each joint earns its place.
Screw both motors to the chassis, wheels pushed fully onto the D-shaped shafts, castor at the front. Mount the four sensor heads in a row, facing down, all at the same height, spaced so the line falls between the middle two when the robot is centred.
Because the heads are separate boards on leads, you can space them to suit the chassis rather than building the chassis around the sensor. Use that.
Stage 2: the motors, on a stand, one at a time
Wire the L298N as its page describes — enable jumpers off, six logic wires, separate supply, grounds joined — and run the geared motor page’s one-at-a-time test with the wheels off the ground.
One wheel will turn backwards. The two motors face opposite ways on the chassis, so this is expected rather than puzzling. Swap that motor’s two wires at the terminal block.
Find the starting duty cycle too: step up from a standstill until both wheels turn reliably. Often around 60 to 80. Below that they hum and do not move.
Stage 3: the sensors, still on the stand
Set the four trimmers. Then run the sensor page’s display sketch and slide the robot across the line by hand, watching the pattern move: . # . . becoming . . # ..
Confirm which physical head is which, by covering them one at a time. The ribbon can be plugged in either way round, and a reversed array makes a robot that steers away from the line — which looks like a broken gain and is not.
Confirm the polarity as well. On most of these boards the output is LOW over black; check yours.
Stage 4: position, without motors
Run the position sketch from the sensor page and slide the robot across the line again, watching the number go -3, -1, +1, +3. Check that lifting it off the card gives NO_LINE and not 0.
Stage 5: closed loop, on the stand
The complete sketch, with the wheels still off the ground. Move the line under the robot by hand and watch the wheels respond: the line to the left should slow the left wheel.
If they respond the wrong way, the array is reversed or the motors are swapped. Fix it here, not on the floor.
Stage 6: the floor
Put it down on the easy course, press the button, and tune the gain.
Pin reference
| Device | Pin | Connect to | Notes |
|---|---|---|---|
| L298N | ENA |
Uno D10 |
Left speed, PWM, jumper off |
| L298N | IN1 |
Uno D9 |
Left direction |
| L298N | IN2 |
Uno D8 |
Left direction |
| L298N | IN3 |
Uno D7 |
Right direction |
| L298N | IN4 |
Uno D6 |
Right direction |
| L298N | ENB |
Uno D5 |
Right speed, PWM, jumper off |
| L298N | OUT1, OUT2 |
Left motor | Either way round; corrected in stage 2 |
| L298N | OUT3, OUT4 |
Right motor | |
| L298N | +12V, GND |
Battery pack, and Uno GND |
Grounds joined |
| Sensor | VCC |
Uno 5V |
Board and emitters |
| Sensor | GND |
Uno GND |
|
| Sensor | D1 |
Uno A0 |
Leftmost, used as a digital pin |
| Sensor | D2 |
Uno A1 |
Centre left |
| Sensor | D3 |
Uno A2 |
Centre right |
| Sensor | D4 |
Uno A3 |
Rightmost |
| Button | one leg | Uno D2 |
INPUT_PULLUP, start and stop |
| Button | other leg | Uno GND |
|
| Status LED | Uno D13 |
The onboard one; on means lost |
The complete sketch
// Siratek: a line following robot.
// Proportional steering from a four-head tracking sensor.
// Driver pins. ENA and ENB must be PWM-capable.
const int enA = 10, in1 = 9, in2 = 8; // left
const int enB = 5, in3 = 7, in4 = 6; // right
// The sensor, on the analog pins used as digital inputs.
const int sensorPins[4] = {A0, A1, A2, A3};
const int weights[4] = {-3, -1, 1, 3}; // leftmost to rightmost
const int buttonPin = 2;
const int statusLed = 13;
// ---- Tuned on YOUR course. Start here and adjust. ----
int BASE_SPEED = 120; // slow, until the gain is right
float GAIN = 18.0; // raise until it weaves, then back off
const int MINIMUM_SPEED = 70; // below this the motors hum
const int SEARCH_SPEED = 100;
// ------------------------------------------------------
const unsigned long LOST_GIVE_UP = 1200; // stop searching after this
const int NO_LINE = 99;
const int ALL_DARK = 98;
bool running = false;
int lastSeen = 0;
unsigned long lostSince = 0;
unsigned long lastButton = 0;
void leftMotor(int speed) {
digitalWrite(in1, speed >= 0);
digitalWrite(in2, speed < 0);
analogWrite(enA, abs(speed));
}
void rightMotor(int speed) {
digitalWrite(in3, speed >= 0);
digitalWrite(in4, speed < 0);
analogWrite(enB, abs(speed));
}
void drive(int left, int right) {
leftMotor(constrain(left, -255, 255));
rightMotor(constrain(right, -255, 255));
}
void halt() {
drive(0, 0);
}
int linePosition() {
int total = 0;
int seen = 0;
for (int i = 0; i < 4; i++) {
// LOW over black on most of these boards. Confirm yours in stage 3.
if (digitalRead(sensorPins[i]) == LOW) {
total += weights[i];
seen++;
}
}
if (seen == 0) {
return NO_LINE;
}
if (seen == 4) {
return ALL_DARK; // a junction, the end marker, or lifted off
}
return total / seen;
}
void setup() {
int pins[] = {enA, in1, in2, enB, in3, in4};
for (int i = 0; i < 6; i++) {
pinMode(pins[i], OUTPUT);
}
for (int i = 0; i < 4; i++) {
pinMode(sensorPins[i], INPUT);
}
pinMode(buttonPin, INPUT_PULLUP);
pinMode(statusLed, OUTPUT);
halt();
Serial.begin(9600);
Serial.println(F("Siratek: press the button to start. Press again to stop."));
}
void loop() {
// The button starts and stops, with a crude debounce.
if (digitalRead(buttonPin) == LOW && millis() - lastButton > 400) {
lastButton = millis();
running = !running;
if (!running) {
halt();
Serial.println(F("stopped"));
} else {
lostSince = 0;
Serial.println(F("running"));
// A brief full-power kick, because the motors will not start
// from rest at the speed we actually want.
drive(255, 255);
delay(60);
}
}
if (!running) {
halt();
digitalWrite(statusLed, LOW);
return;
}
int position = linePosition();
if (position == ALL_DARK) {
halt();
running = false;
digitalWrite(statusLed, HIGH);
Serial.println(F("all four dark: junction, end marker, or lifted off"));
return;
}
if (position == NO_LINE) {
digitalWrite(statusLed, HIGH);
if (lostSince == 0) {
lostSince = millis();
}
if (millis() - lostSince >= LOST_GIVE_UP) {
halt();
running = false;
Serial.println(F("line lost: stopped"));
return;
}
// Keep turning the way we last saw it.
if (lastSeen < 0) {
drive(-SEARCH_SPEED, SEARCH_SPEED);
} else {
drive(SEARCH_SPEED, -SEARCH_SPEED);
}
return;
}
// We can see the line.
digitalWrite(statusLed, LOW);
lostSince = 0;
lastSeen = position;
int correction = (int) (GAIN * position);
int left = BASE_SPEED + correction;
int right = BASE_SPEED - correction;
// Do not ask a motor for a speed at which it only hums.
if (left > 0 && left < MINIMUM_SPEED) {
left = MINIMUM_SPEED;
}
if (right > 0 && right < MINIMUM_SPEED) {
right = MINIMUM_SPEED;
}
drive(left, right);
}
Four decisions worth explaining
There is no delay() in the control path. The only one is the starting kick, which happens once when the button is pressed. At walking pace a fifty-millisecond delay is several centimetres driven blind, and that is what a robot sawing across the line usually turns out to be.
NO_LINE and ALL_DARK are separate answers, and neither is zero. A position of 0 means centred. Collapsing either of the other two into it is the bug that drives a robot off a table, and it is an easy one to write by accident.
MINIMUM_SPEED is applied after the correction, not before. The arithmetic happily produces a left speed of 30, at which the motor hums and does nothing while the right wheel drives on — so the robot turns much harder than intended. Raising small positive speeds to the threshold is the fix, and it is why the robot still steers properly at low base speeds.
Losing the line stops the robot rather than searching forever. Persistence for a second, then a stop. A robot that spins in a corner indefinitely is both useless and hard on the gearboxes.
Testing it
Stand first, floor second. Every line in this table is worth doing in order.
| Try this | It should |
|---|---|
| On a stand, slide the line to the left | The left wheel slows |
| On a stand, slide the line right | The right wheel slows |
| On a stand, lift it off the card | Wheels search one way, then stop after a second |
| Cover all four heads | Stop, with the status LED on |
| On the floor, gain too low | Drift off on the curves |
| Raise the gain until it weaves | A gentle regular weave along a straight line |
| Raise it further | A violent oscillation; now you know the limit |
| Back it off, run the course | Follows it, including the curves |
Raise BASE_SPEED by 40 |
Probably oscillates again: re-tune the gain |
| Hold a wheel while it runs | It should not reset. If it does, the supply is the problem |
| Run it on a shiny floor | Likely worse than on card; matte surfaces reflect usefully |
| Run it in direct sunlight | Likely erratic; these are indoor sensors |
The three gain rows are the project. Watching the same robot go from lazy to ideal to unstable because of one number is the clearest demonstration of feedback you can get on a bench, and it costs nothing but a few minutes on the floor.
Making it yours
- Add the full PID. The derivative term damps the weave and lets you run much faster; the integral term pulls out a persistent off-centre bias. Both are real improvements once P is tuned, and neither is worth adding before then.
- Handle junctions. Right now all four dark stops the robot. Counting junctions, or always turning the same way at one, turns a line into a maze.
- Fit encoders. The geared motors have a spare shaft on the other side, exactly for a slotted disc. Then the robot knows how far it has travelled as well as where the line is.
- An obstacle sensor. An HC-SR04 on the front, and the robot stops instead of pushing a chair across the room. A servo under the sensor lets it look around before deciding.
- A wider array. Five or more heads, spaced further apart, sees a sharp corner before it has lost the line.
- Make it wireless. HC-05 Bluetooth to change the gain and base speed while it runs, which turns tuning from a dozen uploads into one session with a phone.
That last one is the practical one. Tuning by editing a constant and re-uploading is slow enough that people stop before they have found the good value.
Troubleshooting
| Symptom | Likely cause | Check or fix |
|---|---|---|
| Steers away from the line | Sensor array reversed | Cover each head and find the order |
| Steers away, order correct | Motor leads swapped | One motor faces the other way; swap its wires |
| Oscillates violently | Gain too high for the speed | Lower the gain, or lower the speed |
| Drifts off on curves | Gain too low | Raise it until a weave appears |
| Follows straights, fails corners | Speed too high for the radius | Slow down, or widen the corner |
| Wheels hum, robot does not move | Below the starting duty cycle | The kick in the sketch; check MINIMUM_SPEED |
| Only full speed, no steering | ENA/ENB jumpers still fitted |
Remove them; they must be on PWM pins |
| Resets when it bumps something | Stall current, near 1 A each | A supply that holds up, grounds joined |
| Drives straight off the card | NO_LINE treated as centred |
They are separate answers for this reason |
| One channel never changes | That head’s height, then its trimmer | Height first; all four must match |
| All four always read the same | Too close to the floor | The emitter is flooding the receiver; raise them |
| Erratic outdoors or by a window | Infrared in daylight | Shade it; these are indoor parts |
| Worse as the batteries drain | Expected | Fresh cells, and re-check the gain |
| Loses the line on the same corner every time | Array too narrow for that radius | Space the heads wider |
| Works on the stand, not on the floor | Weight and friction change everything | Re-tune on the floor; the stand only proves direction |
Sources and attribution
All text, wiring guidance and code in this project are original Siratek work.
- Sensor behaviour, the separate heads, the working height, the
DTandDRmarking, the weighted position number and theNO_LINEdistinction are as set out in A Four-Way Line Tracking Sensor, read from the Siratek product photographs for item 2943. - Driver wiring, the enable jumpers, the PWM pin requirement and the bridge voltage drop are as set out in Driving DC Motors with an L298N.
- The motor mismatch, the stall current near 1 A, the starting duty cycle and the spare shaft for encoders are as set out in Geared Motors and Wheels for a Robot. Product 7406 is one motor and wheel; this project needs two.
- The proportional controller, the pin reassignment to
A0–A3, the search behaviour and theMINIMUM_SPEEDhandling are Siratek’s own design decisions for this project.
Gain, base speed and the starting duty cycle depend on the chassis, the surface and the batteries, so the tutorial gives a tuning procedure rather than values to copy. This project has not yet been built and run on hardware. No third-party tutorial text, diagrams, photographs or code has been reproduced here.

