<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aaron Qian</title>
    <description>The latest articles on DEV Community by Aaron Qian (@aaronqian).</description>
    <link>https://dev.to/aaronqian</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F110125%2F295476a5-bcc7-4e9a-8b47-33950bc265e3.jpeg</url>
      <title>DEV Community: Aaron Qian</title>
      <link>https://dev.to/aaronqian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aaronqian"/>
    <language>en</language>
    <item>
      <title>OSC DEV V006 Rev B Is Ready</title>
      <dc:creator>Aaron Qian</dc:creator>
      <pubDate>Sat, 02 May 2026 02:34:12 +0000</pubDate>
      <link>https://dev.to/aaronqian/osc-dev-v006-rev-b-is-ready-1k0g</link>
      <guid>https://dev.to/aaronqian/osc-dev-v006-rev-b-is-ready-1k0g</guid>
      <description>&lt;p&gt;Rev A took board surgery to power on. Then I hit an &lt;a href="https://aaronqian.com/log/2026-04-25-rev-a-uart-rx-stuck-high/" rel="noopener noreferrer"&gt;RX line that refused to go LOW&lt;/a&gt;. 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.&lt;/p&gt;

&lt;p&gt;If you're new here, &lt;a href="https://github.com/OpenServoCore" rel="noopener noreferrer"&gt;OpenServoCore&lt;/a&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Status: designed, not fabricated.&lt;/strong&gt; I've reviewed Rev B carefully and don't expect another Rev A-scale surprise. But the hardware hasn't been built or validated yet. If you want to fab one yourself, wait for the bringup post. Or fab it at your own risk knowing the design is unproven.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The big-ticket items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VDD/VCC swap.&lt;/li&gt;
&lt;li&gt;UART RX contention. Added jumper on RX line to select between LinkE or TTL port.&lt;/li&gt;
&lt;li&gt;Various silkscreen label fixes.&lt;/li&gt;
&lt;li&gt;Battery polarity.&lt;/li&gt;
&lt;li&gt;V006 OPA pin mapping for differential current sensing, and as a result:

&lt;ul&gt;
&lt;li&gt;Reset pin &amp;amp; button gone, assigned as OPN2.&lt;/li&gt;
&lt;li&gt;Hardware fault trip via CMP2.&lt;/li&gt;
&lt;li&gt;5V instead of RST pin for WCH-LinkE port.&lt;/li&gt;
&lt;li&gt;Added VSYS sense to extra pin.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Added:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;External NTC connector with onboard / external select jumper.&lt;/li&gt;
&lt;li&gt;PWM servo header for system ID.&lt;/li&gt;
&lt;li&gt;Dual-mode encoder input (TIM2 quadrature or ADC analog).&lt;/li&gt;
&lt;li&gt;First silkscreen logo for the project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;KiCad project: &lt;a href="https://github.com/OpenServoCore/open-servo-core/tree/main/hardware/boards/osc-dev-v006" rel="noopener noreferrer"&gt;hardware/boards/osc-dev-v006&lt;/a&gt;. Full revision delta in the &lt;a href="https://github.com/OpenServoCore/open-servo-core/blob/main/hardware/boards/osc-dev-v006/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9wm90gi772ysueh3uzh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9wm90gi772ysueh3uzh.webp" alt="Rev B board render" width="800" height="802"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Interactive 3D model of the board is available on the &lt;a href="https://aaronqian.com/log/2026-04-29-osc-dev-v006-rev-b-announcement/" rel="noopener noreferrer"&gt;original post&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's Fixed from Rev A
&lt;/h2&gt;

&lt;p&gt;The fixes are the boring half of the story. They're show-stoppers when they're broken, and invisible when they're right. Five things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VDD / VCC swap.&lt;/strong&gt; This is &lt;em&gt;the&lt;/em&gt; Rev A bug, the one that caused the &lt;a href="https://aaronqian.com/log/2026-04-03-ch32v006-dev-board-first-spin/" rel="noopener noreferrer"&gt;MCU surgery saga&lt;/a&gt;. Rev B has the schematic right. A fresh chip will power on without magnet wire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top-row test-point silkscreen.&lt;/strong&gt; Every label was wrong on Rev A (the photo lives in the &lt;a href="https://aaronqian.com/log/2026-04-25-rev-a-uart-rx-stuck-high/" rel="noopener noreferrer"&gt;TX_EN debug post&lt;/a&gt;). The labels are now what the underlying nets actually are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encoder connector labels.&lt;/strong&gt; Same story, smaller surface area.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JST-PH battery polarity.&lt;/strong&gt; Was reversed. Now it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TX_EN / UART RX contention.&lt;/strong&gt; This one came out of the &lt;a href="https://aaronqian.com/log/2026-04-25-rev-a-uart-rx-stuck-high/" rel="noopener noreferrer"&gt;scope-debug session&lt;/a&gt; where I figured out the half-duplex buffer was actively driving RX. Rev B adds JP2, a jumper that routes the MCU's RX either through the TTL buffer (DXL mode, normal operation) or floats it (LinkE plain-UART mode, with the LinkE driving RX directly via J4). The point is that UART now works on a bare chip with no firmware running, which is the property that &lt;code&gt;wchisp&lt;/code&gt; and similar tools actually require. Recovery-path peripherals shouldn't depend on firmware to function. A jumper is a small price for that property.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's New
&lt;/h2&gt;

