Quickstart¶
Read a DYNAMIXEL servo from your MCU: wire three signals, write a small transport class, call four driver functions.
Before you power the servo
Feed the servo from its own supply (12 V for an XC430-W150), never from the MCU's rail, and join the grounds. The example below never enables torque; keep it that way until reads are clean.
1. Wire it¶
| MCU | Servo (TTL, 3-pin) | Notes |
|---|---|---|
| UART TX and RX | Pin 3, DATA | Through a half-duplex circuit (Hardware) |
| GND | Pin 1, GND | Common with the servo supply ground |
| Pin 2, VDD | Servo supply only, not the MCU rail |
Start at 57600 bit/s, 8N1, with one servo on a short cable: that is the factory setting (ID 1). The Hardware page has the RS-485 pinout, the half-duplex options and the supply ranges.
2. Add the driver¶
Add the repository as a submodule, with its own submodule (the ROBOTIS SDK), and link the hf::dynamixel CMake
target. Installation covers CMake and ESP-IDF.
git submodule add https://github.com/hardfoc/hf-dynamixel-driver.git external/hf-dynamixel-driver
git submodule update --init --recursive external/hf-dynamixel-driver
3. Write the transport¶
The transport is the only platform code. It writes and reads UART bytes, waits for the end of transmission, flips
the bus direction if your circuit has a direction pin, and reads a microsecond clock. Replace the platform_*
calls with your SDK's.
class MyUart : public dynamixel::Transport<MyUart> {
public:
// Configure the UART: 8 data bits, no parity, 1 stop bit at cfg.baud_rate. No servo traffic.
dynamixel::DriverError open(const dynamixel::TransportConfig& cfg) noexcept {
platform_dir_set(false); // listen
return platform_uart_set_baud(cfg.baud_rate) ? dynamixel::DriverError::None
: dynamixel::DriverError::SerialError;
}
void close() noexcept {}
dynamixel::DriverError set_baud_rate(uint32_t baud) noexcept {
return platform_uart_set_baud(baud) ? dynamixel::DriverError::None
: dynamixel::DriverError::SerialError;
}
// Queue bytes for transmission; report how many were accepted (a partial write is fine).
dynamixel::TransportIo write_some(const uint8_t* data, std::size_t length) noexcept {
platform_dir_set(true); // drive the bus
const int n = platform_uart_write(data, length);
if (n < 0) {
return {0, dynamixel::DriverError::SerialError};
}
return {static_cast<std::size_t>(n), dynamixel::DriverError::None};
}
// Block until the last stop bit has left the pin, then release the bus for the reply.
dynamixel::DriverError finish_transmit() noexcept {
const bool done = platform_uart_wait_tx_done();
platform_dir_set(false);
return done ? dynamixel::DriverError::None : dynamixel::DriverError::SerialError;
}
// Return what has arrived so far; 0 bytes is not an error.
dynamixel::TransportIo read_some(uint8_t* out, std::size_t max) noexcept {
const int n = platform_uart_read(out, max);
if (n < 0) {
return {0, dynamixel::DriverError::SerialError};
}
return {static_cast<std::size_t>(n), dynamixel::DriverError::None};
}
void discard_stale_input() noexcept { platform_uart_flush_rx(); }
uint64_t now_us() noexcept { return platform_micros(); }
// Optional: lets the driver sleep instead of spinning while it waits.
void delay_ms_impl(uint32_t ms) noexcept { platform_delay_ms(ms); }
};
Porting explains each method and the three half-duplex wirings.
4. Read the servo¶
#include "dynamixel.hpp"
int read_servo_1(MyUart& uart) {
dynamixel::Bus bus{dynamixel::TransportView{uart}}; // braces: see "most vexing parse"
dynamixel::TransportConfig cfg{}; // 1. 57600 bit/s, 8N1: factory default
if (!bus.Open(cfg)) {
return -1; // your transport's open() failed
}
dynamixel::Device servo(bus, 1); // 2. factory ID is 1
const auto ping = servo.Identify(); // ping + catalog lookup
if (ping.error == dynamixel::DriverError::UnsupportedRequest) {
servo.BindXSeriesFallback(); // answered, model not in the catalog
} else if (!ping) {
return -2; // Timeout: wiring, power, baud, ID
}
const auto tel = servo.ReadTelemetry(); // 3. torque stays off
if (!tel) {
return -3;
}
std::printf("position %ld ticks, %u.%u V\n", static_cast<long>(tel.value.position),
tel.value.voltage_0p1_v / 10U, tel.value.voltage_0p1_v % 10U);
return 0;
}
What each step does:
Bus::Open()calls your transport'sopen()with the configuration. A defaultTransportConfigis 57600 bit/s with no local echo. Nothing is sent yet.Device::Identify()pings ID 1, looks the model number up in the catalog and reads the operating mode. An X-series model the catalog does not list answers withUnsupportedRequest;BindXSeriesFallback()then attaches the shared X-series control table.ReadTelemetry()reads position, velocity, load, PWM, input voltage, temperature and the hardware error byte. It writes nothing.
Write dynamixel::Bus bus{dynamixel::TransportView{uart}}; with braces. With parentheses,
Bus bus(TransportView(uart)); declares a function, not a bus.
This example is built by the host CI, so it compiles against the current headers.
If it does not work¶
| Symptom | First thing to check |
|---|---|
Identify() returns Timeout |
Baud rate and ID, the half-duplex circuit, a common ground, servo power |
EchoMismatch |
Your RX hears your TX: set EchoPolicy::ExpectedLocalEcho (Configuration) |
ChecksumError now and then |
Cable length, baud too high for the circuit, ground noise |
UnsupportedRequest |
Model not in the catalog: bind the fallback (Model support) |
Next¶
- API guide: change the mode, move, sync read and write, handle errors.
- Examples: ESP32-C6 and ESP32-S3 applications for read-only, scan and motion.
- Model support: the catalog and how to add a servo.