rigless 0.1.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The chips a board puts on a part's buses, and the seams they attach through.
//!
//! A peripheral of an MCU belongs to `soc`, and a chip on a board belongs here. The two meet at a
//! trait per bus kind. The controller calls it and knows nothing of what answers. The chip
//! implements it and knows nothing of registers. That's the whole arrangement, and it's why a host
//! test can drive a card with no MCU in sight, and a firmware can drive it with no test in sight.
//!
//! A chip owns its own document, and no model here names an SoC. The same card belongs on any part
//! with a card interface. That's why it's a chip and not a peripheral.
//!
//! If your bench wants something known on a bus, and not the chip that will eventually be there,
//! use a stand-in the controller's own module keeps. A stand-in answers something recognisable, so a
//! controller that never reached it gets caught. A chip answers what the part it models would
//! answer. They're different things, and they live apart.

pub mod ak8963;
pub mod fram;
pub mod i2c;
pub mod icm20608;
mod invensense;
pub mod lis3mdl;
pub mod memory_card;
pub mod mpu9250;
pub mod ms5611;
pub mod nmea;
pub mod sd;
pub mod spi;
pub mod uart;

use crate::snapshot::{Error, Reader, Store, Writer};

/// A chip's place on a bus: the chip, whether it answers, how many transactions it's had, and the
/// one-shot faults waiting for it.
///
/// A controller holds each chip it carries in one and reaches it through [`Place::answering`], which
/// is `None` while the place is silent. So a silent chip takes the path the controller already takes
/// with nothing there, whatever the bus: a select reads what it reads with nothing selected, an
/// address isn't acknowledged, a serial line rests at mark, a card never answers a command. What a
/// seam does with a silent chip beyond that is the seam's, like a serial line losing what the far end
/// says. The board that hung the chip reaches it through [`Place::chip_mut`] whatever the flag,
/// because what the chip senses goes on while it's silent. That's a cut connection, not a chip that
/// lost power: it keeps its state and answers as it did once the flag clears. The chip never learns
/// it was silenced.
///
/// The controller marks where each transaction with the chip begins and ends, with [`Place::begin`]
/// and [`Place::release`], and passes every word the chip answers through [`Place::answered`]. What
/// a transaction is belongs to the bus: a select to its deselect, a card's command, or a word on a
/// serial line, where nothing else begins or ends one. That's what [`Placed`] counts and where its
/// one-shot faults land.
#[derive(Debug)]
pub struct Place<T: ?Sized> {
    chip: Box<T>,
    silent: bool,
    /// A silence asked for while the chip was in a transaction, which takes effect when it ends.
    waiting: Option<bool>,
    /// Whether a transaction with the chip is under way.
    open: bool,
    /// How many transactions with the chip have ended.
    ended: u64,
    /// The bits the next word the chip answers crosses with flipped.
    flip: u64,
    /// How many of the transactions still to begin the chip loses.
    lose: u32,
    /// Whether it's losing the one under way.
    losing: bool,
}

/// A place keeps whether it answers, what it's counted and what's waiting for it, and the chip keeps
/// itself.
impl<T: Store + ?Sized> Store for Place<T> {
    fn store(&self, to: &mut Writer) -> Result<(), Error> {
        let Self {
            chip,
            silent,
            waiting,
            open,
            ended,
            flip,
            lose,
            losing,
        } = self;
        to.put(silent)?;
        to.put(waiting)?;
        to.put(open)?;
        to.put(ended)?;
        to.put(flip)?;
        to.put(lose)?;
        to.put(losing)?;
        chip.store(to)
    }

    fn load(&mut self, from: &mut Reader) -> Result<(), Error> {
        let Self {
            chip,
            silent,
            waiting,
            open,
            ended,
            flip,
            lose,
            losing,
        } = self;
        *silent = from.take()?;
        *waiting = from.take()?;
        *open = from.take()?;
        *ended = from.take()?;
        *flip = from.take()?;
        *lose = from.take()?;
        *losing = from.take()?;
        chip.load(from)
    }
}

impl<T: ?Sized> Place<T> {
    /// A place with `chip` in it, answering.
    #[must_use]
    pub fn new(chip: Box<T>) -> Self {
        Self {
            chip,
            silent: false,
            waiting: None,
            open: false,
            ended: 0,
            flip: 0,
            lose: 0,
            losing: false,
        }
    }

    /// The chip, for the controller to call, or `None` while the place is silent or losing the
    /// transaction under way.
    pub fn answering(&mut self) -> Option<&mut T> {
        (!self.silent && !self.losing).then_some(&mut *self.chip)
    }

    /// The chip, whether or not it answers.
    #[must_use]
    pub fn chip(&self) -> &T {
        &self.chip
    }

    /// The chip, whether or not it answers, for the board that hung it.
    pub fn chip_mut(&mut self) -> &mut T {
        &mut self.chip
    }

    /// Stops the chip answering, or lets it answer again, from now on.
    pub fn silence(&mut self, silent: bool) {
        self.silent = silent;
        self.waiting = None;
    }

    /// The same, once the transaction the chip is in ends at [`Place::release`], so a transaction is
    /// never cut in half and a chip that comes back starts at the next one.
    pub fn silence_at_release(&mut self, silent: bool) {
        self.waiting = Some(silent);
    }

    /// A transaction with the chip begins, whether or not it answers. Call it before reaching the
    /// chip for it, so a drop waiting for it takes it whole.
    pub fn begin(&mut self) {
        self.open = true;
        if self.lose > 0 {
            self.lose -= 1;
            self.losing = true;
        }
    }

