rigless 0.3.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 smallest board there is: one whole STM32F427, a rail, one crystal, a known peer on every
//! serial port and every serial interface, two on the third interface behind two chip selects, two
//! on the first inter-integrated circuit interface at a 7-bit and a 10-bit address, known voltages
//! on the converters' pins, and a memory card.
//!
//! 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's a
//! board to put it on. So this fits the least that still lets something happen. It fits a rail,
//! because a regulator with no supply has nothing to regulate. It fits one crystal, because a part
//! with nothing on its oscillator pins can't run its loop, and half of what a clock tree does is
//! then out of reach. And it fits a peer on every serial interface, because a bus with nothing on
//! the far end proves only that a flag moved. `I2C2` and `I2C3` carry none, the same as the Pixracer's
//! own `I2C1`, so the run also proves the address nothing answers to on a bus with nothing on it at
//! all, not only a bus with one peer that declines a different address.
//!
//! 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::chip::memory_card::MemoryCard;
use crate::chip::sd;
use crate::chip::spi::Pattern;
use crate::chip::uart;
use crate::device::Level;
use crate::machine::armv7m;
use crate::machine::{Board, Crystal, Supply, Target};
use crate::snapshot::store;
use crate::soc::Error;
use crate::soc::stm32f427::peripherals::SERIAL_PORTS;
use crate::soc::stm32f427::peripherals::adc::Channel;
use crate::soc::stm32f427::peripherals::gpio::Pin;
use crate::soc::stm32f427::peripherals::i2c;
use crate::soc::stm32f427::peripherals::spi::Select;
use crate::soc::stm32f427::{SERIAL_INTERFACES, Stm32f427};

/// The serial interface that carries the two selected peers, `SPI3`, by [`SERIAL_INTERFACES`]' order.
const SELECTED: usize = 2;

/// 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: [u32; 8] = [0x5A, 0xA5, 0x3C, 0xC3, 0x1234, 0xF0, 0x6B, 0x96];

/// The third serial interface carries two peers behind two pins of port D rather than one tied low, so
/// the bench can prove a chip select: the firmware drops one pin and must read this word, drops the other
/// and must read the other's. Neither is in [`MISO_PATTERN`], so a controller answering from the wrong
/// interface is caught as well as one answering from the wrong peer.
pub const ON_PD10: [u32; 1] = [0xD1];
/// The second of the two, behind the pin the Pixracer's barometer sits on.
pub const ON_PD7: [u32; 1] = [0x71];

/// What the peer on every serial port offers as soon as the receiver is enabled, one word per
/// frame, and then nothing, which is what lets the idle line be checked. None of it is a word the
/// conformance firmware sends, so a port reading its own transmission back is caught.
pub const RX_PATTERN: [u16; 5] = [0x1E, 0xE1, 0x2D, 0xD2, 0x4B];

/// The address the peer on `I2C1` answers to.
pub const I2C_PEER_ADDRESS: u8 = 0x50;

/// What the peer on `I2C1` answers, one byte per read, then repeating the last, none of it a byte
/// the conformance firmware writes, so a controller reading its own write back is caught.
pub const I2C_PATTERN: [u8; 3] = [0x2D, 0x3E, 0x4F];

/// A second `I2C1` address, past the 7-bit range, so the run also proves the 10-bit header state
/// machine, section 27.3.3.
pub const I2C_TEN_BIT_ADDRESS: u16 = 0x145;
/// What the 10-bit peer answers, disjoint from [`I2C_PATTERN`] so a check that reached the wrong one
/// of the two peers on `I2C1` is caught.
pub const I2C_TEN_BIT_PATTERN: [u8; 2] = [0x5C, 0x6D];

/// What the bench puts on every converter's pins, as a channel and its millivolts: half the rail on
/// channel 2 and on channel 10, a quarter on channel 3. Two different shares, so a converter that answered
/// the wrong channel would answer the wrong word, and channel 10 because it is the first row of the other
/// sampling-time register.
pub const ON_ADC: [(u8, u32); 3] = [(2, 1650), (10, 1650), (3, 825)];

/// Blocks on the card in the bench's slot, which is as small as a card can be and still be one a firmware
/// addresses by block: sixty-four blocks is thirty-two kilobytes.
const CARD_BLOCKS: usize = 64;

