Voltage Control
Tuning flywheels, turrets, arms, elevators, and linear deployments with voltage requests.
This section tunes each mechanism with VelocityVoltage, PositionVoltage, MotionMagicVoltage, and MotionMagicExpoVoltage. All gains here are in Volts.
Voltage requests compensate for battery sag: ask for 8 V and the firmware measures the supply and adjusts its duty cycle to deliver 8 V, as long as the battery can. They do not compensate for back-EMF. A spinning motor pushes back against its own supply, and kV is exactly the voltage needed to cancel that.
The motor equation from §1.4.1 again:
Substituting :
The second term is what kV cancels. It also explains why mechanisms get weaker near top speed: the faster the motor spins, the less voltage is left over to produce torque. (This limit is physical and applies in torque-current mode too. See §3.1.4.)
2.1 Flywheel (Velocity)
A flywheel is mostly inertia plus some drag. At speed, only friction and air drag oppose it, so holding speed takes little effort. What matters is spin-up and recovery when a game piece passes through.
2.1.1 Control Output Equation
For VelocityVoltage (simple velocity PID, no profile):
kD is left out on purpose. In velocity control, D acts on the rate of change of velocity error (acceleration), which is mostly noise. Use the kA feedforward if you need acceleration response.
For MotionMagicVelocityVoltage, which limits how fast the velocity setpoint changes. It is uncommon for flywheels, but useful if spin-up current is browning out the robot:
where now ramps toward the target at the configured MotionMagicAcceleration (with optional MotionMagicJerk smoothing).
2.1.2 Parameter Effects
| Gain | Effect on Output |
|---|---|
| kS | Constant voltage to overcome stiction. Without it, low-speed setpoints undershoot because friction eats the small commanded voltage. |
| kV | The main feedforward. With the right kV, steady-state error is near zero before kP does anything. Too low → undershoot. Too high → overshoot, and kP/kI must pull the output back down, which is sluggish. |
| kA | Adds the voltage needed for the commanded acceleration, speeding up spin-up. Only matters with a profiled velocity or rapid setpoint changes. |
| kP | Pulls velocity error to zero. More kP recovers faster from disturbances but also amplifies measurement noise, which is significant because velocity is derived from position. |
| kI | Removes steady-state offset from unmodeled friction. Almost never needed if kS and kV are right. Watch for windup: Phoenix 6 slots have no integral zone, so if you use kI, disable it (switch to a slot with kI = 0) when far from the setpoint. |
2.1.3 Tuning Procedure
- Run SysId in voltage mode to get kS, kV, kA. Expect results near the no-load values from §1.6.3 plus 5–15% for friction: about kV ≈ 0.12–0.14 V/(rotation/s) at the rotor for a Kraken X60, and 0.10–0.11 for an X44.
- Check kV with kP = kI = 0. Command a setpoint and check the steady-state speed. If it settles 5% low, kV is 5% low. Fix that before adding kP.
- Add kP. Size it from the voltage headroom you have left. At 80 rps with kV = 0.12, holding speed takes ~9.6 V, leaving ~2.4 V. To use all of that headroom when the error reaches 20 rps:
- Test with real game pieces. Shoot and watch the velocity trace in AdvantageScope. Raise kP until recovery is fast enough. Back off if it oscillates.
- Skip kI unless something is steadily dragging the flywheel off speed. That is rare.
2.1.4 Pitfalls
- Tuning without game pieces. A loaded flywheel behaves very differently from a free-spinning one. Always test with real shots.
- Mixing unit conventions. If
SensorToMechanismRatioincludes the gear reduction, the gains must be per flywheel rps, not rotor rps. Pick one convention and stick to it. - Noisy velocity. Phoenix 6's default velocity filtering is reasonable. If you see several rps of jitter at a steady setpoint, check the sensor source (CANcoder vs. integrated rotor sensor) and the mechanics (loose magnet, slipping coupler).
2.2 Turret (Position)
A turret spins about a vertical axis, so gravity doesn't act on it. Inertia and friction dominate, and nothing pulls it back to a position: the controller alone holds it there.
2.2.1 Control Output Equation
For PositionVoltage (simple position PID):
where and is determined by the StaticFeedforwardSign config (see §1.2.1):
- With the default
UseVelocitySign: , which is zero when the request has no velocity, so kS does nothing. - With
UseClosedLoopSign: , so kS pushes in the direction of the error. Use this for plainPositionVoltageon a turret.
kV and kA do nothing in plain PositionVoltage: the setpoint is a step, so there is no velocity or acceleration reference for them to multiply (unless you pass Velocity yourself).
For MotionMagicVoltage or MotionMagicExpoVoltage (profiled position):
Now the profile supplies and every loop, so kV and kA do real work. Leave StaticFeedforwardSign at the default UseVelocitySign: kS acts during motion and drops to zero at the setpoint.
2.2.2 Parameter Effects
| Gain | Effect on Output |
|---|---|
| kS | The small voltage that gets the turret moving against static friction. Most noticeable on direct-drive or low-ratio turrets, where stiction is a large share of the available torque. Its sign depends on StaticFeedforwardSign (see above). |
| kV | (Profiled only) Voltage to hold the commanded velocity. With the right kV, the turret follows the profile smoothly. |
| kA | (Profiled only) Voltage for the commanded acceleration. Without it, the turret lags behind the profile while accelerating and catches up during cruise. |
| kP | Pulls position to the setpoint. The main term. |
| kI | Removes leftover error from imperfect kS/kV. Often zero. |
| kD | Damps oscillation. Mass plus kP behaves like a mass on a spring, so D is useful here, unlike in velocity control. Too much kD makes the turret feel sluggish and amplifies sensor noise. |
2.2.3 Recommended Mode
Use Motion Magic Expo on the competition robot. A turret tracking a moving target benefits a lot from a smooth, achievable trajectory. Plain PositionVoltage is fine for small aim corrections (under ~5°), but on large moves it asks for near-infinite acceleration, causing overshoot and current spikes.
2.2.4 Tuning Procedure
- Set soft limits in
SoftwareLimitSwitchConfigsbefore tuning anything. A turret hitting a hard stop at speed strips gears. - Set
StaticFeedforwardSign: defaultUseVelocitySignfor Motion Magic,UseClosedLoopSignfor plainPositionVoltage. - Run SysId in voltage mode for kS, kV, kA. A single Kraken X60 at ~50:1 should give kV around 6–7 V per (turret rotation/s). A Kraken X44 at the same ratio should give around 5–5.5.
- Feedforward only. Enter kS, kV, kA with kP = kI = kD = 0 and send a slow
MotionMagicExpoVoltagemove. The turret should follow the profile loosely. - Add kP. Aim for 1–2 V of correction at 5° of error. 5° is 0.014 turret rotations, so V/rotation. (These are mechanism units, not rotor units. This is where
SensorToMechanismRatiomatters.) - Add kD. Start around and increase until the overshoot is gone.
- Tune the profile last:
- Trapezoidal cruise velocity: leave 2–3 V of headroom above the kV term. With kV = 6 and a 9 V budget, cruise ≈ 1.5 rps.
- Trapezoidal acceleration: start where V (1 V margin), then raise it while watching tracking error.
- Expo: set
MotionMagicExpo_kVandMotionMagicExpo_kAto 1.1–1.2× the SysId kV and kA (§1.4.2), withMotionMagicCruiseVelocity = 0so the profile runs to what the motor allows.
2.2.5 Continuous Wrap
If the turret can spin without limit (no hard stops, e.g., with a slip ring), enable ContinuousWrap in ClosedLoopGeneralConfigs. The controller then takes the shortest path: "go from 350° to 10°" turns 20° instead of 340°. This needs SensorToMechanismRatio set so one mechanism rotation is one turret revolution, since the error wraps to ±0.5 rotation.
2.2.6 Pitfalls
- CANcoder direction reversed. If the CANcoder's
SensorDirectiondoesn't match the motor's positive direction, any kP makes the turret run away. Check by driving it slowly with a small positive output and confirming the reported position increases. - Tuning with the wrong inertia. A turret with the shooter mounted has a different kA than a bare one. Re-check the feedforwards on the finished assembly.
2.3 Arm (Position)
An arm rotates against gravity. Gravity torque on the joint follows the cosine of the angle from horizontal, so the voltage needed to hold the arm changes throughout its range. That is the main difference from a turret.
2.3.1 Control Output Equation
For PositionVoltage with GravityType = Arm_Cosine:
with set by StaticFeedforwardSign (§1.2.1). Use UseClosedLoopSign with PositionVoltage: kG already supplies the holding voltage, and kS only needs to break stiction on small corrections, in whichever direction the error points.
Phoenix 6 computes from the measured position, so position 0 must mean horizontal, and one mechanism rotation must be one full arm revolution. If the zero is wrong, the cosine is wrong everywhere. If your natural zero is elsewhere (a hard stop, or a center of gravity that is off the arm's line), set GravityArmPositionOffset in the slot (range ±0.25 rotation) instead of moving the zero.
For MotionMagicExpoVoltage (recommended):
At (horizontal), gravity torque is largest and the term is the full . At (straight up or down), and no gravity feedforward is needed. In between, it scales smoothly.
2.3.2 Parameter Effects
| Gain | Effect on Output |
|---|---|
| kG | Holding voltage at horizontal, scaled by . With the right kG, the arm holds anywhere with no PID effort. Too low → it sags below the setpoint. Too high → it drifts above. With the wrong sign, the arm runs away. |
| kS | Gearbox stiction. Often small (0.1–0.3 V) on well-greased arms, larger on direct-drive arms or arms with a brake. |
| kV | (Profiled only) Voltage per rps of commanded motion. |
| kA | (Profiled only) Voltage per rps² of commanded acceleration. Important for long arms with weight at the end. |
| kP | Position correction. Don't overdo it: high kP plus an imperfect kG oscillates, worst where kG is most wrong (often near horizontal). |
| kD | Damps oscillation. More important than on a turret, because gravity pushes harder or softer depending on angle. |
| kI | Useful here. The cosine model is never perfect (the arm flexes, the CG isn't exactly where you measured), and a small kI cleans up the final error. |
2.3.3 Calculating kG from First Principles
You can check SysId's kG by hand in three steps:
- Gravity torque at the joint with the arm horizontal:
- Torque at the motor, divided by the gear reduction ():
- The current for that torque (), then the voltage to push that current through the motor at zero speed ():
is the arm mass, m/s², the distance from pivot to center of gravity, and and come from §1.6.2.
Worked example. A 5 kg arm with CG at 0.4 m, gear ratio 100:1, on a Kraken X60 in voltage mode (FOC commutation, so N·m/A, Ω from §1.6.2):
It's small because at 100:1 the motor barely has to push. It agrees with the torque-mode holding current for the same arm in §1.6.3: V at zero speed. Expect the measured value to be somewhat higher, since a warm motor and the wiring add resistance.
Try your own arm. The calculator gives kG and kA in both Volts and Amps. Its defaults match the worked example above.
| Gain | Voltage mode | Torque-current mode |
|---|---|---|
| kG | 0.251 V | 10.1 A |
| kA per arm rot/s² | 0.0644 V | 2.59 A |
kG = m·g·L / (G·kt) in Amps. The voltage-mode value is that current times R, the voltage needed to push it at zero speed. kA uses an inertia of 0.8 kg·m² about the pivot.
Starting estimates only. Ignores friction, cables, and the motor's own rotor inertia. Measured voltage-mode values usually come out higher, because a warm motor and the wiring add resistance. Cross-check against SysId or the bracketing test.
If SysId's kG is far from this estimate:
- Check the zero first. If the arm is really at when the encoder reads , gravity follows , and the fit against is distorted by the part. How much, and in which direction, depends on the range the test covered: over a range symmetric about horizontal kG comes out low (about ), but over a one-sided range such as 0°→90° it can come out high. So a wrong zero can explain an error in either direction.
- Then check the inputs: mass, CG distance, and gear ratio.
2.3.4 Recommended Mode
Motion Magic Expo Voltage. Arms benefit even more than turrets from a smooth profile, because gravity makes step responses unpredictable. A step from 0° to 90° starts at full gravity torque and ends at none, so plain PositionVoltage sees very different dynamics at each end of the move.
2.3.5 Tuning Procedure
- Set the zero. Use a level or a known-horizontal hard stop, then
setPosition(0)(or set the CANcoder magnet offset). Check by moving the arm to vertical: it should read 0.25 rotation. - Set
StaticFeedforwardSign: defaultUseVelocitySignfor Motion Magic,UseClosedLoopSignfor plainPositionVoltage. - Run SysId's arm test in voltage mode for kS, kV, kA, kG. Without SysId, start from the hand-calculated kG.
- Feedforward only. Enter the feedforwards with PID at zero. At horizontal, the arm should hold. Moved slowly through its range, it should follow loosely. If it sags at some angles but not others, suspect the zero.
- Add kP. Start small. For a typical FRC arm in rotations:
- Add kD. Typically to . Increase until overshoot is acceptable.
- Add a small kI (~0.1–1.0 V/(rotation·s)) if the final position drifts.
- Tune the profile:
- Trapezoidal: choose a cruise velocity that leaves headroom at the worst gravity load. With kG = 0.6 V, kV = 4 V/rps, and a 2 V margin: rps.
- Expo: set
MotionMagicExpo_kVandMotionMagicExpo_kAto 1.1–1.2× the SysId kV and kA (§1.4.2). The profile then stays within what the motor can do.
2.3.6 Pitfalls
GravityTypeleft at the default. The default isElevator_Static, which applies kG as a constant. WithoutGravityType = Arm_Cosine, the arm is over-supported near vertical and under-supported near horizontal (or the reverse).- Two-joint arms. When a second joint moves relative to the first, gravity torque on the shoulder depends on the elbow angle too, which a single cosine can't capture. Compute the gravity feedforward in code and pass it through the request's
FeedForwardargument every loop, with slot kG = 0 (see §3.3.4). Don't writeSlot0Configs.kGevery loop: config writes are slow, blocking CAN transactions. - Different friction up and down. Once kG is right, any remaining up/down difference is friction, and a slot holds only one kS. Use one slot per direction (as in §2.4.6), or pick a compromise kS and let a small kI absorb the rest.
- Leave
ContinuousWrapoff. Arms have a limited range. Set soft limits instead.
2.4 Elevator (Position)
An elevator lifts a mass straight up against gravity. Unlike an arm, the gravity load usually doesn't depend on position (see §2.4.3 for the exception). The dynamics are linear, which makes elevators simpler than arms.
2.4.1 Control Output Equation
For PositionVoltage with GravityType = Elevator_Static:
with set by StaticFeedforwardSign (§1.2.1). Use UseClosedLoopSign with PositionVoltage so kS helps with small height corrections. With the default, kS never engages on a step setpoint.
kG is a constant here: with GravityType = Elevator_Static, the firmware always adds it. Its sign is the direction that opposes gravity, which is positive in the usual setup where positive output raises the elevator.
For MotionMagicExpoVoltage:
2.4.2 Parameter Effects
| Gain | Effect on Output |
|---|---|
| kG | Constant voltage to hold against gravity at any height. Check it with PID gains at zero: the elevator should hover, neither rising nor falling. |
| kS | Friction. Often larger than on a turret because of bearing and rail friction. |
| kV | (Profiled only) Voltage per unit of velocity, in whatever units SensorToMechanismRatio gives you (drum rotations/s, or m/s). |
| kA | (Profiled only) Important on elevators, which carry a lot of mass. Without it, the elevator lags the profile while accelerating. |
| kP | Position correction. Often the largest gain on a well-fed-forward elevator, because rigid rails and low backlash tolerate it. |
| kD | Damps oscillation. Useful, but usually less critical than on arms, since gravity doesn't change with position. |
| kI | Occasionally useful if the guides bind near the bottom of travel. Usually unnecessary. |
2.4.3 Multi-Stage and Cascaded Elevators
How the stages are rigged decides whether one kG is enough:
- Cascade rigging. All stages move together, each at a fixed multiple of the carriage speed. The load on the motor is the same at every height, so one kG works everywhere.
- Continuous rigging. The stages move one after another, so the moving mass, and with it the gravity load, can change when the next stage starts moving. If the change is big enough to matter, measure kG on each stretch of travel and switch slots by height, or pass the right value through the request's
FeedForwardargument each loop.
Check which rigging you have before tuning. If you're not sure, use the bracketing test in §2.4.5 at the bottom, middle, and top of travel: matching values mean one kG is enough.
2.4.4 Recommended Mode
Motion Magic Expo Voltage. Elevators travel far and benefit from a profile that stays within what the motor can do. Plain PositionVoltage is fine for small adjustments (fine-tuning a scoring height), but on full-travel moves it causes current spikes at both ends.
2.4.5 Tuning Procedure
- Set soft limits and a homing routine. An elevator hitting a hard stop at speed snaps chains and cables and bends frames. Use
ForwardSoftLimitEnable/ReverseSoftLimitEnableand their...Thresholdsettings inSoftwareLimitSwitchConfigs. - Set
StaticFeedforwardSign: defaultUseVelocitySignfor Motion Magic,UseClosedLoopSignfor plainPositionVoltage. - Find kG by hand (bracketing). With PID off, command a small upward voltage (start at 0.5 V) and raise it:
- When the elevator just stops sliding down, you're at (friction is helping hold it).
- When it just starts rising, you're at .
- kG is the midpoint and kS is half the difference. The torque-mode version in §3.4.4 works the same way.
- Run SysId's elevator test for kS, kV, kA, kG together, and compare with step 3. If there isn't enough travel for a clean run, split it into shorter segments.
- Feedforward only. With PID at zero, the elevator should hold at any height.
- Add kP. Elevators are rigid and tolerate fairly high kP. If your unit is meters, start around 1 V per cm of error (100 V/m) and increase until recovery is brisk.
- Add kD. Often around . Increase if moves overshoot at the top or bottom.
- Set up Expo (§1.4.2):
MotionMagicExpo_kVandMotionMagicExpo_kAat 1.1–1.2× the SysId kV and kA. LeaveMotionMagicCruiseVelocity = 0for full speed, or set it to cap top speed.
2.4.6 Asymmetric Friction
Elevators often have different friction going up and going down. A slot holds only one kS magnitude. StaticFeedforwardSign only decides where its sign comes from. To handle truly asymmetric friction:
- Two slots: slot 0 for up, slot 1 for down, chosen with the request's
Slotargument based on the direction of travel. Tune each slot's kS for its direction. - Or a compromise kS, letting kI absorb the small difference.
2.4.7 Pitfalls
- kG with the wrong sign. The motor pushes the elevator down while "holding". Always test a hold by hand first.
- Drift over a season. Cables stretch and bearings wear, so friction creeps up. Re-run SysId monthly if you can, and after major repairs.
- Dropping while disabled. Motors produce no output while the robot is disabled, so an elevator left high can fall. Use brake mode (
NeutralMode = Brake), and have the first command after enable hold the current position so kG takes over right away.
2.5 Linear Deployment (Position)
A "linear deployment" is any short-stroke mechanism: an intake extension, a climber hook, or a small pivot that moves roughly linearly over its range. Typically it has:
- Short travel (a few inches to a foot)
- Hard stops at both ends (or a hard stop plus a soft limit)
- Low to moderate inertia
- A gravity load that may be constant, absent, or angle-dependent
Tune it like a small elevator or arm, depending on how gravity acts on it.
2.5.1 Control Output Equation
If the gravity load is constant (the deployment travels vertically or on a fixed incline, and kG = 0 for a horizontal slide):
If gravity is angle-dependent (deployment pivots, e.g., a kicker arm that swings through a small range):
Use GravityType = Elevator_Static for the first case and Arm_Cosine for the second. Choose as in §1.2.1: UseClosedLoopSign for plain PositionVoltage, the default UseVelocitySign for Motion Magic. (kV and kA only act when there is a profile.)
2.5.2 Recommended Mode
For very short moves (under ~30° or ~4 inches), plain PositionVoltage is often fine and quicker to set up than Motion Magic. The move is so short that it is almost all acceleration and deceleration anyway.
For longer or frequently repeated moves, Motion Magic Expo Voltage still wins: the smooth profile means less chain and cable wear and fewer current spikes that can brown out the robot.
2.5.3 Tuning Procedure (Abbreviated)
- Set hard stops and soft limits. Deployments often run into a hard stop as part of normal use, so a hard stop makes a natural zero. Set it at boot from a limit switch or a homing routine (§2.5.4).
- Set
StaticFeedforwardSign. Short deployments are usually driven by plainPositionVoltage, so useUseClosedLoopSign. With Motion Magic, leave the default. - Choose a gravity model. Does the deployment work against gravity over a meaningful part of its range? If so, set
GravityTypeand find kG. If not (e.g., a horizontal slide on rollers), set kG = 0. - Run a quick SysId if there is enough travel. If there isn't, measure by hand:
- kS: raise the voltage slowly until it starts moving.
- kV: drive at a steady speed and read the voltage (
getMotorVoltage()). Then kV = (voltage − kS) / speed. - kA: usually 0 for short, light deployments.
- Add kP. It is often high, since these mechanisms are rigid and move only a few units.
- Add a little kD if it overshoots.
- Skip kI unless the deployment has odd hysteresis.
2.5.4 Homing and Zeroing
The most common failure on a linear deployment is losing its zero. Options:
- Home against a hard stop. On enable, drive slowly toward a known hard stop. When stator current rises above a threshold while velocity is near zero (poll
getStatorCurrent()andgetVelocity()), call that position zero. Cheap and effective. - Limit switch. Set the reverse limit in
HardwareLimitSwitchConfigs(ReverseLimitSource,ReverseLimitType). Phoenix 6 can zero automatically when it triggers, withReverseLimitAutosetPositionEnableandReverseLimitAutosetPositionValue(and the matchingForward*pair). - Absolute encoder (CANcoder). The most robust option and removes homing entirely, as long as the whole travel fits within one CANcoder rotation.
2.5.5 Pitfalls
- Trusting position across power cycles without homing. The Talon FX's built-in position only counts from wherever it booted.
- Tuning at one extension and running at another. If the gravity load changes with position, model it: with
GravityType, or with aFeedForwardvalue computed in code. - No margin around the limits. Setpoints right at a soft limit trip it on every overshoot, and a soft limit right at the hard stop still lets the mechanism slam into it. Keep setpoints a little inside the soft limits and soft limits a little inside the hard stops (about 0.5–1°, or a few millimetres).