    /// The transaction the chip was in has ended, so it's counted and a silence waiting for it takes
    /// effect. A release with none under way counts nothing.
    pub fn release(&mut self) {
        if std::mem::take(&mut self.open) {
            self.ended += 1;
        }
        self.losing = false;
        if let Some(silent) = self.waiting.take() {
            self.silent = silent;
        }
    }

    /// A word the chip answered, as it crosses to the controller: with the bits a flip waits on
    /// flipped, once. Bits past the word's width flip nothing.
    pub fn answered(&mut self, word: u32) -> u32 {
        let flip = std::mem::take(&mut self.flip) & u64::from(u32::MAX);
        word ^ u32::try_from(flip).expect("masked to the word's width")
    }

    /// A word the chip sent on a line where each word is a transaction of its own: counted, flipped
    /// by a flip waiting for it, and `None` where a drop takes it, so it's lost.
    pub fn sent(&mut self, word: u32) -> Option<u32> {
        self.begin();
        let word = (!self.losing).then(|| self.answered(word));
        self.release();
        word
    }
}

/// A place reached without its chip's type: what a trigger counts on it and the one-shot faults it
/// fires there. A board hands one out for each chip it names.
pub trait Placed {
    /// How many transactions with the chip have ended.
    fn ended(&self) -> u64;

    /// Flips `mask`'s bits in the next word the chip answers. Two flips before it compose, so a bit
    /// flipped twice crosses as it was.
    fn flip(&mut self, mask: u64);

    /// Loses the next transaction to begin with the chip, as if it were silent for that one alone.
    /// The one under way, if any, carries on as it was, and a second drop loses the one after.
    fn drop_next(&mut self);
}

impl<T: ?Sized> Placed for Place<T> {
    fn ended(&self) -> u64 {
        self.ended
    }

    fn flip(&mut self, mask: u64) {
        self.flip ^= mask;
    }

    fn drop_next(&mut self) {
        self.lose += 1;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_silent_place_keeps_its_chip_from_the_controller_and_not_from_the_board() {
        let mut place = Place::new(Box::new(7_u8));
        assert_eq!(place.answering().copied(), Some(7));
        place.silence(true);
        assert_eq!(place.answering(), None);
        *place.chip_mut() = 8;
        assert_eq!(*place.chip(), 8, "the board still reaches it");
        place.silence(false);
        assert_eq!(place.answering().copied(), Some(8), "and it answers again");
    }

    #[test]
    fn a_place_counts_the_transactions_that_end_and_a_release_with_none_open_counts_nothing() {
        let mut place = Place::new(Box::new(7_u8));
        place.release();
        assert_eq!(place.ended(), 0);
        place.begin();
        assert_eq!(place.ended(), 0, "not until it ends");
        place.release();
        place.release();
        assert_eq!(place.ended(), 1);
    }

    #[test]
    fn a_flip_takes_the_next_word_alone() {
        let mut place = Place::new(Box::new(7_u8));
        assert_eq!(place.answered(0x1234), 0x1234);
        place.flip(0x0101);
        assert_eq!(place.answered(0x1234), 0x1335);
        assert_eq!(place.answered(0x1234), 0x1234, "once");
    }

    #[test]
    fn a_drop_takes_the_next_transaction_whole_and_lets_the_one_under_way_finish() {
        let mut place = Place::new(Box::new(7_u8));
        place.begin();
        place.drop_next();
        assert_eq!(place.answering().copied(), Some(7), "the one under way");
        place.release();
        place.begin();
        assert_eq!(place.answering(), None, "the next is lost");
        place.release();
        place.begin();
        assert_eq!(place.answering().copied(), Some(7), "and only that one");
        place.release();
        assert_eq!(place.ended(), 3, "a lost transaction still happened");
    }

    #[test]
    fn a_drop_during_a_lost_transaction_loses_the_next_too_and_two_flips_compose() {
        let mut place = Place::new(Box::new(7_u8));
        place.drop_next();
        place.begin();
        place.drop_next();
        assert_eq!(place.answering(), None, "the one under way stays lost");
        place.release();
        place.begin();
        assert_eq!(place.answering(), None, "and so is the next");
        place.release();
        place.begin();
        assert_eq!(place.answering().copied(), Some(7));
        place.flip(0x3);
        place.flip(0x1);
        assert_eq!(place.answered(0), 0x2, "a bit flipped twice is as it was");
    }

    #[test]
    fn a_word_on_a_serial_line_is_a_transaction_that_a_drop_loses_and_a_flip_changes() {
        let mut place = Place::new(Box::new(7_u8));
        assert_eq!(place.sent(0x41), Some(0x41));
        place.drop_next();
        place.flip(0x01);
        assert_eq!(place.sent(0x41), None, "lost, and the flip waits");
        assert_eq!(place.sent(0x41), Some(0x40));
        assert_eq!(place.ended(), 3);
    }

    #[test]
    fn a_silence_asked_for_mid_transaction_waits_for_its_release() {
        let mut place = Place::new(Box::new(7_u8));
        place.silence_at_release(true);
        assert_eq!(
            place.answering().copied(),
            Some(7),
            "the transaction finishes"
        );
        place.release();
        assert_eq!(place.answering(), None);
        place.silence_at_release(false);
        place.silence(true);
        place.release();
        assert_eq!(
            place.answering(),
            None,
            "a silence from now on replaces one waiting"
        );
    }
}