Module 5: Differential Steering & Turning

Level: 🟢 Beginner Board: Arduino Uno Prerequisites: Module 04 Estimated time: 45–60 minutes Goal: Write turn functions that produce consistent arc and pivot maneuvers using differential wheel speeds.


What You'll Learn

By the end of this module you will understand why motors drift even at identical command values and how to measure and apply a trim constant. You will write arc turn functions that produce smooth curved paths, and you will calibrate straight-line travel over a 3-metre run. You drove a square in Module 4 using fixed delay times; this module replaces magic numbers with a geometric understanding of how your robot actually steers.


5.1 Why motors are never perfectly matched

Send the same PWM value to both motors and the robot will drift. Not because your wiring is wrong, not because your code has a bug, but because no two DC motors are mechanically identical from the factory. Winding resistance varies slightly. Brush contact differs. Gearbox friction is never the same on both sides. At identical command values, one motor turns fractionally faster than the other, and the robot curves.

The drift is not a bug. It is physics. Understanding it is what separates a builder who fights their robot from one who works with it.

<svg viewBox="0 0 760 280" width="100%" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <marker id="aB5" 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>
        <marker id="aR5" 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="#c8420a" stroke-width="1.5" stroke-linecap="round"/></marker>
        <marker id="aG5" 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="#1a6b4a" stroke-width="1.5" stroke-linecap="round"/></marker>
      </defs>

      <!-- Left: uncompensated drift -->
      <text x="175" y="22" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" letter-spacing="1" fill="#c8420a">UNCOMPENSATED</text>
      <text x="175" y="38" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">ENA=ENB=180, robot drifts right</text>
      <line x1="175" y1="60" x2="175" y2="230" stroke="#c4bfb0" stroke-width="1" stroke-dasharray="6 4"/>
      <text x="175" y="250" text-anchor="middle" font-family="Manrope,sans-serif" font-size="10" fill="#6b6a65">intended</text>
      <path d="M175 60 Q200 130 240 230" fill="none" stroke="#c8420a" stroke-width="2.5" marker-end="url(#aR5)"/>
      <text x="260" y="230" font-family="Manrope,sans-serif" font-size="10" fill="#c8420a">actual</text>
      <rect x="162" y="50" width="26" height="16" rx="3" fill="#edeae0" stroke="#b0aba0" stroke-width="1.5"/>
      <circle cx="175" cy="50" r="4" fill="#3d3c38"/>
      <rect x="100" y="80" width="12" height="40" rx="2" fill="#1a6b4a" opacity=".6"/>
      <text x="106" y="76" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#6b6a65">L</text>
      <text x="106" y="130" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#1a6b4a">180</text>
      <rect x="238" y="90" width="12" height="50" rx="2" fill="#1a6b4a" opacity=".9"/>
      <text x="244" y="86" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#6b6a65">R</text>
      <text x="244" y="150" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#1a6b4a">180*</text>
      <text x="244" y="164" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#c8420a">faster</text>

      <!-- Divider -->
      <line x1="350" y1="20" x2="350" y2="260" stroke="#c4bfb0" stroke-width="1" stroke-dasharray="4 4"/>

      <!-- Right: compensated -->
      <text x="555" y="22" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" letter-spacing="1" fill="#1a6b4a">COMPENSATED</text>
      <text x="555" y="38" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">Reduce faster side slightly</text>
      <line x1="555" y1="60" x2="555" y2="230" stroke="#1a6b4a" stroke-width="2.5" marker-end="url(#aG5)"/>
      <text x="555" y="250" text-anchor="middle" font-family="Manrope,sans-serif" font-size="10" fill="#1a6b4a">straight</text>
      <rect x="542" y="50" width="26" height="16" rx="3" fill="#edeae0" stroke="#1a6b4a" stroke-width="1.5"/>
      <circle cx="555" cy="50" r="4" fill="#3d3c38"/>
      <rect x="480" y="80" width="12" height="40" rx="2" fill="#1a6b4a" opacity=".6"/>
      <text x="486" y="76" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#6b6a65">L</text>
      <text x="486" y="130" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#1a6b4a">180</text>
      <rect x="618" y="88" width="12" height="38" rx="2" fill="#1a6b4a" opacity=".7"/>
      <text x="624" y="84" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#6b6a65">R</text>
      <text x="624" y="136" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#1a6b4a">172</text>
      <text x="624" y="150" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#1a6b4a">trimmed</text>
      <rect x="370" y="200" width="250" height="48" rx="3" fill="#f5f2eb" stroke="#c4bfb0" stroke-width="1"/>
      <text x="380" y="219" font-family="IBM Plex Mono,monospace" font-size="10" fill="#3d3c38">const int RTRIM = 8; // subtract from R</text>
      <text x="380" y="237" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">Tune until straight. Write it down.</text>
    </svg>

