From 26f880d16e515fe0d83bba34ee1f5001a67ed39d Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Mon, 23 Mar 2026 19:37:08 -0700 Subject: [PATCH 01/12] add simple RingBuf data structure. --- tinyboot/src/lib.rs | 2 + tinyboot/src/ringbuf.rs | 139 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 tinyboot/src/ringbuf.rs diff --git a/tinyboot/src/lib.rs b/tinyboot/src/lib.rs index a0c077c..ec3531b 100644 --- a/tinyboot/src/lib.rs +++ b/tinyboot/src/lib.rs @@ -15,6 +15,8 @@ pub mod app; pub mod core; /// Protocol frame dispatcher. pub mod protocol; +/// Fixed-size ring buffer for buffered flash writes. +pub mod ringbuf; /// Platform abstraction traits. pub mod traits; diff --git a/tinyboot/src/ringbuf.rs b/tinyboot/src/ringbuf.rs new file mode 100644 index 0000000..7722993 --- /dev/null +++ b/tinyboot/src/ringbuf.rs @@ -0,0 +1,139 @@ +/// Fixed-size ring buffer. +/// +/// Designed for use with `PageWriter` where `N = 2 * PAGE_SIZE`. +/// When consumed in PAGE_SIZE-aligned chunks, `peek` always returns +/// a contiguous slice (no wrap on the read side). +pub struct RingBuf { + buf: core::mem::MaybeUninit<[u8; N]>, + head: usize, + tail: usize, +} + +impl Default for RingBuf { + fn default() -> Self { + Self { + buf: core::mem::MaybeUninit::uninit(), + head: 0, + tail: 0, + } + } +} + +impl RingBuf { + /// Bytes available to read. + #[inline(always)] + pub fn len(&self) -> usize { + self.head.wrapping_sub(self.tail) % N + } + + /// Returns true if the buffer is empty. + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.head == self.tail + } + + /// Push bytes into the buffer, handling wrap-around. + /// Returns the number of bytes actually written (short if full). + pub fn push(&mut self, data: &[u8]) -> usize { + let free = N - 1 - self.len(); + let n = data.len().min(free); + + let ptr = self.buf.as_mut_ptr() as *mut u8; + let mut head = self.head; + for i in 0..n { + // SAFETY: i < n <= data.len(), head < N + unsafe { *ptr.add(head) = *data.get_unchecked(i) }; + head += 1; + if head == N { + head = 0; + } + } + self.head = head; + n + } + + /// Borrow up to `n` contiguous readable bytes without consuming. + /// + /// Returns fewer than `n` bytes only if fewer are available. + /// When `N = 2 * PAGE_SIZE` and you consume in PAGE_SIZE chunks, + /// this always returns a contiguous slice (read side never wraps mid-page). + pub fn peek(&self, n: usize) -> &[u8] { + let available = self.len().min(n); + // SAFETY: tail + available <= N, guaranteed by len() and consume() modulo N + unsafe { + core::slice::from_raw_parts(self.buf.as_ptr().cast::().add(self.tail), available) + } + } + + /// Advance the read pointer by `n` bytes. + pub fn consume(&mut self, n: usize) { + self.tail = (self.tail + n) % N; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty() { + let buf = RingBuf::<8>::default(); + assert_eq!(buf.len(), 0); + assert_eq!(buf.peek(8), &[]); + } + + #[test] + fn push_and_peek() { + let mut buf = RingBuf::<8>::default(); + assert_eq!(buf.push(&[1, 2, 3]), 3); + assert_eq!(buf.len(), 3); + assert_eq!(buf.peek(3), &[1, 2, 3]); + } + + #[test] + fn consume_advances() { + let mut buf = RingBuf::<8>::default(); + buf.push(&[1, 2, 3, 4]); + buf.consume(2); + assert_eq!(buf.len(), 2); + assert_eq!(buf.peek(2), &[3, 4]); + } + + #[test] + fn push_wraps() { + let mut buf = RingBuf::<8>::default(); + buf.push(&[1, 2, 3, 4, 5]); + buf.consume(4); // tail=4, head=5 + assert_eq!(buf.push(&[6, 7, 8, 9]), 4); // wraps: 6,7,8 at end, 9 at start + assert_eq!(buf.len(), 5); + assert_eq!(buf.peek(1), &[5]); + } + + #[test] + fn full_returns_short() { + let mut buf = RingBuf::<4>::default(); + assert_eq!(buf.push(&[1, 2, 3]), 3); // capacity is N-1 = 3 + assert_eq!(buf.push(&[4]), 0); // full + assert_eq!(buf.len(), 3); + } + + #[test] + fn page_aligned_peek_is_contiguous() { + // Simulates PageWriter pattern: N=8, PAGE_SIZE=4 + let mut buf = RingBuf::<8>::default(); + + // Fill first page + buf.push(&[1, 2, 3, 4]); + assert_eq!(buf.peek(4), &[1, 2, 3, 4]); + buf.consume(4); + + // Fill second page + buf.push(&[5, 6, 7, 8]); + assert_eq!(buf.peek(4), &[5, 6, 7, 8]); + buf.consume(4); + + // Back to first page slot + buf.push(&[9, 10, 11, 12]); + assert_eq!(buf.peek(4), &[9, 10, 11, 12]); + } +} From 43400acc311044f1dd5174048087e7d378cfc7c0 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 24 Mar 2026 14:04:46 -0700 Subject: [PATCH 02/12] refactor flash v0 to add fast write primitives as well as renaming constructors to clearify intentions. --- tinyboot-ch32-app/src/lib.rs | 2 +- tinyboot-ch32-boot/src/platform/boot_state.rs | 4 +- tinyboot-ch32-boot/src/platform/storage.rs | 4 +- tinyboot-ch32-hal/src/flash/v0.rs | 83 ++++++++++++------- 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/tinyboot-ch32-app/src/lib.rs b/tinyboot-ch32-app/src/lib.rs index e17ac83..1a70b57 100644 --- a/tinyboot-ch32-app/src/lib.rs +++ b/tinyboot-ch32-app/src/lib.rs @@ -86,7 +86,7 @@ impl TBBootClient for Ch32BootClient { buf[8..16].copy_from_slice(&meta); flash::unlock(); iwdg::feed(); - let w = flash::FlashWriter::ob(); + let w = flash::FlashWriter::opt(); w.erase_start(); w.erase(flash::OB_BASE); w.operation_end(); diff --git a/tinyboot-ch32-boot/src/platform/boot_state.rs b/tinyboot-ch32-boot/src/platform/boot_state.rs index b14c907..0a43d56 100644 --- a/tinyboot-ch32-boot/src/platform/boot_state.rs +++ b/tinyboot-ch32-boot/src/platform/boot_state.rs @@ -59,7 +59,7 @@ impl BootMetaStore { } let buf = unsafe { &*(buf.as_ptr() as *const [u8; 16]) }; - let w = FlashWriter::ob(); + let w = FlashWriter::opt(); w.erase_start(); w.erase(OB_BASE); w.operation_end(); @@ -80,7 +80,7 @@ impl BootMetaStore { return None; } let next = current & (current >> 1); - let w = FlashWriter::ob(); + let w = FlashWriter::opt(); w.write_start(); w.write(META_OB_BASE + offset as u32 * 2, next as u16); w.operation_end(); diff --git a/tinyboot-ch32-boot/src/platform/storage.rs b/tinyboot-ch32-boot/src/platform/storage.rs index ac291c2..3abfefc 100644 --- a/tinyboot-ch32-boot/src/platform/storage.rs +++ b/tinyboot-ch32-boot/src/platform/storage.rs @@ -71,7 +71,7 @@ impl NorFlash for Storage { if to as usize > self.app_size { return Err(StorageError::OutOfBounds); } - let writer = FlashWriter::standard(); + let writer = FlashWriter::usr(); writer.erase_start(); let mut addr = self.app_base + from; let end = self.app_base + to; @@ -100,7 +100,7 @@ impl NorFlash for Storage { if offset as usize + bytes.len() > self.app_size { return Err(StorageError::OutOfBounds); } - let writer = FlashWriter::standard(); + let writer = FlashWriter::usr(); writer.write_start(); let mut addr = self.app_base + offset; for pair in bytes.chunks_exact(2) { diff --git a/tinyboot-ch32-hal/src/flash/v0.rs b/tinyboot-ch32-hal/src/flash/v0.rs index 1de237e..e905d12 100644 --- a/tinyboot-ch32-hal/src/flash/v0.rs +++ b/tinyboot-ch32-hal/src/flash/v0.rs @@ -1,28 +1,25 @@ const KEY1: u32 = 0x4567_0123; const KEY2: u32 = 0xCDEF_89AB; - -fn flash() -> ch32_metapac::flash::Flash { - ch32_metapac::FLASH -} +const FLASH: ch32_metapac::flash::Flash = ch32_metapac::FLASH; #[inline(always)] fn wait_busy() { - while flash().statr().read().bsy() {} + while FLASH.statr().read().bsy() {} } /// Unlock flash controller for all operations (KEYR + MODEKEYR + OBKEYR). pub fn unlock() { - flash().keyr().write(|w| w.set_keyr(KEY1)); - flash().keyr().write(|w| w.set_keyr(KEY2)); - flash().modekeyr().write(|w| w.set_modekeyr(KEY1)); - flash().modekeyr().write(|w| w.set_modekeyr(KEY2)); - flash().obkeyr().write(|w| w.set_optkey(KEY1)); - flash().obkeyr().write(|w| w.set_optkey(KEY2)); + 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)); + FLASH.obkeyr().write(|w| w.set_optkey(KEY1)); + FLASH.obkeyr().write(|w| w.set_optkey(KEY2)); } /// Lock flash controller. pub fn lock() { - flash().ctlr().write(|w| { + FLASH.ctlr().write(|w| { w.set_lock(true); w.set_flock(true); }); @@ -35,8 +32,10 @@ pub struct FlashWriter { write_bit: u8, } -const PG: u8 = 0; // PG bit - standard programming - 2b +const FTPG: u8 = 16; // FTPG bit - fast page programming - 64B const FTER: u8 = 17; // FTER bit - fast page erase - 64B +const BUFRST: u8 = 18; // BUFRST bit - fast-program buffer reset +const BUFLOAD: u8 = 19; // BUFLOAD bit - fast-program buffer load const OBPG: u8 = 4; // OBPG bit - option byte programming const OBER: u8 = 5; // OBER bit - option byte erase @@ -45,16 +44,16 @@ const OBWRE: u8 = 9; // OBWRE bit - option byte write enabled. This needs to be const STRT: u8 = 6; // STRT bit - start operation impl FlashWriter { - /// Writer for regular operations (PG / FTER). - pub const fn standard() -> Self { + /// Writer for user flash (FTPG / FTER). + pub const fn usr() -> Self { Self { erase_bit: FTER, - write_bit: PG, + write_bit: FTPG, } } /// Writer for option bytes (OBPG / OBER). - pub const fn ob() -> Self { + pub const fn opt() -> Self { Self { erase_bit: OBER, write_bit: OBPG, @@ -63,13 +62,13 @@ impl FlashWriter { /// check for write protection error pub fn check_wrprterr(&self) -> bool { - let statr = flash().statr().read(); + let statr = FLASH.statr().read(); if statr.wrprterr() { - flash().statr().write(|w| w.set_wrprterr(true)); + FLASH.statr().write(|w| w.set_wrprterr(true)); return true; } if statr.eop() { - flash().statr().write(|w| w.set_eop(true)); + FLASH.statr().write(|w| w.set_eop(true)); } false } @@ -79,10 +78,11 @@ impl FlashWriter { pub fn write_start(&self) { let write_bit = self.write_bit as usize; let ctlr_start = (1 << OBWRE) | (1 << write_bit); - flash().ctlr().write(|w| w.0 = ctlr_start); + FLASH.ctlr().write(|w| w.0 = ctlr_start); } - /// Halfword (2-byte) write. + /// Halfword (2-byte) write. used only for OB writes. + /// Use `fast_write_*` for flash writes. #[inline(always)] pub fn write(&self, addr: u32, value: u16) { unsafe { core::ptr::write_volatile(addr as *mut u16, value) }; @@ -94,7 +94,7 @@ impl FlashWriter { pub fn erase_start(&self) { let erase_bit = self.erase_bit as usize; let ctlr_start = (1 << OBWRE) | (1 << erase_bit); - flash().ctlr().write(|w| w.0 = ctlr_start); + FLASH.ctlr().write(|w| w.0 = ctlr_start); } /// Erase (64-byte page for flash, full OB erase for option bytes). @@ -102,8 +102,8 @@ impl FlashWriter { let erase_bit = self.erase_bit as usize; let ctlr = (1 << OBWRE) | (1 << erase_bit) | (1 << STRT); - flash().addr().write(|w| w.set_addr(addr)); - flash().ctlr().write(|w| w.0 = ctlr); + FLASH.addr().write(|w| w.set_addr(addr)); + FLASH.ctlr().write(|w| w.0 = ctlr); wait_busy(); } @@ -111,16 +111,39 @@ impl FlashWriter { #[inline(always)] pub fn operation_end(&self) { let ctlr_end = 1 << OBWRE; // preserve OB write enable bit - flash().ctlr().write(|w| w.0 = ctlr_end); + FLASH.ctlr().write(|w| w.0 = ctlr_end); + } + + /// Clear the internal 64-byte fast-programming buffer (BUFRST). + pub fn fast_write_buf_reset(&self) { + let ctlr = (1 << OBWRE) | (1 << FTPG) | (1 << BUFRST); + FLASH.ctlr().write(|w| w.0 = ctlr); + wait_busy(); + } + + /// Load 4 bytes into the fast-programming buffer at `addr`. + pub fn fast_write_buf_load(&self, addr: u32, value: u32) { + unsafe { core::ptr::write_volatile(addr as *mut u32, value) }; + let ctlr = (1 << OBWRE) | (1 << FTPG) | (1 << BUFLOAD); + FLASH.ctlr().write(|w| w.0 = ctlr); + wait_busy(); + } + + /// Program the buffered page to flash at `page_addr`. + pub fn fast_write_page_program(&self, page_addr: u32) { + FLASH.addr().write(|w| w.set_addr(page_addr)); + let ctlr = (1 << OBWRE) | (1 << FTPG) | (1 << STRT); + FLASH.ctlr().write(|w| w.0 = ctlr); + wait_busy(); } } pub fn is_boot_mode() -> bool { - flash().statr().read().boot_mode() + 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)); + 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 7b416e0196e422cfda8b8feef53abfdca2699c79 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 24 Mar 2026 14:09:09 -0700 Subject: [PATCH 03/12] define PAGE_SIZE in individual flash hal implementation and use that as ERASE_SIZE and WRITE_SIZE in flash trait. --- tinyboot-ch32-boot/src/platform/storage.rs | 19 ++++++------------- tinyboot-ch32-hal/src/flash/v0.rs | 3 +++ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/tinyboot-ch32-boot/src/platform/storage.rs b/tinyboot-ch32-boot/src/platform/storage.rs index 3abfefc..d782c48 100644 --- a/tinyboot-ch32-boot/src/platform/storage.rs +++ b/tinyboot-ch32-boot/src/platform/storage.rs @@ -3,10 +3,7 @@ use embedded_storage::nor_flash::{ }; use tinyboot::traits::boot::Storage as StorageTrait; -use tinyboot_ch32_hal::flash::FlashWriter; - -const FLASH_WRITE_SIZE: usize = 2; -const FLASH_ERASE_SIZE: usize = 64; +use tinyboot_ch32_hal::flash::{FlashWriter, PAGE_SIZE}; /// Flash storage configuration. pub struct StorageConfig { @@ -59,13 +56,11 @@ impl ErrorType for Storage { } impl NorFlash for Storage { - const WRITE_SIZE: usize = FLASH_WRITE_SIZE; - const ERASE_SIZE: usize = FLASH_ERASE_SIZE; + const WRITE_SIZE: usize = PAGE_SIZE; + const ERASE_SIZE: usize = PAGE_SIZE; fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { - if !(from as usize).is_multiple_of(FLASH_ERASE_SIZE) - || !(to as usize).is_multiple_of(FLASH_ERASE_SIZE) - { + if !(from as usize).is_multiple_of(PAGE_SIZE) || !(to as usize).is_multiple_of(PAGE_SIZE) { return Err(StorageError::NotAligned); } if to as usize > self.app_size { @@ -77,7 +72,7 @@ impl NorFlash for Storage { let end = self.app_base + to; while addr < end { writer.erase(addr); - addr += FLASH_ERASE_SIZE as u32; + addr += PAGE_SIZE as u32; } writer.operation_end(); // Write-protection check is debug-only: unlock() disables protection @@ -92,9 +87,7 @@ impl NorFlash for Storage { } fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { - if !(offset as usize).is_multiple_of(FLASH_WRITE_SIZE) - || !bytes.len().is_multiple_of(FLASH_WRITE_SIZE) - { + if !(offset as usize).is_multiple_of(PAGE_SIZE) || !bytes.len().is_multiple_of(PAGE_SIZE) { return Err(StorageError::NotAligned); } if offset as usize + bytes.len() > self.app_size { diff --git a/tinyboot-ch32-hal/src/flash/v0.rs b/tinyboot-ch32-hal/src/flash/v0.rs index e905d12..485d393 100644 --- a/tinyboot-ch32-hal/src/flash/v0.rs +++ b/tinyboot-ch32-hal/src/flash/v0.rs @@ -32,6 +32,9 @@ pub struct FlashWriter { write_bit: u8, } +/// Flash page size in bytes (erase and fast-write granularity). +pub const PAGE_SIZE: usize = 64; + const FTPG: u8 = 16; // FTPG bit - fast page programming - 64B const FTER: u8 = 17; // FTER bit - fast page erase - 64B const BUFRST: u8 = 18; // BUFRST bit - fast-program buffer reset From 2316792d3d48976807f4557b33d16019e9b3c136 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 24 Mar 2026 14:46:00 -0700 Subject: [PATCH 04/12] adapt flash write method to accomdate for full page writes. --- tinyboot-ch32-boot/src/platform/storage.rs | 23 +++++++++++++++------- tinyboot-ch32-hal/src/flash/v0.rs | 3 +++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/tinyboot-ch32-boot/src/platform/storage.rs b/tinyboot-ch32-boot/src/platform/storage.rs index d782c48..7100042 100644 --- a/tinyboot-ch32-boot/src/platform/storage.rs +++ b/tinyboot-ch32-boot/src/platform/storage.rs @@ -3,7 +3,7 @@ use embedded_storage::nor_flash::{ }; use tinyboot::traits::boot::Storage as StorageTrait; -use tinyboot_ch32_hal::flash::{FlashWriter, PAGE_SIZE}; +use tinyboot_ch32_hal::flash::{BUF_LOAD_SIZE, FlashWriter, PAGE_SIZE}; /// Flash storage configuration. pub struct StorageConfig { @@ -87,20 +87,29 @@ impl NorFlash for Storage { } fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { - if !(offset as usize).is_multiple_of(PAGE_SIZE) || !bytes.len().is_multiple_of(PAGE_SIZE) { + if !(offset as usize).is_multiple_of(PAGE_SIZE) + || bytes.len() > PAGE_SIZE + || !bytes.len().is_multiple_of(BUF_LOAD_SIZE) + { return Err(StorageError::NotAligned); } if offset as usize + bytes.len() > self.app_size { return Err(StorageError::OutOfBounds); } + let base = self.app_base + offset; + let mut addr = base; + let mut ptr = bytes.as_ptr() as *const u32; let writer = FlashWriter::usr(); writer.write_start(); - let mut addr = self.app_base + offset; - for pair in bytes.chunks_exact(2) { - let halfword = u16::from_le_bytes([pair[0], pair[1]]); - writer.write(addr, halfword); - addr += 2; + writer.fast_write_buf_reset(); + for _ in 0..bytes.len() / BUF_LOAD_SIZE { + // SAFETY: ptr advances within bounds, read_unaligned handles alignment + let word = unsafe { ptr.read_unaligned() }; + writer.fast_write_buf_load(addr, word); + addr += BUF_LOAD_SIZE as u32; + ptr = unsafe { ptr.add(1) }; } + writer.fast_write_page_program(base); writer.operation_end(); // See erase() for rationale on debug-only write-protection check. diff --git a/tinyboot-ch32-hal/src/flash/v0.rs b/tinyboot-ch32-hal/src/flash/v0.rs index 485d393..87570ae 100644 --- a/tinyboot-ch32-hal/src/flash/v0.rs +++ b/tinyboot-ch32-hal/src/flash/v0.rs @@ -35,6 +35,9 @@ pub struct FlashWriter { /// Flash page size in bytes (erase and fast-write granularity). pub const PAGE_SIZE: usize = 64; +/// Fast-write buffer load size in bytes. +pub const BUF_LOAD_SIZE: usize = 4; + const FTPG: u8 = 16; // FTPG bit - fast page programming - 64B const FTER: u8 = 17; // FTER bit - fast page erase - 64B const BUFRST: u8 = 18; // BUFRST bit - fast-program buffer reset From 01876fcd19b629e795a241ba78bdd50810d1dd3f Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 24 Mar 2026 15:31:35 -0700 Subject: [PATCH 05/12] Add Flush command --- tinyboot-protocol/src/lib.rs | 7 +++++-- tinyboot/src/protocol.rs | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tinyboot-protocol/src/lib.rs b/tinyboot-protocol/src/lib.rs index 6d0b914..d4fac7b 100644 --- a/tinyboot-protocol/src/lib.rs +++ b/tinyboot-protocol/src/lib.rs @@ -77,12 +77,14 @@ pub enum Cmd { Verify = 0x03, /// Reset the device. `addr=0`: boot app, `addr=1`: enter bootloader. 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 <= 0x04 + b <= 0x05 } } @@ -130,7 +132,8 @@ 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(0x05)); + assert!(Cmd::is_valid(Cmd::Flush as u8)); + assert!(!Cmd::is_valid(0x06)); assert!(!Cmd::is_valid(0xFF)); } diff --git a/tinyboot/src/protocol.rs b/tinyboot/src/protocol.rs index 3af1610..c02d941 100644 --- a/tinyboot/src/protocol.rs +++ b/tinyboot/src/protocol.rs @@ -165,6 +165,7 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> Dispatcher<'a, }; self.platform.ctl.system_reset(mode); } + Cmd::Flush => {} } self.frame From 21849680c12433bf7d4e1cedc011ef31d770583a Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 24 Mar 2026 15:56:07 -0700 Subject: [PATCH 06/12] update protocol dispatcher write command and add flush command. --- tinyboot/src/protocol.rs | 57 ++++++++++++++++++------- tinyboot/tests/state_transitions.rs | 65 +++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/tinyboot/src/protocol.rs b/tinyboot/src/protocol.rs index c02d941..48d2de8 100644 --- a/tinyboot/src/protocol.rs +++ b/tinyboot/src/protocol.rs @@ -1,15 +1,25 @@ +use crate::ringbuf::RingBuf; use crate::traits::boot::{BootCtl, BootMetaStore, Platform, Storage, Transport}; use crate::traits::{BootMode, BootState}; use tinyboot_protocol::crc::{CRC_INIT, crc16}; use tinyboot_protocol::frame::{Frame, InfoData, VerifyData}; use tinyboot_protocol::{Cmd, ReadError, Status}; -/// Protocol dispatcher. Borrows the platform, owns the frame. +/// Protocol dispatcher with write buffering. +/// +/// Incoming write data is accumulated in a ring buffer and flushed to storage +/// in page-sized chunks using fast page programming. The host must send a +/// `Flush` command to commit any remaining partial page before `Verify`, +/// or before skipping to a non-sequential address. pub struct Dispatcher<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> { /// Mutable reference to the platform peripherals. pub platform: &'a mut Platform, /// Reusable frame buffer. pub frame: Frame, + /// Write buffer. Sized for 2 × 64-byte pages. + buf: RingBuf<128>, + /// Expected address of the next sequential write. `None` = accept any. + next_addr: Option, } impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> Dispatcher<'a, T, S, B, C> { @@ -18,9 +28,21 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> Dispatcher<'a, Self { platform, frame: Frame::default(), + buf: RingBuf::default(), + next_addr: None, } } + /// Write `n` bytes from the buffer to storage, deriving the address from `next_addr`. + fn write_buf(&mut self, next: u32, n: usize) { + let addr = next - self.buf.len() as u32; + let data = self.buf.peek(n); + if self.platform.storage.write(addr, data).is_err() { + self.frame.status = Status::WriteError; + } + self.buf.consume(n); + } + /// Read a frame, dispatch the command, and send the response. /// Returns Err only for transport IO errors. Frame-level errors /// (bad CRC, invalid frame) are silently skipped. @@ -112,21 +134,21 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> Dispatcher<'a, self.frame.status = Status::Unsupported; } else { let addr = self.frame.addr; - if addr >= capacity - || addr + data_len as u32 > capacity + if addr + data_len as u32 > capacity || !addr.is_multiple_of(write_size) + || self.next_addr.is_some_and(|n| n != addr) { self.frame.status = Status::AddrOutOfBounds; - } else if self - .platform - .storage - // SAFETY: data_len <= MAX_PAYLOAD validated by frame.read() overflow check - .write(addr, unsafe { - self.frame.data.raw.get_unchecked(..data_len) - }) - .is_err() - { - self.frame.status = Status::WriteError; + } 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); + // Flush full page + if self.buf.len() >= S::WRITE_SIZE { + self.write_buf(next, S::WRITE_SIZE); + } } } } @@ -165,7 +187,14 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> Dispatcher<'a, }; self.platform.ctl.system_reset(mode); } - Cmd::Flush => {} + Cmd::Flush => { + if let Some(next) = self.next_addr { + if !self.buf.is_empty() { + self.write_buf(next, self.buf.len()); + } + self.next_addr = None; + } + } } self.frame diff --git a/tinyboot/tests/state_transitions.rs b/tinyboot/tests/state_transitions.rs index 2f0bb3b..226c8a5 100644 --- a/tinyboot/tests/state_transitions.rs +++ b/tinyboot/tests/state_transitions.rs @@ -342,6 +342,10 @@ fn write_from_updating_succeeds() { .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.dispatch().unwrap(); + assert_eq!(d.frame.status, Status::Ok); assert_eq!(&d.platform.storage.data[..4], &[0xDE, 0xAD, 0xBE, 0xEF]); assert_eq!(d.platform.boot_meta.state, BootState::Updating); } @@ -386,6 +390,8 @@ fn write_at_offset() { .load_request(Cmd::Write, 8, 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]); } @@ -496,6 +502,11 @@ fn full_update_cycle() { 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.dispatch().unwrap(); + assert_eq!(d.frame.status, Status::Ok); + // Verify: Updating → Validating (app_size=4) d.platform .transport @@ -529,6 +540,11 @@ fn reflash_from_validating() { d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); + // Flush buffered writes + d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); + d.dispatch().unwrap(); + assert_eq!(d.frame.status, Status::Ok); + // Verify: Updating → Validating d.platform .transport @@ -538,3 +554,52 @@ fn reflash_from_validating() { assert_eq!(d.platform.boot_meta.state, BootState::Validating); assert_eq!(d.platform.boot_meta.app_size, fw.len() as u32); } + +// ============================================================================= +// Flush +// ============================================================================= + +#[test] +fn flush_commits_buffered_write() { + let mut p = platform(BootState::Updating); + let mut d = Dispatcher::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); + + d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); + d.dispatch().unwrap(); + assert_eq!(d.frame.status, Status::Ok); + assert_eq!(&d.platform.storage.data[..2], &[0xAA, 0xBB]); +} + +#[test] +fn flush_empty_is_ok() { + let mut p = platform(BootState::Idle); + let mut d = Dispatcher::new(&mut p); + d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); + d.dispatch().unwrap(); + assert_eq!(d.frame.status, Status::Ok); +} + +#[test] +fn write_non_sequential_without_flush_rejected() { + let mut p = platform(BootState::Updating); + let mut d = Dispatcher::new(&mut p); + + // First write at addr 0 + d.platform.transport.load_request(Cmd::Write, 0, 4, &[0; 4]); + d.dispatch().unwrap(); + assert_eq!(d.frame.status, Status::Ok); + + // Non-sequential write without Flush + d.platform + .transport + .load_request(Cmd::Write, 128, 4, &[0; 4]); + d.dispatch().unwrap(); + assert_eq!(d.frame.status, Status::AddrOutOfBounds); +} From 3c6146d713df44f4ab867ac05a1e3b869a0f4d80 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 24 Mar 2026 16:05:28 -0700 Subject: [PATCH 07/12] Dynamically compute protocol buffer size during build. --- examples/ch32/system-flash/boot/src/main.rs | 4 +- examples/ch32/user-flash/boot/src/main.rs | 4 +- tinyboot-ch32-boot/src/lib.rs | 9 +++- tinyboot/src/core.rs | 6 +-- tinyboot/src/protocol.rs | 11 +++-- tinyboot/tests/state_transitions.rs | 52 +++++++++++---------- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/examples/ch32/system-flash/boot/src/main.rs b/examples/ch32/system-flash/boot/src/main.rs index c293d08..429fc6d 100644 --- a/examples/ch32/system-flash/boot/src/main.rs +++ b/examples/ch32/system-flash/boot/src/main.rs @@ -14,7 +14,7 @@ use panic_halt as _; use tinyboot_ch32_boot::{ - BaudRate, BootCtl, BootCtlConfig, BootMetaStore, Core, Duplex, Platform, Pull, Storage, + BaudRate, BootCtl, BootCtlConfig, BootMetaStore, Duplex, Platform, Pull, Storage, StorageConfig, Usart, UsartConfig, UsartMapping, }; @@ -54,5 +54,5 @@ fn main() -> ! { const BOOT_VER: u16 = tinyboot_ch32_boot::pkg_version!(); let platform = Platform::new(transport, storage, boot_meta, ctl, BOOT_VER); - Core::new(platform).run(); + tinyboot_ch32_boot::run(platform); } diff --git a/examples/ch32/user-flash/boot/src/main.rs b/examples/ch32/user-flash/boot/src/main.rs index 3f4512a..933f70e 100644 --- a/examples/ch32/user-flash/boot/src/main.rs +++ b/examples/ch32/user-flash/boot/src/main.rs @@ -17,7 +17,7 @@ use defmt_rtt as _; use panic_halt as _; use tinyboot_ch32_boot::{ - BaudRate, BootCtl, BootCtlConfig, BootMetaStore, Core, Duplex, Platform, Pull, Storage, + BaudRate, BootCtl, BootCtlConfig, BootMetaStore, Duplex, Platform, Pull, Storage, StorageConfig, Usart, UsartConfig, UsartMapping, }; @@ -60,5 +60,5 @@ fn main() -> ! { const BOOT_VER: u16 = tinyboot_ch32_boot::pkg_version!(); let platform = Platform::new(transport, storage, boot_meta, ctl, BOOT_VER); - Core::new(platform).run(); + tinyboot_ch32_boot::run(platform); } diff --git a/tinyboot-ch32-boot/src/lib.rs b/tinyboot-ch32-boot/src/lib.rs index 9298644..affc2e7 100644 --- a/tinyboot-ch32-boot/src/lib.rs +++ b/tinyboot-ch32-boot/src/lib.rs @@ -18,12 +18,19 @@ pub use platform::{ }; // Re-exports so boot examples only need this one crate. -pub use tinyboot::Core; pub use tinyboot::traits::boot::Platform; pub use tinyboot_ch32_hal::gpio::Pull; pub use tinyboot_ch32_hal::{Pin, UsartMapping}; pub use tinyboot_protocol::pkg_version; +/// Protocol write buffer size (2 × page size). +const PROTOCOL_BUF_SIZE: usize = 2 * tinyboot_ch32_hal::flash::PAGE_SIZE; + +/// Run the bootloader. Hides the protocol buffer size const generic. +pub fn run(platform: Platform) -> ! { + tinyboot::Core::<_, _, _, _, PROTOCOL_BUF_SIZE>::new(platform).run() +} + #[unsafe(link_section = ".tinyboot_version")] #[used] static BOOT_VERSION: u16 = tinyboot_protocol::pkg_version!(); diff --git a/tinyboot/src/core.rs b/tinyboot/src/core.rs index b9ad0cb..b72d28d 100644 --- a/tinyboot/src/core.rs +++ b/tinyboot/src/core.rs @@ -4,7 +4,7 @@ use crate::traits::{BootMode, BootState}; /// Bootloader entry point. Checks boot state, validates the app, and either /// boots the application or enters the protocol loop for firmware updates. -pub struct Core +pub struct Core where T: Transport, S: Storage, @@ -14,7 +14,7 @@ where platform: Platform, } -impl Core +impl Core where T: Transport, S: Storage, @@ -87,7 +87,7 @@ where log_info!("Entering bootloader mode"); self.platform.storage.unlock(); - let mut d = protocol::Dispatcher::new(&mut self.platform); + let mut d = protocol::Dispatcher::<_, _, _, _, BUF>::new(&mut self.platform); loop { let _ = d.dispatch(); diff --git a/tinyboot/src/protocol.rs b/tinyboot/src/protocol.rs index 48d2de8..8e80715 100644 --- a/tinyboot/src/protocol.rs +++ b/tinyboot/src/protocol.rs @@ -11,18 +11,21 @@ use tinyboot_protocol::{Cmd, ReadError, Status}; /// in page-sized chunks using fast page programming. The host must send a /// `Flush` command to commit any remaining partial page before `Verify`, /// or before skipping to a non-sequential address. -pub struct Dispatcher<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> { +pub struct Dispatcher<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usize> +{ /// Mutable reference to the platform peripherals. pub platform: &'a mut Platform, /// Reusable frame buffer. pub frame: Frame, - /// Write buffer. Sized for 2 × 64-byte pages. - buf: RingBuf<128>, + /// Write buffer. Sized for 2 × page size. + buf: RingBuf, /// Expected address of the next sequential write. `None` = accept any. next_addr: Option, } -impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> Dispatcher<'a, T, S, B, C> { +impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usize> + Dispatcher<'a, T, S, B, C, BUF> +{ /// Create a new dispatcher for the given platform. pub fn new(platform: &'a mut Platform) -> Self { Self { diff --git a/tinyboot/tests/state_transitions.rs b/tinyboot/tests/state_transitions.rs index 226c8a5..8e0ad39 100644 --- a/tinyboot/tests/state_transitions.rs +++ b/tinyboot/tests/state_transitions.rs @@ -191,6 +191,8 @@ impl BootMetaStore for MockBootMeta { } type TestPlatform = Platform; +// 2 × MockStorage ERASE_SIZE (64) +const TEST_BUF_SIZE: usize = 128; fn platform(state: BootState) -> TestPlatform { Platform::new( @@ -216,7 +218,7 @@ fn erase_data(byte_count: u16) -> [u8; 2] { #[test] fn erase_from_idle_transitions_to_updating() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Erase, 0, 2, &erase_data(64)); @@ -228,7 +230,7 @@ fn erase_from_idle_transitions_to_updating() { #[test] fn erase_from_updating_stays_updating() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Erase, 0, 2, &erase_data(64)); @@ -241,7 +243,7 @@ fn erase_from_updating_stays_updating() { fn erase_from_validating_transitions_to_updating_and_clears_checksum() { let mut p = platform(BootState::Validating); p.boot_meta.checksum = 0x1234; - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Erase, 0, 2, &erase_data(64)); @@ -254,7 +256,7 @@ fn erase_from_validating_transitions_to_updating_and_clears_checksum() { #[test] fn erase_validates_addr_alignment() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Erase, 32, 2, &erase_data(64)); @@ -265,7 +267,7 @@ fn erase_validates_addr_alignment() { #[test] fn erase_validates_byte_count_alignment() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Erase, 0, 2, &erase_data(32)); @@ -276,7 +278,7 @@ fn erase_validates_byte_count_alignment() { #[test] fn erase_validates_bounds() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Erase, 256, 2, &erase_data(64)); @@ -287,7 +289,7 @@ fn erase_validates_bounds() { #[test] fn erase_rejects_zero_byte_count() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Erase, 0, 2, &erase_data(0)); @@ -301,7 +303,7 @@ fn erase_bulk_multiple_pages() { p.storage.data[0] = 0x42; p.storage.data[64] = 0x42; p.storage.data[128] = 0x42; - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Erase, 0, 2, &erase_data(192)); @@ -324,7 +326,7 @@ fn erase_bulk_multiple_pages() { #[test] fn write_from_idle_rejected() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Write, 0, 4, &[0xDE, 0xAD, 0xBE, 0xEF]); @@ -336,7 +338,7 @@ fn write_from_idle_rejected() { #[test] fn write_from_updating_succeeds() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Write, 0, 4, &[0xDE, 0xAD, 0xBE, 0xEF]); @@ -353,7 +355,7 @@ fn write_from_updating_succeeds() { #[test] fn write_from_validating_rejected() { let mut p = platform(BootState::Validating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Write, 0, 4, &[0xDE, 0xAD, 0xBE, 0xEF]); @@ -364,7 +366,7 @@ fn write_from_validating_rejected() { #[test] fn write_validates_addr_alignment() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform.transport.load_request(Cmd::Write, 1, 4, &[0; 4]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::AddrOutOfBounds); @@ -373,7 +375,7 @@ fn write_validates_addr_alignment() { #[test] fn write_validates_bounds() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Write, 256, 4, &[0; 4]); @@ -384,7 +386,7 @@ fn write_validates_bounds() { #[test] fn write_at_offset() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Write, 8, 4, &[0x01, 0x02, 0x03, 0x04]); @@ -405,7 +407,7 @@ fn write_at_offset() { #[test] fn verify_from_idle_rejected() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform.transport.load_request(Cmd::Verify, 4, 0, &[]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Unsupported); @@ -417,7 +419,7 @@ fn verify_from_updating_transitions_to_validating() { let fw = [0x01, 0x02, 0x03, 0x04]; p.storage.data[..4].copy_from_slice(&fw); let app_size = fw.len() as u32; - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Verify, app_size, 0, &[]); @@ -434,7 +436,7 @@ fn verify_from_updating_transitions_to_validating() { #[test] fn verify_from_validating_rejected() { let mut p = platform(BootState::Validating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform.transport.load_request(Cmd::Verify, 4, 0, &[]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Unsupported); @@ -443,7 +445,7 @@ fn verify_from_validating_rejected() { #[test] fn verify_rejects_zero_app_size() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform.transport.load_request(Cmd::Verify, 0, 0, &[]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::AddrOutOfBounds); @@ -453,7 +455,7 @@ fn verify_rejects_zero_app_size() { #[test] fn verify_rejects_app_size_exceeding_capacity() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + 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(); @@ -468,7 +470,7 @@ fn verify_rejects_app_size_exceeding_capacity() { #[test] fn info_reports_geometry() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform.transport.load_request(Cmd::Info, 0, 0, &[]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); @@ -486,7 +488,7 @@ fn info_reports_geometry() { #[test] fn full_update_cycle() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); // Erase: Idle → Updating d.platform @@ -523,7 +525,7 @@ fn reflash_from_validating() { let mut p = platform(BootState::Validating); p.boot_meta.checksum = 0x1234; p.boot_meta.app_size = 4; - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); // Erase: Validating → Updating (reflash, clears checksum + app_size) d.platform @@ -562,7 +564,7 @@ fn reflash_from_validating() { #[test] fn flush_commits_buffered_write() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform .transport .load_request(Cmd::Write, 0, 2, &[0xAA, 0xBB]); @@ -580,7 +582,7 @@ fn flush_commits_buffered_write() { #[test] fn flush_empty_is_ok() { let mut p = platform(BootState::Idle); - let mut d = Dispatcher::new(&mut p); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); d.platform.transport.load_request(Cmd::Flush, 0, 0, &[]); d.dispatch().unwrap(); assert_eq!(d.frame.status, Status::Ok); @@ -589,7 +591,7 @@ fn flush_empty_is_ok() { #[test] fn write_non_sequential_without_flush_rejected() { let mut p = platform(BootState::Updating); - let mut d = Dispatcher::new(&mut p); + 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]); From 027f2b5b6361569543432cecaeb69d1743cf999f Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 24 Mar 2026 16:14:27 -0700 Subject: [PATCH 08/12] add flush to cli --- tinyboot-cli/src/client.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tinyboot-cli/src/client.rs b/tinyboot-cli/src/client.rs index 6ca71e1..4291649 100644 --- a/tinyboot-cli/src/client.rs +++ b/tinyboot-cli/src/client.rs @@ -175,7 +175,10 @@ impl Client { on_progress("Writing", chunk_idx, total_chunks); } - // 4. Verify — CRC only covers firmware bytes (no padding) + // 4. Flush buffered writes + self.flush()?; + + // 5. Verify — CRC only covers firmware bytes (no padding) let expected_crc = crc16(CRC_INIT, firmware); self.frame.cmd = Cmd::Verify; @@ -194,6 +197,14 @@ 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. pub fn reset(&mut self, bootloader: bool) { From 39d86ccbff054c23e8641f8dbdfbd5e6be94dac1 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Tue, 24 Mar 2026 16:57:38 -0700 Subject: [PATCH 09/12] fix v0.rs flash_ctlr bit constant error. --- tinyboot-ch32-hal/src/flash/v0.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tinyboot-ch32-hal/src/flash/v0.rs b/tinyboot-ch32-hal/src/flash/v0.rs index 87570ae..00b32c2 100644 --- a/tinyboot-ch32-hal/src/flash/v0.rs +++ b/tinyboot-ch32-hal/src/flash/v0.rs @@ -40,8 +40,8 @@ pub const BUF_LOAD_SIZE: usize = 4; const FTPG: u8 = 16; // FTPG bit - fast page programming - 64B const FTER: u8 = 17; // FTER bit - fast page erase - 64B -const BUFRST: u8 = 18; // BUFRST bit - fast-program buffer reset -const BUFLOAD: u8 = 19; // BUFLOAD bit - fast-program buffer load +const BUFLOAD: u8 = 18; // BUFLOAD bit - fast-program buffer load +const BUFRST: u8 = 19; // BUFRST bit - fast-program buffer reset const OBPG: u8 = 4; // OBPG bit - option byte programming const OBER: u8 = 5; // OBER bit - option byte erase From 93bae186c6be98eb3671d70e06b211c4877f60e2 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Wed, 25 Mar 2026 01:14:24 -0700 Subject: [PATCH 10/12] misc optimizations --- tinyboot-ch32-boot/src/lib.rs | 1 + tinyboot-ch32-hal/build.rs | 2 +- tinyboot-ch32-hal/src/flash/v0.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tinyboot-ch32-boot/src/lib.rs b/tinyboot-ch32-boot/src/lib.rs index affc2e7..69c618b 100644 --- a/tinyboot-ch32-boot/src/lib.rs +++ b/tinyboot-ch32-boot/src/lib.rs @@ -27,6 +27,7 @@ pub use tinyboot_protocol::pkg_version; const PROTOCOL_BUF_SIZE: usize = 2 * tinyboot_ch32_hal::flash::PAGE_SIZE; /// Run the bootloader. Hides the protocol buffer size const generic. +#[inline(always)] pub fn run(platform: Platform) -> ! { tinyboot::Core::<_, _, _, _, PROTOCOL_BUF_SIZE>::new(platform).run() } diff --git a/tinyboot-ch32-hal/build.rs b/tinyboot-ch32-hal/build.rs index 528998e..9faf571 100644 --- a/tinyboot-ch32-hal/build.rs +++ b/tinyboot-ch32-hal/build.rs @@ -192,7 +192,7 @@ fn generate_pin_and_usart_mapping(out: &Path) -> Result<(), Box> { // regs() writeln!( code, - " pub fn regs(self) -> ch32_metapac::usart::Usart {{" + " pub const fn regs(self) -> ch32_metapac::usart::Usart {{" )?; writeln!(code, " match self {{")?; for ((peri, remap), group) in &groups { diff --git a/tinyboot-ch32-hal/src/flash/v0.rs b/tinyboot-ch32-hal/src/flash/v0.rs index 00b32c2..15f189f 100644 --- a/tinyboot-ch32-hal/src/flash/v0.rs +++ b/tinyboot-ch32-hal/src/flash/v0.rs @@ -18,6 +18,7 @@ pub fn unlock() { } /// Lock flash controller. +#[inline(always)] pub fn lock() { FLASH.ctlr().write(|w| { w.set_lock(true); From ae1dcf639be12c9b3a06f49ef413f85b98578fe3 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Wed, 25 Mar 2026 23:26:45 -0700 Subject: [PATCH 11/12] fix CI apt-get issue. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85a481d..e7d372c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: working-directory: tinyboot-cli steps: - uses: actions/checkout@v4 - - run: sudo apt-get install -y libudev-dev + - run: sudo apt-get update && sudo apt-get install -y libudev-dev - run: rustup toolchain install stable --profile minimal --component rustfmt,clippy - run: cargo fmt --all -- --check - run: cargo clippy -- -D warnings From b45d1f56ada4ba655ef3342b643fe1ad346158d5 Mon Sep 17 00:00:00 2001 From: Aaron Qian Date: Wed, 25 Mar 2026 23:35:06 -0700 Subject: [PATCH 12/12] Make sure one failed test in matrix doesn't cancel all others. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7d372c..50dc509 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,7 @@ jobs: name: Examples runs-on: ubuntu-latest strategy: + fail-fast: false matrix: example: [system-flash, user-flash] defaults: