roostField / Lab
Curriculum
Phase 01Lesson 1 of 5
70 min
Instrument the robotWeeks 1–2

Inside the arm: actuators, encoders, and control modes

The WidowX AI on your desk is a sealed production system: six integrated brushless joints on an internal CAN FD bus behind a 500 Hz real-time controller. The AK60-6 on your bench is the same actuator species with the lid off. Open it up — outrunner, planetary gear, encoder, FOC firmware — and learn the impedance law every modern joint runs before you build policies on top of it.

After this lesson you can
  • Trace a policy action from a UDP packet on the host through the iNerve's 500 Hz loop and the internal CAN FD bus to torque at a joint, naming each layer's rate and what it hides.
  • Explain the MIT-mode control law (torque = kp·position-error + kd·velocity-error + feedforward) as a firmware-level impedance controller, and choose the five command fields to realize stiff position, pure torque, or a gravity-compensated float.
  • Compute reflected inertia through a 6:1 versus 350:1 reduction, and gravity-torque margin for the rated 1.5 kg payload at full 0.769 m reach against the WidowX AI's 27 N·m shoulder-class effort limit.
  • Bring up a CubeMars AK60-6 on a bench CAN bus — wiring verified against the manual, termination checked, MIT mode enabled — and log position/velocity/torque telemetry under a commanded sinusoid.

You have spent years on one side of a host-device boundary: CPU code that queues work into a GPU whose firmware, scheduler, and clocks you influence but do not control. Your new hardware has the same shape — and this time there are two specimens. The WidowX AI on your desk is the production system: a 6-DOF arm plus parallel-jaw gripper whose joints are integrated brushless actuators on an internal CAN FD bus, run by a real-time controller in the base — the iNerve — at 500 Hz, with your workstation talking to it over Ethernet. It is sealed and not user-serviceable, exactly like the GPU in your workstation. The CubeMars AK60-6 on your bench is the teaching specimen: the same class of actuator as the arm's joints, alone on the bench with nothing between you and its firmware but a USB–CAN adapter. The arm is what you will run policies on; the bare actuator is the one you get to take apart, wire up, and command frame by frame. Every intelligent thing you will build in this course — up to and including a π₀-class VLA emitting action chunks — ultimately becomes torque at joints like this one. This lesson opens one up.

Conceptual Foundation: Impedance, Inertia, and Stability

Before manipulating the MIT-mode parameters, define the physical quantity they shape. Mechanical impedance is the dynamic relationship between applied torque and joint motion. Two related conventions appear in the literature, so name the denominator. The rotational dynamic stiffness is torque over angular displacement: Zθ(s)=τ(s)/θ(s)=kp+kdsZ_\theta(s)=\tau(s)/\theta(s)=k_p+k_d s for the spring-damper law. The velocity-based mechanical impedance is Zω(s)=τ(s)/ω(s)=kd+kp/sZ_\omega(s)=\tau(s)/\omega(s)=k_d+k_p/s because ω(s)=sθ(s)\omega(s)=s\theta(s). Both describe the same controller; mixing the first formula with the second definition is a units error. The abstraction is useful because it lets the commanded joint behave like a predictable virtual spring and damper at its interface with the world.

A critical property of this behavior is backdrivability. This is the ability of external forces applied at the output to rotate the motor rotor. It is not a binary property but a function of the inertia ratio. The reflected inertia JreflectedJ_{\text{reflected}} is the equivalent inertia seen by the load, calculated as N2JrotorN^2 J_{\text{rotor}}, where JrotorJ_{\text{rotor}} is the moment of inertia of the motor rotor about its spin axis. If JreflectedJ_{\text{reflected}} is small relative to the load inertia, the load can 'drag' the motor, allowing the encoder to sense external forces. High gear ratios increase reflected inertia, making the joint feel locked and reducing its ability to sense contact.

ζ  =  kd2kpJload\zeta \;=\; \frac{k_d}{2\sqrt{k_p J_{\text{load}}}}
The damping ratio ζ\zeta determines the stability of the closed-loop system. JloadJ_{\text{load}} is the total inertia at the output.

