diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d49d41..c6bd828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 = `, 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. @@ -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` and `Data` 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`. diff --git a/lib/core/src/addressing.rs b/lib/core/src/addressing.rs new file mode 100644 index 0000000..e07ce0f --- /dev/null +++ b/lib/core/src/addressing.rs @@ -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 &'static [u8; L] + Copy> { + uid: F, + selected: bool, +} + +impl &'static [u8; L] + Copy> Addressable { + /// 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 &'static [u8; L] + Copy> Addressing for Addressable { + 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: &mut A, + frame: &mut Frame, + 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 +} diff --git a/lib/core/src/app.rs b/lib/core/src/app.rs index ecf56eb..fef9679 100644 --- a/lib/core/src/app.rs +++ b/lib/core/src/app.rs @@ -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}; @@ -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 { +/// 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

, config: AppConfig, ctl: C, meta: M, + addressing: A, + wait_bytes: fn(u8), } -impl App { - /// Create a new app client. +impl App { + /// Create an unaddressed app client. pub fn new(config: AppConfig, ctl: C, meta: M) -> Self { + Self::with_addressing(config, ctl, meta, Unaddressed, |_| {}) + } +} + +impl App { + /// 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, } } @@ -73,13 +99,7 @@ impl App { 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(); } @@ -95,18 +115,34 @@ impl App { 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 { diff --git a/lib/core/src/core.rs b/lib/core/src/core.rs index dfe13fa..6ca2a63 100644 --- a/lib/core/src/core.rs +++ b/lib/core/src/core.rs @@ -1,3 +1,4 @@ +use crate::addressing::{Addressing, Unaddressed}; use crate::platform::Platform; use crate::protocol; use crate::traits::{BootCtl, BootMetaStore, BootState, RunMode, Storage, Transport}; @@ -5,7 +6,7 @@ use tinyboot_protocol::frame::DEFAULT_PAYLOAD; /// Bootloader entry point. Validates the app and either hands off or enters /// the protocol loop. -pub struct Core +pub struct Core where T: Transport, S: Storage, @@ -13,27 +14,49 @@ where C: BootCtl, { platform: Platform, + addressing: A, } -impl Core +impl Core 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) -> Self { - Core { platform } + Self::with_addressing(platform, Unaddressed) + } +} + +impl Core +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, 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), } } @@ -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(); diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index dd7c9c6..13e3bfc 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -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. diff --git a/lib/core/src/protocol.rs b/lib/core/src/protocol.rs index 2e770ab..efd36ae 100644 --- a/lib/core/src/protocol.rs +++ b/lib/core/src/protocol.rs @@ -1,3 +1,4 @@ +use crate::addressing::{self, Addressing, Unaddressed}; use crate::platform::Platform; use crate::traits::{BootCtl, BootMetaStore, BootState, RunMode, Storage, Transport}; use tinyboot_protocol::crc::{CRC_INIT, crc16}; @@ -18,6 +19,7 @@ pub struct Dispatcher< C: BootCtl, const BUF: usize, const P: usize = DEFAULT_PAYLOAD, + A: Addressing = Unaddressed, > { /// Platform peripherals. pub platform: &'a mut Platform, @@ -28,6 +30,8 @@ pub struct Dispatcher< fill: usize, /// Address of the next sequential write; [`ANY`] after a flush. next: u32, + /// Bus addressing: [`Unaddressed`] or [`Addressable`](crate::addressing::Addressable). + pub addressing: A, } /// No write is pending: the next may start at any page-aligned address. @@ -37,16 +41,34 @@ const ANY: u32 = u32::MAX; struct Page(core::mem::MaybeUninit<[u8; N]>); impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usize, const P: usize> - Dispatcher<'a, T, S, B, C, BUF, P> + Dispatcher<'a, T, S, B, C, BUF, P, Unaddressed> { - /// Create a new dispatcher over the given platform. + /// Create an unaddressed dispatcher over the given platform. pub fn new(platform: &'a mut Platform) -> Self { + Self::with_addressing(platform, Unaddressed) + } +} + +impl< + 'a, + T: Transport, + S: Storage, + B: BootMetaStore, + C: BootCtl, + const BUF: usize, + const P: usize, + A: Addressing, +> Dispatcher<'a, T, S, B, C, BUF, P, A> +{ + /// Create a dispatcher over the given platform and bus addressing. + pub fn with_addressing(platform: &'a mut Platform, addressing: A) -> Self { Self { platform, frame: Frame::default(), page: Page(core::mem::MaybeUninit::uninit()), fill: 0, next: ANY, + addressing, } } @@ -242,50 +264,80 @@ impl<'a, T: Transport, S: Storage, B: BootMetaStore, C: BootCtl, const BUF: usiz } /// Read a frame, dispatch, send the response. Err only on transport IO; - /// frame-level errors (bad CRC) are silently skipped. + /// frame-level errors (bad CRC) are silently skipped. An addressable + /// device replies only to a `Select` matching it and, while selected, + /// to other requests; it never answers a frame it cannot read or + /// another device's reply. pub fn dispatch(&mut self) -> Result<(), ReadError> { let status = self.frame.read(&mut self.platform.transport)?; - if status == Status::Ok { - let data_len = self.frame.len as usize; - let addr = self.frame.addr(); - let flags = self.frame.flags; - let capacity = self.platform.storage.capacity() as u32; - let state = self.platform.boot_meta.boot_state(); - self.frame.len = 0; - self.frame.status = Status::Ok; - - let cmd = self.frame.cmd; - if cmd == Cmd::Write { - // SAFETY: cmd is Write, so flags.write is the active variant. - self.cmd_write(addr, data_len, unsafe { flags.write }, capacity, state); - } else if cmd == Cmd::Erase { - self.cmd_erase(addr, capacity, state); - } else if cmd == Cmd::Info { - self.cmd_info(capacity); - } else if cmd == Cmd::Verify { - self.cmd_verify(addr, capacity, state); + let reply = if A::ADDRESSABLE { + if status != Status::Ok || self.frame.status != Status::Request { + false + } else if self.frame.cmd == Cmd::Select { + let transport = &mut self.platform.transport; + addressing::select(&mut self.addressing, &mut self.frame, |n| { + transport.wait_bytes(n) + }) + } else if self.addressing.selected() { + self.handle_cmd(); + true } else { - // SAFETY: cmd is Reset, so flags.reset is the active variant. - let flags = unsafe { flags.reset }; - // A reset to the app skips the bootloader's META check on - // chips that hand off by run mode, so a half-written image - // would run. - if state == BootState::Updating && !flags.contains(ResetFlags::BOOTLOADER) { - self.frame.status = Status::Unsupported; - } else { - self.cmd_reset(addr, flags); - } + false } - } else { + } else if status != Status::Ok { self.frame.len = 0; self.frame.status = status; - } + true + } else { + self.handle_cmd(); + true + }; + if !reply { + return Ok(()); + } self.frame .send(&mut self.platform.transport) .map_err(|_| ReadError)?; embedded_io::Write::flush(&mut self.platform.transport).map_err(|_| ReadError) } + + #[inline(always)] + fn handle_cmd(&mut self) { + let data_len = self.frame.len as usize; + let addr = self.frame.addr(); + let flags = self.frame.flags; + let capacity = self.platform.storage.capacity() as u32; + let state = self.platform.boot_meta.boot_state(); + self.frame.len = 0; + self.frame.status = Status::Ok; + + let cmd = self.frame.cmd; + if cmd == Cmd::Write { + // SAFETY: cmd is Write, so flags.write is the active variant. + self.cmd_write(addr, data_len, unsafe { flags.write }, capacity, state); + } else if cmd == Cmd::Erase { + self.cmd_erase(addr, capacity, state); + } else if cmd == Cmd::Info { + self.cmd_info(capacity); + } else if cmd == Cmd::Verify { + self.cmd_verify(addr, capacity, state); + } else if A::ADDRESSABLE || cmd == Cmd::Reset { + // SAFETY: cmd is Reset (an addressable device took Select + // above), so flags.reset is the active variant. + let flags = unsafe { flags.reset }; + // A reset to the app skips the bootloader's META check on + // chips that hand off by run mode, so a half-written image + // would run. + if state == BootState::Updating && !flags.contains(ResetFlags::BOOTLOADER) { + self.frame.status = Status::Unsupported; + } else { + self.cmd_reset(addr, flags); + } + } else { + self.frame.status = Status::Unsupported; + } + } } diff --git a/lib/core/src/traits.rs b/lib/core/src/traits.rs index 1c4260e..a205611 100644 --- a/lib/core/src/traits.rs +++ b/lib/core/src/traits.rs @@ -35,7 +35,16 @@ impl BootState { } /// Firmware transfer transport. -pub trait Transport: embedded_io::Read + embedded_io::Write {} +pub trait Transport: embedded_io::Read + embedded_io::Write { + /// Wait about `n` byte-times (10 bit-times each) at the current baud. + /// An addressable device waits a draw of 0 to 15 before its `Select` + /// reply, so devices running the same code do not answer in unison. + /// The default returns at once; give a shared bus a real wait (it may + /// overshoot). + fn wait_bytes(&mut self, n: u8) { + let _ = n; + } +} /// App-region flash. [`as_slice`](Storage::as_slice) gives zero-copy reads /// (flash is memory-mapped on supported chips). diff --git a/lib/core/tests/addressing.rs b/lib/core/tests/addressing.rs new file mode 100644 index 0000000..2468fcb --- /dev/null +++ b/lib/core/tests/addressing.rs @@ -0,0 +1,418 @@ +//! Addressable devices sharing one simulated wire: a Select walk finds +//! every id, and a device speaks only when selected. + +mod common; + +use common::*; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::OnceLock; +use tinyboot_core::addressing::{Addressable, Addressing}; +use tinyboot_core::app::{App, AppConfig}; +use tinyboot_core::protocol::Dispatcher; +use tinyboot_core::traits::BootState; +use tinyboot_protocol::crc::{CRC_INIT, crc16}; +use tinyboot_protocol::frame::{DEFAULT_PAYLOAD, Flags, Frame}; +use tinyboot_protocol::{Cmd, ResetFlags, Status}; + +// This bootloader's image tail: no compatibility key. +tinyboot_core::boot_version!(); + +type Uid = fn() -> &'static [u8; 12]; +type Dev<'a> = Dispatcher< + 'a, + MockTransport, + MockStorage, + MockBootMeta, + MockBootCtl, + TEST_BUF_SIZE, + DEFAULT_PAYLOAD, + Addressable<12, Uid>, +>; + +/// Ids sharing long prefixes, so the walk descends deep: 0 and 1 differ +/// only in their last bit, 2 and 3 in their first. +static UIDS: [[u8; 12]; 6] = [ + [ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x01, 0x23, 0x45, 0x07, + ], + [ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x01, 0x23, 0x45, 0x87, + ], + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ], + [ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0x7E, 0x01, 0x23, 0x45, 0x07, + ], +]; + +fn uid() -> &'static [u8; 12] { + &UIDS[K] +} + +const UID_FNS: [Uid; 6] = [uid::<0>, uid::<1>, uid::<2>, uid::<3>, uid::<4>, uid::<5>]; + +fn platforms(n: usize) -> Vec { + (0..n).map(|_| platform(BootState::Idle)).collect() +} + +fn devices<'a>(ps: &'a mut [TestPlatform], uids: &[Uid]) -> Vec> { + ps.iter_mut() + .zip(uids) + .map(|(p, &u)| Dispatcher::with_addressing(p, Addressable::new(u))) + .collect() +} + +/// Load a frame as the host or another device would send it. +fn load(t: &mut MockTransport, cmd: Cmd, status: Status, flags: u8, addr: u32, data: &[u8]) { + let mut f = Frame::::default(); + f.cmd = cmd; + f.status = status; + f.set_addr(addr); + f.flags = Flags { raw: flags }; + f.len = data.len() as u16; + unsafe { f.data.raw[..data.len()].copy_from_slice(data) }; + let mut tmp = MockTransport::new(); + f.send(&mut tmp).unwrap(); + t.rx_buf[..tmp.tx_pos].copy_from_slice(&tmp.tx_buf[..tmp.tx_pos]); + t.rx_len = tmp.tx_pos; + t.rx_pos = 0; + t.tx_pos = 0; + t.waited = None; +} + +/// The device's reply, read back as a host would; `None` if it was silent. +fn reply(t: &MockTransport) -> Option { + if t.tx_pos == 0 { + return None; + } + let mut wire = MockTransport::new(); + wire.rx_buf[..t.tx_pos].copy_from_slice(&t.tx_buf[..t.tx_pos]); + wire.rx_len = t.tx_pos; + let mut f = Frame::default(); + assert_eq!(f.read(&mut wire).unwrap(), Status::Ok); + assert_eq!(wire.rx_pos, wire.rx_len, "trailing bytes after the reply"); + Some(f) +} + +/// `Select` data for a prefix of `bits` (bit k = id[k / 8] >> (k % 8) & 1). +fn select_data(seed: u8, bits: &[bool]) -> Vec { + let mut d = vec![seed, bits.len() as u8]; + d.resize(2 + bits.len().div_ceil(8), 0); + for (k, &b) in bits.iter().enumerate() { + d[2 + k / 8] |= (b as u8) << (k % 8); + } + d +} + +fn bits_of(uid: &[u8]) -> Vec { + (0..uid.len() * 8) + .map(|k| uid[k / 8] >> (k % 8) & 1 != 0) + .collect() +} + +enum Heard { + Nothing, + Clean(Frame), + Garble, +} + +/// Send one host request to every device; replies from two or more at once +/// garble, as on a real wire. +fn exchange(devs: &mut [Dev], cmd: Cmd, flags: u8, addr: u32, data: &[u8]) -> Heard { + let mut heard = Vec::new(); + for d in devs.iter_mut() { + load( + &mut d.platform.transport, + cmd, + Status::Request, + flags, + addr, + data, + ); + d.dispatch().unwrap(); + heard.extend(reply(&d.platform.transport)); + } + match heard.len() { + 0 => Heard::Nothing, + 1 => Heard::Clean(heard.pop().unwrap()), + _ => Heard::Garble, + } +} + +fn select(devs: &mut [Dev], seed: u8, bits: &[bool]) -> Heard { + exchange(devs, Cmd::Select, 0, 0, &select_data(seed, bits)) +} + +fn uid_of(f: &Frame) -> Vec { + unsafe { f.data.raw[..f.len as usize].to_vec() } +} + +fn walk(devs: &mut [Dev], prefix: &mut Vec, seed: &mut u8, found: &mut Vec>) { + *seed = seed.wrapping_add(1); + match select(devs, *seed, prefix) { + Heard::Nothing => {} + Heard::Clean(f) => { + let id = uid_of(&f); + assert!(bits_of(&id).starts_with(prefix), "reply outside the prefix"); + found.push(id); + } + Heard::Garble => { + assert!(prefix.len() < 96, "two devices with one id"); + for b in [false, true] { + prefix.push(b); + walk(devs, prefix, seed, found); + prefix.pop(); + } + } + } +} + +#[test] +fn select_walk_finds_every_uid() { + let mut ps = platforms(UIDS.len()); + let mut devs = devices(&mut ps, &UID_FNS); + let mut found = Vec::new(); + walk(&mut devs, &mut Vec::new(), &mut 0, &mut found); + found.sort(); + let mut want: Vec> = UIDS.iter().map(|u| u.to_vec()).collect(); + want.sort(); + assert_eq!(found, want); +} + +#[test] +fn full_uid_selects_one_and_deselects_the_rest() { + let mut ps = platforms(3); + let mut devs = devices(&mut ps, &UID_FNS[..3]); + + for k in [1, 0, 2] { + let Heard::Clean(f) = select(&mut devs, 7, &bits_of(&UIDS[k])) else { + panic!("select of device {k} not answered by one device"); + }; + assert_eq!(f.status, Status::Ok); + assert_eq!(uid_of(&f), UIDS[k]); + for (j, d) in devs.iter().enumerate() { + assert_eq!(d.addressing.selected(), j == k); + } + let Heard::Clean(f) = exchange(&mut devs, Cmd::Info, 0, 0, &[]) else { + panic!("info not answered by one device"); + }; + assert_eq!(f.status, Status::Ok); + } +} + +#[test] +fn empty_prefix_selects_all_and_too_long_a_prefix_none() { + let mut ps = platforms(3); + let mut devs = devices(&mut ps, &UID_FNS[..3]); + + assert!(matches!(select(&mut devs, 0, &[]), Heard::Garble)); + assert!(devs.iter().all(|d| d.addressing.selected())); + + // 97 bits: longer than any id, so it deselects everyone. + let data = [0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + assert!(matches!( + exchange(&mut devs, Cmd::Select, 0, 0, &data), + Heard::Nothing + )); + assert!(devs.iter().all(|d| !d.addressing.selected())); +} + +#[test] +fn unselected_devices_never_transmit() { + let mut ps = platforms(3); + let mut devs = devices(&mut ps, &UID_FNS[..3]); + + for cmd in [Cmd::Info, Cmd::Erase, Cmd::Write, Cmd::Verify, Cmd::Reset] { + assert!(matches!( + exchange(&mut devs, cmd, 0, 0, &[0, 0]), + Heard::Nothing + )); + } + // A prefix matching none of them. + let mut bits = bits_of(&UIDS[0]); + bits[94] = !bits[94]; + assert!(matches!(select(&mut devs, 0, &bits), Heard::Nothing)); + + for d in devs.iter_mut() { + let t = &mut d.platform.transport; + // Bad CRC. + load(t, Cmd::Info, Status::Request, 0, 0, &[]); + t.rx_buf[t.rx_len - 1] ^= 1; + d.dispatch().unwrap(); + assert!(reply(&d.platform.transport).is_none()); + // Unknown command. + let t = &mut d.platform.transport; + load(t, Cmd::Info, Status::Request, 0, 0, &[]); + t.rx_buf[2] = 0x06; + d.dispatch().unwrap(); + assert!(reply(&d.platform.transport).is_none()); + // Payload overflow. + let t = &mut d.platform.transport; + load(t, Cmd::Info, Status::Request, 0, 0, &[]); + t.rx_buf[8..10].copy_from_slice(&(DEFAULT_PAYLOAD as u16 + 1).to_le_bytes()); + d.dispatch().unwrap(); + assert!(reply(&d.platform.transport).is_none()); + } +} + +#[test] +fn selected_device_ignores_bad_frames_and_other_replies() { + let mut ps = platforms(1); + let mut devs = devices(&mut ps, &UID_FNS[..1]); + assert!(matches!(select(&mut devs, 0, &[]), Heard::Clean(_))); + let d = &mut devs[0]; + + let t = &mut d.platform.transport; + load(t, Cmd::Info, Status::Request, 0, 0, &[]); + t.rx_buf[t.rx_len - 1] ^= 1; + d.dispatch().unwrap(); + assert!(reply(&d.platform.transport).is_none()); + + // Another device's Info reply on the shared wire. + load( + &mut d.platform.transport, + Cmd::Info, + Status::Ok, + 0, + 0, + &[0; 18], + ); + d.dispatch().unwrap(); + assert!(reply(&d.platform.transport).is_none()); + assert!(d.addressing.selected()); +} + +/// Two ids drawing the same slot under seed 0. +fn colliding_pair() -> &'static [[u8; 12]; 2] { + static PAIR: OnceLock<[[u8; 12]; 2]> = OnceLock::new(); + PAIR.get_or_init(|| { + let id = |k: u8| [k, 0x5A, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x33]; + let slot = |u: &[u8]| crc16(CRC_INIT, u) & 15; + let b = (1..=255).find(|&k| slot(&id(k)) == slot(&id(0))).unwrap(); + [id(0), id(b)] + }) +} + +fn pair0() -> &'static [u8; 12] { + &colliding_pair()[0] +} + +fn pair1() -> &'static [u8; 12] { + &colliding_pair()[1] +} + +#[test] +fn unison_replies_are_split_by_the_seed() { + let mut ps = platforms(2); + let mut devs = devices(&mut ps, &[pair0, pair1]); + let mut split = false; + for seed in 0..=255u8 { + assert!(matches!(select(&mut devs, seed, &[]), Heard::Garble)); + let w: Vec = devs + .iter() + .map(|d| d.platform.transport.waited.unwrap()) + .collect(); + assert!(w.iter().all(|&n| n < 16)); + if seed == 0 { + assert_eq!(w[0], w[1], "the pair should collide under seed 0"); + } + split |= w[0] != w[1]; + } + assert!(split, "no seed separates the pair"); +} + +#[test] +fn reset_reply_is_flushed_before_the_reset() { + let mut ps = platforms(1); + let mut devs = devices(&mut ps, &UID_FNS[..1]); + assert!(matches!(select(&mut devs, 0, &[]), Heard::Clean(_))); + let d = &mut devs[0]; + load( + &mut d.platform.transport, + Cmd::Reset, + Status::Request, + ResetFlags::BOOTLOADER.bits(), + 0, + &[], + ); + let flushes = d.platform.transport.flushes; + assert!(catch_unwind(AssertUnwindSafe(|| d.dispatch())).is_err()); + let f = reply(&d.platform.transport).expect("no Reset reply"); + assert_eq!(f.status, Status::Ok); + assert_eq!(d.platform.transport.flushes, flushes + 1); +} + +#[test] +fn unaddressed_bootloader_refuses_select() { + let mut p = platform(BootState::Idle); + let mut d = Dispatcher::<_, _, _, _, TEST_BUF_SIZE>::new(&mut p); + load( + &mut d.platform.transport, + Cmd::Select, + Status::Request, + 0, + 0, + &[0, 0], + ); + d.dispatch().unwrap(); + assert_eq!( + reply(&d.platform.transport).unwrap().status, + Status::Unsupported + ); +} + +static APP_WAITED: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0xFF); + +fn app_wait(n: u8) { + APP_WAITED.store(n, std::sync::atomic::Ordering::Relaxed); +} + +#[test] +fn addressable_app_answers_only_once_selected() { + let config = AppConfig { + capacity: 1024, + erase_size: 64, + boot_version: 1, + boot_compat: 0, + app_version: 2, + }; + let mut app: App = App::with_addressing( + config, + MockBootCtl, + MockBootMeta::new(BootState::Idle), + Addressable::new(UID_FNS[3]), + app_wait, + ); + let mut rx = MockTransport::new(); + let mut tx = MockTransport::new(); + + load(&mut rx, Cmd::Info, Status::Request, 0, 0, &[]); + app.poll(&mut rx, &mut tx); + assert!(reply(&tx).is_none()); + + load( + &mut rx, + Cmd::Select, + Status::Request, + 0, + 0, + &select_data(3, &bits_of(&UIDS[3])), + ); + app.poll(&mut rx, &mut tx); + let f = reply(&tx).expect("no Select reply"); + assert_eq!(uid_of(&f), UIDS[3]); + assert!(APP_WAITED.load(std::sync::atomic::Ordering::Relaxed) < 16); + + tx.tx_pos = 0; + load(&mut rx, Cmd::Info, Status::Request, 0, 0, &[]); + app.poll(&mut rx, &mut tx); + let f = reply(&tx).expect("no Info reply"); + assert_eq!(unsafe { f.data.info }.mode, 1); +} diff --git a/lib/core/tests/common/mod.rs b/lib/core/tests/common/mod.rs index e2b5a58..713c0ba 100644 --- a/lib/core/tests/common/mod.rs +++ b/lib/core/tests/common/mod.rs @@ -17,6 +17,8 @@ pub struct MockTransport { pub rx_pos: usize, pub tx_buf: [u8; 512], pub tx_pos: usize, + /// Last `wait_bytes` argument. + pub waited: Option, pub flushes: usize, } @@ -28,6 +30,7 @@ impl MockTransport { rx_pos: 0, tx_buf: [0; 512], tx_pos: 0, + waited: None, flushes: 0, } } @@ -91,7 +94,11 @@ impl embedded_io::Write for MockTransport { } } -impl Transport for MockTransport {} +impl Transport for MockTransport { + fn wait_bytes(&mut self, n: u8) { + self.waited = Some(n); + } +} // Word-aligned like flash, for the bootloader's word load of an image key. #[repr(C, align(4))] diff --git a/lib/protocol/src/lib.rs b/lib/protocol/src/lib.rs index 3c4d975..0dc8053 100644 --- a/lib/protocol/src/lib.rs +++ b/lib/protocol/src/lib.rs @@ -188,12 +188,19 @@ pub enum Cmd { Verify = 0x03, /// Reset the device. See [`ResetFlags`]. Reset = 0x04, + /// Select addressable devices by unique id prefix: data `[seed, + /// prefix_len, prefix..]`, `prefix_len` in bits. Every device whose id + /// starts with the prefix is selected and replies `Ok` with its id after + /// a slot drawn from `seed`; every other device is deselected and + /// silent. A full id selects one device, an empty prefix every device, + /// a `prefix_len` longer than any id none. + Select = 0x05, } impl Cmd { /// Returns true if `b` is a valid command code. pub fn is_valid(b: u8) -> bool { - b <= 0x04 + b <= 0x05 } } @@ -258,8 +265,8 @@ mod tests { #[test] 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::Select as u8)); + assert!(!Cmd::is_valid(0x06)); assert!(!Cmd::is_valid(0xFF)); }