Module 12: Calibration, Tuning & Debugging

Level: 🟢 Beginner Board: Arduino Uno + full robot Prerequisites: Module 11 Estimated time: 60–90 minutes Goal: Diagnose and fix drift, sensor noise, and timing issues so the robot's autonomous behavior is consistent and reliable.


What You'll Learn

By the end of this module you will be able to apply a systematic five-step debugging process to any hardware failure. You will measure and correct motor drift using the RTRIM constant and characterize your ultrasonic sensor across different surfaces. You will also tune avoidance timing parameters so the robot escapes corners reliably.


The robot works. But "works" covers a wide range. At one end the robot moves and sometimes avoids things. At the other it navigates reliably for 10 minutes without getting stuck. This module closes that gap. You'll measure what your robot actually does, find the specific causes of failure, and fix them one at a time with a systematic approach that applies to any hardware problem.


12.1 Systematic debugging

Random fixes rarely work. The correct approach is to isolate, measure, and test one variable at a time. This is the same method used in production robotics, scientific experiments, and software engineering. The steps are always the same:

<svg viewBox="0 0 760 200" width="100%" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="aD12" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#1a1a18" stroke-width="1.5" stroke-linecap="round"/></marker>
  </defs>

  <!-- Steps -->
  <rect x="30" y="70" width="110" height="60" rx="5" fill="#daeaf5" stroke="#1f4d8c" stroke-width="1.5"/>
  <text x="85" y="95" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#1f4d8c">1. OBSERVE</text>
  <text x="85" y="111" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#2c4a6e">describe exactly</text>
  <text x="85" y="123" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#2c4a6e">what fails</text>

  <line x1="142" y1="100" x2="168" y2="100" stroke="#1a1a18" stroke-width="1.5" marker-end="url(#aD12)"/>

  <rect x="170" y="70" width="110" height="60" rx="5" fill="#f5eed8" stroke="#c8aa60" stroke-width="1.5"/>
  <text x="225" y="95" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#8a6800">2. ISOLATE</text>
  <text x="225" y="111" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#5a4200">reproduce it with</text>
  <text x="225" y="123" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#5a4200">one variable</text>

  <line x1="282" y1="100" x2="308" y2="100" stroke="#1a1a18" stroke-width="1.5" marker-end="url(#aD12)"/>

  <rect x="310" y="70" width="110" height="60" rx="5" fill="#d8f0e5" stroke="#1a6b4a" stroke-width="1.5"/>
  <text x="365" y="95" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#1a6b4a">3. MEASURE</text>
  <text x="365" y="111" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#0e3d28">Serial Monitor,</text>
  <text x="365" y="123" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#0e3d28">multimeter, tape</text>

  <line x1="422" y1="100" x2="448" y2="100" stroke="#1a1a18" stroke-width="1.5" marker-end="url(#aD12)"/>

  <rect x="450" y="70" width="110" height="60" rx="5" fill="#fde8e8" stroke="#c8420a" stroke-width="1.5"/>
  <text x="505" y="95" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8420a">4. CHANGE</text>
  <text x="505" y="111" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#7a2000">one thing only,</text>
  <text x="505" y="123" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#7a2000">then re-test</text>

  <line x1="562" y1="100" x2="588" y2="100" stroke="#1a1a18" stroke-width="1.5" marker-end="url(#aD12)"/>

  <rect x="590" y="70" width="110" height="60" rx="5" fill="#edeae0" stroke="#c4bfb0" stroke-width="1.5"/>
  <text x="645" y="95" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#3d3c38">5. VERIFY</text>
  <text x="645" y="111" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#6b6a65">did the fix hold</text>
  <text x="645" y="123" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#6b6a65">across 5+ runs?</text>

  <!-- Loop back if not verified -->
  <path d="M645 132 Q645 170 365 170 Q85 170 85 132" fill="none" stroke="#c4bfb0" stroke-width="1" stroke-dasharray="4 3" marker-end="url(#aD12)"/>
  <text x="365" y="186" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">not fixed → back to step 2</text>
</svg>

Fig 12.1 - Debugging loop. Observe the failure precisely, isolate it to one variable, measure what's actually happening, change one thing, verify it holds. Never change two things at once: you won't know which one fixed it.

The most common debugging mistake is changing multiple things simultaneously. If the robot starts working after you changed STOP_DISTANCE_CM and re-routed the sensor wires at the same time, you don't know which change mattered. When the problem returns, you have nothing to go on.

