Skip to content

Tutorial 7.1: PID Control (Advanced)

Time: ~25 minutes Prerequisites: All previous tutorials Level: Bonus/Advanced


What is PID?

PID stands for Proportional-Integral-Derivative. It's a control algorithm that helps robots move more accurately by adjusting power based on how far you are from your target.

Real-World Analogies

The Thermostat Analogy

Your home thermostat is a simple controller:

    SET TEMPERATURE: 70°F

    Room is 60°F     → Heater ON (big correction)
    Room is 68°F     → Heater ON (small correction)
    Room is 70°F     → Heater OFF (at target!)
    Room is 72°F     → AC ON (overshoot correction)

PID does the same thing, but smarter!

The Video Game Analogy

In racing games, when you approach a checkpoint: - Far away: Full speed ahead! - Getting closer: Start slowing down - Almost there: Gentle tap on brakes - At checkpoint: Perfect stop

That's exactly what PID does for your robot!

The Basketball Shot Analogy

When shooting free throws: - Miss by 2 feet short: Add LOTS more power next shot - Miss by 6 inches short: Add a little more power - Perfect shot: Same power! - Overshoot by 6 inches: Reduce power slightly

PID adjusts motor power the same way you adjust your shot!

The Driving Analogy

When approaching a stop sign: - 100 feet away: Full speed - 50 feet away: Start braking - 10 feet away: Light brake - At line: Stopped perfectly

If you only knew "GO" and "STOP" (bang-bang), you'd either stop too early or slam into the intersection!

PID Overview Flowchart

flowchart LR
    subgraph "PID Controller"
        A["Target\n(90°)"] --> B["Calculate\nError"]
        C["Current\n(85°)"] --> B
        B --> D["Error\n= 5°"]
        D --> E["P Term\nKp × Error"]
        D --> F["I Term\nKi × Sum"]
        D --> G["D Term\nKd × Change"]
        E --> H["Add\nTerms"]
        F --> H
        G --> H
        H --> I["Motor\nPower"]
    end

    style A fill:#c8e6c9,stroke:#2e7d32
    style C fill:#bbdefb,stroke:#1565c0
    style D fill:#fff3e0,stroke:#ef6c00
    style I fill:#f8bbd9,stroke:#c2185b

Why Do We Need PID?

Bang-Bang vs PID Control Comparison

flowchart TB
    subgraph "Bang-Bang Control (Bad)"
        A1["Heading: 0°"] --> A2["Motors ON\n100%"]
        A2 --> A3["Heading: 85°"]
        A3 --> A4["Motors ON\n100%"]
        A4 --> A5["Heading: 90°"]
        A5 --> A6["Motors OFF"]
        A6 --> A7["OVERSHOOT!\n95°"]
        A7 --> A8["Turn back..."]
        A8 --> A9["UNDERSHOOT!\n88°"]
        A9 --> A10["Oscillates\nforever"]
    end

    style A7 fill:#ffcdd2,stroke:#c62828
    style A9 fill:#ffcdd2,stroke:#c62828
    style A10 fill:#ffcdd2,stroke:#c62828
flowchart TB
    subgraph "PID Control (Good)"
        B1["Heading: 0°"] --> B2["Motors at\n80%"]
        B2 --> B3["Heading: 50°"]
        B3 --> B4["Motors at\n40%"]
        B4 --> B5["Heading: 85°"]
        B5 --> B6["Motors at\n5%"]
        B6 --> B7["Heading: 90°"]
        B7 --> B8["Motors at\n0%"]
        B8 --> B9["Smooth stop!"]
    end

    style B9 fill:#c8e6c9,stroke:#2e7d32

The Key Difference

Control Type Behavior Result
Bang-Bang Full power until target Overshoot, oscillate
PID Power proportional to distance Smooth, accurate stop

The P in PID: Proportional

Proportional means the correction is proportional to the error. The farther you are from the target, the bigger the correction!

Error Calculation Flowchart

