rigless 0.0.4

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; needs the rigless-sidecar package from rigless-labs/qemu
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: Apache-2.0
//! The STM32F427: its peripheral models, one module per generated register layout, and the MCU that
//! is made of them.
//!
//! A model is a building block. It is written against one register layout and knows nothing of any
//! part or board, so a second part of the family carrying the same layout has it for free — the
//! serial interface here is `Spi`, once, and this part holds six of it.
//!
//! [`Stm32f427`] is the whole microcontroller, and it holds its models as **fields**: `mcu.rcc`,
//! `mcu.spi1`, `mcu.tim2`. Reaching one is a field access that cannot fail. It runs on its own,
//! with no board: [`Stm32f427::new`] gives a part fresh out of reset, running on its internal
//! oscillator, with nothing on its pins.
//!
//! A board is that MCU plus what a board adds. Nothing here knows about a board.

pub mod gpio;
pub mod pwr;
pub mod rcc;
pub mod spi;
pub mod tim;

use crate::device::{DeviceContext, ErasedDevice, RegisterFile};
use crate::mcu::Mcu;
use crate::soc::{Bus, Error};
use crate::time::VirtualNanos;

use gpio::Gpio;
use pwr::Pwr;
use rcc::Rcc;
use spi::Spi;
use tim::Tim;

/// What every bus runs at until a firmware touches the clock tree: the 16 MHz HSI, since `RCC_CFGR` resets
/// to zero and both peripheral prescalers are one. RM0090 rev 22 sections 6.2.2 and 6.3.3. Once a model can
/// read another's registers these become the tree's own answers, RL-112.
const RESET_BUS_HZ: std::num::NonZeroU64 =
    std::num::NonZeroU64::new(16_000_000).expect("sixteen million");

/// One whole STM32F427: the bus, and a model for every instance this part has one for.
///
/// The fields are the part's inventory. Everything else about it — where each answers, what its
/// registers reset to, the corrections against RM0090 — is the bus, built by
/// [`memory_map::builder`](super::memory_map::builder).
///
/// A part with no crystals, no rail and nothing on its buses: those are a board's, and a board
/// attaches them to these fields after the part is built.
#[derive(Debug)]
pub struct Stm32f427 {
    /// Reset and clock control, RM0090 rev 22 chapter 6.
    pub rcc: Rcc,
    /// The power controller, sections 5.5.1 and 5.5.2.
    pub pwr: Pwr,
    /// Port A, chapter 8. ST's file gives every port a layout of its own, so this part carries one
    /// port model where the manual gives eleven ports; RL-131 is what makes the other ten free.
    pub gpioa: Gpio,
    /// The six serial interfaces of chapter 28, in the manual's order. The two extended blocks the
    /// same chapter gives another job are not among them; see [`SERIAL_INTERFACES`].
    pub spi: [Spi; SERIAL_INTERFACES.len()],
    /// The general-purpose timer of chapter 18. ST's file gives `TIM2` a layout of its own, RL-131.
    pub tim2: Tim,
    bus: Bus,
    /// Which field answers for each slot of the bus, resolved once in [`Stm32f427::new`] so that
    /// nothing after it looks a peripheral up by name.
    wiring: Box<[Field]>,
    /// Where each field's register file lives, resolved once for the same reason.
    slots: Slots,
}

/// The slot each model's register file is in. A model keeps what the manual calls its state in the
/// register file and what it calls its wires in itself, so reaching one to drive it from outside
/// means reaching both; these indices are what pairs them without a name.
#[derive(Debug, Clone, Copy)]
struct Slots {
    rcc: usize,
    pwr: usize,
    gpioa: usize,
    spi: [usize; SERIAL_INTERFACES.len()],
    tim2: usize,
}

/// Which of the MCU's fields answers for one slot of its bus.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Field {
    /// A slot this part has no model for: its register file answers alone.
    None,
    Rcc,
    Pwr,
    Gpioa,
    /// The nth serial interface, indexing [`Stm32f427::spi`].
    Spi(usize),
    Tim2,
}

impl Field {
    /// Every field this part has, which is what [`Stm32f427::new`] walks.
    ///
    /// Adding a field to the MCU means adding it here, and [`Field::instance`] below is an exhaustive
    /// match, so the compiler refuses a field that has no instance to answer for. That is the whole
    /// reason the mapping is written this way round: a table of names would let a field be forgotten
    /// and leave it pointing at whatever slot came first.
    fn all() -> Vec<Self> {
        let mut all = vec![Self::Rcc, Self::Pwr, Self::Gpioa, Self::Tim2];
        all.extend((0..SERIAL_INTERFACES.len()).map(Self::Spi));
        all
    }

    /// The instance this field answers for.
    fn instance(self) -> &'static str {
        match self {
            Self::None => unreachable!("the unmodelled slot answers for no instance"),
            Self::Rcc => "RCC",
            Self::Pwr => "PWR",
            Self::Gpioa => "GPIOA",
            Self::Spi(nth) => SERIAL_INTERFACES[nth],
            Self::Tim2 => "TIM2",
        }
    }
}

/// The serial interfaces of chapter 28, in the manual's order, which is the order of
/// [`Stm32f427::spi`].
///
/// RM0090 rev 22 section 28.4.2: "The extended I2Ss (`I2Sx_ext`) can be used only in full duplex
/// mode. The `I2Sx_ext` operate always in slave mode." `I2S2ext` and `I2S3ext` carry this layout and are not
/// serial interfaces, so they are not here and the part answers for neither; RL-116 is the I2S.
pub const SERIAL_INTERFACES: [&str; 6] = ["SPI1", "SPI2", "SPI3", "SPI4", "SPI5", "SPI6"];

impl Stm32f427 {
    /// The part out of reset: every model fresh, running on the internal oscillator, nothing on its pins.
    ///
    /// Fails only if the part's own description is wrong — a field whose instance the tables do not
    /// carry, or a field wired to a slot whose layout it was not written for.
    pub fn new() -> Result<Self, Error> {
        let bus = super::memory_map::builder().build()?;
        let mut wiring = vec![Field::None; bus.slots().len()].into_boxed_slice();
        let mut slots = Slots {
            rcc: 0,
            pwr: 0,
            gpioa: 0,
            spi: [0; SERIAL_INTERFACES.len()],
            tim2: 0,
        };
        for field in Field::all() {
            let instance = field.instance();
            let slot = bus
                .slot_of(instance)
                .ok_or(Error::UnknownInstance { instance })?;
            wiring[slot] = field;
            match field {
                Field::Rcc => slots.rcc = slot,
                Field::Pwr => slots.pwr = slot,
                Field::Gpioa => slots.gpioa = slot,
                Field::Spi(nth) => slots.spi[nth] = slot,
                Field::Tim2 => slots.tim2 = slot,
                Field::None => unreachable!("Field::all names no unmodelled slot"),
            }
        }
        let mut mcu = Self {
            rcc: Rcc::new(),
            pwr: Pwr::new(),
            gpioa: Gpio::default(),
            spi: std::array::from_fn(|_| Spi::with_clock(RESET_BUS_HZ)),
            tim2: Tim::with_clock(RESET_BUS_HZ),
            bus,
            wiring,
            slots,
        };
        mcu.check()?;
        Ok(mcu)
    }
}