12.2 The Serial Monitor as your primary instrument

Every sensor reading, state transition, and motor command can be printed in real time. Serial Monitor costs nothing; it's already wired (USB cable to your laptop). Use it aggressively.

Effective Serial output format:

// Unreadable — too dense, no structure
Serial.println(dist);

// Readable — labeled, consistent rate
Serial.print("d="); Serial.print(dist);
Serial.print(" state="); Serial.print(currentState);
Serial.print(" mode="); Serial.println(currentMode == RC ? "RC" : "AUTO");

Print at a controlled rate. Use the 5 Hz lastPrintTime timer from Module 09. At 9600 baud, unrestricted printing can itself slow the loop enough to affect sensor timing.

Reading the output: watch for three patterns:

Pattern What it tells you
d=400 continuously Sensor not receiving echo: check TRIG/ECHO wires and GND connection
d= oscillating wildly between 1 and 400 ECHO wire intermittent: press it firmly into the breadboard
State cycling BACKING → TURNING → FORWARD → BACKING repeatedly without driving more than a few cm BACK_DURATION too short or STOP_DISTANCE_CM too large: robot re-detects same obstacle immediately

12.3 The five most common failure modes

These cover roughly 90% of the problems encountered with this robot after it's initially working.

Failure 1: Robot drifts sideways when driving forward

Symptom: Robot curves left or right instead of going straight. Gets worse at higher speeds.

Cause: Motors have slightly different actual speeds at the same PWM value. This is normal for DC gear motors; no two are identical.

Fix: Re-run the RTRIM calibration from Module 05. Place the robot on a tile floor with clearly visible grid lines. Drive forward for 2 seconds. Measure the lateral drift. Adjust RTRIM by 5, re-test. The correct value makes the robot travel within ±5 cm of a straight line over 1 m.

// If robot curves LEFT: left motor is too fast (or right too slow)
// Increase RTRIM — reduces right motor speed to match left
const int RTRIM = 10;  // was 0, drift was 8cm left over 1m

// If robot curves RIGHT: right motor is too fast
// Use a negative value
const int RTRIM = -8;

Failure 2: Robot stops too far from, or crashes into, obstacles

Symptom: STOP_DISTANCE_CM = 20 but robot stops 35 cm away, or still makes contact with the wall.

Cause A (stops too far): Sensor angled downward. It's detecting the floor before the wall. Remount the sensor horizontally.

Cause B (crashes): Coasting distance is longer than expected. At high CRUISE values, the robot travels 8–15 cm after motors cut. Increase STOP_DISTANCE_CM by 5 until it stops cleanly, then test at three different starting distances to confirm consistency.

Measurement: mark a line on the floor 20 cm from a flat wall. Run the robot toward the wall 5 times from 80 cm away. Record where the front of the robot stops each time. Average the 5 readings. The average should be within ±3 cm of STOP_DISTANCE_CM. If variance is high (>5 cm run-to-run), the sensor mount is vibrating; re-secure it.

Failure 3: Robot gets permanently stuck in a corner

Symptom: In autonomous mode, robot reverses, turns slightly, immediately re-detects the corner from a different angle, reverses again. Repeats indefinitely.

Cause: The corner reflects sound from both walls. The sensor sees dist < STOP_DISTANCE_CM immediately after every turn because the adjacent wall is also within range.

Fixes (try in order):

  1. Increase TURN_DURATION by 100 ms. A sharper turn may clear the corner.
  2. Increase BACK_DURATION by 100 ms to give more space before turning.
  3. Add a minimum drive distance after each avoidance: set a flag justAvoided = true after entering FORWARD from TURNING, and suppress the obstacle check for the first 300 ms of the FORWARD state.
// Minimum drive time after avoidance — prevents immediate re-detection
case FORWARD:
  driveForward(CRUISE);
  bool coolingDown = (now - stateStartTime < 300);
  if (!coolingDown && lastDist < STOP_DISTANCE_CM) {
    if (++obstacleCount >= OBSTACLE_CONFIRM) {
      obstacleCount = 0;
      enterState(BACKING, now);
    }
  } else if (!coolingDown) {
    obstacleCount = 0;
  }
  break;

Failure 4: Bluetooth commands lag or arrive out of order

Symptom: Pressing 'F' on the phone produces no response for 500+ ms, or the robot executes a command from two button presses ago.