/// What every block of that card holds before anything writes to it.
///
/// Not zero, for the reason [`MISO_PATTERN`] is not: a controller that never reached its card fills a
/// buffer with zeros just as happily, so a check that read zeros back would be asserting nothing. The
/// pattern is the block's own number in its first byte and a fixed mark in the rest, so a read of the wrong
/// block is caught as well as a read of no block at all.
fn card_contents() -> Vec<u8> {
    (0..CARD_BLOCKS)
        .flat_map(|block| {
            let mut bytes = vec![0xC5_u8; sd::BLOCK];
            bytes[0] = u8::try_from(block).expect("sixty-four blocks");
            bytes
        })
        .collect()
}

/// The core the part is run on, at 168 MHz, the most RM0090 rev 22 section 3.5.1 allows at scale 1
/// without over-drive, and in the shape PM0214 rev 10 gives 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;
/// The core is a Cortex-M4, RM0090 rev 22 section 1. The part has the bit-band regions an M4 is
/// given, and an access at an alias reaches the bus at the instruction's own size, PM0214 rev 10
/// section 2.2.5.
const VARIANT: armv7m::Variant = armv7m::Variant::M4;
/// The core is a Cortex-M4 with FPU, RM0090 rev 22 section 1, and the unit is single precision,
/// DS9405 rev 13 section 2, so FPv4-SP.
const FPU: armv7m::Fpu = armv7m::Fpu::FpV4Sp;
/// The part has an MPU, DS9405 rev 13 section 2, and it has eight regions, PM0214 rev 10 sections
/// 4.2 and 4.2.5.
const MPU_REGIONS: armv7m::MpuRegions = armv7m::MpuRegions::Eight;
/// Flash answers at the reset vector, as it does with BOOT0 low, RM0090 rev 22 section 2.4.
const BOOT: &str = "flash";

/// 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, a peer on every serial interface and every serial port the part
    /// carries, two of them behind chip selects on the third interface, two peers on the first
    /// inter-integrated circuit interface, known voltages on three pins of every converter, and a
    /// memory card.
    fn fit(&mut self) {
        self.mcu.rcc.attach_crystals(self.hse.0, Crystal::NONE.0);
        let rail = self.rail.0;
        self.mcu.set_supply(rail);
        for nth in 0..SERIAL_INTERFACES.len() {
            if nth == SELECTED {
                continue;
            }
            self.mcu.spi[nth].attach(Pattern::answering(MISO_PATTERN));
        }
        for (words, pin) in [(ON_PD10, 10), (ON_PD7, 7)] {
            self.mcu.spi[SELECTED].attach_selected(
                Pattern::answering(words),
                Select {
                    port: "GPIOD",
                    pin: Pin::new(pin).expect("a port has sixteen pins"),
                    active: Level::Low,
                },
            );
        }
        for nth in 0..SERIAL_PORTS.len() {
            self.mcu.usart[nth].attach(uart::Pattern::answering(RX_PATTERN));
        }
        self.mcu.i2c[0].attach(i2c::Pattern::answering(I2C_PEER_ADDRESS, I2C_PATTERN));
        self.mcu.i2c[0].attach(i2c::TenBit::answering(
            I2C_TEN_BIT_ADDRESS,
            I2C_TEN_BIT_PATTERN,
        ));
        // `VREF+` and `VBAT` are the rail, as a board with neither brought out has them, table 65 and 13.11.
        for converter in &mut self.mcu.adc {
            converter.set_reference(rail);
            converter.set_backup_battery(rail);
            for (number, millivolts) in ON_ADC {
                let channel = Channel::new(number).expect("a pin channel");
                converter.set_input(channel, millivolts);
            }
        }
        self.mcu.sdio.attach(MemoryCard::holding(&card_contents()));
    }
}

store!(Stm32f427Bench {
    values: [],
    parts: [mcu],
    built: [hse, rail],
});

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

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

    fn mcu(&mut self, target: usize) -> Option<&mut dyn crate::mcu::Mcu> {
        (target == 0).then_some(&mut self.mcu)
    }

    fn targets(&mut self) -> Vec<Target<'_>> {
        let core = armv7m::Core {
            variant: VARIANT,
            clock_hz: CLOCK_HZ,
            interrupt_lines: NonZeroU32::new(u32::from(
                crate::mcu::Operate::cpu(&self.mcu).interrupt_lines,
            ))
            .expect("an MCU with interrupts"),
            priority_bits: PRIORITY_BITS,
            refclk_divider: REFCLK_DIVIDER,
            instructions_per_second: armv7m::INSTRUCTIONS_PER_SECOND,
            boot: BOOT,
        }
        .spec_with_parts(
            &crate::cpu::armv7m::windows(crate::mcu::Mcu::bus(&self.mcu)),
            FPU,
            MPU_REGIONS,
        );
        vec![Target::new(MCU, &mut self.mcu, core)]
    }
}

/// 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"
            );
        }
    }
}