/// Each model with the register file it keeps half its state in, paired by the slot resolved at
/// construction. This is what a board drives a part through: `mcu.power()` rather than a name and a
/// downcast, and it cannot fail.
macro_rules! paired {
    ($name:ident, $driving:ident, $field:ident, $model:ty, $what:literal) => {
        #[doc = concat!("The ", $what, " and the registers it keeps its state in.")]
        pub fn $name(&mut self) -> (&mut $model, &mut RegisterFile) {
            let Self {
                $field, bus, slots, ..
            } = self;
            ($field, bus.slot_mut(slots.$field).registers_mut())
        }

        #[doc = concat!("Drives an input of the ", $what, " at `at`, an input the bus does not carry: it")]
        /// gets its registers and a context as on a bus access, so it may schedule and raise. This is
        /// the door a board's own parts come through.
        pub fn $driving<T>(
            &mut self,
            at: VirtualNanos,
            input: impl FnOnce(&mut $model, &mut RegisterFile, &mut DeviceContext<'_>) -> T,
        ) -> T {
            let Self {
                $field, bus, slots, ..
            } = self;
            let (carried, mut context) = bus.call(at, slots.$field);
            input($field, carried.registers_mut(), &mut context)
        }
    };
}

impl Stm32f427 {
    paired!(clocks, drive_clocks, rcc, Rcc, "clock tree");
    paired!(power, drive_power, pwr, Pwr, "regulator");
    paired!(port_a, drive_port_a, gpioa, Gpio, "port A");
    paired!(timer, drive_timer, tim2, Tim, "timer");

    /// The nth serial interface of [`SERIAL_INTERFACES`], with the registers it keeps its state in.
    pub fn serial(&mut self, nth: usize) -> (&mut Spi, &mut RegisterFile) {
        let Self {
            spi, bus, slots, ..
        } = self;
        (&mut spi[nth], bus.slot_mut(slots.spi[nth]).registers_mut())
    }

    /// Drives an input of the nth serial interface at `at`; see the other `drive_` methods.
    pub fn drive_serial<T>(
        &mut self,
        at: VirtualNanos,
        nth: usize,
        input: impl FnOnce(&mut Spi, &mut RegisterFile, &mut DeviceContext<'_>) -> T,
    ) -> T {
        let Self {
            spi, bus, slots, ..
        } = self;
        let (carried, mut context) = bus.call(at, slots.spi[nth]);
        input(&mut spi[nth], carried.registers_mut(), &mut context)
    }
}

impl Mcu for Stm32f427 {
    fn bus(&self) -> &Bus {
        &self.bus
    }

    fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
        let Self {
            rcc,
            pwr,
            gpioa,
            spi,
            tim2,
            bus,
            wiring,
            ..
        } = self;
        let model: Option<&mut dyn ErasedDevice> = match wiring[slot] {
            Field::None => None,
            Field::Rcc => Some(rcc),
            Field::Pwr => Some(pwr),
            Field::Gpioa => Some(gpioa),
            Field::Spi(nth) => Some(&mut spi[nth]),
            Field::Tim2 => Some(tim2),
        };
        (bus, model)
    }

    fn bus_mut(&mut self) -> &mut Bus {
        &mut self.bus
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::soc::{Peripheral, RegisterSpec};

    /// Every instance of `layout` the part carries.
    fn instances_of(layout: &'static [RegisterSpec]) -> impl Iterator<Item = &'static Peripheral> {
        super::super::generated::PERIPHERALS
            .iter()
            .filter(move |peripheral| std::ptr::eq(peripheral.registers, layout))
    }

    #[test]
    fn every_field_answers_for_the_one_instance_it_is_named_for() {
        let mut mcu = Stm32f427::new().expect("the part assembles");
        let slots = mcu.bus().slots().len();
        // Walked through `split`, so this is the dispatch answering rather than the table it was built
        // from: a field wired to the wrong slot shows up as the wrong instance name.
        let mut answered: Vec<(Field, &'static str)> = (0..slots)
            .filter_map(|slot| {
                let field = mcu.wiring[slot];
                let name = mcu.bus().slots()[slot].peripheral().name;
                mcu.split(slot).1.map(|_| (field, name))
            })
            .collect();
        answered.sort_by_key(|(_, name)| *name);
        let mut expected: Vec<(Field, &'static str)> = Field::all()
            .into_iter()
            .map(|field| (field, field.instance()))
            .collect();
        expected.sort_by_key(|(_, name)| *name);
        assert_eq!(
            answered, expected,
            "every field the part has answers for exactly the instance it is named for"
        );
    }

    #[test]
    fn the_part_answers_for_every_serial_interface_and_neither_extended_block() {
        let carried: Vec<&str> = instances_of(&super::super::generated::spi1::REGISTERS)
            .map(|one| one.name)
            .collect();
        for instance in SERIAL_INTERFACES {
            assert!(
                carried.contains(&instance),
                "{instance} is not an instance of the serial interface's layout"
            );
        }
        // Section 28.4.2 gives these the I2S's job, so the part does not answer for them.
        for extended in ["I2S2ext", "I2S3ext"] {
            assert!(carried.contains(&extended));
            assert!(!SERIAL_INTERFACES.contains(&extended));
        }
    }

    #[test]
    fn the_part_says_how_much_of_itself_it_models() {
        let mut mcu = Stm32f427::new().expect("the part assembles");
        let instances = mcu.bus().slots().len();
        let modelled = (0..instances)
            .filter(|&slot| mcu.split(slot).1.is_some())
            .count();
        assert_eq!(modelled, Field::all().len());
        assert!(modelled < instances);
        println!("{modelled} of {instances} instances carry a model");
    }
}