Cause: Bytes are accumulating in the serial buffer. The robot processes one byte per loop iteration, but if the phone sends bytes faster than the loop runs (rare) or the loop is blocked (a remaining delay() call somewhere), bytes queue up.

Fix: Search your sketch for any delay() calls not inside enterState(). Remove them using the millis() pattern from Module 09. Each 50 ms delay adds 50 ms of worst-case command lag. Also verify the phone app is not appending newlines to commands. A newline after every command doubles the byte rate and can cause the buffer to fill during rapid button presses.

// Diagnostic: drain the serial buffer at startup
void setup() {
  // ...
  while (btSerial.available()) btSerial.read();  // clear any startup garbage
}

Failure 5: Robot works on the bench but fails on the floor

Symptom: Everything tests fine when you hold the robot in your hand. On the floor, the sensor reads 400 constantly, or the robot drives erratically.

Cause A: Sensor aimed at floor. The sensor face must be roughly horizontal: when the robot sits on the floor, the chassis can tilt slightly, aiming a forward-mounted sensor downward. Check mount angle with the robot on the floor, not in your hand.

Cause B: Wheel traction varies. On carpet, motors may stall at the same PWM value that spins freely on tile. Increase CRUISE by 20 if the robot seems sluggish on your test surface.

Cause C: Power sag. Fresh batteries give 6V. Partially discharged AA batteries drop to 5V or less under motor load. Measure battery voltage with a multimeter while the motors are running (not at rest). Below 4.8V under load, performance degrades significantly. Replace batteries or use a higher-capacity pack.

12.4 Measuring straight-line accuracy

Straight-line accuracy matters for any future navigation task. Here's a reproducible measurement protocol:

<svg viewBox="0 0 760 200" width="100%" xmlns="http://www.w3.org/2000/svg">

  <!-- Floor grid -->
  <rect x="40" y="30" width="680" height="140" rx="3" fill="#f5f2eb" stroke="#c4bfb0" stroke-width="1"/>

  <!-- Grid lines -->
  <line x1="40" y1="100" x2="720" y2="100" stroke="#c4bfb0" stroke-width="1"/>
  <line x1="200" y1="30" x2="200" y2="170" stroke="#c4bfb0" stroke-width="0.5" stroke-dasharray="3 3"/>
  <line x1="360" y1="30" x2="360" y2="170" stroke="#c4bfb0" stroke-width="0.5" stroke-dasharray="3 3"/>
  <line x1="520" y1="30" x2="520" y2="170" stroke="#c4bfb0" stroke-width="0.5" stroke-dasharray="3 3"/>
  <line x1="680" y1="30" x2="680" y2="170" stroke="#c4bfb0" stroke-width="0.5" stroke-dasharray="3 3"/>
  <text x="200" y="26" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">50cm</text>
  <text x="360" y="26" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">100cm</text>
  <text x="520" y="26" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">150cm</text>
  <text x="680" y="26" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">200cm</text>

  <!-- Start position -->
  <rect x="52" y="88" width="20" height="14" rx="2" fill="#1a6b4a"/>
  <polygon points="72,95 66,89 66,101" fill="#a8e8c8"/>
  <text x="72" y="82" font-family="Manrope,sans-serif" font-size="9" fill="#1a6b4a">start</text>

  <!-- Target line -->
  <line x1="40" y1="100" x2="720" y2="100" stroke="#1f4d8c" stroke-width="2"/>

  <!-- Three run paths -->
  <path d="M72 95 Q250 88 680 96" fill="none" stroke="#1a6b4a" stroke-width="1.5" stroke-dasharray="5 3"/>
  <path d="M72 95 Q300 102 680 106" fill="none" stroke="#c8b400" stroke-width="1.5" stroke-dasharray="5 3"/>
  <path d="M72 95 Q200 92 680 91" fill="none" stroke="#c8420a" stroke-width="1.5" stroke-dasharray="5 3"/>

  <!-- End markers -->
  <circle cx="680" cy="96" r="4" fill="#1a6b4a"/>
  <circle cx="680" cy="106" r="4" fill="#c8b400"/>
  <circle cx="680" cy="91" r="4" fill="#c8420a"/>

  <!-- Drift measurement brackets -->
  <line x1="688" y1="91" x2="688" y2="106" stroke="#888" stroke-width="1.5"/>
  <line x1="684" y1="91" x2="692" y2="91" stroke="#888" stroke-width="1"/>
  <line x1="684" y1="106" x2="692" y2="106" stroke="#888" stroke-width="1"/>
  <text x="700" y="102" font-family="IBM Plex Mono,monospace" font-size="10" fill="#3d3c38">drift</text>
  <text x="700" y="114" font-family="IBM Plex Mono,monospace" font-size="10" fill="#3d3c38">range</text>

  <!-- Good / bad annotation -->
  <rect x="40" y="150" width="160" height="14" rx="2" fill="#d8f0e5"/>
  <text x="120" y="161" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#1a6b4a">good: &lt;5cm drift at 200cm</text>
  <rect x="210" y="150" width="180" height="14" rx="2" fill="#fde8e8"/>
  <text x="300" y="161" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#c8420a">needs RTRIM: &gt;8cm drift at 200cm</text>
