← WRITING·2026-03-12·11 minRobotics

Building a working ST3215 driver from scratch

The ST3215 is a smart servo motor — half stepper, half sensor, full headache to talk to. When I needed twelve of them for my quadruped, I looked for a Python library. I found three. None of them worked reliably.

So I wrote one.

The protocol

ST3215 servos communicate over a half-duplex UART bus. One wire, both directions, with explicit direction switching. The packet format is simple: header, ID, length, instruction, parameters, checksum.

The catch: the half-duplex switch timing. Send too fast and you're writing while the servo is still replying. Send too slow and you've wasted latency on a real-time control loop.

class ST3215:
    HEADER = bytes([0xFF, 0xFF])
    
    def _build_packet(self, servo_id: int, instruction: int, params: bytes) -> bytes:
        length = len(params) + 2
        checksum = (~(servo_id + length + instruction + sum(params))) & 0xFF
        return self.HEADER + bytes([servo_id, length, instruction]) + params + bytes([checksum])

What the existing libraries got wrong

Library A hard-coded a 10ms sleep after every direction switch. That's 10ms per command × 12 servos × 50Hz control loop = you're not hitting 50Hz.

Library B didn't handle the half-duplex switch at all — it opened two serial ports and hoped for the best. This works exactly once on exactly one hardware setup.

Library C was a thin wrapper around a C extension that required compiling against a specific kernel version. No.

The solution: measured timing

I measured the actual turnaround time with a logic analyzer. For the ST3215 at 1Mbaud, the safe minimum between write-end and read-start is about 120µs. I expose this as a configurable parameter with a sane default.

Async support

The v1.2 release added async/await. Running concurrent position commands for all twelve servos went from 12ms serial to ~2ms parallel.

async def set_positions(self, commands: list[tuple[int, int]]) -> None:
    await asyncio.gather(*[
        self.set_position(servo_id, pos)
        for servo_id, pos in commands
    ])

The library is on PyPI as st3215-servo. MIT licensed. If it saves you six hours of UART debugging, consider it a gift.