Skip to content

Commit f02c537

Browse files
authored
collect writes in a one-page buffer, so a write may span pages (#41)
1 parent d228596 commit f02c537

10 files changed

Lines changed: 88 additions & 190 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4.
2222
- `boot_version!(compat = "name")` and `app_version!(compat = "name")` (re-exported from core): a bootloader with a compatibility key accepts only app images with the same key. The V00x and V103 examples set one.
2323
- The bootloader runs its USART at the baud stored in boot metadata, falling back to `UsartConfig::baud` when nothing is stored or `pclk` cannot reach the rate (divisor below 16). Divisors are computed at build time, so there is no runtime divide; the CH32V003 example bootloader grows by 64 bytes.
2424
- `app::BaudRate` re-export for `App::set_baud` / `App::baud`.
25-
- `boot::run_with_payload::<N>` runs the bootloader with an `N`-byte frame payload, up to a flash page; `boot::run` uses the 52-byte default.
25+
- `boot::run_with_payload::<N>` runs the bootloader with an `N`-byte frame payload (at least 18; a Write may span flash pages); `boot::run` uses the 52-byte default.
2626

2727
### Changed
2828

@@ -44,6 +44,7 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4.
4444
- **Breaking:** `BootMetaStore` has `baud` and `set_baud`, and `refresh` keeps the stored baud.
4545
- **Breaking:** `Core`, `Dispatcher` and `App` take the frame's payload capacity as a const parameter (default `DEFAULT_PAYLOAD`), and `AppConfig` has `boot_compat`. Info reports the stored boot baud, the bootloader's compatibility key and the payload capacity, in both modes.
4646
- **Breaking:** the image tail is 6 bytes, `[compat u32, version u16]`, read through the `__tb_tail` symbol the macros define. `tb-app.x` starts it word-aligned and exports `__tb_boot_tail` (was `__tb_boot_version_addr`); `tb-boot.x` places it in the last 6 bytes of BOOT. The version stays in the last 2 bytes of each image.
47+
- **Breaking:** writes collect in a one-page buffer instead of a two-page ring buffer, and the `ringbuf` module is gone. `BUF` on `Core` and `Dispatcher` is the storage's write size (was twice it), halving the RAM. The CH32V003 example bootloader shrinks by about 100 bytes.
4748
- `Reset` to the app is refused with `Unsupported` while an update is in progress (`Updating`). Chips that hand off by run mode start the app without the bootloader checking META, so a half-written image used to run. `Reset` with `BOOTLOADER` and `Erase` still restart the update.
4849

4950
## [tinyboot-protocol 0.5.0] - 2026-10-20

‎ch32/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ fn main() -> ! {
4646

4747
`Storage` and `BootMetaStore` are initialized from linker symbols automatically. `boot_version!()` ends the image with its tail: a compatibility key and the crate's `Cargo.toml` version. `boot_version!(compat = "name")` sets the key, and the bootloader then accepts only app images built with the same `app_version!(compat = "name")` (see [app integration](https://openservocore.github.io/tinyboot/app-integration.html)).
4848

49-
`boot::run` reads frames with up to 52 payload bytes (a whole frame fits one 64-byte USB packet); `boot::run_with_payload::<N>` takes up to a flash page per Write for faster updates, at `N` bytes of RAM. Hosts read the capacity from Info.
49+
`boot::run` reads frames with up to 52 payload bytes (a whole frame fits one 64-byte USB packet); `boot::run_with_payload::<N>` takes `N` bytes per Write (at least 18, not limited to a flash page) for faster updates, at `N` bytes of RAM. Hosts read the capacity from Info.
5050

5151
For configuring RS-485 half-duplex, DXL TTL, or alternate pin remaps, see the [transports guide](https://openservocore.github.io/tinyboot/transports.html). For CH32V103 in system-flash mode, `BootCtl::new` takes additional arguments for the external BOOT_CTL circuit — see [boot-ctl](https://openservocore.github.io/tinyboot/boot-ctl.html).
5252

‎ch32/src/boot.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,16 @@ pub fn run(transport: impl tinyboot_core::traits::Transport, ctl: BootCtl) -> !
3131
}
3232

3333
/// Run the bootloader with a `P`-byte frame payload, at least the 18-byte
34-
/// Info reply and at most a flash page. A larger payload takes fewer
34+
/// Info reply; a Write may span flash pages. A larger payload takes fewer
3535
/// Writes for an image and costs `P` bytes of RAM. Does not return.
3636
#[inline(always)]
3737
pub fn run_with_payload<const P: usize>(
3838
transport: impl tinyboot_core::traits::Transport,
3939
ctl: BootCtl,
4040
) -> ! {
4141
const {
42-
assert!(P >= InfoData::SIZE as usize && P <= PAGE_SIZE);
42+
assert!(P >= InfoData::SIZE as usize);
4343
}
4444
let platform = Platform::new(transport, Storage::default(), BootMetaStore::default(), ctl);
45-
tinyboot_core::Core::<_, _, _, _, { 2 * PAGE_SIZE }, P>::new(platform).run()
45+
tinyboot_core::Core::<_, _, _, _, PAGE_SIZE, P>::new(platform).run()
4646
}

‎docs/design.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Beyond the usual Cargo profile tricks (`opt-level = "z"`, LTO, `codegen-units =
3030
- **`repr(C)` frame with union data** — CRC is computed directly over the struct memory via pointer cast; no serialization step, no intermediate buffer.
3131
- **`MaybeUninit` frame buffer** — the 64-byte `Frame` struct (default 52-byte payload) is reused every iteration without zero-initialization.
3232
- **Bit-bang CRC16** — no lookup table, trades speed for ~512 bytes of flash savings.
33+
- **One-page write buffer** - Write data is copied into a single page buffer that goes to flash each time it fills, so a Write may span pages without a ring buffer's index arithmetic.
3334
- **Bit-clear state transitions** — forward state changes (Idle → Updating, trial consumption) flip 1→0 bits without erasing, avoiding a full erase + rewrite cycle.
3435
- **Avoid `memset` / `memcpy`** — these pull in expensive `core` routines; manual byte loops and volatile writes keep the linker from dragging in library code.
3536
- **`.write()` over `.modify()`** — register writes use direct writes instead of read-modify-write, saving the read and mask operations.

‎lib/core/src/lib.rs‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ pub mod core;
1414
pub mod platform;
1515
/// Protocol frame dispatcher.
1616
pub mod protocol;
17-
/// Fixed-size ring buffer for buffered flash writes.
18-
pub mod ringbuf;
1917
/// Platform abstraction traits.
2018
pub mod traits;
2119

‎lib/core/src/protocol.rs‎

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
use crate::platform::Platform;
2-
use crate::ringbuf::RingBuf;
32
use crate::traits::{BootCtl, BootMetaStore, BootState, RunMode, Storage, Transport};
43
use tinyboot_protocol::crc::{CRC_INIT, crc16};
54
use tinyboot_protocol::frame::{DEFAULT_PAYLOAD, Frame, InfoData, VerifyData};
65
use tinyboot_protocol::{COMPAT_UNSET, Cmd, ReadError, ResetFlags, Status, WriteFlags};
76

87
/// Protocol dispatcher with write buffering.
98
///
10-
/// Writes are accumulated in a ring buffer and flushed a page at a time.
9+
/// Writes are accumulated in a page buffer and written a page at a time.
1110
/// The host sets [`WriteFlags::FLUSH`] on the last write of each contiguous
12-
/// region to commit the trailing partial page. `P` is the frame's data
13-
/// capacity, reported by Info; `BUF` must hold a page plus `P - 1` bytes.
11+
/// region to commit the trailing partial page. `BUF` is the page, the
12+
/// storage's write size; `P` is the frame's data capacity, reported by Info.
1413
pub struct Dispatcher<
1514
'a,
1615
T: Transport,
@@ -24,12 +23,19 @@ pub struct Dispatcher<
2423
pub platform: &'a mut Platform<T, S, B, C>,
2524
/// Reusable frame buffer.
2625
pub frame: Frame<P>,
27-
/// 2 × page size.
28-
buf: RingBuf<BUF>,
29-
/// Expected address of the next sequential write; `None` accepts any.
30-
next_addr: Option<u32>,
26+
/// Page being filled; `fill` bytes, ending just before `next`.
27+
page: Page<BUF>,
28+
fill: usize,
29+
/// Address of the next sequential write; [`ANY`] after a flush.
30+
next: u32,
3131
}
3232

33+
/// No write is pending: the next may start at any page-aligned address.
34+
const ANY: u32 = u32::MAX;
35+
36+
#[repr(align(4))]
37+
struct Page<const N: usize>(core::mem::MaybeUninit<[u8; N]>);
38+
3339
impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usize, const P: usize>
3440
Dispatcher<'a, T, S, B, C, BUF, P>
3541
{
@@ -38,18 +44,18 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz
3844
Self {
3945
platform,
4046
frame: Frame::default(),
41-
buf: RingBuf::default(),
42-
next_addr: None,
47+
page: Page(core::mem::MaybeUninit::uninit()),
48+
fill: 0,
49+
next: ANY,
4350
}
4451
}
4552

46-
fn write_buf(&mut self, next: u32, n: usize) {
47-
let addr = next - self.buf.len() as u32;
48-
let data = self.buf.peek(n);
49-
if self.platform.storage.write(addr, data).is_err() {
53+
fn write_page(&mut self, end: u32, n: usize) {
54+
// SAFETY: the first n <= BUF bytes were filled.
55+
let data = unsafe { core::slice::from_raw_parts(self.page.0.as_ptr().cast::<u8>(), n) };
56+
if self.platform.storage.write(end - n as u32, data).is_err() {
5057
self.frame.status = Status::WriteError;
5158
}
52-
self.buf.consume(n);
5359
}
5460

5561
#[inline(always)]
@@ -61,36 +67,45 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz
6167
capacity: u32,
6268
state: BootState,
6369
) {
70+
const { assert!(BUF == S::WRITE_SIZE) };
6471
if state != BootState::Updating {
6572
self.frame.status = Status::Unsupported;
6673
return;
6774
}
6875

69-
let flush = flags.contains(WriteFlags::FLUSH);
70-
76+
let mut fill = self.fill;
7177
if addr + data_len as u32 > capacity
72-
|| (self.next_addr.is_none() && !addr.is_multiple_of(S::WRITE_SIZE as u32))
73-
|| self.next_addr.is_some_and(|n| n != addr)
78+
|| if self.next == ANY {
79+
!addr.is_multiple_of(BUF as u32)
80+
} else {
81+
addr != self.next
82+
}
7483
{
7584
self.frame.status = Status::AddrOutOfBounds;
7685
return;
7786
}
7887

79-
// SAFETY: data_len <= P validated by frame.read()
80-
self.buf
81-
.push(unsafe { self.frame.data.raw.get_unchecked(..data_len) });
82-
let next = addr + data_len as u32;
83-
self.next_addr = Some(next);
84-
if self.buf.len() >= S::WRITE_SIZE {
85-
self.write_buf(next, S::WRITE_SIZE);
88+
let mut next = addr;
89+
let dst = self.page.0.as_mut_ptr().cast::<u8>();
90+
for i in 0..data_len {
91+
// SAFETY: i < data_len <= P (frame.read() validated it), fill < BUF.
92+
unsafe { *dst.add(fill) = *self.frame.data.raw.get_unchecked(i) };
93+
fill += 1;
94+
next += 1;
95+
if fill == BUF {
96+
self.write_page(next, BUF);
97+
fill = 0;
98+
}
8699
}
87-
if flush {
88-
if !self.buf.is_empty() {
89-
self.write_buf(next, self.buf.len());
100+
if flags.contains(WriteFlags::FLUSH) {
101+
if fill != 0 {
102+
self.write_page(next, fill);
103+
fill = 0;
90104
}
91-
self.buf.reset();
92-
self.next_addr = None;
105+
next = ANY;
93106
}
107+
self.fill = fill;
108+
self.next = next;
94109
}
95110

96111
#[inline(always)]

‎lib/core/src/ringbuf.rs‎

Lines changed: 0 additions & 148 deletions
This file was deleted.

‎lib/core/tests/common/mod.rs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ impl nor_flash::NorFlash for MockStorage {
138138
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
139139
let start = offset as usize;
140140
let end = start + bytes.len();
141+
assert!(
142+
start.is_multiple_of(Self::WRITE_SIZE),
143+
"write not page-aligned"
144+
);
145+
assert!(bytes.len() <= Self::WRITE_SIZE, "write crosses a page");
141146
if end > self.data.len() {
142147
return Err(nor_flash::NorFlashErrorKind::OutOfBounds);
143148
}
@@ -224,7 +229,7 @@ impl BootMetaStore for MockBootMeta {
224229
}
225230

226231
pub type TestPlatform = Platform<MockTransport, MockStorage, MockBootMeta, MockBootCtl>;
227-
pub const TEST_BUF_SIZE: usize = 128;
232+
pub const TEST_BUF_SIZE: usize = 4;
228233

229234
pub fn platform(state: BootState) -> TestPlatform {
230235
Platform::new(

0 commit comments

Comments
 (0)