diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85a481d..50dc509 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 @@ -59,6 +59,7 @@ jobs: name: Examples runs-on: ubuntu-latest strategy: + fail-fast: false matrix: example: [system-flash, user-flash] defaults: 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-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/lib.rs b/tinyboot-ch32-boot/src/lib.rs index 9298644..69c618b 100644 --- a/tinyboot-ch32-boot/src/lib.rs +++ b/tinyboot-ch32-boot/src/lib.rs @@ -18,12 +18,20 @@ 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. +#[inline(always)] +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-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..7100042 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::{BUF_LOAD_SIZE, FlashWriter, PAGE_SIZE}; /// Flash storage configuration. pub struct StorageConfig { @@ -59,25 +56,23 @@ 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 { 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; 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,22 +87,29 @@ 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() > 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 writer = FlashWriter::standard(); + 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/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 1de237e..15f189f 100644 --- a/tinyboot-ch32-hal/src/flash/v0.rs +++ b/tinyboot-ch32-hal/src/flash/v0.rs @@ -1,28 +1,26 @@ 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. +#[inline(always)] pub fn lock() { - flash().ctlr().write(|w| { + FLASH.ctlr().write(|w| { w.set_lock(true); w.set_flock(true); }); @@ -35,8 +33,16 @@ pub struct FlashWriter { write_bit: u8, } -const PG: u8 = 0; // PG bit - standard programming - 2b +/// 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 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 @@ -45,16 +51,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 +69,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 +85,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 +101,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 +109,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 +118,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)); } 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) { 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/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/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/protocol.rs b/tinyboot/src/protocol.rs index 3af1610..8e80715 100644 --- a/tinyboot/src/protocol.rs +++ b/tinyboot/src/protocol.rs @@ -1,26 +1,51 @@ +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. -pub struct Dispatcher<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> { +/// 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, 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 × 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 { 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 +137,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,6 +190,14 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl> Dispatcher<'a, }; self.platform.ctl.system_reset(mode); } + 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/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]); + } +} diff --git a/tinyboot/tests/state_transitions.rs b/tinyboot/tests/state_transitions.rs index 2f0bb3b..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,12 +338,16 @@ 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]); 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); } @@ -349,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]); @@ -360,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); @@ -369,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]); @@ -380,12 +386,14 @@ 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]); 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]); } @@ -399,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); @@ -411,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, &[]); @@ -428,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); @@ -437,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); @@ -447,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(); @@ -462,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); @@ -480,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 @@ -496,6 +504,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 @@ -512,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 @@ -529,6 +542,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 +556,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::<_, _, _, _, 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); + + 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::<_, _, _, _, 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); +} + +#[test] +fn write_non_sequential_without_flush_rejected() { + 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.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); +}