flowchart LR
    A["Target\n90°"] --> C{"-"}
    B["Current\n30°"] --> C
    C --> D["Error\n= 60°"]
    D --> E["× Kp\n(0.5)"]
    E --> F["Correction\n= 30%"]

    style A fill:#c8e6c9,stroke:#2e7d32
    style B fill:#bbdefb,stroke:#1565c0
    style D fill:#fff3e0,stroke:#ef6c00
    style F fill:#f8bbd9,stroke:#c2185b

The P Formula

Error = Target - Current

Correction = Kp × Error

Where Kp is a tuning constant (like 0.5)

Think of Kp as a "sensitivity dial": - Kp = 0.1 → Very gentle, slow response - Kp = 0.5 → Balanced response - Kp = 2.0 → Very aggressive, may overshoot

Example: P Controller for Turning

def turn_to_heading(target_heading):
    """Turn to a specific heading using P control."""
    Kp = 0.5  # Tuning constant

    while True:
        current = inertial_sensor.heading()
        error = target_heading - current

        # Handle wraparound (0-360)
        if error > 180:
            error -= 360
        if error < -180:
            error += 360

        # If close enough, stop
        if abs(error) < 2:
            left_motors.stop()
            right_motors.stop()
            break

        # Calculate correction
        correction = Kp * error

        # Apply to motors (turn in place)
        left_motors.spin(FORWARD, correction, PERCENT)
        right_motors.spin(FORWARD, -correction, PERCENT)

        wait(20, MSEC)

Tuning Kp

flowchart TB
    subgraph "Kp Effects Comparison"
        direction TB

        A["Kp = 0.1\n(Too Low)"]
        A1["❌ Slow response"]
        A2["❌ May not reach target"]
        A3["❌ Friction wins"]

        B["Kp = 0.5-1.0\n(Just Right)"]
        B1["✅ Quick response"]
        B2["✅ Minimal overshoot"]
        B3["✅ Accurate stop"]

        C["Kp = 2.0\n(Too High)"]
        C1["❌ Fast but overshoots"]
        C2["❌ Oscillates"]
        C3["❌ Jerky movement"]

        A --> A1 --> A2 --> A3
        B --> B1 --> B2 --> B3
        C --> C1 --> C2 --> C3
    end

    style A fill:#ffcdd2,stroke:#c62828
    style A1 fill:#ffcdd2,stroke:#c62828
    style A2 fill:#ffcdd2,stroke:#c62828
    style A3 fill:#ffcdd2,stroke:#c62828
    style B fill:#c8e6c9,stroke:#2e7d32
    style B1 fill:#c8e6c9,stroke:#2e7d32
    style B2 fill:#c8e6c9,stroke:#2e7d32
    style B3 fill:#c8e6c9,stroke:#2e7d32
    style C fill:#ffcdd2,stroke:#c62828
    style C1 fill:#ffcdd2,stroke:#c62828
    style C2 fill:#ffcdd2,stroke:#c62828
    style C3 fill:#ffcdd2,stroke:#c62828

What Each Symptom Means

Symptom Cause Fix
Robot never reaches target Kp too low Increase Kp
Robot oscillates around target Kp too high Decrease Kp
Robot stops perfectly Kp just right Keep it!

The I in PID: Integral

Integral accumulates error over time. It fixes steady-state error - when the robot gets close but can't quite reach the target.

The Friction Problem

flowchart LR
    subgraph "P-Only Problem"
        A["Target: 90°\nCurrent: 88°"] --> B["Error = 2°"]
        B --> C["Correction =\n0.5 × 2 = 1%"]
        C --> D["1% power\ncan't overcome\nfriction!"]
    end

    style D fill:#ffcdd2,stroke:#c62828

The I Term Solution

flowchart TB
    subgraph "Integral Accumulation"
        A["Error = 2°\nCycle 1"] --> B["integral = 2"]
        B --> C["Error = 2°\nCycle 2"]
        C --> D["integral = 4"]
        D --> E["Error = 2°\nCycle 3"]
        E --> F["integral = 6"]
        F --> G["..."]
        G --> H["Error = 2°\nCycle 10"]
        H --> I["integral = 20"]
        I --> J["Now Ki × 20\nOVERCOMES\nfriction!"]
    end

    style J fill:#c8e6c9,stroke:#2e7d32

