Autonomous Control System — VEX IQ 2nd Gen

Drive straight.
Turn precise.
Win matches.

I built a full PID control system for my VEX IQ robot — not because I had to, but because open-loop driving lost me matches I should have won. This documents what I built, why it works, and everything that went wrong first.

VEX IQ World Championship
Elementary School Korea Champion
C++
VEXcode IQ — text coding
00 / Demo

Watch it drive

01 / Hardware

What the robot is made of

PlatformVEX IQ 2nd Generation
Wheel diameter650 mm
Wheel count4 wheels (front + rear)
Gear ratio1 : 1 — motor shaft drives wheel directly
Wheel circumference≈ 2042 mm (π × 650)
Track width2100 mm — left to right
Axle spacing1730 mm — front to rear
Left motorPORT 6, reversed
Right motorPORT 7, forward
Heading sensorBuilt-in Brain Inertial (no external gyro)
Drive styleLeft Arcade — left stick only, fwd ×1.0, turn ×0.65
02 / Control Theory

How PID actually works

output = kp × error + ki × Σerror + kd × Δerror
// runs every 20 ms until error < tolerance for 10 consecutive cycles
P

Proportional

Output scales with distance to goal. 500 mm away → full power. 20 mm away → almost nothing. This is why the robot slows down automatically — no separate deceleration code needed.

drive kp = 0.12
turn kp = 0.40
I

Integral

Accumulates error over time. If friction keeps the robot 15 mm short, the integral grows until it pushes through. Can also cause oscillation if set too high — so I kept it off for most cases.

drive ki = 0.0 (disabled)
turn ki = 0.0 (disabled)
D

Derivative

Reacts to how fast the error is changing. If the robot is closing in fast, D kicks in as a brake. Without it, the robot shoots past the target and oscillates. Higher kd = harder stop.

drive kd = 0.25
turn kd = 0.80

The drive function actually runs two PID loops simultaneously: one for distance (encoder → mm), and a second for heading (gyro → degrees). The heading correction is subtracted from one motor and added to the other — so the robot stays straight even if one side is slightly stronger.

03 / Competition

Why this matters at worlds

Autonomous is scored in millimeters

I've watched robots miss a game element by 4 cm in autonomous and lose the match by that margin. PID keeps positional error under 20 mm on every run — that's the difference between picking up an object and driving past it.

Battery voltage kills open-loop drives

A timed drive that works perfectly at 100% battery will stop 10–15 cm short by your fifth match of the day. PID doesn't care — it reads encoders, not time, so it drives the same distance regardless of voltage.

2° of drift = 35 mm off course

Over 1000 mm, a 2° heading error puts the robot almost 4 cm to the side. The gyro correction loop runs 50 times per second and trims left/right power in real time. The robot tracks straight even on rough tiles.

One button, full autonomous — then back to driver

At the world level, you need both. My setup lets me drive manually with Left Arcade, hit a button to run a precise autonomous sequence, and the robot returns to manual control automatically when it's done.

Going to worlds three times taught me that the robots that win aren't always the fastest or the strongest. They're the ones that do the same thing correctly every single match.

04 / Development

Everything that went wrong

01
Robot went forward, then backward, then forward — forever
Classic PID oscillation. kp was 0.35 — strong enough that the robot blew past the target, reversed hard to correct, overshot again the other way. Lowered kp to 0.12. Raised kd to 0.25 so it brakes before reaching the target, not after.
kp ↓ kd ↑
02
turnDeg(90) turned left instead of right
Motor sign convention was flipped inside turnDeg(). Left was getting +output and Right was getting -output. Swapped them. Now positive angle = clockwise = right turn.
Sign fix
03
Turning oscillated near the target angle
After fixing direction, the robot would rotate past 90°, come back, go past again. kp was still too high (0.8), and kd wasn't strong enough to dampen the approach. Tuned to kp 0.4, kd 0.8 — it now locks on and holds.
kp 0.8→0.4, kd 0.5→0.8
04
180° turn stopped halfway — never finished
The 3-second timeout was firing before the robot could complete a 180° rotation. And near the target, output dropped so low the motors couldn't overcome static friction. Fixed by doubling the timeout to 6 seconds and adding a minimum power floor of ±12%.
Timeout + minPower
05
driveStraightMM(1000) always stopped at ~985 mm
Friction was absorbing the last 15 mm of travel. Adding ki caused oscillation. Instead, I multiplied the target by 1.05 — the robot aims for 1050 mm and lands on 1000 after friction losses. Simple and stable.
targetMM × 1.05
05 / Source

