Control modes & tuning¶
PiperPilot drives the real arm through one of two control backends,
selected by arm.backend in the config (or by CLI shortcut). Both accept the
same stream of absolute EEF pose targets from the teleop loop — they differ in
where inverse kinematics runs and how the joints are servoed:
piper_mit(the default, CLI--impedance) — compliant MIT impedance control. The host solves IK and streams per-joint spring-damper commands. Contact force is bounded, so the arm yields when it touches something.piper(CLI--position) — firmware position control viamove_p. Tightest tracking, but rigid: the position servo pushes through contact.
A third backend, fake (CLI --sim), simulates the arm for pipeline testing
and is not covered here — see Architecture.
Choosing a mode¶
piper_mit (default) |
piper (--position) |
|
|---|---|---|
| Principle | Host DLS-IK → per-joint MIT impedance (T = kp·(q_des − q) − kd·v + t_ff) + torque-snapshot gravity feed-forward |
move_p streams EEF poses; IK runs in the arm firmware |
| Contact behaviour | Compliant — force bounded ≈ cartesian stiffness × 0.12 m tether (~20 N with shipped gains) | Rigid (position servo) |
| Measured tracking | 14.1 mm dynamic RMS, 11 mm static hold error (shipped soft gains; higher kp tracks tighter) | 1.34 mm dynamic RMS, ≈0 static error |
| Estimated latency | ~90 ms | ~10 ms |
| When to use | Everyday data collection, contact-rich tasks, safe RL exploration | High-precision free-space tasks |
Numbers were measured on a Piper-X with firmware S-V1.8-9 using
piper-track-test (3 cm circle, 5 s per lap) — see
Benchmarks for the methodology.
Position mode (piper, --position)¶
The simplest backend: every teleop tick (at teleop.rate_hz, 100 Hz by
default) the host calls move_p([x, y, z, roll, pitch, yaw]) with the
absolute EEF target. The firmware does the rest:
- IK runs in the firmware. The host never touches joint space.
- Target-overwrite semantics make streaming at 50–200 Hz safe — each new target simply replaces the previous one.
- Units are meters/radians in the base frame, with
R = Rz(yaw) @ Ry(pitch) @ Rx(roll). The SDK hard-limits pitch to ±90°; the teleop safety envelope stays clear of it withteleop.limits.pitch_abs_max_deg: 88.0. arm.speed_percentscales the firmware's motion speed.
This gives the tightest tracking the arm can do (1.34 mm dynamic RMS, ~10 ms latency, essentially zero static error). The trade-off: on contact the position servo keeps pushing. Use it for free-space precision work, not for tasks where the gripper is expected to touch the world.
Impedance mode (piper_mit, default)¶
In impedance mode the arm behaves like a 6-joint spring-damper pulled toward a streamed joint target. The command path, in order:
1. Host-side DLS-IK¶
The firmware only does IK for move_p, so joint-space (move_mit) control
needs the host to convert EEF pose targets into joint targets. The solver is
damped least squares on pyAgxArm's MDH forward-kinematics model:
- Warm-started from the previous joint target and fed small per-tick deltas, so the configured 4 iterations converge comfortably at 100 Hz. The numeric Jacobian costs 6 extra FK calls per iteration; the 6-link MDH FK is ~0.1 ms in pure Python.
- Per-iteration joint updates are clamped to ±0.2 rad to keep iterates tame near singularities, and every iterate is clamped to the joint limits.
- If the solver does not converge (unreachable or singular target), the backend holds the previous joint target instead of chasing a bad solution.
The solver knobs live under impedance.ik:
| Key | Default | Meaning |
|---|---|---|
ik.iters |
4 |
DLS iterations per tick |
ik.damping |
0.05 |
DLS lambda |
ik.pos_tol |
0.002 |
position convergence tolerance (m) |
2. Per-joint MIT impedance law¶
Each of the 6 joints receives a move_mit frame implementing
with per-joint gains from the config:
impedance:
kp: [5, 25, 5, 8, 8, 5] # N*m/rad (SDK reference: 10)
kd: [0.3, 1.2, 0.3, 0.45, 0.45, 0.3] # damping (SDK reference: 0.8)
t_ff: [0, 0, 0, 0, 0, 0] # manual feed-forward trim (N*m)
3. Per-tick joint velocity clamp¶
Each tick, the new IK solution may move the joint target by at most
impedance.max_joint_vel / teleop.rate_hz — with the defaults
(1.5 rad/s at 100 Hz) that is 0.015 rad per joint per tick. Sudden input
jumps therefore turn into bounded-speed motion, never a lunge.
4. SDK joint-limit intersection¶
Your configured impedance.joint_limits_deg are automatically intersected
with the SDK's per-model limit preset (with a 2° safety margin), and the
effective limits are printed at startup. This matters because a joint clamped
by the SDK/firmware while the impedance loop keeps pulling on it buzzes and
drifts — the IK must never command what the lower layers would clamp.
Piper-X wrist limits
On the Piper-X, joints 4 and 5 are ±89° in the SDK preset — tighter than
you might expect. Whatever you put in joint_limits_deg, the
intersection wins. Check the [piper-mit] joint limits (deg): ...
startup line for the values actually in force.
What happens at MIT entry¶
Switching a loaded arm from the firmware position servo to impedance control
is the most delicate moment in the whole stack. The entry sequence in
PiperArmMIT exists to make that handover torque-continuous:
- Latch the target from measured joints. The first joint target is a copy of the measured joint angles, so the arm is never asked to jump.
- Torque snapshot — taken before the mode switch. With
gravity_ff: true(default), the backend samples each motor's holding torque 5 times over 100 ms while the position servo is still active. That average is exactly the gravity-plus-friction torque needed to hold the entry pose. It is applied as feed-forward unramped from the first MIT frame: it replaces exactly what the position servo was outputting, so the handover produces no torque step — no clunk, no droop at the entry pose. - Mode switch, sent once. Automatic mode re-sending is disabled and
set_motion_mode('mit')is issued a single time before streaming begins. - Soft-start gain ramp.
kpandkdramp from ~0 to 100 % oversoft_start_s(0.8 s default). Without the ramp, full spring stiffness snapping onto the measured pose produces the audible handover "clunk". The gravity snapshot deliberately does not ramp (see step 2). - Post-ramp deflection check. 0.7 s after the ramp completes, a one-shot sanity check compares measured joints against the target. A wrong-signed torque snapshot would double the gravity deflection instead of cancelling it — so if any joint has deflected more than 0.2 rad, the snapshot is zeroed with a warning and the arm falls back to plain spring-damper behaviour.
- 50 Hz keepalive. A background thread re-sends the hold frame whenever no command has gone out for 40 ms. This lets the gain ramp complete even if teleop is idle, and guarantees the firmware always has fresh impedance targets — the spring behaviour with the clutch released is well-defined, not "whatever the last frame said".
engage_on_start. With the defaulttrue, the backend enters impedance hold right at startup (retrying for up to 3 s while joint feedback comes up). Set it tofalseto defer entry to the first clutch engage.- Exit handback.
stop()(and any firmware-planned move such as homing) leaves MIT mode by re-enabling automatic mode handling and issuing amove_jat the measured joints — a firmware position hold. The arm cannot slump when the process exits.
One related subtlety: under gravity an impedance-controlled arm sags slightly below its commanded target. The clutch therefore latches onto the held target pose (forward kinematics of the joint target), not the sagged measured pose — otherwise every clutch re-engage would ratchet the arm downward.
Bounded contact force¶
The teleop safety envelope tethers the commanded target to the measured EEF
pose: teleop.limits.max_cmd_deviation: 0.12 m. In impedance mode this turns
stiffness into a force bound:
contact force ≈ cartesian stiffness × 0.12 m tether ≈ ~20 N with the shipped gains.
If the gripper presses into a surface (or a human hand), the target can lead the measured pose by at most 12 cm, so the spring can pull with at most that bounded force — no matter how far the operator keeps moving. This is why impedance mode is the default for contact-rich demonstrations and for safe RL exploration: the worst-case interaction force is a config-determined constant, not a function of operator error.
Tuning guide¶
Gains are tuned on the robot, by symptom:
| Symptom | Knob |
|---|---|
| Arm droops under gravity away from the entry pose | Raise kp on the drooping joint(s) |
| Audible hum or buzz | Lower kp and kd — too-high kd amplifies velocity-sensor noise into hum (SDK reference kd is 0.8; the shipped values are lower) |
| One joint feels hard to push during the gravity test | Lower that joint's kp/kd |
| Tracking feels loose or sluggish | Raise kp (higher kp tracks tighter); back off as soon as it starts to hum |
The gravity-test probe¶
gravity_test.yaml (in the repository root) is a near-zero-gain overlay for
feeling each joint by hand and probing the firmware's own gravity behaviour:
impedance:
kp: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
kd: [1.0, 1.5, 1.2, 0.5, 0.5, 0.3]
engage_on_start: true
With kp ~0 there is no spring holding the arm up; kd keeps any fall slow
and damped. Watch the arm for ~20 s after MIT mode active (keep your hands
ready to catch it):
- Arm holds its pose → the firmware does gravity-compensate in MIT mode.
- Arm sinks slowly → no firmware gravity compensation (the droop was
previously masked by friction); host-side
t_ffgravity feed-forward would be needed.
While it runs, push each joint by hand — this is the easiest way to feel
which joint's damping is off before touching the per-joint kd values.
Gains are part of the dataset provenance
The control backend and its gains (backend, kp, kd, t_ff,
gravity_ff, firmware, fps) are recorded per session in the dataset's
provenance metadata, because a learned policy is bound to the controller
it was trained against. Retuning between sessions of one dataset prints a
prominent warning at collection time. See
Dataset format.
Related pages¶
- Configuration — the full
impedance:andteleop:key reference, and how--configoverlays work. - Benchmarks — how the tracking numbers were measured.
- CLI reference —
--position,--impedance,--simand friends. - Troubleshooting — includes the impedance-hum entry.