diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e664f..e1da5df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4. ## [Unreleased] +## [tinyboot-core 0.6.1] - 2026-10-20 + +### Added + +- `App::reset(mode, baud)` resets into the bootloader or the app, storing the bootloader's baud first when given. An app with its own protocol uses it to start a firmware update. + +### Changed + +- The app's `Reset` command goes through `App::reset`, so it skips the metadata write when the baud is already stored. + ## [tinyboot 0.5.1] - 2026-10-20 ### Fixed diff --git a/ch32/Cargo.toml b/ch32/Cargo.toml index 91202d5..8664ee1 100644 --- a/ch32/Cargo.toml +++ b/ch32/Cargo.toml @@ -52,7 +52,7 @@ repository = "https://github.com/OpenServoCore/tinyboot" license = "MIT OR Apache-2.0" [workspace.dependencies] -tinyboot-core = { version = "0.6.0", path = "../lib/core", default-features = false } +tinyboot-core = { version = "0.6.1", path = "../lib/core", default-features = false } tinyboot-ch32 = { version = "0.7.0", path = ".", default-features = false } embedded-hal = "1" embedded-io = "0.7" diff --git a/ch32/README.md b/ch32/README.md index 7960e1c..36ca24a 100644 --- a/ch32/README.md +++ b/ch32/README.md @@ -67,6 +67,8 @@ loop { `app::poll` handles `Info` (responds with capacity, erase size, versions, `mode = 1`, the stored boot baud and the bootloader's compatibility key) and `Reset` (resets the device; `ResetFlags::BOOTLOADER` reboots into the bootloader, otherwise into the app). All other commands return `Status::Unsupported`. On a shared bus, `app::new_addressable_app(ctl, wait_bytes)` also handles `Select` and stays silent until selected. +An app with its own protocol, instead of `poll`, calls `app.reset(RunMode::Service, Some(baud))` to send the device to its bootloader at `baud`. + See the [app integration guide](https://openservocore.github.io/tinyboot/app-integration.html) for a complete app including `embedded_io` transport wrapping and peripheral ownership. ## Features diff --git a/docs/app-integration.md b/docs/app-integration.md index b631ee1..49f13f5 100644 --- a/docs/app-integration.md +++ b/docs/app-integration.md @@ -82,6 +82,19 @@ uart_config.baudrate = app.baud().map_or(115200, |b| b.hz()); `set_baud()` erases and rewrites the metadata page (skipped when the rate is already stored), in a critical section like `confirm()`. +## `reset()` - into the bootloader from your own protocol + +An app that speaks its own protocol on the bus, instead of running `poll()`, can still send the device to its bootloader when the host asks. `app.reset(mode, baud)` does what the `Reset` command does. It stores `baud` when given, as `set_baud()` does, then sets the run mode and resets. + +```rust +use tinyboot_ch32::app::{BaudRate, traits::RunMode}; + +// The host asked for a firmware update at 1 Mbaud. +app.reset(RunMode::Service, Some(BaudRate::B1000000)); +``` + +`RunMode::Service` stays in the bootloader, and `RunMode::HandOff` boots the app again. The baud goes in before the run mode on purpose. On CH32 a metadata write rewrites the flash status register, which would drop a boot mode set before it. + ## Compatibility key - only the right image installs `app_version!()` and `boot_version!()` end each image with a 6-byte tail: a 32-bit compatibility key, then the version. Give the bootloader a key and it accepts only images that carry the same one: diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 6678630..76047ae 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tinyboot-core" description = "Core bootloader library for tinyboot" -version = "0.6.0" +version = "0.6.1" edition.workspace = true repository.workspace = true license.workspace = true diff --git a/lib/core/src/app.rs b/lib/core/src/app.rs index fef9679..01931da 100644 --- a/lib/core/src/app.rs +++ b/lib/core/src/app.rs @@ -86,9 +86,30 @@ impl App) -> ! { + self.reset_with(mode, baud.map(|b| b as u8)) + } + + /// The baud before the run mode: a CH32 META write rewrites the flash + /// status register whole, which would drop a boot mode set before it. + fn reset_with(&mut self, mode: RunMode, baud: Option) -> ! { + if let Some(baud) = baud { + self.store_baud(baud); + } + self.ctl.set_run_mode(mode); + self.ctl.reset() + } + + fn store_baud(&mut self, baud: u8) { critical_section::with(|_| { - if self.meta.baud() != baud as u8 { - let _ = self.meta.set_baud(baud as u8); + if self.meta.baud() != baud { + let _ = self.meta.set_baud(baud); } }); } @@ -161,13 +182,15 @@ impl App { let flags = unsafe { self.frame.flags.reset }; - if flags.contains(ResetFlags::BAUD) { - let _ = self.meta.set_baud(self.frame.addr() as u8); - } - if flags.contains(ResetFlags::BOOTLOADER) { - self.ctl.set_run_mode(RunMode::Service); - } - self.ctl.reset(); + let baud = flags + .contains(ResetFlags::BAUD) + .then(|| self.frame.addr() as u8); + let mode = if flags.contains(ResetFlags::BOOTLOADER) { + RunMode::Service + } else { + RunMode::HandOff + }; + self.reset_with(mode, baud); } _ => { self.frame.len = 0; diff --git a/lib/core/tests/common/mod.rs b/lib/core/tests/common/mod.rs index 713c0ba..0785030 100644 --- a/lib/core/tests/common/mod.rs +++ b/lib/core/tests/common/mod.rs @@ -11,6 +11,20 @@ use tinyboot_protocol::{Cmd, Status, WriteFlags}; // -- Mocks -- +std::thread_local! { + /// META baud writes, run-mode sets and resets on this thread, in order. + pub static EVENTS: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; +} + +fn event(e: String) { + EVENTS.with(|v| v.borrow_mut().push(e)); +} + +/// Drain this thread's [`EVENTS`]. +pub fn take_events() -> Vec { + EVENTS.with(|v| v.take()) +} + pub struct MockTransport { pub rx_buf: [u8; 512], pub rx_len: usize, @@ -166,8 +180,11 @@ impl BootCtl for MockBootCtl { fn run_mode(&self) -> RunMode { RunMode::HandOff } - fn set_run_mode(&mut self, _mode: RunMode) {} + fn set_run_mode(&mut self, mode: RunMode) { + event(format!("{mode:?}")); + } fn reset(&mut self) -> ! { + event("reset".into()); panic!("mock reset") } fn hand_off(&mut self) -> ! { @@ -217,6 +234,7 @@ impl BootMetaStore for MockBootMeta { fn set_baud(&mut self, baud: u8) -> Result<(), ()> { self.baud = baud; self.baud_writes += 1; + event(format!("baud {baud}")); Ok(()) } fn advance(&mut self) -> Result { diff --git a/lib/core/tests/state_transitions.rs b/lib/core/tests/state_transitions.rs index 2b8ce37..91ec66a 100644 --- a/lib/core/tests/state_transitions.rs +++ b/lib/core/tests/state_transitions.rs @@ -8,7 +8,7 @@ mod common; use common::*; use tinyboot_core::app::{App, AppConfig}; use tinyboot_core::protocol::Dispatcher; -use tinyboot_core::traits::BootState; +use tinyboot_core::traits::{BootState, RunMode}; use tinyboot_protocol::crc::{CRC_INIT, crc16}; use tinyboot_protocol::frame::DEFAULT_PAYLOAD; use tinyboot_protocol::frame::Flags; @@ -479,6 +479,59 @@ fn app_reset_with_baud_flag_stores_baud() { assert_eq!(a.baud(), Some(BaudRate::B2000000)); } +fn reset_events( + a: &mut App, + mode: RunMode, + baud: Option, +) -> Vec { + take_events(); + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| a.reset(mode, baud))); + assert!(r.is_err(), "reset must not return"); + take_events() +} + +#[test] +fn app_reset_stores_the_baud_before_the_run_mode() { + let mut a = app(); + let ev = reset_events(&mut a, RunMode::Service, Some(BaudRate::B1000000)); + assert_eq!(ev, ["baud 9", "Service", "reset"]); +} + +#[test] +fn app_reset_skips_an_unchanged_baud() { + let mut a = app(); + a.set_baud(BaudRate::B1000000); + let ev = reset_events(&mut a, RunMode::Service, Some(BaudRate::B1000000)); + assert_eq!(ev, ["Service", "reset"]); +} + +#[test] +fn app_reset_to_the_app_leaves_meta_alone() { + let mut a = app(); + assert_eq!( + reset_events(&mut a, RunMode::HandOff, None), + ["HandOff", "reset"] + ); +} + +#[test] +fn app_reset_command_takes_the_same_order() { + let mut a = app(); + let mut rx = MockTransport::new(); + rx.load_request_flags( + Cmd::Reset, + BaudRate::B500000 as u32, + reset_flags(ResetFlags::BAUD | ResetFlags::BOOTLOADER), + 0, + &[], + ); + let mut tx = MockTransport::new(); + take_events(); + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| a.poll(&mut rx, &mut tx))); + assert!(r.is_err(), "reset must not return"); + assert_eq!(take_events(), ["baud 7", "Service", "reset"]); +} + // ============================================================================= // Multi-step sequences // =============================================================================