Skip to content

Commit ee7563c

Browse files
committed
make half-duplex safe on a shared wire: hdsel before te, open-drain park with per-write push-pull
1 parent 8d024cd commit ee7563c

5 files changed

Lines changed: 38 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4.
44

55
## [Unreleased]
66

7+
### Fixed
8+
9+
- `tinyboot-ch32`: half-duplex is now safe on a shared (multi-drop) wire. `usart::init` sets HDSEL before TE — the reverse order latched the half-duplex TX output LOW until the first own transmission, clamping the bus from init (measured on CH32V006). The single-wire pin now parks AF open-drain (the bus pull-up owns idle) and flips to push-pull only around writes, so an idle node never drives against other talkers.
10+
711
## [tinyboot-ch32-rt 0.4.1] - 2026-05-08
812

913
### Fixed

‎ch32/src/hal/usart/usart_common.rs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ pub use ch32_metapac::usart::Usart as Regs;
22

33
#[inline(always)]
44
pub fn init(r: Regs, pclk: u32, baud: u32, half_duplex: bool) {
5+
// HDSEL strictly before TE: enabling the transmitter first latches the
6+
// half-duplex TX output LOW until the first own transmission, which
7+
// clamps a shared wire even through an open-drain pin (measured on
8+
// CH32V006: the line sat low from init and rose the moment the pin
9+
// left AF mode).
10+
if half_duplex {
11+
r.ctlr3().write(|w| w.set_hdsel(true));
12+
}
13+
514
// CTLR1 default (M=0, PCE=0, STOP=00) gives 8N1; just set TE+RE.
615
r.ctlr1().write(|w| {
716
w.set_te(true);
817
w.set_re(true);
918
});
1019

11-
if half_duplex {
12-
r.ctlr3().write(|w| w.set_hdsel(true));
13-
}
14-
1520
let brr = (pclk + baud / 2) / baud;
1621
r.brr().write_value(ch32_metapac::usart::regs::Brr(brr));
1722

‎ch32/src/platform/transport/usart/mod.rs‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ pub struct UsartConfig {
5959
pub struct Usart {
6060
regs: crate::hal::usart::Regs,
6161
tx_en: Option<TxEnConfig>,
62+
/// Half-duplex: the single-wire pin, CNF-flipped per write — push-pull
63+
/// while driving, open-drain parked so an idle node never fights the
64+
/// wire's other talkers (the bus pull-up owns the idle level).
65+
drive_pin: Option<Pin>,
6266
}
6367

6468
impl tinyboot_core::traits::Transport for Usart {}
@@ -89,8 +93,12 @@ impl Usart {
8993

9094
afio::set_usart_remap(usart_n, config.mapping.remap_value());
9195

92-
gpio::configure(tx_pin, PinMode::AF_PUSH_PULL);
93-
if !half_duplex {
96+
if half_duplex {
97+
// Park the single wire open-drain: TX idles mark (released) and
98+
// RX keeps hearing through the pin; writes flip to push-pull.
99+
gpio::configure(tx_pin, PinMode::AF_OPEN_DRAIN);
100+
} else {
101+
gpio::configure(tx_pin, PinMode::AF_PUSH_PULL);
94102
gpio::configure(rx_pin, PinMode::input_pull(config.rx_pull));
95103
}
96104

@@ -106,18 +114,25 @@ impl Usart {
106114
Usart {
107115
regs,
108116
tx_en: config.tx_en,
117+
drive_pin: half_duplex.then_some(tx_pin),
109118
}
110119
}
111120

112121
#[inline(always)]
113122
fn set_tx_mode(&self) {
123+
if let Some(pin) = self.drive_pin {
124+
gpio::configure(pin, PinMode::AF_PUSH_PULL);
125+
}
114126
if let Some(ref tx_en) = self.tx_en {
115127
gpio::set_level(tx_en.pin, tx_en.tx_level);
116128
}
117129
}
118130

119131
#[inline(always)]
120132
fn set_rx_mode(&self) {
133+
if let Some(pin) = self.drive_pin {
134+
gpio::configure(pin, PinMode::AF_OPEN_DRAIN);
135+
}
121136
if let Some(ref tx_en) = self.tx_en {
122137
gpio::set_level(tx_en.pin, invert(tx_en.tx_level));
123138
}

‎docs/transports.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Usart::new(&UsartConfig {
2525

2626
## Setup 2: single-wire UART (MCU-level half duplex)
2727

28-
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).
28+
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.
2929

3030
```rust
3131
Usart::new(&UsartConfig {
@@ -35,6 +35,10 @@ Usart::new(&UsartConfig {
3535
});
3636
```
3737

38+
**Hardware requirement: the wire needs an external pull-up.** In `Duplex::Half` 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.
39+
40+
This discipline 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.
41+
3842
## Setup 3: full-duplex UART + external half-duplex buffer (RS-485 / DXL TTL)
3943

4044
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.
@@ -67,6 +71,8 @@ Usart::new(&UsartConfig {
6771
});
6872
```
6973

74+
The open-drain idle park applies in this setup too — it's the `Duplex::Half` discipline, 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.
75+
7076
## What `tx_en` actually does
7177

7278
When configured, the driver toggles the direction pin around every frame:

‎docs/troubleshooting.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Something in the UART chain isn't right. Work through these in order:
2828
- **`rx_pull`** — floating RX lines need `Pull::Up`; externally pulled-up lines should use `Pull::None`.
2929
- **RS-485 / DXL TTL** — a DE/RE pin must be configured via `TxEnConfig`, with `tx_level` matching the transceiver's DE polarity.
3030
- **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).
31+
- **Half-duplex missing pull-up** — `Duplex::Half` 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).
3132

3233
---
3334

0 commit comments

Comments
 (0)