&lt;p&gt;These are the additions, with the differential current sensing piece up front because it's the one Rev A literally can't do. The rest are features I folded in while the respin was happening anyway, mostly aimed at letting the dev board double as a system-ID rig later (motor constants, stock-servo characterization, that kind of thing). Same MCU, same motor driver, same shunt, same DXL bus. One board, three roles. The marginal cost of each was small enough that it would have been silly not to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Differential current sensing, actually differential this time
&lt;/h3&gt;

&lt;p&gt;This is the Rev A defect I haven't written up until now, and arguably the most serious one. &lt;code&gt;ISNS-&lt;/code&gt; was routed to &lt;code&gt;OPN0&lt;/code&gt;, which isn't a valid negative input for the V006's differential opamp. The only configuration the silicon would actually accept was one where the OPA's negative input was internally tied to ground. Which means the Kelvin sense traces on the GND side of the 10 mΩ shunt were doing nothing. The board was effectively single-ended, sensing the shunt against silicon GND, and could only see current flowing one direction. For a front end whose whole job is to feed a cascade current loop, that's a fatal flaw.&lt;/p&gt;

&lt;p&gt;Rev B fixes this. &lt;code&gt;ISNS-&lt;/code&gt; is now on &lt;code&gt;OPN2&lt;/code&gt; instead of &lt;code&gt;OPN0&lt;/code&gt;. &lt;code&gt;ISNS+&lt;/code&gt; stays on the OPA positive side via &lt;code&gt;OPP0&lt;/code&gt;. Because &lt;code&gt;OPN2&lt;/code&gt; shares its package pin with &lt;code&gt;nRST&lt;/code&gt;, I had to remove the reset button and free up the pin for analog use. This means the USER option byte gets programmed to disable reset-on-NRST at provisioning. See Pin Remap for the full domino chain that hangs off this one decision.&lt;/p&gt;

&lt;p&gt;What this actually buys is worth the trade-off though. Three things, in order of how big a deal each one is.&lt;/p&gt;

&lt;p&gt;The first is accuracy. The Kelvin sense traces I'd laid out on either side of &lt;code&gt;RS1&lt;/code&gt; finally do their job. The OPA differentially measures the voltage across the shunt, instead of measuring single-ended with the negative input bonded to ground. Without actually using the Kelvin traces, the reading will be polluted by voltage drops along the GND return path.&lt;/p&gt;

&lt;p&gt;The second, and the bigger deal, is bidirectional visibility. Rev A could only see current flowing &lt;em&gt;into&lt;/em&gt; the motor. With differential OPA setup, the board can see current flowing in the other direction too, such as back-EMF during deceleration, regen, freewheeling current through the diodes during PWM off-time. All of these produce reverse current through the shunt, and a correct PI current loop needs the signed integral of that signal, not a clipped one. I'm sure more uses will surface as the firmware gets real (better state estimation, that kind of thing). Back-EMF and regen are the ones I can name with confidence today.&lt;/p&gt;

&lt;p&gt;The third is that this is basically free. The shunt and MCU's internal opamp supports this with no extra BOM. The only cost is the hardware reset feature, which I can live without.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardware overcurrent / fault protection (CMP2)
&lt;/h3&gt;

&lt;p&gt;The OPA output also feeds CMP2, the V006's internal comparator. The comparator can be configured to raise a fault on overcurrent and stop PWM generation without firmware runtime involvement. Even if the main loop is hung, the trip still fires through silicon.&lt;/p&gt;

&lt;p&gt;With a 10 mΩ shunt, 32x gain on the PGA in self-biased differential mode (VBEN=1, VBSEL=1), and DRV's 4 A peak, the shunt drops ±40 mV at full-scale current. The OPA output sits at a ~1.44 V bias and swings ±1.28 V around it, putting the absolute range at 0.16 V to 2.72 V. This is well inside the ADC's 3.3 V range. The CMP2 negative side can be configured to trip on ~85% of VDD, which is ~2.8 V. This means although it won't trip on this development board on overcurrent (the DRV8212P self-limits at 4 A first), it CAN detect motor shorts and catastrophic current surges. The dev board is designed to be flexible for testing different kinds of servos, but still offer some protection against magic smokes.&lt;/p&gt;

