Skip to content

API guide

How to do the common jobs with the driver: open the bus, find and identify servos, move them, read them, talk to several at once and handle errors.

Every snippet on this page is compiled by the host CI from tests/docs/api_tasks.cpp, with MyUart the transport from Porting and DriverError standing for dynamixel::DriverError. The API reference lists every member.

The shape of the API

Class Owns
Bus One serial line: open, baud rate, ping, scan, register reads and writes by ID, sync read and write
Device One servo on a bus: identify, torque, operating mode, motion profile, goals, telemetry
ModelDescriptor A servo's control-table addresses and capabilities; the catalog is in dynamixel_model.hpp
Transport<Derived>, TransportView Your UART class and the view the bus holds of it

Every call is noexcept and returns DriverResult<T>: value, error and detail (the SDK result code, the servo's error byte, byte counts and a timestamp). It converts to bool: if (!servo.ReadPosition()) means it failed. ToString(error) names the error.

Open the bus

dynamixel::TransportConfig cfg{};
cfg.baud_rate = 57600;                               // must match the servo's Baud Rate (address 8)
cfg.echo_policy = dynamixel::EchoPolicy::None;       // ExpectedLocalEcho on a single-wire link
cfg.latency_ms = 2;                                  // slack added to every reply timeout
const auto opened = bus.Open(cfg);
if (!opened) {
  std::printf("open: %s\n", dynamixel::ToString(opened.error).data());
  return false;
}

Create the bus with braces, dynamixel::Bus bus{dynamixel::TransportView{uart}};, and keep uart alive as long as the bus. Open() calls your transport's open(); it sends nothing. Configuration lists every TransportConfig field.

Find the servos

uint8_t ids[16];
const auto found = bus.Scan(ids, 16, 0, 20);          // ping IDs 0..20, keep up to 16 answers
for (uint8_t i = 0; found && i < found.value; ++i) {
  const auto ping = bus.Ping(ids[i]);
  if (ping) {
    std::printf("id %u model %u fw %u\n", ids[i], ping.value.model_number,
                ping.value.firmware_version);
  }
}

Scan() pings each ID in turn and costs one reply timeout per absent ID, so keep the range short. Any device ID from 0 to 252 is valid (Protocol 2.0, section 2.3); the default range is all of them. ID 253 is not a device ID and 254 is the broadcast ID, so Scan(), Ping(), ReadRegister() and the sync calls reject them.

Identify a servo

const auto ping = servo.Identify();                   // ping, catalog lookup, read Operating Mode
if (ping.error == DriverError::UnsupportedRequest) {
  // The servo answered; ping.value.model_number is not in kKnownModels.
  servo.BindXSeriesFallback();                        // shared X-series addresses
} else if (!ping) {
  return false;
}
std::printf("%s\n", servo.model()->name);

Identify() binds the servo's ModelDescriptor and caches its operating mode. Until a model is bound, the typed calls below return UnsupportedRequest. Model support explains the catalog and the fallback.

Move to a position

// Operating Mode is in EEPROM: torque must be off to change it.
if (!servo.SetTorqueEnabled(false) ||
    !servo.SetOperatingMode(dynamixel::OperatingMode::Position) ||
    !servo.SetMotionProfile(20, 50)) {                 // Profile Acceleration, Profile Velocity
  return false;
}

const auto here = servo.ReadPosition();               // never command a goal you did not read near
if (!here || !servo.SetTorqueEnabled(true)) {
  return false;
}
return servo.SetGoalPosition(here.value + 64).ok();   // 64 ticks = 5.6 degrees

Operating Mode is an EEPROM register: SetOperatingMode() reads Torque Enable first and returns Busy if torque is on. It never turns torque off for you. A goal for a mode the servo is not in returns UnsupportedRequest without sending anything.

OperatingMode Goal call Unit (XC430-W150)
Position (3, factory): 0 to 360° SetGoalPosition() 1 pulse, 4096 per turn
ExtendedPosition (4): multi-turn SetGoalPosition() 1 pulse
Velocity (1) SetGoalVelocity() 0.229 rev/min
Pwm (16): voltage SetGoalPwm() 0.113 %

SetMotionProfile(acceleration, velocity) writes Profile Acceleration (address 108) and Profile Velocity (112), which shape every position move; 0 means unlimited.

Spin at a velocity

if (!servo.SetTorqueEnabled(false) ||
    !servo.SetOperatingMode(dynamixel::OperatingMode::Velocity) ||
    !servo.SetTorqueEnabled(true)) {
  return false;
}
return servo.SetGoalVelocity(44).ok();                // 44 x 0.229 rev/min = about 10 rev/min

Read state

const auto tel = servo.ReadTelemetry();
if (!tel) {
  return;                                             // the position read failed
}
const auto& t = tel.value;
std::printf("pos %ld vel %ld pwm %d %u.%u V %u C hw 0x%02x\n", static_cast<long>(t.position),
            static_cast<long>(t.velocity), t.pwm, t.voltage_0p1_v / 10U, t.voltage_0p1_v % 10U,
            t.temperature_c, t.hardware_error);
if (servo.model()->effort_is_current) {
  std::printf("current %d x 2.69 mA\n", t.load);      // XM / XH: Present Current
} else {
  std::printf("load %d x 0.1 %%\n", t.load);           // XC / XL: Present Load
}

ReadTelemetry() makes two reads: one of the contiguous Present block, Present PWM (124) to Present Temperature (146), 23 bytes, and one of Hardware Error Status (70). It fails when the block read fails; hardware_error stays 0 when its own read fails, and detail is the block read's. When the block reply would not fit max_packet_bytes or the servo refuses the range with an error number, it falls back to one read per register, which fails only when the position read fails. For one value, ReadPosition() is a single read. For several servos at once, use a sync read.

Read or write several servos at once

const uint8_t ids[2] = {1, 2};
uint8_t pos1[4] = {}, pos2[4] = {};
uint8_t* const out[2] = {pos1, pos2};
dynamixel::SyncReadItem items[2];
const auto got = bus.SyncRead(132, 4, ids, 2, out, items);  // Present Position, both servos
if (!got || got.value != 2) {
  return false;                                       // items[i].valid says which one is missing
}

const uint8_t goal1[4] = {0x00, 0x08, 0, 0};          // 2048, little-endian
const uint8_t goal2[4] = {0x00, 0x04, 0, 0};          // 1024
const uint8_t* const in[2] = {goal1, goal2};
return bus.SyncWrite(116, 4, ids, in, 2).ok();        // Goal Position, one packet, no replies

A sync write is one broadcast packet and no replies. A sync read is one request and one status packet per servo; SyncRead() collects each reply on its own, so a missing servo marks only its own SyncReadItem invalid and the return value is the number of valid replies. Both take one address and length for all servos, little-endian payloads.

Use any register directly

uint8_t delay = 0;                                    // Return Delay Time, address 9, 2 us per unit
if (!servo.ReadRegister(9, &delay, 1)) {
  return false;
}
const uint8_t shorter = 0;                            // EEPROM: torque must be off
return servo.WriteRegister(9, &shorter, 1, dynamixel::WritePolicy::ReadbackVerify).ok();

ReadRegister() and WriteRegister() work with or without a bound model, on the Device or on the Bus by ID. The write policy says how sure you want to be:

WritePolicy Sends Returns success when
TxOnly Write, no reply expected The packet left the host (the only policy allowed for the broadcast ID)
TxRx (default) Write, waits for the status packet The servo acknowledged it without an error
ReadbackVerify Write, then a read of the same bytes The bytes read back match (up to 64 bytes)

Change the baud rate

const uint8_t index_1m = 3;                           // Baud Rate (address 8): 3 = 1 Mbit/s
const auto wr = servo.WriteRegister(8, &index_1m, 1); // EEPROM: torque must be off
if (!wr) {
  return false;
}
return bus.SetHostBaudRate(1000000).ok();             // follow it on the host side

Baud Rate (address 8) takes an index, 0 to 7 (Hardware). Once the servo runs at the new rate, SetHostBaudRate() moves the host. Every servo on the bus has to follow, or the host loses the ones left behind.

Handle errors

const auto r = servo.SetGoalPosition(2048);
switch (r.error) {
  case DriverError::None:
    break;
  case DriverError::Timeout:                          // no status packet: wiring, baud, ID, power
  case DriverError::ChecksumError:                    // corrupted reply: cable, termination, baud
    std::printf("link: %s\n", dynamixel::ToString(r.error).data());
    break;
  case DriverError::UncertainOutcome:                 // write sent, no reply: it may have applied
    std::printf("re-read before retrying\n");
    break;
  case DriverError::DeviceError:                      // the servo answered with an error byte
    std::printf("device error 0x%02x\n", r.detail.device_error);
    break;
  default:                                            // UnsupportedRequest: wrong mode or no model
    std::printf("%s (comm %ld)\n", dynamixel::ToString(r.error).data(),
                static_cast<long>(r.detail.comm_code));
    break;
}
DriverError Meaning What to do
Timeout No status packet in time Check ID, baud, wiring, power
ChecksumError, MalformedResponse A reply arrived, damaged Cable, ground, baud too high for the circuit
EchoMismatch Local echo expected and not heard, or wrong Match EchoPolicy to the wiring
UncertainOutcome A write was sent and its reply lost Read the register back before retrying
DeviceError The servo answered with an error byte detail.device_error: bit 7 hardware alert, bits 0 to 6 the error number
UnsupportedRequest No model bound, or wrong mode for the call Identify() or bind a model; change mode
Busy Torque on during a mode change, or the bus is mid-transaction Torque off first
BufferTooSmall The request's packet is longer than max_packet_bytes; nothing was sent Split it, or raise the limit (Configuration)
NotInitialized, InvalidParameter, InvalidId Bus not open, a null buffer or zero length, ID above 252 Fix the call

When bit 7 of the error byte is set, read Hardware Error Status (address 70, also in Telemetry::hardware_error): bit 0 input voltage, bit 2 overheating, bit 3 motor encoder, bit 4 electrical shock, bit 5 overload. On the faults enabled in Shutdown (address 63) the servo clears Torque Enable, and only a reboot turns it back on: a power cycle, or the Reboot instruction. Troubleshooting maps symptoms to causes.

Recover from a hardware error

uint8_t hw = 0;                                       // Hardware Error Status, address 70
servo.ReadRegister(70, &hw, 1);                       // DeviceError with the Alert bit still fills hw
if (hw == 0) {
  return true;
}
std::printf("hardware error 0x%02x, rebooting\n", hw); // fix the cause first (supply, load, heat)
const auto rb = servo.Reboot();                       // Reboot instruction (0x08)
if (!rb && rb.error != DriverError::UncertainOutcome) {
  return false;                                       // UncertainOutcome: it may be rebooting already
}
for (int attempt = 0; attempt < 20; ++attempt) {      // the servo answers again once it has booted
  if (bus.Ping(servo.id())) {
    return true;                                      // torque is off; RAM is back to its defaults
  }
}
return false;

Reboot() (on the Device, or Bus::Reboot(id)) sends the Reboot instruction, 0x08 (Protocol 2.0, section 5.7). The servo answers before it restarts; the Alert bit in that reply is the fault being cleared, so it does not count as an error, and detail.device_error keeps it. A non-zero error number is DeviceError; no reply is UncertainOutcome, because the servo may have rebooted anyway. After a reboot the RAM area is back to its defaults: torque is off and Hardware Error Status is 0. The broadcast ID reboots every servo and gets no reply. Fix the cause of the fault first, or it latches again.