Think of it like pushing a heavy box: - One small push (P) isn't enough - But 10 small pushes adding up (I) finally moves it!

PI Controller

def turn_to_heading_pi(target_heading):
    """Turn using PI control."""
    Kp = 0.5
    Ki = 0.01  # Small integral gain

    integral = 0

    while True:
        current = inertial_sensor.heading()
        error = target_heading - current

        # Handle wraparound
        if error > 180:
            error -= 360
        if error < -180:
            error += 360

        # Accumulate error
        integral += error

        # Stop condition
        if abs(error) < 2:
            left_motors.stop()
            right_motors.stop()
            break

        # PI calculation
        correction = (Kp * error) + (Ki * integral)

        left_motors.spin(FORWARD, correction, PERCENT)
        right_motors.spin(FORWARD, -correction, PERCENT)

        wait(20, MSEC)

For beginners: Skip the I term at first! P-only often works well enough.

The D in PID: Derivative

Derivative predicts future error based on rate of change.

    Error is decreasing rapidly?
    → Don't overcorrect, you're about to reach target!

    Error is increasing?
    → Something's wrong, add more correction!

Full PID (Optional)

def turn_to_heading_pid(target_heading):
    """Full PID control (advanced)."""
    Kp = 0.5
    Ki = 0.01
    Kd = 0.1

    integral = 0
    previous_error = 0

    while True:
        current = inertial_sensor.heading()
        error = target_heading - current

        # Handle wraparound
        if error > 180:
            error -= 360
        if error < -180:
            error += 360

        # Calculate I and D terms
        integral += error
        derivative = error - previous_error
        previous_error = error

        # Stop condition
        if abs(error) < 2:
            left_motors.stop()
            right_motors.stop()
            break

        # Full PID
        correction = (Kp * error) + (Ki * integral) + (Kd * derivative)

        left_motors.spin(FORWARD, correction, PERCENT)
        right_motors.spin(FORWARD, -correction, PERCENT)

        wait(20, MSEC)

PID Tuning Guide

Step-by-Step Tuning Flowchart

flowchart TD
    START["START HERE\nKp = 0.5, Ki = 0, Kd = 0\n(P-only control)"]
    START --> TEST1

    TEST1{"Does robot\nOVERSHOOT?"}
    TEST1 -->|"YES"| FIX1["Decrease Kp\n(try 0.3)"]
    TEST1 -->|"NO"| TEST2
    FIX1 --> TEST1

    TEST2{"Is robot\nTOO SLOW?"}
    TEST2 -->|"YES"| FIX2["Increase Kp\n(try 0.7)"]
    TEST2 -->|"NO"| TEST3
    FIX2 --> TEST1

    TEST3{"Does robot\nNOT REACH target?\n(steady-state error)"}
    TEST3 -->|"YES"| FIX3["Add small Ki\n(try 0.01)"]
    TEST3 -->|"NO"| DONE1["P-only works!\nYou're done!"]
    FIX3 --> TEST4

    TEST4{"Does robot now\nOVERSHOOT more?"}
    TEST4 -->|"YES"| FIX4["Decrease Ki\n(try 0.005)"]
    TEST4 -->|"NO"| DONE2["PI works!\nYou're done!"]
    FIX4 --> TEST4

    style START fill:#e3f2fd,stroke:#1565c0
    style DONE1 fill:#c8e6c9,stroke:#2e7d32
    style DONE2 fill:#c8e6c9,stroke:#2e7d32
    style FIX1 fill:#fff3e0,stroke:#ef6c00
    style FIX2 fill:#fff3e0,stroke:#ef6c00
    style FIX3 fill:#fff3e0,stroke:#ef6c00
    style FIX4 fill:#fff3e0,stroke:#ef6c00

Quick Reference Starting Values

Robot Type Kp Start Ki Start Notes
Turning in place 0.5-1.0 0.01 Most common
Driving straight 0.3-0.5 0.005 Less aggressive
Arm movement 0.2-0.4 0.01 Avoid jerky motion

