Motor drivers
Serial bus servos (recommended)
ST-3032
ST-3032 (Feetech STS3032) serial bus servo.
Smaller sibling of the ST-3215 — same SCS protocol, same 4096-count 12-bit magnetic encoder, same operating modes (0 = position, 1 = wheel), same default 1 Mbps bus. The only differences are mechanical / electrical.
Full datasheet: docs/datasheets/feetech_sts3032.pdf (model
ST-3032-C062, Edition A/0 2025-11-30). Headline specs at typical
12 V operation, all ±10% per Feetech:
Working voltage 9–14 V (typ 12 V) ST-3215 typ 6–12.6 V
No-load speed 0.067 s/60° = 148 RPM = 888 °/s
(driver default ``max_dps=600`` is below
this — instantiate with ``max_dps=900`` if
you want to chase no-load top speed)
Stall torque 10 kg·cm (139 oz·in) ST-3215 ~30 kg·cm
Stall current 1.6 A
Rated torque 3.3 kg·cm (1/3 of stall)
Rated current 500 mA
No-load current 100 mA
Idle current 20 mA
Kt 6.3 kg·cm/A
Size 23 × 12 × 27.5 mm ST-3215 larger
Weight 20.6 g
Gear 1/205, coreless motor
Backlash ≤ 1.0°
Over-temp shutdown 80 °C
Over-current > 80 % stall for 2 s
Max position update 1 ms
Because the wire protocol is identical, ST3032 and ST3032Motor
are thin marker subclasses of ST3215 / ST3215Motor with no
behavioural override. Use them when your bench actually carries an
ST-3032 — the typed name documents the hardware and gives us a place
to specialise defaults later (lower max_dps, torque caps) if hardware
testing surfaces a real difference.
ST-3032 and ST-3215 instances on the same UART share the bus registry, so mixing them on one daisy chain is fine — each servo is addressed by its 1-byte ID regardless of model.
⚠ Voltage rail: an ST-3032 will brown out below ~9 V. Don’t share a 6 V bus with ST-3215s; budget a separate 12 V rail.
- class openbricks.drivers.st3032.ST3032(servo_id, uart_id=1, tx=17, rx=16, baud=1_000_000, dir_pin=None, min_raw=0, max_raw=4095, range_deg=360)[source]
Bases:
ST3215One ST-3032 in position-servo mode (Servo interface).
Behaves identically to
ST3215— see that class for the full API. Subclassed only to let user code spell the actual hardware in place.
- class openbricks.drivers.st3032.ST3032Motor(servo_id, uart_id=1, tx=17, rx=16, baud=1_000_000, dir_pin=None, invert=False, steps_per_dps=_DEFAULT_STEPS_PER_DPS, max_dps=600.0)[source]
Bases:
ST3215MotorOne ST-3032 in wheel/continuous-rotation mode (Motor interface).
Behaves identically to
ST3215Motor— see that class forrun_speed/angle/run_anglesemantics. Subclassed only to let user code spell the actual hardware in place.
ST-3215
ST-3215 (Waveshare/FeeTech) serial bus servo.
These servos daisy-chain on a half-duplex UART bus: one TX line shared by host and all servos, each servo addressed by a 1-byte ID. Packet format (as in the FeeTech SCServo / Dynamixel-style protocol):
0xFF 0xFF ID LEN INSTR PARAM… CHECKSUM
CHECKSUM = ~(ID + LEN + INSTR + sum(PARAM)) & 0xFF LEN = number of params + 2
Common instructions:
0x01 PING -> probe the servo
0x02 READ -> READ reg len
0x03 WRITE -> WRITE reg value...
Key registers (ST-3215):
0x21 Operation Mode — 0 = position, 1 = wheel/continuous
0x28 Torque Switch — 1 = enable, 0 = coast
0x2A Goal position (low, high) — int16, 0..4095 over ~360°
0x2E Goal speed (low, high) — sign-magnitude; in wheel mode this
is the velocity setpoint (bit 15 of high byte = direction)
0x38 Present position (low, high) — read only. 12-bit absolute
angle within one revolution: 0..4095 over 360°. Wraps to 0
at every full turn; we accumulate multi-turn revolutions in
software via a wrap heuristic in ``ST3215Motor.angle``.
Half-duplex wiring: most ST-3215 boards use a single data line driven by a
TX/RX switching circuit, but MicroPython UART pins are usually separate. If
your adapter exposes TX and RX separately, just wire them normally. If you
have a true half-duplex bus, you’ll need a driver chip or a direction-enable
GPIO — set dir_pin below.
Two classes here:
ST3215— position-mode (Servo interface), for grippers / lifts / sensor turrets.move_to(angle)blocks until reached.ST3215Motor— continuous-rotation mode (Motor interface), for drivebase wheels. Switches the servo into mode=1 at construction and exposesrun_speed(dps)/angle()/brake()so it drops intoDriveBasethe same wayMG370Motordoes.
The ST-3032 (smaller sibling — 12 V, ~10 kg·cm, same SCS protocol)
ships as marker subclasses in openbricks.drivers.st3032.
Only a minimal subset of the protocol is implemented here. PR welcome.
- class openbricks.drivers.st3215.ST3215(servo_id, uart_id=1, tx=17, rx=16, baud=1_000_000, dir_pin=None, min_raw=0, max_raw=4095, range_deg=360)[source]
Bases:
ServoOne ST-3215 servo on a shared bus.
- class openbricks.drivers.st3215.ST3215Motor(servo_id, uart_id=1, tx=17, rx=16, baud=1_000_000, dir_pin=None, invert=False, steps_per_dps=_DEFAULT_STEPS_PER_DPS, max_dps=600.0)[source]
Bases:
MotorOne ST-3215 in wheel/continuous-rotation mode.
Implements the openbricks
Motorinterface so it drops directly intoDriveBase. The servo’s internal velocity loop handles closed-loop speed tracking — we just write the setpoint and read the multi-turn accumulated angle.angle()accumulates in software because the servo’s Present-Position register is a 16-bit signed counter that wraps every ~3 turns (4096 counts/rev × ~8 turns to wrap at ±32767). Same wrap-correction shape asPCNTEncoder.- hold()[source]
Actively hold the current shaft angle so the position PID resists rotation. Subsequent
run_speed/brake/coastcalls transparently restore wheel mode.Holding never crosses a turn boundary (the shaft is meant to stay put), so the mechanism is chosen to avoid disturbing the servo’s turn model:
If this motor has already been switched to multi-turn step mode (a prior
run_angle), hold with a zero-count step — step mode keeps holding its current target.Otherwise (e.g. a
DriveBasewheel that has only ever run in velocity mode), hold in single-turn position mode with goal=present. This deliberately does NOT zero the angle-limit registers, so a wheel motor keeps its stock single-turn present-position reads and its odometry stays intact.
- done()[source]
Pybricks-style status check for an in-flight
run_angle(wait=False)move. ReturnsTrueif no move is in flight (the normal case) or the active move has parked (its remaining-to-target register has counted down to within tolerance). ReturnsFalsewhile the move is still running.Calling
done()is what advances the move: each call reads the step-mode remaining register once. For a move larger than 7 turns (issued as back-to-back steps),done()writes the next step once the current one parks, and on the final step runs the end-of-movethen=dispatch and clears the pending state. So polling cadence matters for >7-turn moves — the wheel sits idle at each step boundary until the nextdone()advances it. With a typicaltime.sleep_ms(10)poll that’s a single-tick gap; if you never poll, a multi-step move stalls after the first step.
- angle()[source]
Return shaft angle in degrees, multi-turn accumulated.
In STEP mode the present-position register reads remaining-to- target rather than absolute position, so we can’t derive the angle from it — return the software accumulator, which
run_angleadvances by the executed step counts as each step parks. In wheel/position mode, rebuild the accumulator from the encoder via the wrap heuristic.
- run_angle(deg_per_s, target_angle, wait=True, tolerance_deg=0.5, kp=None, poll_ms=None, debug=False, then='coast')[source]
Rotate by
target_angledegrees at up todeg_per_s, ending withintolerance_degof the target.target_angleis RELATIVE and UNBOUNDED —run_angle(200, 360)rotates one full turn forward,run_angle(200, 1080)three turns,run_angle(200, -540)one and a half turns back. Direction is the sign oftarget_angle.Implementation: the servo is driven in step mode (op_mode=3) for the move. In step mode the goal-position register is a signed relative step — write N counts and the shaft advances N counts from where it is, with no single-turn 0/4095 wrap to cross (the prerequisite is angle limits = 0, set once on the first call by
_ensure_step_limits). This is what makes moves past 180° / past one full turn work: the older single-turn position mode (op_mode=0) clamps to 0..4095 and a target across the boundary was executed the wrong way round, capping real motion at roughly half a turn.A single step write covers up to ±7 turns (the STS multi-turn envelope); larger moves are issued as back-to-back ±7-turn steps, each parking before the next — still no boundary to cross. The servo’s internal PID handles convergence (≈0.088° per encoder count); completion is detected by reading the step-mode remaining-to-target register and waiting for it to count down to ~0 (see
_read_step_remaining). The shaft-angle accumulator is advanced by the counts actually travelled soangle()/reset_anglestay correct across the move.thenselects the end-state, pybricks-style:"coast"(default) — cut torque; wheel free-wheels. The nextrun_speed/brake/run_angletransparently re-enables torque and restores the mode it needs."brake"— restore wheel mode and write goal_speed=0 so the servo’s velocity loop actively holds zero rotation rate."hold"— leave the servo in step mode; its position PID is already holding the target and resisting rotation.
wait=Falsekicks off the move and returns immediately without blocking. Use it for concurrent multi-motor moves, pybricks-style:left.run_angle(60, 720, wait=False) right.run_angle(60, 720, wait=False) while not (left.done() and right.done()): time.sleep_ms(10)
Multi-revolution targets are supported in
wait=Falsemode too. For a move within ±7 turns the whole thing is one step write anddone()simply reports convergence; for a larger movedone()issues each subsequent ±7-turn step once the previous one parks, so you must keep polling. The end-statethen=dispatch is deferred untildone()reports the final step has converged.Any subsequent motion command (
run,run_speed,brake,coast,hold,run_angle) supersedes a pendingwait=Falsemove — the new command takes over and the pending state is dropped (pybricks “new command wins”).The legacy
kp/poll_ms/debugarguments are accepted for back-compat with the velocity-mode implementation but no longer apply — the PID lives on the servo, not in Python.
- class openbricks.drivers.st3215.SyncServoGroup(servos)[source]
Bases:
objectCoordinated multi-servo writes via SCServo SYNC WRITE.
All servos must share one
_SCServoBus(same UART). Mixed servo types (ST3215,ST3215Motor,ST3032,ST3032Motor) are fine since they all speak the same SCS protocol — SYNC WRITE just blasts the same register on all listed IDs.Use this whenever you have multiple servos that should apply a setpoint at the same packet boundary (drivebase wheels, multi- finger gripper) — each servo receives its byte slot of the broadcast packet at the same instant, instead of N serialised individual writes.
Example
from openbricks.drivers.st3215 import ST3215Motor, SyncServoGroup left = ST3215Motor(servo_id=1) right = ST3215Motor(servo_id=2, invert=True) group = SyncServoGroup([left, right]) # Both wheels start moving at the same packet boundary — # one SYNC WRITE instead of two individual writes. group.set_goal_speeds([200, 200])
- set_goal_speeds(speeds_dps)[source]
Write goal-speed on every servo in one SYNC WRITE packet.
speeds_dpsis a list parallel to the servos given at construction. Each servo’s own_encode_goal_speedis used, so per-servoinvert/steps_per_dps/max_dpsare respected.Servos that don’t expose
_encode_goal_speed(i.e. the position-modeST3215class) raiseTypeError.
DC gear motors with encoders
JGB37-520
JGB37-520 geared DC motor with magnetic quadrature encoder.
This is a 6-wire motor: two power leads (into an H-bridge, typically L298N), Vcc and GND for the hall sensors, and two encoder channels A and B.
- Typical spec sheet values (varies by gear ratio variant):
Encoder CPR at motor shaft: 11
Gear ratio (example): 1:30 -> output shaft CPR = 11 * 30 * 4 = 1320 edges (multiply by 4 because we count both edges on both channels)
This class is a thin Python wrapper over the C Servo type in
_openbricks_native (see native/user_c_modules/openbricks/servo.c).
The wrapper’s job is to build the hardware handles (Pin / PWM / encoder)
from pin numbers and pass them to the native servo — the closed-loop
control tick runs in C at the motor_process tick rate (1 kHz default).
- class openbricks.drivers.jgb37_520.JGB37Motor(in1, in2, pwm, encoder_a, encoder_b, counts_per_output_rev=1320, invert=False, encoder_invert=False, kp=_DEFAULT_KP)[source]
Bases:
Motor- run_angle(deg_per_s, target_angle, wait=True, accel_dps2=720.0)[source]
Rotate by
target_angledegrees, following a trapezoidal profile at up todeg_per_s.The native servo builds the profile once and the scheduler samples it at 1 kHz, so acceleration / cruise / deceleration phases are smooth and the move stops cleanly at the target without overshoot (apart from small integration error).
With
wait=Falsethe scheduler keeps running the profile; the caller can pollself._servo.is_done()or eventually callbrake()/coast().
MG370
MG370 geared DC motor with GMR (giant magnetoresistance) quadrature encoder.
The GMR variant has a 500-PPR encoder on the motor shaft, giving massively
more resolution than a standard Hall-effect encoder — at a 1:34 gearbox
the output shaft sees 500 * 4 * 34.014 ≈ 68028 CPR. That edge rate is
too fast for the software QuadratureEncoder (Pin.irq() tops out around
5-10 kHz), so this driver uses the native PCNTEncoder — a C wrapper
over the ESP32 PCNT peripheral — baked into the firmware image.
Every MG370Motor instance needs its own PCNT unit — ESP32 has 8, ESP32-S3 has 4. Pick unit=0 for the first motor, unit=1 for the second, etc.
Apart from the encoder layer, MG370Motor is identical to JGB37Motor:
same native Servo underneath, same control tick, same closed-loop API.
H-bridge drivers (open loop)
L298N
L298N H-bridge motor driver.
The L298N drives a brushed DC motor via two direction pins (IN1/IN2) and one
PWM pin (EN-A or EN-B). It’s open-loop: the class knows nothing about the
physical motor speed or position. For closed-loop use, wrap one of these in
jgb37_520.JGB37Motor (which adds an encoder).
Pinout recap for one channel:
IN1 = direction bit A
IN2 = direction bit B
PWM = enable / speed (tie to VCC for 100%, PWM for speed control)
IN1 IN2 result
--- --- ----------
0 0 coast
0 1 reverse
1 0 forward
1 1 brake
TB6612FNG
TB6612FNG dual MOSFET H-bridge — re-exposes L298NMotor under the
TB6612 name.
Both chips drive each motor channel with the same three signals:
IN1 — direction bit 1 IN2 — direction bit 2 PWM — speed (PWM duty cycle)
…so the openbricks driver is identical. TB6612 is generally the better commodity choice — MOSFETs instead of Darlingtons, ~0.3 V drop instead of ~1.8 V, 3.3 V-logic compatible directly from an ESP32 GPIO, and 3.2 A peak per channel vs L298N’s 2 A.
Wiring difference to be aware of: TB6612 has a chip-level STBY pin that must be held high for either channel to operate. Tie it to 3.3 V on your breakout, or drive a spare GPIO high at boot — openbricks doesn’t model STBY because most breakout modules already pull it up.