Fig 5.1: Motor mismatch causes drift even at identical command values. The fix is a trim constant: measure which side is faster, subtract a small offset from that side's PWM value. Find the number that produces a straight line, then treat it as a hardware constant in your code rather than a tunable parameter you keep changing.

Finding your trim value is a calibration step, not a guessing game. Put the robot on a straight tape line and run it forward for 2 metres at a fixed speed. Measure the drift. If it pulls right, the right motor is faster; reduce ENB by a small amount (start with 5–10 out of 255). Repeat until the robot tracks the tape line with less than 3cm of drift over 2 metres. Write the trim constant into your code as a named constant, not a buried number.


5.2 Arc turns and speed ratios

In Module 4 you used pivot turns: one wheel forward, one reverse, spin in place. Those work for obstacle avoidance but look abrupt and consume extra time at low speeds. Arc turns keep both wheels moving forward while one runs faster than the other. The result is a smooth curve rather than a stopped rotation.

<svg viewBox="0 0 760 320" width="100%" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <marker id="aBl5" 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="#1f4d8c" stroke-width="1.5" stroke-linecap="round"/></marker>
        <marker id="aRd5" 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="#c8420a" stroke-width="1.5" stroke-linecap="round"/></marker>
        <marker id="aGn5" 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="#1a6b4a" stroke-width="1.5" stroke-linecap="round"/></marker>
      </defs>

      <!-- WIDE ARC (small speed difference) -->
      <text x="185" y="22" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" letter-spacing="1" fill="#1f4d8c">GENTLE ARC</text>
      <text x="185" y="38" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">L=180, R=140 → wide curve</text>
      <path d="M100 290 Q120 180 200 100" fill="none" stroke="#1f4d8c" stroke-width="2.5" stroke-dasharray="8 4" marker-end="url(#aBl5)"/>
      <rect x="88" y="278" width="24" height="16" rx="3" fill="#edeae0" stroke="#b0aba0" stroke-width="1.5"/>
      <circle cx="20" cy="180" r="5" fill="none" stroke="#1f4d8c" stroke-width="1.5"/>
      <line x1="20" y1="180" x2="100" y2="290" stroke="#1f4d8c" stroke-width="0.75" stroke-dasharray="3 3"/>
      <line x1="20" y1="180" x2="200" y2="100" stroke="#1f4d8c" stroke-width="0.75" stroke-dasharray="3 3"/>
      <text x="14" y="200" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#1f4d8c">ICR</text>
      <rect x="260" y="240" width="10" height="44" rx="2" fill="#1a6b4a"/>
      <text x="265" y="236" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#6b6a65">L=180</text>
      <rect x="280" y="252" width="10" height="32" rx="2" fill="#1f4d8c"/>
      <text x="285" y="248" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#6b6a65">R=140</text>

      <!-- TIGHT ARC (large speed difference) -->
      <text x="560" y="22" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" letter-spacing="1" fill="#c8420a">TIGHT ARC</text>
      <text x="560" y="38" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">L=180, R=50 → sharp curve</text>
      <path d="M500 290 Q490 230 520 170 Q550 120 600 100" fill="none" stroke="#c8420a" stroke-width="2.5" stroke-dasharray="8 4" marker-end="url(#aRd5)"/>
      <rect x="488" y="278" width="24" height="16" rx="3" fill="#edeae0" stroke="#b0aba0" stroke-width="1.5"/>
      <circle cx="460" cy="180" r="5" fill="none" stroke="#c8420a" stroke-width="1.5"/>
      <line x1="460" y1="180" x2="500" y2="290" stroke="#c8420a" stroke-width="0.75" stroke-dasharray="3 3"/>
      <text x="450" y="200" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8420a">ICR</text>
      <rect x="640" y="238" width="10" height="48" rx="2" fill="#1a6b4a"/>
      <text x="645" y="234" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#6b6a65">L=180</text>
      <rect x="660" y="274" width="10" height="12" rx="2" fill="#c8420a"/>
      <text x="665" y="270" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#6b6a65">R=50</text>

      <!-- Formula box -->
      <rect x="300" y="220" width="180" height="80" rx="4" fill="#1a1a18"/>
      <text x="390" y="242" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">arc radius formula</text>
      <line x1="310" y1="250" x2="470" y2="250" stroke="#333" stroke-width="1"/>
      <text x="390" y="270" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="13" fill="#f5f2eb">R = L(vR+vL)/(vR-vL)</text>
      <text x="390" y="290" text-anchor="middle" font-family="Manrope,sans-serif" font-size="10" fill="#555">L = half wheelbase</text>
    </svg>

