From 41829124e07c43452a5548efe57aa6918e8caa93 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Fri, 17 Apr 2026 19:49:41 -0700 Subject: [PATCH 01/15] add feature flags --- ch32/Cargo.toml | 5 +++++ ch32/src/lib.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/ch32/Cargo.toml b/ch32/Cargo.toml index 12a666a..c97c5c2 100644 --- a/ch32/Cargo.toml +++ b/ch32/Cargo.toml @@ -29,6 +29,11 @@ ch32v003f4p6 = ["ch32-metapac/ch32v003f4p6"] ch32v003a4m6 = ["ch32-metapac/ch32v003a4m6"] ch32v003f4u6 = ["ch32-metapac/ch32v003f4u6"] ch32v003j4m6 = ["ch32-metapac/ch32v003j4m6"] +ch32v002x4x6 = ["ch32-metapac/ch32v002x4x6"] +ch32v004x6x1 = ["ch32-metapac/ch32v004x6x1"] +ch32v005x6x6 = ["ch32-metapac/ch32v005x6x6"] +ch32v006x8x6 = ["ch32-metapac/ch32v006x8x6"] +ch32v007x8x6 = ["ch32-metapac/ch32v007x8x6"] ch32v103c6t6 = ["ch32-metapac/ch32v103c6t6"] ch32v103c8t6 = ["ch32-metapac/ch32v103c8t6"] ch32v103c8u6 = ["ch32-metapac/ch32v103c8u6"] diff --git a/ch32/src/lib.rs b/ch32/src/lib.rs index 8ffdcb7..2e6b1e6 100644 --- a/ch32/src/lib.rs +++ b/ch32/src/lib.rs @@ -9,10 +9,15 @@ //! - [`app`] — app-side client. #[cfg(not(any( + feature = "ch32v002x4x6", feature = "ch32v003f4p6", feature = "ch32v003a4m6", feature = "ch32v003f4u6", feature = "ch32v003j4m6", + feature = "ch32v004x6x1", + feature = "ch32v005x6x6", + feature = "ch32v006x8x6", + feature = "ch32v007x8x6", feature = "ch32v103c6t6", feature = "ch32v103c8t6", feature = "ch32v103c8u6", From 776e6ff316dba564e08fe6daf3e44cfd5cfee94d Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Fri, 17 Apr 2026 19:50:23 -0700 Subject: [PATCH 02/15] exclude boot_pin config for v00x --- ch32/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ch32/build.rs b/ch32/build.rs index d668d2e..87defc1 100644 --- a/ch32/build.rs +++ b/ch32/build.rs @@ -24,7 +24,7 @@ fn main() -> Result<(), Box> { // boot_pin: chips with a hardware BOOT0 pin (V103) need an external // RC/flip-flop; others (V003) select via the BOOT_MODE register. - let boot_pin = !cfgs.iter().any(|c| c == "flash_v0"); + let boot_pin = !cfgs.iter().any(|c| c == "flash_v0" || c == "flash_v00x"); println!("cargo::rustc-check-cfg=cfg(boot_pin)"); if boot_pin { println!("cargo:rustc-cfg=boot_pin"); From 3eace65990c0d954fb014e443462a57008fa6ac3 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Fri, 17 Apr 2026 19:50:40 -0700 Subject: [PATCH 03/15] Add v00x flash hal --- ch32/src/hal/flash/mod.rs | 1 + ch32/src/hal/flash/v00x.rs | 106 +++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 ch32/src/hal/flash/v00x.rs diff --git a/ch32/src/hal/flash/mod.rs b/ch32/src/hal/flash/mod.rs index 67e5357..e409608 100644 --- a/ch32/src/hal/flash/mod.rs +++ b/ch32/src/hal/flash/mod.rs @@ -1,4 +1,5 @@ #[cfg_attr(flash_v0, path = "v0.rs")] +#[cfg_attr(flash_v00x, path = "v00x.rs")] #[cfg_attr(flash_v1, path = "v1.rs")] mod family; diff --git a/ch32/src/hal/flash/v00x.rs b/ch32/src/hal/flash/v00x.rs new file mode 100644 index 0000000..b33b752 --- /dev/null +++ b/ch32/src/hal/flash/v00x.rs @@ -0,0 +1,106 @@ +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" + ); + // Clear EOP (W1C) — required after every BUFRST, BUFLOAD, STRT. + FLASH.statr().write(|w| w.set_eop(true)); +} + +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)); +} + +#[inline(always)] +fn lock() { + FLASH.ctlr().write(|w| { + w.set_lock(true); + w.set_flock(true); + }); +} + +pub const PAGE_SIZE: usize = 256; +const BUF_LOAD_SIZE: usize = 4; + +/// Erase one 256-byte page (RM §18.4.6). +pub fn erase(addr: u32) { + unlock(); + FLASH.ctlr().write(|w| w.set_fter(true)); + FLASH.addr().write(|w| w.set_far(addr)); + FLASH.ctlr().write(|w| { + w.set_fter(true); + w.set_strt(true); + }); + wait_busy(); + FLASH.ctlr().write(|_| {}); + lock(); +} + +/// Fast-page write (RM §18.4.5). `addr` 4-byte aligned, `data.len()` +/// a multiple of 4, must not cross a page boundary. +pub fn write(addr: u32, data: &[u8]) { + let page_base = addr & !(PAGE_SIZE as u32 - 1); + debug_assert!( + (addr as usize & (BUF_LOAD_SIZE - 1)) == 0, + "write: addr not word-aligned" + ); + debug_assert!( + data.len().is_multiple_of(BUF_LOAD_SIZE), + "write: len not word-aligned" + ); + debug_assert!( + addr + data.len() as u32 <= page_base + PAGE_SIZE as u32, + "write: crosses page boundary" + ); + + unlock(); + FLASH.ctlr().write(|w| w.set_ftpg(true)); + FLASH.ctlr().write(|w| { + w.set_ftpg(true); + w.set_bufrst(true); + }); + wait_busy(); + + // Load each 4-byte chunk into the buffer. + let mut buf_addr = addr; + let mut ptr = data.as_ptr() as *const u32; + for _ in 0..data.len() / BUF_LOAD_SIZE { + let word = unsafe { ptr.read() }; + unsafe { core::ptr::write_volatile(buf_addr as *mut u32, word) }; + FLASH.ctlr().write(|w| { + w.set_ftpg(true); + w.set_bufload(true); + }); + wait_busy(); + buf_addr += BUF_LOAD_SIZE as u32; + ptr = unsafe { ptr.add(1) }; + } + + FLASH.addr().write(|w| w.set_far(page_base)); + FLASH.ctlr().write(|w| { + w.set_ftpg(true); + w.set_strt(true); + }); + wait_busy(); + FLASH.ctlr().write(|_| {}); + lock(); +} + +pub fn boot_mode() -> bool { + FLASH.statr().read().boot_mode() +} + +pub fn set_boot_mode(mode: bool) { + FLASH.boot_modekeyp().write(|w| w.set_modekeyr(KEY1)); + FLASH.boot_modekeyp().write(|w| w.set_modekeyr(KEY2)); + FLASH.statr().write(|w| w.set_boot_mode(mode)); +} From 125dfde97dad596e3a29aba69832b8a532f34cde Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Fri, 17 Apr 2026 19:51:13 -0700 Subject: [PATCH 04/15] add afio hal --- ch32/src/hal/afio/mod.rs | 1 + ch32/src/hal/afio/v00x.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 ch32/src/hal/afio/v00x.rs diff --git a/ch32/src/hal/afio/mod.rs b/ch32/src/hal/afio/mod.rs index 3ed9069..bde3051 100644 --- a/ch32/src/hal/afio/mod.rs +++ b/ch32/src/hal/afio/mod.rs @@ -1,4 +1,5 @@ #[cfg_attr(afio_v003, path = "v0.rs")] +#[cfg_attr(afio_v00x, path = "v00x.rs")] #[cfg_attr(afio_v3, path = "v3.rs")] mod family; diff --git a/ch32/src/hal/afio/v00x.rs b/ch32/src/hal/afio/v00x.rs new file mode 100644 index 0000000..6f52b7d --- /dev/null +++ b/ch32/src/hal/afio/v00x.rs @@ -0,0 +1,12 @@ +#[inline(always)] +pub fn set_usart_remap(n: u8, remap: u8) { + if remap == 0 { + return; + } + let afio = ch32_metapac::AFIO; + match n { + 1 => afio.pcfr1().modify(|w| w.set_usart1_rm(remap)), + 2 => afio.pcfr1().modify(|w| w.set_usart2_rm(remap)), + _ => {} + } +} From 6ed58fcaf57138d18ee3da93e0852d5b7fd1a9f3 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Fri, 17 Apr 2026 19:51:24 -0700 Subject: [PATCH 05/15] add rcc hal --- ch32/src/hal/rcc/mod.rs | 1 + ch32/src/hal/rcc/v00x.rs | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 ch32/src/hal/rcc/v00x.rs diff --git a/ch32/src/hal/rcc/mod.rs b/ch32/src/hal/rcc/mod.rs index 0dd9dcb..b86dc9a 100644 --- a/ch32/src/hal/rcc/mod.rs +++ b/ch32/src/hal/rcc/mod.rs @@ -1,4 +1,5 @@ #[cfg_attr(rcc_v003, path = "v0.rs")] +#[cfg_attr(rcc_v00x, path = "v00x.rs")] #[cfg_attr(rcc_v1, path = "v1.rs")] mod family; diff --git a/ch32/src/hal/rcc/v00x.rs b/ch32/src/hal/rcc/v00x.rs new file mode 100644 index 0000000..de77e47 --- /dev/null +++ b/ch32/src/hal/rcc/v00x.rs @@ -0,0 +1,45 @@ +pub fn enable_gpio(port_index: usize) { + // IOPxEN starts at bit 2: IOPA=2, IOPB=3, IOPC=4, IOPD=5. + ch32_metapac::RCC + .pb2pcenr() + .modify(|w| w.0 |= 1 << (2 + port_index)); +} + +pub fn enable_afio() { + ch32_metapac::RCC.pb2pcenr().modify(|w| w.set_afioen(true)); +} + +const USART1EN: u32 = 1 << 14; +const USART2EN: u32 = 1 << 13; + +/// PB2 bit for USART `n`, 0 if not on PB2. +pub const fn usart_apb2_bit(n: u8) -> u32 { + match n { + 1 => USART1EN, + 2 => USART2EN, + _ => 0, + } +} + +pub fn enable_usart(n: u8) { + let bit = usart_apb2_bit(n); + if bit != 0 { + ch32_metapac::RCC.pb2pcenr().modify(|w| w.0 |= bit); + } +} + +/// Set PB2 enables in one write. Safe only during init. +#[inline(always)] +pub fn enable_apb2(bits: u32) { + ch32_metapac::RCC.pb2pcenr().write(|w| w.0 = bits); +} + +/// Pulse-reset and disable all PB2 peripherals. +#[inline(always)] +pub fn reset_apb2() { + let rcc = ch32_metapac::RCC; + let enabled = rcc.pb2pcenr().read().0; + rcc.pb2prstr().write(|w| w.0 = enabled); + rcc.pb2prstr().write(|w| w.0 = 0); + rcc.pb2pcenr().write(|w| w.0 = 0); +} From 50127eb59cbf7182c76afd2d6cee8ac0920e04e8 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 00:05:45 -0700 Subject: [PATCH 06/15] add ch32v00x examples --- examples/ch32/v00x/.cargo/config.toml | 12 + examples/ch32/v00x/Cargo.lock | 674 ++++++++++++++++++ examples/ch32/v00x/Cargo.toml | 24 + examples/ch32/v00x/app/Cargo.toml | 20 + examples/ch32/v00x/app/build.rs | 42 ++ .../app/memory_x/ch32v002x4x6/system-flash.x | 27 + .../app/memory_x/ch32v002x4x6/user-flash.x | 29 + .../app/memory_x/ch32v004x6x1/system-flash.x | 27 + .../app/memory_x/ch32v004x6x1/user-flash.x | 29 + .../app/memory_x/ch32v005x6x6/system-flash.x | 27 + .../app/memory_x/ch32v005x6x6/user-flash.x | 29 + .../app/memory_x/ch32v006x8x6/standalone.x | 13 + .../app/memory_x/ch32v006x8x6/system-flash.x | 27 + .../app/memory_x/ch32v006x8x6/user-flash.x | 29 + .../app/memory_x/ch32v007x8x6/system-flash.x | 27 + .../app/memory_x/ch32v007x8x6/user-flash.x | 29 + examples/ch32/v00x/app/src/main.rs | 86 +++ examples/ch32/v00x/app/src/transport.rs | 40 ++ examples/ch32/v00x/boot/Cargo.toml | 19 + examples/ch32/v00x/boot/build.rs | 43 ++ .../boot/memory_x/ch32v002x4x6/system-flash.x | 18 + .../boot/memory_x/ch32v002x4x6/user-flash.x | 22 + .../boot/memory_x/ch32v004x6x1/system-flash.x | 18 + .../boot/memory_x/ch32v004x6x1/user-flash.x | 22 + .../boot/memory_x/ch32v005x6x6/system-flash.x | 18 + .../boot/memory_x/ch32v005x6x6/user-flash.x | 22 + .../boot/memory_x/ch32v006x8x6/system-flash.x | 18 + .../boot/memory_x/ch32v006x8x6/user-flash.x | 22 + .../boot/memory_x/ch32v007x8x6/system-flash.x | 18 + .../boot/memory_x/ch32v007x8x6/user-flash.x | 22 + examples/ch32/v00x/boot/src/main.rs | 45 ++ .../ch32/v00x/riscv32ec-unknown-none-elf.json | 19 + examples/ch32/v00x/rust-toolchain.toml | 3 + 33 files changed, 1520 insertions(+) create mode 100644 examples/ch32/v00x/.cargo/config.toml create mode 100644 examples/ch32/v00x/Cargo.lock create mode 100644 examples/ch32/v00x/Cargo.toml create mode 100644 examples/ch32/v00x/app/Cargo.toml create mode 100644 examples/ch32/v00x/app/build.rs create mode 100644 examples/ch32/v00x/app/memory_x/ch32v002x4x6/system-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v002x4x6/user-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v004x6x1/system-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v004x6x1/user-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v005x6x6/system-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v005x6x6/user-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v006x8x6/standalone.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v006x8x6/system-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v006x8x6/user-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v007x8x6/system-flash.x create mode 100644 examples/ch32/v00x/app/memory_x/ch32v007x8x6/user-flash.x create mode 100644 examples/ch32/v00x/app/src/main.rs create mode 100644 examples/ch32/v00x/app/src/transport.rs create mode 100644 examples/ch32/v00x/boot/Cargo.toml create mode 100644 examples/ch32/v00x/boot/build.rs create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v002x4x6/system-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v002x4x6/user-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v004x6x1/system-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v004x6x1/user-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v005x6x6/system-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v005x6x6/user-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v006x8x6/system-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v006x8x6/user-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v007x8x6/system-flash.x create mode 100644 examples/ch32/v00x/boot/memory_x/ch32v007x8x6/user-flash.x create mode 100644 examples/ch32/v00x/boot/src/main.rs create mode 100644 examples/ch32/v00x/riscv32ec-unknown-none-elf.json create mode 100644 examples/ch32/v00x/rust-toolchain.toml diff --git a/examples/ch32/v00x/.cargo/config.toml b/examples/ch32/v00x/.cargo/config.toml new file mode 100644 index 0000000..2819115 --- /dev/null +++ b/examples/ch32/v00x/.cargo/config.toml @@ -0,0 +1,12 @@ +[build] +target = "riscv32ec-unknown-none-elf.json" + +[unstable] +build-std = ["core"] +json-target-spec = true + +[target.riscv32ec-unknown-none-elf] +rustflags = [ + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=--nmagic", +] diff --git a/examples/ch32/v00x/Cargo.lock b/examples/ch32/v00x/Cargo.lock new file mode 100644 index 0000000..e000a0a --- /dev/null +++ b/examples/ch32/v00x/Cargo.lock @@ -0,0 +1,674 @@ +# 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", +] + +[[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", + "tinyboot-ch32-rt", +] + +[[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 (git+https://github.com/ch32-rs/ch32-metapac?rev=7cd6fb70d07de3a987c63d0d8f5547bb8261e403)", + "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.1.0" +source = "git+https://github.com/ch32-rs/ch32-metapac?rev=7cd6fb70d07de3a987c63d0d8f5547bb8261e403#7cd6fb70d07de3a987c63d0d8f5547bb8261e403" +dependencies = [ + "riscv 0.12.1", + "vcell", +] + +[[package]] +name = "ch32-metapac" +version = "0.1.0" +source = "git+https://github.com/ch32-rs/ch32-metapac#f8011f710e5c5fcac84ec693f21543d00513a9ca" +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.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" +version = "0.3.0" +dependencies = [ + "ch32-metapac 0.1.0 (git+https://github.com/ch32-rs/ch32-metapac)", + "critical-section", + "embedded-hal 1.0.0", + "embedded-io 0.7.1", + "embedded-storage", + "tinyboot-core", +] + +[[package]] +name = "tinyboot-ch32-rt" +version = "0.3.0" + +[[package]] +name = "tinyboot-core" +version = "0.3.0" +dependencies = [ + "critical-section", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "embedded-storage", + "tinyboot-protocol", +] + +[[package]] +name = "tinyboot-protocol" +version = "0.3.0" +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/v00x/Cargo.toml b/examples/ch32/v00x/Cargo.toml new file mode 100644 index 0000000..5aa1300 --- /dev/null +++ b/examples/ch32/v00x/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +members = ["boot", "app"] +exclude = ["uart-debug"] +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/v00x/app/Cargo.toml b/examples/ch32/v00x/app/Cargo.toml new file mode 100644 index 0000000..1de7455 --- /dev/null +++ b/examples/ch32/v00x/app/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "app" +version = "3.2.1" +edition.workspace = true + +[dependencies] +tinyboot-ch32 = { path = "../../../../ch32", 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 = ["v2"] } +defmt = "1.0" +defmt-rtt = "1.1" + +[features] +default = ["ch32v006x8x6", "system-flash"] +system-flash = ["tinyboot-ch32/system-flash"] +user-flash = [] +standalone = [] +ch32v006x8x6 = ["tinyboot-ch32/ch32v006x8x6", "ch32-hal/ch32v006f8p6", "ch32-hal/rt"] diff --git a/examples/ch32/v00x/app/build.rs b/examples/ch32/v00x/app/build.rs new file mode 100644 index 0000000..03a542f --- /dev/null +++ b/examples/ch32/v00x/app/build.rs @@ -0,0 +1,42 @@ +const CHIPS: &[&str] = &["ch32v006x8x6"]; + +fn main() { + let system_flash = cfg_has("CARGO_FEATURE_SYSTEM_FLASH"); + let user_flash = cfg_has("CARGO_FEATURE_USER_FLASH"); + let standalone = cfg_has("CARGO_FEATURE_STANDALONE"); + + let flash_mode = if standalone { + "standalone" + } else { + match (system_flash, user_flash) { + (true, false) => "system-flash", + (false, true) => "user-flash", + _ => panic!("Enable exactly one flash mode: `system-flash`, `user-flash`, or `standalone`"), + } + }; + + let chip = CHIPS + .iter() + .find(|c| cfg_has(&format!("CARGO_FEATURE_{}", c.to_uppercase()))) + .expect("No chip variant selected"); + + 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/{chip}/{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"); + if standalone { + println!("cargo:rustc-link-arg=-Tlink.x"); + } else { + println!("cargo:rustc-link-arg=-Ttb-app.x"); + println!("cargo:rustc-link-arg=-Ttb-run-mode.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/v00x/app/memory_x/ch32v002x4x6/system-flash.x b/examples/ch32/v00x/app/memory_x/ch32v002x4x6/system-flash.x new file mode 100644 index 0000000..8746cd4 --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v002x4x6/system-flash.x @@ -0,0 +1,27 @@ +/* CH32V002 application memory layout for system-flash bootloader (16K flash, 4K RAM). + * + * 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 = 4K + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000, LENGTH = 16K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 16K - 256 + META : ORIGIN = 0x08000000 + 16K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v002x4x6/user-flash.x b/examples/ch32/v00x/app/memory_x/ch32v002x4x6/user-flash.x new file mode 100644 index 0000000..ec42a6d --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v002x4x6/user-flash.x @@ -0,0 +1,29 @@ +/* CH32V002 application memory layout for user-flash bootloader (16K flash, 4K RAM). + * + * Flash map: + * 0x0000_0000 .. 0x0000_07FF bootloader 2 KB + * 0x0000_0800 .. 0x0000_3EFF application 14 KB - 256 B + * 0x0000_3F00 .. 0x0000_3FFF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 4K - 4 /* __tb_run_mode */ + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000 + 2K, LENGTH = 14K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 14K - 256 + META : ORIGIN = 0x08000000 + 16K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v004x6x1/system-flash.x b/examples/ch32/v00x/app/memory_x/ch32v004x6x1/system-flash.x new file mode 100644 index 0000000..30b37de --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v004x6x1/system-flash.x @@ -0,0 +1,27 @@ +/* CH32V004 application memory layout for system-flash bootloader (32K flash, 6K RAM). + * + * 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 = 6K + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000, LENGTH = 32K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 32K - 256 + META : ORIGIN = 0x08000000 + 32K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v004x6x1/user-flash.x b/examples/ch32/v00x/app/memory_x/ch32v004x6x1/user-flash.x new file mode 100644 index 0000000..9a739f3 --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v004x6x1/user-flash.x @@ -0,0 +1,29 @@ +/* CH32V004 application memory layout for user-flash bootloader (32K flash, 6K RAM). + * + * Flash map: + * 0x0000_0000 .. 0x0000_07FF bootloader 2 KB + * 0x0000_0800 .. 0x0000_7EFF application 30 KB - 256 B + * 0x0000_7F00 .. 0x0000_7FFF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 6K - 4 /* __tb_run_mode */ + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000 + 2K, LENGTH = 30K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 30K - 256 + META : ORIGIN = 0x08000000 + 32K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v005x6x6/system-flash.x b/examples/ch32/v00x/app/memory_x/ch32v005x6x6/system-flash.x new file mode 100644 index 0000000..e947e1b --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v005x6x6/system-flash.x @@ -0,0 +1,27 @@ +/* CH32V005 application memory layout for system-flash bootloader (32K flash, 6K RAM). + * + * 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 = 6K + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000, LENGTH = 32K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 32K - 256 + META : ORIGIN = 0x08000000 + 32K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v005x6x6/user-flash.x b/examples/ch32/v00x/app/memory_x/ch32v005x6x6/user-flash.x new file mode 100644 index 0000000..f6b31fb --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v005x6x6/user-flash.x @@ -0,0 +1,29 @@ +/* CH32V005 application memory layout for user-flash bootloader (32K flash, 6K RAM). + * + * Flash map: + * 0x0000_0000 .. 0x0000_07FF bootloader 2 KB + * 0x0000_0800 .. 0x0000_7EFF application 30 KB - 256 B + * 0x0000_7F00 .. 0x0000_7FFF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 6K - 4 /* __tb_run_mode */ + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000 + 2K, LENGTH = 30K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 30K - 256 + META : ORIGIN = 0x08000000 + 32K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v006x8x6/standalone.x b/examples/ch32/v00x/app/memory_x/ch32v006x8x6/standalone.x new file mode 100644 index 0000000..963fb57 --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v006x8x6/standalone.x @@ -0,0 +1,13 @@ +/* CH32V006 standalone (no bootloader) — for smoke testing only. */ +MEMORY +{ + FLASH : ORIGIN = 0x00000000, LENGTH = 62K + RAM : ORIGIN = 0x20000000, LENGTH = 8K +} + +REGION_ALIAS("REGION_TEXT", FLASH); +REGION_ALIAS("REGION_RODATA", FLASH); +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/v00x/app/memory_x/ch32v006x8x6/system-flash.x b/examples/ch32/v00x/app/memory_x/ch32v006x8x6/system-flash.x new file mode 100644 index 0000000..c07e089 --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v006x8x6/system-flash.x @@ -0,0 +1,27 @@ +/* CH32V006 application memory layout for system-flash bootloader (62K flash, 8K RAM). + * + * 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 = 8K + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000, LENGTH = 62K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 62K - 256 + META : ORIGIN = 0x08000000 + 62K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v006x8x6/user-flash.x b/examples/ch32/v00x/app/memory_x/ch32v006x8x6/user-flash.x new file mode 100644 index 0000000..7f497a1 --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v006x8x6/user-flash.x @@ -0,0 +1,29 @@ +/* CH32V006 application memory layout for user-flash bootloader (62K flash, 8K RAM). + * + * Flash map: + * 0x0000_0000 .. 0x0000_07FF bootloader 2 KB + * 0x0000_0800 .. 0x0000_F6FF application 60 KB - 256 B + * 0x0000_F700 .. 0x0000_F7FF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 8K - 4 /* __tb_run_mode */ + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000 + 2K, LENGTH = 60K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 60K - 256 + META : ORIGIN = 0x08000000 + 62K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v007x8x6/system-flash.x b/examples/ch32/v00x/app/memory_x/ch32v007x8x6/system-flash.x new file mode 100644 index 0000000..8ac19ac --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v007x8x6/system-flash.x @@ -0,0 +1,27 @@ +/* CH32V007 application memory layout for system-flash bootloader (62K flash, 8K RAM). + * + * 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 = 8K + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000, LENGTH = 62K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 62K - 256 + META : ORIGIN = 0x08000000 + 62K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/memory_x/ch32v007x8x6/user-flash.x b/examples/ch32/v00x/app/memory_x/ch32v007x8x6/user-flash.x new file mode 100644 index 0000000..a943d83 --- /dev/null +++ b/examples/ch32/v00x/app/memory_x/ch32v007x8x6/user-flash.x @@ -0,0 +1,29 @@ +/* CH32V007 application memory layout for user-flash bootloader (62K flash, 8K RAM). + * + * Flash map: + * 0x0000_0000 .. 0x0000_07FF bootloader 2 KB + * 0x0000_0800 .. 0x0000_F6FF application 60 KB - 256 B + * 0x0000_F700 .. 0x0000_F7FF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 8K - 4 /* __tb_run_mode */ + + /* Execution mirror of APP */ + CODE : ORIGIN = 0x00000000 + 2K, LENGTH = 60K - 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 60K - 256 + META : ORIGIN = 0x08000000 + 62K - 256, LENGTH = 256 +} + +/* qingke-rt expects a FLASH region */ +REGION_ALIAS("FLASH", CODE); + +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/v00x/app/src/main.rs b/examples/ch32/v00x/app/src/main.rs new file mode 100644 index 0000000..44250eb --- /dev/null +++ b/examples/ch32/v00x/app/src/main.rs @@ -0,0 +1,86 @@ +//! Example app for the tinyboot bootloader (CH32V00x). +//! +//! - TIM2 interrupt blinks LED on PD0 at 1 Hz. +//! - Main loop listens on USART1 (Remap3: TX=PC0, RX=PC1) and reboots into +//! the bootloader when it receives a 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.PD0, 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. + // ch32-hal generic param picks the remap: + // 0 (default): TX=PD5, RX=PD6 + // 1: TX=PD6, RX=PD5 + // 2: TX=PD0, RX=PD1 + // 3: TX=PC0, RX=PC1 + let mut uart_config = usart::Config::default(); + uart_config.baudrate = 115200; + let uart = Uart::new_blocking::<3>(p.USART1, p.PC1, p.PC0, uart_config).unwrap(); + let (tx, rx) = uart.split(); + let mut rx = transport::Rx(rx); + let mut tx = transport::Tx(tx); + + let mut app = tinyboot_ch32::app::new_app(tinyboot_ch32::app::BootCtl::new()); + 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/v00x/app/src/transport.rs b/examples/ch32/v00x/app/src/transport.rs new file mode 100644 index 0000000..1b08e54 --- /dev/null +++ b/examples/ch32/v00x/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/v00x/boot/Cargo.toml b/examples/ch32/v00x/boot/Cargo.toml new file mode 100644 index 0000000..4854a34 --- /dev/null +++ b/examples/ch32/v00x/boot/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "boot" +version = "1.2.3" +edition.workspace = true + +[dependencies] +tinyboot-ch32 = { path = "../../../../ch32", default-features = false } +tinyboot-ch32-rt = { path = "../../../../ch32/rt" } +panic-halt = "1.0" + +[features] +default = ["ch32v006x8x6", "system-flash"] +system-flash = ["tinyboot-ch32/system-flash"] +user-flash = [] +ch32v002x4x6 = ["tinyboot-ch32/ch32v002x4x6"] +ch32v004x6x1 = ["tinyboot-ch32/ch32v004x6x1"] +ch32v005x6x6 = ["tinyboot-ch32/ch32v005x6x6"] +ch32v006x8x6 = ["tinyboot-ch32/ch32v006x8x6"] +ch32v007x8x6 = ["tinyboot-ch32/ch32v007x8x6"] diff --git a/examples/ch32/v00x/boot/build.rs b/examples/ch32/v00x/boot/build.rs new file mode 100644 index 0000000..c70d97b --- /dev/null +++ b/examples/ch32/v00x/boot/build.rs @@ -0,0 +1,43 @@ +const CHIPS: &[&str] = &[ + "ch32v002x4x6", + "ch32v004x6x1", + "ch32v005x6x6", + "ch32v006x8x6", + "ch32v007x8x6", +]; + +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 chip = CHIPS + .iter() + .find(|c| cfg_has(&format!("CARGO_FEATURE_{}", c.to_uppercase()))) + .expect("No chip variant selected"); + + 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/{chip}/{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-run-mode.x"); +} + +fn cfg_has(key: &str) -> bool { + std::env::var_os(key).is_some() +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v002x4x6/system-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v002x4x6/system-flash.x new file mode 100644 index 0000000..4c65fb4 --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v002x4x6/system-flash.x @@ -0,0 +1,18 @@ +/* CH32V002 system-flash bootloader memory layout (16K flash, 4K RAM). + * + * The bootloader runs from system flash at 0x1FFF0000. + * All 16 KB of user flash is available for the application. + * Boot metadata occupies the last page (256 bytes) of user flash. + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 4K + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 3K + 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 16K - 256 + META : ORIGIN = 0x08000000 + 16K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v002x4x6/user-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v002x4x6/user-flash.x new file mode 100644 index 0000000..5da6523 --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v002x4x6/user-flash.x @@ -0,0 +1,22 @@ +/* CH32V002 user-flash bootloader memory layout (16K flash, 4K RAM). + * + * The bootloader occupies the first 2 KB of user flash. + * Boot metadata occupies the last page (256 bytes) of user flash. + * + * Flash map: + * 0x0800_0000 .. 0x0800_07FF bootloader 2 KB + * 0x0800_0800 .. 0x0800_3EFF application 14 KB - 256 B + * 0x0800_3F00 .. 0x0800_3FFF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 4K - 4 /* __tb_run_mode */ + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 2K + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 14K - 256 + META : ORIGIN = 0x08000000 + 16K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v004x6x1/system-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v004x6x1/system-flash.x new file mode 100644 index 0000000..d113c68 --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v004x6x1/system-flash.x @@ -0,0 +1,18 @@ +/* CH32V004 system-flash bootloader memory layout (32K flash, 6K RAM). + * + * The bootloader runs from system flash at 0x1FFF0000. + * All 32 KB of user flash is available for the application. + * Boot metadata occupies the last page (256 bytes) of user flash. + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 6K + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 3K + 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 32K - 256 + META : ORIGIN = 0x08000000 + 32K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v004x6x1/user-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v004x6x1/user-flash.x new file mode 100644 index 0000000..dc54a90 --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v004x6x1/user-flash.x @@ -0,0 +1,22 @@ +/* CH32V004 user-flash bootloader memory layout (32K flash, 6K RAM). + * + * The bootloader occupies the first 2 KB of user flash. + * Boot metadata occupies the last page (256 bytes) of user flash. + * + * Flash map: + * 0x0800_0000 .. 0x0800_07FF bootloader 2 KB + * 0x0800_0800 .. 0x0800_7EFF application 30 KB - 256 B + * 0x0800_7F00 .. 0x0800_7FFF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 6K - 4 /* __tb_run_mode */ + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 2K + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 30K - 256 + META : ORIGIN = 0x08000000 + 32K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v005x6x6/system-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v005x6x6/system-flash.x new file mode 100644 index 0000000..436a88b --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v005x6x6/system-flash.x @@ -0,0 +1,18 @@ +/* CH32V005 system-flash bootloader memory layout (32K flash, 6K RAM). + * + * The bootloader runs from system flash at 0x1FFF0000. + * All 32 KB of user flash is available for the application. + * Boot metadata occupies the last page (256 bytes) of user flash. + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 6K + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 3K + 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 32K - 256 + META : ORIGIN = 0x08000000 + 32K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v005x6x6/user-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v005x6x6/user-flash.x new file mode 100644 index 0000000..3ddfaf3 --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v005x6x6/user-flash.x @@ -0,0 +1,22 @@ +/* CH32V005 user-flash bootloader memory layout (32K flash, 6K RAM). + * + * The bootloader occupies the first 2 KB of user flash. + * Boot metadata occupies the last page (256 bytes) of user flash. + * + * Flash map: + * 0x0800_0000 .. 0x0800_07FF bootloader 2 KB + * 0x0800_0800 .. 0x0800_7EFF application 30 KB - 256 B + * 0x0800_7F00 .. 0x0800_7FFF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 6K - 4 /* __tb_run_mode */ + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 2K + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 30K - 256 + META : ORIGIN = 0x08000000 + 32K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v006x8x6/system-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v006x8x6/system-flash.x new file mode 100644 index 0000000..bb541d0 --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v006x8x6/system-flash.x @@ -0,0 +1,18 @@ +/* CH32V006 system-flash bootloader memory layout (62K flash, 8K RAM). + * + * The bootloader runs from system flash at 0x1FFF0000. + * All 62 KB of user flash is available for the application. + * Boot metadata occupies the last page (256 bytes) of user flash. + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 8K + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 3K + 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 62K - 256 + META : ORIGIN = 0x08000000 + 62K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v006x8x6/user-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v006x8x6/user-flash.x new file mode 100644 index 0000000..b2ae6c8 --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v006x8x6/user-flash.x @@ -0,0 +1,22 @@ +/* CH32V006 user-flash bootloader memory layout (62K flash, 8K RAM). + * + * The bootloader occupies the first 2 KB of user flash. + * Boot metadata occupies the last page (256 bytes) of user flash. + * + * Flash map: + * 0x0800_0000 .. 0x0800_07FF bootloader 2 KB + * 0x0800_0800 .. 0x0800_F6FF application 60 KB - 256 B + * 0x0800_F700 .. 0x0800_F7FF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 8K - 4 /* __tb_run_mode */ + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 2K + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 60K - 256 + META : ORIGIN = 0x08000000 + 62K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v007x8x6/system-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v007x8x6/system-flash.x new file mode 100644 index 0000000..cdd38fb --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v007x8x6/system-flash.x @@ -0,0 +1,18 @@ +/* CH32V007 system-flash bootloader memory layout (62K flash, 8K RAM). + * + * The bootloader runs from system flash at 0x1FFF0000. + * All 62 KB of user flash is available for the application. + * Boot metadata occupies the last page (256 bytes) of user flash. + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 8K + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 3K + 256 + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x1FFF0000, LENGTH = 3K + 256 + APP : ORIGIN = 0x08000000, LENGTH = 62K - 256 + META : ORIGIN = 0x08000000 + 62K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/memory_x/ch32v007x8x6/user-flash.x b/examples/ch32/v00x/boot/memory_x/ch32v007x8x6/user-flash.x new file mode 100644 index 0000000..f01badc --- /dev/null +++ b/examples/ch32/v00x/boot/memory_x/ch32v007x8x6/user-flash.x @@ -0,0 +1,22 @@ +/* CH32V007 user-flash bootloader memory layout (62K flash, 8K RAM). + * + * The bootloader occupies the first 2 KB of user flash. + * Boot metadata occupies the last page (256 bytes) of user flash. + * + * Flash map: + * 0x0800_0000 .. 0x0800_07FF bootloader 2 KB + * 0x0800_0800 .. 0x0800_F6FF application 60 KB - 256 B + * 0x0800_F700 .. 0x0800_F7FF boot meta 256 B + */ +MEMORY +{ + RAM : ORIGIN = 0x20000000, LENGTH = 8K - 4 /* __tb_run_mode */ + + /* Execution mirror of BOOT */ + CODE : ORIGIN = 0x00000000, LENGTH = 2K + + /* Physical flash addresses */ + BOOT : ORIGIN = 0x08000000, LENGTH = 2K + APP : ORIGIN = 0x08000000 + 2K, LENGTH = 60K - 256 + META : ORIGIN = 0x08000000 + 62K - 256, LENGTH = 256 +} diff --git a/examples/ch32/v00x/boot/src/main.rs b/examples/ch32/v00x/boot/src/main.rs new file mode 100644 index 0000000..200e91c --- /dev/null +++ b/examples/ch32/v00x/boot/src/main.rs @@ -0,0 +1,45 @@ +//! Bootloader example for CH32V00x (V006/V007). +//! +//! Flash-mode features: +//! - `system-flash`: runs from 3328-byte system flash; all 62 KB user flash free for the app. +//! - `user-flash`: occupies first 2 KB of user flash; app gets the remaining 60 KB - 256 B. + +#![no_std] +#![no_main] + +use panic_halt as _; +use tinyboot_ch32_rt as _; + +tinyboot_ch32::boot::boot_version!(); + +use tinyboot_ch32::boot::prelude::*; + +#[unsafe(export_name = "main")] +fn main() -> ! { + // USART1 transport. Remap options (CH32V00x): + // Remap0 (default): TX=PD5, RX=PD6 + // Remap1: TX=PD6, RX=PD5 + // Remap2: TX=PD0, RX=PD1 + // Remap3: TX=PC0, RX=PC1 + // Remap4: TX=PD1, RX=PB3 + // Remap5: TX=PB3, RX=PD1 + // Remap6: TX=PC5, RX=PC6 + // Remap7: TX=PB5, RX=PB6 + // Remap8: TX=PA0, RX=PA1 + // Remap9: TX=PA0, RX=PC4 + // + // rx_pull: Pull::Up for floating lines; Pull::None if externally pulled up. + // + // RS-485 half-duplex with DE pin: + // duplex: Duplex::Half, + // tx_en: Some(TxEnConfig { pin: Pin::PC2, tx_level: Level::High }), + let transport = Usart::new(&UsartConfig { + duplex: Duplex::Full, + baud: BaudRate::B115200, + pclk: 8_000_000, + mapping: UsartMapping::Usart1Remap3, + rx_pull: Pull::None, + tx_en: Some(TxEnConfig { pin: Pin::PC2, tx_level: Level::Low }), + }); + tinyboot_ch32::boot::run(transport, BootCtl::new()); +} diff --git a/examples/ch32/v00x/riscv32ec-unknown-none-elf.json b/examples/ch32/v00x/riscv32ec-unknown-none-elf.json new file mode 100644 index 0000000..f748cf4 --- /dev/null +++ b/examples/ch32/v00x/riscv32ec-unknown-none-elf.json @@ -0,0 +1,19 @@ +{ + "arch": "riscv32", + "atomic-cas": false, + "cpu": "generic-rv32", + "crt-objects-fallback": "false", + "data-layout": "e-m:e-p:32:32-i64:64-n32-S32", + "eh-frame-header": false, + "emit-debug-gdb-scripts": false, + "features": "+e,+c,+forced-atomics", + "linker": "rust-lld", + "linker-flavor": "gnu-lld", + "llvm-target": "riscv32", + "abi": "ilp32e", + "llvm-abiname": "ilp32e", + "max-atomic-width": 32, + "panic-strategy": "abort", + "relocation-model": "static", + "target-pointer-width": 32 +} diff --git a/examples/ch32/v00x/rust-toolchain.toml b/examples/ch32/v00x/rust-toolchain.toml new file mode 100644 index 0000000..db4fae7 --- /dev/null +++ b/examples/ch32/v00x/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src", "rustfmt", "clippy", "rust-analyzer"] From e0ff4213c8eae7a84a9afe37ce35f3e9e3c1f09e Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 01:27:14 -0700 Subject: [PATCH 07/15] ensure dispatcher flushes transport. This is important for RS485 / DXL TTL to work correctly --- lib/core/src/protocol.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/core/src/protocol.rs b/lib/core/src/protocol.rs index 024415f..5bc9678 100644 --- a/lib/core/src/protocol.rs +++ b/lib/core/src/protocol.rs @@ -197,6 +197,8 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz self.frame .send(&mut self.platform.transport) - .map_err(|_| ReadError) + .map_err(|_| ReadError)?; + + embedded_io::Write::flush(&mut self.platform.transport).map_err(|_| ReadError) } } From c19c762722bb880866ec6b2ada5984fa21946a64 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 02:06:28 -0700 Subject: [PATCH 08/15] properly reset ring buffer after flush. --- lib/core/src/protocol.rs | 1 + lib/core/src/ringbuf.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/lib/core/src/protocol.rs b/lib/core/src/protocol.rs index 5bc9678..037769b 100644 --- a/lib/core/src/protocol.rs +++ b/lib/core/src/protocol.rs @@ -190,6 +190,7 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz if !self.buf.is_empty() { self.write_buf(next, self.buf.len()); } + self.buf.reset(); self.next_addr = None; } } diff --git a/lib/core/src/ringbuf.rs b/lib/core/src/ringbuf.rs index 384ac3a..33b07ec 100644 --- a/lib/core/src/ringbuf.rs +++ b/lib/core/src/ringbuf.rs @@ -70,6 +70,14 @@ impl RingBuf { pub fn consume(&mut self, n: usize) { self.tail = (self.tail + n) % N; } + + /// Reset head/tail to 0. Caller must ensure the buffer is empty. + #[inline(always)] + pub fn reset(&mut self) { + debug_assert!(self.is_empty(), "reset of non-empty ring buffer"); + self.head = 0; + self.tail = 0; + } } #[cfg(test)] From 642f8095a3a7082289f6dad79b2ddf30246ce1e4 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 02:57:15 -0700 Subject: [PATCH 09/15] add tx_en support for ch32v00x example app. Needed to test against OpenServoCore dev board. --- examples/ch32/v00x/app/src/main.rs | 21 +++++++++++- examples/ch32/v00x/app/src/transport.rs | 44 ++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/examples/ch32/v00x/app/src/main.rs b/examples/ch32/v00x/app/src/main.rs index 44250eb..cfe0135 100644 --- a/examples/ch32/v00x/app/src/main.rs +++ b/examples/ch32/v00x/app/src/main.rs @@ -32,6 +32,13 @@ fn panic(_info: &core::panic::PanicInfo) -> ! { type Shared = Mutex>>; static LED: Shared> = Mutex::new(RefCell::new(None)); +fn invert_level(level: Level) -> Level { + match level { + Level::High => Level::Low, + Level::Low => Level::High, + } +} + #[qingke_rt::entry] fn main() -> ! { let p = ch32_hal::init(Default::default()); @@ -58,7 +65,19 @@ fn main() -> ! { let uart = Uart::new_blocking::<3>(p.USART1, p.PC1, p.PC0, uart_config).unwrap(); let (tx, rx) = uart.split(); let mut rx = transport::Rx(rx); - let mut tx = transport::Tx(tx); + + // RS-485 DE/RE on PC2, matching the bootloader. tx_level=Low means idle-RX + // drives the pin High (inverse), which tri-states U4A on the V006 dev board + // so LinkE UART TX can reach MCU_RX without contention. + let tx_level = Level::Low; + let tx_en_pin = Output::new(p.PC2, invert_level(tx_level), Default::default()); + let mut tx = transport::Tx { + uart: tx, + tx_en: Some(transport::TxEn { + pin: tx_en_pin, + tx_level, + }), + }; let mut app = tinyboot_ch32::app::new_app(tinyboot_ch32::app::BootCtl::new()); app.confirm(); diff --git a/examples/ch32/v00x/app/src/transport.rs b/examples/ch32/v00x/app/src/transport.rs index 1b08e54..4f3fc8a 100644 --- a/examples/ch32/v00x/app/src/transport.rs +++ b/examples/ch32/v00x/app/src/transport.rs @@ -1,5 +1,6 @@ -//! embedded_io adapters for ch32-hal blocking UART. +//! embedded_io adapters for ch32-hal blocking UART with optional RS-485 DE/RE. +use ch32_hal::gpio::{Level, Output}; use ch32_hal::mode::Blocking; use ch32_hal::usart::{Instance, UartRx, UartTx}; use core::convert::Infallible; @@ -21,7 +22,36 @@ impl Read for Rx<'_, T> { } } -pub struct Tx<'d, T: Instance>(pub UartTx<'d, T, Blocking>); +pub struct TxEn<'d> { + pub pin: Output<'d>, + /// Level driven to enable TX; the inverse enables RX. + pub tx_level: Level, +} + +impl TxEn<'_> { + #[inline(always)] + fn set_tx(&mut self) { + self.pin.set_level(self.tx_level); + } + + #[inline(always)] + fn set_rx(&mut self) { + self.pin.set_level(invert(self.tx_level)); + } +} + +#[inline(always)] +fn invert(level: Level) -> Level { + match level { + Level::High => Level::Low, + Level::Low => Level::High, + } +} + +pub struct Tx<'d, T: Instance> { + pub uart: UartTx<'d, T, Blocking>, + pub tx_en: Option>, +} impl ErrorType for Tx<'_, T> { type Error = Infallible; @@ -29,12 +59,18 @@ impl ErrorType for Tx<'_, T> { impl Write for Tx<'_, T> { fn write(&mut self, buf: &[u8]) -> Result { - let _ = self.0.blocking_write(buf); + if let Some(tx_en) = self.tx_en.as_mut() { + tx_en.set_tx(); + } + let _ = self.uart.blocking_write(buf); Ok(buf.len()) } fn flush(&mut self) -> Result<(), Self::Error> { - let _ = self.0.blocking_flush(); + let _ = self.uart.blocking_flush(); + if let Some(tx_en) = self.tx_en.as_mut() { + tx_en.set_rx(); + } Ok(()) } } From 5004f0271b568534b758b3511d953bd84af5d288 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 15:41:44 -0700 Subject: [PATCH 10/15] reduce protocol address to 24 bits and reserve the 8 bits for per-command flags. Removes Confirm command --- cli/Cargo.lock | 1 + cli/src/client.rs | 66 +++++++------- examples/ch32/v003/Cargo.lock | 9 +- examples/ch32/v00x/Cargo.lock | 9 +- examples/ch32/v103/Cargo.lock | 9 +- lib/core/src/app.rs | 4 +- lib/core/src/protocol.rs | 34 +++---- lib/core/tests/state_transitions.rs | 133 ++++++++++++++-------------- lib/protocol/Cargo.toml | 1 + lib/protocol/src/frame.rs | 54 +++++++++-- lib/protocol/src/lib.rs | 33 +++++-- 11 files changed, 218 insertions(+), 135 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index fb69a51..d5fcb0e 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -533,6 +533,7 @@ dependencies = [ name = "tinyboot-protocol" version = "0.3.0" dependencies = [ + "bitflags 2.11.1", "embedded-io", "embedded-io-async", ] diff --git a/cli/src/client.rs b/cli/src/client.rs index 1bb9447..e665154 100644 --- a/cli/src/client.rs +++ b/cli/src/client.rs @@ -1,7 +1,7 @@ use log::debug; use tinyboot_protocol::crc::{CRC_INIT, crc16}; -use tinyboot_protocol::frame::{EraseData, Frame, MAX_PAYLOAD}; -use tinyboot_protocol::{Cmd, Status}; +use tinyboot_protocol::frame::{EraseData, Flags, Frame, MAX_PAYLOAD}; +use tinyboot_protocol::{Cmd, ResetFlags, Status, WriteFlags}; #[derive(Debug, thiserror::Error)] pub enum FlashError { @@ -45,7 +45,9 @@ impl Client { self.frame.status = Status::Request; debug!( ">> {:?} addr={:#X} len={}", - self.frame.cmd, self.frame.addr, self.frame.len + self.frame.cmd, + self.frame.addr(), + self.frame.len ); self.frame .send(&mut self.transport) @@ -71,7 +73,8 @@ impl Client { /// Query device geometry. pub fn info(&mut self) -> Result { self.frame.cmd = Cmd::Info; - self.frame.addr = 0; + self.frame.set_addr(0); + self.frame.flags = Flags { raw: 0 }; self.frame.len = 0; self.transact()?; @@ -110,7 +113,8 @@ impl Client { let mut addr = 0u32; while addr < capacity { self.frame.cmd = Cmd::Erase; - self.frame.addr = addr; + self.frame.set_addr(addr); + self.frame.flags = Flags { raw: 0 }; self.frame.len = 2; self.frame.data.erase = EraseData { byte_count: erase_size as u16, @@ -147,7 +151,8 @@ impl Client { let mut erase_addr = 0u32; while erase_addr < info.capacity { self.frame.cmd = Cmd::Erase; - self.frame.addr = erase_addr; + self.frame.set_addr(erase_addr); + self.frame.flags = Flags { raw: 0 }; self.frame.len = 2; self.frame.data.erase = EraseData { byte_count: erase_size as u16, @@ -157,8 +162,8 @@ impl Client { on_progress("Erasing", erase_addr, info.capacity); } - // 3. Write — chunk by MAX_PAYLOAD, pad to 4-byte alignment with 0xFF - let page_size = erase_size as usize; + // 3. Write — chunk by MAX_PAYLOAD, pad to 4-byte alignment with 0xFF. + // Set FLUSH flag on the last chunk. let padded_len = firmware.len().next_multiple_of(4); let total_chunks = padded_len.div_ceil(MAX_PAYLOAD) as u32; let mut offset = 0usize; @@ -166,6 +171,7 @@ impl Client { while offset < padded_len { let end = (offset + MAX_PAYLOAD).min(padded_len); let len = end - offset; + let is_last = end >= padded_len; // Copy firmware bytes, pad remainder with 0xFF let fw_end = end.min(firmware.len()); let fw_bytes = fw_end.saturating_sub(offset); @@ -175,17 +181,20 @@ impl Client { self.frame.data.raw[fw_bytes..len].fill(0xFF); } self.frame.cmd = Cmd::Write; - self.frame.addr = offset as u32; + self.frame.set_addr(offset as u32); + self.frame.flags = Flags { + write: if is_last { + WriteFlags::FLUSH + } else { + WriteFlags::empty() + }, + }; self.frame.len = len as u16; 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; + // Response frame corrupted — retry from current offset. + debug!("CRC mismatch at {offset:#X}, retrying"); continue; } Err(e) => return Err(e), @@ -195,14 +204,12 @@ impl Client { on_progress("Writing", chunk_idx, total_chunks); } - // 4. Flush buffered writes - self.flush()?; - - // 5. Verify — CRC covers original firmware bytes only (not padding) + // 4. Verify — CRC covers original firmware bytes only (not padding) let expected_crc = crc16(CRC_INIT, firmware); self.frame.cmd = Cmd::Verify; - self.frame.addr = fw_size; + self.frame.set_addr(fw_size); + self.frame.flags = Flags { raw: 0 }; self.frame.len = 0; self.transact()?; @@ -217,19 +224,18 @@ impl Client { Ok(info) } - /// Flush buffered writes on the device. - pub fn flush(&mut self) -> Result<(), FlashError> { - self.frame.cmd = Cmd::Flush; - self.frame.addr = 0; - self.frame.len = 0; - self.transact() - } - /// Reset the device. Does not wait for a response since the device resets immediately. - /// `bootloader=true` (addr=1): enter bootloader. `bootloader=false` (addr=0): boot app. + /// `bootloader=true`: enter bootloader. `bootloader=false`: boot app. pub fn reset(&mut self, bootloader: bool) { self.frame.cmd = Cmd::Reset; - self.frame.addr = u32::from(bootloader); + self.frame.set_addr(0); + self.frame.flags = Flags { + reset: if bootloader { + ResetFlags::BOOTLOADER + } else { + ResetFlags::empty() + }, + }; self.frame.len = 0; self.frame.status = Status::Request; let _ = self.frame.send(&mut self.transport); diff --git a/examples/ch32/v003/Cargo.lock b/examples/ch32/v003/Cargo.lock index e000a0a..3ae86d4 100644 --- a/examples/ch32/v003/Cargo.lock +++ b/examples/ch32/v003/Cargo.lock @@ -33,6 +33,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "bitmaps" version = "3.2.1" @@ -118,7 +124,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" dependencies = [ - "bitflags", + "bitflags 1.3.2", "defmt-macros", ] @@ -645,6 +651,7 @@ dependencies = [ name = "tinyboot-protocol" version = "0.3.0" dependencies = [ + "bitflags 2.11.1", "embedded-io 0.7.1", "embedded-io-async 0.7.0", ] diff --git a/examples/ch32/v00x/Cargo.lock b/examples/ch32/v00x/Cargo.lock index e000a0a..3ae86d4 100644 --- a/examples/ch32/v00x/Cargo.lock +++ b/examples/ch32/v00x/Cargo.lock @@ -33,6 +33,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "bitmaps" version = "3.2.1" @@ -118,7 +124,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" dependencies = [ - "bitflags", + "bitflags 1.3.2", "defmt-macros", ] @@ -645,6 +651,7 @@ dependencies = [ name = "tinyboot-protocol" version = "0.3.0" dependencies = [ + "bitflags 2.11.1", "embedded-io 0.7.1", "embedded-io-async 0.7.0", ] diff --git a/examples/ch32/v103/Cargo.lock b/examples/ch32/v103/Cargo.lock index e000a0a..3ae86d4 100644 --- a/examples/ch32/v103/Cargo.lock +++ b/examples/ch32/v103/Cargo.lock @@ -33,6 +33,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "bitmaps" version = "3.2.1" @@ -118,7 +124,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" dependencies = [ - "bitflags", + "bitflags 1.3.2", "defmt-macros", ] @@ -645,6 +651,7 @@ dependencies = [ name = "tinyboot-protocol" version = "0.3.0" dependencies = [ + "bitflags 2.11.1", "embedded-io 0.7.1", "embedded-io-async 0.7.0", ] diff --git a/lib/core/src/app.rs b/lib/core/src/app.rs index dffb954..784ed1c 100644 --- a/lib/core/src/app.rs +++ b/lib/core/src/app.rs @@ -3,7 +3,7 @@ use crate::traits::{BootCtl, BootMetaStore, BootState, RunMode}; use tinyboot_protocol::frame::{Frame, InfoData}; -use tinyboot_protocol::{Cmd, Status}; +use tinyboot_protocol::{Cmd, ResetFlags, Status}; /// App-side configuration. pub struct AppConfig { @@ -102,7 +102,7 @@ impl App { }; } Cmd::Reset => { - if self.frame.addr == 1 { + if unsafe { self.frame.flags.reset }.contains(ResetFlags::BOOTLOADER) { self.ctl.set_run_mode(RunMode::Service); } self.ctl.reset(); diff --git a/lib/core/src/protocol.rs b/lib/core/src/protocol.rs index 037769b..429234e 100644 --- a/lib/core/src/protocol.rs +++ b/lib/core/src/protocol.rs @@ -3,13 +3,13 @@ use crate::ringbuf::RingBuf; use crate::traits::{BootCtl, BootMetaStore, BootState, RunMode, Storage, Transport}; use tinyboot_protocol::crc::{CRC_INIT, crc16}; use tinyboot_protocol::frame::{Frame, InfoData, VerifyData}; -use tinyboot_protocol::{Cmd, ReadError, Status}; +use tinyboot_protocol::{Cmd, ReadError, ResetFlags, Status, WriteFlags}; /// Protocol dispatcher with write buffering. /// /// Writes are accumulated in a ring buffer and flushed a page at a time. -/// The host must `Flush` before `Verify` or before skipping to a non-sequential -/// address, otherwise the trailing partial page is lost. +/// The host sets [`WriteFlags::FLUSH`] on the last write of each contiguous +/// region to commit the trailing partial page. pub struct Dispatcher<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usize> { /// Platform peripherals. @@ -59,6 +59,8 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz } let data_len = self.frame.len as usize; + let addr = self.frame.addr(); + let flags = self.frame.flags; let capacity = self.platform.storage.capacity() as u32; let erase_size = S::ERASE_SIZE as u32; let write_size = S::WRITE_SIZE as u32; @@ -87,7 +89,7 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz }; } Cmd::Erase => { - let addr = self.frame.addr; + let byte_count = unsafe { self.frame.data.erase }.byte_count as u32; if !addr.is_multiple_of(erase_size) || !byte_count.is_multiple_of(erase_size) @@ -131,7 +133,9 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz if state != BootState::Updating { self.frame.status = Status::Unsupported; } else { - let addr = self.frame.addr; + + let flush = unsafe { flags.write }.contains(WriteFlags::FLUSH); + if addr + data_len as u32 > capacity || (self.next_addr.is_none() && !addr.is_multiple_of(write_size)) || self.next_addr.is_some_and(|n| n != addr) @@ -146,6 +150,13 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz if self.buf.len() >= S::WRITE_SIZE { self.write_buf(next, S::WRITE_SIZE); } + if flush { + if !self.buf.is_empty() { + self.write_buf(next, self.buf.len()); + } + self.buf.reset(); + self.next_addr = None; + } } } } @@ -153,7 +164,7 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz if state != BootState::Updating { self.frame.status = Status::Unsupported; } else { - let app_size = self.frame.addr; + let app_size = addr; let sz = app_size as usize; if sz == 0 || sz > capacity as usize { self.frame.status = Status::AddrOutOfBounds; @@ -177,7 +188,7 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz } Cmd::Reset => { let _ = self.frame.send(&mut self.platform.transport); - let mode = if self.frame.addr == 1 { + let mode = if unsafe { flags.reset }.contains(ResetFlags::BOOTLOADER) { RunMode::Service } else { RunMode::HandOff @@ -185,15 +196,6 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz self.platform.ctl.set_run_mode(mode); self.platform.ctl.reset(); } - Cmd::Flush => { - if let Some(next) = self.next_addr { - if !self.buf.is_empty() { - self.write_buf(next, self.buf.len()); - } - self.buf.reset(); - self.next_addr = None; - } - } } self.frame diff --git a/lib/core/tests/state_transitions.rs b/lib/core/tests/state_transitions.rs index 11d8f26..8446582 100644 --- a/lib/core/tests/state_transitions.rs +++ b/lib/core/tests/state_transitions.rs @@ -12,8 +12,8 @@ use tinyboot_core::Platform; use tinyboot_core::protocol::Dispatcher; use tinyboot_core::traits::{BootCtl, BootMetaStore, BootState, RunMode, Storage, Transport}; use tinyboot_protocol::crc::{CRC_INIT, crc16}; -use tinyboot_protocol::frame::Frame; -use tinyboot_protocol::{Cmd, Status}; +use tinyboot_protocol::frame::{Flags, Frame}; +use tinyboot_protocol::{Cmd, Status, WriteFlags}; // -- Mocks -- @@ -37,9 +37,14 @@ impl MockTransport { } fn load_request(&mut self, cmd: Cmd, addr: u32, len: u16, data: &[u8]) { + self.load_request_flags(cmd, addr, Flags { raw: 0 }, len, data); + } + + fn load_request_flags(&mut self, cmd: Cmd, addr: u32, flags: Flags, len: u16, data: &[u8]) { let mut frame = Frame::default(); frame.cmd = cmd; - frame.addr = addr; + frame.set_addr(addr); + frame.flags = flags; frame.len = len; frame.status = Status::Request; unsafe { frame.data.raw[..data.len()].copy_from_slice(data) }; @@ -198,7 +203,6 @@ impl BootMetaStore for MockBootMeta { } type TestPlatform = Platform; -// 2 × MockStorage ERASE_SIZE (64) const TEST_BUF_SIZE: usize = 128; fn platform(state: BootState) -> TestPlatform { @@ -214,11 +218,14 @@ fn erase_data(byte_count: u16) -> [u8; 2] { byte_count.to_le_bytes() } +fn flush_flags() -> Flags { + Flags { + write: WriteFlags::FLUSH, + } +} + // ============================================================================= -// Erase: state transitions -// | Idle | → Updating | addr/size valid | step down state byte | -// | Updating | → Updating | addr/size valid | none | -// | Validating | → Updating | addr/size valid | refresh (state=Updating, clear checksum) | +// Erase // ============================================================================= #[test] @@ -318,15 +325,11 @@ fn erase_bulk_multiple_pages() { assert_eq!(d.platform.storage.data[0], 0xFF); assert_eq!(d.platform.storage.data[64], 0xFF); assert_eq!(d.platform.storage.data[128], 0xFF); - // Beyond erase range untouched assert_eq!(d.platform.storage.data[192], 0xFF); } // ============================================================================= -// Write: state transitions -// | Idle | reject | | not in update | -// | Updating | → Updating | addr/size valid | none | -// | Validating | reject | | not in update | +// Write // ============================================================================= #[test] @@ -345,13 +348,13 @@ fn write_from_idle_rejected() { fn write_from_updating_succeeds() { let mut p = platform(BootState::Updating); let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); - d.platform - .transport - .load_request(Cmd::Write, 0, 4, &[0xDE, 0xAD, 0xBE, 0xEF]); - d.dispatch().unwrap(); - assert_eq!(d.frame.status, Status::Ok); - // Flush buffered write to storage - d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); + d.platform.transport.load_request_flags( + Cmd::Write, + 0, + flush_flags(), + 4, + &[0xDE, 0xAD, 0xBE, 0xEF], + ); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); assert_eq!(&d.platform.storage.data[..4], &[0xDE, 0xAD, 0xBE, 0xEF]); @@ -393,21 +396,20 @@ fn write_validates_bounds() { fn write_at_offset() { let mut p = platform(BootState::Updating); let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); - d.platform - .transport - .load_request(Cmd::Write, 8, 4, &[0x01, 0x02, 0x03, 0x04]); + d.platform.transport.load_request_flags( + Cmd::Write, + 4, + flush_flags(), + 4, + &[0x01, 0x02, 0x03, 0x04], + ); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); - d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); - d.dispatch().unwrap(); - assert_eq!(&d.platform.storage.data[8..12], &[0x01, 0x02, 0x03, 0x04]); + assert_eq!(&d.platform.storage.data[4..8], &[0x01, 0x02, 0x03, 0x04]); } // ============================================================================= -// Verify: state transitions -// | Idle | reject | | not in update | -// | Updating | → Validating | CRC match | refresh (state=Validating, write checksum) | -// | Validating | reject | | already verified | +// Verify // ============================================================================= #[test] @@ -462,7 +464,6 @@ fn verify_rejects_zero_app_size() { fn verify_rejects_app_size_exceeding_capacity() { let mut p = platform(BootState::Updating); let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); - // capacity is 256, send 257 d.platform.transport.load_request(Cmd::Verify, 257, 0, &[]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::AddrOutOfBounds); @@ -470,7 +471,7 @@ fn verify_rejects_app_size_exceeding_capacity() { } // ============================================================================= -// Info: works in all states (no state change) +// Info // ============================================================================= #[test] @@ -496,26 +497,19 @@ fn full_update_cycle() { let mut p = platform(BootState::Idle); let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); - // Erase: Idle → Updating d.platform .transport .load_request(Cmd::Erase, 0, 2, &erase_data(64)); d.dispatch().unwrap(); assert_eq!(d.platform.boot_meta.state, BootState::Updating); - // Write: Updating → Updating let fw = [0x01, 0x02, 0x03, 0x04]; - d.platform.transport.load_request(Cmd::Write, 0, 4, &fw); - d.dispatch().unwrap(); - assert_eq!(d.frame.status, Status::Ok); - assert_eq!(d.platform.boot_meta.state, BootState::Updating); - - // Flush buffered writes - d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); + d.platform + .transport + .load_request_flags(Cmd::Write, 0, flush_flags(), 4, &fw); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); - // Verify: Updating → Validating (app_size=4) d.platform .transport .load_request(Cmd::Verify, fw.len() as u32, 0, &[]); @@ -533,81 +527,84 @@ fn reflash_from_validating() { p.boot_meta.app_size = 4; let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); - // Erase: Validating → Updating (reflash, clears checksum + app_size) d.platform .transport .load_request(Cmd::Erase, 0, 2, &erase_data(64)); d.dispatch().unwrap(); assert_eq!(d.platform.boot_meta.state, BootState::Updating); assert_eq!(d.platform.boot_meta.checksum, 0xFFFF); - assert_eq!(d.platform.boot_meta.app_size, 0xFFFF_FFFF); - // Write succeeds let fw = [0xAB, 0xCD, 0xEF, 0x01]; - d.platform.transport.load_request(Cmd::Write, 0, 4, &fw); - d.dispatch().unwrap(); - assert_eq!(d.frame.status, Status::Ok); - - // Flush buffered writes - d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); + d.platform + .transport + .load_request_flags(Cmd::Write, 0, flush_flags(), 4, &fw); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); - // Verify: Updating → Validating d.platform .transport .load_request(Cmd::Verify, fw.len() as u32, 0, &[]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); assert_eq!(d.platform.boot_meta.state, BootState::Validating); - assert_eq!(d.platform.boot_meta.app_size, fw.len() as u32); } // ============================================================================= -// Flush +// Write FLUSH flag // ============================================================================= #[test] -fn flush_commits_buffered_write() { +fn flush_flag_commits_buffered_write() { let mut p = platform(BootState::Updating); let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); + d.platform .transport .load_request(Cmd::Write, 0, 2, &[0xAA, 0xBB]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); - // Data is buffered (2 < WRITE_SIZE=4), not yet in storage - assert_eq!(d.platform.storage.data[0], 0xFF); + assert_eq!(d.platform.storage.data[0], 0xFF); // still buffered - d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); + d.platform + .transport + .load_request_flags(Cmd::Write, 2, flush_flags(), 2, &[0xCC, 0xDD]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); - assert_eq!(&d.platform.storage.data[..2], &[0xAA, 0xBB]); + assert_eq!(&d.platform.storage.data[..4], &[0xAA, 0xBB, 0xCC, 0xDD]); } #[test] -fn flush_empty_is_ok() { - let mut p = platform(BootState::Idle); +fn write_non_sequential_without_flush_rejected() { + let mut p = platform(BootState::Updating); let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); - d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); + + d.platform.transport.load_request(Cmd::Write, 0, 4, &[0; 4]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); + + d.platform + .transport + .load_request(Cmd::Write, 128, 4, &[0; 4]); + d.dispatch().unwrap(); + assert_eq!(d.frame.status, Status::AddrOutOfBounds); } #[test] -fn write_non_sequential_without_flush_rejected() { +fn flush_flag_resets_state_for_new_region() { let mut p = platform(BootState::Updating); let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); - // First write at addr 0 - d.platform.transport.load_request(Cmd::Write, 0, 4, &[0; 4]); + d.platform + .transport + .load_request_flags(Cmd::Write, 0, flush_flags(), 4, &[0xAA; 4]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); - // Non-sequential write without Flush + // After FLUSH, next write can be at any page-aligned address d.platform .transport - .load_request(Cmd::Write, 128, 4, &[0; 4]); + .load_request_flags(Cmd::Write, 128, flush_flags(), 4, &[0xBB; 4]); d.dispatch().unwrap(); - assert_eq!(d.frame.status, Status::AddrOutOfBounds); + assert_eq!(d.frame.status, Status::Ok); + assert_eq!(&d.platform.storage.data[128..132], &[0xBB; 4]); } diff --git a/lib/protocol/Cargo.toml b/lib/protocol/Cargo.toml index 269eba0..0f28c45 100644 --- a/lib/protocol/Cargo.toml +++ b/lib/protocol/Cargo.toml @@ -10,5 +10,6 @@ categories = ["embedded", "no-std"] readme = "README.md" [dependencies] +bitflags = "2" embedded-io = { workspace = true } embedded-io-async = { workspace = true } diff --git a/lib/protocol/src/frame.rs b/lib/protocol/src/frame.rs index 73c88ca..86aa2cb 100644 --- a/lib/protocol/src/frame.rs +++ b/lib/protocol/src/frame.rs @@ -2,7 +2,7 @@ use core::mem::MaybeUninit; use crate::crc::{CRC_INIT, crc16}; use crate::sync::Sync; -use crate::{Cmd, ReadError, Status}; +use crate::{Cmd, ReadError, ResetFlags, Status, WriteFlags}; /// Maximum data payload size per frame. pub const MAX_PAYLOAD: usize = 64; @@ -42,6 +42,21 @@ pub struct VerifyData { pub crc: u16, } +/// Per-command flags occupying addr byte 3. +/// +/// Each command can define its own bitflags type as a union variant. +/// All variants are `u8` so the union is always 1 byte. +#[repr(C)] +#[derive(Clone, Copy)] +pub union Flags { + /// Raw byte access. + pub raw: u8, + /// [`Cmd::Write`] flags. + pub write: WriteFlags, + /// [`Cmd::Reset`] flags. + pub reset: ResetFlags, +} + /// Union-typed data payload. /// /// Provides zero-cost typed access to frame data. Reading fields is unsafe @@ -71,8 +86,12 @@ pub struct Frame { pub cmd: Cmd, /// Response status (always [`Status::Request`] for requests). pub status: Status, - /// Flash address (for Write/Erase) or mode selector (for Reset). - pub addr: u32, + /// Address bits [15:0]. + pub addr_lo: u16, + /// Address bits [23:16]. + pub addr_hi: u8, + /// Per-command flags (addr byte 3). + pub flags: Flags, /// Data payload length in bytes (0..64). pub len: u16, /// Payload data (union-typed). @@ -90,13 +109,30 @@ impl Default for Frame { frame.sync = Sync::default(); frame.cmd = Cmd::Info; frame.status = Status::Request; - frame.addr = 0; + frame.addr_lo = 0; + frame.addr_hi = 0; + frame.flags = Flags { raw: 0 }; frame.len = 0; frame.crc = [0; 2]; frame } } +impl Frame { + /// Reconstruct the 24-bit address from lo + hi. + #[inline(always)] + pub fn addr(&self) -> u32 { + self.addr_lo as u32 | (self.addr_hi as u32) << 16 + } + + /// Set the 24-bit address (lo + hi). Does not touch flags. + #[inline(always)] + pub fn set_addr(&mut self, addr: u32) { + self.addr_lo = addr as u16; + self.addr_hi = (addr >> 16) as u8; + } +} + impl Frame { fn as_bytes(&self, offset: usize, len: usize) -> &[u8] { debug_assert!(offset + len <= core::mem::size_of::()); @@ -291,10 +327,10 @@ mod tests { let mut f = Frame { cmd, status, - addr, - len: data.len() as u16, ..Default::default() }; + f.set_addr(addr); + f.len = data.len() as u16; unsafe { f.data.raw[..data.len()].copy_from_slice(data) }; f } @@ -310,7 +346,7 @@ mod tests { frame2.read(&mut MockReader::new(sink.written())).unwrap(); assert_eq!(frame2.cmd, Cmd::Write); assert_eq!(frame2.len, 2); - assert_eq!(frame2.addr, 0x0800); + assert_eq!(frame2.addr(), 0x0800); assert_eq!(frame2.status, Status::Request); assert_eq!(unsafe { &frame2.data.raw[..2] }, &[0xDE, 0xAD]); } @@ -354,7 +390,7 @@ mod tests { let mut frame2 = Frame::default(); frame2.read(&mut MockReader::new(sink.written())).unwrap(); - assert_eq!(frame2.addr, 0x0001_0800); + assert_eq!(frame2.addr(), 0x0001_0800); } #[test] @@ -379,7 +415,7 @@ mod tests { host.read(&mut MockReader::new(resp_sink.written())) .unwrap(); assert_eq!(host.cmd, Cmd::Write); - assert_eq!(host.addr, 0x0400); + assert_eq!(host.addr(), 0x0400); assert_eq!(host.status, Status::Ok); } diff --git a/lib/protocol/src/lib.rs b/lib/protocol/src/lib.rs index 58b2451..48aade9 100644 --- a/lib/protocol/src/lib.rs +++ b/lib/protocol/src/lib.rs @@ -12,7 +12,29 @@ pub mod crc; pub mod frame; pub(crate) mod sync; -pub use frame::{Data, EraseData, InfoData, MAX_PAYLOAD, VerifyData}; +pub use frame::{Data, EraseData, Flags, InfoData, MAX_PAYLOAD, VerifyData}; + +use bitflags::bitflags; + +bitflags! { + /// Flags for [`Cmd::Write`] (addr byte 3). + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct WriteFlags: u8 { + /// Commit the page after this write and reset write state. + /// Set on the last write of each contiguous region (address jump + /// or end of transfer). + const FLUSH = 1 << 7; + } +} + +bitflags! { + /// Flags for [`Cmd::Reset`] (addr byte 3). + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct ResetFlags: u8 { + /// Enter bootloader (service mode) instead of booting the app. + const BOOTLOADER = 1 << 0; + } +} /// Pack a semantic version into a `u16` using 5.5.6 encoding. /// @@ -74,16 +96,14 @@ pub enum Cmd { Write = 0x02, /// Compute CRC16 over app region and transition to Validating. Verify = 0x03, - /// Reset the device. `addr=0`: boot app, `addr=1`: enter bootloader. + /// Reset the device. See [`ResetFlags`]. Reset = 0x04, - /// Flush buffered writes to storage. - Flush = 0x05, } impl Cmd { /// Returns true if `b` is a valid command code. pub fn is_valid(b: u8) -> bool { - b <= 0x05 + b <= 0x04 } } @@ -130,8 +150,7 @@ mod tests { fn cmd_is_valid() { assert!(Cmd::is_valid(Cmd::Info as u8)); assert!(Cmd::is_valid(Cmd::Reset as u8)); - assert!(Cmd::is_valid(Cmd::Flush as u8)); - assert!(!Cmd::is_valid(0x06)); + assert!(!Cmd::is_valid(0x05)); assert!(!Cmd::is_valid(0xFF)); } From d4768a9082765c85f61547b31082a4c7aa7412db Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 19:59:02 -0700 Subject: [PATCH 11/15] refactor dispatcher for readability and reduce size by eliminating jump table in .rodata --- lib/core/src/protocol.rs | 290 +++++++++++++++++++++------------------ 1 file changed, 160 insertions(+), 130 deletions(-) diff --git a/lib/core/src/protocol.rs b/lib/core/src/protocol.rs index 429234e..671eec5 100644 --- a/lib/core/src/protocol.rs +++ b/lib/core/src/protocol.rs @@ -44,6 +44,153 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz self.buf.consume(n); } + #[inline(always)] + fn cmd_write( + &mut self, + addr: u32, + data_len: usize, + flags: WriteFlags, + capacity: u32, + state: BootState, + ) { + if state != BootState::Updating { + self.frame.status = Status::Unsupported; + return; + } + + let flush = flags.contains(WriteFlags::FLUSH); + + if addr + data_len as u32 > capacity + || (self.next_addr.is_none() && !addr.is_multiple_of(S::WRITE_SIZE as u32)) + || self.next_addr.is_some_and(|n| n != addr) + { + self.frame.status = Status::AddrOutOfBounds; + return; + } + + // SAFETY: data_len <= MAX_PAYLOAD validated by frame.read() + self.buf + .push(unsafe { self.frame.data.raw.get_unchecked(..data_len) }); + let next = addr + data_len as u32; + self.next_addr = Some(next); + if self.buf.len() >= S::WRITE_SIZE { + self.write_buf(next, S::WRITE_SIZE); + } + if flush { + if !self.buf.is_empty() { + self.write_buf(next, self.buf.len()); + } + self.buf.reset(); + self.next_addr = None; + } + } + + #[inline(always)] + fn cmd_erase(&mut self, addr: u32, capacity: u32, state: BootState) { + let erase_size = S::ERASE_SIZE as u32; + let byte_count = unsafe { self.frame.data.erase }.byte_count as u32; + + if !addr.is_multiple_of(erase_size) + || !byte_count.is_multiple_of(erase_size) + || byte_count == 0 + || addr + byte_count > capacity + { + self.frame.status = Status::AddrOutOfBounds; + return; + } + + match state { + BootState::Idle => { + if self.platform.boot_meta.advance().is_err() { + self.frame.status = Status::WriteError; + } + } + BootState::Validating => { + if self + .platform + .boot_meta + .refresh(0xFFFF, BootState::Updating, 0xFFFF_FFFF) + .is_err() + { + self.frame.status = Status::WriteError; + } + } + BootState::Updating => {} + } + + if self.frame.status == Status::Ok + && self + .platform + .storage + .erase(addr, addr + byte_count) + .is_err() + { + self.frame.status = Status::WriteError; + } + } + + #[inline(always)] + fn cmd_info(&mut self, capacity: u32) { + let app_sz = self.platform.boot_meta.app_size(); + let app_ver = if app_sz != 0xFFFF_FFFF { + // SAFETY: non-default app_size implies Verify bounded it. + let base = self.platform.storage.as_slice().as_ptr(); + unsafe { base.add(app_sz as usize - 2).cast::().read_volatile() } + } else { + 0xFFFF + }; + self.frame.len = 12; + self.frame.data.info = InfoData { + capacity, + erase_size: S::ERASE_SIZE as u16, + boot_version: crate::tinyboot_version(), + app_version: app_ver, + mode: 0, + }; + } + + #[inline(always)] + fn cmd_verify(&mut self, addr: u32, capacity: u32, state: BootState) { + if state != BootState::Updating { + self.frame.status = Status::Unsupported; + return; + } + + let app_size = addr; + let sz = app_size as usize; + if sz == 0 || sz > capacity as usize { + self.frame.status = Status::AddrOutOfBounds; + return; + } + + // SAFETY: sz bounded against capacity above. + let crc = crc16(CRC_INIT, unsafe { + self.platform.storage.as_slice().get_unchecked(..sz) + }); + self.frame.len = 2; + self.frame.data.verify = VerifyData { crc }; + if self + .platform + .boot_meta + .refresh(crc, BootState::Validating, app_size) + .is_err() + { + self.frame.status = Status::WriteError; + } + } + + #[inline(always)] + fn cmd_reset(&mut self, flags: ResetFlags) { + let _ = self.frame.send(&mut self.platform.transport); + let mode = if flags.contains(ResetFlags::BOOTLOADER) { + RunMode::Service + } else { + RunMode::HandOff + }; + self.platform.ctl.set_run_mode(mode); + self.platform.ctl.reset(); + } + /// Read a frame, dispatch, send the response. Err only on transport IO; /// frame-level errors (bad CRC) are silently skipped. pub fn dispatch(&mut self) -> Result<(), ReadError> { @@ -62,140 +209,23 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz let addr = self.frame.addr(); let flags = self.frame.flags; let capacity = self.platform.storage.capacity() as u32; - let erase_size = S::ERASE_SIZE as u32; - let write_size = S::WRITE_SIZE as u32; let state = self.platform.boot_meta.boot_state(); self.frame.len = 0; self.frame.status = Status::Ok; - match self.frame.cmd { - Cmd::Info => { - self.frame.len = 12; - let app_sz = self.platform.boot_meta.app_size(); - let app_ver = if app_sz != 0xFFFF_FFFF { - // SAFETY: non-default app_size implies Verify bounded it. - let base = self.platform.storage.as_slice().as_ptr(); - unsafe { base.add(app_sz as usize - 2).cast::().read_volatile() } - } else { - 0xFFFF - }; - let boot_version = crate::tinyboot_version(); - self.frame.data.info = InfoData { - capacity, - erase_size: erase_size as u16, - boot_version, - app_version: app_ver, - mode: 0, - }; - } - Cmd::Erase => { - - let byte_count = unsafe { self.frame.data.erase }.byte_count as u32; - if !addr.is_multiple_of(erase_size) - || !byte_count.is_multiple_of(erase_size) - || byte_count == 0 - || addr + byte_count > capacity - { - self.frame.status = Status::AddrOutOfBounds; - } else { - match state { - // Idle → Updating via bit-clear. - BootState::Idle => { - if self.platform.boot_meta.advance().is_err() { - self.frame.status = Status::WriteError; - } - } - // Validating → Updating (app failed, reflashing). - BootState::Validating => { - if self - .platform - .boot_meta - .refresh(0xFFFF, BootState::Updating, 0xFFFF_FFFF) - .is_err() - { - self.frame.status = Status::WriteError; - } - } - BootState::Updating => {} - } - if self.frame.status == Status::Ok - && self - .platform - .storage - .erase(addr, addr + byte_count) - .is_err() - { - self.frame.status = Status::WriteError; - } - } - } - Cmd::Write => { - if state != BootState::Updating { - self.frame.status = Status::Unsupported; - } else { - - let flush = unsafe { flags.write }.contains(WriteFlags::FLUSH); - - if addr + data_len as u32 > capacity - || (self.next_addr.is_none() && !addr.is_multiple_of(write_size)) - || self.next_addr.is_some_and(|n| n != addr) - { - self.frame.status = Status::AddrOutOfBounds; - } else { - // SAFETY: data_len <= MAX_PAYLOAD validated by frame.read() - self.buf - .push(unsafe { self.frame.data.raw.get_unchecked(..data_len) }); - let next = addr + data_len as u32; - self.next_addr = Some(next); - if self.buf.len() >= S::WRITE_SIZE { - self.write_buf(next, S::WRITE_SIZE); - } - if flush { - if !self.buf.is_empty() { - self.write_buf(next, self.buf.len()); - } - self.buf.reset(); - self.next_addr = None; - } - } - } - } - Cmd::Verify => { - if state != BootState::Updating { - self.frame.status = Status::Unsupported; - } else { - let app_size = addr; - let sz = app_size as usize; - if sz == 0 || sz > capacity as usize { - self.frame.status = Status::AddrOutOfBounds; - } else { - // SAFETY: sz bounded against capacity above. - let crc = crc16(CRC_INIT, unsafe { - self.platform.storage.as_slice().get_unchecked(..sz) - }); - self.frame.len = 2; - self.frame.data.verify = VerifyData { crc }; - if self - .platform - .boot_meta - .refresh(crc, BootState::Validating, app_size) - .is_err() - { - self.frame.status = Status::WriteError; - } - } - } - } - Cmd::Reset => { - let _ = self.frame.send(&mut self.platform.transport); - let mode = if unsafe { flags.reset }.contains(ResetFlags::BOOTLOADER) { - RunMode::Service - } else { - RunMode::HandOff - }; - self.platform.ctl.set_run_mode(mode); - self.platform.ctl.reset(); - } + let cmd = self.frame.cmd; + if cmd == Cmd::Write { + // SAFETY: cmd is Write, so flags.write is the active variant. + self.cmd_write(addr, data_len, unsafe { flags.write }, capacity, state); + } else if cmd == Cmd::Erase { + self.cmd_erase(addr, capacity, state); + } else if cmd == Cmd::Info { + self.cmd_info(capacity); + } else if cmd == Cmd::Verify { + self.cmd_verify(addr, capacity, state); + } else { + // SAFETY: cmd is Reset, so flags.reset is the active variant. + self.cmd_reset(unsafe { flags.reset }); } self.frame From e3986a8e32ecd9cb0f05dc3af1a4bfaadb20d3b0 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 20:32:23 -0700 Subject: [PATCH 12/15] dedupe frame.send call to reduce flash size. --- lib/core/src/protocol.rs | 52 +++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/lib/core/src/protocol.rs b/lib/core/src/protocol.rs index 671eec5..e4fdf75 100644 --- a/lib/core/src/protocol.rs +++ b/lib/core/src/protocol.rs @@ -196,36 +196,32 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz pub fn dispatch(&mut self) -> Result<(), ReadError> { let status = self.frame.read(&mut self.platform.transport)?; - if status != Status::Ok { + if status == Status::Ok { + let data_len = self.frame.len as usize; + let addr = self.frame.addr(); + let flags = self.frame.flags; + let capacity = self.platform.storage.capacity() as u32; + let state = self.platform.boot_meta.boot_state(); self.frame.len = 0; - self.frame.status = status; - return self - .frame - .send(&mut self.platform.transport) - .map_err(|_| ReadError); - } - - let data_len = self.frame.len as usize; - let addr = self.frame.addr(); - let flags = self.frame.flags; - let capacity = self.platform.storage.capacity() as u32; - let state = self.platform.boot_meta.boot_state(); - self.frame.len = 0; - self.frame.status = Status::Ok; - - let cmd = self.frame.cmd; - if cmd == Cmd::Write { - // SAFETY: cmd is Write, so flags.write is the active variant. - self.cmd_write(addr, data_len, unsafe { flags.write }, capacity, state); - } else if cmd == Cmd::Erase { - self.cmd_erase(addr, capacity, state); - } else if cmd == Cmd::Info { - self.cmd_info(capacity); - } else if cmd == Cmd::Verify { - self.cmd_verify(addr, capacity, state); + self.frame.status = Status::Ok; + + let cmd = self.frame.cmd; + if cmd == Cmd::Write { + // SAFETY: cmd is Write, so flags.write is the active variant. + self.cmd_write(addr, data_len, unsafe { flags.write }, capacity, state); + } else if cmd == Cmd::Erase { + self.cmd_erase(addr, capacity, state); + } else if cmd == Cmd::Info { + self.cmd_info(capacity); + } else if cmd == Cmd::Verify { + self.cmd_verify(addr, capacity, state); + } else { + // SAFETY: cmd is Reset, so flags.reset is the active variant. + self.cmd_reset(unsafe { flags.reset }); + } } else { - // SAFETY: cmd is Reset, so flags.reset is the active variant. - self.cmd_reset(unsafe { flags.reset }); + self.frame.len = 0; + self.frame.status = status; } self.frame From 9e81e730893e79b75a9c78419adfb54a45c1b45b Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 21:31:03 -0700 Subject: [PATCH 13/15] move UART transport to the second system flash region so it can compile with all features enabled. --- ch32/build.rs | 13 ++++++ ch32/split-sysflash.x | 10 +++++ ch32/src/platform/transport/usart/mod.rs | 14 +++++- examples/ch32/v003/app/src/main.rs | 14 +++++- examples/ch32/v003/app/src/transport.rs | 44 +++++++++++++++++-- examples/ch32/v003/boot/src/main.rs | 5 ++- examples/ch32/v00x/app/src/main.rs | 9 +--- examples/ch32/v00x/app/src/transport.rs | 2 +- examples/ch32/v103/app/src/main.rs | 14 +++++- examples/ch32/v103/app/src/transport.rs | 44 +++++++++++++++++-- examples/ch32/v103/boot/build.rs | 3 ++ .../ch32/v103/boot/memory_x/system-flash.x | 12 ++--- examples/ch32/v103/boot/src/main.rs | 5 ++- 13 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 ch32/split-sysflash.x diff --git a/ch32/build.rs b/ch32/build.rs index 87defc1..79e362c 100644 --- a/ch32/build.rs +++ b/ch32/build.rs @@ -66,6 +66,14 @@ fn main() -> Result<(), Box> { } } + // split_sysflash: system flash is split into two regions by option bytes. + // Flash HAL functions are placed in the second region (.text2). + println!("cargo::rustc-check-cfg=cfg(split_sysflash)"); + if system_flash && boot_pin { + println!("cargo:rustc-cfg=split_sysflash"); + cfgs.push("split_sysflash".to_string()); + } + println!("cargo:cfgs={}", cfgs.join(",")); generate_pin_and_usart_mapping(out)?; @@ -75,6 +83,11 @@ fn main() -> Result<(), Box> { fs::copy("tb-run-mode.x", out.join("tb-run-mode.x"))?; println!("cargo:rerun-if-changed=tb-run-mode.x"); + if system_flash && boot_pin { + fs::copy("split-sysflash.x", out.join("split-sysflash.x"))?; + println!("cargo:rerun-if-changed=split-sysflash.x"); + } + println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rerun-if-changed=build.rs"); diff --git a/ch32/split-sysflash.x b/ch32/split-sysflash.x new file mode 100644 index 0000000..882f5a1 --- /dev/null +++ b/ch32/split-sysflash.x @@ -0,0 +1,10 @@ +/* Split system-flash: places .text2 into the second system-flash region + * (CODE2/BOOT2). Only linked when the memory layout defines those regions. */ +SECTIONS +{ + .text2 : ALIGN(4) + { + *(.text2 .text2.*); + } > CODE2 AT> BOOT2 +} +INSERT AFTER .rodata; diff --git a/ch32/src/platform/transport/usart/mod.rs b/ch32/src/platform/transport/usart/mod.rs index d2385c9..cbc87fb 100644 --- a/ch32/src/platform/transport/usart/mod.rs +++ b/ch32/src/platform/transport/usart/mod.rs @@ -55,7 +55,9 @@ pub struct Usart { impl tinyboot_core::traits::Transport for Usart {} impl Usart { - #[inline(always)] + #[cfg_attr(split_sysflash, inline(never))] + #[cfg_attr(not(split_sysflash), inline(always))] + #[cfg_attr(split_sysflash, unsafe(link_section = ".text2"))] pub fn new(config: &UsartConfig) -> Self { let tx_pin = config.mapping.tx_pin(); let rx_pin = config.mapping.rx_pin(); @@ -126,6 +128,8 @@ impl ErrorType for Usart { } impl embedded_io::Read for Usart { + #[cfg_attr(split_sysflash, inline(never))] + #[cfg_attr(split_sysflash, unsafe(link_section = ".text2"))] fn read(&mut self, buf: &mut [u8]) -> Result { if buf.is_empty() { return Ok(0); @@ -134,6 +138,8 @@ impl embedded_io::Read for Usart { Ok(1) } + #[cfg_attr(split_sysflash, inline(never))] + #[cfg_attr(split_sysflash, unsafe(link_section = ".text2"))] fn read_exact( &mut self, buf: &mut [u8], @@ -147,11 +153,15 @@ impl embedded_io::Read for Usart { } impl embedded_io::Write for Usart { + #[cfg_attr(split_sysflash, inline(never))] + #[cfg_attr(split_sysflash, unsafe(link_section = ".text2"))] fn write(&mut self, buf: &[u8]) -> Result { self.write_all(buf)?; Ok(buf.len()) } + #[cfg_attr(split_sysflash, inline(never))] + #[cfg_attr(split_sysflash, unsafe(link_section = ".text2"))] fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { self.set_tx_mode(); let regs = self.regs; @@ -161,6 +171,8 @@ impl embedded_io::Write for Usart { Ok(()) } + #[cfg_attr(split_sysflash, inline(never))] + #[cfg_attr(split_sysflash, unsafe(link_section = ".text2"))] fn flush(&mut self) -> Result<(), Self::Error> { usart::flush(self.regs); self.set_rx_mode(); diff --git a/examples/ch32/v003/app/src/main.rs b/examples/ch32/v003/app/src/main.rs index 93743da..cbff5b9 100644 --- a/examples/ch32/v003/app/src/main.rs +++ b/examples/ch32/v003/app/src/main.rs @@ -58,7 +58,19 @@ fn main() -> ! { let uart = Uart::new_blocking::<0>(p.USART1, p.PD6, p.PD5, uart_config).unwrap(); let (tx, rx) = uart.split(); let mut rx = transport::Rx(rx); - let mut tx = transport::Tx(tx); + + // RS-485 DE/RE on PC2, matching the bootloader. tx_level=Low means idle-RX + // drives the pin High (inverse), which tri-states the transceiver so the + // programmer UART TX can reach MCU_RX without contention. + let tx_level = Level::Low; + let tx_en_pin = Output::new(p.PC2, transport::invert(tx_level), Default::default()); + let mut tx = transport::Tx { + uart: tx, + tx_en: Some(transport::TxEn { + pin: tx_en_pin, + tx_level, + }), + }; let mut app = tinyboot_ch32::app::new_app(tinyboot_ch32::app::BootCtl::new()); app.confirm(); diff --git a/examples/ch32/v003/app/src/transport.rs b/examples/ch32/v003/app/src/transport.rs index 1b08e54..6f6b0c1 100644 --- a/examples/ch32/v003/app/src/transport.rs +++ b/examples/ch32/v003/app/src/transport.rs @@ -1,5 +1,6 @@ -//! embedded_io adapters for ch32-hal blocking UART. +//! embedded_io adapters for ch32-hal blocking UART with optional RS-485 DE/RE. +use ch32_hal::gpio::{Level, Output}; use ch32_hal::mode::Blocking; use ch32_hal::usart::{Instance, UartRx, UartTx}; use core::convert::Infallible; @@ -21,7 +22,36 @@ impl Read for Rx<'_, T> { } } -pub struct Tx<'d, T: Instance>(pub UartTx<'d, T, Blocking>); +pub struct TxEn<'d> { + pub pin: Output<'d>, + /// Level driven to enable TX; the inverse enables RX. + pub tx_level: Level, +} + +impl TxEn<'_> { + #[inline(always)] + fn set_tx(&mut self) { + self.pin.set_level(self.tx_level); + } + + #[inline(always)] + fn set_rx(&mut self) { + self.pin.set_level(invert(self.tx_level)); + } +} + +#[inline(always)] +pub fn invert(level: Level) -> Level { + match level { + Level::High => Level::Low, + Level::Low => Level::High, + } +} + +pub struct Tx<'d, T: Instance> { + pub uart: UartTx<'d, T, Blocking>, + pub tx_en: Option>, +} impl ErrorType for Tx<'_, T> { type Error = Infallible; @@ -29,12 +59,18 @@ impl ErrorType for Tx<'_, T> { impl Write for Tx<'_, T> { fn write(&mut self, buf: &[u8]) -> Result { - let _ = self.0.blocking_write(buf); + if let Some(tx_en) = self.tx_en.as_mut() { + tx_en.set_tx(); + } + let _ = self.uart.blocking_write(buf); Ok(buf.len()) } fn flush(&mut self) -> Result<(), Self::Error> { - let _ = self.0.blocking_flush(); + let _ = self.uart.blocking_flush(); + if let Some(tx_en) = self.tx_en.as_mut() { + tx_en.set_rx(); + } Ok(()) } } diff --git a/examples/ch32/v003/boot/src/main.rs b/examples/ch32/v003/boot/src/main.rs index d1bfa62..7b53983 100644 --- a/examples/ch32/v003/boot/src/main.rs +++ b/examples/ch32/v003/boot/src/main.rs @@ -33,7 +33,10 @@ fn main() -> ! { pclk: 8_000_000, mapping: UsartMapping::Usart1Remap0, rx_pull: Pull::None, - tx_en: None, + tx_en: Some(TxEnConfig { + pin: Pin::PC2, + tx_level: Level::Low, + }), }); tinyboot_ch32::boot::run(transport, BootCtl::new()); } diff --git a/examples/ch32/v00x/app/src/main.rs b/examples/ch32/v00x/app/src/main.rs index cfe0135..c70af5d 100644 --- a/examples/ch32/v00x/app/src/main.rs +++ b/examples/ch32/v00x/app/src/main.rs @@ -32,13 +32,6 @@ fn panic(_info: &core::panic::PanicInfo) -> ! { type Shared = Mutex>>; static LED: Shared> = Mutex::new(RefCell::new(None)); -fn invert_level(level: Level) -> Level { - match level { - Level::High => Level::Low, - Level::Low => Level::High, - } -} - #[qingke_rt::entry] fn main() -> ! { let p = ch32_hal::init(Default::default()); @@ -70,7 +63,7 @@ fn main() -> ! { // drives the pin High (inverse), which tri-states U4A on the V006 dev board // so LinkE UART TX can reach MCU_RX without contention. let tx_level = Level::Low; - let tx_en_pin = Output::new(p.PC2, invert_level(tx_level), Default::default()); + let tx_en_pin = Output::new(p.PC2, transport::invert(tx_level), Default::default()); let mut tx = transport::Tx { uart: tx, tx_en: Some(transport::TxEn { diff --git a/examples/ch32/v00x/app/src/transport.rs b/examples/ch32/v00x/app/src/transport.rs index 4f3fc8a..6f6b0c1 100644 --- a/examples/ch32/v00x/app/src/transport.rs +++ b/examples/ch32/v00x/app/src/transport.rs @@ -41,7 +41,7 @@ impl TxEn<'_> { } #[inline(always)] -fn invert(level: Level) -> Level { +pub fn invert(level: Level) -> Level { match level { Level::High => Level::Low, Level::Low => Level::High, diff --git a/examples/ch32/v103/app/src/main.rs b/examples/ch32/v103/app/src/main.rs index 503e2e7..ee97030 100644 --- a/examples/ch32/v103/app/src/main.rs +++ b/examples/ch32/v103/app/src/main.rs @@ -56,7 +56,19 @@ fn main() -> ! { 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); + + // RS-485 DE/RE on PC2, matching the bootloader. tx_level=Low means idle-RX + // drives the pin High (inverse), which tri-states the transceiver so the + // programmer UART TX can reach MCU_RX without contention. + let tx_level = Level::Low; + let tx_en_pin = Output::new(p.PC2, transport::invert(tx_level), Default::default()); + let mut tx = transport::Tx { + uart: tx, + tx_en: Some(transport::TxEn { + pin: tx_en_pin, + tx_level, + }), + }; // system-flash: pass BOOT0 pin, the level that selects system flash, // and the reset delay in cycles (RC ~1ms @ 8MHz = 8000; flip-flop: 0). diff --git a/examples/ch32/v103/app/src/transport.rs b/examples/ch32/v103/app/src/transport.rs index 1b08e54..6f6b0c1 100644 --- a/examples/ch32/v103/app/src/transport.rs +++ b/examples/ch32/v103/app/src/transport.rs @@ -1,5 +1,6 @@ -//! embedded_io adapters for ch32-hal blocking UART. +//! embedded_io adapters for ch32-hal blocking UART with optional RS-485 DE/RE. +use ch32_hal::gpio::{Level, Output}; use ch32_hal::mode::Blocking; use ch32_hal::usart::{Instance, UartRx, UartTx}; use core::convert::Infallible; @@ -21,7 +22,36 @@ impl Read for Rx<'_, T> { } } -pub struct Tx<'d, T: Instance>(pub UartTx<'d, T, Blocking>); +pub struct TxEn<'d> { + pub pin: Output<'d>, + /// Level driven to enable TX; the inverse enables RX. + pub tx_level: Level, +} + +impl TxEn<'_> { + #[inline(always)] + fn set_tx(&mut self) { + self.pin.set_level(self.tx_level); + } + + #[inline(always)] + fn set_rx(&mut self) { + self.pin.set_level(invert(self.tx_level)); + } +} + +#[inline(always)] +pub fn invert(level: Level) -> Level { + match level { + Level::High => Level::Low, + Level::Low => Level::High, + } +} + +pub struct Tx<'d, T: Instance> { + pub uart: UartTx<'d, T, Blocking>, + pub tx_en: Option>, +} impl ErrorType for Tx<'_, T> { type Error = Infallible; @@ -29,12 +59,18 @@ impl ErrorType for Tx<'_, T> { impl Write for Tx<'_, T> { fn write(&mut self, buf: &[u8]) -> Result { - let _ = self.0.blocking_write(buf); + if let Some(tx_en) = self.tx_en.as_mut() { + tx_en.set_tx(); + } + let _ = self.uart.blocking_write(buf); Ok(buf.len()) } fn flush(&mut self) -> Result<(), Self::Error> { - let _ = self.0.blocking_flush(); + let _ = self.uart.blocking_flush(); + if let Some(tx_en) = self.tx_en.as_mut() { + tx_en.set_rx(); + } Ok(()) } } diff --git a/examples/ch32/v103/boot/build.rs b/examples/ch32/v103/boot/build.rs index 44b422b..f919647 100644 --- a/examples/ch32/v103/boot/build.rs +++ b/examples/ch32/v103/boot/build.rs @@ -23,6 +23,9 @@ fn main() { println!("cargo:rerun-if-changed=memory_x"); println!("cargo:rustc-link-arg=-Ttb-boot.x"); println!("cargo:rustc-link-arg=-Ttb-run-mode.x"); + if system_flash { + println!("cargo:rustc-link-arg=-Tsplit-sysflash.x"); + } } fn cfg_has(key: &str) -> bool { diff --git a/examples/ch32/v103/boot/memory_x/system-flash.x b/examples/ch32/v103/boot/memory_x/system-flash.x index a3d21c9..90ec04b 100644 --- a/examples/ch32/v103/boot/memory_x/system-flash.x +++ b/examples/ch32/v103/boot/memory_x/system-flash.x @@ -13,11 +13,13 @@ MEMORY { RAM : ORIGIN = 0x20000000, LENGTH = 20K - 4 /* __tb_run_mode */ - /* Execution mirror of BOOT */ - CODE : ORIGIN = 0x00000000, LENGTH = 2048 + /* Execution mirror of BOOT / BOOT2 */ + CODE : ORIGIN = 0x00000000, LENGTH = 2048 + CODE2 : ORIGIN = 0x00000900, LENGTH = 1792 /* Physical flash addresses */ - BOOT : ORIGIN = 0x1FFFF000, LENGTH = 2048 - APP : ORIGIN = 0x08000000, LENGTH = 64K - 128 - META : ORIGIN = 0x08000000 + 64K - 128, LENGTH = 128 + BOOT : ORIGIN = 0x1FFFF000, LENGTH = 2048 + BOOT2 : ORIGIN = 0x1FFFF900, LENGTH = 1792 + APP : ORIGIN = 0x08000000, LENGTH = 64K - 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 index b06ff93..39a7ba7 100644 --- a/examples/ch32/v103/boot/src/main.rs +++ b/examples/ch32/v103/boot/src/main.rs @@ -26,7 +26,10 @@ fn main() -> ! { pclk: 8_000_000, mapping: UsartMapping::Usart1Remap0, rx_pull: Pull::None, - tx_en: None, + tx_en: Some(TxEnConfig { + pin: Pin::PC2, + tx_level: Level::Low, + }), }); // system-flash: GPIO drives the external BOOT0 circuit. The Level arg is From 16335ee062e8ce4bd499966514e4f19d2da3fd93 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 23:42:07 -0700 Subject: [PATCH 14/15] udpate docs. thanks AI --- README.md | 65 +++++++++++++++++++++--------------------- ch32/README.md | 27 +++++++++--------- lib/protocol/README.md | 59 ++++++++++++++++++++++++-------------- vendor/README.md | 14 ++++----- 4 files changed, 91 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index ab195f6..eb8ef3c 100644 --- a/README.md +++ b/README.md @@ -18,30 +18,30 @@ tinyboot currently supports **UART / RS-485** transport. The table below tracks ✅ Verified | ❓ Untested (same die, likely works — volunteer needed) | 📋 Planned -| Chip | Feature Flag | System Flash | Status | Blocker | -| ------------ | -------------- | ------------------------- | ------ | -------------------------------------- | -| CH32V003F4P6 | `ch32v003f4p6` | `0x1FFFF000` (1920B) | ✅ | -- | -| 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]) | -| CH32V006X8X6 | `ch32v006x8x6` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) | -| CH32V007X8X6 | `ch32v007x8x6` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) | -| CH32X033F8P6 | `ch32x033f8p6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | -| CH32X034F8P6 | `ch32x034f8p6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | -| CH32X034F8U6 | `ch32x034f8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | -| CH32X035C8T6 | `ch32x035c8t6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | -| CH32X035F7P6 | `ch32x035f7p6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | -| CH32X035F8U6 | `ch32x035f8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | -| CH32X035G8R6 | `ch32x035g8r6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | -| CH32X035G8U6 | `ch32x035g8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | -| CH32X035R8T6 | `ch32x035r8t6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- | +| Chip | Feature Flag | System Flash | Status | +| ------------ | -------------- | ------------------------- | ------ | +| CH32V003F4P6 | `ch32v003f4p6` | `0x1FFFF000` (1920B) | ✅ | +| 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) | ❓ | +| CH32V004X6X1 | `ch32v004x6x1` | `0x1FFF0000` (3KB + 256B) | ❓ | +| CH32V005X6X6 | `ch32v005x6x6` | `0x1FFF0000` (3KB + 256B) | ❓ | +| CH32V006X8X6 | `ch32v006x8x6` | `0x1FFF0000` (3KB + 256B) | ✅ | +| CH32V007X8X6 | `ch32v007x8x6` | `0x1FFF0000` (3KB + 256B) | ❓ | +| CH32X033F8P6 | `ch32x033f8p6` | `0x1FFF0000` (3KB + 256B) | 📋 | +| CH32X034F8P6 | `ch32x034f8p6` | `0x1FFF0000` (3KB + 256B) | 📋 | +| CH32X034F8U6 | `ch32x034f8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | +| CH32X035C8T6 | `ch32x035c8t6` | `0x1FFF0000` (3KB + 256B) | 📋 | +| CH32X035F7P6 | `ch32x035f7p6` | `0x1FFF0000` (3KB + 256B) | 📋 | +| CH32X035F8U6 | `ch32x035f8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | +| CH32X035G8R6 | `ch32x035g8r6` | `0x1FFF0000` (3KB + 256B) | 📋 | +| CH32X035G8U6 | `ch32x035g8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | +| CH32X035R8T6 | `ch32x035r8t6` | `0x1FFF0000` (3KB + 256B) | 📋 | ## Features @@ -86,16 +86,17 @@ ch32/ CH32 implementation cli/ tinyboot — host CLI flasher examples/ch32/v003/ CH32V003 boot + app examples +examples/ch32/v00x/ CH32V00x (V002/V004/V005/V006/V007) boot + app examples examples/ch32/v103/ CH32V103 boot + app examples ``` -| Crate | Description | -| ---------------------------------------- | --------------------------------------------------------------------------------------------------- | -| [`tinyboot-core`](lib/core/) | Platform-agnostic bootloader core (protocol dispatcher, boot state machine, app validation) | -| [`tinyboot-protocol`](lib/protocol/) | Wire protocol (frame format, CRC16, commands) | -| [`tinyboot-ch32`](ch32/) | CH32 HAL and tinyboot platform — use `boot` for bootloader binaries, `app` for application binaries | -| [`tinyboot-ch32-rt`](ch32/rt/) | Minimal CH32 runtime for bootloader binaries that can't afford full `qingke-rt` | -| [`tinyboot`](cli/) | CLI firmware flasher over UART | +| Crate | Description | +| ------------------------------------ | --------------------------------------------------------------------------------------------------- | +| [`tinyboot-core`](lib/core/) | Platform-agnostic bootloader core (protocol dispatcher, boot state machine, app validation) | +| [`tinyboot-protocol`](lib/protocol/) | Wire protocol (frame format, CRC16, commands) | +| [`tinyboot-ch32`](ch32/) | CH32 HAL and tinyboot platform — use `boot` for bootloader binaries, `app` for application binaries | +| [`tinyboot-ch32-rt`](ch32/rt/) | Minimal CH32 runtime for bootloader binaries that can't afford full `qingke-rt` | +| [`tinyboot`](cli/) | CLI firmware flasher over UART | ## Rust Version @@ -204,5 +205,3 @@ Please [open an issue](https://github.com/OpenServoCore/tinyboot/issues) before ## License Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option. - -[ch32-data-29]: https://github.com/ch32-rs/ch32-data/pull/29 diff --git a/ch32/README.md b/ch32/README.md index d9d7298..376f3e7 100644 --- a/ch32/README.md +++ b/ch32/README.md @@ -2,16 +2,16 @@ Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — see the main README to get started. -CH32 HAL and tinyboot platform for CH32V003 and CH32V103. Exposes a bootloader-side entry point ([`boot`]) and an app-side client ([`app`]) built on a small in-crate HAL ([`hal`]). +CH32 HAL and tinyboot platform for CH32V003, CH32V00x (V002/V004/V005/V006/V007), and CH32V103. Exposes a bootloader-side entry point ([`boot`]) and an app-side client ([`app`]) built on a small in-crate HAL ([`hal`]). ## Modules -| Module | For | What it provides | -| ---------- | ----------------------- | --------------------------------------------------------------------------------------------------- | -| `boot` | Bootloader binaries | `run()`, `BootCtl`, USART transport (`Usart`, `UsartConfig`, `BaudRate`, `Duplex`, `TxEnConfig`) | -| `app` | Application binaries | `new_app()`, `App`, `BootCtl`, the `tinyboot_core::app` types | -| `hal` | Both | `flash`, `gpio`, `usart`, `afio`, `rcc`, `pfic`, `iwdg`; auto-generated `Pin` and `UsartMapping` | -| `platform` | (internal) | `tinyboot_core::traits` impls for Storage, Transport, BootCtl, BootMetaStore | +| Module | For | What it provides | +| ---------- | -------------------- | ------------------------------------------------------------------------------------------------ | +| `boot` | Bootloader binaries | `run()`, `BootCtl`, USART transport (`Usart`, `UsartConfig`, `BaudRate`, `Duplex`, `TxEnConfig`) | +| `app` | Application binaries | `new_app()`, `App`, `BootCtl`, the `tinyboot_core::app` types | +| `hal` | Both | `flash`, `gpio`, `usart`, `afio`, `rcc`, `pfic`, `iwdg`; auto-generated `Pin` and `UsartMapping` | +| `platform` | (internal) | `tinyboot_core::traits` impls for Storage, Transport, BootCtl, BootMetaStore | ## Bootloader example @@ -79,13 +79,14 @@ For CH32V103 `system-flash` apps, pass the same `BootCtl::new(pin, level, delay) ## Features -| Feature | Description | -| --------------------------------------------------- | --------------------------------------------------------- | -| `ch32v003f4p6` / `a4m6` / `f4u6` / `j4m6` | CH32V003 chip variants | -| `ch32v103c6t6` / `c8t6` / `c8u6` / `r8t6` | CH32V103 chip variants | -| `system-flash` | Build for the system-flash bootloader region | +| Feature | Description | +| ------------------------------------------------------------------ | -------------------------------------------- | +| `ch32v003f4p6` / `a4m6` / `f4u6` / `j4m6` | CH32V003 chip variants | +| `ch32v002x4x6` / `v004x6x1` / `v005x6x6` / `v006x8x6` / `v007x8x6` | CH32V00x chip variants | +| `ch32v103c6t6` / `c8t6` / `c8u6` / `r8t6` | CH32V103 chip variants | +| `system-flash` | Build for the system-flash bootloader region | -Complete boot + app examples live in [`examples/ch32/v003`](../examples/ch32/v003/) and [`examples/ch32/v103`](../examples/ch32/v103/). +Complete boot + app examples live in [`examples/ch32/v003`](../examples/ch32/v003/), [`examples/ch32/v00x`](../examples/ch32/v00x/), and [`examples/ch32/v103`](../examples/ch32/v103/). ## Linker scripts diff --git a/lib/protocol/README.md b/lib/protocol/README.md index dcc385c..8f91657 100644 --- a/lib/protocol/README.md +++ b/lib/protocol/README.md @@ -11,23 +11,24 @@ A single `Frame` struct is used for both requests (host to device) and responses ``` 0 1 2 3 4 5 6 7 8 9 10 10+len 10+len+2 +-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+- - - -+-------+-------+ - | SYNC0 | SYNC1 | CMD |STATUS | ADDR (u32 LE) | LEN_LO LEN_HI | DATA... | CRC_LO CRC_HI | - | 0xAA | 0x55 | | | | | | | + | SYNC0 | SYNC1 | CMD |STATUS | ADDR (u24 LE) | FLAGS | LEN_LO LEN_HI | DATA... | CRC_LO CRC_HI | + | 0xAA | 0x55 | | | | | | | | +-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+- - - -+-------+-------+ |<--------------------- header (10 bytes) --------------------->|<- payload ->|<--- CRC --->| ``` Total frame size = 12 bytes overhead + payload. Maximum payload is 64 bytes (`MAX_PAYLOAD`). -| Field | Size | Description | -| ------ | ------- | ------------------------------------------------------------- | -| SYNC | 2 bytes | Preamble `0xAA 0x55` for frame synchronization | -| CMD | 1 byte | Command code | -| STATUS | 1 byte | `Request (0x00)` for requests, result status for responses | -| ADDR | 4 bytes | Flash address (u32 LE). Echoed in responses | -| LEN | 2 bytes | Data payload length (u16 LE, 0..64) | -| DATA | 0..64 | Payload bytes | -| CRC | 2 bytes | CRC16-CCITT (LE) over SYNC + CMD + STATUS + ADDR + LEN + DATA | +| Field | Size | Description | +| ------ | ------- | --------------------------------------------------------------------- | +| SYNC | 2 bytes | Preamble `0xAA 0x55` for frame synchronization | +| CMD | 1 byte | Command code | +| STATUS | 1 byte | `Request (0x00)` for requests, result status for responses | +| ADDR | 3 bytes | Flash address (u24 LE). Echoed in responses | +| FLAGS | 1 byte | Per-command flags (see below). Occupies addr byte 3 | +| LEN | 2 bytes | Data payload length (u16 LE, 0..64) | +| DATA | 0..64 | Payload bytes | +| CRC | 2 bytes | CRC16-CCITT (LE) over SYNC + CMD + STATUS + ADDR + FLAGS + LEN + DATA | ## Commands @@ -35,10 +36,27 @@ Total frame size = 12 bytes overhead + payload. Maximum payload is 64 bytes (`MA | ---- | ------ | -------------- | -------------------------------------------------------------------------- | | 0x00 | Info | Host to Device | Query device info (capacity, erase size, versions, mode) | | 0x01 | Erase | Host to Device | Erase `byte_count` bytes at addr (first erase transitions Idle → Updating) | -| 0x02 | Write | Host to Device | Write data at address | +| 0x02 | Write | Host to Device | Write data at address. `WriteFlags::FLUSH` commits trailing partial page | | 0x03 | Verify | Host to Device | Compute CRC16 over `addr` bytes of app, store checksum + state in metadata | -| 0x04 | Reset | Host to Device | Reset the device | -| 0x05 | Flush | Host to Device | Flush buffered writes to storage | +| 0x04 | Reset | Host to Device | Reset the device. `ResetFlags::BOOTLOADER` enters bootloader (service) | + +## Per-command flags + +Byte 3 of the address field carries per-command flags. Each command defines its own bitflags type; unused bits are reserved and must be zero. + +### `WriteFlags` (Cmd::Write) + +| Bit | Flag | Description | +| --- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 7 | `FLUSH` | Commit the buffered page after this write and reset write state. Required on the last write of each contiguous region (before an address jump or at end of transfer). | + +### `ResetFlags` (Cmd::Reset) + +| Bit | Flag | Description | +| --- | ------------ | ----------------------------------------------- | +| 0 | `BOOTLOADER` | Enter the bootloader (service mode) after reset | + +With no flag set, `Cmd::Reset` boots the app (hand-off mode). ### Info response @@ -64,12 +82,12 @@ Request payload (2 bytes via `EraseData`): | ------ | ------- | ---------- | --------------------------------- | | 0 | 2 bytes | byte_count | Number of bytes to erase (u16 LE) | -### Flush +### Flushing buffered writes -The host must send a Flush command to commit any partial page still buffered on the device. Flush is required: +Writes are accumulated in a ring buffer on the device and flushed a page at a time. The host sets `WriteFlags::FLUSH` on the Write that ends a contiguous region to commit the trailing partial page. Flush is required: -- **After the final Write** — otherwise the last partial page may not be written to flash, causing Verify to fail. -- **Before skipping an address range** — if the host advances the write address (e.g. skipping a gap between segments), it must Flush first to commit the buffered data at the previous address. +- **On the final Write** — otherwise the last partial page may not be written to flash, causing Verify to fail. +- **Before skipping an address range** — if the host advances the write address (e.g. skipping a gap between segments), it must flush the previous region first. ### Write alignment @@ -79,7 +97,7 @@ Write payloads must be padded to a 4-byte boundary. The device writes to flash 4 The `addr` field carries the application size in bytes. The device computes CRC16 over the first `addr` bytes of flash (the actual firmware, not the full region), stores the checksum and app size in boot metadata, and transitions to Validating state. -If Verify returns `CrcMismatch`, check that all Write payloads were padded to 4 bytes and that Flush was sent after the last Write (and before any address skips). +If Verify returns `CrcMismatch`, check that all Write payloads were padded to 4 bytes and that `WriteFlags::FLUSH` was set on the last Write (and on the last Write of each contiguous region before any address skip). Response returns 2 bytes via the `VerifyData` struct: @@ -141,8 +159,7 @@ Device -> Ok Host -> Write addr=0x0040 data=[64 bytes] Device -> Ok ... - -Host -> Flush +Host -> Write addr=0x13C0 data=[54 bytes] flags=WriteFlags::FLUSH Device -> Ok Host -> Verify addr=5110 (app_size) diff --git a/vendor/README.md b/vendor/README.md index 1c77fdd..242634c 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -5,18 +5,18 @@ overwritten system flash and need to recover. ## Files -| 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 | +| File | Chip | Address | Size | +| ----------------------------- | -------- | ------------ | ---------- | +| `ch32v003-system-flash.bin` | CH32V003 | `0x1FFFF000` | 1920 bytes | +| `ch32v006-system-flash.bin` | CH32V006 | `0x1FFF0000` | 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 +wlink flash vendor/ch32v006-system-flash.bin --address 0x1FFF0000 # CH32V103 has split system flash regions, so we need to flash each region separately. wlink flash vendor/ch32v103-system-flash-1.bin --address 0x1FFFF000 From 9608d18bc9373c60d4168216226641e9ccf5eadf Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Sun, 19 Apr 2026 23:42:50 -0700 Subject: [PATCH 15/15] Update CHANGELOG for CH32V00x support and protocol changes --- CHANGELOG.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 793b4ec..3e22abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,20 +2,31 @@ ## [Unreleased] +### Added + +- **CH32V00x support** — full bootloader and app support for CH32V002/V004/V005/V006/V007 (system flash at `0x1FFF0000`, 3KB + 256B). +- `tinyboot-ch32-rt` crate: minimal `_start` + `link.x` for bootloader binaries that can't afford full `qingke-rt`. +- V103 split BOOT/BOOT2 system-flash regions: UART transport placed in `.text2` (second region) so all features fit. +- `tx_en` support in the CH32V00x example app for RS-485 / DXL TTL loopback. + ### Changed -- Switched `ch32-metapac` to git in preparation for V00x support. +- **Breaking:** protocol address reduced to 24 bits; addr byte 3 is now a per-command `Flags` byte. `WriteFlags::FLUSH` replaces the standalone `Cmd::Flush`; `ResetFlags::BOOTLOADER` replaces `addr=1` signaling on Reset. +- **Breaking:** `Frame::addr: u32` split into `addr_lo: u16` + `addr_hi: u8` + `flags: Flags` union; use `Frame::addr()` / `Frame::set_addr()` accessors. - **Breaking:** merged `tinyboot-ch32-hal`, `tinyboot-ch32-boot`, and `tinyboot-ch32-app` into a single `tinyboot-ch32` crate (`::boot`, `::app`, `::hal` modules). - **Breaking:** replaced `BootMode` with `RunMode { HandOff, Service }`, and reshaped `BootCtl` around `run_mode()`/`set_run_mode()`, `reset()`, and `hand_off()`. - **Breaking:** `tinyboot_core::traits` flattened; `BootClient` gone, app behaviour moved to `tinyboot_core::app::App`. - **Breaking:** V103 + `system-flash` `BootCtl::new` now takes `(Pin, Level, u32)` for the external BOOT0 circuit; other combinations stay unit-arg. - **Breaking:** removed `Storage::unlock()` — flash lock/unlock is now scoped per operation inside the HAL. +- Switched `ch32-metapac` to git for V00x support. - Run-mode persistence split into variant-specific backends (`BOOT_MODE` register on V003 system-flash, RAM magic word elsewhere, placed at the last 4 bytes of RAM to avoid colliding with `qingke-rt`'s highcode-init flag). - Non-HAL `bool` parameters converted to enums (`Duplex`, `Level`, `Pull`, `RunMode`, `BootSrc`). +- Dispatcher refactored for readability and reduced size (eliminated `.rodata` jump table; shared `frame.send` path). -### Added +### Fixed -- `tinyboot-ch32-rt` crate: minimal `_start` + `link.x` for bootloader binaries that can't afford full `qingke-rt`. +- Dispatcher now flushes the transport after send — required for RS-485 / DXL TTL half-duplex. +- Ring buffer properly reset after flush. ## [0.3.0] - 2026-04-15