The gains kpk_p and kdk_d are not arbitrary; they must satisfy a stability constraint. The damping ratio ζ\zeta quantifies how the system responds to disturbances. If ζ<1\zeta < 1, the system is underdamped and will oscillate around the target. If ζ>1\zeta > 1, it is overdamped and responds slowly. For critical damping (ζ=1\zeta = 1), the system reaches the target as fast as possible without overshoot. The natural frequency ωn=kp/Jload\omega_n = \sqrt{k_p / J_{\text{load}}} sets the upper limit of the tracking bandwidth. A higher kpk_p increases bandwidth but also increases the torque demand for a given error, potentially hitting current limits.

Worked Example: Calculating Critical Damping

Consider a joint with a load inertia Jload=0.05J_{\text{load}} = 0.05 kg·m². We want to design a controller that is critically damped (ζ=1.0\zeta = 1.0) to ensure fast, stable response without overshoot. We choose a stiffness kp=50k_p = 50 N·m/rad. We need to find the required damping coefficient kdk_d.

  1. Calculate the term under the square root: kpJload=50×0.05=2.51.581\sqrt{k_p J_{\text{load}}} = \sqrt{50 \times 0.05} = \sqrt{2.5} \approx 1.581 N·m·s/rad.
  2. Solve for kdk_d using the damping ratio formula: kd=2ζkpJload=2×1.0×1.581=3.162k_d = 2 \zeta \sqrt{k_p J_{\text{load}}} = 2 \times 1.0 \times 1.581 = 3.162 N·m·s/rad.
  3. Verify the natural frequency: ωn=50/0.05=100031.6\omega_n = \sqrt{50 / 0.05} = \sqrt{1000} \approx 31.6 rad/s, which is about 5 Hz. This is a reasonable bandwidth for a robotic joint.

If we had chosen kd=0.8k_d = 0.8 N·m·s/rad (a common default for softer motions), the damping ratio would be ζ=0.8/(2×1.581)0.25\zeta = 0.8 / (2 \times 1.581) \approx 0.25. This is significantly underdamped, meaning the joint would oscillate several times before settling. For a precise positioning task, this would be unacceptable. The calculation shows that gain selection is a direct function of the physical load, not just a preference for 'stiffness'.

Checkpoint 01

You increase kpk_p from 50 to 200 N·m/rad but keep kdk_d at 3.162 N·m·s/rad. What happens to the system's behavior?

Anatomy of an integrated actuator: the AK60-6 with the lid off

Start with the unit of composition. The AK60-6 is a 380-gram disc that contains an entire robot joint: motor, gearbox, sensing, and control electronics in one sealed package. It is the actuator class MIT's Mini Cheetah made famous — the quadruped-joint recipe that has since quietly taken over modern manipulation. Four subsystems, and the rest of this lesson is a tour of the consequences of each one.

  • A brushless outrunner motor, KV80 winding. Outrunner means the magnet ring spins around the stator: a large air-gap radius buys more torque per amp than an inrunner of the same mass, at the cost of top speed — the right trade for a joint.
  • A 6:1 planetary reducer. One modest stage, not hundreds stacked. It multiplies torque by six and — the number that defines this actuator class — multiplies reflected inertia by only thirty-six.
  • A 21-bit magnetic encoder (the inner-ring angle sensor): 2212.12^{21} \approx 2.1 million counts per revolution, about 3 microradians per count.
  • An FOC drive board: field-oriented control of the phase currents at a 10 kHz-class rate, plus overcurrent, over/under-voltage, and over-temperature protection. When you command this actuator, you are talking to this board.

Two datasheet numbers define the envelope, and both trace back to the winding. KV is the motor constant quoted as no-load rpm per volt at the winding; its reciprocal is the torque constant, Kt9.55/KVK_t \approx 9.55 / K_V N·m per amp — about 0.12 N·m/A for this KV80 winding, or roughly 0.7 N·m/A at the output shaft before gear losses. Lower KV means more copper turns: more torque per amp, less speed per volt. The result at the rated 24 V: 3 N·m rated torque, 9 N·m peak, about 490 rpm rated speed and 640 rpm no-load. Read rated-versus-peak exactly like sustained-versus-boost clocks: rated is what thermals allow indefinitely; peak is a burst budget measured in seconds, spent on acceleration and impacts, repaid as heat — the on-board over-temperature protection is what forecloses on the debt.