&lt;p&gt;For swap boards, the shunt can be sized so that the &lt;em&gt;fault threshold of the specific servo&lt;/em&gt; lands near the top of the OPA range. Same role as on the dev board: catch hardware failure, not normal operation. For example, the SG90 datasheet specs a 650 mA stall, but inrush can spike well above that on startup, so we want the trip set with margin, somewhere around ~1.2 A (~2× datasheet stall) to ride above normal operation while still catching motor shorts and jams beyond mechanical stall. With a 35 mΩ shunt (a common 1206 1W value, easy to source), the OPA swings ±1.33 V around the 1.44 V bias at 1.2 A, putting the output at ~2.77 V. That's right at the CMP2 trip threshold of ~2.8 V (VBCMPSEL=10). Normal stall (650 mA) lands at ~2.16 V on the OPA output, comfortably below the trip. Bidirectionally, the ADC sees from −1.30 A (negative rail) to +1.67 A (positive rail) before saturation, plenty of headroom for back-EMF and regen.&lt;/p&gt;

&lt;h3&gt;
  
  
  External NTC connector and JP1 source select
&lt;/h3&gt;

&lt;p&gt;The onboard NTC (&lt;code&gt;TH1&lt;/code&gt;) measures ambient board temperature. That was useful as a stand-in for the V003's lost internal temperature sensor in the first version. It's not what you want during motor-stress testing, where the temperature that matters is the temperature of the motor windings. Rev B adds an external NTC connector (J6) and a jumper (JP1) to pick between the two. Onboard for ordinary firmware-dev work, motor-attached for characterization runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  PWM servo header (J3)
&lt;/h3&gt;

&lt;p&gt;A standard 1×3 hobby servo header, driven from &lt;code&gt;IN1&lt;/code&gt;. With this connector, the encoder, and proper firmware, it opens up the possibility for servo characterization such as positional accuracy, slew rate, error percentage on repeated sweep / return-to-center tests. I have some fun ideas on setting up a rig based on this board to automatically ID a servo by streaming the data using the same single-wire UART protocol.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dual-mode encoder input (J8)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ENCA&lt;/code&gt; and &lt;code&gt;ENCB&lt;/code&gt; are now pin-mapped to &lt;em&gt;both&lt;/em&gt; TIM2 and the ADC. Same 2×2 header, two acquisition modes, picked in firmware. The reason for this is that I didn't want to commit the dev board to one encoder strategy.&lt;/p&gt;

&lt;p&gt;In TIM2 mode, the connector takes a standard digital quadrature encoder, magnetic or optical, and counts edges in hardware with no MCU overhead. This is the default mode for motor-speed feedback during motor-ID runs and for any off-the-shelf encoder integration.&lt;/p&gt;

&lt;p&gt;In ADC mode, the same pins are sampled by the ADC, which means the connector also accepts ratiometric analog encoders. The specific use case in my head is something like the IR-quadrature sensor stack used in &lt;a href="https://github.com/adamb314/ServoProject" rel="noopener noreferrer"&gt;Adam Bäckström's ServoProject&lt;/a&gt;, where you read sin/cos directly off a pair of photodiodes, do sub-count interpolation in firmware, and end up with very high effective resolution from a homebrew flex-PCB sensor. Backlash-compensation work and the higher-precision experiments live here.&lt;/p&gt;

&lt;h3&gt;
  
  
  VSNS for direct VSYS sensing
&lt;/h3&gt;

&lt;p&gt;Rev A inferred the system voltage from &lt;code&gt;VSNA&lt;/code&gt; and &lt;code&gt;VSNB&lt;/code&gt; (the motor-terminal sense dividers). That's fine when the motor is running, but it breaks when it's idle. Rev B adds a dedicated &lt;code&gt;VSNS&lt;/code&gt; divider so the board measures &lt;code&gt;VSYS&lt;/code&gt; directly regardless of what the motor driver is doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  WCH-LinkE 5 V as a fourth power input (J4 pin 6)
&lt;/h3&gt;

&lt;p&gt;The WCH-LinkE programmer has a &lt;code&gt;+5V&lt;/code&gt; line to the target board. Since we no longer have &lt;code&gt;nRST&lt;/code&gt;, this pin is now repurposed in Rev B as a power input, gated by an SS54 into the same OR network as USB-C, the JST battery, and the screw terminal. Now you can use LinkE as the sole power source for the board during development. No need for a separate USB-C cable.&lt;/p&gt;

&lt;h2&gt;
  
  
  PCB Art / Logo
&lt;/h2&gt;

&lt;p&gt;I delayed and delayed coming up with a logo for OpenServoCore because one, I'm not really good at graphic design, and two, I just kept on prioritizing PCB / firmware related tasks. But I somehow decided that it was time because I want to actually include a nice looking logo on the Rev B PCB. So, I designed one this week, and it came out better than I thought.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7w9ve8nx9nlr6r7kaeqq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7w9ve8nx9nlr6r7kaeqq.webp" title="O is an encoder disk, S is a servo disk mount, C is a servo horn sweeping" alt="Logo" width="800" height="1015"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The logo purposefully uses a two-tone design. This is because I can use one tone (white) as the silkscreen for the OSC letters themselves. And then use the golden color of exposed copper for the finer decorative symbols. It came out surprisingly well, given that I'm not a graphic designer.&lt;/p&gt;

