Firmware
Bare-metal Rust firmware for OpenServoCore — motor control, bootloaders, and embedded protocols on CH32.
Current
A servo protocol for cheap MCUs: stream processing on the wire
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.
A chip-agnostic architecture for bare-metal embedded Rust
Embedded Rust has excellent building blocks like HALs, PACs, and async executors. But very little is published about how to structure a serious firmware project on top of them. Most examples stop at blinky with a HAL. What does the architecture look like when you have a real protocol stack, a motor control loop, a dozen interacting peripherals, and the ambition to support more than one chip? And what if you are bare-metal, with no RTOS and no async executor, just interrupt handlers and a main loop? This article documents the architecture OpenServoCore ’s firmware actually uses, in enough detail to build it for your own project. Overview first, then a zoom into every layer with its internal organization and a code example, all the way down to the discrete-event simulation. The architecture has earned some trust the hard way. The firmware’s entire wire protocol was ripped out and replaced mid-project, and the layering held. The logic layers compile and unit-test on a desktop with no hardware attached. And the simulation caught over a dozen real bugs before they ever reached silicon. The code examples are teaching-sized but structurally faithful to the shipping code, and the codebase itself is public if you want the full-scale version. If you are new here, the project overview has the background.
Using the SPI peripheral as a DMA-fed CRC engine
While building the wire protocol for OpenServoCore , I ran into a small problem that was costing a lot of CPU. Every frame on the servo bus carries a CRC-16 checksum, the bus runs at 3 megabaud, and the chip doing the checking is a $0.16 CH32V006 with no CRC peripheral. It does have an SPI block though, and it turns out there is a perfectly good CRC engine hiding inside it. This article is a how-to for commandeering that engine as a DMA-fed CRC coprocessor for data that has nothing to do with SPI. In my case that’s UART frames, but the engine doesn’t care where the bytes came from. Everything below is the current shipping design, verified against the code in the OpenServoCore repo , and as far as I know the trick works on every WCH CH32 chip with an SPI block. If you are new here, the project overview has the background.
HSI trim: calibrating a crystalless MCU over the bus
Cheap microcontrollers run on internal RC oscillators, no crystal. On the CH32V006 that oscillator is called the HSI (High Speed Internal), and the datasheet promises ±1% frequency accuracy. The five chips on my bench measured up to 7,000+ ppm (parts per million) apart from each other. For most firmware that’s irrelevant, but for a servo bus where devices time replies off each other’s transmissions, it’s the difference between a working chain and garbled bytes. This article documents how OpenServoCore calibrates a fleet’s clocks today, using nothing but the bus wire itself. There are no extra pins, no factory calibration step, and no per-chip fixture. It’s the current shipping design, code-verified and bench-measured. There are three pieces, and you can lift each one independently: a broadcast break train for absolute calibration at boot, a differential drift tracker that runs continuously off normal traffic, and the trim loop that turns error into oscillator steps. If you want the background first, the protocol design article covers the bus this runs on, and the project overview has the full picture.
Dynamixel 2.0 servo side: implementing Fast Sync/Bulk Read
This post documents what I learned implementing Dynamixel Protocol 2.0 on the servo side. OpenServoCore has since moved to its own wire protocol , but everything below stands on its own if you are building a Dynamixel-compatible device. Fast Sync Read and Fast Bulk Read are the instructions that make a Dynamixel 2.0 bus fast. You send one request and get one shared reply carrying every servo’s data back-to-back. They are also, as far as I can tell, completely undocumented from the servo side. There is plenty written about sending a Fast Sync Read, but nothing about being one of the servos answering it. This is part 2 of a pair. Part 1 covered how a servo detects the moment a packet ends on the wire, and I’ll assume that vocabulary here (wire-end, publish time, jitter, RDT). This article is the build itself. It covers the wire layout, the CRC math, the slot arithmetic, the hardware transmit start, and the CRC patch that races your own transmit DMA, each with the code. The worked example is the CH32V006 ($0.16, 48 MHz, no crystal), and the code comes from the frozen DXL-era tree , trimmed to teaching size. Every mechanism has an equivalent on any small MCU with a timer, DMA, and a UART. If you are new here, the project overview has the full picture.
Dynamixel 2.0 servo side: RX timing on the CH32V006
This post documents what I learned implementing Dynamixel Protocol 2.0 on the servo side. OpenServoCore has since moved to its own wire protocol , but everything below stands on its own if you are building a Dynamixel-compatible device. There is plenty of material online about talking to Dynamixel servos. Host libraries, ROS drivers, GUI tools, you name it. But there is almost nothing about being one. If you are putting a microcontroller inside a servo, building a custom sensor that lives on a Dynamixel bus, or emulating a Dynamixel device (what the Dynamixel2Arduino library calls a Slave), you are mostly on your own. Well, I spent several months being one. This article and the next one document what that took, so you don’t have to rediscover it yourself. This one covers how a servo knows when the host stopped talking. That sounds trivial, but it turns out to be the foundation everything else stands on, and this time I’m going to walk through the actual build. Ring setup, interrupt handlers, formulas, and the code for each. The next one covers Fast Sync/Bulk Read, the protocol’s group-read mode. All examples run on the CH32V006, a $0.16 RISC-V microcontroller, because that’s what OpenServoCore runs on. The code is real, lifted from the frozen DXL-era tree and trimmed down to teaching size, and the recipe transfers to any small chip with a UART, DMA, and a free-running timer. Keep the Dynamixel Protocol 2.0 reference manual handy, I will use its vocabulary (packets, instructions, status replies) without re-explaining it. And if you are new here, the project overview has the full picture.
OpenServoCore - Cascaded PID Loop Working
Finally got cascaded PID stable. Overshoot down from 15% to 2%, settling time 120ms.