Skip to content

Mini-Project 1: A Single-Jointed Arm From Math to Motion

learnfrc.com
learnfrc.comAuthor
Veer Bajaj
Veer BajajMaintainer

Design a single-jointed arm: a 24 in aluminum arm carrying a 3 lb game piece at the tip, pivoting from horizontal (0 rad) up to vertical (1.57 rad). We’ll size the reduction, pick parts, then write closed-loop code.

Gravity torque is highest when the arm is horizontal: tau = m * g * r. The arm itself (~2 lb at its 12 in center of mass) plus a 3 lb load at 24 in:

  • Arm: 0.91 kg * 9.81 * 0.305 m = 2.72 Nm
  • Load: 1.36 kg * 9.81 * 0.61 m = 8.14 Nm
  • Total holding torque ≈ 10.9 Nm at the joint.

A single NEO Vortex (REV-21-1652) produces 3.6 Nm stall torque and 6784 RPM free speed (per REV’s published motor specs). You never run a motor at stall, so target ~20-25% of stall as a usable continuous holding torque: ~0.8 Nm at the motor. Required reduction:

10.9 Nm / 0.8 Nm ≈ 14:1 minimum; round up for margin to a clean 48:1.

Build this with a REV MAXPlanetary (system kit REV-21-2100): stack a 4:1 and 3:1 cartridge = 12:1, then a final 16T:64T sprocket/chain stage = 4:1, for a total 48:1. MAXPlanetary cartridges are sold individually as 3:1 (REV-21-2101), 4:1 (REV-21-2102), 5:1 (REV-21-2103), and 9:1. To drive the MAXPlanetary with a NEO Vortex you also need the Vortex MAXPlanetary Input Kit (REV-21-2130), which couples the Vortex’s keyed shaft into the gearbox input stage.

Mount a REV Through Bore Encoder V2 (REV-11-3174) on the dead axle so it reads true joint angle regardless of chain stretch. It has a 1/2 in hex through bore and outputs both absolute (pulse-width) and incremental (quadrature) signals; read its absolute output through your controller’s data/IO port and convert pulse width to radians in code.

Use a ProfiledPIDController for smooth motion and ArmFeedforward for gravity. The key insight: ArmFeedforward’s kG term is multiplied by cos(angle) internally, because gravity torque scales with the horizontal projection. The constructor order is (kS, kG, kV, kA).

private final ProfiledPIDController pid =
new ProfiledPIDController(
4.0, 0.0, 0.1,
new TrapezoidProfile.Constraints(2.0, 5.0)); // rad/s, rad/s^2
private final ArmFeedforward ff =
new ArmFeedforward(0.2, 0.45, 1.9, 0.05); // kS, kG, kV, kA (volts)
public void setGoal(double goalRad) { pid.setGoal(goalRad); }
@Override
public void periodic() {
double measured = encoder.getPosition(); // radians, absolute
double pidVolts = pid.calculate(measured);
var sp = pid.getSetpoint();
double ffVolts = ff.calculate(sp.position, sp.velocity); // cos(angle) handled inside
motor.setVoltage(pidVolts + ffVolts);
}

The example gains above are placeholders; measure your own with SysId.

Per WPILib’s feedforward guidance: find kG first (increase until the arm just holds horizontal without drifting), then kV (matches steady-velocity slope), then kA (matches acceleration curves), then add kP until slight overshoot and back off ~20%. Set a smart current limit of 40 A on the controller so the planetary and chain survive a stall.

  • Size reductions from worst-case (horizontal) gravity torque, then target ~20-25% of motor stall torque as usable holding torque.
  • ArmFeedforward’s constructor order is (kS, kG, kV, kA), and kG is multiplied by cos(angle) automatically; pass it the profiled setpoint position and velocity.
  • Read joint angle with an absolute Through Bore Encoder V2 (REV-11-3174) on the dead axle so chain stretch never corrupts the angle.

This lesson was adapted from learnfrc.com.