diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd54108..8cc2195 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,8 @@ jobs: run: cd ch32 && cargo +nightly clippy --no-default-features --features ${{ matrix.chip }} --target ../examples/ch32/v003/riscv32ec-unknown-none-elf.json -Zbuild-std=core -Zjson-target-spec -- -D warnings - name: Build libraries run: cd ch32 && cargo +nightly build --no-default-features --features ${{ matrix.chip }} --target ../examples/ch32/v003/riscv32ec-unknown-none-elf.json -Zbuild-std=core -Zjson-target-spec --release + - name: Clippy libraries (half-duplex) + run: cd ch32 && cargo +nightly clippy --no-default-features --features ${{ matrix.chip }},half-duplex --target ../examples/ch32/v003/riscv32ec-unknown-none-elf.json -Zbuild-std=core -Zjson-target-spec -- -D warnings - name: Doc libraries run: cd ch32 && cargo +nightly doc --no-default-features --features ${{ matrix.chip }} --target ../examples/ch32/v003/riscv32ec-unknown-none-elf.json -Zbuild-std=core -Zjson-target-spec --no-deps - name: Fmt examples @@ -95,6 +97,8 @@ jobs: run: cd ch32 && cargo clippy --no-default-features --features ${{ matrix.chip }} --target riscv32imc-unknown-none-elf -- -D warnings - name: Build libraries run: cd ch32 && cargo build --no-default-features --features ${{ matrix.chip }} --target riscv32imc-unknown-none-elf --release + - name: Clippy libraries (half-duplex) + run: cd ch32 && cargo clippy --no-default-features --features ${{ matrix.chip }},half-duplex --target riscv32imc-unknown-none-elf -- -D warnings - name: Fmt examples run: cd examples/ch32/v103 && cargo fmt --all -- --check - name: Clippy boot (system-flash) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb815f8..2e820f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4. ## [Unreleased] +### Changed + +- **Breaking:** `tinyboot-ch32`: duplex selection moved from runtime config to compile time. The `Duplex` enum and `UsartConfig::duplex` are gone; enable the new `half-duplex` cargo feature for single-wire operation (full duplex is the default, and `rx_pull` only exists without the feature). Half-duplex now uses a shared-bus drive discipline: the wire parks open-drain while idle and drives push-pull only around writes, so an idle node never contends with the wire's other talkers — **an external pull-up on the wire is now a hardware requirement** (it owns the idle mark level). Costs ~130 bytes over full duplex; full-duplex binaries are byte-identical to before. + +### Fixed + +- `tinyboot-ch32`: half-duplex raises HDSEL before TE — the reverse order latched the TX output LOW until the first own transmission, clamping the wire from init (measured on CH32V006). + ## [tinyboot-ch32-rt 0.4.1] - 2026-05-08 ### Fixed diff --git a/ch32/Cargo.toml b/ch32/Cargo.toml index 88d9c7d..8cb7596 100644 --- a/ch32/Cargo.toml +++ b/ch32/Cargo.toml @@ -41,6 +41,10 @@ ch32v103r8t6 = ["ch32-metapac/ch32v103r8t6"] system-flash = [] +# Single wire (HDSEL): open-drain idle park, push-pull around writes. +# Needs a pull-up on the wire — see docs/transports.md. +half-duplex = [] + [workspace] members = [".", "rt"] default-members = [".", "rt"] diff --git a/ch32/README.md b/ch32/README.md index 6a0f134..f28f7cb 100644 --- a/ch32/README.md +++ b/ch32/README.md @@ -20,7 +20,7 @@ tinyboot-ch32-rt = "0.4" # optional, bootloader-only; on crates.io | Module | For | What it provides | | ---------- | -------------------- | ------------------------------------------------------------------------------------------------ | -| `boot` | Bootloader binaries | `run()`, `BootCtl`, USART transport (`Usart`, `UsartConfig`, `BaudRate`, `Duplex`, `TxEnConfig`) | +| `boot` | Bootloader binaries | `run()`, `BootCtl`, USART transport (`Usart`, `UsartConfig`, `BaudRate`, `TxEnConfig`) | | `app` | Application binaries | `new_app()`, `App`, `BootCtl`, the `tinyboot_core::app` types | | `hal` | Both | `flash`, `gpio`, `usart`, `afio`, `rcc`, `pfic`, `iwdg`; auto-generated `Pin` and `UsartMapping` | | `platform` | (internal) | `tinyboot_core::traits` impls for Storage, Transport, BootCtl, BootMetaStore | @@ -38,7 +38,6 @@ use tinyboot_ch32::boot::prelude::*; #[unsafe(export_name = "main")] fn main() -> ! { let transport = Usart::new(&UsartConfig { - duplex: Duplex::Full, baud: BaudRate::B115200, pclk: 8_000_000, mapping: UsartMapping::Usart1Remap0, diff --git a/ch32/src/boot.rs b/ch32/src/boot.rs index 7c17ec6..80de776 100644 --- a/ch32/src/boot.rs +++ b/ch32/src/boot.rs @@ -2,7 +2,7 @@ use crate::platform::{BootMetaStore, Storage}; -pub use crate::platform::{BaudRate, BootCtl, Duplex, TxEnConfig, Usart, UsartConfig}; +pub use crate::platform::{BaudRate, BootCtl, TxEnConfig, Usart, UsartConfig}; pub use crate::hal::gpio::{Level, Pull}; pub use crate::hal::{Pin, UsartMapping}; @@ -12,7 +12,7 @@ pub use tinyboot_core::{boot_version, pkg_version}; /// Common imports for bootloader binaries. pub mod prelude { pub use super::{ - BaudRate, BootCtl, Duplex, Level, Pin, Pull, TxEnConfig, Usart, UsartConfig, UsartMapping, + BaudRate, BootCtl, Level, Pin, Pull, TxEnConfig, Usart, UsartConfig, UsartMapping, }; } diff --git a/ch32/src/hal/usart/usart_common.rs b/ch32/src/hal/usart/usart_common.rs index cc9c0e1..8ae13d1 100644 --- a/ch32/src/hal/usart/usart_common.rs +++ b/ch32/src/hal/usart/usart_common.rs @@ -2,16 +2,18 @@ pub use ch32_metapac::usart::Usart as Regs; #[inline(always)] pub fn init(r: Regs, pclk: u32, baud: u32, half_duplex: bool) { + // HDSEL before TE — the reverse order latches the TX output LOW until + // the first own transmission, clamping the wire from init. + if half_duplex { + r.ctlr3().write(|w| w.set_hdsel(true)); + } + // CTLR1 default (M=0, PCE=0, STOP=00) gives 8N1; just set TE+RE. r.ctlr1().write(|w| { w.set_te(true); w.set_re(true); }); - if half_duplex { - r.ctlr3().write(|w| w.set_hdsel(true)); - } - let brr = (pclk + baud / 2) / baud; r.brr().write_value(ch32_metapac::usart::regs::Brr(brr)); diff --git a/ch32/src/platform/mod.rs b/ch32/src/platform/mod.rs index 95ec3de..a45fe5f 100644 --- a/ch32/src/platform/mod.rs +++ b/ch32/src/platform/mod.rs @@ -8,4 +8,4 @@ pub use crate::hal::{Pin, UsartMapping}; pub use boot_ctl::BootCtl; pub use boot_meta_store::BootMetaStore; pub use storage::Storage; -pub use transport::usart::{BaudRate, Duplex, TxEnConfig, Usart, UsartConfig}; +pub use transport::usart::{BaudRate, TxEnConfig, Usart, UsartConfig}; diff --git a/ch32/src/platform/transport/usart/mod.rs b/ch32/src/platform/transport/usart/mod.rs index 409ddbe..48bec1b 100644 --- a/ch32/src/platform/transport/usart/mod.rs +++ b/ch32/src/platform/transport/usart/mod.rs @@ -2,17 +2,12 @@ use core::convert::Infallible; use embedded_io::ErrorType; -use crate::hal::gpio::{PinMode, Pull}; +use crate::hal::gpio::PinMode; +#[cfg(not(feature = "half-duplex"))] +use crate::hal::gpio::Pull; use crate::hal::{Pin, UsartMapping}; use crate::hal::{afio, gpio, rcc, usart}; -pub enum Duplex { - /// Half-duplex (single wire, RS-485). - Half, - /// Full-duplex (separate TX/RX). - Full, -} - #[derive(Copy, Clone)] #[repr(u32)] pub enum BaudRate { @@ -41,7 +36,6 @@ pub struct TxEnConfig { } pub struct UsartConfig { - pub duplex: Duplex, pub baud: BaudRate, /// USART peripheral clock (Hz). Used for BRR = pclk / baud. /// @@ -51,6 +45,8 @@ pub struct UsartConfig { /// X0xx: HSI 48MHz / HPRE 1 = 48_000_000 pub pclk: u32, pub mapping: UsartMapping, + /// RX pin bias (full duplex only). + #[cfg(not(feature = "half-duplex"))] pub rx_pull: Pull, /// Optional RS-485 DE/RE pin. pub tx_en: Option, @@ -59,6 +55,9 @@ pub struct UsartConfig { pub struct Usart { regs: crate::hal::usart::Regs, tx_en: Option, + /// The single wire: push-pull while driving, open-drain parked. + #[cfg(feature = "half-duplex")] + tx_pin: Pin, } impl tinyboot_core::traits::Transport for Usart {} @@ -71,7 +70,6 @@ impl Usart { let tx_pin = config.mapping.tx_pin(); let rx_pin = config.mapping.rx_pin(); let regs = config.mapping.regs(); - let half_duplex = matches!(config.duplex, Duplex::Half); let usart_n = config.mapping.peripheral_index(); @@ -89,8 +87,13 @@ impl Usart { afio::set_usart_remap(usart_n, config.mapping.remap_value()); - gpio::configure(tx_pin, PinMode::AF_PUSH_PULL); - if !half_duplex { + // Park the single wire open-drain: RX hears through the pin, the + // bus pull-up holds mark; writes flip to push-pull. + #[cfg(feature = "half-duplex")] + gpio::configure(tx_pin, PinMode::AF_OPEN_DRAIN); + #[cfg(not(feature = "half-duplex"))] + { + gpio::configure(tx_pin, PinMode::AF_PUSH_PULL); gpio::configure(rx_pin, PinMode::input_pull(config.rx_pull)); } @@ -101,16 +104,25 @@ impl Usart { gpio::set_level(tx_en.pin, invert(tx_en.tx_level)); } - usart::init(regs, config.pclk, config.baud as u32, half_duplex); + usart::init( + regs, + config.pclk, + config.baud as u32, + cfg!(feature = "half-duplex"), + ); Usart { regs, tx_en: config.tx_en, + #[cfg(feature = "half-duplex")] + tx_pin, } } #[inline(always)] fn set_tx_mode(&self) { + #[cfg(feature = "half-duplex")] + gpio::configure(self.tx_pin, PinMode::AF_PUSH_PULL); if let Some(ref tx_en) = self.tx_en { gpio::set_level(tx_en.pin, tx_en.tx_level); } @@ -118,6 +130,8 @@ impl Usart { #[inline(always)] fn set_rx_mode(&self) { + #[cfg(feature = "half-duplex")] + gpio::configure(self.tx_pin, PinMode::AF_OPEN_DRAIN); if let Some(ref tx_en) = self.tx_en { gpio::set_level(tx_en.pin, invert(tx_en.tx_level)); } diff --git a/docs/examples.md b/docs/examples.md index d751f1c..b1d5421 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -45,7 +45,7 @@ The example workspaces are built across a CI matrix: multiple chip variants × s 1. Copy the example that matches your chip (e.g. `examples/ch32/v003/`) to a new directory. 2. In the `boot/Cargo.toml`, remove the extra variant features you don't need. Leave one, set as the default. 3. Pick a flash mode. Delete the `memory_x/` file you don't need, and simplify `build.rs` to just copy the remaining one. -4. In `src/main.rs`, change the UART config (pins, baud, duplex, tx_en) to match your board. +4. In `src/main.rs`, change the UART config (pins, baud, tx_en — and the `half-duplex` cargo feature for single-wire boards) to match your board. 5. Do the same for `app/` — match the UART config, adjust your pins. That gives you a minimal, single-purpose workspace with none of the CI scaffolding. diff --git a/docs/transports.md b/docs/transports.md index 6e17d30..0a3e5b1 100644 --- a/docs/transports.md +++ b/docs/transports.md @@ -2,20 +2,19 @@ The tinyboot protocol runs over any `embedded_io::Read + Write` stream. The CH32 implementation ships a USART transport configured via two **independent** axes: -- **`duplex`** — controls the **MCU's** pin arrangement. - - `Full` — separate RX and TX pins. - - `Half` — RX is muxed onto the TX pin; the MCU uses a single wire. -- **`tx_en`** — optional direction pin for an **external** buffer (RS-485 transceiver, etc.). Independent of `duplex`. Driven to the configured `tx_level` around writes, to the inverse while idle / reading. +- **the `half-duplex` cargo feature** — controls the **MCU's** pin arrangement, at compile time (a bootloader binary serves one board, so there is no runtime switch to pay for). + - off (default) — full duplex: separate RX and TX pins. + - on — RX is muxed onto the TX pin (HDSEL); the MCU uses a single wire, parked open-drain while idle. +- **`tx_en`** — optional direction pin for an **external** buffer (RS-485 transceiver, etc.). Independent of the duplex mode. Driven to the configured `tx_level` around writes, to the inverse while idle / reading. Combining these gives four useful setups. Pick whichever matches your board. ## Setup 1: full-duplex UART (two wires) -Regular UART — separate TX and RX to the host. +Regular UART — separate TX and RX to the host. This is the default (no feature). ```rust Usart::new(&UsartConfig { - duplex: Duplex::Full, tx_en: None, .. }); @@ -23,25 +22,34 @@ Usart::new(&UsartConfig { `rx_pull: Pull::Up` if the RX line can float when the host is disconnected; `Pull::None` if an external pull-up is already present. -## Setup 2: single-wire UART (MCU-level half duplex) +## Setup 2: single-wire UART (the `half-duplex` feature) -The MCU muxes RX onto the TX pin — one wire to the host, no external buffer. Useful when both ends speak half duplex directly (some probes, some DXL servo chains where the MCU is the sole driver on its segment). +The MCU muxes RX onto the TX pin — one wire to the host, no external buffer. Useful when both ends speak half duplex directly: probes, DXL-style servo chains, any shared single-wire bus. + +```toml +tinyboot-ch32 = { version = "...", features = ["", "half-duplex"] } +``` ```rust Usart::new(&UsartConfig { - duplex: Duplex::Half, + // no rx_pull: the mapping's RX pin is unused in half duplex tx_en: None, .. }); ``` +**Hardware requirement: the wire needs an external pull-up.** The driver parks the pin **open-drain** while idle or listening and flips it to push-pull only around its own writes, so between frames nothing holds the line at mark except the pull-up. Without one the wire floats and the receiver sees noise and framing errors — garbage bytes the protocol has to sync past. 4.7–10 kΩ to the bus voltage is typical; go stiffer for high baud rates or long, capacitive buses. + +The open-drain park is what makes the single wire **shareable**: an idle node never drives the line, so several half-duplex devices (and the host) can sit on one bus without contention. The driver also raises HDSEL before enabling the transmitter — the reverse order latches the TX output low until the first own transmission, which would clamp the whole bus from init to the first reply. + +The feature costs ~130 bytes of flash over full duplex, which is why it's a compile-time switch: a full-duplex boot pays nothing (and on the CH32V003's 1920-byte system flash, has nothing to spare). + ## Setup 3: full-duplex UART + external half-duplex buffer (RS-485 / DXL TTL) -This is the OpenServoCore hardware style: the MCU runs regular full-duplex UART to a hardware transceiver (MAX485, 74LVC2G241, etc.), and `tx_en` drives the transceiver's direction pin so its output stage only drives the bus while the MCU is transmitting. +The MCU runs regular full-duplex UART to a hardware transceiver (MAX485, 74LVC2G241, etc.), and `tx_en` drives the transceiver's direction pin so its output stage only drives the bus while the MCU is transmitting. ```rust Usart::new(&UsartConfig { - duplex: Duplex::Full, tx_en: Some(TxEnConfig { pin: Pin::PC2, tx_level: Level::High, // level that puts the transceiver in TX mode @@ -57,16 +65,17 @@ Usart::new(&UsartConfig { ## Setup 4: single-wire UART + external buffer -MCU half-duplex (muxed RX/TX) **and** a direction-controlled external buffer. Valid if your board puts a buffer in front of the MCU's single wire and still needs direction switching. +MCU half-duplex (muxed RX/TX, the `half-duplex` feature) **and** a direction-controlled external buffer. Valid if your board puts a non-auto-direction buffer or level shifter in front of the MCU's single wire — the one pin carries both directions, and `tx_en` tells the buffer which way to point. ```rust Usart::new(&UsartConfig { - duplex: Duplex::Half, tx_en: Some(TxEnConfig { pin: Pin::PC2, tx_level: Level::High }), .. }); ``` +The open-drain idle park applies in this setup too — it follows the `half-duplex` feature, independent of `tx_en` — so the stub between the pin and the buffer input needs a pull-up (or a transceiver with internal biasing) to keep the buffer's input at mark while the MCU listens. + ## What `tx_en` actually does When configured, the driver toggles the direction pin around every frame: @@ -90,7 +99,7 @@ On a single-wire bus where the host's TX and RX are also tied to the data line `UsartMapping` picks the AFIO remap and selects which physical pins carry TX / RX. Available mappings are codegen'd per chip — check the generated `UsartMapping` enum in `tinyboot-ch32`, and cross-reference against the USART / AFIO sections of your chip's datasheet for the pin assignments. -In `Duplex::Half`, only the TX pin is used; the RX pin of the mapping is unused. +With the `half-duplex` feature, only the TX pin is used; the RX pin of the mapping is unused (and `UsartConfig` has no `rx_pull` field). ## Matching the app side @@ -99,7 +108,7 @@ The app's USART configuration must match the bootloader's: - Same USART instance (e.g. USART1). - Same pins / remap. - Same baud rate. -- Same `duplex` mode. +- Same wire arrangement (the bootloader's `half-duplex` feature ↔ the app's single-wire vs two-wire UART setup). - Same `tx_en` pin and `tx_level` (if used). If any of these differ, the app can still run — but it won't be able to receive `Reset` or `Info` over the bus, so remote bootloader entry won't work. See the [app integration guide](app-integration.md) for the app-side wiring. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ba7d8c5..15bb093 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -28,6 +28,7 @@ Something in the UART chain isn't right. Work through these in order: - **`rx_pull`** — floating RX lines need `Pull::Up`; externally pulled-up lines should use `Pull::None`. - **RS-485 / DXL TTL** — a DE/RE pin must be configured via `TxEnConfig`, with `tx_level` matching the transceiver's DE polarity. - **Half-duplex contention** — on some boards the programmer's TX driver and the MCU's TX driver both reach MCU_RX. Flipping `tx_level` (so the MCU's side is tri-stated while idle) often resolves it. See the [transports guide](transports.md). +- **Half-duplex missing pull-up** — the `half-duplex` feature parks the pin open-drain while idle, so the wire needs an external pull-up to hold mark between frames. A floating wire reads as noise and framing errors. See the [transports guide](transports.md). --- diff --git a/examples/ch32/v003/boot/src/main.rs b/examples/ch32/v003/boot/src/main.rs index 7b53983..fd95992 100644 --- a/examples/ch32/v003/boot/src/main.rs +++ b/examples/ch32/v003/boot/src/main.rs @@ -24,11 +24,9 @@ fn main() -> ! { // // rx_pull: Pull::Up for floating lines; Pull::None if externally pulled up. // - // RS-485 half-duplex with DE pin: - // duplex: Duplex::Half, - // tx_en: Some(TxEnConfig { pin: Pin::PC2, tx_level: Level::High }), + // Single-wire half duplex: enable tinyboot-ch32's `half-duplex` + // feature (drops `rx_pull`; the wire needs a pull-up). let transport = Usart::new(&UsartConfig { - duplex: Duplex::Full, baud: BaudRate::B115200, pclk: 8_000_000, mapping: UsartMapping::Usart1Remap0, diff --git a/examples/ch32/v00x/boot/src/main.rs b/examples/ch32/v00x/boot/src/main.rs index becf5df..ea44c66 100644 --- a/examples/ch32/v00x/boot/src/main.rs +++ b/examples/ch32/v00x/boot/src/main.rs @@ -51,11 +51,9 @@ fn main() -> ! { // // rx_pull: Pull::Up for floating lines; Pull::None if externally pulled up. // - // RS-485 half-duplex with DE pin: - // duplex: Duplex::Half, - // tx_en: Some(TxEnConfig { pin: Pin::PC2, tx_level: Level::High }), + // Single-wire half duplex: enable tinyboot-ch32's `half-duplex` + // feature (drops `rx_pull`; the wire needs a pull-up). let transport = Usart::new(&UsartConfig { - duplex: Duplex::Full, baud: BaudRate::B3000000, pclk: 48_000_000, mapping: UsartMapping::Usart1Remap3, diff --git a/examples/ch32/v103/boot/src/main.rs b/examples/ch32/v103/boot/src/main.rs index 39a7ba7..479f503 100644 --- a/examples/ch32/v103/boot/src/main.rs +++ b/examples/ch32/v103/boot/src/main.rs @@ -21,7 +21,6 @@ fn main() -> ! { // Remap0 (default): TX=PA9, RX=PA10 // Remap1: TX=PB6, RX=PB7 let transport = Usart::new(&UsartConfig { - duplex: Duplex::Full, baud: BaudRate::B115200, pclk: 8_000_000, mapping: UsartMapping::Usart1Remap0,