Arduino Learning Studiorobotics pathway

R14-U1 · usually age 14 · Robotics Engineers

Distance, Speed and Odometry

How far did the robot really travel?

Pilot unit plan · 5 sessions · not yet classroom-validated

Build: Encoder-equipped mobile robot

The rover learns to measure its own travel: a slotted disc and a photo interrupter on each wheel, counted by interrupts on D2 and D3 (so STOP moves to A0 and STBY to D8), the distance per count from a measured wheel, speed from counts per interval, and how far measured travel differs from what was expected, and why.

  • Sense
  • Test and debug

Who it is for

Age is a starting point, never a limit. A learner who can show what this unit starts from can begin here at any age.

How they learn
By reading and changing real Arduino sketches, predicting, testing in a model, then building and testing the real circuit.
The adult's role
An adult checks every circuit and anything that moves before it is powered.
Session
75–90 minutes
Hardware
Arduino Uno R3 or compatible (5 V). The adult checks the power circuit and the motor driver. Motors never run from a pin; share GND between supplies.

Starts from

Builds, and what it is the input for

In the studio today

The labs run a model of each sketch in the browser. They prepare the real build; they never count as a working circuit.

Sessions

  1. Session 1 · 90 min

    Where do the counts come from?

    The encoder: a slotted disc that turns with each wheel, and a photo interrupter (a Sharp GP1A57HRJ00F on SparkFun's breakout: PWR, GND and SIG) whose beam the disc's bars block. SIG goes LOW when the beam is blocked and HIGH through a slot: the datasheet gives the high level as at least 4.9 V and the low level as at most 0.4 V (at 16 mA), with the sensor on the board's 5V (its operating supply is 4.5–17 V). The output drives itself both ways (the datasheet's diagram shows a 15 kΩ resistor in its output stage): it is not open-collector, so pinMode(pin, INPUT) is enough, with no pull-up. It also means a pin set as an OUTPUT and HIGH would be driven against the sensor: a short. So SIG is wired only after a sketch that leaves pins 2 and 3 as inputs is on the board. On the UNO only pins 2 and 3 work with attachInterrupt(), and the rover already uses both (STOP on D2, STBY on D3), so this unit re-plans the pins: encoders D2 (left) and D3 (right), STBY to D8, STOP to A0, an analog pin used as a digital input with its pull-up. Not D13: its on-board LED makes it a poor input.

    In the studio

    Hands-on

    In Steer with two motors, find STBY in the driver's table: what do both wheels do while it is LOW? (The lab's wiring view still shows STBY on D3; on this rover it moves to D8.) The studio's labs still put STBY on D3 and STOP on D2: never paste a lab's pin lines into the rover's sketch; take every pin from the plan's pin table. Before the session the adult solders each photo interrupter, its 220 Ω resistor and a 0.1 µF capacitor across PWR and GND onto its breakout, then mounts a slotted disc so it turns with each wheel, and a breakout so the disc passes through its gap without touching, at least 4 mm down into it, with the open side away from windows and lamps. With the motor battery switched off and unplugged and USB unplugged, check that each disc turns freely through its gap. Still unpowered, follow the pin table: move the STOP button's wire from D2 to A0 and the driver's STBY wire from D3 to D8, then wire each breakout's PWR to the 5V rail and GND to GND, and leave both SIG wires unplugged for now. Change your sketch: STBY to 8, STOP to A0 with pinMode(A0, INPUT_PULLUP), pinMode(2, INPUT) and pinMode(3, INPUT), and print digitalRead(2) and digitalRead(3). After the adult's check, plug in USB only and upload it (or Bare Minimum, which sets no pins, if your sketch is not ready): the old sketch on the board still sets D3 as an OUTPUT. Unplug USB, then wire SIG to D2 (left) and D3 (right), routed away from the motor leads. Plug USB in again and turn each wheel slowly by hand: the reading must change from HIGH to LOW and back as bars and slots pass.

    Starting code: The learner's own sketch that drives the rover (from R13-U3 or the Rescue Rover course), with STBY set to 8, STOP read from A0, pins 2 and 3 set as INPUT, and the HC-SR04 reading taken out, its Trig pin still set as an OUTPUT and written LOW in setup() so D9 never floats.

    Pins

    D2
    left wheel encoder SIG (photo interrupter breakout)
    D3
    right wheel encoder SIG (photo interrupter breakout)
    D4
    driver AIN1 (left motor)
    D5
    driver PWMA (left motor)
    D6
    driver PWMB (right motor)
    D7
    driver AIN2 (left motor)
    D8
    driver STBY moved from D3
    D9
    HC-SR04 Trig (wired, not read; kept OUTPUT and LOW)
    D10
    HC-SR04 Echo (wired, not read in these sketches)
    D11
    driver BIN2 (right motor)
    D12
    driver BIN1 (right motor)
    A0
    STOP button (other side to GND) moved from D2

    Evidence to collect

    The rover's new pin table, checked against the wiring by the learner, and a short table per wheel: disc position (bar or slot in the gap) → reading, with the STOP button tested on A0.

    Predict
    Before turning a wheel, predict what digitalRead(2) prints while a bar sits in the gap, and while a slot does.
    Debug
    If a reading never changes, unplug USB and check that breakout's SIG wire, then that the disc really passes through the beam.
    Reflect
    Why did STOP and STBY have to move, and why not to D13?

    ⚠ Adult checks before power-on: The motor battery's power-off switch is off and its holder unplugged: no motor is driven in this session. The motor battery reaches the driver's VM only, through its power-off switch; the board's 5V goes to the driver's VCC only; every GND is joined. Before every upload: setup() never sets pin 2 or 3 as OUTPUT (the encoders drive those pins). Before USB first goes in: each breakout's PWR on 5V and GND on GND, not reversed, its capacitor across them; both SIG wires unplugged; STOP from A0 to GND, never across 5V and GND; STBY on D8; nothing else on D2, D3, D8 or A0. SIG goes in only after the upload, unpowered, to D2 or D3 only. Chassis tools: screwdrivers pushed away from hands; side-cutters cut cable ties flush, eye protection on, sharp ends turned in; hot glue low-temperature or adult-only (it burns); soldering or drilling adult-only. No wire is moved, pulled or plugged in while anything is powered: power-off switch off and battery unplugged first, then USB.

    Works towards: Odometry

  2. Session 2 · 90 min

    Why count with an interrupt?

    Polling vs interrupts: loop() reading D2 can miss a slot while it is busy printing or waiting, but an interrupt runs a short function, the ISR, the moment the pin rises: attachInterrupt(digitalPinToInterrupt(2), countLeft, RISING), and the same on pin 3 for countRight. Each ISR only adds 1 to its counter: the countLeft ISR adds 1 to leftCount, the countRight ISR adds 1 to rightCount. A variable shared with an ISR is declared volatile: volatile unsigned long leftCount = 0; volatile unsigned long rightCount = 0. An unsigned long is 4 bytes and the UNO's ATmega328P is an 8-bit microcontroller, so loop() reads it in several steps, and an edge in between could give a value that is half old, half new. So loop() copies both counts with interrupts off (noInterrupts(), copy, interrupts()) and works only with the copies. Keep the ISRs that short: inside an ISR millis() does not advance and delay() does not work.

    In the studio

    No studio lab: this session is off-screen.

    Hands-on

    Start from your session 1 sketch. Write the two counters and ISRs and attach them with RISING. In loop(), every 100 ms by millis(), copy the counts with interrupts off and print them beside a polled count: loop() itself reads D2 and adds 1 to its own polled counter when it sees LOW then HIGH. Upload, open the Serial Monitor before the motor battery goes on (opening it restarts the UNO), lift the rover, wheels in the air, then switch the motor battery on. Run the left wheel at the lowest value that starts it, then at 160 of 255, ten seconds each, and compare the interrupt count with the polled one. Then put a deliberate delay(50) in loop() and run both speeds again.

    Starting code: The learner's session 1 sketch, plus counting lines modelled on attachInterrupt()'s reference: a volatile counter, a short ISR and attachInterrupt(digitalPinToInterrupt(pin), ISR, mode).

    Pins

    D2
    left wheel encoder SIG (photo interrupter breakout)
    D3
    right wheel encoder SIG (photo interrupter breakout)
    D4
    driver AIN1 (left motor)
    D5
    driver PWMA (left motor)
    D6
    driver PWMB (right motor)
    D7
    driver AIN2 (left motor)
    D8
    driver STBY moved from D3
    D9
    HC-SR04 Trig (wired, not read; kept OUTPUT and LOW)
    D10
    HC-SR04 Echo (wired, not read in these sketches)
    D11
    driver BIN2 (right motor)
    D12
    driver BIN1 (right motor)
    A0
    STOP button (other side to GND) moved from D2

    Evidence to collect

    A table: the wheel's setting, with and without the delay(50) → the interrupt count and the polled count over ten seconds, and a sentence on which count to trust and why; the sketch's volatile declarations and its copy with interrupts off, explained in the learner's words.

    Predict
    Which count will be lower, the polled one or the interrupt one, and when will the gap grow?
    Debug
    If the interrupt count stays at 0, check the pin number inside digitalPinToInterrupt and that the counter you print is the one the ISR changes.
    Reflect
    What could go wrong if loop() read leftCount while the ISR was changing it, and how does the copy prevent it?

    ⚠ Adult checks before power-on: Before every upload: setup() never sets pin 2 or 3 as OUTPUT (the encoders drive those pins). The motor battery reaches the driver's VM only, through its power-off switch; the board's 5V goes to the driver's VCC only; every GND is joined. Lifted, wheels in the air, for every run in this session; the power-off switch in reach. Fingers and hair clear of wheels and discs while the motor battery is on. Lift the rover (wheels in the air) before switching the motor battery back on: the sketch keeps running on USB, so the wheels take its last command at once.

    Works towards: Odometry

  3. Session 3 · 80 min

    How far is one count?

    Distance per count: the wheel's circumference (π × diameter, or measured by rolling it) ÷ the counts in one wheel turn. Both are measured, not looked up: the disc's slots and any gearing between the disc and the wheel are inside the count. One count is the smallest step the rover can measure, and two ways of measuring the circumference rarely agree exactly: that difference is a measurement error.

    In the studio

    No studio lab: this session is off-screen.

    Hands-on

    With the motor battery switched off and unplugged, upload your session 2 sketch with USB only. Put a chalk or tape mark on each tyre. Turn the left wheel slowly by hand exactly ten turns and note the count; repeat three times, then do the right wheel. Measure each wheel's diameter with a ruler and work out π × diameter; then roll the rover slowly by hand along a tape measure for ten wheel turns and divide the distance by ten. Compare the two circumferences and choose one, with a reason. Work out the cm per count for each wheel.

    Starting code: The learner's session 2 sketch, unchanged.

    Pins

    D2
    left wheel encoder SIG (photo interrupter breakout)
    D3
    right wheel encoder SIG (photo interrupter breakout)
    D4
    driver AIN1 (left motor)
    D5
    driver PWMA (left motor)
    D6
    driver PWMB (right motor)
    D7
    driver AIN2 (left motor)
    D8
    driver STBY moved from D3
    D9
    HC-SR04 Trig (wired, not read; kept OUTPUT and LOW)
    D10
    HC-SR04 Echo (wired, not read in these sketches)
    D11
    driver BIN2 (right motor)
    D12
    driver BIN1 (right motor)
    A0
    STOP button (other side to GND) moved from D2

    Evidence to collect

    A table of counts for ten turns (three tries per wheel), both circumferences and how each was measured, their difference, the cm per count for each wheel with the working shown, and a prediction: the counts for 100 cm.

    Predict
    Will the ruler or the rolled circumference be larger, and by about how much?
    Debug
    If the three counts for ten turns differ by more than a few, check that the disc is tight and does not slip on its shaft.
    Reflect
    Which part of your cm per count are you least sure of, and why?

    ⚠ Adult checks before power-on: The motor battery's power-off switch is off and its holder unplugged: no motor is driven in this session. The motor battery reaches the driver's VM only, through its power-off switch; the board's 5V goes to the driver's VCC only; every GND is joined. Before every upload: setup() never sets pin 2 or 3 as OUTPUT (the encoders drive those pins). Hands clear of the discs; the wheels are turned only by hand, slowly.

    Works towards: Odometry, Measuring error

  4. Session 4 · 90 min

    How fast is it going?

    Speed from counts: every 100 ms (a millis() timer), the counts since the last sample × cm per count ÷ 0.1 s gives cm/s. A speed table: analogWrite() value → each wheel's speed, lifted and on the floor, so the two wheels can be compared at the same setting. The table is the data the controller in R14-U2 is tuned from. PWMA and PWMB are on pins 5 and 6, where the analogWrite() reference warns that low values (e.g. 0 - 10) give a higher duty cycle than set, because millis() shares their timer; the AVR core's analogWrite() writes the pin LOW for 0, so 0 is still off. Measure each start value: never assume it.

    In the studio

    Hands-on

    In Control a motor, predict the shape of a speed table: below which value does the model motor not start? The studio's labs still put STBY on D3 and STOP on D2: never paste a lab's pin lines into the rover's sketch; take every pin from the plan's pin table. On the rover, start from your session 3 sketch and print one line every 100 ms: the time from millis(), the left and right counts in that interval and both speeds in cm/s. Upload, open the Serial Monitor before the motor battery goes on (opening it restarts the UNO), lift the rover, wheels in the air, then switch the motor battery on. Step both wheels' value up in tens from the lowest value that starts them to 160 of 255, noting each wheel's speed. Then on the floor, never above 160 of 255, drive straight at three values from your table, each run ending by itself after 3 s by millis() with a short brake, and note the speeds and each wheel's floor start value, with the USB cable held clear by the adult.

    Starting code: The learner's session 3 sketch, printing one line every 100 ms.

    Pins

    D2
    left wheel encoder SIG (photo interrupter breakout)
    D3
    right wheel encoder SIG (photo interrupter breakout)
    D4
    driver AIN1 (left motor)
    D5
    driver PWMA (left motor)
    D6
    driver PWMB (right motor)
    D7
    driver AIN2 (left motor)
    D8
    driver STBY moved from D3
    D9
    HC-SR04 Trig (wired, not read; kept OUTPUT and LOW)
    D10
    HC-SR04 Echo (wired, not read in these sketches)
    D11
    driver BIN2 (right motor)
    D12
    driver BIN1 (right motor)
    A0
    STOP button (other side to GND) moved from D2

    Evidence to collect

    A speed table (value → left and right counts per 100 ms and speed in cm/s, lifted and on the floor), the start value of each wheel, lifted and on the floor, and a sentence on which wheel is faster at the same setting and what that would do to a straight run.

    Predict
    At the same analogWrite() value, will both wheels give the same speed? Which will be faster?
    Debug
    If a speed jumps between two values at a steady setting, look at the counts per interval: one count more or less is the smallest step.
    Reflect
    Was the floor speed lower or higher than the lifted speed at the same value, and why?

    ⚠ Adult checks before power-on: Before every upload: setup() never sets pin 2 or 3 as OUTPUT (the encoders drive those pins). The motor battery reaches the driver's VM only, through its power-off switch; the board's 5V goes to the driver's VCC only; every GND is joined. Lifted for the first run of every new sketch; then the first run on the floor at the lowest value that starts both wheels, never above 160 of 255; later runs stay within that ceiling, on a clear floor with the power-off switch in reach. Each floor run ends by itself after 3 s: check that line before the upload. The USB cable is held clear of the wheels by the adult. Fingers and hair clear of wheels and discs while the motor battery is on. Lift the rover (wheels in the air) before switching the motor battery back on: the sketch keeps running on USB, so the wheels take its last command at once.

    Works towards: Odometry

  5. Session 5 · 90 min

    How far did it really go, and how wrong were we?

    Expected vs measured travel: drive until the counts say 100 cm, brake, and measure with a tape. Each wheel's distance is its counts × its own cm per count. Error = measured − expected, in cm and as a percentage of the expected distance; the spread (largest minus smallest) over five trials. Where error comes from: the circumference, one count's size, wheel slip, the rover rolling on after the brake, one wheel faster than the other. A trial far from the others is a failure to analyse from its log before anything changes. A drive that ends on a count has a new failure: if a SIG wire comes loose or a disc slips, the counts stop and the rover would run on, so the stall rule ends every driving state. One encoder channel counts slots, not direction: a count after the brake may be the wheel rolling on or rolling back.

    In the studio

    No studio lab: this session is off-screen.

    Hands-on

    Start from your session 4 sketch: drive forward until the average of the two wheels' distances (each wheel's counts × its own cm per count) reaches 100 cm, then short-brake. Add the stall rule to the drive state, with each wheel's floor start value from your table and the drive's measured time (100 cm ÷ your floor speed until a good run is logged), and print the state and the reason whenever it changes. Upload and open the Serial Monitor before the motor battery goes on (opening it restarts the UNO). Test the stall rule safely first: motor battery off, on USB only, start the drive: the counts stay at 0, so the rule must go to STOPPED within 5 samples, and the Serial Monitor must show it. Then lift the rover, wheels in the air, switch the motor battery on and run it lifted once. Mark a start line and put the rover's front edge on it each time. Then run five trials on the floor at one value, never above 160 of 255, with the USB cable held clear by the adult: measure where the front edge stops and log the final counts. Work out each error, the mean error and the spread. Then run five more, printing the counts again a second after the brake: how many counts came after the brake command, and which way could the wheel have turned?

    Starting code: The learner's session 4 sketch, set to brake at the count for 100 cm.

    Pins

    D2
    left wheel encoder SIG (photo interrupter breakout)
    D3
    right wheel encoder SIG (photo interrupter breakout)
    D4
    driver AIN1 (left motor)
    D5
    driver PWMA (left motor)
    D6
    driver PWMB (right motor)
    D7
    driver AIN2 (left motor)
    D8
    driver STBY moved from D3
    D9
    HC-SR04 Trig (wired, not read; kept OUTPUT and LOW)
    D10
    HC-SR04 Echo (wired, not read in these sketches)
    D11
    driver BIN2 (right motor)
    D12
    driver BIN1 (right motor)
    A0
    STOP button (other side to GND) moved from D2

    Evidence to collect

    The unit's evidence: compare expected and measured travel. A trial table of ten trials (expected distance, measured travel, error in cm and in per cent, counts after the brake), the mean error and the spread; the error sources ranked by the learner's own numbers; one failed or odd trial analysed with its log; and the stall test's log analysed as a fault (the samples, the counts, the state and the reason printed).

    Predict
    Will the rover go too far or stop short of 100 cm, and by about how many centimetres?
    Debug
    If the stall test never reaches STOPPED, stop there: print each wheel's count and value per sample and check the rule. If one trial is far from the others, read its log before changing anything: did a wheel slip, or did the counts stop early?
    Reflect
    Which error source was largest, and what one change would shrink it?

    ⚠ Adult checks before power-on: Before every upload: setup() never sets pin 2 or 3 as OUTPUT (the encoders drive those pins). The motor battery reaches the driver's VM only, through its power-off switch; the board's 5V goes to the driver's VCC only; every GND is joined. Lifted for the first run of every new sketch; then the first run on the floor at the lowest value that starts both wheels, never above 160 of 255; later runs stay within that ceiling, on a clear floor with the power-off switch in reach. The stall test has reached STOPPED before any run with the motor battery on. The stall rule: in every driving state, go to STOPPED if a wheel's count is unchanged for 5 samples while its value is above its measured start value, or if the segment runs over twice its measured time. The USB cable is held clear of the wheels by the adult. Fingers and hair clear of wheels and discs while the motor battery is on. Lift the rover (wheels in the air) before switching the motor battery back on: the sketch keeps running on USB, so the wheels take its last command at once.

    Works towards: Odometry, Measuring error

