Porting¶
Run the driver on a new MCU by writing one class: a UART transport derived from dynamixel::Transport<Self>.
The driver never calls your SDK directly. It calls eight methods on your class, declared in
inc/dynamixel_transport.hpp. The base class uses CRTP, so the calls are
resolved at compile time; the bus holds the class through a TransportView, a pointer and a table of function
pointers, so Bus itself is not a template.
Figure description and sources
From top to bottom. Your application talks to one Bus per serial line and one Device per servo; the public members listed are the Bus and Device methods from inc/dynamixel_bus.hpp and inc/dynamixel_device.hpp. The bus encodes and decodes Protocol 2.0 packets with the ROBOTIS DYNAMIXEL SDK, behind a private PortHandler adapter, and reaches the line through TransportView, the type-erased view of a Transport
Drawn from: inc/dynamixel_bus.hpp Bus members; inc/dynamixel_device.hpp Device members; inc/dynamixel_transport.hpp Transport hooks; figure module docs/diagrams/figures/driver-layers.py.
What you implement¶
| Method | Contract |
|---|---|
open(cfg) |
Configure the UART for cfg.baud_rate, 8N1. Send nothing. Return DriverError::None or SerialError |
close() |
Release the UART |
set_baud_rate(baud) |
Change the host rate only |
write_some(data, n) |
Queue up to n bytes; return how many were accepted. A partial write is fine: the driver calls again |
finish_transmit() |
Block until the last stop bit is on the wire, then release the line for the reply |
read_some(out, max) |
Copy the bytes that have arrived, up to max, without blocking. 0 bytes is not an error |
discard_stale_input() |
Drop everything in the receive buffer; called at the start of a transaction |
now_us() |
A monotonic microsecond clock; reply timeouts are measured with it |
delay_ms_impl(ms), yield_impl() |
Optional. Without them the base versions do nothing |
Return DriverError::SerialError (or {0, SerialError}) only for a UART failure. Timeouts, checksums and echo
checks are the driver's job.
The transport¶
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); }
};
Then bind it, with braces:
Rules for the half-duplex line¶
- Turn around in
finish_transmit(). It must not return before the UART's shift register is empty: release the direction pin earlier and the last byte is cut; later and the start of the reply is lost. The factory Return Delay Time gives you 500 µs. - Flip DIR in the transport, nowhere else.
DeviceandBusnever see a pin. With an auto-direction transceiver there is nothing to flip. - Single wire, TX and RX joined: leave RX enabled while you transmit and set
EchoPolicy::ExpectedLocalEcho. The driver reads back and checks the echo itself;read_some()returns the echo bytes like any others. - No delays of your own. The reply timeout already allows for the packet time and
latency_ms. - Give
read_some()a receive buffer. It is polled; bytes that arrive between polls must not be dropped. A ring buffer filled by an interrupt or the UART driver's own FIFO is enough.
Reference implementations¶
| Transport | Where |
|---|---|
| ESP-IDF UART, optional direction GPIO | hf_dynamixel_esp_uart.hpp |
hf-core BaseUart |
HalUartDynamixelComm in hf-core, handlers/dynamixel/DynamixelHandler.h |
| Scripted fake for host tests | tests/host/fake_transport.hpp |
The ESP-IDF transport is a template over the UART number, the TX, RX and DIR pins and the DIR polarity; it uses
uart_wait_tx_done() for finish_transmit() and esp_timer_get_time() for now_us().
Check your transport¶
- Build it: the transport on this page is compiled by the host CI from
tests/docs/my_uart.hpp, withTransport<MyUart>instantiated over it. Bus::Ping(1)on a factory servo at 57600 bit/s: aPingInfowith model 1070 for an XC430-W150 proves both directions.- Read Present Position in a loop of 1000 reads: any
TimeoutorChecksumErrorpoints at the turn-around or the receive path. - On a single-wire link,
EchoMismatchmeansecho_policydoes not match the wiring.
Threads¶
The driver is not thread-safe. Give each bus one owner task, or guard every call on a bus, its devices and its transport with one mutex. Separate buses on separate UARTs can run in parallel.