&lt;p&gt;There's also a small QR code on the silkscreen that points at the docs page for this board. I'd been putting this off for the same reason. This is a board meant for other people, and "scan to read the docs" is pretty much the standard these days.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCU Pin Remap
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxppei9y3no5sqwouzobv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxppei9y3no5sqwouzobv.webp" title="Rev B pin assignments on the CH32V006F8P6. ISNS- now lands on OPN2 (the pin nRST used to occupy), ISNS+ on OPP0, ENCA/ENCB on dual-purpose TIM2/ADC channels, and STAT on a TIM1 channel." alt="CH32V006F8P6 Rev B pin assignments" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pin map is where the rest of the design rearranges itself around the OPN2 fix. Five things changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ISNS+&lt;/code&gt; / &lt;code&gt;ISNS-&lt;/code&gt; → &lt;code&gt;OPP0&lt;/code&gt; / &lt;code&gt;OPN2&lt;/code&gt;.&lt;/strong&gt; Fixes the Rev A defect where &lt;code&gt;ISNS-&lt;/code&gt; was on &lt;code&gt;OPN0&lt;/code&gt; and the OPA could only run single-ended. See Differential current sensing for the why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;nRST&lt;/code&gt; removed.&lt;/strong&gt; It shares a pin with &lt;code&gt;OPN2&lt;/code&gt;, so the OPA fix forced this. The USER option byte gets programmed at provisioning to disable reset-on-NRST. The reset button and its RC debounce network are gone with it. Net pin budget: minus one reset, plus one working differential current sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ENCA&lt;/code&gt; / &lt;code&gt;ENCB&lt;/code&gt; selectable between TIM2 and ADC.&lt;/strong&gt; Same connector, two acquisition modes, picked in firmware. See Dual-mode encoder input for the why. Pin-map cost was finding channels that have &lt;em&gt;both&lt;/em&gt; TIM2 capture &lt;em&gt;and&lt;/em&gt; an ADC channel attached, which constrained the rest of the placement more than I expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STAT LED moved to a TIM1 channel.&lt;/strong&gt; Gives the LED PWM brightness and pattern control instead of binary on/off. With the remaining pins, one of them happens to be TIM1 CH4 for remap 7. So I'm assigning the STAT LED to this pin for brightness control / breathing etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Board renamed.&lt;/strong&gt; &lt;code&gt;servo-dev-board-ch32v006&lt;/code&gt; → &lt;code&gt;osc-dev-v006&lt;/code&gt;. Matches the project's naming convention now that the board family is filling in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Get It
&lt;/h2&gt;

&lt;p&gt;KiCad project lives in the OSC monorepo under &lt;a href="https://github.com/OpenServoCore/open-servo-core/tree/main/hardware/boards/osc-dev-v006" rel="noopener noreferrer"&gt;hardware/boards/osc-dev-v006&lt;/a&gt;. The full revision delta is in &lt;a href="https://github.com/OpenServoCore/open-servo-core/blob/main/hardware/boards/osc-dev-v006/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt; inside that directory.&lt;/p&gt;

&lt;p&gt;There is one more note on component availability. When the first revision went out, JLCPCB had no &lt;code&gt;CH32V006F8P6&lt;/code&gt; stock at all and PCBWay had to source the chip externally. Now both JLCPCB and LCSC have restocked the part. At the time of writing, there are a couple thousand of them, so if you are confident enough to fabricate one, it should be doable via JLCPCB now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I don't expect any more serious issues with Rev B. It should just be plug and play. My plan is to order Rev B, do one more pass of validation, and start writing code for the OSC firmware. Most of the bringup work is already done on Rev A. When Rev B arrives, I just need to tweak the register setup a bit and it should be good to go.&lt;/p&gt;

&lt;p&gt;The focus now shifts to firmware, fingers crossed.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>embedded</category>
      <category>electronics</category>
      <category>riscv</category>
    </item>
    <item>
      <title>Rev A UART Validation - RX Stuck High (TTL Buffer / TX_EN Gotcha)</title>
      <dc:creator>Aaron Qian</dc:creator>
      <pubDate>Sun, 26 Apr 2026 10:41:26 +0000</pubDate>
      <link>https://dev.to/aaronqian/my-rx-line-was-stuck-high-and-txen-was-the-fix-4hab</link>
      <guid>https://dev.to/aaronqian/my-rx-line-was-stuck-high-and-txen-was-the-fix-4hab</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;If you're new to the project, &lt;a href="https://github.com/OpenServoCore" rel="noopener noreferrer"&gt;OpenServoCore&lt;/a&gt; is my effort to turn cheap MG90S-class servos into networked smart actuators with sensor feedback, cascade control, and a DYNAMIXEL-style TTL bus. &lt;a href="https://github.com/OpenServoCore/tinyboot" rel="noopener noreferrer"&gt;tinyboot&lt;/a&gt; is the Rust bootloader that runs on those boards. It fits in the CH32V003's 1920-byte system flash and gives you CRC-validated OTA updates over UART, with trial boot and automatic rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;For those of you who just want a summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; UART RX on the Rev A V006 dev board wouldn’t go low. TX worked fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triage:&lt;/strong&gt; Scope showed only ~180 mV ripple on a 3.3 V line. Something was actively driving it high.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnosis:&lt;/strong&gt; The half-duplex TTL front-end was the culprit. &lt;code&gt;TX_EN&lt;/code&gt; wasn’t just “TX enable”, it controlled whether the buffer was actively driving RX.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still suggest you to at least skim through though. Those scope debug photos, videos, and schematic screenshots are probably still worth your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial Troubleshooting
