diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 549207d..2781a15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/README.md b/README.md index 769a5cc..89d16a3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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]) | @@ -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 | @@ -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 diff --git a/ch32/app/Cargo.toml b/ch32/app/Cargo.toml index 4fbb12b..c056493 100644 --- a/ch32/app/Cargo.toml +++ b/ch32/app/Cargo.toml @@ -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"] diff --git a/ch32/app/README.md b/ch32/app/README.md index 2dd3d47..646a633 100644 --- a/ch32/app/README.md +++ b/ch32/app/README.md @@ -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(); @@ -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()` | @@ -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. diff --git a/ch32/app/src/lib.rs b/ch32/app/src/lib.rs index d130fd8..a62f81f 100644 --- a/ch32/app/src/lib.rs +++ b/ch32/app/src/lib.rs @@ -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(); @@ -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) -> ! { @@ -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 { +pub fn new_app(boot_ctl: boot_request::Config) -> App { + boot_request::init(&boot_ctl); unsafe extern "C" { static __tb_boot_version_addr: u8; static __tb_app_capacity: u8; @@ -102,6 +70,6 @@ pub fn new_app() -> App { boot_version: unsafe { boot_ver_addr.read_volatile() }, app_version: tinyboot::tinyboot_version(), }, - Ch32BootClient, + BootClient { config: boot_ctl }, ) } diff --git a/ch32/boot/Cargo.toml b/ch32/boot/Cargo.toml index d12f52a..a84dd61 100644 --- a/ch32/boot/Cargo.toml +++ b/ch32/boot/Cargo.toml @@ -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"] diff --git a/ch32/boot/README.md b/ch32/boot/README.md index 509f9b7..578d2c1 100644 --- a/ch32/boot/README.md +++ b/ch32/boot/README.md @@ -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 | diff --git a/ch32/boot/build.rs b/ch32/boot/build.rs index 5082901..38fd57f 100644 --- a/ch32/boot/build.rs +++ b/ch32/boot/build.rs @@ -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}"); } } diff --git a/ch32/boot/src/lib.rs b/ch32/boot/src/lib.rs index e4ab853..931fe76 100644 --- a/ch32/boot/src/lib.rs +++ b/ch32/boot/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] #![warn(missing_docs)] +#![allow(unexpected_cfgs)] //! CH32 bootloader platform implementation. //! @@ -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. @@ -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() } diff --git a/ch32/boot/src/platform/boot_ctl.rs b/ch32/boot/src/platform/boot_ctl.rs index 336dcc6..4b07c47 100644 --- a/ch32/boot/src/platform/boot_ctl.rs +++ b/ch32/boot/src/platform/boot_ctl.rs @@ -1,21 +1,24 @@ use tinyboot::traits::BootMode; use tinyboot::traits::boot::BootCtl as TBBootCtl; -use tinyboot_ch32_hal::pfic; +use tinyboot_ch32_hal::{boot_request, pfic}; /// CH32 boot control (reset, boot mode selection). /// /// In user-flash mode, caches the app entry point from `__tb_app_entry` linker symbol. pub struct BootCtl { + config: boot_request::Config, #[cfg(not(feature = "system-flash"))] app_entry: u32, } -impl Default for BootCtl { - /// In user-flash mode, reads `__tb_app_entry` linker symbol for the app entry point. +impl BootCtl { + /// Create boot control with the given boot request configuration. #[inline(always)] - fn default() -> Self { + pub fn new(config: boot_request::Config) -> Self { + boot_request::init(&config); Self { + config, #[cfg(not(feature = "system-flash"))] app_entry: { unsafe extern "C" { @@ -29,33 +32,23 @@ impl Default for BootCtl { impl TBBootCtl for BootCtl { fn is_boot_requested(&self) -> bool { - #[cfg(feature = "system-flash")] - { - tinyboot_ch32_hal::flash::is_boot_mode() - } - #[cfg(not(feature = "system-flash"))] - { - tinyboot_ch32_hal::boot_request::is_boot_requested() - } + boot_request::is_boot_requested() } fn system_reset(&mut self, mode: BootMode) -> ! { let bootloader = mode == BootMode::Bootloader; - #[cfg(feature = "system-flash")] - { - tinyboot_ch32_hal::flash::set_boot_mode(bootloader); - pfic::system_reset() + boot_request::set_boot_request(&self.config, bootloader); + // Allow time for external boot mode circuit (RC) to settle + // before triggering reset. ~1ms at 8MHz = 8000 iterations. + for _ in 0..8000u16 { + core::hint::spin_loop(); } #[cfg(not(feature = "system-flash"))] - { - tinyboot_ch32_hal::boot_request::set_boot_request(bootloader); - if bootloader { - pfic::system_reset() - } else { - tinyboot_ch32_hal::flash::lock(); - tinyboot_ch32_hal::rcc::reset_apb2(); - pfic::jump(self.app_entry) - } + if !bootloader { + tinyboot_ch32_hal::flash::lock(); + tinyboot_ch32_hal::rcc::reset_apb2(); + pfic::jump(self.app_entry) } + pfic::system_reset() } } diff --git a/ch32/boot/src/platform/mod.rs b/ch32/boot/src/platform/mod.rs index 75febc0..b75a2b8 100644 --- a/ch32/boot/src/platform/mod.rs +++ b/ch32/boot/src/platform/mod.rs @@ -6,6 +6,7 @@ mod transport; pub use boot_ctl::BootCtl; pub use boot_state::BootMetaStore; pub use storage::Storage; +pub use tinyboot_ch32_hal::boot_request::Config as BootCtlConfig; pub use tinyboot_ch32_hal::gpio::Pull; pub use tinyboot_ch32_hal::{Pin, UsartMapping}; pub use transport::usart::{BaudRate, Duplex, TxEnConfig, Usart, UsartConfig}; diff --git a/ch32/boot/src/platform/transport/usart/mod.rs b/ch32/boot/src/platform/transport/usart/mod.rs index d6d0834..f618f5c 100644 --- a/ch32/boot/src/platform/transport/usart/mod.rs +++ b/ch32/boot/src/platform/transport/usart/mod.rs @@ -74,23 +74,26 @@ impl Usart { pub fn new(config: &UsartConfig) -> Self { let tx_pin = config.mapping.tx_pin(); let rx_pin = config.mapping.rx_pin(); - let remap = config.mapping.remap_value(); let regs = config.mapping.regs(); let half_duplex = matches!(config.duplex, Duplex::Half); - // Batch-enable GPIO port(s), AFIO, and USART1 clocks + let usart_n = config.mapping.peripheral_index(); + + // Batch-enable GPIO port(s), AFIO, and USART clocks rcc::enable_apb2( (1 << (2 + tx_pin.port_index())) | (1 << (2 + rx_pin.port_index())) - | 1 // AFIO (bit 0) - | (1 << 14), // USART1 (bit 14) + | 1 // AFIO + | rcc::usart_apb2_bit(usart_n), ); - - // Set pin remap if non-default - if remap != 0 { - afio::set_usart1_remap(remap); + // Enable USART on APB1 if not on APB2 (e.g. USART2/3) + if rcc::usart_apb2_bit(usart_n) == 0 { + rcc::enable_usart(usart_n); } + // Set pin remap + afio::set_usart_remap(usart_n, config.mapping.remap_value()); + // Configure pins gpio::configure(tx_pin, PinMode::AF_PUSH_PULL); if !half_duplex { @@ -101,11 +104,12 @@ impl Usart { if let Some(ref tx_en) = config.tx_en { rcc::enable_gpio(tx_en.pin.port_index()); gpio::configure(tx_en.pin, PinMode::OUTPUT_PUSH_PULL); - if tx_en.active_high { - gpio::set_low(tx_en.pin); + let rx_level = if tx_en.active_high { + gpio::Level::Low } else { - gpio::set_high(tx_en.pin); - } + gpio::Level::High + }; + gpio::set_level(tx_en.pin, rx_level); } // Initialize USART @@ -120,22 +124,24 @@ impl Usart { #[inline(always)] fn set_tx_mode(&self) { if let Some(ref tx_en) = self.tx_en { - if tx_en.active_high { - gpio::set_high(tx_en.pin); + let level = if tx_en.active_high { + gpio::Level::High } else { - gpio::set_low(tx_en.pin); - } + gpio::Level::Low + }; + gpio::set_level(tx_en.pin, level); } } #[inline(always)] fn set_rx_mode(&self) { if let Some(ref tx_en) = self.tx_en { - if tx_en.active_high { - gpio::set_low(tx_en.pin); + let level = if tx_en.active_high { + gpio::Level::Low } else { - gpio::set_high(tx_en.pin); - } + gpio::Level::High + }; + gpio::set_level(tx_en.pin, level); } } } diff --git a/ch32/boot/src/rt/mod.rs b/ch32/boot/src/rt/mod.rs index 66ca07b..25ee58a 100644 --- a/ch32/boot/src/rt/mod.rs +++ b/ch32/boot/src/rt/mod.rs @@ -1,2 +1,5 @@ #[cfg(pfic_rv2)] core::arch::global_asm!(include_str!("v2.S")); + +#[cfg(pfic_rv3)] +core::arch::global_asm!(include_str!("v3.S")); diff --git a/ch32/boot/src/rt/v3.S b/ch32/boot/src/rt/v3.S new file mode 100644 index 0000000..8fd4422 --- /dev/null +++ b/ch32/boot/src/rt/v3.S @@ -0,0 +1,28 @@ + # ========================================================================= + # Minimal bootloader runtime for system-flash builds (QingKe V3). + # + # Omits .data copy, .bss zeroing, and interrupt vector table to save + # space โ€” critical when the bootloader must fit in 2048 bytes. + # + # Safe because the system-flash bootloader: + # - Has no initialized statics (no .data) + # - Has no zero-init statics (no .bss) + # - Never enables interrupts (polling only) + # - Only runs from power-on reset (SRAM is undefined anyway) + # + # User-flash builds should use the full qingke-rt runtime which + # includes .data/.bss init for defmt-rtt and system-reset safety. + # ========================================================================= + + .section .init, "ax" + .global _start + .align 2 + +_start: + .option push + .option norelax + la gp, __global_pointer$ + .option pop + + la sp, _stack_top + j main diff --git a/ch32/hal/Cargo.toml b/ch32/hal/Cargo.toml index 155b956..92a492a 100644 --- a/ch32/hal/Cargo.toml +++ b/ch32/hal/Cargo.toml @@ -28,6 +28,10 @@ ch32v003f4p6 = ["ch32-metapac/ch32v003f4p6"] ch32v003a4m6 = ["ch32-metapac/ch32v003a4m6"] ch32v003f4u6 = ["ch32-metapac/ch32v003f4u6"] ch32v003j4m6 = ["ch32-metapac/ch32v003j4m6"] +ch32v103c6t6 = ["ch32-metapac/ch32v103c6t6"] +ch32v103c8t6 = ["ch32-metapac/ch32v103c8t6"] +ch32v103c8u6 = ["ch32-metapac/ch32v103c8u6"] +ch32v103r8t6 = ["ch32-metapac/ch32v103r8t6"] # Bootloader runs from system flash (uses BOOT_MODE register for boot request). # When disabled, bootloader runs from user flash (uses RAM magic word for boot request). diff --git a/ch32/hal/README.md b/ch32/hal/README.md index 4902e53..fc210ae 100644 --- a/ch32/hal/README.md +++ b/ch32/hal/README.md @@ -15,7 +15,7 @@ Minimal hardware abstraction layer for tinyboot on CH32 microcontrollers. This i | `afio` | Alternate function I/O and pin remapping | | `pfic` | Interrupt controller and system reset | | `iwdg` | Independent watchdog timer feed | -| `boot_request` | RAM-based boot request signaling (user-flash only) | +| `boot_request` | Boot request signaling (reg/ram/gpio, auto-selected) | ## Code generation @@ -31,7 +31,8 @@ This keeps the source chip-agnostic while producing zero-overhead accessors. | Feature | Description | | -------------- | ----------------------------------------------------------------------------- | | `ch32v003f4p6` | CH32V003F4P6 chip variant (default, for rust-analyzer; CI uses explicit chip) | -| `system-flash` | Bootloader runs from system flash (uses BOOT_MODE register for boot requests) | +| `ch32v103c8t6` | CH32V103C8T6 chip variant | +| `system-flash` | Bootloader runs from system flash (boot request scheme auto-selected by chip) | ## Notes diff --git a/ch32/hal/build.rs b/ch32/hal/build.rs index 376f565..63a6b12 100644 --- a/ch32/hal/build.rs +++ b/ch32/hal/build.rs @@ -17,21 +17,53 @@ fn main() -> Result<(), Box> { for p in METADATA.peripherals { if let Some(regs) = &p.registers { let cfg = format!("{}_{}", regs.kind, regs.version); - println!("cargo::rustc-check-cfg=cfg({cfg})"); println!("cargo:rustc-cfg={cfg}"); cfgs.push(cfg); } } cfgs.dedup(); + + // Chips with a hardware BOOT0 pin (e.g. CH32V103) need an external circuit + // (RC or flip-flop) to select boot source. Chips without (e.g. CH32V003) + // have a BOOT_MODE register instead. + let boot_pin = !cfgs.iter().any(|c| c == "flash_v0"); + println!("cargo::rustc-check-cfg=cfg(boot_pin)"); + if boot_pin { + println!("cargo:rustc-cfg=boot_pin"); + cfgs.push("boot_pin".to_string()); + } + + // Boot request scheme cfgs โ€” which mechanisms are active: + // boot_req_reg: BOOT_MODE register (system-flash, no boot_pin) + // boot_req_ram: RAM magic word (user-flash OR boot_pin) + // boot_req_gpio: GPIO pin drive (system-flash + boot_pin) + // Note: ram and gpio are both set for system-flash + boot_pin. + let system_flash = env::var("CARGO_FEATURE_SYSTEM_FLASH").is_ok(); + for name in ["boot_req_reg", "boot_req_ram", "boot_req_gpio"] { + println!("cargo::rustc-check-cfg=cfg({name})"); + } + if system_flash && !boot_pin { + println!("cargo:rustc-cfg=boot_req_reg"); + cfgs.push("boot_req_reg".to_string()); + } + if !system_flash || boot_pin { + println!("cargo:rustc-cfg=boot_req_ram"); + cfgs.push("boot_req_ram".to_string()); + } + if system_flash && boot_pin { + println!("cargo:rustc-cfg=boot_req_gpio"); + cfgs.push("boot_req_gpio".to_string()); + } + println!("cargo:cfgs={}", cfgs.join(",")); generate_pin_and_usart_mapping(out)?; - #[cfg(not(feature = "system-flash"))] - { - fs::copy("tb-user-flash.x", out.join("tb-user-flash.x"))?; - println!("cargo:rerun-if-changed=tb-user-flash.x"); - } + // Boot request RAM magic word linker script. + // Always copied (4 bytes NOLOAD โ€” unused by reg scheme but avoids + // conditional complexity in downstream build scripts). + fs::copy("tb-boot-req.x", out.join("tb-boot-req.x"))?; + println!("cargo:rerun-if-changed=tb-boot-req.x"); println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rerun-if-changed=build.rs"); @@ -205,6 +237,19 @@ fn generate_pin_and_usart_mapping(out: &Path) -> Result<(), Box> { } writeln!(code, " }}")?; writeln!(code, " }}")?; + writeln!(code)?; + + // peripheral_index() โ€” USART peripheral number (1, 2, 3, ...) + writeln!(code, " pub const fn peripheral_index(self) -> u8 {{")?; + writeln!(code, " match self {{")?; + for (peri, remap) in groups.keys() { + let variant = format!("{}Remap{}", capitalize_peripheral(peri), remap); + // Extract trailing digits from e.g. "USART1" โ†’ 1 + let index: String = peri.chars().filter(|c| c.is_ascii_digit()).collect(); + writeln!(code, " UsartMapping::{variant} => {index},")?; + } + writeln!(code, " }}")?; + writeln!(code, " }}")?; writeln!(code, "}}")?; diff --git a/ch32/hal/src/afio/mod.rs b/ch32/hal/src/afio/mod.rs index f834b22..91caad9 100644 --- a/ch32/hal/src/afio/mod.rs +++ b/ch32/hal/src/afio/mod.rs @@ -1,4 +1,5 @@ #[cfg_attr(afio_v0, path = "v0.rs")] +#[cfg_attr(afio_v3, path = "v3.rs")] mod family; pub use family::*; diff --git a/ch32/hal/src/afio/v0.rs b/ch32/hal/src/afio/v0.rs index fcc60c8..acbe047 100644 --- a/ch32/hal/src/afio/v0.rs +++ b/ch32/hal/src/afio/v0.rs @@ -1,7 +1,12 @@ #[inline(always)] -pub fn set_usart1_remap(remap: u8) { - ch32_metapac::AFIO.pcfr1().write(|w| { - w.set_usart1_rm(remap & 1 != 0); - w.set_usart1_rm1(remap & 2 != 0); - }); +pub fn set_usart_remap(n: u8, remap: u8) { + if remap == 0 { + return; + } + if n == 1 { + ch32_metapac::AFIO.pcfr1().write(|w| { + w.set_usart1_rm(remap & 1 != 0); + w.set_usart1_rm1(remap & 2 != 0); + }); + } } diff --git a/ch32/hal/src/afio/v3.rs b/ch32/hal/src/afio/v3.rs new file mode 100644 index 0000000..27aebd6 --- /dev/null +++ b/ch32/hal/src/afio/v3.rs @@ -0,0 +1,18 @@ +#[inline(always)] +pub fn set_usart_remap(n: u8, remap: u8) { + if remap == 0 { + return; + } + match n { + 1 => ch32_metapac::AFIO + .pcfr1() + .modify(|w| w.set_usart1_rm(remap & 1 != 0)), + 2 => ch32_metapac::AFIO + .pcfr1() + .modify(|w| w.set_usart2_rm(remap & 1 != 0)), + 3 => ch32_metapac::AFIO + .pcfr1() + .modify(|w| w.set_usart3_rm(remap)), + _ => {} + } +} diff --git a/ch32/hal/src/boot_request.rs b/ch32/hal/src/boot_request.rs index 04cca46..005273d 100644 --- a/ch32/hal/src/boot_request.rs +++ b/ch32/hal/src/boot_request.rs @@ -1,14 +1,93 @@ -const BOOT_REQUEST_MAGIC: u32 = 0xB007_CAFE; +//! Boot request signaling and boot mode selection. +//! +//! Three schemes selected automatically by chip capabilities and flash mode: +//! +//! - **reg**: `BOOT_MODE` register (system-flash, chips without `boot_pin`) +//! - **ram**: RAM magic word (user-flash, all chips) +//! - **gpio**: RAM magic word + GPIO pin (system-flash, chips with `boot_pin`) +//! +//! Scheme cfgs (`boot_req_reg`, `boot_req_ram`, `boot_req_gpio`) are set by +//! the build script. `ram` and `gpio` are both active for system-flash + boot_pin. -unsafe extern "C" { - static mut __tb_boot_request: u32; +// โ”€โ”€ Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/// Boot control hardware configuration. +/// +/// For the gpio scheme (system-flash on chips with `boot_pin`), this +/// configures the GPIO pin connected to the BOOT0 control circuit. +/// For all other schemes, no hardware configuration is needed. +#[cfg(boot_req_gpio)] +#[derive(Copy, Clone)] +pub struct Config { + /// GPIO pin connected to the BOOT0 control circuit. + pub pin: crate::Pin, + /// `true` if driving HIGH selects system flash (default for RC circuit). + pub active_high: bool, +} + +/// Boot control hardware configuration (no-op for reg and ram schemes). +#[cfg(not(boot_req_gpio))] +#[derive(Copy, Clone, Default)] +pub struct Config; + +// โ”€โ”€ Public API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/// Initialize boot control hardware. Call once at startup. +/// +/// For the gpio scheme, configures the BOOT_CTL pin as push-pull output +/// and defaults it to the system-flash direction. For other schemes this +/// is a no-op. +pub fn init(_config: &Config) { + #[cfg(boot_req_gpio)] + { + use crate::gpio::{self, PinMode}; + crate::rcc::enable_gpio(_config.pin.port_index()); + gpio::configure(_config.pin, PinMode::OUTPUT_PUSH_PULL); + drive_boot_pin(_config.pin, _config.active_high, true); + } } +/// Check whether a boot-to-bootloader request is pending. pub fn is_boot_requested() -> bool { - unsafe { core::ptr::read_volatile(&raw const __tb_boot_request) == BOOT_REQUEST_MAGIC } + #[cfg(boot_req_reg)] + return crate::flash::boot_mode(); + + #[cfg(boot_req_ram)] + return unsafe { core::ptr::read_volatile(&raw const __tb_boot_request) == BOOT_REQUEST_MAGIC }; +} + +/// Signal (or clear) boot intent for the next reset. +pub fn set_boot_request(_config: &Config, request: bool) { + #[cfg(boot_req_reg)] + crate::flash::set_boot_mode(request); + + #[cfg(boot_req_ram)] + { + let val = if request { BOOT_REQUEST_MAGIC } else { 0 }; + unsafe { core::ptr::write_volatile(&raw mut __tb_boot_request, val) }; + } + + #[cfg(boot_req_gpio)] + drive_boot_pin(_config.pin, _config.active_high, request); +} + +// โ”€โ”€ Private โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +#[cfg(boot_req_ram)] +const BOOT_REQUEST_MAGIC: u32 = 0xB007_CAFE; + +#[cfg(boot_req_ram)] +unsafe extern "C" { + static mut __tb_boot_request: u32; } -pub fn set_boot_request(request: bool) { - let val = if request { BOOT_REQUEST_MAGIC } else { 0 }; - unsafe { core::ptr::write_volatile(&raw mut __tb_boot_request, val) }; +#[cfg(boot_req_gpio)] +fn drive_boot_pin(pin: crate::Pin, active_high: bool, system_flash: bool) { + use crate::gpio::{self, Level}; + let level = if active_high == system_flash { + Level::High + } else { + Level::Low + }; + gpio::set_level(pin, level); } diff --git a/ch32/hal/src/flash/mod.rs b/ch32/hal/src/flash/mod.rs index 9fd72ab..fc0de50 100644 --- a/ch32/hal/src/flash/mod.rs +++ b/ch32/hal/src/flash/mod.rs @@ -1,4 +1,5 @@ #[cfg_attr(flash_v0, path = "v0.rs")] +#[cfg_attr(flash_v1, path = "v1.rs")] mod family; pub use family::*; diff --git a/ch32/hal/src/flash/v0.rs b/ch32/hal/src/flash/v0.rs index f0b2852..8ee0c58 100644 --- a/ch32/hal/src/flash/v0.rs +++ b/ch32/hal/src/flash/v0.rs @@ -112,7 +112,7 @@ pub fn write(addr: u32, data: &[u8]) { FLASH.ctlr().write(|_| {}); } -pub fn is_boot_mode() -> bool { +pub fn boot_mode() -> bool { FLASH.statr().read().boot_mode() } diff --git a/ch32/hal/src/flash/v1.rs b/ch32/hal/src/flash/v1.rs new file mode 100644 index 0000000..21050f0 --- /dev/null +++ b/ch32/hal/src/flash/v1.rs @@ -0,0 +1,141 @@ +const KEY1: u32 = 0x4567_0123; +const KEY2: u32 = 0xCDEF_89AB; +const FLASH: ch32_metapac::flash::Flash = ch32_metapac::FLASH; + +#[inline(always)] +fn wait_busy() { + while FLASH.statr().read().bsy() {} + debug_assert!( + !FLASH.statr().read().wrprterr(), + "flash write protection error" + ); +} + +/// Unlock flash controller (KEYR + MODEKEYR). +pub fn unlock() { + FLASH.keyr().write(|w| w.set_keyr(KEY1)); + FLASH.keyr().write(|w| w.set_keyr(KEY2)); + FLASH.modekeyr().write(|w| w.set_modekeyr(KEY1)); + FLASH.modekeyr().write(|w| w.set_modekeyr(KEY2)); +} + +/// Lock flash controller. +#[inline(always)] +pub fn lock() { + FLASH.ctlr().write(|w| { + w.set_lock(true); + w.set_flock(true); + }); +} + +/// Undocumented flash controller register at offset 0x34. +/// Not in the RM; used by WCH's HAL for read-cache invalidation. +/// +/// See: https://github.com/openwch/ch32v103/blob/f99a84c4c42b6fb676560a9b8b7c737401efe0ad/EVT/EXAM/SRC/Peripheral/src/ch32v10x_flash.c#L210 +const FLASH_RDCACHE_REG: *mut u32 = 0x4002_2034 as *mut u32; + +/// XOR mask used to read from a 4 KB-distant flash address, forcing eviction +/// of the prefetch/read pipeline entry for the page just written or erased. +const RDCACHE_XOR_MASK: u32 = 0x0000_1000; + +/// Invalidate the flash read cache for `addr`. +/// +/// `addr` must be word-aligned (callers always pass page-aligned addresses). +fn invalidate_read_cache(addr: u32) { + let src = addr ^ RDCACHE_XOR_MASK; + let val = unsafe { core::ptr::read_volatile(src as *const u32) }; + unsafe { core::ptr::write_volatile(FLASH_RDCACHE_REG, val) }; +} + +/// Flash page size in bytes (erase and fast-write granularity). +pub const PAGE_SIZE: usize = 128; + +/// Fast-write buffer load size in bytes. +const BUF_LOAD_SIZE: usize = 16; + +// --- User flash (fast page erase/write) --- + +/// Erase a single 128-byte page at `addr` (RM ยง24.4.7). +pub fn erase(addr: u32) { + // Step 4: set FTER + FLASH.ctlr().write(|w| { + w.set_fter(true); + }); + // Step 5: write page address + FLASH.addr().write(|w| w.set_far(addr)); + // Step 6: set STRT + FLASH.ctlr().write(|w| { + w.set_fter(true); + w.set_strt(true); + }); + // Step 7: wait BSY, clear EOP + wait_busy(); + // Clear FTER + FLASH.ctlr().write(|_| {}); + invalidate_read_cache(addr); +} + +/// Write `data` to flash at `addr` (RM ยง24.4.6). +/// +/// Must not cross a page boundary. `addr` must be 4-byte aligned. +/// Trailing bytes are padded to the next BUF_LOAD_SIZE boundary with 0xFF. +pub fn write(addr: u32, data: &[u8]) { + let page_base = addr & !(PAGE_SIZE as u32 - 1); + debug_assert!((addr as usize & 3) == 0, "write: addr not word-aligned"); + debug_assert!( + addr + data.len() as u32 <= page_base + PAGE_SIZE as u32, + "write: crosses page boundary" + ); + + // Step 4: set FTPG alone + FLASH.ctlr().write(|w| { + w.set_ftpg(true); + }); + // Step 5: set BUFRST (with FTPG still set) + FLASH.ctlr().write(|w| { + w.set_ftpg(true); + w.set_bufrst(true); + }); + // Step 6: wait BSY, clear EOP + wait_busy(); + + // Steps 7-9: load 16-byte groups into buffer (8 groups ร— 16 bytes = 128 bytes). + // Each group: write 4 words, then BUFLOAD. + let load_len = (data.len() + BUF_LOAD_SIZE - 1) & !(BUF_LOAD_SIZE - 1); + let mut buf_addr = addr; + let mut pos = 0; + while pos < load_len { + // Build word byte-by-byte; 0xFF fills bytes past data end. + let mut buf = [0xFFu8; 4]; + let mut j = 0; + while j < 4 && pos + j < data.len() { + buf[j] = data[pos + j]; + j += 1; + } + unsafe { core::ptr::write_volatile(buf_addr as *mut u32, u32::from_le_bytes(buf)) }; + buf_addr += 4; + pos += 4; + + // Step 8: BUFLOAD after each 16-byte group + if pos % BUF_LOAD_SIZE == 0 { + FLASH.ctlr().write(|w| { + w.set_ftpg(true); + w.set_bufload(true); + }); + wait_busy(); + } + } + + // Step 10: write page address + FLASH.addr().write(|w| w.set_far(page_base)); + // Step 11: set STRT + FLASH.ctlr().write(|w| { + w.set_ftpg(true); + w.set_strt(true); + }); + // Step 12: wait BSY, clear EOP + wait_busy(); + // Step 14: clear FTPG + FLASH.ctlr().write(|_| {}); + invalidate_read_cache(page_base); +} diff --git a/ch32/hal/src/gpio/mod.rs b/ch32/hal/src/gpio/mod.rs index cd1a8d4..45c9ba8 100644 --- a/ch32/hal/src/gpio/mod.rs +++ b/ch32/hal/src/gpio/mod.rs @@ -1,4 +1,5 @@ #[cfg_attr(gpio_v0, path = "v0.rs")] +#[cfg_attr(gpio_v3, path = "v3.rs")] mod family; pub use family::*; diff --git a/ch32/hal/src/gpio/v0.rs b/ch32/hal/src/gpio/v0.rs index 586d109..dd6b317 100644 --- a/ch32/hal/src/gpio/v0.rs +++ b/ch32/hal/src/gpio/v0.rs @@ -9,6 +9,12 @@ pub enum Pull { Down, } +#[derive(Copy, Clone, PartialEq)] +pub enum Level { + Low, + High, +} + /// GPIO pin configuration. /// /// Encodes the 4-bit CFGLR field `[MODE(2) | CNF(2)]` directly. @@ -55,14 +61,14 @@ pub fn configure(pin: Pin, mode: PinMode) { } } -pub fn set_high(pin: Pin) { - pin.gpio_regs() - .bshr() - .write(|w| w.0 = 1 << pin.pin_number()); -} - -pub fn set_low(pin: Pin) { - pin.gpio_regs().bcr().write(|w| w.0 = 1 << pin.pin_number()); +pub fn set_level(pin: Pin, level: Level) { + if level == Level::High { + pin.gpio_regs() + .bshr() + .write(|w| w.0 = 1 << pin.pin_number()); + } else { + pin.gpio_regs().bcr().write(|w| w.0 = 1 << pin.pin_number()); + } } impl embedded_hal::digital::ErrorType for Pin { @@ -71,12 +77,12 @@ impl embedded_hal::digital::ErrorType for Pin { impl embedded_hal::digital::OutputPin for Pin { fn set_high(&mut self) -> Result<(), Self::Error> { - set_high(*self); + set_level(*self, Level::High); Ok(()) } fn set_low(&mut self) -> Result<(), Self::Error> { - set_low(*self); + set_level(*self, Level::Low); Ok(()) } } diff --git a/ch32/hal/src/gpio/v3.rs b/ch32/hal/src/gpio/v3.rs new file mode 100644 index 0000000..da59ce6 --- /dev/null +++ b/ch32/hal/src/gpio/v3.rs @@ -0,0 +1,95 @@ +use core::convert::Infallible; + +use crate::Pin; + +#[derive(Copy, Clone)] +pub enum Pull { + None, + Up, + Down, +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Level { + Low, + High, +} + +/// GPIO pin configuration. +/// +/// Encodes the 4-bit CFGLR/CFGHR field `[MODE(2) | CNF(2)]` directly. +/// Bit 7 = set ODR high, bit 6 = set ODR low, bits 3:0 = CFG nibble. +#[derive(Copy, Clone)] +pub struct PinMode(u8); + +// MODE bits: INPUT=0b00, OUTPUT_10MHZ=0b01 +// CNF bits (shifted <<2): ANALOG/PP=0b0000, FLOAT/OD=0b0100, PULL/AF_PP=0b1000, AF_OD=0b1100 +impl PinMode { + pub const INPUT_FLOATING: Self = Self(0b0100); // MODE=00 CNF=01 + pub const INPUT_PULL_UP: Self = Self(0b1000 | 0x80); // MODE=00 CNF=10, ODR=1 + pub const INPUT_PULL_DOWN: Self = Self(0b1000 | 0x40); // MODE=00 CNF=10, ODR=0 + pub const OUTPUT_PUSH_PULL: Self = Self(0b0001); // MODE=01 CNF=00 + pub const OUTPUT_OPEN_DRAIN: Self = Self(0b0101); // MODE=01 CNF=01 + pub const AF_PUSH_PULL: Self = Self(0b1001); // MODE=01 CNF=10 + pub const AF_OPEN_DRAIN: Self = Self(0b1101); // MODE=01 CNF=11 + + pub fn input_pull(pull: Pull) -> Self { + match pull { + Pull::Up => Self::INPUT_PULL_UP, + Pull::Down => Self::INPUT_PULL_DOWN, + Pull::None => Self::INPUT_FLOATING, + } + } +} + +#[inline(always)] +pub fn configure(pin: Pin, mode: PinMode) { + let regs = pin.gpio_regs(); + let n = pin.pin_number(); + + let shift = (n % 8) * 4; + let mask = !(0xFu32 << shift); + let bits = ((mode.0 & 0x0F) as u32) << shift; + + if n < 8 { + let prev = regs.cfglr().read().0; + regs.cfglr() + .write_value(ch32_metapac::gpio::regs::Cfglr(prev & mask | bits)); + } else { + let prev = regs.cfghr().read().0; + regs.cfghr() + .write_value(ch32_metapac::gpio::regs::Cfghr(prev & mask | bits)); + } + + if mode.0 & 0xC0 != 0 { + let mut outdr = regs.outdr().read(); + outdr.set_odr(n, mode.0 & 0x80 != 0); + regs.outdr().write_value(outdr); + } +} + +pub fn set_level(pin: Pin, level: Level) { + if level == Level::High { + pin.gpio_regs() + .bshr() + .write(|w| w.0 = 1 << pin.pin_number()); + } else { + pin.gpio_regs().bcr().write(|w| w.0 = 1 << pin.pin_number()); + } +} + +impl embedded_hal::digital::ErrorType for Pin { + type Error = Infallible; +} + +impl embedded_hal::digital::OutputPin for Pin { + fn set_high(&mut self) -> Result<(), Self::Error> { + set_level(*self, Level::High); + Ok(()) + } + + fn set_low(&mut self) -> Result<(), Self::Error> { + set_level(*self, Level::Low); + Ok(()) + } +} diff --git a/ch32/hal/src/lib.rs b/ch32/hal/src/lib.rs index 00f813b..3dd6fde 100644 --- a/ch32/hal/src/lib.rs +++ b/ch32/hal/src/lib.rs @@ -1,10 +1,15 @@ #![no_std] +#![allow(unexpected_cfgs)] #[cfg(not(any( feature = "ch32v003f4p6", feature = "ch32v003a4m6", feature = "ch32v003f4u6", feature = "ch32v003j4m6", + feature = "ch32v103c6t6", + feature = "ch32v103c8t6", + feature = "ch32v103c8u6", + feature = "ch32v103r8t6", )))] compile_error!( "No chip variant selected. Enable a chip feature, e.g.: \ @@ -24,5 +29,4 @@ pub mod pfic; pub mod rcc; pub mod usart; -#[cfg(not(feature = "system-flash"))] pub mod boot_request; diff --git a/ch32/hal/src/pfic/mod.rs b/ch32/hal/src/pfic/mod.rs index 5067ca9..eb956f8 100644 --- a/ch32/hal/src/pfic/mod.rs +++ b/ch32/hal/src/pfic/mod.rs @@ -1,4 +1,5 @@ #[cfg_attr(pfic_rv2, path = "rv2.rs")] +#[cfg_attr(pfic_rv3, path = "rv3.rs")] mod family; pub use family::*; diff --git a/ch32/hal/src/pfic/rv3.rs b/ch32/hal/src/pfic/rv3.rs new file mode 100644 index 0000000..f70e68a --- /dev/null +++ b/ch32/hal/src/pfic/rv3.rs @@ -0,0 +1,19 @@ +/// Jump to an absolute address. Does not return. +pub fn jump(addr: u32) -> ! { + let f: unsafe extern "C" fn() -> ! = unsafe { core::mem::transmute(addr as usize) }; + unsafe { f() } +} + +pub fn system_reset() -> ! { + use ch32_metapac::pfic::vals::{Keycode, Sysreset}; + + // Clear reset status flags (RMVF) โ€” required for boot mode transition + ch32_metapac::RCC.rstsckr().write(|w| w.0 = 1 << 24); + ch32_metapac::PFIC.cfgr().write(|w| { + w.set_keycode(Keycode(0xBEEF)); + w.set_sysreset(Sysreset::RESET); + }); + loop { + core::hint::spin_loop(); + } +} diff --git a/ch32/hal/src/rcc/mod.rs b/ch32/hal/src/rcc/mod.rs index 8d362cc..4f63518 100644 --- a/ch32/hal/src/rcc/mod.rs +++ b/ch32/hal/src/rcc/mod.rs @@ -1,4 +1,5 @@ #[cfg_attr(rcc_v0, path = "v0.rs")] +#[cfg_attr(rcc_v1, path = "v1.rs")] mod family; pub use family::*; diff --git a/ch32/hal/src/rcc/v0.rs b/ch32/hal/src/rcc/v0.rs index 3be08fe..bc39617 100644 --- a/ch32/hal/src/rcc/v0.rs +++ b/ch32/hal/src/rcc/v0.rs @@ -9,10 +9,22 @@ pub fn enable_afio() { ch32_metapac::RCC.apb2pcenr().modify(|w| w.set_afioen(true)); } -pub fn enable_usart1() { - ch32_metapac::RCC - .apb2pcenr() - .modify(|w| w.set_usart1en(true)); +const USART1EN: u32 = 1 << 14; + +/// APB2 enable bit for USART `n`, or 0 if not on APB2. +pub const fn usart_apb2_bit(n: u8) -> u32 { + match n { + 1 => USART1EN, + _ => 0, + } +} + +/// Enable USART `n` clock on whichever bus it lives on. +pub fn enable_usart(n: u8) { + let bit = usart_apb2_bit(n); + if bit != 0 { + ch32_metapac::RCC.apb2pcenr().modify(|w| w.0 |= bit); + } } /// Batch-enable APB2 peripherals in a single write. diff --git a/ch32/hal/src/rcc/v1.rs b/ch32/hal/src/rcc/v1.rs new file mode 100644 index 0000000..89fcebe --- /dev/null +++ b/ch32/hal/src/rcc/v1.rs @@ -0,0 +1,53 @@ +pub fn enable_gpio(port_index: usize) { + // IOPxEN bits are consecutive starting at bit 2: IOPA=2, IOPB=3, IOPC=4, IOPD=5. + ch32_metapac::RCC + .apb2pcenr() + .modify(|w| w.0 |= 1 << (2 + port_index)); +} + +pub fn enable_afio() { + ch32_metapac::RCC.apb2pcenr().modify(|w| w.set_afioen(true)); +} + +const USART1EN: u32 = 1 << 14; + +/// APB2 enable bit for USART `n`, or 0 if not on APB2. +pub const fn usart_apb2_bit(n: u8) -> u32 { + match n { + 1 => USART1EN, + _ => 0, + } +} + +/// Enable USART `n` clock on whichever bus it lives on. +pub fn enable_usart(n: u8) { + let bit = usart_apb2_bit(n); + if bit != 0 { + ch32_metapac::RCC.apb2pcenr().modify(|w| w.0 |= bit); + } else { + let rcc = ch32_metapac::RCC; + match n { + 2 => rcc.apb1pcenr().modify(|w| w.set_usart2en(true)), + 3 => rcc.apb1pcenr().modify(|w| w.set_usart3en(true)), + _ => {} + } + } +} + +/// Batch-enable APB2 peripherals in a single write. +/// Only safe at init before other peripherals are enabled. +#[inline(always)] +pub fn enable_apb2(bits: u32) { + ch32_metapac::RCC.apb2pcenr().write(|w| w.0 = bits); +} + +/// Pulse-reset all APB2 peripherals (USART1, GPIO, AFIO, etc.) +/// then disable their clocks โ€” restores power-on default state. +#[inline(always)] +pub fn reset_apb2() { + let rcc = ch32_metapac::RCC; + let enabled = rcc.apb2pcenr().read().0; + rcc.apb2prstr().write(|w| w.0 = enabled); + rcc.apb2prstr().write(|w| w.0 = 0); + rcc.apb2pcenr().write(|w| w.0 = 0); +} diff --git a/ch32/hal/tb-user-flash.x b/ch32/hal/tb-boot-req.x similarity index 69% rename from ch32/hal/tb-user-flash.x rename to ch32/hal/tb-boot-req.x index 306c774..2c43733 100644 --- a/ch32/hal/tb-user-flash.x +++ b/ch32/hal/tb-boot-req.x @@ -1,8 +1,9 @@ -/* User-flash boot request word. +/* Boot request magic word. * * Reserves 4 bytes at the start of RAM (NOLOAD, preserved across * soft resets). Both bootloader and app must link this script. - * Add -Ttb-user-flash.x to linker flags. */ + * Used by the ram and gpio boot control schemes. + * Add -Ttb-boot-req.x to linker flags. */ SECTIONS { .boot_request ORIGIN(RAM) (NOLOAD) : diff --git a/cli/src/client.rs b/cli/src/client.rs index 46607b4..1bb9447 100644 --- a/cli/src/client.rs +++ b/cli/src/client.rs @@ -158,6 +158,7 @@ impl Client { } // 3. Write โ€” chunk by MAX_PAYLOAD, pad to 4-byte alignment with 0xFF + let page_size = erase_size as usize; let padded_len = firmware.len().next_multiple_of(4); let total_chunks = padded_len.div_ceil(MAX_PAYLOAD) as u32; let mut offset = 0usize; @@ -176,7 +177,19 @@ impl Client { self.frame.cmd = Cmd::Write; self.frame.addr = offset as u32; self.frame.len = len as u16; - self.transact()?; + match self.transact() { + Ok(()) => {} + Err(FlashError::Device(Status::CrcMismatch)) => { + // Response frame corrupted โ€” flush device write buffer + // and restart from the nearest page-aligned offset. + debug!("CRC mismatch at {offset:#X}, retrying from page boundary"); + let _ = self.flush(); + offset &= !(page_size - 1); + chunk_idx = (offset / MAX_PAYLOAD) as u32; + continue; + } + Err(e) => return Err(e), + } offset = end; chunk_idx += 1; on_progress("Writing", chunk_idx, total_chunks); diff --git a/docs/boot-ctl-flip-flop.png b/docs/boot-ctl-flip-flop.png new file mode 100644 index 0000000..8284612 Binary files /dev/null and b/docs/boot-ctl-flip-flop.png differ diff --git a/docs/boot-ctl-rc.png b/docs/boot-ctl-rc.png new file mode 100644 index 0000000..0000116 Binary files /dev/null and b/docs/boot-ctl-rc.png differ diff --git a/docs/boot-ctl.md b/docs/boot-ctl.md new file mode 100644 index 0000000..001924c --- /dev/null +++ b/docs/boot-ctl.md @@ -0,0 +1,73 @@ +# GPIO-Controlled Boot Mode Selection + +Some MCUs (e.g. CH32V103) use hardware boot pins (BOOT0/BOOT1) to select the boot +source at reset. If you choose to run tinyboot from system flash, you need BOOT0 +to default HIGH on power-on (system flash), and provide a way for firmware to +switch to user flash (BOOT0 LOW) across a reset. + +This document describes two circuits for controlling BOOT0 from a single GPIO pin +(BOOT_CTL). Both share the same firmware interface. + +Note: BOOT1 is tied to GND in both circuits. + +## BOOT_CTL GPIO Truth Table + +| BOOT_CTL | Effect | +| ------------------ | ----------------------------- | +| HIGH | Boots system flash (tinyboot) | +| LOW | Boots user flash (app) | + +## Circuit Option 1: RC (Recommended) + +![RC Circuit](boot-ctl-rc.png) + +**How it works:** + +- R1 (10K) pulls BOOT0 HIGH by default (system flash). +- When BOOT_CTL drives LOW, it overpowers the pull-up through R2 (1K), pulling + BOOT0 LOW and discharging C1. +- On reset, the GPIO goes Hi-Z. The discharged capacitor holds BOOT0 LOW long + enough for the chip to sample it (RC time constant = 10K x 100nF = 1ms). +- The capacitor then recharges through R1, returning BOOT0 to HIGH. On any + subsequent reset, the chip boots back into system flash automatically. + +**Tested:** Yes, verified on CH32V103C8T6. The RC time constant of 1ms provides +sufficient hold time across a software-triggered system reset. The capacitor +charges fast enough during power-on reset (POR) that BOOT0 reads HIGH on first +boot. + +## Circuit Option 2: Flip-Flop + +![Flip-Flop Circuit](boot-ctl-flip-flop.png) + +**How it works:** + +- The NPN transistor inverts BOOT_CTL to drive /PRE (active-low preset). +- BOOT_CTL drives /CLR (active-low clear) directly through a 1K series resistor. +- When BOOT_CTL = HIGH: transistor ON, /PRE = LOW (active), Q = HIGH (system flash). +- When BOOT_CTL = LOW: transistor OFF, /CLR = LOW (active), Q = LOW (user flash). +- When BOOT_CTL = Hi-Z (during reset): transistor OFF (/PRE inactive via pull-up), + /CLR inactive (via pull-up). The flip-flop holds its previous state. +- The two 1K series resistors isolate the transistor base from /CLR, preventing + the /CLR pull-up from turning on the transistor during Hi-Z. + +**Power-on state:** The 74AUC1G74 does not have a guaranteed power-on state. An +optional 100nF capacitor from /PRE to GND can be added to force Q HIGH on first +power-up. + +**Tested:** Not yet verified on hardware. The design is sound in theory but has not +been tested. Let me know if you can confirm that it works. + +## Which to Choose + +The RC circuit is simpler (3 passive components, no IC) and has been validated on +hardware. It relies on the capacitor holding BOOT0 LOW for ~1ms across a reset, +which is well within the margin for software-triggered resets. + +The flip-flop circuit is deterministic (no timing dependency) and holds state +indefinitely, but requires more components and has not been tested. It may be +preferable in designs if you need reliable state retention, e.g. a real product +that requires high reliability. + +For most use cases though, my personal opinion is the RC circuit is probably reliable +enough and simpler / lower cost. diff --git a/examples/ch32/v003/Cargo.lock b/examples/ch32/v003/Cargo.lock index 87c8dba..8fdec8a 100644 --- a/examples/ch32/v003/Cargo.lock +++ b/examples/ch32/v003/Cargo.lock @@ -463,8 +463,7 @@ dependencies = [ [[package]] name = "qingke" version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10318bf64fc0befbcd09d7a9a62d8b99972ba3dfdd4a94695f63fd892f47e14e" +source = "git+https://github.com/OpenServoCore/qingke?branch=main#44d490b2ff429ed4689bd7dd1653146f4fff506a" dependencies = [ "bit_field", "cfg-if", @@ -475,8 +474,7 @@ dependencies = [ [[package]] name = "qingke-rt" version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8ba56c91dd9a044bd519778bcf70644d1d936fee4ea20fdc2d0e3bac8f75fb9" +source = "git+https://github.com/OpenServoCore/qingke?branch=main#44d490b2ff429ed4689bd7dd1653146f4fff506a" dependencies = [ "qingke", "qingke-rt-macros", @@ -485,8 +483,7 @@ dependencies = [ [[package]] name = "qingke-rt-macros" version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54edcd0e9b8b5c3c83d313c93869325bfcd969d30d9cef1a1015b0970b21be81" +source = "git+https://github.com/OpenServoCore/qingke?branch=main#44d490b2ff429ed4689bd7dd1653146f4fff506a" dependencies = [ "proc-macro-error", "proc-macro2", @@ -634,7 +631,6 @@ dependencies = [ "critical-section", "embedded-io 0.7.1", "embedded-io-async 0.7.0", - "qingke", "tinyboot-ch32-hal", "tinyboot-core", "tinyboot-protocol", diff --git a/examples/ch32/v003/Cargo.toml b/examples/ch32/v003/Cargo.toml index 31184b2..71cf130 100644 --- a/examples/ch32/v003/Cargo.toml +++ b/examples/ch32/v003/Cargo.toml @@ -17,3 +17,7 @@ opt-level = "z" lto = true codegen-units = 1 panic = "abort" + +[patch.crates-io] +qingke = { git = "https://github.com/OpenServoCore/qingke", branch = "main" } +qingke-rt = { git = "https://github.com/OpenServoCore/qingke", branch = "main" } diff --git a/examples/ch32/v003/app/build.rs b/examples/ch32/v003/app/build.rs index f35d966..f70a4c5 100644 --- a/examples/ch32/v003/app/build.rs +++ b/examples/ch32/v003/app/build.rs @@ -22,12 +22,8 @@ fn main() { println!("cargo:rustc-link-search={out_dir}"); println!("cargo:rerun-if-changed=memory_x"); println!("cargo:rustc-link-arg=-Ttb-app.x"); + println!("cargo:rustc-link-arg=-Ttb-boot-req.x"); println!("cargo:rustc-link-arg=-Tdefmt.x"); - - if user_flash { - println!("cargo:rustc-link-arg=-Ttb-user-flash.x"); - println!("cargo:rustc-link-arg=--wrap=_setup_interrupts"); - } } fn cfg_has(key: &str) -> bool { diff --git a/examples/ch32/v003/app/src/main.rs b/examples/ch32/v003/app/src/main.rs index d37f98b..63d227b 100644 --- a/examples/ch32/v003/app/src/main.rs +++ b/examples/ch32/v003/app/src/main.rs @@ -23,8 +23,6 @@ use critical_section::Mutex; use defmt_rtt as _; -#[cfg(feature = "user-flash")] -tinyboot_ch32_app::fix_mtvec!(); tinyboot_ch32_app::app_version!(); #[panic_handler] @@ -66,7 +64,7 @@ fn main() -> ! { let mut tx = transport::Tx(tx); // Tinyboot app client - let mut app = tinyboot_ch32_app::new_app(); + let mut app = tinyboot_ch32_app::new_app(tinyboot_ch32_app::BootCtlConfig); app.confirm(); defmt::info!("Boot confirmed, app ready."); diff --git a/examples/ch32/v003/boot/build.rs b/examples/ch32/v003/boot/build.rs index bcc7d1b..0fcd839 100644 --- a/examples/ch32/v003/boot/build.rs +++ b/examples/ch32/v003/boot/build.rs @@ -22,10 +22,7 @@ fn main() { println!("cargo:rustc-link-search={out_dir}"); println!("cargo:rerun-if-changed=memory_x"); println!("cargo:rustc-link-arg=-Ttb-boot.x"); - - if user_flash { - println!("cargo:rustc-link-arg=-Ttb-user-flash.x"); - } + println!("cargo:rustc-link-arg=-Ttb-boot-req.x"); } fn cfg_has(key: &str) -> bool { diff --git a/examples/ch32/v003/boot/src/main.rs b/examples/ch32/v003/boot/src/main.rs index d3fe607..bf8802f 100644 --- a/examples/ch32/v003/boot/src/main.rs +++ b/examples/ch32/v003/boot/src/main.rs @@ -40,5 +40,5 @@ fn main() -> ! { rx_pull: Pull::None, tx_en: None, }); - tinyboot_ch32_boot::run(transport); + tinyboot_ch32_boot::run(transport, BootCtlConfig); } diff --git a/examples/ch32/v103/.cargo/config.toml b/examples/ch32/v103/.cargo/config.toml new file mode 100644 index 0000000..9569bd5 --- /dev/null +++ b/examples/ch32/v103/.cargo/config.toml @@ -0,0 +1,13 @@ +[build] +target = "riscv32imc-unknown-none-elf" + +[target.riscv32imc-unknown-none-elf] +rustflags = [ + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=--nmagic", +] +runner = "probe-rs run --chip CH32V103" + +[env] +DEFMT_LOG = "trace" +DEFMT_RTT_BUFFER_SIZE = "128" diff --git a/examples/ch32/v103/Cargo.lock b/examples/ch32/v103/Cargo.lock new file mode 100644 index 0000000..8fdec8a --- /dev/null +++ b/examples/ch32/v103/Cargo.lock @@ -0,0 +1,701 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "app" +version = "3.2.1" +dependencies = [ + "ch32-hal", + "critical-section", + "defmt", + "defmt-rtt", + "embedded-io 0.7.1", + "qingke-rt", + "tinyboot-ch32-app", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "boot" +version = "1.2.3" +dependencies = [ + "panic-halt", + "tinyboot-ch32-boot", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ch32-hal" +version = "0.1.0" +source = "git+https://github.com/ch32-rs/ch32-hal#2ed4f3cab40f42b81d7355f1632ae4cb9ad4d5f1" +dependencies = [ + "bitmaps", + "ch32-metapac 0.1.0", + "critical-section", + "embassy-futures", + "embassy-hal-internal", + "embassy-sync", + "embassy-usb-driver", + "embedded-can", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-hal-nb", + "embedded-storage", + "futures", + "nb 1.1.0", + "proc-macro2", + "qingke", + "qingke-rt", + "quote", + "rand_core", + "sdio-host", +] + +[[package]] +name = "ch32-metapac" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc8e84ea6d10960c6018df65845a5dfa87dfefbc3bb9f058b6ed2a24fec1db6" +dependencies = [ + "riscv 0.11.1", + "vcell", +] + +[[package]] +name = "ch32-metapac" +version = "0.1.0" +source = "git+https://github.com/ch32-rs/ch32-metapac?rev=7cd6fb70d07de3a987c63d0d8f5547bb8261e403#7cd6fb70d07de3a987c63d0d8f5547bb8261e403" +dependencies = [ + "riscv 0.12.1", + "vcell", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt-rtt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d5a25c99d89c40f5676bec8cefe0614f17f0f40e916f98e345dae941807f9e" +dependencies = [ + "critical-section", + "defmt", +] + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" + +[[package]] +name = "embassy-hal-internal" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95285007a91b619dc9f26ea8f55452aa6c60f7115a4edc05085cd2bd3127cd7a" +dependencies = [ + "num-traits", +] + +[[package]] +name = "embassy-sync" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async 0.6.1", + "futures-core", + "futures-sink", + "heapless", +] + +[[package]] +name = "embassy-usb-driver" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17119855ccc2d1f7470a39756b12068454ae27a3eabb037d940b5c03d9c77b7a" +dependencies = [ + "embedded-io-async 0.6.1", +] + +[[package]] +name = "embedded-can" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d2e857f87ac832df68fa498d18ddc679175cf3d2e4aa893988e5601baf9438" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal 1.0.0", + "nb 1.1.0", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io 0.6.1", +] + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "embedded-io 0.7.1", +] + +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "panic-halt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qingke" +version = "0.6.1" +source = "git+https://github.com/OpenServoCore/qingke?branch=main#44d490b2ff429ed4689bd7dd1653146f4fff506a" +dependencies = [ + "bit_field", + "cfg-if", + "critical-section", + "riscv 0.15.0", +] + +[[package]] +name = "qingke-rt" +version = "0.6.1" +source = "git+https://github.com/OpenServoCore/qingke?branch=main#44d490b2ff429ed4689bd7dd1653146f4fff506a" +dependencies = [ + "qingke", + "qingke-rt-macros", +] + +[[package]] +name = "qingke-rt-macros" +version = "0.6.1" +source = "git+https://github.com/OpenServoCore/qingke?branch=main#44d490b2ff429ed4689bd7dd1653146f4fff506a" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "riscv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5c1b8bf41ea746266cdee443d1d1e9125c86ce1447e1a2615abd34330d33a9" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", +] + +[[package]] +name = "riscv" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea8ff73d3720bdd0a97925f0bf79ad2744b6da8ff36be3840c48ac81191d7a7" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", + "paste", + "riscv-macros 0.1.0", + "riscv-pac", +] + +[[package]] +name = "riscv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", + "paste", + "riscv-macros 0.3.0", + "riscv-pac", +] + +[[package]] +name = "riscv-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f265be5d634272320a7de94cea15c22a3bfdd4eb42eb43edc528415f066a1f25" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "riscv-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "riscv-pac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" + +[[package]] +name = "sdio-host" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93c025f9cfe4c388c328ece47d11a54a823da3b5ad0370b22d95ad47137f85a" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tinyboot-ch32-app" +version = "0.2.1" +dependencies = [ + "critical-section", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "tinyboot-ch32-hal", + "tinyboot-core", + "tinyboot-protocol", +] + +[[package]] +name = "tinyboot-ch32-boot" +version = "0.2.1" +dependencies = [ + "embedded-io 0.7.1", + "embedded-storage", + "tinyboot-ch32-hal", + "tinyboot-core", + "tinyboot-protocol", +] + +[[package]] +name = "tinyboot-ch32-hal" +version = "0.2.1" +dependencies = [ + "ch32-metapac 0.0.6", + "critical-section", + "embedded-hal 1.0.0", + "embedded-io 0.7.1", + "embedded-storage", +] + +[[package]] +name = "tinyboot-core" +version = "0.2.1" +dependencies = [ + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "embedded-storage", + "tinyboot-protocol", +] + +[[package]] +name = "tinyboot-protocol" +version = "0.2.1" +dependencies = [ + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/examples/ch32/v103/Cargo.toml b/examples/ch32/v103/Cargo.toml new file mode 100644 index 0000000..71cf130 --- /dev/null +++ b/examples/ch32/v103/Cargo.toml @@ -0,0 +1,23 @@ +[workspace] +members = ["boot", "app"] +resolver = "2" + +[workspace.package] +edition = "2024" + +[profile.dev] +opt-level = "z" +lto = true +codegen-units = 1 +debug = 2 +debug-assertions = false + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" + +[patch.crates-io] +qingke = { git = "https://github.com/OpenServoCore/qingke", branch = "main" } +qingke-rt = { git = "https://github.com/OpenServoCore/qingke", branch = "main" } diff --git a/examples/ch32/v103/app/Cargo.toml b/examples/ch32/v103/app/Cargo.toml new file mode 100644 index 0000000..e0e7b5d --- /dev/null +++ b/examples/ch32/v103/app/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "app" +version = "3.2.1" +edition.workspace = true + +[dependencies] +tinyboot-ch32-app = { path = "../../../../ch32/app", default-features = false } +embedded-io = "0.7" +critical-section = "1" +ch32-hal = { git = "https://github.com/ch32-rs/ch32-hal", default-features = false } +qingke-rt = { version = "0.6", features = ["v3a"] } +defmt = "1.0" +defmt-rtt = "1.1" + +[features] +default = ["ch32v103c8t6", "system-flash"] +system-flash = ["tinyboot-ch32-app/system-flash"] +user-flash = [] +ch32v103c6t6 = ["tinyboot-ch32-app/ch32v103c6t6", "ch32-hal/ch32v103c6t6", "ch32-hal/rt"] +ch32v103c8t6 = ["tinyboot-ch32-app/ch32v103c8t6", "ch32-hal/ch32v103c8t6", "ch32-hal/rt"] +ch32v103c8u6 = ["tinyboot-ch32-app/ch32v103c8u6", "ch32-hal/ch32v103c8u6", "ch32-hal/rt"] +ch32v103r8t6 = ["tinyboot-ch32-app/ch32v103r8t6", "ch32-hal/ch32v103r8t6", "ch32-hal/rt"] diff --git a/examples/ch32/v103/app/build.rs b/examples/ch32/v103/app/build.rs new file mode 100644 index 0000000..f70a4c5 --- /dev/null +++ b/examples/ch32/v103/app/build.rs @@ -0,0 +1,31 @@ +fn main() { + let system_flash = cfg_has("CARGO_FEATURE_SYSTEM_FLASH"); + let user_flash = cfg_has("CARGO_FEATURE_USER_FLASH"); + + match (system_flash, user_flash) { + (true, false) | (false, true) => {} + _ => panic!("Enable exactly one flash mode: `system-flash` or `user-flash`"), + } + + let flash_mode = if system_flash { + "system-flash" + } else { + "user-flash" + }; + + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let out_dir = std::env::var("OUT_DIR").unwrap(); + let src = format!("{manifest_dir}/memory_x/{flash_mode}.x"); + let dst = format!("{out_dir}/memory.x"); + std::fs::copy(&src, &dst).unwrap_or_else(|e| panic!("copy {src} -> {dst}: {e}")); + + println!("cargo:rustc-link-search={out_dir}"); + println!("cargo:rerun-if-changed=memory_x"); + println!("cargo:rustc-link-arg=-Ttb-app.x"); + println!("cargo:rustc-link-arg=-Ttb-boot-req.x"); + println!("cargo:rustc-link-arg=-Tdefmt.x"); +} + +fn cfg_has(key: &str) -> bool { + std::env::var_os(key).is_some() +} diff --git a/examples/ch32/v103/app/memory_x/system-flash.x b/examples/ch32/v103/app/memory_x/system-flash.x new file mode 100644 index 0000000..7421240 --- /dev/null +++ b/examples/ch32/v103/app/memory_x/system-flash.x @@ -0,0 +1,28 @@ +/* CH32V103 application memory layout for system-flash bootloader. + * + * The application occupies all user flash minus the last page (boot metadata). + * The bootloader lives in the separate system flash region. + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 20K + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000, LENGTH = 64K - 128 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFFF000, LENGTH = 2048 + APP : ORIGIN = 0x08000000, LENGTH = 64K - 128 + META : ORIGIN = 0x08000000 + 64K - 128, LENGTH = 128 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +/* Region aliases required by qingke-rt */ +REGION_ALIAS("REGION_TEXT", CODE); +REGION_ALIAS("REGION_RODATA", CODE); +REGION_ALIAS("REGION_DATA", RAM); +REGION_ALIAS("REGION_BSS", RAM); +REGION_ALIAS("REGION_HEAP", RAM); +REGION_ALIAS("REGION_STACK", RAM); diff --git a/examples/ch32/v103/app/memory_x/user-flash.x b/examples/ch32/v103/app/memory_x/user-flash.x new file mode 100644 index 0000000..e4f743d --- /dev/null +++ b/examples/ch32/v103/app/memory_x/user-flash.x @@ -0,0 +1,33 @@ +/* CH32V103 application memory layout for user-flash bootloader. + * + * The application occupies user flash between the bootloader and + * boot metadata regions. + * + * Flash map: + * 0x0000_0000 .. 0x0000_1FFF bootloader 8KB + * 0x0000_2000 .. 0x0000_FF7F application 56KB - 128B + * 0x0000_FF80 .. 0x0000_FFFF boot meta 128B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 20K + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000 + 8K, LENGTH = 56K - 128 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 8K + APP : ORIGIN = 0x08000000 + 8K, LENGTH = 56K - 128 + META : ORIGIN = 0x08000000 + 64K - 128, LENGTH = 128 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +/* Region aliases required by qingke-rt */ +REGION_ALIAS("REGION_TEXT", CODE); +REGION_ALIAS("REGION_RODATA", CODE); +REGION_ALIAS("REGION_DATA", RAM); +REGION_ALIAS("REGION_BSS", RAM); +REGION_ALIAS("REGION_HEAP", RAM); +REGION_ALIAS("REGION_STACK", RAM); diff --git a/examples/ch32/v103/app/src/main.rs b/examples/ch32/v103/app/src/main.rs new file mode 100644 index 0000000..07793ed --- /dev/null +++ b/examples/ch32/v103/app/src/main.rs @@ -0,0 +1,94 @@ +//! Example application for the tinyboot bootloader (CH32V103). +//! +//! - Timer interrupt blinks LED on PC13 every second +//! - Main loop listens on USART1 (TX=PA9, RX=PA10) for tinyboot commands, +//! reboots into bootloader on receipt of Reset command + +#![no_std] +#![no_main] + +mod transport; + +use core::cell::RefCell; + +use ch32_hal::gpio::{Level, Output}; +use ch32_hal::interrupt::InterruptExt; +use ch32_hal::pac; +use ch32_hal::time::Hertz; +use ch32_hal::timer::low_level::Timer; +use ch32_hal::usart::{self, Uart}; +use critical_section::Mutex; + +use defmt_rtt as _; + +tinyboot_ch32_app::app_version!(); + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + defmt::error!("panic"); + loop {} +} + +type Shared = Mutex>>; +static LED: Shared> = Mutex::new(RefCell::new(None)); + +#[qingke_rt::entry] +fn main() -> ! { + let p = ch32_hal::init(Default::default()); + + // LED blink via TIM2 interrupt (2 Hz toggle = 1 Hz blink) + critical_section::with(|cs| { + LED.borrow_ref_mut(cs) + .replace(Output::new(p.PC13, Level::Low, Default::default())); + }); + let tim = Timer::new(p.TIM2); + tim.set_frequency(Hertz::hz(2)); + tim.enable_update_interrupt(true); + tim.start(); + unsafe { ch32_hal::interrupt::TIM2.enable() }; + + // USART1 blocking โ€” must match the bootloader's pin mapping. + // + // Remap options (CH32V103, ch32-hal generic param): + // 0 (Remap0): TX=PA9, RX=PA10 (default) + // 1 (Remap1): TX=PB6, RX=PB7 + let mut uart_config = usart::Config::default(); + uart_config.baudrate = 115200; + let uart = Uart::new_blocking::<0>(p.USART1, p.PA10, p.PA9, uart_config).unwrap(); + let (tx, rx) = uart.split(); + let mut rx = transport::Rx(rx); + let mut tx = transport::Tx(tx); + + // Tinyboot app client + #[cfg(feature = "system-flash")] + let mut app = tinyboot_ch32_app::new_app(tinyboot_ch32_app::BootCtlConfig { + pin: tinyboot_ch32_app::Pin::PB1, // adjust to your BOOT0 control pin + active_high: true, + }); + + #[cfg(not(feature = "system-flash"))] + let mut app = tinyboot_ch32_app::new_app(tinyboot_ch32_app::BootCtlConfig); + + app.confirm(); + + defmt::info!("Boot confirmed, app ready."); + + loop { + app.poll(&mut rx, &mut tx); + } +} + +#[qingke_rt::interrupt] +fn TIM2() { + pac::TIM2.intfr().modify(|w| w.set_uif(false)); + critical_section::with(|cs| { + if let Some(ref mut led) = *LED.borrow_ref_mut(cs) { + led.toggle(); + if led.is_set_high() { + defmt::info!("LED on"); + } else { + defmt::info!("LED off"); + } + } + }); +} diff --git a/examples/ch32/v103/app/src/transport.rs b/examples/ch32/v103/app/src/transport.rs new file mode 100644 index 0000000..1b08e54 --- /dev/null +++ b/examples/ch32/v103/app/src/transport.rs @@ -0,0 +1,40 @@ +//! embedded_io adapters for ch32-hal blocking UART. + +use ch32_hal::mode::Blocking; +use ch32_hal::usart::{Instance, UartRx, UartTx}; +use core::convert::Infallible; +use embedded_io::{ErrorType, Read, Write}; + +pub struct Rx<'d, T: Instance>(pub UartRx<'d, T, Blocking>); + +impl ErrorType for Rx<'_, T> { + type Error = Infallible; +} + +impl Read for Rx<'_, T> { + fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + let _ = self.0.blocking_read(&mut buf[..1]); + Ok(1) + } +} + +pub struct Tx<'d, T: Instance>(pub UartTx<'d, T, Blocking>); + +impl ErrorType for Tx<'_, T> { + type Error = Infallible; +} + +impl Write for Tx<'_, T> { + fn write(&mut self, buf: &[u8]) -> Result { + let _ = self.0.blocking_write(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + let _ = self.0.blocking_flush(); + Ok(()) + } +} diff --git a/examples/ch32/v103/boot/Cargo.toml b/examples/ch32/v103/boot/Cargo.toml new file mode 100644 index 0000000..6ddf961 --- /dev/null +++ b/examples/ch32/v103/boot/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "boot" +version = "1.2.3" +edition.workspace = true + +[dependencies] +tinyboot-ch32-boot = { path = "../../../../ch32/boot", default-features = false } +panic-halt = "1.0" + +[features] +default = ["ch32v103c8t6", "system-flash"] +system-flash = ["tinyboot-ch32-boot/system-flash"] +user-flash = [] +ch32v103c6t6 = ["tinyboot-ch32-boot/ch32v103c6t6", "tinyboot-ch32-boot/rt"] +ch32v103c8t6 = ["tinyboot-ch32-boot/ch32v103c8t6", "tinyboot-ch32-boot/rt"] +ch32v103c8u6 = ["tinyboot-ch32-boot/ch32v103c8u6", "tinyboot-ch32-boot/rt"] +ch32v103r8t6 = ["tinyboot-ch32-boot/ch32v103r8t6", "tinyboot-ch32-boot/rt"] diff --git a/examples/ch32/v103/boot/build.rs b/examples/ch32/v103/boot/build.rs new file mode 100644 index 0000000..0fcd839 --- /dev/null +++ b/examples/ch32/v103/boot/build.rs @@ -0,0 +1,30 @@ +fn main() { + let system_flash = cfg_has("CARGO_FEATURE_SYSTEM_FLASH"); + let user_flash = cfg_has("CARGO_FEATURE_USER_FLASH"); + + match (system_flash, user_flash) { + (true, false) | (false, true) => {} + _ => panic!("Enable exactly one flash mode: `system-flash` or `user-flash`"), + } + + let flash_mode = if system_flash { + "system-flash" + } else { + "user-flash" + }; + + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let out_dir = std::env::var("OUT_DIR").unwrap(); + let src = format!("{manifest_dir}/memory_x/{flash_mode}.x"); + let dst = format!("{out_dir}/memory.x"); + std::fs::copy(&src, &dst).unwrap_or_else(|e| panic!("copy {src} -> {dst}: {e}")); + + println!("cargo:rustc-link-search={out_dir}"); + println!("cargo:rerun-if-changed=memory_x"); + println!("cargo:rustc-link-arg=-Ttb-boot.x"); + println!("cargo:rustc-link-arg=-Ttb-boot-req.x"); +} + +fn cfg_has(key: &str) -> bool { + std::env::var_os(key).is_some() +} diff --git a/examples/ch32/v103/boot/memory_x/system-flash.x b/examples/ch32/v103/boot/memory_x/system-flash.x new file mode 100644 index 0000000..03728e4 --- /dev/null +++ b/examples/ch32/v103/boot/memory_x/system-flash.x @@ -0,0 +1,23 @@ +/* CH32V103 system-flash bootloader memory layout. + * + * The bootloader runs from the first 2048-byte system flash region. + * All 64KB of user flash is available for the application. + * Boot metadata occupies the last page of user flash. + * + * System flash is split by option bytes: + * 0x1FFFF000 .. 0x1FFFF7FF region 1 2048 bytes (used) + * 0x1FFFF800 .. 0x1FFFF8FF option bytes (gap) + * 0x1FFFF900 .. 0x1FFFFFFD region 2 1792 bytes (unused for now) + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 20K + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 2048 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFFF000, LENGTH = 2048 + APP : ORIGIN = 0x08000000, LENGTH = 64K - 128 + META : ORIGIN = 0x08000000 + 64K - 128, LENGTH = 128 +} diff --git a/examples/ch32/v103/boot/memory_x/user-flash.x b/examples/ch32/v103/boot/memory_x/user-flash.x new file mode 100644 index 0000000..7d52bbe --- /dev/null +++ b/examples/ch32/v103/boot/memory_x/user-flash.x @@ -0,0 +1,22 @@ +/* CH32V103 user-flash bootloader memory layout. + * + * The bootloader occupies the first 8KB of the 64KB user flash. + * Boot metadata occupies the last page of user flash. + * + * Flash map: + * 0x0800_0000 .. 0x0800_1FFF bootloader 8KB + * 0x0800_2000 .. 0x0800_FF7F application 56KB - 128B + * 0x0800_FF80 .. 0x0800_FFFF boot meta 128B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 20K + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 8K + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 8K + APP : ORIGIN = 0x08000000 + 8K, LENGTH = 56K - 128 + META : ORIGIN = 0x08000000 + 64K - 128, LENGTH = 128 +} diff --git a/examples/ch32/v103/boot/src/main.rs b/examples/ch32/v103/boot/src/main.rs new file mode 100644 index 0000000..10ebe13 --- /dev/null +++ b/examples/ch32/v103/boot/src/main.rs @@ -0,0 +1,50 @@ +//! Bootloader example for CH32V103. +//! +//! Two flash modes available via feature flags: +//! +//! **system-flash**: Runs from the 2048-byte system flash region, leaving all +//! 64KB of user flash for the application. Requires external BOOT0 control +//! circuit (RC or flip-flop) on the configured GPIO pin. +//! +//! **user-flash**: Occupies first 8KB of user flash, with the application in +//! the remaining 56KB. + +#![no_std] +#![no_main] + +use panic_halt as _; + +tinyboot_ch32_boot::boot_version!(); + +use tinyboot_ch32_boot::prelude::*; + +#[unsafe(export_name = "main")] +fn main() -> ! { + // USART1 transport for firmware updates. + // + // Remap options (CH32V103): + // Remap0: TX=PA9, RX=PA10 (default) + // Remap1: TX=PB6, RX=PB7 + let transport = Usart::new(&UsartConfig { + duplex: Duplex::Full, + baud: BaudRate::B115200, + pclk: 8_000_000, + mapping: UsartMapping::Usart1Remap0, + rx_pull: Pull::None, + tx_en: None, + }); + + // V103 system-flash: configure GPIO pin driving the external BOOT0 + // control circuit (RC or flip-flop). Adjust pin to your hardware. + #[cfg(feature = "system-flash")] + let config = BootCtlConfig { + pin: Pin::PB1, // adjust to your BOOT0 control pin + active_high: true, // RC circuit: HIGH = system flash + }; + + // V103 user-flash: no GPIO boot control needed, uses RAM magic word. + #[cfg(not(feature = "system-flash"))] + let config = BootCtlConfig; + + tinyboot_ch32_boot::run(transport, config); +} diff --git a/examples/ch32/v103/rust-toolchain.toml b/examples/ch32/v103/rust-toolchain.toml new file mode 100644 index 0000000..db4fae7 --- /dev/null +++ b/examples/ch32/v103/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src", "rustfmt", "clippy", "rust-analyzer"] diff --git a/lib/core/tb-app.x b/lib/core/tb-app.x index ff6a330..1016cc6 100644 --- a/lib/core/tb-app.x +++ b/lib/core/tb-app.x @@ -9,22 +9,26 @@ __tb_meta_base = ORIGIN(META); __tb_boot_version_addr = ORIGIN(BOOT) + LENGTH(BOOT) - 2; __tb_app_capacity = LENGTH(APP); -/* App version placed immediately after all other flash content. */ +/* App version must be the very last loadable flash content. + * The bootloader reads it at app_base + app_size - 2. + * + * Capture the qingke-rt interrupt vector table (orphan section on V3+) + * before .tb_version so it doesn't end up after the version tag. */ SECTIONS { - .tb_version ALIGN(2) : + .tb_vectors ALIGN(4) : { - __tb_version = .; - KEEP(*(.tb_version)); + KEEP(*(.vector_table.interrupts)); + KEEP(*(.vector_table.external_interrupts)); } > CODE } INSERT AFTER .data; -/* qingke-rt's link.x doesn't define a .uninit section. - * defmt-rtt places its buffer in .uninit.* and expects NOLOAD. */ SECTIONS { - .uninit (NOLOAD) : ALIGN(4) + .tb_version ALIGN(2) : { - *(.uninit .uninit.*); - } > RAM -} INSERT AFTER .bss; + __tb_version = .; + KEEP(*(.tb_version)); + } > CODE +} INSERT AFTER .tb_vectors; + diff --git a/scripts/pre-commit b/scripts/pre-commit index 5a44835..486bd26 100755 --- a/scripts/pre-commit +++ b/scripts/pre-commit @@ -7,16 +7,30 @@ set -e (cd lib && cargo test -p tinyboot-core -p tinyboot-protocol) # --- CH32 libraries (embedded target, needs nightly + build-std) --- -TARGET_JSON=../examples/ch32/v003/riscv32ec-unknown-none-elf.json -CHIP=ch32v003f4p6 +V003_JSON=../examples/ch32/v003/riscv32ec-unknown-none-elf.json + +# V003 (cd ch32 && cargo +nightly clippy -p tinyboot-ch32-hal -p tinyboot-ch32-boot -p tinyboot-ch32-app \ - --no-default-features --features $CHIP \ - --target $TARGET_JSON \ + --no-default-features --features ch32v003f4p6 \ + --target $V003_JSON \ -Zbuild-std=core -Zjson-target-spec \ -- -D warnings) +# V103 +(cd ch32 && cargo +nightly clippy -p tinyboot-ch32-hal -p tinyboot-ch32-boot \ + --no-default-features --features ch32v103c8t6 \ + --target riscv32imc-unknown-none-elf \ + -- -D warnings) + # --- Examples --- +# V003 for flash in system-flash user-flash; do (cd examples/ch32/v003 && cargo +nightly clippy -p boot --no-default-features --features "ch32v003f4p6,$flash" -- -D warnings) (cd examples/ch32/v003 && cargo +nightly clippy -p app --no-default-features --features "ch32v003f4p6,$flash" -- -D warnings) done + +# V103 +for flash in system-flash user-flash; do + (cd examples/ch32/v103 && cargo +nightly clippy -p boot --no-default-features --features "ch32v103c8t6,$flash" -- -D warnings) + (cd examples/ch32/v103 && cargo +nightly clippy -p app --no-default-features --features "ch32v103c8t6,$flash" -- -D warnings) +done diff --git a/vendor/README.md b/vendor/README.md index 2116aa5..1c77fdd 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -5,12 +5,20 @@ overwritten system flash and need to recover. ## Files -| File | Chip | Address | Size | -| --------------------------- | ------------ | ------------ | ---------- | -| `ch32v003-system-flash.bin` | CH32V003F4P6 | `0x1FFFF000` | 1920 bytes | +| File | Chip | Address | Size | +| ----------------------------- | ------------ | ------------ | ---------- | +| `ch32v003-system-flash.bin` | CH32V003 | `0x1FFFF000` | 1920 bytes | +| `ch32v006-system-flash.bin` | CH32V006 | `0x1FFFF000` | 3328 bytes | +| `ch32v103-system-flash-1.bin` | CH32V103 | `0x1FFFF000` | 2048 bytes | +| `ch32v103-system-flash-2.bin` | CH32V103 | `0x1FFFF900` | 1792 bytes | ## Restoring ```sh wlink flash vendor/ch32v003-system-flash.bin --address 0x1FFFF000 +wlink flash vendor/ch32v006-system-flash.bin --address 0x1FFFF000 + +# CH32V103 has split system flash regions, so we need to flash each region separately. +wlink flash vendor/ch32v103-system-flash-1.bin --address 0x1FFFF000 +wlink flash vendor/ch32v103-system-flash-2.bin --address 0x1FFFF900 ``` diff --git a/vendor/ch32v103-system-flash-1.bin b/vendor/ch32v103-system-flash-1.bin new file mode 100644 index 0000000..4580d9f Binary files /dev/null and b/vendor/ch32v103-system-flash-1.bin differ diff --git a/vendor/ch32v103-system-flash-2.bin b/vendor/ch32v103-system-flash-2.bin new file mode 100644 index 0000000..d482e3d Binary files /dev/null and b/vendor/ch32v103-system-flash-2.bin differ