Summary

Term What It Does When to Use
P Correction proportional to error Always (start here)
I Fixes steady-state error If robot can't reach target
D Dampens oscillation If robot overshoots

PID in Push Back Competition

PID control is essential for competitive Push Back robots. Here's how each use case helps you score more points:

Accurate Autonomous Turns

def push_back_autonomous():
    """
    Use PID for precise turns to line up with goals.
    Accurate turns = More blocks in the goal!
    """
    # Turn exactly 45° to face the long goal
    turn_to_heading_p(45)  # P controller

    # Drive forward and push blocks
    drivetrain.drive_for(FORWARD, 600, MM)

    # Turn exactly 90° to face center goal
    turn_to_heading_p(135)

    # More accurate turns = More points!

Straight-Line Block Pushing

def push_blocks_straight():
    """
    Use PID to drive STRAIGHT while pushing blocks.
    Without PID, blocks push you off course!
    """
    start_heading = inertial_sensor.heading()
    Kp = 0.5

    while distance_to_goal > 100:
        # Calculate heading error
        current = inertial_sensor.heading()
        error = start_heading - current

        # Handle wraparound
        if error > 180: error -= 360
        if error < -180: error += 360

        correction = Kp * error

        # Apply correction to stay straight
        left_motors.spin(FORWARD, 50 + correction, PERCENT)
        right_motors.spin(FORWARD, 50 - correction, PERCENT)

        wait(20, MSEC)

Precise Parking

flowchart LR
    A["Far from\npark zone"] --> B["Fast\napproach"]
    B --> C["Getting\nclose"]
    C --> D["Slow\ndown"]
    D --> E["Almost\nthere"]
    E --> F["Gentle\nstop"]
    F --> G["PARKED!\n8-30 pts 🎯"]

    style G fill:#c8e6c9,stroke:#2e7d32
def pid_park():
    """
    Use PID to park smoothly in the zone.
    Smooth parking = No rolling out of zone!
    """
    target_distance = 50  # Stop 50mm from back wall
    Kp = 0.3  # Gentle for parking

    while True:
        dist = distance_sensor.object_distance(MM)
        error = dist - target_distance

        if abs(error) < 10:  # Within 10mm
            drivetrain.stop()
            return True  # Parked!

        speed = Kp * error
        speed = max(-30, min(30, speed))  # Slow for parking

        drivetrain.drive(FORWARD, speed, PERCENT)
        wait(20, MSEC)

Progressive Exercises

Beginner: P-Only Turn

Goal: Make the robot turn to exactly 90° using P control.

def turn_to_90():
    Kp = 0.5
    target = 90

    inertial_sensor.calibrate()
    wait(3, SECONDS)

    while True:
        error = target - inertial_sensor.heading()

        if abs(error) < 2:
            break

        correction = Kp * error

        # YOUR CODE: Apply correction to motors
        # Hint: left_motors.spin(FORWARD, correction, PERCENT)
        # Hint: right_motors.spin(FORWARD, -correction, PERCENT)

        wait(20, MSEC)

    # YOUR CODE: Stop the motors

Success criteria: Robot turns smoothly and stops at 90° ±2°


Intermediate: Add Wraparound Handling

Goal: Handle the 0-360° boundary problem.

Problem: What if current = 350° and target = 10°? - Simple math: error = 10 - 350 = -340° (wrong!) - Correct: The shortest turn is +20° (right!)

def turn_to_heading(target):
    Kp = 0.5

    while True:
        current = inertial_sensor.heading()
        error = target - current

        # YOUR CODE: Add wraparound handling
        # If error > 180, subtract 360
        # If error < -180, add 360

        if abs(error) < 2:
            left_motors.stop()
            right_motors.stop()
            break

        correction = Kp * error
        left_motors.spin(FORWARD, correction, PERCENT)
        right_motors.spin(FORWARD, -correction, PERCENT)

        wait(20, MSEC)