Fig 5.2: Arc turn geometry. A small speed difference between left and right wheels produces a wide, gentle arc (ICR far from the robot). A large difference produces a tight curve (ICR close in). When one wheel reverses completely, ICR moves to the slow wheel's position and the robot pivots around it. The arc radius formula gives you the math, but for practical navigation, think in speed ratios first.

// Arc turn function — both wheels forward, different speeds
// leftSpeed and rightSpeed: 0–255
void driveArc(int leftSpeed, int rightSpeed) {
  digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
  digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
  analogWrite(ENA, leftSpeed);
  analogWrite(ENB, rightSpeed - RTRIM);  // apply trim
}

// Example: gentle right curve (left wheel faster → robot curves toward slower right)
driveArc(180, 120);  // left faster → curves right
delay(2000);
stopMotors();

When to use pivot turns vs arc turns

Use pivot turns (one wheel reverse) when you need to change heading quickly in a tight space, for example during obstacle avoidance. Use arc turns when you want smooth trajectory changes during open navigation. A robot that pivots to every waypoint looks jerky and uses more battery. A robot that uses arc turns where possible moves more like a vehicle and less like a clock hand.


5.3 Calibrating straight-line travel

Instructor note Verify the battery holder is mounted centered between the drive wheels. The trim constant compensates for both motor speed mismatch and weight distribution imbalance. If your battery is mounted to one side, the calibration corrects for both problems simultaneously, and the trim value will be wrong after any battery repositioning.

Instructor note A steel ball caster with a ~15mm ball diameter is recommended for this chassis. A caster that is too tall raises the rear of the robot, reducing drive wheel contact pressure and making straight-line calibration unstable; the wheels skip and spin instead of gripping. If your robot wiggles erratically even after trim adjustments, check that the caster height puts the chassis level (or very slightly nose-down) when the drive wheels sit flat on the floor.

This is a physical measurement process, not a coding exercise. You need a tape measure, a 3-metre strip of floor, and patience. The goal is one number: the trim constant for your specific robot.

<svg viewBox="0 0 760 240" width="100%" xmlns="http://www.w3.org/2000/svg">
      <!-- tape line -->
      <line x1="40" y1="160" x2="720" y2="160" stroke="#1a1a18" stroke-width="2"/>
      <line x1="40" y1="164" x2="720" y2="164" stroke="#c8b400" stroke-width="3" stroke-dasharray="20 8"/>
      <!-- measurement ticks -->
      <line x1="40" y1="155" x2="40" y2="170" stroke="#1a1a18" stroke-width="2"/>
      <text x="40" y="184" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">0m</text>
      <line x1="280" y1="155" x2="280" y2="170" stroke="#1a1a18" stroke-width="1.5"/>
      <text x="280" y="184" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">1m</text>
      <line x1="520" y1="155" x2="520" y2="170" stroke="#1a1a18" stroke-width="1.5"/>
      <text x="520" y="184" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">2m</text>
      <line x1="720" y1="155" x2="720" y2="170" stroke="#1a1a18" stroke-width="1.5"/>
      <text x="720" y="184" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">3m</text>

      <!-- Robot at start -->
      <rect x="28" y="142" width="28" height="18" rx="3" fill="#edeae0" stroke="#b0aba0" stroke-width="1.5"/>
      <circle cx="42" cy="142" r="5" fill="#3d3c38"/>

      <!-- Drift paths — three iterations -->
      <path d="M42 155 Q200 155 400 140 Q550 128 690 120" fill="none" stroke="#c8420a" stroke-width="1.5" stroke-dasharray="6 3"/>
      <text x="420" y="116" font-family="Manrope,sans-serif" font-size="10" fill="#c8420a">RTRIM=0 — drifts right</text>
      <path d="M42 155 Q250 155 450 150 Q600 146 690 144" fill="none" stroke="#c8b400" stroke-width="1.5" stroke-dasharray="6 3"/>
      <text x="430" y="140" font-family="Manrope,sans-serif" font-size="10" fill="#8a7a00">RTRIM=5 — less drift</text>
      <line x1="42" y1="160" x2="710" y2="160" stroke="#1a6b4a" stroke-width="2.5"/>
      <text x="460" y="176" font-family="Manrope,sans-serif" font-size="10" fill="#1a6b4a">RTRIM=9 — straight</text>

      <!-- Measurement callout -->
      <line x1="690" y1="120" x2="690" y2="160" stroke="#c8420a" stroke-width="1.5"/>
      <line x1="685" y1="120" x2="695" y2="120" stroke="#c8420a" stroke-width="1.5"/>
      <line x1="685" y1="160" x2="695" y2="160" stroke="#c8420a" stroke-width="1.5"/>
      <text x="710" y="144" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8420a">40mm</text>
      <text x="710" y="158" font-family="Manrope,sans-serif" font-size="10" fill="#6b6a65">drift</text>

      <!-- Step annotations -->
      <text x="380" y="28" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" font-weight="500" fill="#1a1a18">TRIM CALIBRATION PROCEDURE</text>
      <text x="380" y="48" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">1 Run at fixed speed over 3m  2 Measure end drift  3 Adjust RTRIM by half the drift distance  4 Repeat</text>
      <text x="380" y="70" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">Converges in 2–4 passes. Record final value. Test again after battery change.</text>
    </svg>