Materials

  • The rover from R13 (Uno, TB6612FNG, two gear motors, HC-SR04, STOP button; motor battery: 4 × AA alkaline cells (1.5 V each, about 6 V fresh) in a holder whose on/off switch is the power-off switch)
  • Not in the H2 kit list, added for this unit: two slotted encoder discs that turn with the wheels (on the wheel's hub, or on the motor's second shaft end if it has one), with slots and bars each wider than the sensor's 1.8 mm slit and thin enough for its 10 mm gap
  • Two Sharp GP1A57HRJ00F photo interrupters, each soldered by the adult onto a SparkFun Photo Interrupter Breakout (BOB-09322) with a 220 Ω resistor; small brackets, standoffs or cable ties to mount them
  • Two 0.1 µF ceramic capacitors, one soldered by the adult across each breakout's PWR and GND (the datasheet's design guide asks for more than 0.01 µF between VCC and GND near the device)
  • A ruler, a tape measure, masking tape and chalk
  • A computer with the Arduino IDE and a long USB cable for the tether

⚠ Safety before any session

  • Motors run from their own battery through the driver, never from a pin; every GND is joined.
  • To rewire, the encoders included: switch off and disconnect the motor battery first, then unplug USB. Never move a wire while powered.
  • Wheels lifted for the first run of every new sketch; then on the floor never above 160 of 255.
  • Lift the rover (wheels in the air) before switching the motor battery back on: the sketch keeps running on USB, so the wheels take its last command at once.
  • Fingers and hair clear of wheels and discs while the motor battery is on. Turn a wheel by hand only with it switched off and unplugged.
  • The USB cable is held clear of the wheels by the adult.
  • Chassis tools: screwdrivers pushed away from hands; side-cutters cut cable ties flush, eye protection on, sharp ends turned in; hot glue low-temperature or adult-only (it burns); soldering or drilling adult-only.
  • Motor battery: AA alkaline cells only; no NiMH, Li-ion, LiPo, RC packs or 18650 cells.
  • If anything gets warm: switch off or unplug at the holder, don't touch it, tell the adult.

