Getting Dynamixel Servos Working with the Pimoroni Yukon Serial Servo Module

The Short Answer

The Pimoroni Yukon’s Serial Servo Module speaks the Hiwonder/Lewansoul LX protocol — not Dynamixel. The two are completely different serial protocols at nearly every layer, and the Yukon MicroPython library ships with no Dynamixel support at all. The hardware can work; the protocol layer is what’s missing.

What the ‘ID: None’ Error Is Actually Telling You

When you run the Servo Detect example, the code broadcasts LX-format scan packets and listens for LX-format replies. Your Dynamixel is almost certainly responding. It hears bus activity and sends back a well-formed Protocol 1.0 status packet. The problem is that the LX parser reads the response bytes at offsets that mean something specific in the LX packet structure — and those same byte positions carry completely different data in a Dynamixel response. So the parsed ID comes back as None.

You’re not failing to reach the servo. You’re reaching it, reading its reply, and then misinterpreting every byte. That distinction matters when you start debugging.

LX vs Dynamixel: What the Packet Structures Actually Look Like

The LX-16A (Hiwonder/Lewansoul) runs at 115,200 baud and opens every packet with 0x55 0x55, followed by servo ID, data length, command byte, parameters, and a checksum. Compact and straightforward.

Dynamixel Protocol 1.0 — used by the AX series — is different at every level:

  • Header is 0xFF 0xFF
  • Default baud rate on a factory AX servo is 1,000,000 bps (1 Mbps), not 115,200
  • Packet order: header, ID, length, instruction or error byte, parameters, checksum
  • Checksum calculated as (~(ID + Length + Instruction + sum(params))) & 0xFF

Protocol 2.0 — used by newer XL-series models like the XL-330 and XL-430 — changes things again. Three-byte header (0xFF 0xFF 0xFD), a reserved byte, 16-bit length field, and a CRC-16 instead of the simple checksum. Before writing any code, check the Robotis e-Manual for your specific model. XL-320 uses Protocol 2.0; AX-12A uses Protocol 1.0. Don’t assume by series name.

The ‘Dynamixel Compatible’ Label on LX Servos

Some LX-16A listings describe the servo as Dynamixel compatible. This does not mean they share a serial protocol. The claim refers to physical compatibility — similar horn dimensions, similar connector style, roughly the same form factor. The LX communication protocol is entirely proprietary to Hiwonder. Confusing physical compatibility with protocol compatibility is the single most common trap in threads about these servos.

How the Yukon Module Handles Half-Duplex

Both LX and Dynamixel TTL servos use half-duplex UART: TX and RX share one wire, and the controller has to switch between transmit and receive modes. The Yukon Serial Servo Module handles this with a direction-control GPIO on the RP2040. In MicroPython, SerialServoModule exposes a uart object and a duplexer object for exactly this.

So the hardware side is fine. You have UART access and direction control. What’s missing is the protocol layer sitting above it.

Writing a Minimal Dynamixel Protocol 1.0 Driver for the Yukon

A working AX-series driver needs to handle four things cleanly.

Baud rate. Set your UART to 1,000,000 bps. AX servos ship at this rate by default. Scanning at 115,200 returns nothing — not even a garbled reply.

Direction switching. Assert TX direction before writing. Wait until transmission is fully complete (MicroPython’s uart.txdone() or a tight timing delay based on packet length at 1 Mbps), then switch to RX before the servo has time to respond. Miss this window and you’ll either step on the servo’s reply or read your own transmitted bytes back.

Packet construction. For a PING (instruction 0x01, no parameters), the packet is seven bytes:

[0xFF, 0xFF, servo_id, 0x02, 0x01, checksum]

where checksum = (~(servo_id + 2 + 0x01)) & 0xFF.

Response parsing. Read until you see 0xFF 0xFF, then read the length byte to know how many more bytes to pull, verify the checksum, and extract the error byte and any parameters. A PING response is six bytes total. Error byte of zero means the servo is healthy.

A rough sketch in MicroPython:

# Adapt duplexer calls to match your actual Yukon API
def ping_ax(uart, duplexer, servo_id):
    checksum = (~(servo_id + 2 + 0x01)) & 0xFF
    packet = bytes([0xFF, 0xFF, servo_id, 2, 0x01, checksum])
    duplexer.send_on_tx()
    uart.write(packet)
    while not uart.txdone():
        pass
    duplexer.receive_on_rx()
    resp = uart.read(6)  # PING status packet is 6 bytes
    return resp

This is a starting skeleton. Add header-sync logic that keeps reading until it sees 0xFF 0xFF (framing errors are common on a busy bus), proper timeout handling, and checksum verification before you trust any response data.

Baud Rate Scanning When You’ve Lost Track

If a servo’s baud rate has been changed from factory default and you’re not sure what it’s on, you have to scan. Protocol 1.0 stores the baud rate in the control table at address 0x04, but you can’t read it if you can’t talk to the servo in the first place. The safe starting point for any unmodified AX-series servo is 1 Mbps. Work outward from there if PING returns nothing.

Sources


Similar Posts