The actual code

PIDController struct C++
struct PIDController {
  double kp, ki, kd, maxOutput, integralLimit;
  double integral = 0, prevError = 0;
  bool   firstRun = true;

  double calculate(double error) {
    integral += error;
    // windup guard
    if (integral >  integralLimit) integral =  integralLimit;
    if (integral < -integralLimit) integral = -integralLimit;

    double deriv = firstRun ? 0.0 : (error - prevError);
    firstRun = false; prevError = error;

    double out = kp*error + ki*integral + kd*deriv;
    if (out >  maxOutput) out =  maxOutput;
    if (out < -maxOutput) out = -maxOutput;
    return out;
  }
};
driveStraightMM() — dual loop C++
void driveStraightMM(double targetMM, double maxPower = 65.0) {
  Left.setPosition(0, degrees); Right.setPosition(0, degrees);
  double lockHeading = BrainInertial.heading();

  PIDController drive  {0.12, 0.0, 0.25, maxPower, 100.0};
  PIDController heading{1.0,  0.0, 0.1,  25.0,    20.0};

  double goal = targetMM * 1.05; // friction compensation
  int settled = 0;

  while (true) {
    double traveled = (Left.position(degrees) + Right.position(degrees))
                      / 2.0 / 360.0 * WHEEL_CIRCUM;

    double power  = drive.calculate(goal - traveled);
    double steer  = heading.calculate(
                     normalizeAngle(lockHeading - BrainInertial.heading()));

    Left.spin(forward,  power - steer, percent);
    Right.spin(forward, power + steer, percent);

    if (fabs(goal - traveled) < 20.0) settled++;
    else settled = 0;
    if (settled > 10) break; // stable for 200 ms
    wait(20, msec);
  }
  Left.stop(brake); Right.stop(brake);
}
turnDeg() — gyro PID C++
void turnDeg(double angleDeg, double maxPower = 60.0) {
  double target = BrainInertial.heading() + angleDeg;
  PIDController turn{0.4, 0.0, 0.8, maxPower, 30.0};
  int settled = 0;

  while (true) {
    double err = normalizeAngle(target - BrainInertial.heading());
    double out = turn.calculate(err);

    // minimum power — overcome static friction
    if (out > 0 && out < 12)  out =  12;
    if (out < 0 && out > -12) out = -12;

    Left.spin(forward, -out, percent); // +angle = clockwise
    Right.spin(forward,  out, percent);

    if (fabs(err) < 2.0) settled++;
    else settled = 0;
    if (settled > 10) break;
    wait(20, msec);
  }
  Left.stop(brake); Right.stop(brake);
}
main() — manual ↔ PID C++
int main() {
  BrainInertial.calibrate();
  while (BrainInertial.isCalibrating()) wait(50, msec);

  while (true) {
    // left arcade — normal driving
    double fwd  = Controller.AxisA.position() * 1.0;
    double turn = Controller.AxisC.position() * 0.65;
    Left.spin(forward,  fwd + turn, percent);
    Right.spin(forward, fwd - turn, percent);

    // hold ButtonLUp → run autonomous, then return to manual
    if (Controller.ButtonLUp.pressing()) {
      Left.stop(brake); Right.stop(brake);
      wait(200, msec);

      driveStraightMM(1000); wait(300, msec);
      turnDeg(90);            wait(300, msec);
      driveStraightMM(500);
    }
    wait(20, msec);
  }
}

Want the whole thing?

Full source + 20-min video lecture

Every file above, fully commented — plus a full video walkthrough of the PID theory, a line-by-line code breakdown, 5 real bugs I hit while building it, and a slide deck to keep as a reference.

Get it on Gumroad — $9.99
06 / Contact

Get in touch

Questions about the build, the code, or VEX IQ in general? Reach out.

lucykim0907@gmail.com