Test cases: - Turn from 10° to 350° (should turn left 20°, not right 340°) - Turn from 350° to 10° (should turn right 20°, not left 340°)


Challenge: PI Control for Accurate Parking

Goal: Use PI control to overcome friction and park accurately.

Scenario: Your robot needs to park exactly 100mm from the back wall. P-only control stops 5mm short because of friction.

def pi_park(target_distance=100):
    Kp = 0.3
    Ki = 0.01
    integral = 0
    tolerance = 5  # mm

    while True:
        current = distance_sensor.object_distance(MM)
        error = current - target_distance

        # YOUR CODE: Accumulate integral
        # integral += error

        if abs(error) < tolerance:
            drivetrain.stop()
            return True

        # YOUR CODE: Calculate PI correction
        # correction = (Kp * error) + (Ki * integral)

        # Limit speed for parking
        correction = max(-30, min(30, correction))

        drivetrain.drive(FORWARD, correction, PERCENT)
        wait(20, MSEC)

Bonus challenge: Add "integral windup protection" - reset integral when error changes sign!


Common Mistakes with PID

Mistake 1: Wrong Kp Sign

# WRONG: Negative Kp causes wrong direction!
correction = -0.5 * error  # Robot turns away from target!

# RIGHT: Positive Kp
correction = 0.5 * error   # Robot turns toward target

Mistake 2: Missing Wraparound Handling

# WRONG: No wraparound - robot spins 340° instead of 20°
error = target - current

# RIGHT: Handle 0-360 boundary
error = target - current
if error > 180:
    error -= 360
elif error < -180:
    error += 360

Mistake 3: No Stop Condition

# WRONG: Loop never ends!
while True:
    error = target - inertial_sensor.heading()
    correction = Kp * error
    left_motors.spin(FORWARD, correction, PERCENT)
    # No break condition! Robot vibrates forever!

# RIGHT: Stop when close enough
while True:
    error = target - inertial_sensor.heading()
    if abs(error) < 2:  # Tolerance of 2°
        left_motors.stop()
        right_motors.stop()
        break
    # ... rest of code

Mistake 4: Correction Too Powerful

# WRONG: No speed limiting - motors max out
correction = Kp * error  # Could be 100%+ !

# RIGHT: Clamp correction to safe range
correction = Kp * error
correction = max(-50, min(50, correction))  # Limit to ±50%

Mistake 5: Starting with Full PID

# WRONG: Start with all three terms
Kp = 0.5
Ki = 0.1
Kd = 0.5  # Too complex to tune!

# RIGHT: Start P-only, add terms as needed
Kp = 0.5
Ki = 0    # Add later if needed
Kd = 0    # Rarely needed for VEX

How PID Connects to Push Back

Push Back Task PID Use Why It Helps
Autonomous turns P control for heading Accurate alignment with goals
Straight driving P control for heading correction Blocks don't push you off course
Goal approach P control for distance Stop at right distance to push
Parking PI control for precision Guarantee those 8-30 points!
Block pushing P control while driving Maintain heading under load

Push Back Points Gained with PID

flowchart LR
    subgraph "Without PID"
        A1["Miss 2 goals\n-6 points"] --> B1["Overshoot park\n-8 points"]
        B1 --> C1["Push blocks\ncrooked"] --> D1["Lose zone\ncontrol"]
    end

    subgraph "With PID"
        A2["Hit all goals\n+6 points"] --> B2["Perfect park\n+8 points"]
        B2 --> C2["Straight\npushing"] --> D2["Win zone\ncontrol!"]
    end

    style A1 fill:#ffcdd2,stroke:#c62828
    style B1 fill:#ffcdd2,stroke:#c62828
    style A2 fill:#c8e6c9,stroke:#2e7d32
    style B2 fill:#c8e6c9,stroke:#2e7d32
    style D2 fill:#c8e6c9,stroke:#2e7d32

Bottom line: PID can easily add 15-20 points to your match score through: - More accurate autonomous routines - Reliable parking - Better zone control


← Previous: Alliance Coordination | Next: Sensor Integration → | Review Q&A