From e81b4005783463315cca56bfe75f15bcdd357e64 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 20 Oct 2026 02:38:46 -0700 Subject: [PATCH] make ch32 bootloaders and apps addressable by chip unique id --- .github/workflows/ci.yml | 2 ++ CHANGELOG.md | 2 ++ ch32/src/app.rs | 40 +++++++++++++++------ ch32/src/boot.rs | 22 +++++++++--- ch32/src/hal/esig.rs | 11 ++++++ ch32/src/hal/mod.rs | 1 + ch32/src/hal/usart/usart_common.rs | 22 ++++++++++++ ch32/src/platform/transport/usart/mod.rs | 7 +++- examples/ch32/v003/app/src/main.rs | 13 ++++++- examples/ch32/v003/boot/src/bin/heaviest.rs | 35 ++++++++++++++++++ examples/ch32/v003/boot/src/main.rs | 5 ++- examples/ch32/v00x/app/src/main.rs | 13 ++++++- examples/ch32/v00x/boot/src/main.rs | 5 ++- examples/ch32/v103/app/src/main.rs | 25 +++++++++---- examples/ch32/v103/boot/src/main.rs | 5 ++- 15 files changed, 179 insertions(+), 29 deletions(-) create mode 100644 ch32/src/hal/esig.rs create mode 100644 examples/ch32/v003/boot/src/bin/heaviest.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7626843..a00f34b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,8 @@ jobs: run: cd examples/ch32/v003 && cargo +nightly clippy -p boot --no-default-features --features "${{ matrix.chip }},system-flash" -- -D warnings - name: Build boot (system-flash) run: cd examples/ch32/v003 && cargo +nightly build -p boot --no-default-features --features "${{ matrix.chip }},system-flash" --release + - name: Size gate (every option at once fits system flash) + run: cd examples/ch32/v003 && cargo +nightly build -p boot --bin heaviest --no-default-features --features "${{ matrix.chip }},system-flash" --release - name: Clippy boot (user-flash) run: cd examples/ch32/v003 && cargo +nightly clippy -p boot --no-default-features --features "${{ matrix.chip }},user-flash" -- -D warnings - name: Build boot (user-flash) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6bd828..48d69ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4. ### Added +- Addressable devices for a shared bus. `boot::run` takes the bus addressing: `Addressable::new(chip_uid)` keeps the bootloader silent until a host selects it by the chip's 96-bit unique id, `Unaddressed` answers every frame as before. `app::new_addressable_app(ctl, wait_bytes)` does the same in the app. `chip_uid` reads the ESIG unique id, and `Usart` implements `Transport::wait_bytes` from its baud divisor. - `boot_version!(compat = "name")` and `app_version!(compat = "name")` (re-exported from core): a bootloader with a compatibility key accepts only app images with the same key. The V00x and V103 examples set one. - The bootloader runs its USART at the baud stored in boot metadata, falling back to `UsartConfig::baud` when nothing is stored or `pclk` cannot reach the rate (divisor below 16). Divisors are computed at build time, so there is no runtime divide; the CH32V003 example bootloader grows by 64 bytes. - `app::BaudRate` re-export for `App::set_baud` / `App::baud`. @@ -26,6 +27,7 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4. ### Changed +- **Breaking:** `boot::run` and `boot::run_with_payload` take a third argument, the bus addressing (`Unaddressed` for the previous behavior). - **Breaking:** `BaudRate` is `tinyboot_protocol::BaudRate`, a `u8` index enum with `hz()`, instead of a `u32` enum whose discriminant was the rate. - **Breaking:** the USART wire and DE pin are chosen by type, and the `half-duplex` cargo feature is gone. `UsartConfig` takes `wire: FullDuplex { rx_pull, drive }` (`PushPull` / `OpenDrain`) or `HalfDuplex { drive }` (`PushPullWhileSending` / `OpenDrain`), and `tx_en: NoTxEn` or `TxEn { pin, tx_level }` (was `Option`). A bootloader builds only the pin switching its setup needs, with each switch precomputed in `Usart::new` (one BSHR store for the DE pin, one CNF0 bit flip for push-pull while sending), and `HalfDuplex { drive: PushPull }` does not compile. Bootloaders shrink: CH32V003 example 40 bytes, CH32V00x 48, CH32V103 68, a half-duplex CH32V006 board 164. - The DE pin's GPIO clock is enabled in `Usart::new`'s single RCC write, and `set_boot_mode` is shared by the hand-off and Reset paths. diff --git a/ch32/src/app.rs b/ch32/src/app.rs index 2815ad9..ae7060f 100644 --- a/ch32/src/app.rs +++ b/ch32/src/app.rs @@ -4,30 +4,48 @@ use crate::hal::flash; use crate::platform::BootMetaStore; pub use crate::hal::Pin; +pub use crate::hal::esig::uid as chip_uid; pub use crate::hal::gpio::Level; pub use crate::platform::BootCtl; pub use tinyboot_core::BaudRate; +pub use tinyboot_core::addressing::{Addressable, Addressing, Unaddressed}; pub use tinyboot_core::app::{App, AppConfig}; pub use tinyboot_core::traits; pub use tinyboot_core::{app_version, pkg_version}; /// Create an [`App`] configured for CH32 hardware. pub fn new_app(ctl: BootCtl) -> App { + App::new(config(), ctl, BootMetaStore::default()) +} + +/// Create an addressable [`App`]: silent on a shared bus until a host +/// selects it by the chip's unique id. `wait_bytes(n)` waits about `n` +/// byte-times at the bus baud, to stagger `Select` replies. +pub fn new_addressable_app( + ctl: BootCtl, + wait_bytes: fn(u8), +) -> App { + App::with_addressing( + config(), + ctl, + BootMetaStore::default(), + Addressable::new(chip_uid), + wait_bytes, + ) +} + +fn config() -> AppConfig { unsafe extern "C" { static __tb_boot_tail: tinyboot_core::ImageTail; static __tb_app_capacity: u8; } let boot_tail = &raw const __tb_boot_tail; let app_capacity = unsafe { &__tb_app_capacity as *const u8 as u32 }; - App::new( - AppConfig { - capacity: app_capacity, - erase_size: flash::PAGE_SIZE as u16, - boot_version: unsafe { (*boot_tail).version }, - boot_compat: unsafe { (*boot_tail).compat }, - app_version: tinyboot_core::tinyboot_version(), - }, - ctl, - BootMetaStore::default(), - ) + AppConfig { + capacity: app_capacity, + erase_size: flash::PAGE_SIZE as u16, + boot_version: unsafe { (*boot_tail).version }, + boot_compat: unsafe { (*boot_tail).compat }, + app_version: tinyboot_core::tinyboot_version(), + } } diff --git a/ch32/src/boot.rs b/ch32/src/boot.rs index a60004f..b1fe8b8 100644 --- a/ch32/src/boot.rs +++ b/ch32/src/boot.rs @@ -7,17 +7,20 @@ pub use crate::platform::{ PushPullWhileSending, TxEn, TxEnable, Usart, UsartConfig, Wire, }; +pub use crate::hal::esig::uid as chip_uid; pub use crate::hal::gpio::{Level, Pull}; pub use crate::hal::{Pin, UsartMapping}; pub use tinyboot_core::Platform; +pub use tinyboot_core::addressing::{Addressable, Addressing, Unaddressed}; use tinyboot_core::frame::{DEFAULT_PAYLOAD, InfoData}; pub use tinyboot_core::{boot_version, pkg_version}; /// Common imports for bootloader binaries. pub mod prelude { pub use super::{ - BaudRate, BootCtl, FullDuplex, HalfDuplex, Level, NoTxEn, OpenDrain, Pin, Pull, PushPull, - PushPullWhileSending, TxEn, Usart, UsartConfig, UsartMapping, + Addressable, BaudRate, BootCtl, FullDuplex, HalfDuplex, Level, NoTxEn, OpenDrain, Pin, + Pull, PushPull, PushPullWhileSending, TxEn, Unaddressed, Usart, UsartConfig, UsartMapping, + chip_uid, }; } @@ -25,9 +28,17 @@ pub const PAGE_SIZE: usize = crate::hal::flash::PAGE_SIZE; /// Run the bootloader with the default 52-byte frame payload. Does not /// return. +/// +/// `addressing` is [`Unaddressed`] for one device per bus, or +/// `Addressable::new(chip_uid)` for many devices on one bus, each +/// silent until a host selects it by its unique id. #[inline(always)] -pub fn run(transport: impl tinyboot_core::traits::Transport, ctl: BootCtl) -> ! { - run_with_payload::(transport, ctl) +pub fn run( + transport: impl tinyboot_core::traits::Transport, + ctl: BootCtl, + addressing: impl Addressing, +) -> ! { + run_with_payload::(transport, ctl, addressing) } /// Run the bootloader with a `P`-byte frame payload, at least the 18-byte @@ -37,10 +48,11 @@ pub fn run(transport: impl tinyboot_core::traits::Transport, ctl: BootCtl) -> ! pub fn run_with_payload( transport: impl tinyboot_core::traits::Transport, ctl: BootCtl, + addressing: impl Addressing, ) -> ! { const { assert!(P >= InfoData::SIZE as usize); } let platform = Platform::new(transport, Storage::default(), BootMetaStore::default(), ctl); - tinyboot_core::Core::<_, _, _, _, PAGE_SIZE, P>::new(platform).run() + tinyboot_core::Core::<_, _, _, _, PAGE_SIZE, P, _>::with_addressing(platform, addressing).run() } diff --git a/ch32/src/hal/esig.rs b/ch32/src/hal/esig.rs new file mode 100644 index 0000000..762f40a --- /dev/null +++ b/ch32/src/hal/esig.rs @@ -0,0 +1,11 @@ +//! ESIG electronic signature: the factory-programmed 96-bit unique id in +//! system flash, at the same address on every supported family. + +const UNIID: *const u8 = 0x1FFF_F7E8 as *const u8; + +/// The chip's 96-bit unique id, 12 bytes least significant first. +#[inline(always)] +pub fn uid() -> &'static [u8; 12] { + // SAFETY: ESIG is always-readable factory flash, at any access width. + unsafe { &*(UNIID as *const [u8; 12]) } +} diff --git a/ch32/src/hal/mod.rs b/ch32/src/hal/mod.rs index c19d8c0..99172d5 100644 --- a/ch32/src/hal/mod.rs +++ b/ch32/src/hal/mod.rs @@ -6,6 +6,7 @@ mod generated { pub use generated::{Pin, UsartMapping}; pub mod afio; +pub mod esig; pub mod flash; pub mod gpio; pub mod iwdg; diff --git a/ch32/src/hal/usart/usart_common.rs b/ch32/src/hal/usart/usart_common.rs index e69e05e..6d9a433 100644 --- a/ch32/src/hal/usart/usart_common.rs +++ b/ch32/src/hal/usart/usart_common.rs @@ -31,6 +31,28 @@ pub fn write_byte(r: ch32_metapac::usart::Usart, byte: u8) { r.datar().write(|w| w.set_dr(byte as u16)); } +/// Spin at least `n` byte-times: BRR is PCLK cycles per bit, each spin +/// takes at least one cycle, and a byte is counted as 16 bits. +#[inline(always)] +pub fn wait_bytes(r: ch32_metapac::usart::Usart, n: u8) { + let bit = r.brr().read().0; + // Nested, not one n * 16 * bit count: no multiply on V003. + let mut bits = (n as u32) << 4; + while bits != 0 { + // Bottom-tested: an enabled USART's BRR is at least 16. + let mut c = bit; + loop { + // SAFETY: empty asm, only keeps the loop. + unsafe { core::arch::asm!("") }; + c -= 1; + if c == 0 { + break; + } + } + bits -= 1; + } +} + #[inline(always)] pub fn flush(r: ch32_metapac::usart::Usart) { while !r.statr().read().tc() {} diff --git a/ch32/src/platform/transport/usart/mod.rs b/ch32/src/platform/transport/usart/mod.rs index 16725bc..3d0cf75 100644 --- a/ch32/src/platform/transport/usart/mod.rs +++ b/ch32/src/platform/transport/usart/mod.rs @@ -80,7 +80,12 @@ pub struct Usart { tx_en: E::Switch, } -impl tinyboot_core::traits::Transport for Usart {} +impl tinyboot_core::traits::Transport for Usart { + #[inline(always)] + fn wait_bytes(&mut self, n: u8) { + usart::wait_bytes(self.regs, n); + } +} impl Usart { #[cfg_attr(split_sysflash, inline(never))] diff --git a/examples/ch32/v003/app/src/main.rs b/examples/ch32/v003/app/src/main.rs index c9fcffd..8ca3093 100644 --- a/examples/ch32/v003/app/src/main.rs +++ b/examples/ch32/v003/app/src/main.rs @@ -47,7 +47,8 @@ fn main() -> ! { tim.start(); unsafe { ch32_hal::interrupt::TIM2.enable() }; - let mut app = tinyboot_ch32::app::new_app(tinyboot_ch32::app::BootCtl::new()); + let mut app = + tinyboot_ch32::app::new_addressable_app(tinyboot_ch32::app::BootCtl::new(), wait_bytes); // USART1 blocking — must match the bootloader's pin mapping. // ch32-hal generic param picks the remap: @@ -85,6 +86,16 @@ fn main() -> ! { } } +/// Stagger `Select` replies: about `n` byte-times at USART1's baud (BRR +/// is clock cycles per bit). +fn wait_bytes(n: u8) { + let bit = pac::USART1.brr().read().0; + for _ in 0..n as u32 * 10 * bit { + // SAFETY: empty asm, only keeps the loop. + unsafe { core::arch::asm!("") }; + } +} + #[qingke_rt::interrupt] fn TIM2() { pac::TIM2.intfr().modify(|w| w.set_uif(false)); diff --git a/examples/ch32/v003/boot/src/bin/heaviest.rs b/examples/ch32/v003/boot/src/bin/heaviest.rs new file mode 100644 index 0000000..190b89b --- /dev/null +++ b/examples/ch32/v003/boot/src/bin/heaviest.rs @@ -0,0 +1,35 @@ +//! Size gate, built by CI beside the example: every option that costs +//! flash at once (one-wire push-pull, DE pin, compatibility key, +//! addressable, a 256-byte payload). Its system-flash build must still +//! link into the 1920-byte system flash. + +#![no_std] +#![no_main] + +use panic_halt as _; +use tinyboot_ch32_rt as _; + +tinyboot_ch32::boot::boot_version!(compat = "tinyboot-example-v003"); + +use tinyboot_ch32::boot::prelude::*; + +#[unsafe(export_name = "main")] +fn main() -> ! { + let transport = Usart::new(&UsartConfig { + baud: BaudRate::B115200, + pclk: 8_000_000, + mapping: UsartMapping::Usart1Remap0, + wire: HalfDuplex { + drive: PushPullWhileSending, + }, + tx_en: TxEn { + pin: Pin::PC2, + tx_level: Level::Low, + }, + }); + tinyboot_ch32::boot::run_with_payload::<256>( + transport, + BootCtl::new(), + Addressable::new(chip_uid), + ); +} diff --git a/examples/ch32/v003/boot/src/main.rs b/examples/ch32/v003/boot/src/main.rs index 9def1e1..f356bfb 100644 --- a/examples/ch32/v003/boot/src/main.rs +++ b/examples/ch32/v003/boot/src/main.rs @@ -40,5 +40,8 @@ fn main() -> ! { tx_level: Level::Low, }, }); - tinyboot_ch32::boot::run(transport, BootCtl::new()); + // Addressable: silent on a shared bus until a host selects this chip by + // its unique id; the CLI does that for a single device on its own. One + // device per bus and nothing to select: `Unaddressed`. + tinyboot_ch32::boot::run(transport, BootCtl::new(), Addressable::new(chip_uid)); } diff --git a/examples/ch32/v00x/app/src/main.rs b/examples/ch32/v00x/app/src/main.rs index be00c1b..38ef140 100644 --- a/examples/ch32/v00x/app/src/main.rs +++ b/examples/ch32/v00x/app/src/main.rs @@ -50,7 +50,8 @@ fn main() -> ! { tim.start(); unsafe { ch32_hal::interrupt::TIM2.enable() }; - let mut app = tinyboot_ch32::app::new_app(tinyboot_ch32::app::BootCtl::new()); + let mut app = + tinyboot_ch32::app::new_addressable_app(tinyboot_ch32::app::BootCtl::new(), wait_bytes); // USART1 blocking — must match the bootloader's pin mapping. // ch32-hal generic param picks the remap: @@ -88,6 +89,16 @@ fn main() -> ! { } } +/// Stagger `Select` replies: about `n` byte-times at USART1's baud (BRR +/// is clock cycles per bit). +fn wait_bytes(n: u8) { + let bit = pac::USART1.brr().read().0; + for _ in 0..n as u32 * 10 * bit { + // SAFETY: empty asm, only keeps the loop. + unsafe { core::arch::asm!("") }; + } +} + #[qingke_rt::interrupt] fn TIM2() { pac::TIM2.intfr().modify(|w| w.set_uif(false)); diff --git a/examples/ch32/v00x/boot/src/main.rs b/examples/ch32/v00x/boot/src/main.rs index af98f70..0b5b797 100644 --- a/examples/ch32/v00x/boot/src/main.rs +++ b/examples/ch32/v00x/boot/src/main.rs @@ -67,5 +67,8 @@ fn main() -> ! { tx_level: Level::High, }, }); - tinyboot_ch32::boot::run(transport, BootCtl::new()); + // Addressable: silent on a shared bus until a host selects this chip by + // its unique id; the CLI does that for a single device on its own. One + // device per bus and nothing to select: `Unaddressed`. + tinyboot_ch32::boot::run(transport, BootCtl::new(), Addressable::new(chip_uid)); } diff --git a/examples/ch32/v103/app/src/main.rs b/examples/ch32/v103/app/src/main.rs index 49b161c..a073839 100644 --- a/examples/ch32/v103/app/src/main.rs +++ b/examples/ch32/v103/app/src/main.rs @@ -49,16 +49,17 @@ fn main() -> ! { // system-flash: pass BOOT0 pin, the level that selects system flash, // and the reset delay in cycles (RC ~1ms @ 8MHz = 8000; flip-flop: 0). - let mut app = tinyboot_ch32::app::new_app(core::cfg_select! { - feature = "system-flash" => { - tinyboot_ch32::app::BootCtl::new( + let mut app = tinyboot_ch32::app::new_addressable_app( + core::cfg_select! { + feature = "system-flash" => tinyboot_ch32::app::BootCtl::new( tinyboot_ch32::app::Pin::PB1, tinyboot_ch32::app::Level::High, 8000, - ) - } - _ => tinyboot_ch32::app::BootCtl::new(), - }); + ), + _ => tinyboot_ch32::app::BootCtl::new(), + }, + wait_bytes, + ); // USART1 blocking — must match the bootloader's pin mapping. // ch32-hal generic param picks the remap: @@ -94,6 +95,16 @@ fn main() -> ! { } } +/// Stagger `Select` replies: about `n` byte-times at USART1's baud (BRR +/// is clock cycles per bit). +fn wait_bytes(n: u8) { + let bit = pac::USART1.brr().read().0; + for _ in 0..n as u32 * 10 * bit { + // SAFETY: empty asm, only keeps the loop. + unsafe { core::arch::asm!("") }; + } +} + #[qingke_rt::interrupt] fn TIM2() { pac::TIM2.intfr().modify(|w| w.set_uif(false)); diff --git a/examples/ch32/v103/boot/src/main.rs b/examples/ch32/v103/boot/src/main.rs index f61f467..da269ec 100644 --- a/examples/ch32/v103/boot/src/main.rs +++ b/examples/ch32/v103/boot/src/main.rs @@ -47,5 +47,8 @@ fn main() -> ! { _ => BootCtl::new(), }; - tinyboot_ch32::boot::run(transport, ctl); + // Addressable: silent on a shared bus until a host selects this chip by + // its unique id; the CLI does that for a single device on its own. One + // device per bus and nothing to select: `Unaddressed`. + tinyboot_ch32::boot::run(transport, ctl, Addressable::new(chip_uid)); }