&lt;/h2&gt;

&lt;p&gt;The Rev A &lt;a href="https://github.com/OpenServoCore/open-servo-core/tree/main/hardware/boards/servo-dev-board-ch32v006" rel="noopener noreferrer"&gt;CH32V006 OpenServoCore dev board&lt;/a&gt; just came back from fabrication, and the first task was bringing the V006 port of tinyboot up on it. The V006's UART controller shares the same silicon as the V003, so you'd think the existing HAL driver would just work. But alas, things are never that simple... The &lt;code&gt;tinyboot&lt;/code&gt; CLI couldn't reach the flashed bootloader at all. Same silicon, same driver, but the V006 was completely silent on UART.&lt;/p&gt;

&lt;p&gt;To eliminate the possibility of other driver or init issues, I wrote a separate UART test app to dig into it more thoroughly. I first wrote a bare minimal app that just initialized UART using the same HAL driver and sent out "Hello world" periodically via the TX line. For the PC, I used &lt;a href="https://github.com/neundorf/CuteCom" rel="noopener noreferrer"&gt;CuteCom&lt;/a&gt;, a graphical serial terminal. Immediately I got garbage, but this is actually a good sign. It means the MCU is sending things out, just the clock needs some tweaking. And after some RCC tweaks, I was able to get the right clocks setup to receive TX with no issues. So far so good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrowing It Down
&lt;/h2&gt;

&lt;p&gt;The test app was happily transmitting bytes out over UART, but sending bytes back was a completely different story. The line was basically silent. It didn't matter how I tweaked the UART register settings or how many times I read and re-read the reference manual, I always got silence, nothing was reaching the chip. Given that the HAL code and the UART controller in the silicon are essentially the same as the V003, and that "Hello world" was coming out of the MCU's TX (proving clock init is sound), the only conclusion I could come up with was that the issue isn't in the firmware. It's in the hardware.&lt;/p&gt;

&lt;p&gt;So I moved on to the schematic and the PCB layout, looking for anything wrong: a swapped pull-up, an inverted wire, anything. The UART TX/RX connector I used for testing is routed directly from the MCU. The same TX/RX routes also branch off to a half-duplex DXL TTL front-end. I had a brief suspicion that this might have something to do with RX not working, but couldn't come up with a plausible theory to back it up, so I moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope to the Rescue
&lt;/h2&gt;

&lt;p&gt;When I ran out of ideas on why RX didn't work, I decided to look at the signal directly with a scope to isolate whether anything was actually reaching the MCU. I have a neat one liner for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;yes &lt;/span&gt;U | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/ttyACM0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This bash command sends alternating zeros and ones to the MCU's RX line, making scoping easy. It works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;yes U&lt;/code&gt; repeatedly sends out &lt;code&gt;U\n&lt;/code&gt; to STDOUT. &lt;code&gt;U&lt;/code&gt; is &lt;code&gt;0x55&lt;/code&gt; in hex, and &lt;code&gt;0b01010101&lt;/code&gt; in binary, which produces alternating HIGHs and LOWs on the scope once the start and stop bits are taken into account.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tr -d '\n'&lt;/code&gt; strips &lt;code&gt;\n&lt;/code&gt; from the stream, so we have &lt;code&gt;UUUUUU&lt;/code&gt; forever.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;gt; /dev/ttyACM0&lt;/code&gt; pipes the whole stream into the TTY, in this case my WCH-LinkE serial port.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I hooked up the scope, blasted &lt;code&gt;UUUUUUUUU&lt;/code&gt; into the RX line, and what I saw was a square wave, but a teeny tiny one...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxr41fuxl57ahhr2mc2e.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxr41fuxl57ahhr2mc2e.webp" title="PCB layout open on screen, scope in the middle, dev board with the programmer below." alt="Bench setup with the dev board, scope hooks on RX, and the PCB layout I kept re-checking" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Zooming in on the scope:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhr7fcoip9f6oftabl7om.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhr7fcoip9f6oftabl7om.webp" title="Min 3.20 V, max 3.38 V. Less than 200 mV of amplitude on what should have been a 0 V to 3.3 V square wave." alt="Scope showing a roughly 180 mV square wave riding on top of 3.3 V instead of swinging rail to rail" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The waveform was there. The shape was right. But it was riding on top of 3.3 V with maybe 180 mV of amplitude. The maximum was 3.38 V, the minimum 3.20 V. Something was holding the line near the rail so hard that my USB UART adapter could only pull it down by a couple hundred millivolts. This is a big clue!&lt;/p&gt;

