After implementing Dynamixel 2.0 on the servo side , Fast instructions included , I replaced it. OpenServoCore now runs its own wire protocol, designed around what those months taught me about what a cheap microcontroller can and cannot do well. This article documents the design and its measured results. Ping turnaround dropped from 62.8 µs into the 30 to 41 µs band on the same $0.16 chip, and most of the timing machinery the DXL era needed got deleted along the way. The build-by-build numbers are in the Real Numbers section, warts and all.

The centerpiece is an idea worth stealing even if you never build a bus protocol. I call it stream processing. Instead of receive everything, verify, parse, then reply, the servo works on the frame while it is still arriving. It decodes early, checksums in hardware in parallel, and starts transmitting the reply before the reply is even finished being built. If your project has any latency-sensitive communication, this is the part you can reuse.

Everything here is running, measured code in the OpenServoCore repo , and the project overview has the background.

TL;DR

For the impatient, the design in four bullets:

  • Frame with a UART break. It’s an out-of-band delimiter no data pattern can forge, and the UART’s LIN break detector turns it into one hardware interrupt per frame.
  • Process the stream as it arrives. Dispatch and stage the reply while the frame’s tail is still on the wire, then gate the effects on the CRC verdict, basically the same speculation trick CPUs use.
  • The V006 has no CRC peripheral, but its SPI block hides a CRC engine. Feed it by DMA and it’s a free coprocessor that outruns the wire 8 to 1.
  • Derive positions from ring data, and project times from the byte stream’s pace, instead of trusting an interrupt’s arrival time.

The rest of the article is how each piece works, and the numbers that back it up.

Why Replace Dynamixel

The two Dynamixel articles ended on the same observation. Every hard problem traced back to the wire format. A frame that begins with an ordinary data pattern (FF FF FD 00) must be hunted in the byte stream. Since data can imitate the header, the protocol needs byte stuffing to prevent forgeries, which means an unstuffing pass, which means per-byte work. Reply timing hangs on estimates, and the estimates have jitter, which turns into gaps or collisions.

So the new protocol’s philosophy is an efficient wire and a quick turnaround, both achieved by deleting machinery rather than adding it.

Here are the design constraints, honestly stated. It must run whole on the cheapest tier of MCU, with one UART, one DMA ring, no input capture, and no crystal. And it gives up DXL compatibility and multi-host arbitration to get there. A single host schedules the bus, and every exchange is host-initiated.

Framing With A UART Break

The frame delimiter is a UART break. A break is the line held low for ten bit-times. That’s one bit longer than any legal UART character can manage, because every real byte contains a stop bit (a high) within ten bit-times. So no data sequence can imitate a break. It is an out-of-band signal in a protocol that otherwise only has bytes.

Compare what this deletes against the DXL era:

  • Header hunting is gone. The receiver doesn’t search for a pattern, because the break is the frame boundary. Better still, it’s a hardware event. The break shape matches the LIN standard’s break definition, so the UART’s LIN break detector fires on it directly, one interrupt per frame, delivered by silicon.
  • Byte stuffing is gone, along with its encode pass, its unstuff pass, and its “how long is this frame really” ambiguity. Payloads are unrestricted bytes.
  • Resync anxiety is gone. Any corruption costs at most the damaged frame, and the next break is a hardware resync point. There is no cold-path recovery scan.

The frame itself is five overhead bytes. ID, length, instruction, and two CRC bytes, against DXL’s ten-to-twelve plus stuffing. And one small field placement turns out to be really important. LEN is the second byte, so a receiver knows exactly where the frame ends two bytes in. Hold that thought, the next section is built on it.

This trick applies to more than servo buses. If you control both ends of a wire, buy yourself an out-of-band delimiter. Framing that can’t be forged by data deleted entire subsystems here. And if your UART speaks LIN, the break detector is sitting there unused.

Stream Processing

Here’s the conventional way a microcontroller handles a request. Call it the batch model:

receive all N bytes → verify CRC → parse → dispatch → build reply → compute reply CRC → transmit

Every arrow is a wait, and nothing overlaps. The CPU idles while bytes arrive, then does all its thinking after the last byte, then the wire idles while the CPU builds and checksums the reply. At 1 Mbaud, a modest request spends about 100 µs arriving, and the batch model wastes every one of those microseconds. Then it pays its whole processing bill after the frame ends, directly on the turnaround the host sees.

