When a motor hits a hard stop — an intake jam, an arm at its limit, a claw crushing a game piece — it draws full current and overheats. Stall detection cuts power automatically, protecting the motor and preventing match failures.
1
What Is a Stall
2
Current
3
Velocity
// Section 01
What a Motor Stall Actually Is
A stall happens when a motor is commanded to move but physically cannot — something is blocking it. Understanding the physics helps you write better detection code.
🚫
A stalled motor draws maximum current continuously. The V5 Smart Motor is current-limited to 2.5A, but at stall that current flows entirely as heat with no mechanical output. Within 30–60 seconds of continuous stall, the motor triggers thermal protection and cuts out — leaving you with a disabled mechanism mid-match.
Common Stall Scenarios in VRC
Intake jam — a game element jams between the intake rollers and the robot frame. Driver holds R1. Motor stalls for 10+ seconds. Overheats.
Arm hitting a hard stop — arm is commanded past its physical limit. The mechanical hard stop blocks it. Without detection, the motor pushes against the stop indefinitely.
Claw over-gripping — claw closes on a game element and the driver keeps holding the button. Motor stalls against its own gearbox or the game piece.
Robot defense — opponent robot blocks your drive. Drive motors stall at high power for several seconds.
Two Ways to Detect a Stall in Code
The V5 Smart Motor reports both its current draw (get_current_draw() in milliamps) and its actual velocity (get_actual_velocity() in RPM). A stall shows up as one or both of:
High current, low/zero velocity — motor is being commanded but not moving
Commanded speed ≠ actual velocity — large gap between what you asked for and what is happening
The next two sections show both approaches with complete code.
1 / 3
// Section 02
Current-Based Stall Detection
The simplest and most reliable method. If current is above threshold and velocity is near zero for longer than a brief window — it is a stall.
The Current + Velocity Check
Both conditions together prevent false positives: high current alone happens during normal acceleration. Low velocity alone happens when the motor is not being driven. The combination — high current AND low velocity — is a stall.
src/intake.cpp — stall detection for an intake motor
// Stall detection constants — tune these to your robotconstint STALL_CURRENT_MA = 2200; // milliamps (2.5A limit, 2.2A = likely stall)constdouble STALL_VELOCITY_RPM = 5.0; // RPM below this = not movingconstint STALL_TIME_MS = 300; // ms above threshold before actingstaticuint32_t stallStartTime = 0;
staticbool isStalled = false;
boolintakeCheckStall() {
int current = intake.get_current_draw(); // milliampsdouble velocity = abs(intake.get_actual_velocity());
bool highCurrent = (current > STALL_CURRENT_MA);
bool lowVelocity = (velocity < STALL_VELOCITY_RPM);
if (highCurrent && lowVelocity) {
if (stallStartTime == 0) {
stallStartTime = pros::millis(); // start the timer
}
if (pros::millis() - stallStartTime > STALL_TIME_MS) {
isStalled = true;
}
} else {
stallStartTime = 0; // reset timer if not stalling
isStalled = false;
}
return isStalled;
}
// Updated intake control — stops on stallvoidintakeForward() {
if (intakeCheckStall()) {
intake.move(0); // stall detected — cut powerreturn;
}
intake.move(127);
}
💡
The 300ms timer prevents false positives. A motor briefly draws high current during startup and acceleration. By requiring the high-current + low-velocity condition to persist for 300ms, you avoid cutting power during normal operation. Tune this window lower (150ms) for mechanisms that jam quickly, higher (500ms) if getting false stall cuts.
Adding Auto-Reverse on Stall
For intakes, cutting power is not always enough — a jammed game element stays jammed. Auto-reversing for a brief moment often clears it:
staticuint32_t reverseEndTime = 0;
voidintakeForwardSmart() {
// If we are in the auto-reverse window, keep reversingif (pros::millis() < reverseEndTime) {
intake.move(-80);
return;
}
if (intakeCheckStall()) {
reverseEndTime = pros::millis() + 400; // reverse for 400ms
intake.move(-80);
return;
}
intake.move(127);
}
2 / 3
// Section 03
Velocity-Based Detection & Hard Stop Protection
For arms and lifts that hit physical end-stops, detecting velocity alone is cleaner — and protecting hard stops with a position limit prevents the motor from fighting the frame.
Velocity-Only Stall Detection
For arms with position presets using move_absolute(), current-based detection can interfere with the motor's internal PID. Velocity-only detection is simpler and more compatible:
src/arm.cpp — velocity stall detection
constdouble ARM_STALL_VELOCITY = 3.0; // RPM — arm is effectively stoppedconstint ARM_STALL_MS = 400; // ms before declaring stallstaticuint32_t armStallStart = 0;
voidarmManualSafe(int speed) {
double vel = abs(arm.get_actual_velocity());
// Only check stall when actively commanding movementif (abs(speed) > 20 && vel < ARM_STALL_VELOCITY) {
if (armStallStart == 0) armStallStart = pros::millis();
if (pros::millis() - armStallStart > ARM_STALL_MS) {
arm.move(0); // cut power — arm is against a hard stopreturn;
}
} else {
armStallStart = 0;
}
arm.move(speed);
}
Protecting Hard Stops With Position Limits
Even better than detecting a stall after it happens: prevent the arm from ever reaching the hard stop in the first place. Use the motor's encoder to define soft limits:
// Define safe position range during initialize() after zeroing the armconstint ARM_MIN = 0; // floor position (encoder ticks)constint ARM_MAX = 1800; // max height — tune to your robotvoidarmManualLimited(int speed) {
int pos = (int)arm.get_position();
// Block downward movement at floor limitif (pos <= ARM_MIN && speed < 0) { arm.move(0); return; }
// Block upward movement at height limitif (pos >= ARM_MAX && speed > 0) { arm.move(0); return; }
arm.move(speed);
}
⚠️
Calibrate arm zero position in initialize() by running the arm to its lowest position (physical stop at low speed), then calling arm.tare_position(). This sets the encoder to 0 at the floor. All soft limits are then relative to this calibrated zero — consistent across every power cycle.
Which Method to Use
Intake — use current + velocity detection with auto-reverse. Jams are unpredictable, auto-reverse clears most of them.
Arm/Lift with manual control — use position limits (soft stops) to prevent reaching the hard stop at all. Add velocity detection as a secondary safeguard.
Arm with move_absolute() — let the motor's built-in PID handle most cases. Add velocity detection only if the arm is regularly hitting hard limits.
Drive motors — generally not worth adding stall detection. Drive stalls are brief (opponent defense) and the motor thermal protection handles longer stalls acceptably.
⚙ STEM HighlightElectrical Engineering: Motor Stall Physics & Thermal Management
A stalled V5 motor draws maximum current (2.5A) but produces zero rotational work — all electrical power converts to heat via I²R losses in the windings. Power = I²R; at 2.5A stall, a motor dissipates all 11W as heat. The motor’s thermal mass (specific heat × mass) determines how quickly temperature rises — a lightweight motor reaches thermal limits faster than a heavy one under the same power. The motor’s internal temperature sensor implements thermal runaway protection — exactly the same principle used in lithium battery management systems.
🎤 Interview line: “Motor stall converts all electrical power to heat via I²R losses — at 2.5A stall current, the motor dissipates 11W as heat with zero useful work output. Our stall detection cuts power before the thermal limit, implementing software-based thermal protection analogous to the hardware thermal cutoffs in battery management systems.”
🔬 Check for Understanding
A V5 motor stalls for 3 seconds. Its current is 2.5A and resistance is approximately 2Ω. How much energy was dissipated as heat?