Notes for the adult

  • The H2 kit list has no wheel encoders (encoders appear in H3's kit), so this unit adds two. They are optical, not magnetic: the safety baseline bans loose small magnets. Before session 1, solder the photo interrupters, resistors and capacitors onto their breakouts and mount the discs and breakouts on the rover, so the session starts with checking and wiring.
  • The encoder's output is not open-collector: the datasheet's internal connection diagram shows a 15 kΩ resistor in its output stage, and its high level (VOH) is at least 4.9 V with VCC = 5 V, so pinMode(pin, INPUT) is enough. It also pulls LOW when the beam is blocked, so a pin set as an OUTPUT and HIGH would be driven against it, and the labs' sketches (and rover sketches built from them) set stbyPin = 3 as an OUTPUT. That is why the new-pin sketch (or Bare Minimum) is uploaded before any SIG wire goes in, and why you check every sketch before it is uploaded: setup() never sets pin 2 or 3 as OUTPUT.
  • From this unit on the rover's pins change: encoders D2 and D3 (the only pins attachInterrupt() can use on the UNO), STBY D8, STOP A0. The studio's labs still show STBY on D3 and STOP on D2; on this rover, follow the plan's pin table.
  • The studio's labs still put STBY on D3 and STOP on D2: never paste a lab's pin lines into the rover's sketch; take every pin from the plan's pin table.
  • The stall rule is the unit's runaway guard: a loose SIG wire or a slipped disc stops the counts, and a sketch that drives until a count would otherwise run on. It is tested with the motor battery off before any floor run, from session 5 on.
  • Interrupts have costs, said honestly: an ISR delays loop() a little on every edge, so keep it to adding 1; while interrupts are off for the copy, an edge waits until interrupts() (keep that copy to two lines); and the AVR core notes that pulseIn() works better with interrupts off, so an HC-SR04 reading taken while the encoders count can come out short. These sketches do not read the HC-SR04.
  • Each session lists a line to predict before power-on, a first check when it goes wrong, and a question to reflect on before tidying up.
  • Before the first upload, test an upload on each computer (Blink, with its own board and cable), so the session starts on time.
  • Learners who are ready can start here at any age once they can drive a rover from code and analyse a failure with data. Readiness decides, not age.
  • No studio lab models encoders yet; the motor lab is used only to predict the shape of the speed table. The rover's own counts and the tape measure are the evidence.

Planned studio features

Not built yet. The unit runs without them.

  • An encoder/odometry lab: a slotted disc turning with a wheel, its counts with polling and with an interrupt, and distance per count from a measured wheel.
  • An odometry error worksheet: expected and measured travel per trial, the mean error and the spread, kept in the browser only.
  • Notebook pages for unit plans (today the notebook holds only the Rescue Rover course).

Sources

The facts this plan relies on, in each source's own words.