The streaming model doesn’t wait around. There are three mechanisms, and each one is a bit bolder than the last.

Dispatch Before The Verdict

Because LEN arrives second, the servo knows the frame’s full extent almost immediately. So at header time it can already read the instruction, decode arguments, run the dispatch, and stage the complete reply, all while the frame’s remaining bytes (including its CRC) are still on the wire. The work happens under the frame’s own wire time, where it costs nothing.

But wait, the CRC hasn’t been checked yet. The frame could be corrupt! This is exactly the trick modern CPUs use. They call it speculation. Do the work assuming success, and hold the effects until the check lands. Every staged effect is gated by the verdict at frame end. A staged reply is sent on CRC pass and quietly discarded on fail. A staged register write is committed on pass and reverted on fail. The verdict only decides whether the effects land, and none of the work waits for it. On a clean bus the bet basically always wins. A corrupt frame pays one revert, off the happy path, inside the host’s retry contract anyway.

The result is that at frame end, the only thing left on the reply’s critical path is “poll a finished CRC and pull the trigger”, because the thinking already happened during the frame.

The CRC Is Hardware, Running In Parallel

The V006 has no CRC peripheral. It has an SPI block though, and SPI blocks on this chip family contain a CRC engine meant for checksumming SPI transfers. Feed that engine by DMA, no SPI pins involved, and it becomes a free-standing CRC coprocessor. It runs about 0.36 µs per byte, roughly eight times faster than the wire delivers bytes, at approximately zero CPU.

On receive, the engine chews through the frame’s covered bytes while the CRC bytes are still in flight, so the verdict is ready the moment it’s needed. On transmit it’s even better. The reply starts leaving the wire before its own CRC exists. The transmission fires (break, header, payload streaming by DMA) while the engine computes the checksum in parallel, and the result is patched into the frame’s two trailing bytes before the transmit DMA reaches them. The engine outruns the wire 8 to 1, so it is guaranteed to win the race, no luck needed. The firmware fires first and appends the checksum later.

This is also a good excuse to go datasheet spelunking on your own chip, looking for things like a CRC engine hiding in an SPI block, a timer that can trigger DMA, or a peripheral chain that runs with zero CPU. There is usually offload in there you didn’t know you had.

Reads: Three Engines In Parallel

The read path takes the streaming idea the furthest. A read reply’s payload lives in the control table, the register map the control loop is actively writing. The batch instinct says the CPU copies the span out of the table, the CPU computes the checksum, and then transmission starts. That is three sequential jobs on the CPU, and each one scales with the read’s size.

The shipped path gives each job to a different piece of hardware and runs all three at the same time.

  1. Table to snapshot buffer. Staging the read kicks off a fire-and-forget memory-to-memory DMA that streams the requested span into a dedicated snapshot buffer (~0.125 µs/byte).
  2. Snapshot to CRC engine. A second DMA channel feeds the same snapshot into the commandeered SPI CRC engine (~0.36 µs/byte).
  3. Snapshot to wire. The transmit DMA streams the same snapshot out of the UART at wire speed.

There is no synchronization in this pipeline at all, no flags, no waiting on the copy, and no “is the CRC done yet” polls. Correctness comes from speed ordering and bus priority instead. The copy starts earliest, is the fastest of the three, and its DMA channel sits above the CRC feed’s in the arbitration ladder. The CRC engine outruns the wire eight to one. Each consumer structurally cannot catch its producer, so no handshake needs to exist, which also means there are no handshake bugs to run into.

The CPU does very little here. It arms three channels at stage time, then wakes once near the end to move the finished checksum from the engine’s register into the reply’s trailing bytes. One quirk lives in that final step. The engine is an SPI block moonlighting as a CRC coprocessor, and it holds the sum bit-reversed, because its shifter mirrors the reflected algorithm. So the patch does one 40-cycle bit-reverse per frame. That’s a small tax for a free coprocessor, and a fair sample of what borrowed peripherals are like. They work, and they’re a little awkward about it.