The gearbox is an impedance transformer — the mechanical analog of the electrical one from your EE past. It scales speed down by the ratio NN, torque up by NN (times an efficiency near 0.9 for a single planetary stage), and — the property that decides how the joint feels — it reflects the rotor's inertia by N2N^2. Write the rotor's kinetic energy in terms of the link velocity and the square falls out:

Ek  =  12Jrotorωmotor2  =  12Jrotor(Nωlink)2  =  12N2JrotorJreflected  ωlink2E_k \;=\; \tfrac{1}{2}\, J_{\text{rotor}}\, \omega_{\text{motor}}^2 \;=\; \tfrac{1}{2}\, J_{\text{rotor}}\, (N\, \omega_{\text{link}})^2 \;=\; \tfrac{1}{2}\, \underbrace{N^2 J_{\text{rotor}}}_{J_{\text{reflected}}}\; \omega_{\text{link}}^2
Seen from the output, rotor inertia scales by N². At N = 6 that is a factor of 36; at a hobby servo's 350:1 it is 122,500.

Plug in numbers. An outrunner rotor this size has an inertia of order 10510^{-5} kg·m² — a pancake of magnets at a couple of centimeters of radius. Through 6:1 that reflects to roughly 4×1044 \times 10^{-4} kg·m², a rounding error next to any real load on the output. Through 350:1 the same rotor would reflect to over a full kg·m² and dominate everything downstream. Friction reflects by NN, not N2N^2, and tells the same story: a couple of millinewton-meters of motor drag becomes ~0.01 N·m through 6:1 but ~0.7 N·m through 350:1. This is why the AK60-6 is backdrivable — grab the output and it turns, the motor spinning a gentle six revolutions per output turn — while a 350:1 hobby servo feels locked. The class name is quasi-direct drive: enough reduction for useful torque, little enough that torque flows honestly in both directions. Backdrivable means the joint can feel the world through its own motor current, and torque you command at the winding actually arrives at the link — the property the next section's control law depends on. One caution while the lid is off: the 21-bit encoder's 3 µrad step is a statement about quantization, not truth. Resolution is not accuracy — a distinction the arm will put a price on shortly.

The MIT-mode law: an impedance controller in firmware

The AK60-6 has two firmware personalities. One is a conventional menu of servo modes — position loop, velocity loop, current loop; pick one, send one setpoint. The other, MIT mode (named for the Mini Cheetah firmware that popularized it), collapses the menu into a single command type: every frame you send carries five numbers — desired position pdesp_{des}, desired velocity vdesv_{des}, gains kpk_p and kdk_d, and a feedforward torque τff\tau_{ff} — and the drive board evaluates one law at FOC rate:

τ  =  kp(pdesp)  +  kd(vdesv)  +  τff\tau \;=\; k_p\,(p_{des} - p) \;+\; k_d\,(v_{des} - v) \;+\; \tau_{ff}
The MIT-mode control law. The firmware evaluates it at its 10 kHz-class loop rate; your job is to choose what the five numbers mean.

Read it physically, not algebraically. The kpk_p term is a spring of stiffness kpk_p N·m/rad anchored at pdesp_{des}; the kdk_d term is a damper of kdk_d N·m·s/rad referenced to vdesv_{des}; τff\tau_{ff} is a bias force you superimpose. Commanding this actuator is not "go to angle X" — it is "behave as if a spring-damper of my choosing connected you to a moving anchor." That is an impedance controller, and it runs in firmware, which matters for one reason above all: contact. Contact dynamics play out in single milliseconds, far too fast for any host loop — but when the link hits something, the spring law keeps holding at FOC rate between your host packets, so the actuator's response to a collision is decided by the kpk_p and kdk_d you chose in advance, not by how fast your Python process can react. And because the law spans the whole torque interface, the familiar modes are just corner cases of it:

