Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4.

### Added

- `addressing`: the `Addressing` trait, `Unaddressed`, and `Addressable::new(uid)` for devices sharing a bus. An addressable device answers `Select` when its id starts with the prefix, after a slot of 0 to 15 byte-times drawn from its id and the host's seed, and any other command only while selected. It never answers a frame it cannot read or another device's reply. `Dispatcher::with_addressing`, `Core::with_addressing` and `App::with_addressing` take it.
- `Transport::wait_bytes(n)`, a wait of about `n` byte-times for the `Select` slot; the default returns at once.
- Compatibility keys. `boot_version!` and `app_version!` take `compat = "name"` (hashed at compile time) or `compat_key = <u32>`, and end the image with a 6-byte tail, `ImageTail { compat, version }`. A bootloader with a key refuses `Verify` of an image with another key, or none, with `Status::Incompatible` and stays in `Updating`; without a key it carries no check (with LTO the unset key folds away).
- `Reset` with `ResetFlags::BAUD` stores addr byte 0 as the bootloader's baud before resetting, in the bootloader and in `App::poll`.
- `App::baud` returns the stored bootloader baud; `App::set_baud` stores one, skipping the flash write when it is already stored.
Expand All @@ -53,6 +55,7 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4.

### Added

- `Cmd::Select` (0x05), data `[seed, prefix_len, prefix..]`: selects every addressable device whose unique id starts with the prefix, and deselects the rest.
- `Frame<N>` and `Data<N>` take the payload capacity as a const parameter, `DEFAULT_PAYLOAD` = 52 so a whole frame fits one 64-byte USB full-speed packet.
- `Status::Incompatible` (0x07), `COMPAT_UNSET` and `compat_key(name)`, a compile-time 32-bit FNV-1a hash that never returns `COMPAT_UNSET`.
- `BaudRate`, the 14 rates from 9600 to 3M as append-only `u8` indices, with `hz`, `from_u8`, `from_hz` and `ALL`.
Expand Down
146 changes: 146 additions & 0 deletions lib/core/src/addressing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! Sharing a bus: [`Unaddressed`] answers every frame, [`Addressable`]
//! answers only once a host selects it by its unique id.

use tinyboot_protocol::Status;
use tinyboot_protocol::crc::{CRC_INIT, crc16};
use tinyboot_protocol::frame::Frame;

/// How a device shares its bus. `Copy`, so the bootloader hands it to its
/// dispatcher without moving the platform.
pub trait Addressing: Copy {
/// `false` for [`Unaddressed`]; `Select` is then unsupported and every
/// frame is answered.
const ADDRESSABLE: bool;

/// Unique id, 1 to [`MAX_UID_LEN`] bytes, least significant first.
fn uid(&self) -> &'static [u8];

/// Whether this device answers commands other than `Select`.
fn selected(&self) -> bool;

/// Select or deselect this device.
fn set_selected(&mut self, selected: bool);
}

/// One device per bus: every frame is answered, as without addressing.
#[derive(Clone, Copy)]
pub struct Unaddressed;

impl Addressing for Unaddressed {
const ADDRESSABLE: bool = false;

#[inline(always)]
fn uid(&self) -> &'static [u8] {
&[]
}

#[inline(always)]
fn selected(&self) -> bool {
true
}

#[inline(always)]
fn set_selected(&mut self, _: bool) {}
}

/// Many devices per bus. A device answers a `Select` whose prefix its
/// unique id starts with, and stays silent to everything else until such a
/// `Select` selects it. Selection is in RAM, so every reset starts
/// deselected.
/// `F` returns the id; a function item such as `chip_uid` keeps the
/// device's state to the one `selected` byte and lets the id's address and
/// length fold into the code.
#[derive(Clone, Copy)]
pub struct Addressable<const L: usize, F: Fn() -> &'static [u8; L] + Copy> {
uid: F,
selected: bool,
}

impl<const L: usize, F: Fn() -> &'static [u8; L] + Copy> Addressable<L, F> {
/// Address the device by the id `uid` returns (1 to [`MAX_UID_LEN`]
/// bytes, least significant first), typically the chip's factory
/// unique id.
#[inline(always)]
pub const fn new(uid: F) -> Self {
const { assert!(L >= 1 && L <= MAX_UID_LEN) };
Self {
uid,
selected: false,
}
}
}

impl<const L: usize, F: Fn() -> &'static [u8; L] + Copy> Addressing for Addressable<L, F> {
const ADDRESSABLE: bool = true;

#[inline(always)]
fn uid(&self) -> &'static [u8] {
(self.uid)()
}

#[inline(always)]
fn selected(&self) -> bool {
self.selected
}

#[inline(always)]
fn set_selected(&mut self, selected: bool) {
self.selected = selected;
}
}

