Trailblazer Robotics
Unit 5 · Week 10

Implementing Odometry

Translating encoder changes into (x, y, θ) updates with a tiny bit of trig.

You'll learn

The algorithm (simplified)

Every 10 ms:

  1. Read the left and right encoder positions.
  2. Calculate how much each wheel moved since last update (ΔL and ΔR).
  3. Average distance: d = (ΔL + ΔR) / 2.
  4. Change in heading: Δθ = (ΔL − ΔR) / trackWidth.
  5. Update heading: θ += Δθ.
  6. Update position using the average heading during the move:
    • x += d * cos(θ - Δθ/2)
    • y += d * sin(θ - Δθ/2)
Why average heading? The robot rotated during the update. Using the heading at the midpoint of the move gives a much better estimate than using the start or end heading, because the motion between samples is best modeled as a short arc.

The C++ implementation

#include "vex.h"
#include <cmath>
// using namespace vex;  (auto-included by VEXcode IQ)

// --- Configure for your robot ---
const double WHEEL_DIAM_IN = 2.75;      // your wheel diameter
const double TRACK_WIDTH   = 11.0;      // distance between left and right wheels
const double TICKS_PER_REV = 360.0;     // motor encoder ticks per revolution

// --- Pose ---
double poseX = 0.0;
double poseY = 0.0;
double poseT = 0.0;   // theta in radians

// --- Previous encoder readings ---
double prevL = 0;
double prevR = 0;

double ticksToInches(double ticks) {
  double revs = ticks / TICKS_PER_REV;
  return revs * (3.14159 * WHEEL_DIAM_IN);
}

void odomUpdate() {
  double curL = LeftMotor.position(degrees);
  double curR = RightMotor.position(degrees);

  double dL = ticksToInches(curL - prevL);
  double dR = ticksToInches(curR - prevR);
  prevL = curL;
  prevR = curR;

  double d  = (dL + dR) / 2.0;          // forward distance moved
  double dT = (dL - dR) / TRACK_WIDTH;  // change in heading (radians)

  double thetaMid = poseT + dT / 2.0;   // midpoint heading
  poseX += d * cos(thetaMid);
  poseY += d * sin(thetaMid);
  poseT += dT;
}

// Run this in its own task so the main code is free
int odomTask() {
  while (true) {
    odomUpdate();
    wait(10, msec);
  }
  return 0;
}

int main() {
  // start the tracking task
  task tracker(odomTask);

  // ... your autonomous code, which can read poseX, poseY, poseT anytime
}

Tuning

Using the position

// Drive to a point on the field
void driveToPoint(double targetX, double targetY) {
  while (true) {
    double dx = targetX - poseX;
    double dy = targetY - poseY;
    double dist = sqrt(dx*dx + dy*dy);
    if (dist < 1.0) break;  // within 1 inch

    // P-control: aim at the target, then move toward it
    double targetHeading = atan2(dy, dx);
    double headingError  = targetHeading - poseT;
    // ... apply PID to turn while moving
    wait(20, msec);
  }
}

Limitations to be honest about

Two-wheel odometry using only drive encoders is the simplest setup, but it has weaknesses:

Curated references

Coding Challenge

Square the field with odometry

Using only the tracked pose (poseX, poseY, poseT) from this week's odometry task, drive the robot in a 24-inch square and return to the starting position with the same heading.

No solution is given for this challenge.

Find the Bug

Two odometry implementations with subtle bugs. Find them.

Problem 10A
This odometry update runs every 10 ms. After a few seconds, x and y are massively too large, like the robot drove a mile. What's wrong?
void odomUpdate() {
  double curL = LeftMotor.position(degrees);
  double curR = RightMotor.position(degrees);

  double dL = ticksToInches(curL - prevL);
  double dR = ticksToInches(curR - prevR);
  // (no update of prevL / prevR here)

  double d  = (dL + dR) / 2.0;
  double dT = (dL - dR) / TRACK_WIDTH;

  poseX += d * cos(poseT + dT/2.0);
  poseY += d * sin(poseT + dT/2.0);
  poseT += dT;
}
Problem 10B
This odometry compiles and runs, but x and y end up off by a noticeable amount after every turn. Forward motion looks fine. What is being calculated wrong?
void odomUpdate() {
  // ... encoders read and delta-ed correctly ...
  double d  = (dL + dR) / 2.0;
  double dT = (dL - dR) / TRACK_WIDTH;

  poseX += d * cos(poseT);   // using start heading
  poseY += d * sin(poseT);   // using start heading
  poseT += dT;
}
← Week 9 Next: State Machines →