Extending the Library¶
DXL2-C is intentionally small: it implements the wire format and stops there. This chapter shows where the extension points are, how to build the common higher-level pieces on top of the public API, and how to change the library itself when that is the right thing to do.
Where to extend¶
| You want to… | Do it in | Library change needed? |
|---|---|---|
| Support another transport (RS-485, USB CDC, TCP, CAN gateway) | your write function and receive loop | No |
| Emulate a device model | your packet handler and Control Table | No |
| Send SYNC, BULK or other multi-device instructions | parameter builders on top of dxl2_link_send() |
No |
| Use a private, vendor-specific instruction | your handler; the codec passes any instruction byte through | No |
| Handle bigger packets | DXL2_MAX_PACKET_SIZE |
No, configuration only |
| Run several buses | one struct dxl2_link per bus |
No |
| Fix a bug or change wire-format behavior | the library sources | Yes — see Contributing |
Almost everything is done on top of the library. Keep it that way: your extensions stay portable, and you can update DXL2-C without merging changes.
Adding a transport¶
A transport needs two pieces of code.
Receive. Deliver received bytes to dxl2_link_receive() from one context. Any
chunking works. Typical shapes:
| Transport | Receive loop |
|---|---|
| UART with RX interrupt | ISR writes to a ring buffer; main loop or thread drains it |
| UART with DMA | Idle-line or half/full-transfer interrupt signals a thread; the thread passes the new region |
| USB CDC / TCP socket | Pass each received block as it arrives |
| Log file or capture | Read the file in blocks and pass them in order |
Transmit. Implement dxl2_write_fn:
The contract:
datais one complete packet, header to CRC. Send alllengthbytes in order, without interleaving other output.- Return
0on success or a negative value on failure; the value is returned bydxl2_link_send*(). Avoid-1, which isDXL2_PARSE_ERROR. - The buffer is valid only during the call. A buffered or DMA transmitter must copy it.
- On a half-duplex bus, enable the driver before the first byte and switch back to receive when the last byte has left the shift register — not when the FIFO is empty.
- If several contexts can send, serialize the calls with a mutex.
Use write_context to pass your driver instance, so one write function serves several
buses:
struct uart_port; /* your driver */
int uart_port_write(struct uart_port* port, const uint8_t* data, size_t length);
static int link_write(void* context, const uint8_t* data, size_t length)
{
return uart_port_write((struct uart_port*) context, data, length);
}
static struct dxl2_link bus_a;
static struct dxl2_link bus_b;
void buses_init(struct uart_port* port_a, struct uart_port* port_b)
{
const struct dxl2_link_config config_a = {.write = link_write, .write_context = port_a};
const struct dxl2_link_config config_b = {.write = link_write, .write_context = port_b};
(void) dxl2_link_init(&bus_a, &config_a);
(void) dxl2_link_init(&bus_b, &config_b);
}
Building a Control Table¶
A device is mostly a register map. A table-driven description keeps the READ and WRITE handlers generic and makes range and access checks automatic:
enum access
{
ACCESS_R, /* read-only */
ACCESS_RW, /* read-write, RAM */
ACCESS_RW_EEPROM, /* read-write only while torque is off */
};
struct item
{
uint16_t address;
uint8_t size;
enum access access;
};
static const struct item items[] = {
{0U, 2U, ACCESS_R}, /* Model Number */
{6U, 1U, ACCESS_R}, /* Firmware Version */
{7U, 1U, ACCESS_RW_EEPROM}, /* ID */
{64U, 1U, ACCESS_RW}, /* Torque Enable */
{65U, 1U, ACCESS_RW}, /* LED */
{116U, 4U, ACCESS_RW}, /* Goal Position */
{132U, 4U, ACCESS_R}, /* Present Position */
};
static uint8_t table[CONTROL_TABLE_SIZE];
/* DXL2_ERR_NONE if [address, address + length) may be written now. */
static uint8_t check_write(uint16_t address, uint16_t length, bool torque_on)
{
uint8_t error = DXL2_ERR_NONE;
for (size_t i = 0U; i < (sizeof(items) / sizeof(items[0])); ++i)
{
const struct item* item = &items[i];
const bool overlaps = (item->address < (address + length))
&& (address < (item->address + item->size));
if (overlaps && ((item->access == ACCESS_R)
|| ((item->access == ACCESS_RW_EEPROM) && torque_on)))
{
error = DXL2_ERR_ACCESS;
}
}
return error;
}
Apply side effects after the write — for example, a new ID takes effect after the Status
Packet has been sent with the old one. The simulator of DXL2 Studio
(tools/dxl2_gui/native/dxl2_sim.c) is a complete reference implementation of an X-series
Control Table, including REG_WRITE/ACTION, FACTORY_RESET, SYNC and BULK instructions.
Building instruction parameters¶
The codec encodes any instruction; it does not need to know its parameter layout. Build
the parameters with the little-endian helpers and send them with dxl2_link_send().
| Instruction | Parameters |
|---|---|
| READ | address (2), length (2) |
| WRITE, REG_WRITE | address (2), data (n) |
| SYNC_READ, FAST_SYNC_READ | address (2), length (2), ID (1) × devices |
| SYNC_WRITE | address (2), length (2), then per device: ID (1), data (length) |
| BULK_READ, FAST_BULK_READ | per device: ID (1), address (2), length (2) |
| BULK_WRITE | per device: ID (1), address (2), length (2), data (length) |
| FACTORY_RESET, CLEAR, BACKUP | one or more option bytes, see the ROBOTIS specification |
SYNC and BULK instructions are sent to the broadcast ID. A SYNC_WRITE builder that sets the 4-byte Goal Position (address 116 on X-series servos) of several servos at once:
/* Returns 0 on success, DXL2_PARSE_ERROR if the packet does not fit. */
int sync_write_u32(struct dxl2_link* link, uint16_t address, const uint8_t* ids,
const uint32_t* values, size_t count)
{
uint8_t parameters[DXL2_MAX_PARAMETER_SIZE];
size_t length = 4U;
int result = DXL2_PARSE_ERROR;
if ((4U + (count * 5U)) <= sizeof(parameters))
{
dxl2_put_u16(¶meters[0], address);
dxl2_put_u16(¶meters[2], 4U); /* bytes per device */
for (size_t i = 0U; i < count; ++i)
{
parameters[length] = ids[i];
dxl2_put_u32(¶meters[length + 1U], values[i]);
length += 5U;
}
result = dxl2_link_send(link, DXL2_BROADCAST_ID, DXL2_INST_SYNC_WRITE, parameters,
(uint16_t) length);
}
return result;
}
dxl2_link_send() also returns DXL2_PARSE_ERROR when byte stuffing makes the packet
larger than DXL2_MAX_PACKET_SIZE, so the size check above is a fast path, not the only
guard.
For SYNC_READ and BULK_READ, every addressed device answers with its own Status Packet, in the order of the request; collect them in your handler as shown for broadcast PING. FAST_SYNC_READ and FAST_BULK_READ return one combined Status Packet whose parameter layout is described in the ROBOTIS specification; parse it in your handler.
Vendor-specific instructions¶
The parser and the encoder accept any instruction byte. If you control every node on a
bus, you can define private instructions, for example to update firmware. Choose values
that the specification does not use, keep them out of public buses, and document them —
a future revision of the protocol may assign them. Do not add them to dxl2_defs.h,
which only contains values from the specification.
Bridging two buses¶
A gateway that forwards packets between two buses needs no special support: each bus gets its own link, and each handler re-sends what it receives on the other link.
static void forward_a_to_b(const struct dxl2_packet* packet, void* context)
{
(void) context;
(void) dxl2_link_send(&bus_b, packet->id, packet->instruction, packet->parameters,
packet->parameter_length);
}
Re-encoding regenerates the stuffing and the CRC, so packets that arrived corrupted are never forwarded — the parser has already dropped them.
Changing the configuration¶
DXL2_MAX_PACKET_SIZE is the only build-time parameter. Raise it if your devices use
large READ, SYNC or BULK transfers; lower it to save RAM on small MCUs. Define it for the
whole build, so that the library and the application agree on the structure sizes:
The value must be between 10 and 65542; the build stops with an error otherwise. See Configuration for its effect on RAM and stack.
Contributing to the library¶
Changes to DXL2-C itself are welcome when they fix a bug, follow the specification more closely, or add something every user needs. The rules keep the library certifiable:
- Code. C11, no new dependencies, no heap, no global mutable state. Public symbols
use the
dxl2_/DXL2_prefix and have Doxygen comments. Follow the MISRA rules used in the code: onereturnper function, parentheses around mixed operators,(void)for ignored results,++/--as separate statements, array indexing instead of pointer arithmetic. - Tests. Every behavior change comes with a GoogleTest test in
tests/unit/gtest_dxl2_*.cpp(register new files indxl2.cmake). Line and branch coverage must stay at 100 %. When the codec changes, update the Python twin intests/integration/dxl2/protocol.py; the cross-check compares both. - Analysis.
misra-analyzemust report 0 violations. An unavoidable deviation is recorded in the deviation register. - Format.
clang-formatwith the repository.clang-formatmust report no changes. - Documentation. Update the affected pages in
docs/, the stack table in Configuration when a function's frame changes, the diagrams (python tools/docs/diagrams.py) when the structure changes, and add an entry under[Unreleased]inCHANGELOG.md. - Compatibility. The public API is stable within a major version: do not remove or change public functions, structure members or return values without a major release.
Run the whole definition of done locally before opening a pull request:
cmake --preset "DXL2[UnitTests]" && cmake --build "build/DXL2[UnitTests]" --target coverage
cmake --preset "DXL2[IntegrationTests]" && cmake --build "build/DXL2[IntegrationTests]" --target integration_tests
cmake --preset "DXL2[Debug]" && cmake --build "build/DXL2[Debug]" --target misra-analyze
cmake --preset "DXL2[Examples]" && cmake --build "build/DXL2[Examples]" && ctest --test-dir "build/DXL2[Examples]" -L example
Pull requests target the development branch. CI runs the same checks and blocks the
merge if any of them fails.