Here’s what all that buys. The CPU cost of a read is constant regardless of its size. A 4-byte telemetry poll and a 200-byte table dump cost the same three channel-arms and one wake, and the reply’s first byte never waits on the payload, thanks to fire-first. The only size-dependent term left is the wire itself, which no design can delete. The copy also buys two things a direct-from-table stream couldn’t. Every reply carries a consistent point-in-time snapshot even if the control loop writes mid-read, and the table’s memory alignment stops mattering to the downstream engines. Meanwhile the motor loop keeps running underneath. The bus, at full throughput, costs the core almost nothing.

Writes, honestly, don’t get this treatment, and they shouldn’t. An inbound write’s payload already sits verified-whole in the receive ring, since no frame is applied until its CRC passes. Staging validates it in place, and the commit is a short CPU copy gated by the verdict. Small transfers don’t earn a DMA channel. The streaming machinery goes where the bulk of the bytes are, and that’s the read path.

Deriving Positions And Times From Ring Data

This is the subtlest piece, and it’s the part that makes the whole design solid. The transport has one central rule. Frame positions are derived from ring data instead of being sampled at interrupt entry, and times are projected from the byte stream instead of being taken from a wake’s arrival.

Here’s why. Interrupt service lags. If a handler timestamps “now” at entry, or samples the DMA cursor to ask “what byte just arrived”, it inherits that lag wholesale. And the lag is at its worst exactly when the bus is busiest. Instead, every deadline is computed as a projection from what’s in the ring:

deadline = now + missing_bytes × byte_time

The trick is that now and the ring cursor advance together. If the handler ran late, the cursor has also advanced correspondingly, so the lag cancels out of the projection. The residual error is bounded by one byte-time and always lands on the late side, which, as the DXL articles established, is the harmless side.

The same rule turns the receive ring into a message queue for free. Frames are contiguous, so each frame’s end is the next frame’s start. A backlog of frames that arrived while the CPU was busy is walked directly out of ring data, so there is no event queue and nothing droppable. Break interrupts can coalesce or lag arbitrarily, the data is all still there, and position comes from the data.

The rule I took away from this is to never trust an interrupt’s arrival time as a measurement. I project from the data stream instead, and let the stream itself be the queue.

One Exchange, End To End

Putting it all together, here is a ping at 1 Mbaud (one byte-time = 10 µs):

Measured on the V006 at 1 Mbaud, 30.4 µs from instruction wire-end to the reply’s break falling, on the lean transport build, against 62.8 µs for the tuned DXL 2.0 stack on identical silicon. The current full-featured build runs about 41 µs, and the Real Numbers section below has the full table and the honest history. A deliberate 12 µs reply gap accounts for a large slice of the total. It’s a fixed courtesy window for the host to release the wire, specified in microseconds rather than byte-times so it neither balloons at low baud nor vanishes at high. The rest is trigger bodies and interrupt entries.

Under a zero-gap burst flood, the mean barely moves. The streaming pipeline doesn’t degrade under pressure, because this is exactly the load it was designed for.

What Else The Redesign Deleted

The DXL-era articles cataloged machinery that existed to survive the wire format. Here is what the new protocol made unnecessary: the input-capture edge timestamping (never needed, there is no grid to hit), the hardware timer TX kickoff (replies are break-led and event-driven, so “enable the channel when ready” suffices), the Return Delay Time register and its entire tuning surface, the byte-stuffing codec, the header hunter, and the software CRC fold. Even the bus direction buffer is gone on the next board revision. The UART’s single-wire mode plus a drive discipline (open-drain when listening, push-pull only while talking) turned out to be all the hardware the bus needs.

Group reads got simpler too. Where DXL’s Fast mode needed cumulative CRC checkpoints and microsecond hand-offs , the new chain is just ordinary status frames in sequence. Each servo’s frame is independently CRC’d. Each break gives every listener a hardware resync per element. And a servo whose predecessor goes silent takes over after a specified deadline and flags it, so the chain reports its own damage instead of collapsing silently. The problem the DXL checkpoint format solved simply doesn’t exist on this wire.

And the clock story collapsed into something almost embarrassingly small. The DXL era needed three compensation knobs per chip. Now the host broadcasts a train of bare breaks at exact crystal-timed spacing, every servo stamps them at the same interrupt path (so entry latency cancels in the differences), and each chip trims its oscillator toward the host’s spacing. A few milliseconds of bus time calibrates the whole fleet at once, and reply timing itself needs no calibration anymore, because nothing is scheduled against a grid. The full design is its own article .