ModeRecipeWhat the joint feels likeUse it for
Stiff position servohigh kpk_p, moderate kdk_d, τff=0\tau_{ff} = 0A rigid joint that fights every disturbanceClassic setpoint tracking; hides the error dynamics
Compliant trackinglow kpk_p, low kdk_dA soft spring pulling toward the targetContact-rich moves; collisions become bounces, not breakage
Pure torquekp=kd=0k_p = k_d = 0, τff\tau_{ff} onlyDirect force; nothing holds positionForce control and system ID; the load decides where things end up
Damped floatkp=0k_p = 0, small kdk_d, τff=0\tau_{ff} = 0Moving through honeySafe manual repositioning
Gravity-comp floatkp,kd0k_p, k_d \approx 0, τff\tau_{ff} = gravity modelWeightless — hangs wherever you leave itKinesthetic teaching, leader–follower teleop
Servo modes (non-MIT)one setpoint, onboard cascaded loopsWhatever the vendor tunedSimple single-axis jobs without per-command gains
One law, the whole mode space: MIT-frame recipes and the servo-mode fallback
Checkpoint 02

In MIT mode you send a frame with p_des = +1.57 rad, v_des = 0, kp = 0, kd = 0, τ_ff = 0. What does the actuator do?

The WidowX AI: the same ideas, sealed and shipped

Now scale the specimen up. The WidowX AI is six integrated brushless actuators — the same species as your bench unit, larger and not user-serviceable — built into a 6-DOF arm with a parallel-jaw gripper, on an internal CAN FD bus you never touch. The controller you do touch is the iNerve in the base: it runs the real-time loop at 500 Hz, closing joint-level control against the actuators and streaming joint state back to your workstation over Ethernet/UDP at roughly the same rate with sub-millisecond timing. Note what changed from the hobby-arm world: the robot has a real-time controller now, and it is not your Python process. Your workstation is a client — it proposes targets and consumes telemetry, and the 500 Hz loop runs whether or not your process shows up on time. Six degrees of freedom also means the arm generically reaches full position-plus-orientation poses — all of SE(3) — within its dexterous workspace: no missing-DOF excuses when a grasp demands a specific approach angle.

JointPosition limitVelocity limitEffort limit
J0 (base yaw)±180°360°/s27 N·m
J10° to 180°360°/s27 N·m
J20° to 135°360°/s27 N·m
J3, J4±90°540°/s7 N·m
J5±180°540°/s7 N·m
Gripper40 mm stroke100 N grip force
Published WidowX AI joint limits (Trossen specification sheet)

The arm-level numbers frame everything downstream: 1.5 kg payload, 0.769 m reach (1.4 m span), 4 kg arm mass, 1 mm repeatability, fed by a 24 V / 25 A supply. Your follower-configuration unit ships with the precision-grip fingers (molded silicone pads) and an Intel RealSense D405 on the wrist mount — the camera whose frames you will spend the rest of this module timestamping and calibrating. Read the joint table like a capacity planner: 27 N·m on the base yaw and the two gravity-bearing shoulder-class axes, 7 N·m on the three wrist joints that mostly steer the last few hundred grams, and velocity limits generous enough — 360°/s at the shoulder is nearly 5 m/s at full reach — that your runtime will clamp far below them.

You reach all of it through `libtrossen_arm`, the open-source C++ driver with Python bindings, which exposes four joint modes: position, velocity, effort, and external effort. Map them back to the law you just learned and the driver stops being magic. Position and velocity modes are stiff tracking with vendor-tuned loop gains — the high-kpk_p corner. Effort mode is the τff\tau_{ff}-only corner: raw torque targets. External effort is the gravity-comp float, shipped as a product: the controller adds its own gravity and friction model — the driver calls these effort corrections and friction parameters, and both are configurable, alongside joint modes, joint limits, and motor parameters — so the arm hangs weightless and a human can move it by hand. That mode is the basis of kinesthetic teaching and leader–follower teleop, and it exists only because the actuators are backdrivable. What the driver hides is the per-command gain freedom: you select a mode per joint rather than kpk_p/kdk_d per packet, and the joint-firmware interface underneath is Trossen's business, not yours. That is the correct trade for a production arm — and after the bench exercise you will know exactly what is being traded.

Now read the spec sheet like a capacity planner with a gravity bill to pay. The worst static case is the arm horizontal at full reach with the rated payload — every gram at its longest lever. Trossen does not publish link masses or lengths, so model the moving mass with an illustrative point-mass breakdown of a 0.77 m-reach, 4 kg-class arm and label it as such:

