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 smallest board there is: one whole STM32F427, a rail, one crystal, and a known peer on every
//! serial interface.
//!
//! It exists to prove the part rather than to be a board. A board's sensors and its crystal are
//! things a conformance run must not depend on, and a part has to be provable before there is a
//! board to put it on. So this fits the least that still lets something happen: a rail, because a
//! regulator with no supply has nothing to regulate; one crystal, because a part with nothing on its
//! oscillator pins cannot run its loop and half of what a clock tree does is then out of reach; and
//! one peer per serial interface, because a bus with nothing on the far end proves only that a flag
//! moved.
//!
//! Its fields are what it has, the same as any board. The difference is what it does not have: no
//! low-speed crystal, and no integrated circuit that a real board would carry.

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::peripherals::spi::Pattern;
use crate::soc::stm32f427::{SERIAL_INTERFACES, Stm32f427};

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

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

/// What every peer answers, one word per transfer, none of it a word the conformance firmware sends, so a
/// model shifting its own transmission back is caught rather than passed. The fifth is wider than a byte
/// because it is the one a sixteen-bit frame takes: an eight-bit frame cannot carry it, which is what lets
/// that check tell the two widths apart. The last is what the bit-order check receives, and no other word
/// here is its reverse, so a model that both lost its place in the queue and ignored `LSBFIRST` cannot land
/// on the expected answer by accident.
pub const MISO_PATTERN: [u16; 8] = [0x5A, 0xA5, 0x3C, 0xC3, 0x1234, 0xF0, 0x6B, 0x96];

/// The core the part is run on, at the frequency and the shape RM0090 rev 22 and PM0214 rev 10 give it.
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 part implements four priority bits, PM0214 rev 10 section 4.3.
const PRIORITY_BITS: u8 = 4;
/// Flash answers at the reset vector, as it does with BOOT0 low, 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 bench.
#[derive(Debug)]
pub struct Stm32f427Bench {
    /// The microcontroller, whole and the same one any board takes.
    pub mcu: Stm32f427,
    /// The crystal the bench fits, inside the 4 to 26 MHz RM0090 rev 22 section 6.2.1 accepts; eight
    /// megahertz divides to the 2 MHz input section 6.3.2 recommends for the loop.
    pub hse: Crystal,
    /// The rail the bench regulates to, inside the 1.7 to 3.6 V DS9405 rev 13 table 17 allows the part
    /// and above every detector threshold of its table 22, so the supervisor is quiet unless a test
    /// drives it down.
    pub rail: Supply,
}

impl Stm32f427Bench {
    /// The bench as it is built: the part out of reset, then the little this board fits.
    pub fn new() -> Result<Self, Error> {
        let mut bench = Self {
            mcu: Stm32f427::new()?,
            hse: Crystal(8_000_000),
            rail: Supply(3300),
        };
        bench.fit();
        Ok(bench)
    }

    /// What the bench puts on the part's pins: a crystal on the high-speed pair, nothing on the
    /// low-speed one, a rail, and a peer on every serial interface the part carries.
    fn fit(&mut self) {
        self.mcu.rcc.attach_crystals(self.hse.0, Crystal::NONE.0);
        let rail = self.rail.0;
        let (power, registers) = self.mcu.power();
        power.set_supply(rail, registers);
        for nth in 0..SERIAL_INTERFACES.len() {
            self.mcu.spi[nth].attach(Pattern::answering(MISO_PATTERN));
        }
    }
}

impl Board for Stm32f427Bench {
    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 bench behind the constructor table of [`crate::machine::all`].
pub fn boxed() -> Result<Box<dyn Board>, Error> {
    Ok(Box::new(Stm32f427Bench::new()?))
}

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

    #[test]
    fn the_bench_fits_its_crystal_to_the_high_speed_pin() {
        let bench = Stm32f427Bench::new().unwrap();
        assert_eq!(bench.hse, Crystal(8_000_000));
    }

    #[test]
    fn the_bench_hangs_a_peer_on_every_serial_interface_the_part_carries() {
        let bench = Stm32f427Bench::new().unwrap();
        assert_eq!(bench.mcu.spi.len(), SERIAL_INTERFACES.len());
        for interface in &bench.mcu.spi {
            assert!(
                interface.attached(),
                "a bus with nothing on the far end proves only that a flag moved"
            );
        }
    }
}