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.