Real Numbers

Turnaround here always means instruction wire-end to reply-break fall, measured by the bus adapter’s edge-capture instrument on the wire itself, so no UART is involved in the measurement. 50-exchange sweeps per point, with a zero-failed-exchanges requirement before a mean even counts. Measured means on the current build:

turnaround (µs)0.5 M1 M2 M3 M
PING35.540.949.548.7
READ, 16 B telemetry40.641.149.151.6
WRITE, 4 B validated + acked82.088.397.598.9

Builds swing ±5 µs from flash layout alone. The CI gates sit about 6 µs above these means, so they catch regressions but are not targets to aim for.

Three observations are worth pulling out of the table.

  • Reads confirm the three-engine design with numbers. A 16-byte read turns around within 0.2 µs of a ping at 1 Mbaud (41.1 vs 40.9). The reply waits only on staging the snapshot kickoff, never on the payload. Read size does not appear in the turnaround at all.
  • Write cost is all in the rule checks. The validated write costs about 2x a ping, and basically none of it is the 4 bytes. The time goes to the dispatch body running goal-position’s cross-register limit rules. The production hot loop never pays it anyway, because GWRITE is NOREPLY.
  • The shape flips at 2 Mbaud. At 0.5M and 1M the turnaround is gap-bound. The fixed 12 µs reply gap dominates, and the pipeline hid under the frame’s own wire time. At 2M and 3M it’s pipeline-bound. A short frame is fully ringed before there’s any wire time left to hide under, so the CPU pipeline serializes after frame end. That’s why 3M is barely different from 2M, and why halving the baud doesn’t halve the turnaround.

The group ops are absent from the table because they’re designed not to have turnarounds. GWRITE and COMMIT draw no reply. They cost pure wire time, back-to-back frames are legal, and the ack is implicit in the next telemetry cycle. A rejected write surfaces as that servo’s ALERT bit. A GREAD chain is ordinary status frames in sequence, each slot triggering off its predecessor’s frame end plus the reply gap. The production hot loop (GWRITE, COMMIT, GREAD, hammered zero-gap for thousands of cycles per baud) is gated on something stricter than latency, zero stale read-backs and zero missed replies, at every baud, with no tolerance budget. Fun fact, that strictness caught a “servo glitch” that turned out to live in the measuring instrument. The servo was never dropping frames.

One honesty note, because the numbers moved. The transport work’s lean build measured 30.4 µs pings at 1M, and today’s full-featured build measures 40.9. That’s the real cost of shipped features. Richer table rules, the management plane, chain machinery, plus flash-layout luck. It’s exactly what the budget gates exist to catch and cap. So when a project’s README shows you a single glossy latency number, it’s worth asking which build produced it.

Measured Facts Behind The Design

One habit from this redesign deserves its own section. Every physical-layer behavior the protocol leans on was measured on silicon before the design committed to it. There are fifteen numbered facts, from “the break detector fires exactly once per break, at the span’s end” to “the chip cannot be detuned far enough to break framing at 3 Mbaud”. The spec cites them inline the way a paper cites sources.

When a design decision traces to a measured fact, I found the arguments end pretty quickly. And porting to new silicon becomes a checklist, you just re-verify the facts on the new chip instead of re-arguing the design. The architecture that made the whole protocol swap cheap is its own article.

Caveats

This design has its costs too, so here is where it pays.

  • The break-framed receiver has one accepted weakness. Garble that happens to look like a plausible frame header parks the resolver briefly until real data kills it, bounded by a CRC failure or a 64-byte-time timeout.
  • The protocol is single-host by construction, with no multi-host arbitration, ever.
  • Walking away from DXL wire compatibility means every host-side tool (libraries, GUIs, adapters) is now mine to build. That cost is real, and I chose it with eyes open.

What’s Next

The protocol is bench-proven from 0.5 to 3 Mbaud on multi-servo chains, and it now carries everything OpenServoCore does. It hasn’t been tested outside my own bench yet, meaning other people’s buses, other people’s chips, and the host-side ecosystem it needs to grow. That part isn’t a protocol design problem, and it’s where this project goes next.

If you build something on this design, or find a hole in it, I want to hear about it!