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 Pixracer: one whole STM32F427, the two crystals on its oscillator pins, and the rail its
//! regulator sees.
//!
//! The board **has** these things — they are its fields, and `pixracer.mcu.rcc` is a field path, not
//! a lookup. The MCU comes complete from [`Stm32f427::new`]; nothing here decides which of its own
//! peripherals exist, because that is the part's business and a board that chose would make every
//! board a different part.
//!
//! **Where an external IC lives, and why.** A board decides which chips exist and what each hangs off;
//! that decision is here, in `Pixracer::fit`, beside the crystals. The chip itself then lives on the
//! interface it is wired to — `mcu.spi[0].attach(..)` — because that is where the wires are and where
//! a transfer has to reach it synchronously, the same shape QEMU gives an `SSIBus`. Holding it in a
//! board field instead would mean the controller could not reach it during a transfer without shared
//! mutability, which buys a prettier field path and costs the thing the field is for.
//!
//! No chip hangs here yet: there are no chip models, and a field for a part that cannot be modelled
//! would be a place-holder rather than a board. The seam is real and the bench exercises it.

use std::num::{NonZeroU32, NonZeroU64};

use crate::machine::armv7m;
use crate::machine::{Board, Crystal, Supply, Target};
use crate::sidecar::InterruptLine;
use crate::soc::Error;
use crate::soc::stm32f427::Stm32f427;

/// The name `rigless run --machine` matches.
pub const NAME: &str = "pixracer";

/// The one MCU on the board.
pub const MCU: &str = "stm32f427";

/// The core clock the board's crystal and PLL give the MCU, RM0090 rev 22 section 6.2.
const CLOCK_HZ: NonZeroU64 = NonZeroU64::new(168_000_000).expect("a running clock");
/// The reference clock of the core's timer is the core clock over eight, PM0214 rev 10 section 4.5.
const REFCLK_DIVIDER: NonZeroU32 = NonZeroU32::new(8).expect("a divider divides");
/// The MCU implements four priority bits, PM0214 rev 10 section 4.3.
const PRIORITY_BITS: u8 = 4;
/// BOOT0 is tied low on the board, so the main flash answers at the reset vector, RM0090 rev 22 section 2.4.
const BOOT: &str = "flash";
/// Every instruction takes four nanoseconds, the rate the runtime fixes for every target.
const ICOUNT_SHIFT: u32 = 2;

/// The Pixracer.
#[derive(Debug)]
pub struct Pixracer {
    /// The microcontroller, whole.
    pub mcu: Stm32f427,
    /// The high-speed crystal the board fits, which is what the PLL multiplies up to the core clock.
    pub hse: Crystal,
    /// The low-speed crystal the board fits for the real-time clock.
    pub lse: Crystal,
    /// The rail the board regulates to, above every detector threshold of DS9405 rev 13 table 22.
    pub rail: Supply,
}

impl Pixracer {
    /// The board as it is built: the MCU out of reset, then what this board fits attached to it.
    pub fn new() -> Result<Self, Error> {
        let mut board = Self {
            mcu: Stm32f427::new()?,
            hse: Crystal(24_000_000),
            lse: Crystal(32_768),
            rail: Supply(3300),
        };
        board.fit();
        Ok(board)
    }

    /// What the board puts on the part's pins. Every line here is a wire on the schematic.
    fn fit(&mut self) {
        self.mcu.rcc.attach_crystals(self.hse.0, self.lse.0);
        let rail = self.rail.0;
        let (power, registers) = self.mcu.power();
        power.set_supply(rail, registers);
    }
}

impl Board for Pixracer {
    fn name(&self) -> &'static str {
        NAME
    }

    fn target_names(&self) -> Vec<&'static str> {
        vec![MCU]
    }

    fn targets(&mut self) -> Vec<Target<'_>> {
        let sidecar = armv7m::Core {
            model: armv7m::Model::M4,
            clock_hz: CLOCK_HZ,
            interrupt_lines: NonZeroU32::new(u32::from(
                crate::mcu::Mcu::cpu(&self.mcu).interrupt_lines,
            ))
            .expect("an MCU with interrupts"),
            priority_bits: PRIORITY_BITS,
            refclk_divider: REFCLK_DIVIDER,
            icount_shift: ICOUNT_SHIFT,
            boot: BOOT,
        }
        .command();
        vec![Target {
            name: MCU,
            mcu: &mut self.mcu,
            sidecar,
            interrupt_line: InterruptLine::new,
        }]
    }
}

/// The board behind the constructor table of [`crate::machine::all`].
pub fn boxed() -> Result<Box<dyn Board>, Error> {
    Ok(Box::new(Pixracer::new()?))
}

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

    #[test]
    fn the_board_has_its_mcu_and_reaches_a_model_by_field() {
        let mut board = Pixracer::new().unwrap();
        assert_eq!(board.name(), NAME);
        // No lookup and no downcast: the clock tree is a field of a field, and the crystal the board
        // fits is what it answers with.
        let (clocks, registers) = board.mcu.clocks();
        assert_eq!(
            clocks.sysclk(registers),
            16_000_000,
            "the part wakes on its own oscillator"
        );
        assert_eq!(board.hse, Crystal(24_000_000));
    }

    /// A board decides what hangs off its buses, and the chip answers the controller inside one transfer.
    /// This is the seam a sensor model arrives on; the bench drives the same one with a known pattern.
    #[test]
    fn a_board_can_hang_a_chip_on_one_of_its_interfaces() {
        use crate::soc::stm32f427::peripherals::spi::Peer;

        /// The smallest thing that is a chip rather than a pattern: it answers what it is asked plus one.
        #[derive(Debug, Default)]
        struct Counter(u16);

        impl Peer for Counter {
            fn transfer(&mut self, mosi: u16) -> u16 {
                self.0 = mosi.wrapping_add(1);
                self.0
            }
        }

        let mut board = Pixracer::new().unwrap();
        assert!(
            !board.mcu.spi[0].attached(),
            "a Pixracer fits no chip yet, so nothing is on its first interface"
        );
        board.mcu.spi[0].attach(Counter::default());
        assert!(board.mcu.spi[0].attached());
    }

    #[test]
    fn the_machine_routes_every_cpu_line_and_no_more() {
        let mut board = Pixracer::new().unwrap();
        let lines = board.mcu.cpu().interrupt_lines;
        let targets = board.targets();
        let [target] = &targets[..] else {
            panic!("one MCU on the board");
        };
        assert!((target.interrupt_line)(lines - 1).is_some());
        assert_eq!(target.sidecar.binary, armv7m::BINARY);
    }

    #[test]
    fn the_machine_takes_its_interrupt_count_from_the_part() {
        let mut board = Pixracer::new().unwrap();
        let lines = board.mcu.cpu().interrupt_lines;
        let targets = board.targets();
        let declared = targets[0]
            .sidecar
            .properties
            .iter()
            .find(|property| property.name == "num-irq")
            .expect("the machine is told its interrupt count");
        assert_eq!(
            declared.value,
            lines.to_string(),
            "the count the engine is given is the count the MCU has"
        );
    }
}