&lt;p&gt;To rule out the wiring, I touched RX directly to ground. The line snapped to 0 V cleanly. So the connection was fine. The driver just couldn't win against whatever was holding RX high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the Schematics
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9b06218rzssfgnnjw0ox.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9b06218rzssfgnnjw0ox.webp" title="The DXL TTL front-end. Two tri-state buffers share a TX_EN gate: U4A drives RX from DATA when TX_EN is low, U4B drives DATA from TX when TX_EN is high. R14 pulls RX up, R15 pulls TX_EN down (default to listen), R16 pulls DATA up." alt="Three-block schematic of the half-duplex front-end: buffer power, the RX/TX direction-switching pair built on a 74LVC2G241, and the pull-up/pull-down resistors on RX, TX_EN, and DATA" width="800" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at the TTL DXL front-end again, the RX line goes through a tri-state buffer (&lt;code&gt;SN74LVC2G241&lt;/code&gt;) that implements half-duplex direction switching. &lt;code&gt;TX_EN&lt;/code&gt; selects the direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TX_EN&lt;/code&gt; low (default, held by a pulldown): the buffer routes &lt;code&gt;DATA&lt;/code&gt; to &lt;code&gt;RX&lt;/code&gt;. The MCU listens to the bus.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TX_EN&lt;/code&gt; high: the buffer routes &lt;code&gt;TX&lt;/code&gt; to &lt;code&gt;DATA&lt;/code&gt;. The MCU talks to the bus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is when I had my facepalm moment. I had been picturing the buffer as some kind of passive switch that just connects two wires together. But after thinking about it for a few minutes, I realized that's not the case at all.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;TX_EN&lt;/code&gt; is low, the buffer's output stage is &lt;em&gt;actively driving&lt;/em&gt; RX with whatever it sees on &lt;code&gt;DATA&lt;/code&gt;. And &lt;code&gt;DATA&lt;/code&gt; has its own 10K pullup, per the DXL TTL reference, so when nobody is talking on the bus, &lt;code&gt;DATA&lt;/code&gt; sits at 3.3 V. The buffer reads that, and pushes 3.3 V back out through its high-side MOSFET onto RX. Simplified, RX is pretty much hooked directly to 3.3 V through maybe 20 Ω of MOSFET resistance, acting as a &lt;strong&gt;strong&lt;/strong&gt; pull-up.&lt;/p&gt;

&lt;p&gt;So when I was sending bytes from the USB UART adapter into RX, I was not fighting a passive 10K pullup. I was fighting a CMOS push-pull output stage. The 74LVC2G241 has very low high-side R_DS(on) and 24 mA of drive. A typical USB UART chip's TX output is much weaker. The two formed a voltage divider, and the buffer won. The line stayed parked near 3.3 V, with my adapter pulling it down by a couple hundred millivolts every time it tried to send a 0. That was my "ripple."&lt;/p&gt;

&lt;p&gt;A hard short to ground bypasses that contention entirely, which is why poking RX with a ground clip snapped it cleanly to 0 V.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Absurd Workaround
&lt;/h2&gt;

&lt;p&gt;As absurd as it sounds, the workaround was to assert &lt;code&gt;TX_EN&lt;/code&gt;, while reading from RX. Why would I assert the &lt;em&gt;transmit&lt;/em&gt; enable when my problem was on the receive side? Because &lt;code&gt;TX_EN&lt;/code&gt; isn't really a transmit enable. From firmware's perspective it's asserted when you talk and deasserted when you listen. But electrically, it's a mux select that picks which buffer drives the bus. Treating those as the same thing is what set me up for confusion in the first place.&lt;/p&gt;

&lt;p&gt;With it held high, the &lt;code&gt;DATA&lt;/code&gt; to &lt;code&gt;RX&lt;/code&gt; buffer is disabled, RX falls back to its own pullup, and the USB UART adapter can drive it without a fight. Poking 3.3 V onto &lt;code&gt;TX_EN&lt;/code&gt; confirmed it in real time:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/2giFb58HLf4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Touching 3.3 V to TX_EN snaps the ripple into a clean rail-to-rail square wave. Release it and the line flat-lines back near 3.3 V. The buffer is the whole problem.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For those eagle-eyed viewers, yes. I'm touching the TP that says TX... But the electric trace is actually TX_EN.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xoaj8t4ovukht7syvs0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xoaj8t4ovukht7syvs0.webp" title="Top row: what the silkscreen says each test point is. Bottom row: what the test point is actually wired to. Apart from the GND endcaps and STAT, every label is wrong. The silkscreen never got updated after the nets were shuffled around during layout." alt="PCB layout view of the test point row. The top row of silkscreen labels reads GND, VIN, 3V3, EN, STAT, DBG, DATA, TX, RX, GND. The bottom row, which shows the nets actually connected to each test point, reads GND, +3V3, EN, DBG, STAT, RX, TX, TX_EN, DATA, GND. Almost none of the silkscreen labels line up with the underlying nets." width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is another &lt;a href="https://aaronqian.com/log/2026-04-03-ch32v006-dev-board-first-spin/" rel="noopener noreferrer"&gt;embarrassing yet hilarious mistake&lt;/a&gt; that got its own story, where I somehow "fixed" the issue by creating magic smoke on my board.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Fix
&lt;/h2&gt;

&lt;p&gt;The workaround is assert &lt;code&gt;TX_EN&lt;/code&gt; in firmware, however, this is not a real fix. Drive &lt;code&gt;TX_EN&lt;/code&gt; high in the bootloader's startup code and only release it when the MCU genuinely wants to talk. It works, costs nothing, and ships today. But it means UART functionality on this board now depends on the firmware running correctly.&lt;/p&gt;

&lt;p&gt;The right fix should be in hardware by adding a jumper on the RX line. Shorted means TTL mode, where RX is wired through the buffer as before. Open means plain UART mode, where RX bypasses the buffer entirely and goes straight to the MCU. More involved, requires a board respin, and adds a small amount of inconvenience for the user. But the UART works without any firmware running.&lt;/p&gt;

&lt;p&gt;For Rev. B, I'm going with the jumper.&lt;/p&gt;

&lt;p&gt;The reason is &lt;code&gt;wchisp&lt;/code&gt;. That tool, and others like it, use the UART to read and write the CH32's Option Bytes. Those operations happen &lt;em&gt;outside&lt;/em&gt; of any firmware I write. If my UART hardware depends on my firmware to function, then a misconfigured Option Byte, a half-flashed bootloader, or a chip fresh from the factory can lock me out of the recovery path. The whole appeal of UART-based programming is that it works on a bare chip with nothing running. That property has to live in hardware, not in firmware.&lt;/p&gt;

&lt;p&gt;The rule I'm taking from this: recovery-path peripherals should not depend on firmware to function. For half-duplex TTL designs specifically, that means the RX line has to be usable as a standalone UART without anything running on the MCU. A jumper is a small price to ensure hardware recoverability.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;TX_EN&lt;/code&gt; is really a line selector, and buffers &lt;strong&gt;actively drives&lt;/strong&gt; outputs. I'll remember that one for a while.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>embedded</category>
      <category>electronics</category>
      <category>debugging</category>
    </item>
    <item>
      <title>tinyboot v0.4.0 Released — The API is Stable</title>
      <dc:creator>Aaron Qian</dc:creator>
      <pubDate>Thu, 23 Apr 2026 05:10:00 +0000</pubDate>
      <link>https://dev.to/aaronqian/tinyboot-v040-released-the-api-is-stable-2h76</link>
      <guid>https://dev.to/aaronqian/tinyboot-v040-released-the-api-is-stable-2h76</guid>
      <description>&lt;p&gt;If you've been following tinyboot, you might have noticed there was no announcement for v0.3.0. That release added CH32V103 support, but things were still in flux. Crate structure was shifting, APIs were changing, the protocol was being reworked. I didn't want to write up something that'd be outdated in two weeks. I wanted to wait until the dust settled. And with v0.4.0, we have finally arrived at this point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/OpenServoCore/tinyboot" rel="noopener noreferrer"&gt;OpenServoCore/tinyboot&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Handbook: &lt;a href="https://openservocore.github.io/tinyboot" rel="noopener noreferrer"&gt;openservocore.github.io/tinyboot&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Changelog: &lt;a href="https://github.com/OpenServoCore/tinyboot/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is tinyboot
&lt;/h2&gt;

&lt;p&gt;tinyboot is a minimal bootloader for resource constrained MCUs. It is written in Rust, and it fits in 1920 bytes of system flash, leaving all user flash free for your application, except a small page (64 bytes on V003) of user flash to store boot metadata. It gives you CRC-validated firmware updates over UART with trial boot and automatic fallback to bootloader service mode when trials run out. The kind of safe OTA update story you'd expect from a much larger bootloader, squeezed into the constraints of a $0.22 MCU.&lt;/p&gt;

&lt;p&gt;It's currently focused on the CH32 family, but the core is chip-agnostic and designed to be portable. If you're interested in bringing tinyboot to another chip family, see the porting section of the &lt;a href="https://openservocore.github.io/tinyboot/porting.html" rel="noopener noreferrer"&gt;handbook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm building it as part of &lt;a href="https://dev.to/projects/open-servo-core/"&gt;OpenServoCore&lt;/a&gt;, an open-source smart servo platform. tinyboot handles the OTA updates via the existing single wire UART (Dynamixel TTL), so you don't have to tear your robot apart and open up each servo, unsolder the board just to flash a new firmware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform Support
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CH32V003&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CH32V00x&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;New in v0.4.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CH32V103&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CH32X03x&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  V00x Support
&lt;/h2&gt;

&lt;p&gt;The biggest addition in v0.4.0 is full support for the CH32V00x family: V002, V004, V005, V006, and V007. This is the release that matters most for OpenServoCore, because the OSC dev board runs on the CH32V006. Hardware validation for this release was done on the actual dev board.&lt;/p&gt;

&lt;p&gt;Getting here wasn't entirely smooth. I ran into a hardware issue where the RX line wouldn't work without driving the inverse TX_EN line, which took a scope session to figure out. But that's a story for another post. I'm just glad I was able to finally get RX to work and complete the hardware test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything Fits
&lt;/h2&gt;

&lt;p&gt;TX_EN support used to overflow system flash on some chip variants. That's not acceptable. TX_EN isn't optional for OpenServoCore. It's required for half-duplex RS-485 / DXL TTL communication.&lt;/p&gt;

&lt;p&gt;As of v0.4.0, all chip variants compile with TX_EN enabled and fit in system flash. The V103 was the outlier. It has a split flash layout, and I moved the UART transport into the second region to make it fit. That trick deserves its own post. Down the road, that second region also has enough room for a USB transport, so flashing via USB on V103 is a real possibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crate Restructure
&lt;/h2&gt;

&lt;p&gt;The previous three separate crates (&lt;code&gt;tinyboot-ch32-boot&lt;/code&gt;, &lt;code&gt;-app&lt;/code&gt;, &lt;code&gt;-hal&lt;/code&gt;) are now merged into a single &lt;code&gt;tinyboot-ch32&lt;/code&gt; crate with &lt;code&gt;boot&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;, and &lt;code&gt;hal&lt;/code&gt; modules. There's also a new &lt;code&gt;tinyboot-ch32-rt&lt;/code&gt;, a minimal runtime because &lt;code&gt;qingke-rt&lt;/code&gt; is too large for system flash.&lt;/p&gt;

&lt;p&gt;This wasn't a planned refactor. It was the natural result of continuous iteration during tinyboot's early development. I'd rather get the architecture right early by sacrificing API stability than lock in the wrong abstractions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tinyboot-ch32&lt;/code&gt; is currently git-only and not published to crates.io. It depends on a git version of &lt;code&gt;ch32-metapac&lt;/code&gt; that includes flash fixes for the V00x family that haven't been released yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protocol and API Changes
&lt;/h2&gt;

&lt;p&gt;The wire protocol now uses 24-bit addresses instead of 32-bit, freeing the fourth byte for per-command flags. 24 bits is still 16MB of addressable space, more than enough for these MCUs. The standalone &lt;code&gt;Flush&lt;/code&gt; command is gone; it's now a flag on the final &lt;code&gt;Write&lt;/code&gt;. Cleaner on the wire, simpler in the dispatcher, improves the developer experience, and best of all no size increase due to zero-cost abstractions via Rust's union types.&lt;/p&gt;

&lt;p&gt;On the API side: &lt;code&gt;BootMode&lt;/code&gt; became &lt;code&gt;RunMode&lt;/code&gt; to separate the concept of boot flash region selection vs bootloader run mode (handoff or service). &lt;code&gt;BootClient&lt;/code&gt; was also removed after the crates merge. Boolean parameters in the public API became semantic enums (&lt;code&gt;Duplex::Half&lt;/code&gt; instead of &lt;code&gt;half_duplex: true&lt;/code&gt;). Last but not least, flash lock/unlock is now scoped per operation instead of manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fixes
&lt;/h2&gt;

&lt;p&gt;Two nasty half-duplex communication bugs, both found during hardware validation on the dev board:&lt;/p&gt;

&lt;p&gt;The dispatcher wasn't flushing the transport after sending a response. On a full-duplex UART you'd never notice, but on RS-485 / DXL TTL half-duplex the data just sits in the buffer and never goes out on the wire.&lt;/p&gt;

&lt;p&gt;The ring buffer wasn't resetting its head/tail pointers after flushing buffered writes to flash. The buffer was logically empty but the pointers were advanced, so subsequent writes would eventually wrap incorrectly. I didn't notice this before because I didn't do back-to-back &lt;code&gt;flash&lt;/code&gt; commands with the &lt;code&gt;tinyboot&lt;/code&gt; CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docs Overhaul
&lt;/h2&gt;

&lt;p&gt;Documentation has been completely rewritten for users instead of maintainers, and a user handbook has been created. When the architecture is changing every release, writing user-facing docs is a losing game. Now that things have stabilized, it actually makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Stable" Means
&lt;/h2&gt;

&lt;p&gt;To be clear, tinyboot is not production-grade. But as of v0.4.0, it's stable in the ways that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Architecture stable&lt;/strong&gt;: no more big crate restructures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol stable&lt;/strong&gt;: no more wire format changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features stable&lt;/strong&gt;: all core features compile and fit on all supported chips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior stable&lt;/strong&gt;: no obvious bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means I'm shifting focus. tinyboot satisfies all of OpenServoCore's needs, and my attention is moving to rewriting the OSC firmware. I'll still maintain tinyboot, and issues and PRs are welcome, but active feature development is pausing for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;USB transport for V103 (the second flash region has room).&lt;/li&gt;
&lt;li&gt;CH32X03x support eventually.&lt;/li&gt;
&lt;li&gt;But the immediate priority is OpenServoCore firmware.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>embedded</category>
      <category>bootloader</category>
      <category>ch32</category>
    </item>
  </channel>
</rss>