</svg>

Fig 12.2 - Straight-line test. Drive forward for 200 cm (timed) on a smooth surface. Measure lateral drift at the end point. Three runs, record each. Adjust RTRIM if the average exceeds 5 cm or variance across runs exceeds 4 cm.

Protocol:

  1. Mark a start line and a target line 200 cm ahead with tape.
  2. Drive the robot forward for exactly 2 seconds at your CRUISE value. delay(2000) is acceptable here because this is a calibration sketch, not the main program.
  3. Measure the lateral offset between the robot's midpoint and the target line.
  4. Run 5 times. Record each offset. Calculate mean and range.
  5. If mean offset >5 cm: adjust RTRIM toward correcting the drift direction, re-test.
  6. If range >4 cm (variance is high): the issue is mechanical (wheel slipping, motor connector loose) not RTRIM. Investigate physically.

12.5 Sensor characterization

The HC-SR04's accuracy and reliability vary with surface type, distance, and temperature. A characterized sensor is a trustworthy sensor.

Distance accuracy test: Place the robot facing a flat wall at exactly 30 cm (measured with a tape measure from the sensor face). Run the sensor-only sketch from Module 08. Record 20 readings. The mean should be within ±3 cm of 30. If it's consistently off by a fixed amount, you can add a calibration offset:

long readDistance() {
  // ...sensor code...
  const int CALIBRATION_OFFSET = -2;  // sensor consistently reads 2cm high
  long raw = dur == 0 ? 400 : dur / 58;
  return raw + CALIBRATION_OFFSET;
}

Surface characterization table (record your own results):

Surface Reliable range Notes
Flat painted wall 3–350 cm Best case: perpendicular, hard, flat
Cardboard box 3–300 cm Good, slight absorption at long range
Chair leg (round, ~3cm dia.) 5–80 cm Beam may miss narrow targets; step closer
Glass door 5–200 cm Partial reflection; depends on angle
Cloth or carpet 3–60 cm Heavy absorption; unreliable beyond 60 cm
Person's leg 3–120 cm Clothing absorbs; results vary
Open space / nothing → 400 pulseIn() timeout returns 0 → sentinel 400

Temperature and the /58 constant: The speed of sound is 331 m/s at 0 °C, rising about 0.6 m/s per degree. At 20 °C it's 343 m/s, giving the /58 constant. At 35 °C (a warm room), it's 352 m/s, so the correct divider is 56.8 rather than 58. This introduces a ~2% error at high temperatures. For obstacle avoidance this is irrelevant. If you ever use the sensor for precise measurement, replace the constant with a calculated value using a temperature reading.

12.6 Interrupt-based distance sensing (advanced path)

