rigless 0.0.5

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; a fork of QEMU is built in and executes the instructions
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, 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::chip::sd;
use crate::device::Level;
use crate::emulator::InterruptLine;
use crate::machine::armv7m;
use crate::machine::{Board, Crystal, Supply, Target};
use crate::soc::Error;
use crate::soc::stm32f427::peripherals::adc::Channel;
use crate::soc::stm32f427::peripherals::gpio::Pin;
use crate::soc::stm32f427::peripherals::spi::{Pattern, Select};
use crate::soc::stm32f427::peripherals::{SERIAL_PORTS, usart};
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: [u16; 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: [u16; 1] = [0xD1];
/// The second of the two, behind the pin the Pixracer's barometer sits on.
pub const ON_PD7: [u16; 1] = [0x71];

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

/// 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 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 and every serial port 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() {
            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(usart::Pattern::answering(RX_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(sd::MemoryCard::holding(&card_contents()));
    }
}

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 emulator = 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(&crate::cpu::armv7m::windows(crate::mcu::Mcu::bus(
            &self.mcu,
        )));
        vec![Target {
            name: MCU,
            mcu: &mut self.mcu,
            emulator,
            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"
            );
        }
    }
}