Motion Magic (CTRE)
Motion Magic moves the motion profile off the roboRIO and onto the TalonFX itself. Less network traffic, less loop overhead, faster response — and one fewer place for your code to get out of sync with your hardware.
By the end of this lesson, you will:
- Explain what Motion Magic is and why it runs on the motor controller rather than the roboRIO
- Identify the four configuration parameters Motion Magic requires and what each one physically controls
- Configure a TalonFX for Motion Magic using
MotionMagicConfigsand apply it viaMotorOutputConfigsand slot gains - Command a Motion Magic move using
MotionMagicVoltageorMotionMagicVelocitycontrol requests - Describe the tradeoffs between roboRIO-side profiling (
ProfiledPIDController) and on-controller Motion Magic
What Motion Magic Is — and Where It Runs
In the previous lesson you built a trapezoidal motion profile on the roboRIO using WPILib's TrapezoidProfile and ProfiledPIDController. Every 20 milliseconds, your robot's main loop computed the next setpoint along the trajectory and sent it to the motor controller as a voltage or velocity command.
Motion Magic is CTRE's factory-built version of that same concept — except the profile runs inside the TalonFX motor controller itself, at 1 kHz. Instead of your roboRIO computing a setpoint and transmitting it over CAN 50 times per second, the TalonFX generates its own trajectory at 1000 times per second and closes the loop locally. Your roboRIO sends a single position goal once, and the TalonFX handles everything from there until the motion completes or you send a new goal.
This is not just a convenience. It has real engineering implications.
- Profile computes at 50 Hz (20 ms loop)
- Setpoint transmitted over CAN each loop
- CAN jitter can cause setpoint gaps
- Works with any motor controller
- Easy to log intermediate setpoints
- Gains tuned in Java code / Constants.java
- Supports gravity compensation via WPILib feedforward classes
- Profile computes at 1000 Hz on the TalonFX
- No per-loop CAN traffic for setpoints
- Immune to roboRIO loop overruns
- TalonFX / Kraken X60 only
- Gains configured via Phoenix Tuner X or code
- Slot system for switching gain sets at runtime
- Supports gravity feedforward (kG) in config
Team 2910 uses Motion Magic extensively for elevator and arm control. One reason: at competition, the roboRIO runs many things simultaneously — vision processing, odometry updates, logging, path following. Under load, the main loop can take longer than 20 ms, which means a ProfiledPIDController gets starved for update cycles and its trajectory drifts from what you expected. A Motion Magic request already sent to the TalonFX keeps executing at 1 kHz regardless of what the roboRIO is busy with. The mechanism doesn't feel the roboRIO's stress.
The Four Motion Magic Parameters
Motion Magic requires four configuration values to define the motion profile. These live in MotionMagicConfigs in Phoenix 6. Click each parameter to understand what it controls physically — not just mathematically.
This is the flat top of the trapezoid — the velocity the mechanism will cruise at between the acceleration and deceleration phases. Units are rotations per second at the motor shaft (before any gearing reduction you apply in your sensor ratio configuration).
Set this conservatively first. If your arm's motor-side velocity at full mechanism speed is 80 rot/s, start at 50. Watch in Tuner X's plot view whether the mechanism actually reaches cruise velocity or just follows the acceleration ramp continuously. If it never cruises, either the distance is short (triangular profile — expected) or your cruise velocity is already above the motor's capacity at that load.
By default, Phoenix 6 units are in rotations at the motor shaft. If you configure a SensorToMechanismRatio in your FeedbackConfigs, the sensor position is scaled to mechanism rotations instead, and your Motion Magic parameters should be in mechanism units. If you're controlling an arm, set up your sensor ratio so that 1.0 = 1 full arm rotation, and your cruise velocity/acceleration parameters will be in arm rotations per second — which is much easier to reason about physically.
Slot Gains: The Feedback Side of Motion Magic
Motion Magic generates the trajectory, but it still needs a feedback controller to follow it. In Phoenix 6 that feedback is configured through slots — banks of PID and feedforward gains stored directly on the TalonFX. You can define up to three slots (0, 1, 2) and switch between them at runtime with a single line of code.
For Motion Magic position control, the relevant gains in each slot are:
- kP — proportional gain on position error. Primary correction force.
- kI — integral gain on accumulated position error. Start at 0 — with good feedforward you rarely need it.
- kD — derivative gain on velocity error (how fast position error is changing). Acts as damping against overshoot.
- kS — static feedforward: voltage added in the direction of motion to overcome breakaway friction.
- kV — velocity feedforward: voltage per unit of profiled velocity. Predicts the voltage needed to hold cruise speed without waiting for PID error to develop.
- kA — acceleration feedforward: voltage per unit of profiled acceleration. Helps during the ramp-up and ramp-down phases.
- kG — gravity feedforward: constant voltage to counteract gravity. Available for arm and elevator mechanisms.
Different positions on the same mechanism can have very different dynamics. An arm at 0° (horizontal, full gravitational load) needs very different gains than the same arm at 90° (vertical, gravity perpendicular to motion). Teams sometimes configure slot 0 for low-angle positions and slot 1 for high-angle positions, then switch slots mid-match. This is one of the things Motion Magic makes practical that roboRIO-side profiling makes awkward.
Full Configuration in Code
Phoenix 6 uses a configuration object pattern. You build a TalonFXConfiguration, populate its sub-configs (MotionMagicConfigs, Slot0Configs, FeedbackConfigs, etc.), and apply the entire configuration to the device in one call. This happens once — typically in the subsystem constructor — not every loop.
import com.ctre.phoenix6.configs.*; import com.ctre.phoenix6.controls.MotionMagicVoltage; import com.ctre.phoenix6.hardware.TalonFX; import com.ctre.phoenix6.signals.GravityTypeValue; public class ArmSubsystem extends SubsystemBase { private final TalonFX m_motor = new TalonFX(ArmConstants.MOTOR_CAN_ID, "rio"); // Reusable control request — allocate once, reuse every loop private final MotionMagicVoltage m_mmRequest = new MotionMagicVoltage(0); public ArmSubsystem() { TalonFXConfiguration cfg = new TalonFXConfiguration(); // ── Sensor ratio ────────────────────────────────────────────── // Tell the TalonFX the gear ratio so position reads in arm rotations // (not motor shaft rotations). 1.0 arm rotation = ratio motor rotations. cfg.Feedback.withSensorToMechanismRatio(ArmConstants.GEAR_RATIO); // ── Motion Magic constraints ─────────────────────────────────── cfg.MotionMagic .withMotionMagicCruiseVelocity(0.5) // arm rot/s at mechanism .withMotionMagicAcceleration(1.0) // arm rot/s² at mechanism .withMotionMagicJerk(10.0); // optional smoothing; 0 = disabled // ── Slot 0 gains ─────────────────────────────────────────────── // These are the feedback + feedforward gains used during the profile. // GravityType.Arm_Cosine makes kG scale with cos(angle) automatically. cfg.Slot0 .withKP(24.0) .withKI(0.0) .withKD(0.5) .withKS(0.25) .withKV(0.12) .withKA(0.01) .withKG(0.35) .withGravityType(GravityTypeValue.Arm_Cosine); // ── Apply to hardware ────────────────────────────────────────── // applyConfiguration() flashes these values to the TalonFX over CAN. // In production code, check the StatusCode return value. m_motor.getConfigurator().apply(cfg); } // Call this from a Command's initialize() or execute() public void setGoalRotations(double armRotations) { m_motor.setControl(m_mmRequest.withPosition(armRotations)); } public boolean atGoal() { // ClosedLoopError is in mechanism units after ratio scaling double error = m_motor.getClosedLoopError().getValueAsDouble(); return Math.abs(error) < ArmConstants.POSITION_TOLERANCE_ROT; } public double getPositionRotations() { return m_motor.getPosition().getValueAsDouble(); } @Override public void periodic() { // No profile calculation here — the TalonFX handles it internally. // periodic() is used for telemetry and safety checks only. SmartDashboard.putNumber("Arm/Position", getPositionRotations()); SmartDashboard.putNumber("Arm/ClosedLoopError", m_motor.getClosedLoopError().getValueAsDouble()); } }
The MotionMagicVoltage control request object is allocated in the field declaration, not inside setGoalRotations(). This is intentional. If you write m_motor.setControl(new MotionMagicVoltage(position)) inside periodic(), you're allocating a new object 50 times per second. In Java, this means the garbage collector eventually pauses your program mid-match to reclaim memory — a loop overrun. Allocate once, reuse via .withPosition() which modifies the existing object in place and returns it.
Using Motion Magic in a Command
The command pattern is nearly identical to the ProfiledPIDController version from Lesson 11. The subsystem encapsulates all the hardware interaction; the command only needs to know what goal to set and when to consider the move done.
public class MoveArmCommand extends Command { private final ArmSubsystem m_arm; private final double m_targetRotations; public MoveArmCommand(ArmSubsystem arm, double targetRotations) { m_arm = arm; m_targetRotations = targetRotations; addRequirements(arm); } @Override public void initialize() { m_arm.setGoalRotations(m_targetRotations); // The TalonFX starts the Motion Magic profile immediately. // No further calls needed in execute() — the motor drives itself. } @Override public boolean isFinished() { return m_arm.atGoal(); } @Override public void end(boolean interrupted) { if (interrupted) { // Hold current position — send a new Motion Magic goal at current pos m_arm.setGoalRotations(m_arm.getPositionRotations()); } } }
With roboRIO-side profiling (ProfiledPIDController), the subsystem's periodic() does work every loop, so the command can be purely structural. Motion Magic is even simpler: the profile runs inside the TalonFX, so after initialize() sends the goal, nothing in your Java code needs to run until isFinished() returns true. The execute() override is only needed if you want to update the goal dynamically — for example, a "hold at joystick-trimmed angle" mode where the target changes with driver input.
First-Time Setup Workflow
Getting Motion Magic running correctly the first time requires a specific sequence. Skipping steps — especially the sensor ratio setup — is the most common source of confusion.
Open Phoenix Tuner X, navigate to your TalonFX, and watch the Position signal while you manually move the mechanism by hand. Confirm the reading changes in the expected direction and by the expected amount per mechanism movement. If you rotate the arm by 90° and the position reads 5.2 rotations, your gear ratio is 5.2/0.25 = 20.8:1 — write that down.
If position reads backwards, set cfg.MotorOutput.withInverted(InvertedValue.Clockwise_Positive) or flip the sensor phase. Do this before touching any gains — there's no point tuning a controller running on backwards sensor data.
Set cfg.Feedback.withSensorToMechanismRatio(gearRatio) using the ratio you just measured or calculated. After this, the TalonFX position will read in mechanism rotations (0.0 to 1.0 for a full arm rotation). Confirm by moving the mechanism one full revolution — position should change by exactly 1.0.
Set kP, kI, kD, kS, kA, kG all to 0. Set kV to your mechanism's characterization value (or a rough estimate from SysId / kV = 12V ÷ free-speed-in-rot-per-s). Set cruise velocity to 30% of free speed, acceleration to cruise velocity × 1.5.
Deploy and command a motion. The mechanism should follow the profile shape — visibly accelerating, cruising, decelerating — but may not land precisely at the target. That's expected. You're confirming the profile is being executed before introducing feedback.
Increase kP until the mechanism reliably reaches the target position. Watch ClosedLoopError in Tuner X — it should converge to near zero by the end of the motion. If it oscillates, add kD. If it consistently stops short, increase kP or add a small kS.
For arms, set withGravityType(GravityTypeValue.Arm_Cosine) and tune kG. The Arm_Cosine type automatically scales kG by the cosine of the mechanism position, so the compensation is correct at every angle — not just at 0°. For elevators, use GravityTypeValue.Elevator_Static.
Configure cfg.SoftwareLimitSwitch with forward and reverse soft limits that match your mechanism's physical travel range. This prevents Motion Magic from driving the mechanism into a hard stop if you send it an out-of-range goal. Always enable soft limits before increasing cruise velocity or testing in auto.
Motion Magic vs. ProfiledPIDController: Which to Use
Both approaches produce a trapezoidal motion profile. The choice depends on your hardware situation and what you're optimizing for.
| Consider | Choose ProfiledPIDController | Choose Motion Magic |
|---|---|---|
| Motor controller | SparkMax, SparkFlex, or any motor | TalonFX (Falcon 500, Kraken X60) only |
| Loop overhead | Profile math runs on roboRIO each loop | Profile runs on motor controller at 1 kHz |
| Simulation | Easy — WPILib sim classes match exactly | More setup — need to mock CTRE sim device |
| Logging | Easy access to setpoint via getSetpoint() |
Read from TalonFX signals (slightly more boilerplate) |
| Multi-axis gravity | WPILib ArmFeedforward handles it | kG + GravityTypeValue.Arm_Cosine handles it |
| Competition reliability | Can degrade if roboRIO loop is overloaded | Continues correctly even if roboRIO is busy |
| Gain storage | Constants.java, compiled into code | Slots on device (can edit in Tuner X without redeploy) |
If all your mechanism motors are TalonFX, Motion Magic is usually the better choice for arms and elevators at competition. It reduces roboRIO load, its gains are editable in Tuner X without a redeploy (huge during pit debugging), and the 1 kHz update rate produces noticeably smoother motion. If you're running SparkMax motors, or if simulation is a priority for your workflow, ProfiledPIDController is the appropriate tool. Teams like 2910 use Motion Magic on TalonFX mechanisms and ProfiledPIDController for any mechanisms running REV hardware in the same robot.
🔌 System Check
Motion Magic runs on the TalonFX — which means its behavior depends on the TalonFX's firmware version, CAN ID, and configuration state. Work through this list before enabling:
- Phoenix 6 firmware on the TalonFX. Motion Magic in the Phoenix 6 API will not work with Phoenix 5 firmware. Open Tuner X, find the device, and confirm the firmware version starts with 6.x. If it shows 22.x or 23.x, update firmware before proceeding.
- CANivore vs. "rio" bus string. If your TalonFX is on a CANivore (CAN FD adapter), the constructor is
new TalonFX(id, "Canivore")— the second argument must match your CANivore's configured name. Mismatch means the code creates a device object that silently fails to communicate. - Encoder direction verified before any gains are applied. Motion Magic will drive hard into a hard stop if the encoder reads backwards and kP is nonzero. Verify direction first in Tuner X with zero gains, then add feedback.
- Software limits configured before increasing cruise velocity. Set both forward and reverse soft limits in your config before your first full-speed test. This is not optional for arms and elevators — without limits, a bad goal value can damage the mechanism or damage the robot around it.
- applyConfiguration() return status is checked. In production code, store the
StatusCodereturned bym_motor.getConfigurator().apply(cfg)and log a warning if it's notStatusCode.OK. Silent config failures — where the config appeared to apply but didn't — are rare but genuinely happen on damaged CAN buses.
Knowledge Check
1. A team configures Motion Magic on their elevator TalonFX, but the elevator consistently overshoots the target by about two inches before settling back. The profile shape looks correct in Tuner X. Which gain is most likely the first to adjust?
2. A programmer writes m_motor.setControl(new MotionMagicVoltage(targetPosition)) inside periodic(). The mechanism works correctly during testing but the robot has frequent loop overruns at competition. What is the most likely connection?
3. A team wants to use different PID gains for their arm at low angles (where gravity load is high) versus high angles (where it's near vertical). They're using Phoenix 6 Motion Magic. What is the correct approach?
Configure and Command a Motion Magic Mechanism
- Create an
ArmSubsystemwith a single TalonFX at a CAN ID stored inArmConstants. In the constructor, build a fullTalonFXConfigurationwith placeholder values forSensorToMechanismRatio,MotionMagicCruiseVelocity,MotionMagicAcceleration,MotionMagicJerk, and all Slot0 gains (set all to 0 except kV = 0.12). Apply the configuration and log the returnedStatusCodeto the console. - Add a
setGoalRotations(double)method and anatGoal()method that checksgetClosedLoopError()against a tolerance constant. Inperiodic(), publish position and closed-loop error to SmartDashboard. - Create a
MoveArmCommandthat callssetGoalRotations()ininitialize()and returnsatGoal()fromisFinished(). Handle interruption inend()by holding the current position. Bind it to a controller button inRobotContainer. - If you have a TalonFX available: deploy, open Tuner X, and enable the Signal Plot for
Position,ClosedLoopReference, andClosedLoopError. Command a move and observe whether the reference (the Motion Magic target) follows the expected trapezoidal velocity shape before position catches up. - Bonus: Configure a second slot (Slot1) with kP doubled and kD halved compared to Slot0. Add a method
setSlot(int slot)that updates theMotionMagicVoltagerequest's slot field. Trigger a slot switch from a second controller button and observe the difference in response in Tuner X's plot. Describe in a code comment what physical condition would justify using the faster slot vs. the slower one on your mechanism.