VEX V5 PYTHON | 05.1 PROPORTIONAL CONTROL
Guide objective
Use measured distance and heading error to adjust motor output continuously, then test and tune bounded autonomous movement functions.
01 | WHY USE FEEDBACK?
A timed drive command assumes that a certain output for a certain time produces a certain movement. Battery state, friction and load can change the result.
Closed-loop control repeatedly measures movement, compares it with a target and adjusts the command. Proportional control is a useful first controller because its correction is directly related to the remaining error.
Imagine walking toward a marked line. Far away, you can take large steps. Near the line, you take smaller steps. If you pass it, you step back. A P controller follows a similar idea, using sensor feedback.
02 | TARGET, MEASUREMENT AND ERROR
Error = target - measurement. Keep the signs and units consistent.
| Target distance | Measured distance | Error | Meaning |
|---|---|---|---|
| 500 mm | 100 mm | +400 mm | More forward movement needed |
| 500 mm | 450 mm | +50 mm | Approaching the target |
| 500 mm | 500 mm | 0 mm | At the measured target |
| 500 mm | 530 mm | -30 mm | Passed the target; correction reverses |
For a reverse move, use a negative target and signed encoder change. A target of -300 mm with a measurement of -100 mm gives an error of -200 mm.
Taking the absolute value too early loses direction. Use absolute error only when deciding whether you are within tolerance.
03 | THE PROPORTIONAL RULE
Output = Kp × error. Kp is the proportional gain. It determines how strongly the robot responds to a given error.
| Distance error | Kp | Calculated output |
|---|---|---|
| 400 mm | 0.08 percent/mm | 32% forward |
| 100 mm | 0.08 percent/mm | 8% forward |
| 20 mm | 0.08 percent/mm | 1.6% forward |
| -30 mm | 0.08 percent/mm | 2.4% reverse |
A distance gain measured in percent per millimetre differs from a turn gain measured in percent per degree. Do not reuse the same numerical gain without understanding its units.
The output here is a motor velocity percentage request. It is not a direct measurement of wheel speed or a fixed percentage of motor torque.
04 | LIMIT OUTPUT AND ACCEPT A TOLERANCE
A large initial error can produce an excessive command. Clamp output to a tested cap. With a cap of 25%, a calculated 32% request becomes 25%.
Exact zero error is usually an impractical finish condition. Sensor resolution, small movements and friction can keep the value changing. Instead, define a working tolerance such as ±10 mm or ±2 degrees, then check that the measurement stays inside it for a settling interval.
The starter stops output inside the tolerance window and requires both sides to stay there for 200 ms. If either leaves, correction resumes and the settling timer resets.
These are teaching values. Select tolerances from the physical accuracy your task needs, not simply to make a test pass.
05 | DISTANCE FEEDBACK FROM BOTH SIDES
Convert signed motor turns into estimated wheel travel:
Distance = motor turns × wheel turns per motor turn × wheel circumference. Wheel circumference is π × effective diameter.
The starter averages the two motor readings on each side, records each side’s starting value and controls left and right distance errors separately. It does not reset encoders between calls.
Checking only the average of the left and right sides can hide a problem: 400 mm on one side and 600 mm on the other average to 500 mm. This example requires both sides to reach their target window.
Motor encoders measure rotation, not ground position. Wheel slip can produce apparently successful feedback while the robot stops in the wrong place. Independent side control is not a replacement for heading correction or odometry.
06 | TURNING AND HEADING WRAP
Heading wraps around at 360 degrees. A target of 10 degrees from a current heading of 350 degrees needs a small clockwise turn, not a 340-degree anticlockwise turn.
def heading_error(target, current):
return (target - current + 180) % 360 - 180
print(heading_error(10, 350)) # +20 degrees
print(heading_error(350, 10)) # -20 degreesThe result lies from -180 up to, but excluding, +180 degrees. The exact 180-degree tie chooses the negative direction in this formula.
For verified clockwise-positive heading, a positive error drives the left side forward and the right side backward. Check your sensor convention and motor directions before testing a turn.
turn_by_p accepts relative requests smaller than 180 degrees. Multi-turn rotation targets need a different controller using accumulated rotation rather than wrapped heading.
07 | CONFIGURE AND TEST DIRECTIONS FIRST
- Confirm the four motor ports and cartridge types match the example.
- Measure wheel diameter and external gearing; enter the ratio as wheel turns per motor turn.
- With a supported robot and a separate low-output test, verify that positive commands move both sides forward and make both encoder distances increase.
- Verify that a clockwise turn increases the configured inertial heading.
- Calibrate the inertial sensor while stationary and disabled.
- Only then enable MOTION_CONFIGURED and start short, clear-space tests.
The project starts with movement blocked. A reversed measurement creates positive feedback: the robot moves away from the target and error grows. Stop and fix the direction relationship rather than increasing Kp.
08 | COMPLETE P CONTROL STARTER
Place this below # Begin project code, outside the generated region. Keep VEX imports and the generated Brain above it. Remove conflicting Devices declarations or use one consistent configuration approach.
The example has autonomous movement only; driver mode stops the drivetrain. The functions check enable state, autonomous mode, Controller B and sensor readiness, and stop on ordinary return or exception.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | P CONTROL STARTER
# =============================================================================
# Keep generated imports and brain above '# Begin project code'.
# Manual configuration below: do not duplicate these devices in Devices.
# Left motors: ports 3 and 4; right motors: ports 1 and 2.
# Blue cartridges. Reversal settings must match your physical robot.
# Inertial: Smart Port 20. Controller B cancels movement.
import math
left_motor_1 = Motor(Ports.PORT3, GearSetting.RATIO_6_1, False)
left_motor_2 = Motor(Ports.PORT4, GearSetting.RATIO_6_1, False)
right_motor_1 = Motor(Ports.PORT1, GearSetting.RATIO_6_1, True)
right_motor_2 = Motor(Ports.PORT2, GearSetting.RATIO_6_1, True)
left_drive = MotorGroup(left_motor_1, left_motor_2)
right_drive = MotorGroup(right_motor_1, right_motor_2)
inertial = Inertial(Ports.PORT20)
controller = Controller(PRIMARY)
# =============================================================================
# MEASURED GEOMETRY AND STARTER GAINS (EXAMPLES, NOT TUNED VALUES)
# =============================================================================
MOTION_CONFIGURED = False # Enable after direction/geometry checks
WHEEL_DIAMETER_MM = 100.0 # Replace with effective measured diameter
WHEEL_TURNS_PER_MOTOR_TURN = 1.0 # Direct drive = 1; external gearing changes it
DRIVE_KP = 0.08 # Percent output per mm of error
TURN_KP = 0.7 # Percent output per degree of error
MAX_OUTPUT = 25.0 # Conservative test cap
DRIVE_TOL_MM = 10.0
TURN_TOL_DEG = 2.0
SETTLE_MS = 200
LOOP_MS = 20
ready = False
# =============================================================================
# SHARED OUTPUT AND FEEDBACK
# =============================================================================
def clamp(value, low, high):
return max(low, min(high, value))
def stop_drive():
left_drive.stop(BRAKE)
right_drive.stop(BRAKE)
def side_output(group, value):
if value == 0:
group.stop(BRAKE)
else:
group.spin(FORWARD if value > 0 else REVERSE, abs(value), PERCENT)
def heading_error(target, current):
return (target - current + 180) % 360 - 180
def side_mm(motor_a, motor_b):
turns = (motor_a.position(TURNS) + motor_b.position(TURNS)) / 2
return turns * math.pi * WHEEL_DIAMETER_MM * WHEEL_TURNS_PER_MOTOR_TURN
def permitted():
return (ready and MOTION_CONFIGURED and competition.is_enabled()
and competition.is_autonomous()
and not controller.buttonB.pressing()
and inertial.installed() and not inertial.is_calibrating())
# =============================================================================
# SHARED BOUNDED P LOOP
# errors_fn returns (left_error, right_error) in mm or degrees.
# =============================================================================
def run_p(errors_fn, kp, tolerance, timeout_ms):
stop_drive()
if kp <= 0 or tolerance <= 0 or timeout_ms <= 0:
return "invalid configuration"
started = brain.timer.time(MSEC)
settled_since = None
try:
while True:
if not permitted():
return "cancelled or not ready"
now = brain.timer.time(MSEC)
if now - started >= timeout_ms:
return "timeout"
left_error, right_error = errors_fn()
if not math.isfinite(left_error) or not math.isfinite(right_error):
return "invalid feedback"
inside = (abs(left_error) <= tolerance
and abs(right_error) <= tolerance)
if inside:
stop_drive()
if settled_since is None:
settled_since = now
if now - settled_since >= SETTLE_MS:
return "settled"
else:
settled_since = None
left_u = clamp(kp * left_error, -MAX_OUTPUT, MAX_OUTPUT)
right_u = clamp(kp * right_error, -MAX_OUTPUT, MAX_OUTPUT)
side_output(left_drive, left_u)
side_output(right_drive, right_u)
wait(LOOP_MS, MSEC)
except Exception:
return "feedback or command error"
finally:
stop_drive()
# =============================================================================
# REUSABLE MOVEMENT FUNCTIONS
# =============================================================================
def drive_distance_p(distance_mm, timeout_ms=4000):
left_start = side_mm(left_motor_1, left_motor_2)
right_start = side_mm(right_motor_1, right_motor_2)
def errors():
left_delta = side_mm(left_motor_1, left_motor_2) - left_start
right_delta = side_mm(right_motor_1, right_motor_2) - right_start
return distance_mm - left_delta, distance_mm - right_delta
return run_p(errors, DRIVE_KP, DRIVE_TOL_MM, timeout_ms)
def turn_to_heading_p(target_deg, timeout_ms=3000):
target = target_deg % 360
def errors():
error = heading_error(target, inertial.heading(DEGREES))
# Positive heading error: turn clockwise.
return error, -error
return run_p(errors, TURN_KP, TURN_TOL_DEG, timeout_ms)
def turn_by_p(angle_deg, timeout_ms=3000):
# Shortest-path relative turn; restrict requested angle to under 180.
if abs(angle_deg) >= 180:
stop_drive()
return "relative angle must be less than 180"
return turn_to_heading_p(inertial.heading(DEGREES) + angle_deg, timeout_ms)
# =============================================================================
# COMPETITION ENTRY POINTS: ONE OWNER OF DRIVE OUTPUT
# =============================================================================
def autonomous():
stop_drive()
if not permitted():
return
for name, action in (
("forward", lambda: drive_distance_p(500)),
("right turn", lambda: turn_by_p(90)),
("reverse", lambda: drive_distance_p(-250))
):
result = action()
print(name, result)
if result != "settled":
break # Do not continue the route after a failed move.
wait(300, MSEC)
stop_drive()
def driver_control():
# Teaching project: intentionally no driver movement.
stop_drive()
competition = Competition(driver_control, autonomous)
stop_drive()
# Calibrate while disabled and stationary, before autonomous is enabled.
# calibrate() may block; movement timeouts do not cover this setup call.
if MOTION_CONFIGURED and not competition.is_enabled() and inertial.installed():
inertial.calibrate()
ready = not inertial.is_calibrating() and not competition.is_enabled()
print("P controller ready:", ready)
while True:
wait(100, MSEC)
Run calibration before enabling autonomous. If setup starts enabled, ready stays false and the route remains blocked. Restart disabled to perform the setup correctly.
Timeouts bound the movement loops. They do not supervise the blocking calibration call. A forced task termination may also prevent ordinary finally cleanup, so mode transitions and output ownership still need deliberate testing.
In your team project, merge these functions into the existing competition structure rather than creating a second Competition object or a competing drivetrain thread.
09 | READ THE RETURN STATUS
A movement returns a status, not just a delay. The sample stops the sequence after any result other than settled.
| Status | Meaning / response |
|---|---|
| settled | Feedback remained within tolerance for the required interval |
| timeout | Target was not settled before the deadline; investigate |
| cancelled or not ready | B, mode, enable or readiness guard blocked movement |
| invalid feedback | A non-finite error was detected; no normal movement |
| feedback or command error | An API read or command raised an exception |
Settled means the feedback passed the criterion. Compare the robot’s physical final position too. The starter has no dedicated stall detector; a blocked move relies on cancellation or timeout.
10 | TUNE ONE VARIABLE AT A TIME
- Keep the output cap low and use the same starting mark and target.
- Start with a small positive gain.
- Record completion time, final physical error and whether the robot oscillates.
- If response is slow, increase Kp in small steps.
- If overshoot or repeated corrections grow, reduce Kp and investigate stopping behaviour.
- Repeat forward, reverse and turns in both directions.
Change distance and turn gains separately. Keep the loop period consistent so trials are comparable.
A P controller can stall near the target because its small command cannot overcome friction. Increasing Kp may help but can worsen overshoot. Do not quietly add a large minimum output: it can cause persistent hunting around the target. Later lessons explore additional control terms and feedforward.
11 | COMMON MISTAKES
| Mistake | Why it matters |
|---|---|
| Using absolute error for output | Loses correction direction |
| Mixing mm and motor degrees | Gives the gain the wrong scale |
| Only checking average side distance | Can hide unequal wheel travel |
| Raw target minus heading | Chooses a long turn across zero |
| Huge Kp with a low output cap | May stay saturated until close to the target |
| Single in-tolerance sample | May declare success while crossing the target |
| Tuning on a lifted robot only | Does not reproduce floor friction and load |
| Ignoring timeout status | Continues a route from an unknown position |
CHALLENGES | BUILD AND TUNE FEEDBACK
- Calculate: find signed error and output for a 600 mm target at 250, 580 and 620 mm.
- Limit: apply a 25% cap and explain which commands saturate.
- Wrap: predict turn errors for 350 to 10 and 10 to 350 degrees.
- Verify: document motor directions, encoder signs and wheel conversion.
- Test: compare three 500 mm trials and three reverse trials.
- Tune: compare two distance gains while holding other settings constant.
- Cancel: check B and mode changes during a controlled short test.
- Evaluate: distinguish feedback success from physical arrival.
Success evidence
A labelled configuration, saved code, signed-error calculations and test log. Record target, Kp, cap, return status, completion time and measured final position. Explain one tuning decision using repeated trials.
OFFICIAL REFERENCES
The P controller, gains and challenges are independently written teaching examples. VEX and VEXcode are trademarks of their respective owners.