Skip to content
Aaron Qian
  • Log
  • Archive
  • About
  • Subscribe via RSS
  • GitHub

Log

Build logs and progress updates across all projects.

  • 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.

    Aug 8, 2026
    • firmware
    • ch32v006
    • protocol
    • rust
  • 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.

    Aug 1, 2026
    • firmware
    • rust
    • architecture
    • testing
  • 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.

    Jul 25, 2026
    • firmware
    • ch32v006
    • rust
  • 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.

    Jul 18, 2026
    • firmware
    • ch32v006
    • protocol
    • rust
  • 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.

    Jul 11, 2026
    • firmware
    • ch32v006
    • dynamixel
    • dxl
  • 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.

    Jul 5, 2026
    • firmware
    • ch32v006
    • dynamixel
    • dxl
  • CH32V006 Servo Dev Board (Rev. B) Bring-Up - One Trace Cut

    When the first Rev B board came back from PCBWay, I hooked up the WCH-LinkE, fired up the debugger, and tried to connect. The debugger reported no target at all. The board was clearly powered (3.3 V rail LED on, no heat, no smoke), and the LinkE itself was alive, but whatever was on the other end of the SWD lines wasn’t answering. Unlike Rev A where I failed my way to debug success, I didn’t actually have to probe to know what was going on this time. It immediately hit me that the Rev B schematic puts nRST and OPN2 on the same physical pin, and is tied to ground. This means the chip is held in reset forever, no way of changing nRST to GPIO via LinkE… If you’re new here, OpenServoCore is my effort to turn cheap MG90S-class servos into networked smart actuators with sensor feedback, cascade control, and a DYNAMIXEL-style TTL bus. The CH32V006 dev board is the firmware development platform for this project. Rev B is the second revision, announced in April and fabricated and assembled by PCBWay as a sponsored run.

    Jun 8, 2026
    • hardware
  • CH32V006 Servo Controller Dev Board (Rev. B) Designed

    Rev A took board surgery to power on. Then I hit an RX line that refused to go LOW . Then I noticed a third defect I never wrote up. The differential current sensing on the OPA wasn’t actually differential. Rev B is the respin that fixes all three, plus a handful of features I was going to need anyway. If you’re new here, OpenServoCore is my effort to turn cheap MG90S-class servos into networked smart actuators with sensor feedback, cascade control, and a DYNAMIXEL-style TTL bus. The CH32V006 dev board is the firmware development platform for this project. Rev B is the second revision of that board, routed this week and ready to fab.

    Apr 29, 2026
    • hardware
    • announcement
  • CH32V006 Servo Dev Board (Rev. A) - UART RX Stuck High

    The first time I tried to bring up tinyboot on the Rev A dev board, I got stuck on something much more basic. RX wouldn’t go low. The scope showed a perfectly shaped square wave, riding on top of 3.3 V with a whopping 180 mV of swing. Something was holding the line near the rail so hard my USB UART adapter could only nudge it down a couple hundred millivolts. If you’re new here, OpenServoCore is my effort to turn cheap MG90S-class servos into networked smart actuators with sensor feedback, cascade control, and a DYNAMIXEL-style TTL bus. tinyboot is the Rust bootloader for those boards, and this post is what happened the first time I tried to bring it up on the Rev A CH32V006 dev board . The culprit turned out to be the half-duplex TTL front-end. TX_EN isn’t a transmit enable, it’s a mux select, and with it low the buffer was actively pushing 3.3 V back into RX through a 24 mA CMOS output stage. In this post you’ll find scope photos, a schematic walkthrough, the absurd workaround (assert TX_EN to receive), and why Rev B gets a jumper instead of a firmware fix.

    Apr 25, 2026
    • hardware
  • tinyboot v0.4.0 Released - The API is Stable

    You may have noticed v0.3.0 came and went without an announcement. That was deliberate. Crates were merging, APIs were shifting, the wire protocol was being reworked, and I didn’t want to ship a writeup that’d be stale in two weeks. v0.4.0 is the release where the dust finally settles. If you’re new here, tinyboot is a minimal Rust bootloader for resource-constrained MCUs. It fits in 1920 bytes of system flash and gives you CRC-validated firmware updates over UART, with trial boot and automatic fallback to service mode. I’m building it as part of OpenServoCore so you can OTA-update servo firmware over the same single-wire DXL TTL bus, without tearing the robot apart. Here is what’s in v0.4.0. Full CH32V00x family support (V002 through V007, including the V006 that the OSC dev board runs on), TX_EN now fits in system flash on every supported variant, the three tinyboot-ch32-* crates collapsed into one, 24-bit protocol addresses with per-command flag bits, two nasty half-duplex bugs squashed during dev-board validation, and a docs rewrite aimed at users instead of maintainers. There is also one more announcement. With the API stable, active feature work is pausing while my attention shifts to the OSC firmware rewrite.

    Apr 22, 2026
    • tinyboot
    • announcement
  • CH32V006 Servo Dev Board (Rev. A) First Spin - Faulty Rev Lessons

    The Rev A boards of the CH32V006 dev board came back from PCBWay a few weeks ago, and the bring-up did not go the way I imagined. The 3.3V rail sat at 0.84V, I fed reverse voltage into a $0.22 MCU across multiple power-on cycles, and somehow it still came back to life. If you’re new here, OpenServoCore is my effort to turn cheap MG90S-class servos into networked smart actuators with sensor feedback, cascade control, and a DYNAMIXEL-style TTL bus. The CH32V006 dev board is the firmware development platform for this project. This is the first-spin bringup of Rev A, generously sponsored by PCBWay for both PCB and assembly. If you are short on time, here is the quick version. PCBWay caught footprint and BOM mistakes I missed (and KiCad’s DRC missed), a wrong house number sent the boards to my neighbor, mislabeled test points fed 3.3V into the EN pin, I eventually realized I’d swapped VDD and VCC on the schematic, and it took an hour of magnet-wire surgery under a magnifier to bring one board back. The CH32V006 is, it turns out, surprisingly tough.

    Apr 4, 2026
    • hardware
  • tinyboot v0.2.1 Released - User-Flash Path Fixes

    Turns out shipping a bootloader and actually booting real firmware through it are two different things. The user-flash path was quietly broken, and the week after the initial release was mostly spent hunting down the bugs that only show up once an application is sitting behind the bootloader. If you’re new here, tinyboot is my minimal serial bootloader for the CH32V003 and friends, squeezed into 1920 bytes of system flash. The 0.2.0 release got the protocol and the system-flash variant solid. 0.2.1 is the follow-up that makes the user-flash variant actually usable. The main fixes are a hardcoded mtvec in qingke-rt that pointed the interrupt vector table at the wrong place, APB2 peripherals leaking state from the bootloader into the app, a broken app_version read, an alignment UB in the boot metadata path, and a defmt panic on reset-into-bootloader. I also managed to shave another ~180 bytes off the system-flash build.

    Mar 25, 2026
    • tinyboot
    • announcement
  • tinyboot - Initial Release for CH32V003

    While I was waiting for the CH32V006 dev board to arrive, I ended up going on a side quest: making firmware updates less painful for future OpenServoCore-based servos. Once a servo is built into a system, it is not really practical to pull it back out, open the case, desolder the board, and hook up a debugger just to flash new firmware. Updating over the existing one-wire UART bus is a must-have feature if I wanted to keep both you and me sane. And that is how tinyboot was born. If you’re new here, OpenServoCore is my effort to turn cheap MG90S-class servos into networked smart actuators with sensor feedback, cascade control, and a DYNAMIXEL-style TTL bus. tinyboot is the bootloader half of that story. It is a small Rust bootloader that lives where the WCH factory bootloader normally sits, so a deployed servo can take firmware updates over the same one-wire UART bus it already speaks on. This first release targets the CH32V003, fits in 1920 bytes of system flash, and ships with CRC validation, a trial-boot counter that rolls back bad updates on its own, a modular transport layer (standard UART plus DXL TTL with TX_EN), and a tinyboot-cli for driving updates from the host.

    Mar 20, 2026
    • tinyboot
    • announcement
  • tinyboot - A Minimal Rust Bootloader for CH32

    tinyboot is a Rust bootloader that squeezes into the CH32V003’s 1920-byte system flash, with CRC16 validation and trial boot, and still leaves the entire 16KB user flash free for your application. tinyboot is the bootloader half of OpenServoCore , my effort to turn cheap MG90S-class servos into networked smart actuators. Servos need field updates over the same TTL bus they take commands on, and they have very little flash to spare. So tinyboot has to be tiny, reliable enough to never brick the part, and portable across the CH32 family. This post is the project overview. I’ll go over what tinyboot is, the feature set, the chips it targets (V003 working today, V103 in progress, the rest of the V0/V2/V3/X line planned), and the transports it supports (UART and RS485 working, USB planned). If “Rust in 1920 bytes” sounds like a typo, that’s fair. It surprised me too.

    Mar 19, 2026
    • tinyboot
    • announcement
  • CH32V006 Servo Controller Dev Board - Initial Design

    I’ve been working on [moving OpenServoCore from the $1.85 STM32F301](/log/2025-01-05-ch32-migration/) to the $0.22 CH32V006. Swapping the MCU sounds like a one-line BOM edit, but it turned into the most complex board I’ve designed yet. Weeks in KiCad, and several rounds of Reddit PCB review before I felt good about sending it out. If you’re new here, OpenServoCore is my effort to turn low-cost servos like the MG90S into smart actuators with cascade control and DYNAMIXEL-style communication over single-wire UART (DXL TTL). This board is the firmware development platform for that move, and the first dev board to actually exercise the new MCU end to end. For you busy folks, here is the gist. The board has three-way power ORing for USB-C, bench supply, and 1S/2S LiPo direct, low-side current sensing through CH32V006’s internal PGA op-amps, motor terminal voltage sensing for back-EMF, an NTC to replace the STM32’s internal temperature sensor, single-wire DXL TTL through a 74LVC2G241 buffer, and probably more test points than strictly necessary. It’s off to PCBWay, since JLCPCB was out of CH32V006F8P6 the day I hit submit.

    Mar 7, 2026
    • hardware
  • Thanksgiving Night Build: Dual ITR1204 IR Sensor Test Module

    The house got quiet on Thanksgiving night, so I opened KiCad and finally started the encoder board that’s been sitting in my backlog. It’s a $0.60, dual-channel reflective IR sensor that’s the first step toward closing the loop on cheap servos. If you’re new here, OpenServoCore is my effort to turn cheap MG90S-class servos into networked smart actuators with sensor feedback, cascade control, and a DYNAMIXEL-style TTL bus. To get there I need a reliable, low-cost way to measure motor motion, and this board is Part 1 of that. It’s a tiny ITR1204-based PCB that handles the detection stage. Part 2 will be the MCU and comparator front-end once these come back from fab. In this post you’ll find the schematic and 4-layer layout, a full walk through four operating modes (5 V and 3.3 V, digital and analog) with the math behind each resistor pair, a BOM that lands at roughly $0.60 per board, and JLCPCB ordering notes for both bare-PCB and full PCBA paths. The board is intentionally retunable by swapping two resistor pairs, so the same footprint works as a quadrature edge sensor or a direct-to-ADC analog encoder.

    Nov 28, 2025
    • hardware
  • OpenServoCore - Cascaded PID Loop Working

    Finally got cascaded PID stable. Overshoot down from 15% to 2%, settling time 120ms.

    Jan 14, 2025
    • firmware
  • OpenServoCore - Migration from STM32 to CH32 RISC-V

    Switching from STM32F301 to CH32V303 to cut BOM cost.

    Jan 5, 2025
    • ch32
  • OpenServoCore - Open-Source Smart Servo Platform (CH32V006)

    Democratize Robotics For Everyone

    Dec 15, 2024
    • announcement
    • hardware

© 2026 Aaron Qian

  • RSS feed
  • GitHub
  • LinkedIn
  • X/Twitter
  • Email