Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

hammerHead

An autonomous line-maze solving robot built on an Arduino Nano. It explores an unknown maze using the left-hand rule, records every decision it makes, collapses dead-end excursions into shortcuts as it goes, and saves the solved path to EEPROM so it survives a power cycle.

Version 4.2.4.2 · Full case study →


How it works

Two layers, and keeping them separate is the whole design.

The inner loop is continuous control. A 5-channel IR array reads the line, a weighted average turns those five readings into a single position error, and a PID controller converts that error into a differential correction — added to one wheel, subtracted from the other.

The outer layer is discrete. Every cycle, loop() classifies what the array is looking at. If it sees something other than plain line, it interrupts PID and runs a manoeuvre instead:

readSensor()
  ├── finishDetected()   → record 'F', save to EEPROM, halt
  ├── junctionDetected() → handleJunction()  (Left > Straight > Right)
  ├── deadEndDetected()  → record 'U', turnBack()
  └── else               → followLine()      (PID)

Checks run in that order deliberately: finish first because stopping is irreversible, then junctions, then dead ends.


Path optimisation

This is the part worth reading the code for.

Every dead-end excursion leaves a three-move signature in the recorded path: the turn taken into the branch (X), the U-turn at the dead end (U), and the turn taken on the way back out (Y). A robot that already knew the maze would have made a single move instead of those three.

Most implementations hardcode a substitution table — LUL→S, LUR→U, SUS→U, and so on. reducePath() derives all of them from one formula instead, by treating each move as a rotation:

Move Angle
S straight
L left 90°
U U-turn 180°
R right 270°
combined = (angle(X) + 180 + angle(Y)) mod 360

LUL → 90 + 180 + 90 = 360 ≡ 0° → S. Turned left, hit a dead end, turned left again — the net effect is driving straight through that junction, so that's what gets stored.

Reduction runs live, on every recorded move, not as a post-processing pass. And because collapsing one triple can leave a new U adjacent to another pair, reducePath() loops until no triples remain: R U (S U S)R U US.

The result is that by the time the robot reaches the finish, the path in memory is already the solved shortest route — no separate solving step needed.


Hardware

Subsystem Part
Controller Arduino Nano (ATmega328P)
Line sensor 5-channel IR reflectance array
Motor driver TB6612FNG dual H-bridge
Motors 2 × N20 micro metal gear motor
Input Normally-open pushbutton (replay trigger)

Pin map

Signal Pin Notes
S1S5 A0A4 IR array, S1 = leftmost
PWMA D5 Right motor speed
AIN1 / AIN2 D3 / D4 Right motor direction
PWMB D9 Left motor speed
BIN1 / BIN2 D7 / D8 Left motor direction
replayButton D10 INPUT_PULLUP — pressed reads LOW
LED_BUILTIN D13 Lit while a junction is being handled

Note the A/B mapping: driver channel A drives the right motor, channel B the left.

Sensor polarity is 0 = black, 1 = whiteanalogRead() below threshold counts as black.


Tuning parameters

All live at the top of hammerHead.ino and followLine.ino.

Parameter Value What it does
Kp 50 Proportional gain
Ki 0.0 Integral gain — unused; error is never meant to be steady on a maze
Kd 5 Derivative damping
threshold 500 Analog cutoff between black and white
baseSpeed 150 Cruise PWM
maxSpeed 200 PID output clamp
ovsDelay 360 ms Aligns the rotation centre with the junction before pivoting
ignoreJunction 120 ms Blanking window after a turn, stops re-detecting the junction just left
maxPath 100 Maximum recorded moves

Speed calibration: speed (mm/s) = 1.12 × PWM − 14.10

Tuning order

  1. Set Ki = Kd = 0, raise Kp until it tracks but oscillates.
  2. Add Kd until the oscillation damps out.
  3. Leave Ki at 0 unless there's a persistent drift to one side.
  4. Raise baseSpeed and repeat — gains that hold at 150 may not hold at 200.

Detection details

Position error. Sensors are weighted S1 = −2, S2 = −1, S3 = 0, S4 = +1, S5 = +2 and divided by the number of channels seeing black, giving a continuous error even when the line falls between two sensors.

Line lost. If no sensor sees black, the robot spins in place toward the side it last drifted from — previousError decides the search direction rather than guessing.

Junction. Two or more of {left, straight, right} open at once. handleJunction() applies a stricter test than the initial detection: a real side branch is a perpendicular crossbar, so it must cover both sensors on that side (s1 && s2, or s4 && s5). A skewed pass over a straight line can put a single outer sensor on the line and look like a branch — requiring the pair rejects that, so false junctions resolve to "straight only" and record nothing.

Dead end. All five white, confirmed by nudging forward 200 ms and re-reading.

Finish vs. junction. Both read all-black the instant the array crosses them, so they can only be told apart by what happens next. finishDetected() creeps forward at half speed for up to 500 ms, sampling every 25 ms. A junction crossbar is narrow — the array exits black almost immediately. A finish box is wide enough that the robot is still inside it when the timer expires.

Only real decisions get recorded. A corner with one available option is followed but not written to the path — it isn't a choice, and recording it would corrupt the reduction arithmetic.


Motion smoothing

rampTo(targetLeft, targetRight, duration) steps from the current commanded speed to the target in 10 ms increments rather than jumping. Every junction manoeuvre uses it. A hard step from forward to full reverse produces a torque spike that makes the wheels slip and throws off the turn geometry — ramping costs a few tens of milliseconds and removes the problem.


Repository layout

hammerHead/
├── hammerHead.ino          Pin definitions, globals, setup(), loop()
├── sensing.ino             readSensor() and all detection predicates
├── followLine.ino          PID controller
├── junctionHandling.ino    handleJunction(), turnLeft/Right/Back, spinUntilLine()
├── motorControl.ino        motorControl(), stopMotors(), rampTo()
├── savingPath.ino          recordMove(), reducePath(), EEPROM persistence
└── callGraph_V4.2.4.2.svg  Function call graph

Arduino concatenates .ino files with the main sketch first, which is why path[], pathLength and maxPath are declared in hammerHead.ino — unlike functions, globals get no auto-generated forward declarations.


Build & upload

Requirements: Arduino IDE (or arduino-cli), EEPROM library (bundled with the AVR core).

  1. Clone this repo.
  2. Open hammerHead/hammerHead.ino — the IDE picks up the other .ino files automatically.
  3. Select Board: Arduino Nano, and the right Processor (ATmega328P, or Old Bootloader depending on your clone).
  4. Upload.
  5. Open Serial Monitor at 115200 baud.

On startup the robot prints any path stored in EEPROM from the previous run, then clears its working copy and begins a fresh exploration. EEPROM is only overwritten when a new finish is reached.


Status

Working: exploration, left-hand rule navigation, live path reduction, EEPROM persistence.

Not yet implemented: the replay run. The pushbutton on D10 is wired and read (isReplayButtonPressed()), but nothing acts on it. The plan is to drive normally via followLine() and, at each junction, take the next move from the solved path instead of applying the left-hand rule.

Ideas beyond that

  • Wheel encoders — turn and confirm-forward distances currently depend on timed delays, which drift as the battery discharges.
  • Serial telemetry of position error, for plotting instead of tuning by eye.
  • Separate 5 V regulator for logic, so the current spike from a U-turn can't brown out the Nano.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages