<?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>Forem: Aaron Qian</title>
    <description>The latest articles on Forem by Aaron Qian (@aq1018).</description>
    <link>https://forem.com/aq1018</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>Forem: Aaron Qian</title>
      <link>https://forem.com/aq1018</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/aq1018"/>
    <language>en</language>
    <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://forem.com/aq1018/my-rx-line-was-stuck-high-and-txen-was-the-fix-4hab</link>
      <guid>https://forem.com/aq1018/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://forem.com/aq1018/tinyboot-v040-released-the-api-is-stable-2h76</link>
      <guid>https://forem.com/aq1018/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>
