A pure-Rust, no_std-compatible driver library for ROBOTIS DYNAMIXEL Protocol 2.0
English · 简体中文
dynamixel-rs is a zero-dependency Rust implementation of the
ROBOTIS DYNAMIXEL Protocol 2.0. It provides a type-safe,
allocation-free API for building and parsing instruction and status packets,
plus a thin Serial trait abstraction that bridges the protocol layer to any
transport — std::io, embedded-hal, RS-485 transceivers, or custom UART
drivers on bare metal.
The crate ships with std enabled by default for host-side convenience
(serial-port adapters, std::error::Error integration), and can be switched
to #![no_std] by disabling default features — see no_std Usage.
Protocol 2.0 is the successor to DYNAMIXEL's original Protocol 1.0 and is used by every current X / P / Y / PRO / PRO+ series servo. The wire format features a 4-byte magic header, little-endian length/CRC fields, CRC-16/BUYPASS integrity checking, and byte-stuffing to prevent header collisions in the payload.
- ✅ Pure Rust,
no_std-compatible —stdon by default for convenience; disable default features for Cortex-M / RISC-V / ESP32 / any bare-metal target - ✅ Zero-copy parsing —
Packet<'a>borrows the byte slice; no heap, no copies - ✅ All 16 Protocol 2.0 instructions exposed as ergonomic Builder types
- ✅ Automatic byte-stuffing and CRC-16/BUYPASS on both TX and RX paths
- ✅
Serialtrait abstraction — wire the protocol to anyread_exact/write_alltransport - ✅
StdSerial<T>adapter wraps anystd::io::Read + Write(e.g.serialport,TcpStream) - ✅ High-level
DriverAPI —ping(),read_bytes(),write_bytes()with built-in framing - ✅ Strongly-typed errors —
Error::DeviceError(StatusError)carries the bit-packed status-packet error byte - ✅ 72+ unit tests covering official protocol byte sequences, CRC edge cases, and byte-stuffing round-trips
All 16 instructions from the Protocol 2.0 specification are available as
builder types under dynamixel_rs::commands.
| Byte | Instruction | Builder | Description |
|---|---|---|---|
0x01 |
Ping | unicast::Ping |
Check device presence, get model + firmware |
0x02 |
Read | unicast::Read |
Read N bytes from the Control Table |
0x03 |
Write | unicast::Write |
Write data to the Control Table |
0x04 |
Reg Write | unicast::RegWrite |
Register a Write for later synchronised execution |
0x05 |
Action | unicast::Action |
Trigger pending Reg Writes (unicast or broadcast) |
0x06 |
Factory Reset | unicast::FactoryReset |
Restore factory defaults |
0x08 |
Reboot | unicast::Reboot |
Reboot the target device |
0x10 |
Clear | unicast::Clear |
Reset multi-turn position or clear hardware error |
0x20 |
Control Table Backup | unicast::ControlTableBackup |
Store or restore the Control Table |
0x55 |
Status (Return) | packet::build_status_packet |
Status packet sent by devices (builder for testing) |
0x82 |
Sync Read | multi::SyncRead |
Read the same address from multiple devices |
0x83 |
Sync Write | multi::SyncWrite |
Write the same address to multiple devices |
0x8A |
Fast Sync Read | multi::FastSyncRead |
Sync Read with combined single-response format |
0x92 |
Bulk Read | multi::BulkRead |
Read different addresses from multiple devices |
0x93 |
Bulk Write | multi::BulkWrite |
Write different addresses to multiple devices |
0x9A |
Fast Bulk Read | multi::FastBulkRead |
Bulk Read with combined single-response format |
| Series | Model | Model Number | Firmware | Status |
|---|---|---|---|---|
| X-Series | XL330-M288-T | 1200 | 52 | ✅ Verified — 8-servo chain at 2 Mbps |
Any Protocol 2.0-compatible device should work. Contributions adding to this
table via the examples in examples/ are welcome.
Add to your Cargo.toml:
[dependencies]
dynamixel-rs = "0.1"| Feature | Default | Description |
|---|---|---|
std |
✅ | Enables StdSerial<T> adapter and std::error::Error impl |
embedded-hal |
❌ | (Planned) Integration with the embedded-hal 1.x serial traits |
For a no_std build:
[dependencies]
dynamixel-rs = { version = "0.1", default-features = false }The crate compiles on Rust 1.70.0 and above. MSRV bumps will be documented in the changelog and are considered non-breaking.
Open a serial port, ping servo ID 1, and read its Present Position. Runs on any macOS / Linux / Windows host with a USB ↔ RS-485 adapter.
use dynamixel_rs::driver::{Driver, StdSerial};
use serialport::{DataBits, FlowControl, Parity, StopBits};
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Open the serial port (DYNAMIXEL default: 2 Mbps, 8N1, no flow control).
let port = serialport::new("/dev/ttyUSB0", 2_000_000)
.data_bits(DataBits::Eight)
.parity(Parity::None)
.stop_bits(StopBits::One)
.flow_control(FlowControl::None)
.timeout(Duration::from_millis(100))
.open()?;
// 2. Wrap it in the high-level Driver.
let mut driver = Driver::new(StdSerial::new(port));
// 3. Ping — returns model number + firmware version.
let mut tx = [0u8; 32];
let mut rx = [0u8; 256];
let info = driver.ping(1, &mut tx, &mut rx)?;
println!("ID=1 model={} firmware={}", info.model_number, info.firmware_version);
// 4. Read 4 bytes at address 132 (Present Position on X-series).
let data = driver.read_bytes(1, 132, 4, &mut tx, &mut rx)?;
let position = i32::from_le_bytes([data[0], data[1], data[2], data[3]]);
println!("Present Position = {} ticks", position);
Ok(())
}Every builder follows the same pattern: construct with new(...), then call
build(&mut [u8]) -> Result<usize, Error> to serialise into a caller-supplied
buffer. No heap allocation is ever performed.
use dynamixel_rs::commands::unicast::{Ping, Read, Write};
let mut buf = [0u8; 64];
// Ping to device 1 → FF FF FD 00 01 03 00 01 19 4E
let n = Ping::new(1).build(&mut buf)?;
// Read 4 bytes at address 0x0084 on device 1
let n = Read::new(1, 0x0084, 4).build(&mut buf)?;
// Write [0x01, 0x02, 0x03, 0x04] at address 0x0074 on device 1
let n = Write::new(1, 0x0074, &[0x01, 0x02, 0x03, 0x04]).build(&mut buf)?;use dynamixel_rs::packet::Packet;
let pkt = Packet::parse(&rx_buf[..n])?;
println!("from ID {}", pkt.id());
if let Some(err) = pkt.status_error() {
eprintln!("device reported error: {err:?}");
}
let params: heapless::Vec<u8, 64> = pkt.params_iter().collect();The params_iter() method yields de-stuffed payload bytes lazily, so the
caller never needs to pre-allocate a buffer for unstuffing.
The Driver<S: Serial> wrapper handles protocol framing, send/receive, and
CRC validation so application code stays minimal:
driver.ping(id, &mut tx, &mut rx)?; // → PingResponse
driver.read_bytes(id, addr, len, &mut tx, &mut rx)?; // → &[u8] into rx
driver.write_bytes(id, addr, data, &mut tx, &mut rx)?;SyncRead, SyncWrite, BulkRead, BulkWrite, and their Fast variants
accept an iterator of per-device entries and stream the bytes directly into
the caller's buffer — still zero heap allocations.
use dynamixel_rs::commands::multi::{SyncWrite, BulkRead};
// SyncWrite: set Goal Position (addr 116, 4 bytes) on servos 1, 2, 3.
let entries = [
(1u8, 1000_u32.to_le_bytes()),
(2u8, 2000_u32.to_le_bytes()),
(3u8, 3000_u32.to_le_bytes()),
];
let n = SyncWrite::new(116, 4, entries.iter().map(|(id, d)| (*id, d.as_ref())))
.build(&mut buf)?;
// BulkRead: pull different data from different servos in one packet.
let plan = [
(1u8, 132_u16, 4_u16), // Present Position (4 B) from ID 1
(2u8, 146_u16, 1_u16), // Present Temperature (1 B) from ID 2
];
let n = BulkRead::new(plan).build(&mut buf)?;Runnable examples live in examples/:
| Example | Command | Purpose |
|---|---|---|
ping_motor |
cargo run --example ping_motor |
In-memory loopback demo — no hardware required |
ping_motor (real) |
cargo run --example ping_motor -- /dev/ttyUSB0 |
Ping ID 1 on a real port |
read_all_servos |
cargo run --example read_all_servos |
Scan IDs 1–8 and print model/position/voltage/temperature |
monitor_servo |
cargo run --example monitor_servo -- --port /dev/ttyUSB0 --ids 1,2,3 |
Real-time monitoring loop with clap argument parsing |
test_port |
cargo run --example test_port |
Low-level byte-level protocol verification |
Enable tracing with RUST_LOG=debug to see TX / RX packet dumps:
RUST_LOG=debug cargo run --example read_all_servos┌─────────────────────────────────────────────────────────────┐
│ User application │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ driver.rs Driver<S: Serial> │
│ ├─ ping() │
│ ├─ read_bytes() │
│ ├─ write_bytes() │
│ ├─ send_packet() / recv_packet() │
│ └─ StdSerial<T: std::io::Read+Write> │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ commands/unicast.rs │ │ commands/multi.rs │
│ Ping / Read / Write / │ │ SyncRead / SyncWrite / │
│ RegWrite / Action / │ │ BulkRead / BulkWrite / │
│ FactoryReset / Reboot /│ │ FastSyncRead / │
│ Clear / CtrlTblBackup │ │ FastBulkRead │
└─────────────────────────┘ └─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ packet.rs Packet<'a>::parse(&[u8]) → zero-copy view │
│ build_packet() / build_status_packet() │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ crc.rs │ │ stuffing.rs │
│ CRC-16/BUYPASS │ │ byte-stuff / UnstuffIter │
└─────────────────────────┘ └─────────────────────────────┘
offset │ 0 1 2 3 │ 4 │ 5 6 │ 7 │ 8..N-3 │ N-2 N-1
field │ Header │ ID │ Length │ Instr│ Parameters │ CRC
bytes │ FF FF FD 00 │ XX │ ll lh │ XX │ …stuffed… │ cl ch
Length = bytes from Instruction through CRC (inclusive), little-endian
CRC = CRC-16/BUYPASS over bytes 0..N-2 (everything before CRC)
offset │ 0 1 2 3 │ 4 │ 5 6 │ 7 │ 8 │ 9..N-3 │ N-2 N-1
field │ Header │ ID │ Length │ 0x55 │ Error │ Parameters │ CRC
bytes │ FF FF FD 00 │ XX │ ll lh │ 55 │ EE │ …stuffed… │ cl ch
Error bit 7 = Alert (Hardware Error Status non-zero)
Error bits 6..0 = Error Number (see error::StatusError)
Byte-stuffing rule: in the payload, whenever the sequence FF FF FD appears
it is replaced by FF FF FD FD on the wire.
std is enabled by default; the crate itself is #![no_std]-compatible once
default features are disabled. After switching, implement the Serial trait
on top of your platform's UART driver:
#![no_std]
use dynamixel_rs::{driver::{Driver, Serial}, error::Error};
struct MyUart;
impl Serial for MyUart {
fn write_all(&mut self, bytes: &[u8]) -> Result<(), Error> {
// Block until every byte has been transmitted.
todo!()
}
fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> {
// Block until the buffer is full.
todo!()
}
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
// Read up to buf.len() bytes, return how many.
todo!()
}
}
let mut driver = Driver::new(MyUart);A blanket embedded-hal 1.x serial integration is on the Roadmap.
# Full test suite (72+ unit tests + doc tests)
cargo test
# no_std build verification
cargo build --no-default-features
# Documentation (opens in browser)
cargo doc --open-
embedded-hal1.x serial trait blanket implementation - Async I/O support via
embedded-io-async - Hardware validation across additional series (2XL, P, Y, PRO)
- Typed Control Table abstractions per model
-
defmtlogging feature flag for embedded targets - Python bindings via PyO3 (optional crate)
Contributions are welcome. Before opening a PR:
- Run
cargo fmtandcargo clippy --all-targets. - Ensure
cargo testandcargo build --no-default-featuresboth pass. - For new instructions or hardware support, add a focused unit test with the official byte sequence from the Protocol 2.0 reference.
- Bilingual doc comments (English + 中文) are appreciated but not required for third-party contributions.
Bug reports and feature requests should include the target hardware model, firmware version, and a minimal reproducer.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.
- ROBOTIS e-Manual — DYNAMIXEL Protocol 2.0
- ROBOTIS e-Manual — Control Tables by model
- CRC-16/BUYPASS specification
- Upstream C++ SDK: ROBOTIS-GIT/DynamixelSDK — used as a cross-reference for byte-level framing