τshoulder  =  g(mpayloadL  +  imiri)  =  9.81×(1.5×0.769  +  0.976)    20.9 N⋅m\tau_{\text{shoulder}} \;=\; g\Big(m_{\text{payload}}\, L \;+\; \textstyle\sum_i m_i r_i\Big) \;=\; 9.81 \times \big(1.5 \times 0.769 \;+\; 0.976\big) \;\approx\; 20.9\ \text{N·m}
Arm horizontal at full 0.769 m reach with the rated 1.5 kg payload, against a 27 N·m shoulder-class effort limit. The link geometry is an illustrative approximation — Trossen publishes payload, reach, and effort limits, not link masses.
payload_margin.py — gravity torque vs. the published effort limitpython
import numpy as np

G = 9.81
REACH_M = 0.769            # published reach
EFFORT_LIMIT_NM = 27.0     # published shoulder-class effort limit

# Illustrative point-mass model of a 0.77 m-reach, 4 kg, 6-DOF arm.
# Trossen does not publish link masses or lengths -- these estimate the
# moving mass beyond the shoulder axis; treat them as +/-30%.
segments = np.array([
    [0.90, 0.18],   # upper-arm structure + elbow actuator
    [0.70, 0.42],   # forearm structure + wrist actuators
    [0.80, 0.65],   # wrist assembly + gripper + D405 camera
])
tau_links = G * np.sum(segments[:, 0] * segments[:, 1])

for payload_kg in (0.0, 0.5, 1.0, 1.5):
    tau = tau_links + payload_kg * G * REACH_M
    pct = 100.0 * tau / EFFORT_LIMIT_NM
    headroom = EFFORT_LIMIT_NM - tau
    print(f"payload {payload_kg:.1f} kg: tau {tau:5.1f} N*m | "
          f"{pct:3.0f}% of limit | {headroom:4.1f} N*m left for dynamics")

The output tells the story: the empty outstretched arm costs about 35% of the effort limit; the rated 1.5 kg payload at full reach pushes it to roughly 77%, leaving about 6 N·m of headroom that must cover every acceleration, deceleration, and disturbance in that pose. That is what a payload rating means — not "the arm breaks above this" but "above this, the torque budget stops covering dynamics." And notice which spec is honest about the whole chain: the 1 mm repeatability figure. Encoder counts on this actuator class are measured in microradians — micrometers at the fingertip — but bracket flex, gear lash, and thermal drift all stand between the encoder and the physical gripper. Resolution is not accuracy; the repeatability line is the vendor telling you where the truth floor actually sits. When the wrist D405 disagrees with forward kinematics about where the fingers are, believe the camera — the calibration that reconciles them begins in the next lesson.

Checkpoint 03

Your static model says holding the rated 1.5 kg at full 0.769 m reach takes about 21 N·m at a shoulder-class joint whose published effort limit is 27 N·m. What is the correct reading of the remaining 6 N·m?

What this means for your policies

When a π₀-class VLA starts emitting action chunks in Phase 04, every action lands on this arm as a position (or velocity) target handed to libtrossen_arm and consumed by the iNerve at 500 Hz. The policy runs at a few hertz; the chunk is interpolated into a dense setpoint stream; the impedance machinery below turns each setpoint into torque. Your safety envelope is exactly the published limits: the driver's configurable joint limits and effort ceilings (27 and 7 N·m), velocity caps far below the datasheet 360–540°/s, step-size clamps so the position loop never sees a lunge-sized error, and a gripper command ceiling well under the 100 N maximum — 100 N over a 40 mm stroke is a ten-kilogram-force pinch, sized for secure grasps, not for anything that must survive being squeezed, fingers included. The ecosystem meets you here: the arm has official LeRobot integration and ROS 2 support, and Trossen's OpenPI tutorial targets exactly this hardware — with the caveat that the published examples are bimanual, so your single-arm adaptation needs its own validation. Lab 0 turns these clamps from advice into code.

Studio: bench bring-up of the AK60-6

