Unit contract

Every value in this library says what it is in its own name. position_rad is radians; position_deg is degrees; velocity_erpm is electrical RPM. There is no bare position, because the one thing worse than an ambiguous unit is a plausible one.

Wire units and where they come from

Quantity

Wire

Library type

Conversion

MIT position

uint16 over ±P_max

rad, output side

uint_to_float

MIT velocity

uint12 over ±V_max

rad/s, output side

uint_to_float

MIT torque

uint12 over ±T_max

N·m, output side

uint_to_float

MIT temperature

uint8

°C

raw 40

Servo position

int16 × 0.1

degrees, mechanical †

× π/180

Servo velocity

int16 × 10

ERPM

see below

Servo current

int16 × 0.01

A (q-axis)

† Which side of the gearbox servo position refers to is not documented. position_deg always works; output_rad raises UnresolvedFrameError until you settle it on the bench (step B7) and record it with spec.evolve(...).

ERPM ↔ rad/s

rad/s at output = ERPM · 2π / (60 · pole_pairs · gear_ratio)

For the AK40-10 (14 pole pairs, 10:1) one ERPM is 7.480e-4 rad/s. This number is per-motor and there is no universal constant: TMotorCANControl hard-codes 5.82e-4 for every model, which is 22% wrong for the AK40-10 and 5% wrong even for the AK80-9 it was derived on.

If pole_pairs or gear_ratio is unknown for your spec, the conversion raises instead of guessing.

Rotor versus output

output = rotor / gear_ratio for angles and speeds; torque is the inverse. Accessors are explicit about which side they mean:

state.position_rad  # output shaft
state.position_rotor_rad  # before the gearbox

MIT position and velocity are output-side. For the AK40-10 this is confirmed rather than assumed: the velocity field’s 45.5 rad/s is 434.5 rpm, which is the datasheet’s 435 rpm no-load speed.

Torque and current

Kt is rotor-side, so output torque is amps · Kt · gear_ratio. The AK40-10’s datasheet is self-consistent here: 7.3 A × 0.056 × 10 = 4.09 N·m against a published 4.1 N·m peak.

The MIT reply’s third field is torque, not current — the manual’s own unpack_reply names it so. state.torque_nm is what the wire carries. state.estimated_current_a inverts an idealised lossless model and warns once while Kt is not Source.MEASURED.

MIT Kp and Kd are not in SI units

They are the firmware’s own gains, dimensionless to us, spanning 0–500 and 0–5. They are not stiffness in N·m/rad or damping in N·m·s/rad, and nothing in this library converts them. TMotorCANControl’s set_impedance_gains_real_unit(K, B) documents them as N·m/rad and N·m·s/rad and then passes them through unconverted — the name is the bug.

Feed the derivative, not zero

The firmware computes:

tau = kp * (p_des - p) + kd * (v_des - v) + tau_ff

If you command a moving position while leaving velocity at 0, the kd term actively fights the motion needed to follow it, and you get a following error of roughly kd * v / kp. Measured on an AK40-10 tracking a ±0.5 rad sinusoid at 0.3 Hz (peak 0.94 rad/s) with kp=20, kd=0.5:

command

median following error

velocity=0.0

23.33 mrad

predicted kd·v/kp

23.6 mrad

velocity= target derivative

6.74 mrad

Feeding the derivative cut the error 3.5×, and the prediction matched the measurement to 1%. The 6.74 mrad residual is close to the datasheet’s 5.24 mrad of gearbox backlash, so that is the mechanical floor rather than a tuning problem.

w = 2 * math.pi * freq
m.update(
    position=amp * math.sin(w * t),
    velocity=amp * w * math.cos(w * t),  # not 0.0
    kp=20.0,
    kd=0.5,
)

Quantisation

No field can encode an exact zero. Each is a symmetric range over an even-sized field, so the midpoint sits half an LSB above zero:

Field

LSB

value of a commanded 0.0

position

0.381 mrad

+0.191 mrad

velocity

22.2 mrad/s

+11.1 mrad/s

torque

2.44 mN·m

+1.22 mN·m

The torque floor matters for a safe stop. On an AK40-10 it is 2% of the 60 mN·m break-away torque, so the output genuinely cannot move — but it is a floor, not a null.

Field range is not a limit

A FieldRange exists to be handed to the quantiser and carries bits. A PhysicalLimits says what the motor can do and has no bits, so it can never reach a codec. They are different types because across the AK line they differ in both directions:

Motor

MIT torque field

datasheet peak

binding

AK40-10

±5.0 N·m

4.1 N·m

the motor

AK80-9 V3.0

±18 N·m

22 N·m

the field

ClampMode.EFFECTIVE (the default) clamps to min(field, physical) per field.