The Dynamixel Servo Tester is the firmware target I reach for whenever I’m working with Dynamixel servos — usually setting up a brand-new one before it goes into a creature. It’s an interactive shell for talking to Dynamixel XC330 servos over Protocol 2.0: ping them, scan the bus, read and write registers, change IDs and baud rates, and drive them around to make sure they work.
Like the other bench tools, it presents as a USB serial device with a shell running on the board, so no host software is needed. The neat part is that it’s built on the same Dynamixel library the main controller firmware uses — so if a servo behaves under the tester, I know it’ll behave in a creature.
Hardware setup
The tester talks to servos over a single GPIO pin using a PIO-based half-duplex UART. The XC330 is 3.3 V TTL, the same as the RP2350, so no level shifter is needed.
- Data pin: GPIO 15 (configurable in
src/dynamixel_tester/config.h) - Default baud rate: 1,000,000 bps
- PIO instance: pio0 (uses 2 of 4 state machines)
There are just three connections between the board and the servo:
| RP2350 | Dynamixel |
|---|---|
| GPIO 15 | DATA |
| GND | GND |
| — | VIN (external 5 V supply) |
The servo needs its own 5 V power; the data line connects directly to the GPIO pin.
Building and flashing
cmake -B build .
cmake --build build --target dynamixel-tester-rp2350-arm-s-hw3
That produces dynamixel-tester-rp2350-arm-s-hw3.uf2. Flash it by holding BOOTSEL, plugging in the board, and copying the .uf2 onto the drive that appears.
Connecting
The tester shows up as a USB CDC serial device, so connect with any serial terminal — screen, minicom, PuTTY, whatever you like. USB CDC ignores the host baud rate, so any speed works:
screen /dev/tty.usbmodem* 115200
Type H and press Enter to see the help menu, which includes the current bus baud rate.
Commands
General
| Command | Description |
|---|---|
H |
Show help menu (includes current bus baud rate) |
I |
System info as JSON (version, free heap, uptime, data pin, baud rate) |
R |
Reboot the tester |
Servo discovery
| Command | Description |
|---|---|
P <id> |
Ping a servo. Returns model number and firmware version |
S [start] [end] |
Scan for servos in an ID range (default 0–253) |
Servo configuration
These write to EEPROM registers, so torque must be off (T <id> 0) before changing EEPROM settings.
| Command | Description |
|---|---|
ID <old> <new> |
Change a servo’s ID |
BR <id> <index> |
Set servo baud rate (see the baud rate table below) |
FR <id> <option> |
Factory reset: 0 = all, 1 = keep ID, 2 = keep ID + baud |
RB <id> |
Reboot servo |
Motion control
| Command | Description |
|---|---|
T <id> <0|1> |
Torque disable/enable (must enable before moving) |
M <id> <position> |
Move to position (0–4095, center is 2048) |
SM <id:pos> [id:pos] … |
Sync Write goal positions to multiple servos atomically (e.g. SM 1:2048 2:1024) |
V <id> <velocity> |
Set profile velocity (0 = max speed) |
L <id> <0|1> |
LED off/on |
Status and diagnostics
| Command | Description |
|---|---|
ST <id> |
Read full status: position, temperature, voltage, load, moving, hardware errors |
SS <id1> [id2] … |
Sync Read status from multiple servos in one bus transaction |
RR <id> <addr> <len> |
Read any register (len: 1, 2, or 4 bytes) |
RW <id> <addr> <len> <val> |
Write any register |
Register shortcuts
These read the register when called without a value, or write it when a value is provided.
| Command | Register | Size |
|---|---|---|
OM <id> [mode] |
Operating mode | 1 byte |
PA <id> [accel] |
Profile acceleration | 4 bytes |
HO <id> [offset] |
Homing offset | 4 bytes |
MN <id> [min] |
Min position limit | 4 bytes |
MX <id> [max] |
Max position limit | 4 bytes |
Bus configuration
| Command | Description |
|---|---|
CB <rate> |
Change the PIO bus baud rate (in bps) |
Broadcast
Any command that takes an <id> also accepts 254 as the broadcast ID, which is sent to every servo on the bus at once. Since no servo replies to a broadcast, these are transmit-only (no status back) — handy for turning all LEDs on or off, or enabling and disabling torque everywhere at once. Read commands (P, ST, RR, OM, and friends) won’t return anything useful when broadcast.
Baud rate table
Use the index value with the BR command to change a servo’s baud rate.
| Index | Baud rate |
|---|---|
| 0 | 9,600 |
| 1 | 57,600 (factory default) |
| 2 | 115,200 |
| 3 | 1,000,000 |
| 4 | 2,000,000 |
| 5 | 3,000,000 |
| 6 | 4,000,000 |
| 7 | 4,500,000 |
Index 3 (1 Mbps) is what I use in production. Indices 4–7 work but are sensitive to wire length and signal integrity.
Operating modes
Use the mode value with the OM command.
| Value | Mode | Description |
|---|---|---|
| 0 | Current | Torque (current) control only |
| 1 | Velocity | Continuous rotation |
| 3 | Position | Standard position control (0–4095, default) |
| 4 | Extended Position | Multi-turn position control |
| 5 | Current-based Position | Position control with current limit |
| 16 | PWM | Direct PWM control |
Common workflows
First-time servo setup
Factory-default servos communicate at 57,600 bps. The tester boots at 1 Mbps, so lower the bus rate first:
CB 57600 # Lower bus rate to match factory default
S # Scan to find the servo (factory default ID is 1)
P 1 # Ping to verify communication
T 1 0 # Torque off (required for EEPROM changes)
ID 1 10 # Change ID from 1 to 10
BR 10 3 # Set baud rate to 1Mbps
CB 1000000 # Raise bus back to 1Mbps
P 10 # Verify communication at new settings
Moving a servo
T 1 1 # Enable torque
V 1 50 # Set profile velocity (lower = slower)
M 1 2048 # Move to center position
M 1 0 # Move to minimum
M 1 4095 # Move to maximum
T 1 0 # Disable torque when done
Checking servo health
ST 1 # Read full status
OM 1 # Check operating mode
MN 1 # Check min position limit
MX 1 # Check max position limit
RR 1 144 2 # Read input voltage (register 144, 2 bytes)
Multi-servo sync operations
SS 1 2 3 # Read status from servos 1, 2, 3 in one bus transaction
SM 1:2048 2:1024 # Move servo 1 to 2048 and servo 2 to 1024 atomically
Sync Read (SS) reports position, temperature, voltage, load, and timing for each servo. Servos that don’t respond are listed as “no response.”
Broadcast to all servos
L 254 1 # Turn on LED on all servos
T 254 1 # Enable torque on all servos
M 254 2048 # Move all servos to center
T 254 0 # Disable torque on all servos
L 254 0 # Turn off all LEDs
Changing baud rate
The servo’s baud rate lives in EEPROM (factory default 57,600). The bus baud rate resets to 1,000,000 on every firmware boot. To bring a factory-default servo up to match:
CB 57600— temporarily lower the bus rate to talk to the servoT 1 0— torque offBR 1 3— set the servo to 1 Mbps (index 3)CB 1000000— change the bus back to 1 Mbps
If you lose communication after step 3, the servo is already at the new rate but the bus isn’t — just run CB 1000000 to reconnect.
Architecture
The tester is built on a shared Dynamixel library (src/dynamixel/) that the main controller firmware reuses:
src/dynamixel/ # Shared library
dynamixel_registers.h # XC330 control table addresses
dynamixel_protocol.h/c # Protocol 2.0 packet layer (CRC16, byte stuffing)
dynamixel_hal.h/c # PIO half-duplex UART with DMA
dynamixel_servo.h/c # High-level servo operations
src/dynamixel_tester/ # Tester application
config.h # Pin assignments, buffer sizes
main.c # Entry point
shell.h/c # Interactive command processor
usb.h/c # TinyUSB CDC setup
usb_descriptors.c # USB device identity
A few details worth knowing about the protocol layer:
- PIO UART: two state machines on the same GPIO pin handle TX and RX, with only one active at a time (half-duplex).
- DMA receive: bytes transfer from the PIO RX FIFO straight into a memory buffer, freeing the CPU during receive.
- Byte stuffing: Protocol 2.0 requires stuffing
FF FF FDsequences in packet parameters — handled transparently when building and parsing packets. - CRC16: every packet carries a CRC16 checksum that’s verified on receive.
Common register addresses
For use with the RR and RW commands. See the XC330 e-Manual for the complete control table.
| Address | Size | Name | Access |
|---|---|---|---|
| 7 | 1 | ID | RW |
| 8 | 1 | Baud Rate | RW |
| 10 | 1 | Drive Mode | RW |
| 11 | 1 | Operating Mode | RW |
| 20 | 4 | Homing Offset | RW |
| 48 | 4 | Max Position Limit | RW |
| 52 | 4 | Min Position Limit | RW |
| 64 | 1 | Torque Enable | RW |
| 65 | 1 | LED | RW |
| 80 | 2 | Position D Gain | RW |
| 82 | 2 | Position I Gain | RW |
| 84 | 2 | Position P Gain | RW |
| 108 | 4 | Profile Acceleration | RW |
| 112 | 4 | Profile Velocity | RW |
| 116 | 4 | Goal Position | RW |
| 126 | 2 | Present Load | RO |
| 128 | 4 | Present Velocity | RO |
| 132 | 4 | Present Position | RO |
| 144 | 2 | Present Input Voltage | RO |
| 146 | 1 | Present Temperature | RO |
The full command reference and more detail live in the tester docs on GitHub.