Fig 5.3: Straight-line trim calibration. Run the robot 3 metres, measure drift at the end. Adjust the trim constant by roughly half the drift amount, run again. Converges in 2–4 passes. The trim value is not universal; it may shift slightly when the battery is fresh vs half-depleted, and it changes completely if you swap motors.

Instructor note The trim constant you calibrate on a full battery will drift as the battery depletes. Lower voltage reduces the faster motor more than the slower one (motors are not perfectly symmetric), so the robot that tracked straight on a fresh pack may drift noticeably when the pack drops to 50%. Always calibrate with a battery at the charge level you plan to use, and recheck trim if the robot's straight-line performance suddenly worsens mid-session. A trim drift of 3–5 points over a pack discharge is normal.

Advanced path The arc radius equation from Module 2 gives you a way to calculate the exact speed ratio needed for a specific turning radius: R = L × (vR + vL) / (vR − vL), where L is half the wheelbase in metres and v values are wheel speeds in any consistent unit. To turn a 0.5m radius arc with a 120mm wheelbase (L = 0.06m): solve for vR/vL = (R+L)/(R−L) = 0.56/0.44 = 1.27. So right wheel runs 27% faster than left. At a base speed of 180, that gives roughly L=180, R=180×1.27=229. The math matches practice; if you measure the actual arc and find it does not match, your wheel diameter or wheelbase measurement has error.


Lab 05: Navigate a timed obstacle course by dead reckoning

Set up three obstacles on the floor in a rough triangle: chairs, boxes, whatever is available. Program the robot to navigate around all three and return close to its starting position using only timed drive and turn commands. No sensors. No feedback. Pure geometry and calibration.

Steps

  1. Complete the trim calibration from section 5.3 first. Write the RTRIM constant into your code.
  2. Measure the distances and angles between your obstacles roughly by hand.
  3. Convert distances to delay times: if your robot covers 1 metre in 1,400ms at speed 180, then 60cm takes 840ms.
  4. Convert turns to delay times: if a 90° pivot takes 500ms, a 45° takes 250ms, a 120° takes 667ms.
  5. Write a sequence of driveForward(), driveArc(), and pivot turn calls that navigate the triangle.
  6. Run it. Note where the robot ends up relative to where you intended. Adjust the worst-offending segment first.
  7. Try replacing one pivot turn with an arc turn. Note the difference in path smoothness.

Module milestone The robot navigates a three-obstacle course and returns within 30cm of its starting position, using timed commands only. The trim constant is documented in the code. At least one arc turn is used in the route.


Self-Check: Module 5

Key Terms Glossary

Term Definition
trim constant A fixed offset subtracted from the faster motor's PWM value to compensate for mechanical mismatch. Determined by calibration, stored as a named constant.
arc turn A curved path produced by running both wheels forward at different speeds. Smoother than a pivot turn. Turn radius is controlled by the speed ratio.
pivot turn One wheel forward, one wheel reverse. The robot spins around the center of its wheelbase. Tightest possible turn, uses more battery than an arc.
open-loop control Commanding without feedback. You set motor speed and time, then hope the robot did what you intended. Works in clean conditions, drifts under varying load or battery.
wheelbase (2L) Distance from left tread center to right tread center. Used in the arc radius formula. Measure yours; it is a fixed property of your chassis.
speed ratio The ratio of right wheel speed to left wheel speed (or vice versa). Determines turn radius. A ratio of 1:1 is straight. A ratio of 1:0 is a pivot on the stopped wheel.

Previous: ← Module 04: Your First Arduino Sketch · Next: Module 06: Speed Control with PWM →

Related Blog Posts