/// Largest unique id, in bytes.
pub const MAX_UID_LEN: usize = 16;

/// Handle `Select` (`[seed, prefix_len, prefix..]`): select this device
/// when its id starts with the `prefix_len`-bit prefix (bit `k` of an id
/// is `id[k / 8] >> (k % 8) & 1`), then wait its slot,
/// `wait_bytes(crc16(uid) >> (seed % 16) & 15)`, and reply `Ok` with its id;
/// otherwise deselect it. Returns whether to reply.
#[inline(always)]
pub(crate) fn select<A: Addressing, const N: usize>(
a: &mut A,
frame: &mut Frame<N>,
wait_bytes: impl FnOnce(u8),
) -> bool {
let uid = a.uid();
// SAFETY: N >= 18 (Info) covers the 2 + 16 bytes used here.
let data = unsafe { &mut frame.data.raw };
let seed = data[0];
// Bits of the prefix still to compare; any left after the id means a
// prefix longer than the id, which matches nothing.
let mut rem = data[1] as u32;
let mut diff = 0;
let mut i = 0;
// One pass compares the prefix and copies the id over it for the
// reply: byte i + 2 is read before byte i is written.
// Bottom-tested: an id is never empty.
loop {
// SAFETY: i < uid.len() <= MAX_UID_LEN, and i + 2 < 2 + MAX_UID_LEN <= N.
unsafe {
let u = *uid.get_unchecked(i);
let p = *data.get_unchecked(i + 2);
*data.get_unchecked_mut(i) = u;
let n = rem.min(8);
rem -= n;
diff |= (u ^ p) as u32 & (0xFF >> (8 - n));
}
i += 1;
if i == uid.len() {
break;
}
}
let on = diff | rem == 0;
a.set_selected(on);
if !on {
return false;
}
// The seed picks a window of the id's hash, not the CRC's initial
// value: CRC is linear, so two ids' CRCs differ by the same bits under
// every initial value, and a pair drawing one slot would draw it for
// every seed.
wait_bytes((crc16(CRC_INIT, uid) >> (seed & 15)) as u8 & 15);
frame.len = uid.len() as u16;
frame.status = Status::Ok;
true
}
72 changes: 54 additions & 18 deletions lib/core/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! App-side tinyboot client. Handles boot confirmation and responds to
//! Info/Reset commands so the CLI can drive updates without physical access.

use crate::addressing::{self, Addressing, Unaddressed};
use crate::traits::{BootCtl, BootMetaStore, BootState, RunMode};
use tinyboot_protocol::frame::{DEFAULT_PAYLOAD, Frame, InfoData};
use tinyboot_protocol::{BaudRate, Cmd, ResetFlags, Status};
Expand All @@ -19,22 +20,47 @@ pub struct AppConfig {
pub app_version: u16,
}

