Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ jobs:
- run: cargo build
- run: cargo build --release

ch32:
name: CH32 (${{ matrix.chip }})
ch32-v003:
name: CH32V003 (${{ matrix.chip }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -75,3 +75,39 @@ jobs:
run: cd examples/ch32/v003 && cargo +nightly clippy -p app --no-default-features --features "${{ matrix.chip }},user-flash" -- -D warnings
- name: Build app (user-flash)
run: cd examples/ch32/v003 && cargo +nightly build -p app --no-default-features --features "${{ matrix.chip }},user-flash" --release

ch32-v103:
name: CH32V103 (${{ matrix.chip }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
chip: [ch32v103c6t6, ch32v103c8t6, ch32v103c8u6, ch32v103r8t6]
steps:
- uses: actions/checkout@v6
- name: Install toolchain
run: |
rustup toolchain install nightly --profile minimal --component rust-src,rustfmt,clippy
rustup +nightly target add riscv32imc-unknown-none-elf
- name: Clippy libraries
run: cd ch32 && cargo +nightly clippy -p tinyboot-ch32-hal -p tinyboot-ch32-boot --no-default-features --features ${{ matrix.chip }} --target riscv32imc-unknown-none-elf -- -D warnings
- name: Build libraries
run: cd ch32 && cargo +nightly build -p tinyboot-ch32-hal -p tinyboot-ch32-boot --no-default-features --features ${{ matrix.chip }} --target riscv32imc-unknown-none-elf --release
- name: Fmt examples
run: cd examples/ch32/v103 && cargo +nightly fmt --all -- --check
- name: Clippy boot (system-flash)
run: cd examples/ch32/v103 && cargo +nightly clippy -p boot --no-default-features --features "${{ matrix.chip }},system-flash" -- -D warnings
- name: Build boot (system-flash)
run: cd examples/ch32/v103 && cargo +nightly build -p boot --no-default-features --features "${{ matrix.chip }},system-flash" --release
- name: Clippy boot (user-flash)
run: cd examples/ch32/v103 && cargo +nightly clippy -p boot --no-default-features --features "${{ matrix.chip }},user-flash" -- -D warnings
- name: Build boot (user-flash)
run: cd examples/ch32/v103 && cargo +nightly build -p boot --no-default-features --features "${{ matrix.chip }},user-flash" --release
- name: Clippy app (system-flash)
run: cd examples/ch32/v103 && cargo +nightly clippy -p app --no-default-features --features "${{ matrix.chip }},system-flash" -- -D warnings
- name: Build app (system-flash)
run: cd examples/ch32/v103 && cargo +nightly build -p app --no-default-features --features "${{ matrix.chip }},system-flash" --release
- name: Clippy app (user-flash)
run: cd examples/ch32/v103 && cargo +nightly clippy -p app --no-default-features --features "${{ matrix.chip }},user-flash" -- -D warnings
- name: Build app (user-flash)
run: cd examples/ch32/v103 && cargo +nightly build -p app --no-default-features --features "${{ matrix.chip }},user-flash" --release
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Rust bootloader for resource-constrained microcontrollers. Fits in the CH32V003'

## Chip Compatibility

tinyboot currently supports **UART / RS-485** transport. The table below tracks chip support status.
tinyboot currently supports **UART / RS-485** transport. The table below tracks chip support status. Chips with hardware boot pins (e.g. BOOT0/BOOT1) require a small external circuit to allow firmware-controlled boot mode switching. Please see [GPIO-Controlled Boot Mode Selection](docs/boot-ctl.md) document for more information.

✅ Verified | ❓ Untested (same die, likely works — volunteer needed) | 📋 Planned

Expand All @@ -16,6 +16,10 @@ tinyboot currently supports **UART / RS-485** transport. The table below tracks
| CH32V003A4M6 | `ch32v003a4m6` | `0x1FFFF000` (1920B) | ❓ | -- |
| CH32V003F4U6 | `ch32v003f4u6` | `0x1FFFF000` (1920B) | ❓ | -- |
| CH32V003J4M6 | `ch32v003j4m6` | `0x1FFFF000` (1920B) | ❓ | -- |
| CH32V103C6T6 | `ch32v103c6t6` | `0x1FFFF000` (2048B) | ❓ | -- |
| CH32V103C8T6 | `ch32v103c8t6` | `0x1FFFF000` (2048B) | ✅ | -- |
| CH32V103C8U6 | `ch32v103c8u6` | `0x1FFFF000` (2048B) | ❓ | -- |
| CH32V103R8T6 | `ch32v103r8t6` | `0x1FFFF000` (2048B) | ❓ | -- |
| CH32V002X4X6 | `ch32v002x4x6` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) |
| CH32V004X6X1 | `ch32v004x6x1` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) |
| CH32V005X6X6 | `ch32v005x6x6` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) |
Expand Down Expand Up @@ -75,6 +79,7 @@ ch32/ CH32 implementations
cli/ tinyboot — host CLI flasher

examples/ch32/v003/ CH32V003 boot + app examples
examples/ch32/v103/ CH32V103 boot + app examples
```

| Crate | Description |
Expand All @@ -91,7 +96,7 @@ examples/ch32/v003/ CH32V003 boot + app examples
The workspaces use **edition 2024**.

- **Library crates and CLI** — stable Rust 1.85+
- **CH32 examples** (bootloader and app binaries) — nightly, for `-Zbuild-std` on `riscv32ec-unknown-none-elf`
- **CH32 examples** (bootloader and app binaries) — nightly, for `-Zbuild-std` on `riscv32ec-unknown-none-elf` (V003) or standard `riscv32imc-unknown-none-elf` (V103)

## Linker Region Contract

Expand Down
5 changes: 4 additions & 1 deletion ch32/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ tinyboot-protocol = { workspace = true }
embedded-io = { workspace = true }
embedded-io-async = { workspace = true }
critical-section = "1.2"
qingke = "0.6"

[features]
ch32v003f4p6 = ["tinyboot-ch32-hal/ch32v003f4p6"]
ch32v003a4m6 = ["tinyboot-ch32-hal/ch32v003a4m6"]
ch32v003f4u6 = ["tinyboot-ch32-hal/ch32v003f4u6"]
ch32v003j4m6 = ["tinyboot-ch32-hal/ch32v003j4m6"]
ch32v103c6t6 = ["tinyboot-ch32-hal/ch32v103c6t6"]
ch32v103c8t6 = ["tinyboot-ch32-hal/ch32v103c8t6"]
ch32v103c8u6 = ["tinyboot-ch32-hal/ch32v103c8u6"]
ch32v103r8t6 = ["tinyboot-ch32-hal/ch32v103r8t6"]
system-flash = ["tinyboot-ch32-hal/system-flash"]
8 changes: 5 additions & 3 deletions ch32/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ tinyboot_ch32_app::app_version!();
tinyboot_ch32_app::fix_mtvec!();

// All parameters come from linker symbols — no hardcoded addresses needed.
let mut app = tinyboot_ch32_app::new_app();
// Pass BootCtlConfig for your chip (unit struct for V003, GPIO config for V103).
let mut app = tinyboot_ch32_app::new_app(tinyboot_ch32_app::BootCtlConfig);

// Confirm boot — transitions Validating -> Idle in boot metadata
app.confirm();
Expand All @@ -33,7 +34,7 @@ loop {
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `app_version!()` | Macro that places the crate version (from `Cargo.toml`) in the `.tb_version` linker section |
| `fix_mtvec!()` | Macro that fixes `mtvec` for apps behind a user-flash bootloader. Requires `--wrap=_setup_interrupts` linker arg. Not needed for system-flash |
| `new_app()` | Create an `App` configured for CH32 hardware. Reads geometry from linker symbols (`__tb_boot_version_addr`, `__tb_app_capacity`) |
| `new_app(config)` | Create an `App` configured for CH32 hardware. Takes `BootCtlConfig`; reads geometry from linker symbols |
| `App::confirm()` | Confirm trial boot (Validating -> Idle), preserving checksum in boot metadata |
| `App::poll()` | Poll for and handle one tinyboot command (blocking) |
| `App::poll_async()` | Async version of `poll()` |
Expand All @@ -50,6 +51,7 @@ All other commands receive `Status::Unsupported`.
| Feature | Description |
| -------------- | --------------------------------------- |
| `ch32v003f4p6` | CH32V003F4P6 chip variant (default) |
| `ch32v103c8t6` | CH32V103C8T6 chip variant |
| `system-flash` | App paired with system-flash bootloader |

See [`examples/ch32/v003/app`](../../examples/ch32/v003/app/) for a complete example.
See [`examples/ch32/v003/app`](../../examples/ch32/v003/app/) and [`examples/ch32/v103/app`](../../examples/ch32/v103/app/) for complete examples.
62 changes: 15 additions & 47 deletions ch32/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,21 @@

use tinyboot::traits::BootState;
use tinyboot::traits::app::BootClient as TBBootClient;
use tinyboot_ch32_hal::{flash, iwdg, pfic};
use tinyboot_ch32_hal::{boot_request, flash, iwdg, pfic};

// Re-exports so apps only need this one crate.
pub use boot_request::Config as BootCtlConfig;
pub use tinyboot::app::{App, AppConfig};
pub use tinyboot::traits::app as traits;
pub use tinyboot::{app_version, pkg_version};

#[doc(hidden)]
pub use qingke;

/// Fix `mtvec` for apps linked at a non-zero flash address (user-flash bootloader).
///
/// `qingke-rt` hardcodes `mtvec = 0x0` in its `_setup_interrupts`. This macro
/// generates a linker `--wrap` override that calls the original setup, then
/// rewrites `mtvec` to the actual vector table base.
///
/// Not needed for system-flash bootloaders (app starts at 0x0).
///
/// Place at module scope alongside [`app_version!`]. Requires
/// `--wrap=_setup_interrupts` in `build.rs`:
///
/// ```rust,ignore
/// // build.rs
/// println!("cargo:rustc-link-arg=--wrap=_setup_interrupts");
/// ```
#[cfg(not(feature = "system-flash"))]
#[macro_export]
macro_rules! fix_mtvec {
() => {
#[unsafe(export_name = "__wrap__setup_interrupts")]
unsafe extern "C" fn _tinyboot_setup_interrupts() {
use $crate::qingke::register::mtvec::{self, TrapMode};

unsafe extern "C" {
fn __real__setup_interrupts();
fn _start();
}
unsafe {
__real__setup_interrupts();
mtvec::write(_start as *const () as usize, TrapMode::VectoredAddress);
}
}
};
}
pub use tinyboot_ch32_hal::Pin;

/// CH32 boot client implementation.
pub struct Ch32BootClient;
pub struct BootClient {
config: boot_request::Config,
}

impl TBBootClient for Ch32BootClient {
impl TBBootClient for BootClient {
fn confirm(&mut self) {
critical_section::with(|_| {
let addr = flash::meta_addr();
Expand All @@ -72,11 +38,12 @@ impl TBBootClient for Ch32BootClient {

fn request_update(&mut self) {
critical_section::with(|_| {
#[cfg(feature = "system-flash")]
flash::set_boot_mode(true);
#[cfg(not(feature = "system-flash"))]
tinyboot_ch32_hal::boot_request::set_boot_request(true);
boot_request::set_boot_request(&self.config, true);
});
// Allow time for external boot mode circuit (RC) to settle.
for _ in 0..8000u16 {
core::hint::spin_loop();
}
}

fn system_reset(&mut self) -> ! {
Expand All @@ -88,7 +55,8 @@ impl TBBootClient for Ch32BootClient {
///
/// Reads boot version from `__tb_boot_version_addr`, app capacity from
/// `__tb_app_capacity`, and erase size from `flash::PAGE_SIZE`.
pub fn new_app() -> App<Ch32BootClient> {
pub fn new_app(boot_ctl: boot_request::Config) -> App<BootClient> {
boot_request::init(&boot_ctl);
unsafe extern "C" {
static __tb_boot_version_addr: u8;
static __tb_app_capacity: u8;
Expand All @@ -102,6 +70,6 @@ pub fn new_app() -> App<Ch32BootClient> {
boot_version: unsafe { boot_ver_addr.read_volatile() },
app_version: tinyboot::tinyboot_version(),
},
Ch32BootClient,
BootClient { config: boot_ctl },
)
}
4 changes: 4 additions & 0 deletions ch32/boot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ ch32v003f4p6 = ["tinyboot-ch32-hal/ch32v003f4p6"]
ch32v003a4m6 = ["tinyboot-ch32-hal/ch32v003a4m6"]
ch32v003f4u6 = ["tinyboot-ch32-hal/ch32v003f4u6"]
ch32v003j4m6 = ["tinyboot-ch32-hal/ch32v003j4m6"]
ch32v103c6t6 = ["tinyboot-ch32-hal/ch32v103c6t6"]
ch32v103c8t6 = ["tinyboot-ch32-hal/ch32v103c8t6"]
ch32v103c8u6 = ["tinyboot-ch32-hal/ch32v103c8u6"]
ch32v103r8t6 = ["tinyboot-ch32-hal/ch32v103r8t6"]
rt = []
system-flash = ["tinyboot-ch32-hal/system-flash"]
16 changes: 13 additions & 3 deletions ch32/boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,32 @@ fn main() -> ! {
rx_pull: Pull::None,
tx_en: None,
});
tinyboot_ch32_boot::run(transport);
tinyboot_ch32_boot::run(transport, BootCtlConfig);
}
```

For chips with a hardware BOOT0 pin (e.g. CH32V103), pass a `BootCtlConfig` with the GPIO pin driving the boot mode selection circuit:

```rust
tinyboot_ch32_boot::run(transport, BootCtlConfig {
pin: Pin::PB1,
active_high: true,
});
```

`Storage`, `BootMetaStore`, and `BootCtl` are initialized from linker symbols automatically. The boot version is placed by `boot_version!()` in the `.tb_version` section and read at runtime via the `__tb_version` linker symbol.

See [`examples/ch32/v003/boot`](../../examples/ch32/v003/boot/) for a complete bootloader example.
See [`examples/ch32/v003/boot`](../../examples/ch32/v003/boot/) and [`examples/ch32/v103/boot`](../../examples/ch32/v103/boot/) for complete examples.

## Runtime

The bootloader includes a minimal startup assembly (`v2.S`) — GP/SP init and jump to main (~20 bytes). No .data/.bss init since the system-flash bootloader uses no mutable statics.
The bootloader includes minimal startup assembly — GP/SP init and jump to main (~20 bytes). No .data/.bss init since the system-flash bootloader uses no mutable statics. Separate assembly files per QingKe core version (`v2.S` for V003, `v3.S` for V103).

## Features

| Feature | Description |
| -------------- | -------------------------------------------------------------------- |
| `ch32v003f4p6` | CH32V003F4P6 chip variant (default) |
| `ch32v103c8t6` | CH32V103C8T6 chip variant |
| `rt` | Minimal runtime startup (GP/SP init, no vector table or static init) |
| `system-flash` | Bootloader runs from system flash |
1 change: 0 additions & 1 deletion ch32/boot/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ fn main() {
// Re-emit peripheral cfgs from tinyboot-ch32-hal (exposed via its `links` metadata).
if let Ok(cfgs) = std::env::var("DEP_TINYBOOT_CH32_HAL_CFGS") {
for cfg in cfgs.split(',').filter(|s| !s.is_empty()) {
println!("cargo::rustc-check-cfg=cfg({cfg})");
println!("cargo:rustc-cfg={cfg}");
}
}
Expand Down
18 changes: 11 additions & 7 deletions ch32/boot/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![no_std]
#![warn(missing_docs)]
#![allow(unexpected_cfgs)]

//! CH32 bootloader platform implementation.
//!
Expand All @@ -13,7 +14,8 @@ pub mod platform;
mod rt;

pub use platform::{
BaudRate, BootCtl, BootMetaStore, Duplex, Storage, TxEnConfig, Usart, UsartConfig,
BaudRate, BootCtl, BootCtlConfig, BootMetaStore, Duplex, Storage, TxEnConfig, Usart,
UsartConfig,
};

// Re-exports so boot examples only need this one crate.
Expand All @@ -24,23 +26,25 @@ pub use tinyboot_ch32_hal::{Pin, UsartMapping};

/// Common imports for bootloader binaries.
pub mod prelude {
pub use crate::{BaudRate, Duplex, Pin, Pull, TxEnConfig, Usart, UsartConfig, UsartMapping};
pub use crate::{
BaudRate, BootCtlConfig, Duplex, Pin, Pull, TxEnConfig, Usart, UsartConfig, UsartMapping,
};
}

/// Protocol write buffer size (2 × page size).
const PROTOCOL_BUF_SIZE: usize = 2 * tinyboot_ch32_hal::flash::PAGE_SIZE;
pub const PAGE_SIZE: usize = tinyboot_ch32_hal::flash::PAGE_SIZE;

/// Run the bootloader with the given transport.
/// Run the bootloader with the given transport and boot control config.
///
/// Sets up storage, boot metadata, and boot control from linker symbols,
/// then enters the boot state machine. Does not return.
#[inline(always)]
pub fn run(transport: impl tinyboot::traits::boot::Transport) -> ! {
pub fn run(transport: impl tinyboot::traits::boot::Transport, config: BootCtlConfig) -> ! {
let platform = Platform::new(
transport,
Storage::default(),
BootMetaStore::default(),
BootCtl::default(),
BootCtl::new(config),
);
tinyboot::Core::<_, _, _, _, PROTOCOL_BUF_SIZE>::new(platform).run()
tinyboot::Core::<_, _, _, _, { 2 * PAGE_SIZE }>::new(platform).run()
}
Loading