Tutorial · Intermediate · 1.5 hours
Wall Following and Precise Turns for a Maze Robot
Keep a maze robot centred between walls with a PID loop on its side sensors, and turn exactly 90 degrees using a gyro instead of counting encoder ticks.
Published
Introduction
A maze-solving algorithm assumes the robot is in the cell it thinks it is in. Flood fill computes a perfect route, and then the robot clips a wall on the third turn and every subsequent decision is made about the wrong cell.
So the algorithm is rarely what fails. What fails is motion: staying centred in a corridor, and turning exactly ninety degrees. This tutorial is about those two, and about why the obvious approach to each is the wrong one.
Why dead reckoning alone will not do it
The tempting approach is to drive by encoder counts alone: one cell is N ticks, a quarter turn is M ticks, and the maze is a grid, so just count.
It fails because the errors accumulate and never self-correct. Wheels slip on acceleration. The two motors are not identical. The wheelbase you measured is a millimetre off, so every turn is 88° instead of 90°. Each error is small; none of them cancel. Twenty cells in, the robot is driving diagonally into walls with complete confidence.
The fix is to use the maze itself as a reference. The walls are straight, parallel, and at known spacing — a free, drift-proof measurement, available continuously.
Wall following as a PID loop
With a distance sensor on each side, centring is a control problem you have already solved once in the line follower: compute an error, drive it to zero.
// Positive error = too close to the left wall = steer right.
float error = leftDistance - rightDistance;
float correction = Kp * error + Kd * (error - previousError);
previousError = error;
int base = 120;
setMotors(base + correction, base - correction);
The correction is applied differentially — added to one wheel, subtracted from the other — so the robot’s forward speed stays roughly constant while its heading changes. That is the same structure as a line follower; only the error term is different.
Start with Kd at zero, raise Kp until the robot tracks the corridor while weaving slightly, then add Kd until the weave damps out. Ki is almost never useful here: a maze corridor is short, and integral action mostly adds lag and overshoot at each cell boundary.
The case the naive loop gets wrong
A maze has openings. The moment a side wall ends, that sensor reads the distance to something far away — the opposite wall of an open cell, or nothing at all — and leftDistance - rightDistance becomes a huge bogus error. The robot lurches into the gap it was supposed to drive past.
Check for wall presence before trusting the difference:
const int WALL_THRESHOLD = 100; // mm — beyond this there is no wall
bool leftWall = leftDistance < WALL_THRESHOLD;
bool rightWall = rightDistance < WALL_THRESHOLD;
float error;
if (leftWall && rightWall) {
error = leftDistance - rightDistance; // centre between both
} else if (leftWall) {
error = 2 * (leftDistance - IDEAL_SIDE); // hold one wall at a set distance
} else if (rightWall) {
error = 2 * (IDEAL_SIDE - rightDistance);
} else {
error = 0; // open cell — hold heading instead
}
The one-sided cases double the gain because a single wall gives you half the information: with two walls, drifting 5 mm left changes the difference by 10 mm; with one, it changes by 5.
In the no-wall case, falling back to error = 0 drives straight as far as the motors are concerned — which is not straight in reality. That is where the gyro takes over.
Turning exactly 90 degrees
Counting encoder ticks through a turn is the natural first attempt and it is not accurate enough. A pivot turn slips more than straight driving, and a 2° error per turn is 20° after ten turns — a robot facing diagonally down a corridor.
Integrate the gyro instead. The MPU6050’s Z-axis rate is a direct measurement of how fast the robot is rotating, and it does not care whether the wheels slipped:
float turnDegrees(float target) {
float heading = 0;
unsigned long last = micros();
while (fabs(heading) < fabs(target)) {
unsigned long now = micros();
float dt = (now - last) / 1000000.0;
last = now;
heading += (readGyroZ() - gyroBias) * dt; // bias subtraction is essential
int speed = (target > 0) ? TURN_SPEED : -TURN_SPEED;
setMotors(speed, -speed); // pivot in place
}
setMotors(0, 0);
return heading; // report the real angle achieved
}
Three things make or break this:
Subtract the gyro bias. An uncalibrated gyro reads a degree or two per second while stationary, and you are integrating it. Measure the average while still, exactly as in the tilt tutorial, and subtract it every reading.
Stop early and coast. The robot has rotational momentum, so cutting the motors at exactly 90° overshoots. Stop at about 85° and let it coast in — or better, ramp the turn speed down as the remaining angle shrinks.
Re-square against the front wall. When a turn puts a wall in front of the robot, that wall is a perfect 90° reference. Drive up to it slowly and let the wall-following loop straighten the robot against it, which zeroes accumulated heading error for free. Micromouse builders do this at every opportunity, because it turns a drifting robot into a self-correcting one.
Knowing when a cell has passed
Cell counting is what connects motion to the maze algorithm. Encoder distance gets you approximately there; the walls tell you exactly.
The most reliable trigger is the falling edge of a side wall. As the robot passes a cell boundary, a side sensor that was reading a wall suddenly reads far — that transition happens at a known point in the cell, and it is a physical landmark rather than an accumulated estimate.
Combine the two: use encoders to predict the boundary, then let the wall edge confirm or correct it. When they disagree, believe the wall.
Verify before you let it solve
Four tests, on the real maze, in this order:
- Straight corridor, ten cells. The robot should stay centred and arrive still centred. Weaving means
Kdis too low; drifting to one side means a sensor offset — measure both side sensors against the same wall and correct the difference in software. - One-sided corridor. Remove a wall. The robot must keep going straight rather than lurching into the gap.
- Ten turns in a row. Turn 90° ten times and check that the robot ends up facing its original direction. More than a few degrees out and your gyro bias or coast-down needs work.
- Re-squaring. Drive at a wall, let it square up, and confirm the heading error is gone afterwards.
Only when those four pass is it worth running a maze algorithm on top — a planner given bad position data produces confident nonsense. Watch the planning layer on its own in the maze solver simulator, then put both together in the micromouse project.
Explore the graph
Part of these builds
Projects and learning paths that include this tutorial.
Further reading