/// App-side tinyboot client. `P` is the frame's data capacity.
pub struct App<C: BootCtl, M: BootMetaStore, const P: usize = DEFAULT_PAYLOAD> {
/// App-side tinyboot client. `P` is the frame's data capacity, `A` its
/// bus addressing.
pub struct App<
C: BootCtl,
M: BootMetaStore,
const P: usize = DEFAULT_PAYLOAD,
A: Addressing = Unaddressed,
> {
frame: Frame<P>,
config: AppConfig,
ctl: C,
meta: M,
addressing: A,
wait_bytes: fn(u8),
}

impl<C: BootCtl, M: BootMetaStore, const P: usize> App<C, M, P> {
/// Create a new app client.
impl<C: BootCtl, M: BootMetaStore, const P: usize> App<C, M, P, Unaddressed> {
/// Create an unaddressed app client.
pub fn new(config: AppConfig, ctl: C, meta: M) -> Self {
Self::with_addressing(config, ctl, meta, Unaddressed, |_| {})
}
}

impl<C: BootCtl, M: BootMetaStore, const P: usize, A: Addressing> App<C, M, P, A> {
/// Create an app client with the given bus addressing. `wait_bytes(n)`
/// waits about `n` byte-times at the bus baud before a `Select`
/// reply (see [`Transport::wait_bytes`](crate::traits::Transport::wait_bytes)).
pub fn with_addressing(
config: AppConfig,
ctl: C,
meta: M,
addressing: A,
wait_bytes: fn(u8),
) -> Self {
Self {
frame: Frame::default(),
config,
ctl,
meta,
addressing,
wait_bytes,
}
}

Expand Down Expand Up @@ -73,13 +99,7 @@ impl<C: BootCtl, M: BootMetaStore, const P: usize> App<C, M, P> {
Ok(s) => s,
Err(_) => return,
};
if status == Status::Ok {
self.handle_cmd();
} else {
self.frame.len = 0;
self.frame.status = status;
}
if self.frame.cmd != Cmd::Reset {
if self.process(status) {
let _ = self.frame.send(tx);
let _ = tx.flush();
}
Expand All @@ -95,18 +115,34 @@ impl<C: BootCtl, M: BootMetaStore, const P: usize> App<C, M, P> {
Ok(s) => s,
Err(_) => return,
};
if status == Status::Ok {
self.handle_cmd();
} else {
self.frame.len = 0;
self.frame.status = status;
}
if self.frame.cmd != Cmd::Reset {
if self.process(status) {
let _ = self.frame.send_async(tx).await;
let _ = tx.flush().await;
}
}

/// Act on a read frame; returns whether to reply. Same addressing
/// rules as the bootloader's dispatcher.
fn process(&mut self, status: Status) -> bool {
if A::ADDRESSABLE {
if status != Status::Ok || self.frame.status != Status::Request {
return false;
}
if self.frame.cmd == Cmd::Select {
return addressing::select(&mut self.addressing, &mut self.frame, self.wait_bytes);
}
if !self.addressing.selected() {
return false;
}
} else if status != Status::Ok {
self.frame.len = 0;
self.frame.status = status;
return true;
}
self.handle_cmd();
self.frame.cmd != Cmd::Reset
}

fn handle_cmd(&mut self) {
self.frame.status = Status::Ok;
match self.frame.cmd {
Expand Down
40 changes: 33 additions & 7 deletions lib/core/src/core.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,62 @@
use crate::addressing::{Addressing, Unaddressed};
use crate::platform::Platform;
use crate::protocol;
use crate::traits::{BootCtl, BootMetaStore, BootState, RunMode, Storage, Transport};
use tinyboot_protocol::frame::DEFAULT_PAYLOAD;

/// Bootloader entry point. Validates the app and either hands off or enters
/// the protocol loop.
pub struct Core<T, S, B, C, const BUF: usize, const P: usize = DEFAULT_PAYLOAD>
pub struct Core<T, S, B, C, const BUF: usize, const P: usize = DEFAULT_PAYLOAD, A = Unaddressed>
where
T: Transport,
S: Storage,
B: BootMetaStore,
C: BootCtl,
{
platform: Platform<T, S, B, C>,
addressing: A,
}

impl<T, S, B, C, const BUF: usize, const P: usize> Core<T, S, B, C, BUF, P>
impl<T, S, B, C, const BUF: usize, const P: usize> Core<T, S, B, C, BUF, P, Unaddressed>
where
T: Transport,
S: Storage,
B: BootMetaStore,
C: BootCtl,
{
/// Wrap a platform in a bootloader core.
/// Wrap a platform in an unaddressed bootloader core.
#[inline(always)]
pub fn new(platform: Platform<T, S, B, C>) -> Self {
Core { platform }
Self::with_addressing(platform, Unaddressed)
}
}

impl<T, S, B, C, const BUF: usize, const P: usize, A> Core<T, S, B, C, BUF, P, A>
where
T: Transport,
S: Storage,
B: BootMetaStore,
C: BootCtl,
A: Addressing,
{
/// Wrap a platform in a bootloader core with the given bus addressing.
#[inline(always)]
pub fn with_addressing(platform: Platform<T, S, B, C>, addressing: A) -> Self {
Core {
platform,
addressing,
}
}

/// Run the bootloader. Does not return.
#[inline(always)]
pub fn run(mut self) -> ! {
// Taken before any call: `self` then escapes, and a copy read
// later would be a load instead of a constant.
let addressing = self.addressing;
match self.check_boot_state() {
Ok(RunMode::HandOff) => self.platform.ctl.hand_off(),
Ok(RunMode::Service) | Err(_) => self.enter_bootloader(),
Ok(RunMode::Service) | Err(_) => self.enter_bootloader(addressing),
}
}

Expand Down Expand Up @@ -80,8 +103,11 @@ where
}

#[inline(always)]
fn enter_bootloader(&mut self) -> ! {
let mut d = protocol::Dispatcher::<_, _, _, _, BUF, P>::new(&mut self.platform);
fn enter_bootloader(&mut self, addressing: A) -> ! {
let mut d = protocol::Dispatcher::<_, _, _, _, BUF, P, A>::with_addressing(
&mut self.platform,
addressing,
);

loop {
let _ = d.dispatch();
Expand Down
2 changes: 2 additions & 0 deletions lib/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//! Boot state machine, protocol dispatcher, app validation. Platform-specific
//! behaviour is injected via the traits in [`traits`].

/// Bus addressing: one device per bus, or many selected by unique id.
pub mod addressing;
/// App-side tinyboot client.
pub mod app;
/// Boot state machine and entry point.
Expand Down
Loading
Loading