Mini-Project 1: A Single-Jointed Arm From Math to Motion
The goal
Section titled “The goal”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.
Step 1 — Worst-case torque
Section titled “Step 1 — Worst-case torque”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.
Step 2 — Motor + reduction
Section titled “Step 2 — Motor + reduction”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.
Step 3 — Sensing
Section titled “Step 3 — Sensing”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.
Step 4 — The control code (WPILib Java)
Section titled “Step 4 — The control code (WPILib Java)”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^2private 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); }
@Overridepublic 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.
Step 5 — Tune in order
Section titled “Step 5 — Tune in order”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.
Key takeaways
Section titled “Key takeaways”- 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.