Skip this section if you are on an Arduino Uno. The interrupt approach requires wiring the ECHO pin to D2 or D3 (the Uno's only external interrupt pins). Both pins are already used by SoftwareSerial for the HC-05 Bluetooth connection. Implementing this on a Uno means removing Bluetooth. If you want interrupt-based sensing on the current chassis, upgrade to an Arduino Mega 2560 or Nano Every — both have additional interrupt-capable pins that are free in this course's wiring layout.

Module 09 noted that pulseIn() is still blocking internally: it stalls the loop for up to 30 ms while waiting for the echo. For a walking-speed robot this is fine. For faster robots or tighter control loops, external interrupts eliminate this stall completely.

The approach: use a hardware interrupt on the ECHO pin. When the echo pulse starts, an ISR (Interrupt Service Routine) records the start time. When it ends, the ISR records the end time and computes duration. loop() just reads the result without waiting.

volatile unsigned long echoStart = 0;
volatile unsigned long echoDuration = 0;

void echoISR() {
  if (digitalRead(ECHO_PIN) == HIGH) {
    echoStart = micros();
  } else {
    echoDuration = micros() - echoStart;
  }
}

void setup() {
  // ...
  attachInterrupt(digitalPinToInterrupt(ECHO_PIN), echoISR, CHANGE);
  // Note: on Arduino Uno, only D2 and D3 support external interrupts.
  // D2 and D3 are used by SoftwareSerial in this course — move HC-05 to
  // different pins, or use a Mega/Nano Every which has more interrupt pins.
}

long readDistanceNonBlocking() {
  if (echoDuration == 0) return 400;
  return echoDuration / 58;
  // Remember to reset echoDuration = 0 after each TRIG pulse
}

This advanced path is included for completeness. The pin conflict with SoftwareSerial means it requires hardware changes outside this course's scope. If you move to a Mega or a Nano Every, interrupt-based sensing is the right approach for any robot faster than walking speed.

Lab 12: Diagnose and repair three faults

Each of the three parts simulates a real fault. Diagnose the cause using only Serial Monitor and a tape measure. No guessing, no random changes.

What you need: Completed unified robot from Module 11 · tape measure · multimeter (optional) · spare jumper wires

Part A: Drift calibration

  1. Set RTRIM = 0. Place the robot on a smooth floor facing a clear 200 cm run. Send 'F' for 2 seconds (use a stopwatch), then 'S'.
  2. Measure the lateral offset from the starting center line. Record it. Repeat 4 more times.
  3. Calculate the mean offset and direction (left or right). Adjust RTRIM in increments of 5 until the mean offset is under 5 cm. Record your final RTRIM value; you'll use it in Module 13.

Part B: Sensor fault injection

  1. Deliberately loosen the ECHO wire by half (don't remove it, just reduce its contact). Upload the sensor-only sketch.
  2. Open Serial Monitor. Describe what the output looks like with a loose ECHO wire. When does it read 400? When does it read correctly?
  3. Re-seat the ECHO wire. Confirm readings return to normal. This exercise teaches you to recognize the exact symptom of a bad ECHO connection: not all-400, but intermittent.

Part C: Corner escape

  1. Set BACK_DURATION = 200 (deliberately too short) and TURN_DURATION = 100 (deliberately too short). Upload the unified sketch, switch to autonomous mode, and place the robot in a corner.
  2. Watch it get stuck. Note exactly what the Serial Monitor shows during the stuck loop. How many BACKING → TURNING → FORWARD cycles occur before it re-detects the same obstacle?
  3. Increase BACK_DURATION to 600 and TURN_DURATION to 400. Repeat. Confirm the robot escapes the corner within 2 avoidance cycles.
  4. Optional: add the 300 ms cool-down from section 12.3 and test again. Does it escape more reliably?

Module milestone: Your robot drives straight to within ±5 cm over 200 cm (documented RTRIM value recorded), the sensor fault injection test has been run and documented, and the corner escape parameters are tuned for your chassis. Ready for Module 13: Final Project: Autonomous Navigator.


Self-Check: Module 12

Key Terms Glossary

Term Definition
RTRIM Motor trim constant. Reduces one motor's PWM value relative to the other to compensate for mechanical differences. Positive values reduce the right motor; negative values reduce the left.
coasting distance The distance the robot travels after the motors cut to zero PWM. Determined by speed, inertia, and floor friction. Must be accounted for when setting STOP_DISTANCE_CM.
calibration offset A fixed correction added to raw sensor readings to compensate for systematic error. Only valid when the error is consistent; random noise cannot be corrected with an offset.
ISR Interrupt Service Routine. A function that runs immediately when a hardware event occurs, interrupting whatever the main loop is doing. Must be short and must not use Serial or delay().
volatile A C++ keyword telling the compiler that a variable can be changed outside normal program flow (e.g., inside an ISR). Without it, the compiler may cache the value and miss updates.
variance The spread of repeated measurements around their mean. High variance in straight-line tests points to a mechanical problem (slipping wheel, loose connector); low variance with a large offset points to a calibration problem.
baud rate Serial communication speed in bits per second. Arduino and Serial Monitor must match: 9600 baud throughout this course. A mismatch produces garbled output.
sensor characterization The process of measuring a sensor's accuracy and reliability across different surfaces and distances so its limits are known before relying on it in a real task.

Previous: ← Module 11: Unified RC + Autonomous Control · Next: Module 13: Final Project: Autonomous Navigator →

Related Blog Posts