Everything above becomes real the first time you feel kpk_p change under your hand — which is the point of owning the bench unit. You will wire the actuator to a 24 V supply and the CubeMars USB–CAN adapter, bring the bus up, enable MIT mode, and command a slow sinusoid while logging telemetry. Two rules before any power flows. First: verify the exact connector pinout against the CubeMars manual for your unit — connector conventions and wire colors vary between hardware revisions, this cabling is famously not obvious, and you should trust neither forum posts nor this course on it. Second: bolt or clamp the actuator body to the bench — an unfixtured actuator commanded to 9 N·m torques its own stator instead of the load, and 9 N·m is more than enough to send 380 g flying.

bench_sinusoid.py — MIT-mode sinusoid with telemetry loggingpython
import math
import time

AMPL_RAD = 0.5      # +/-0.5 rad: small, slow, visible
FREQ_HZ = 0.2       # one cycle every 5 s
KP = 8.0            # N*m/rad: a soft spring to start
KD = 0.8            # N*m*s/rad: enough damping to kill ringing
DURATION_S = 30.0
RATE_HZ = 200.0     # host command rate; the FOC loop runs ~50x faster

def mit_command(bus, p_des, v_des, kp, kd, tau_ff):
    """Pack and send one MIT-mode frame.

    Implement the byte layout from the CubeMars manual for YOUR firmware
    version: each field is quantized into a fixed range before packing,
    and the ranges differ between models. Do not copy offsets from forums.
    """
    raise NotImplementedError

def read_reply(bus):
    """Unpack the status reply -> (pos_rad, vel_rad_s, torque_nm)."""
    raise NotImplementedError

def run(bus):
    log = []
    t0 = time.monotonic()
    while (t := time.monotonic() - t0) < DURATION_S:
        w = 2.0 * math.pi * FREQ_HZ
        p_des = AMPL_RAD * math.sin(w * t)
        v_des = AMPL_RAD * w * math.cos(w * t)   # feedforward the derivative
        mit_command(bus, p_des, v_des, KP, KD, tau_ff=0.0)
        pos, vel, tau = read_reply(bus)
        log.append((t, p_des, pos, v_des, vel, tau))
        time.sleep(max(0.0, 1.0 / RATE_HZ - (time.monotonic() - t0 - t)))
    return log   # save raw arrays; analysis happens offline
Studio exercise 01

Bench bring-up: make the AK60-6 move, then feel the gains

Work through bring-up in stages, verifying each before the next. (1) Wiring: 24 V bench supply to the power leads with polarity triple-checked (XT30-class connector), supply current limit set low — 1–2 A — for first power-up; CAN H and CAN L as a twisted pair to the USB–CAN adapter; 120 Ω termination across H/L; common ground between supply and adapter; every pin verified against the CubeMars manual, not guessed from wire colors. (2) Bus-up: configure the adapter to the bitrate given in the manual and confirm you can see the actuator's frames before commanding anything. (3) Motion: enable MIT mode, command the sinusoid from bench_sinusoid.py with the soft gains given, and log tt, pdesp_{des}, pp, vv, τ\tau for 30 seconds. (4) Feel: with the sinusoid stopped and a constant pdesp_{des} held, gently try to turn the output hub by hand at kp=2k_p = 2, then 8, then 20 N·m/rad; repeat with kdk_d at 0 versus 1. Write five sentences connecting what each gain change felt like to what the logged torque trace showed during your disturbance.

Need a hint?

If the bus is silent: measure the resistance across CAN H–L (about 60 Ω with two 120 Ω terminators present), check the common ground, and confirm the bitrate matches the manual — a bitrate mismatch looks exactly like a dead bus. Plot logged position over the commanded sinusoid: a phase lag that shrinks as kpk_p rises is the impedance law working. Keep fingers on the smooth output hub, away from any lever arm or pinch point, and leave the supply current limit low until the gains are proven.

Where this goes next: you can now read the arm the way you read a device datasheet — integrated impedance-controlled joints behind a 500 Hz real-time controller, with a driver whose modes are corner cases of one firmware law. The next lesson, Coordinate frames and rigid transforms, stacks this hardware into geometry: how six joint angles compose into a gripper pose, and why the frame conventions you choose now decide how painful hand-eye calibration with the wrist D405 will be later. The 2 ms control tick and the UDP state stream you met here become line items in the latency budget of Time is a sensor: clocks, timestamps, and latency, and the stop levels and effort clamps become hard requirements of Lab 0's episode logger.