VEX V5 PYTHON | 03.1 SLEW RATE LIMITING
Guide objective
Limit how quickly drivetrain commands change, tune smooth starts and keep stop requests responsive with a time-based Python limiter.
01 | WHY A SUDDEN COMMAND CAN BE A PROBLEM
Imagine moving a joystick straight from centre to full forward. Without smoothing, your code can change its requested motor velocity in one update. A sudden change may cause wheel slip, a jerky start or a less controllable robot.
Slew rate limiting limits how quickly a command changes. Instead of jumping to the requested value, the output approaches it in small steps.
This limits the command, not measured physical acceleration. Robot mass, load, gearing, traction and the motor’s own control system still affect the motion.
02 | TARGET VS CURRENT OUTPUT
The target is what the joystick currently asks for. The current output is the value your limiter last sent to the motor.
If the target is 30% and the current output is 0%, move the output towards 30 over several updates. If the target changes while ramping, approach the new target rather than finishing an outdated ramp.
Store output separately for the left and right sides. Recreating it as zero on every loop would make the ramp start over repeatedly.
03 | USE PERCENTAGE POINTS PER SECOND
A rate of 60 percentage points per second means the command may change by 60 points in one second. This is different from increasing it by 60% of its current value.
maximum change = rate × elapsed seconds 60 × 0.020 = 1.2 percentage points
| Elapsed time | Maximum change at 60 points/s |
|---|---|
| 20 ms | 1.2 points |
| 40 ms | 2.4 points |
| 50 ms | 3.0 points |
Use measured elapsed time so the rate does not depend on an assumed loop frequency. The starter caps a delayed update at 50 ms, preventing one long delay from causing a large output jump. After a delay, the ramp therefore advances less than a strict wall-clock ramp would.
04 | WORK THROUGH A START
| Time after request | Target | Ideal limited output |
|---|---|---|
| 0 ms | 30% | 0% |
| 100 ms | 30% | 6% |
| 200 ms | 30% | 12% |
| 300 ms | 30% | 18% |
| 400 ms | 30% | 24% |
| 500 ms | 30% | 30% |
With a constant target and regular updates, time to target is approximately absolute command change ÷ slew rate. From 0 to 30 at 60 points/s takes 0.5 seconds.
The starter limits normal driver commands to 30%, so pushing the joystick fully forward requests 30%, not 100%. It ramps the final mixed motor commands after applying that limit.
05 | THE REUSABLE LIMITER
def slew(current, target, rate_pps, dt_seconds):
max_change = rate_pps * dt_seconds
change = target - current
if abs(change) <= max_change:
return target
if change > 0:
return current + max_change
return current - max_change
Calculate the allowed change, then compare it with the remaining gap. If the gap fits within this update, return the target exactly. Otherwise move one allowed step towards it.
The function works for positive and negative values. It never deliberately overshoots the target. For example, output 29.5 and target 30 become exactly 30 rather than 30.7.
Here rate_pps must be positive and dt_seconds non-negative. The complete starter supplies fixed settings and a bounded elapsed interval.
06 | WHERE THE LIMITER FITS
- Read the left stick for forward/back and right stick for turning.
- Apply the deadband and turning gain.
- Mix and scale to obtain left and right targets.
- Check immediate stop conditions.
- Ramp each output towards its target.
- Send the outputs and save them for the next update.
Do not reset the stored outputs during normal driving. Do reset them when stopping or entering a new driver-control session, so the next start begins from zero.
Limiting each side independently is straightforward, but during a transition it may change the requested left/right ratio and therefore the curve. Precise autonomous trajectories need more than this driver-control starter.
07 | STOP REQUESTS BYPASS THE RAMP
A symmetric limiter would also slow the command’s return to zero. That could leave the robot driving after the sticks are centred. This starter chooses an explicit policy: centre both driving inputs or hold B to stop immediately in code.
The code issues a brake stop and resets both stored outputs. The robot still has physical momentum, so this is not a claim of instantaneous ground stopping.
B is a held software stop. If you release B while the sticks remain displaced, driving resumes from zero with a fresh ramp. For a latched cancellation, use the pattern from 02.6.
Competition disable bypasses normal ramping as well. Verify it with your actual setup. Do not use smoothing to delay a limit-switch stop, fault response or emergency action.
08 | HANDLE A DIRECTION CHANGE
def ramp_side(current, target, dt_seconds):
# Finish the ramp towards zero before starting the opposite direction.
if current * target < 0:
target = 0
return slew(current, target, SLEW_RATE_PPS, dt_seconds)
If the current command and target have opposite signs, first ramp that side towards zero. On a later update, it can start ramping in the new direction. This avoids a direct positive-to-negative jump.
This is command handling, not a measured standstill detector. Reaching zero output does not prove the wheel has stopped physically. A mechanism that must stop before reversing needs measured velocity or another appropriate confirmation.
If just one mixed side target becomes zero while the robot is still being steered, that side ramps down normally. Centring both driving inputs triggers the immediate stop policy.
09 | COMPLETE SLEW-LIMITED DRIVER PROJECT
Keep the generated VEX imports and Brain setup above this block. Paste below # Begin project code, outside the generated region, replacing the previous driver starter.
Four motors are explicitly declared: left ports 3 and 4, right ports 1 and 2. Match the example cartridges and reversal flags to your robot. Use either manual declarations or Devices-panel definitions for each device, not both.
This guide focuses on driver control. Its autonomous callback does not move the robot; no inertial sensor is required for this example.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER]
# =============================================================================
# ROBOT CONFIGURATION
# - FOUR-motor drivetrain:
# Left = Ports 3 and 4
# Right = Ports 1 and 2
# - Example: blue cartridges, left normal, right reversed.
# VERIFY your actual ports, cartridges and direction settings.
# DRIVER CONTROLS
# - Left joystick vertical (axis3): forward/back
# - Right joystick horizontal (axis1): turn
# - This program MOVES motors in enabled driver-control mode.
# - Add BELOW # Begin project code, outside generated configuration.
# - Do not also define these drive motors in the Devices panel.
# - Keep generated from vex import * and brain = Brain() above.
# =============================================================================
# FOUR-MOTOR DRIVETRAIN
# =============================================================================
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)
# =============================================================================
# CONTROLLER, SETTINGS AND HELPERS
# =============================================================================
controller = Controller(PRIMARY) # Do not duplicate a generated Controller
DEADBAND = 5
DRIVE_LIMIT_PERCENT = 30 # Start low; increase only after testing
TURN_GAIN = 0.7
LOOP_DELAY_MS = 20
SLEW_RATE_PPS = 60 # Percentage points per second
MAX_RAMP_DT_SECONDS = 0.05 # Cap a delayed update to a 50 ms ramp step
DRIVER_STOP_MODE = BRAKE
# Controller B requests an immediate software stop while held.
def apply_deadband(value):
return 0 if abs(value) <= DEADBAND else value
def drive_mix(forward, turn):
left = forward + turn
right = forward - turn
largest = max(100, abs(left), abs(right))
left = left * 100 / largest
right = right * 100 / largest
return (left * DRIVE_LIMIT_PERCENT / 100,
right * DRIVE_LIMIT_PERCENT / 100)
def set_side_output(side, output):
if abs(output) < 0.01:
side.stop(DRIVER_STOP_MODE)
elif output > 0:
side.spin(FORWARD, output, PERCENT)
else:
side.spin(REVERSE, -output, PERCENT)
def slew(current, target, rate_pps, dt_seconds):
max_change = rate_pps * dt_seconds
change = target - current
if abs(change) <= max_change:
return target
if change > 0:
return current + max_change
return current - max_change
def ramp_side(current, target, dt_seconds):
# Finish the ramp towards zero before starting the opposite direction.
if current * target < 0:
target = 0
return slew(current, target, SLEW_RATE_PPS, dt_seconds)
def show_status(message):
brain.screen.clear_screen()
brain.screen.set_cursor(1, 1)
brain.screen.print(message)
def stop_drive():
left_drive.stop(DRIVER_STOP_MODE)
right_drive.stop(DRIVER_STOP_MODE)
# =============================================================================
# PRE-AUTON: CALLED ONCE AT PROJECT STARTUP
# =============================================================================
def pre_autonomous():
stop_drive()
show_status("Setup ready")
# Later: initialise sensors and starting values here.
# Finish setup before the robot is enabled for a match.
# =============================================================================
# AUTONOMOUS: A FINITE ROUTINE
# =============================================================================
def autonomous():
show_status("Autonomous")
# Later: add your tested autonomous action sequence here.
# No autonomous movement in this starter.
stop_drive()
# =============================================================================
# DRIVER CONTROL: REPEATING UPDATES WHILE ENABLED
# =============================================================================
def driver_control():
left_output = 0
right_output = 0
previous_ms = brain.timer.time(MSEC)
stop_drive()
show_status("Slew-limited driver control")
try:
while competition.is_enabled() and competition.is_driver_control():
now_ms = brain.timer.time(MSEC)
dt_seconds = max(0, (now_ms - previous_ms) / 1000)
previous_ms = now_ms
dt_seconds = min(dt_seconds, MAX_RAMP_DT_SECONDS)
forward = apply_deadband(controller.axis3.position())
turn = apply_deadband(controller.axis1.position()) * TURN_GAIN
left_target, right_target = drive_mix(forward, turn)
if controller.buttonB.pressing() or (forward == 0 and turn == 0):
# A deliberate stop bypasses the normal ramp.
stop_drive()
left_output = 0
right_output = 0
else:
left_output = ramp_side(left_output, left_target, dt_seconds)
right_output = ramp_side(right_output, right_target, dt_seconds)
set_side_output(left_drive, left_output)
set_side_output(right_drive, right_output)
wait(LOOP_DELAY_MS, MSEC)
finally:
stop_drive()
# =============================================================================
# PROGRAM START: SETUP, THEN CALLBACK REGISTRATION
# =============================================================================
pre_autonomous()
competition = Competition(driver_control, autonomous)
# Keep the main task alive without running a driving loop here.
while True:
wait(100, MSEC)
Do not reset the shared Brain timer during the driver loop. The elapsed-time calculation uses differences between successive readings.
The finally block provides cleanup on a normal return or Python exception. Do not assume it runs on forced termination; verify competition disable and project stop behaviour separately.
10 | TUNE FOR THE DRIVER AND BUILD
| Slew rate | Ideal 0 to 30% time | Likely command feel |
|---|---|---|
| 30 points/s | 1.0 s | Gentler, slower response |
| 60 points/s | 0.5 s | Starting comparison setting |
| 120 points/s | 0.25 s | More responsive, sharper change |
A smaller rate gives a longer ramp; a larger rate reaches the target sooner. Neither is automatically best. Ask the driver to compare repeatable starts, turns and reversals, and look for slipping or instability.
Use three trials per setting. Keep surface, battery state, payload and driver limit comparable. Retest after changing gearing or robot mass.
This symmetric normal-movement limiter uses one rate for increases and decreases. Later guides can separate acceleration, deceleration and braking policies.
11 | USING THE IDEA IN AUTONOMOUS
Slew limiting can also smooth a continuously updated autonomous output, but it must be integrated into the movement controller. A sequence of timed spin commands alone does not provide distance accuracy.
Do not ramp speed in a separate task that competes with an active SmartDrive command. Choose one owner for drivetrain output, and combine its target tracking, rate limit, deadline and stop conditions in the same control design.
A full motion profile plans how speed changes across a movement, including slowing near the target. A slew limiter only bounds changes in the command it receives.
12 | COMMON MISTAKES
- Output reset inside the loop: the ramp never builds properly.
- Using milliseconds as seconds: the allowed change becomes 1,000 times too large.
- Fixed steps with a changing loop interval: acceleration feel changes with timing.
- Smoothing a stop request: movement continues when immediate shutdown is intended.
- Using measured RPM as percent state: different units and different meanings are mixed.
- Assuming smooth output guarantees traction: physical testing is still required.
CHALLENGES | MEASURE AND TUNE THE RAMP
- Calculate: find the allowed change at 80 points/s over 25 ms.
- Trace: calculate the first five outputs from zero towards 30 with regular 20 ms updates at 60 points/s.
- Inspect: identify where left and right outputs persist between updates.
- Compare: test 30, 60 and 120 points/s at the same low output limit.
- Stop: verify that centring both sticks and holding B bypass the ramp.
- Reverse: explain how the output approaches zero before changing sign.
- Record: document three controlled trials and a justified driver setting.
- Evaluate: explain why command smoothing is not a complete autonomous motion profile.
Check the calculations
80 × 0.025 = 2 percentage points. The first five ideal 20 ms ramp updates are 1.2, 2.4, 3.6, 4.8 and 6.0%. The actual project measures elapsed time rather than assuming each update takes exactly 20 ms.
OFFICIAL REFERENCES
The slew limiter is team-written Python logic. This is an independent learning resource; VEX and VEXcode are trademarks of their respective owners.