rigless 0.0.2

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 STM32F427 on the sidecar's ARMv7-M machine, with the models of release 0.1 bound.

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

use crate::machine::{Machine, Target};
use crate::sidecar::InterruptLine;
use crate::soc::Error;

use crate::machine::armv7m;
use crate::soc::stm32f427;
use crate::soc::stm32f427::peripherals::gpio::Gpio;
use crate::soc::stm32f427::peripherals::tim::Tim;

/// 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 SoC, RM0090 rev 22 section 7.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 SoC 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;

/// What TIM2 counts until the firmware touches the clock tree: after reset the 16 MHz HSI is the system
/// clock and `RCC_CFGR` is zero, so the APB1 prescaler is 1 and the timer clock is the APB1 clock, the HSI.
/// RM0090 rev 22 sections 7.2, 7.2.2, 7.2.6, and 7.3.3.
const TIM2_CLOCK_HZ: NonZeroU64 = NonZeroU64::new(16_000_000).expect("sixteen million");

/// The Pixracer with port A and TIM2 modeled and every other instance register-only.
pub fn machine() -> Result<Machine, Error> {
    let soc = stm32f427::builder()
        .bind("GPIOA", Gpio::default())
        .bind("TIM2", Tim::with_clock(TIM2_CLOCK_HZ))
        .build()?;
    let sidecar = armv7m::Core {
        model: armv7m::Model::M4,
        clock_hz: CLOCK_HZ,
        interrupt_lines: NonZeroU32::new(u32::from(soc.cpu().interrupt_lines))
            .expect("an SoC with interrupts"),
        priority_bits: PRIORITY_BITS,
        refclk_divider: REFCLK_DIVIDER,
        icount_shift: ICOUNT_SHIFT,
        boot: BOOT,
    }
    .command();
    Ok(Machine {
        name: NAME,
        targets: vec![Target {
            name: MCU,
            soc,
            sidecar,
            interrupt_line: InterruptLine::new,
        }],
    })
}

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

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

    #[test]
    fn the_machine_takes_its_interrupt_count_from_the_part() {
        let machine = machine().unwrap();
        let target = &machine.targets[0];
        let declared = target
            .sidecar
            .properties
            .iter()
            .find(|property| property.name == "num-irq")
            .expect("the machine is told its interrupt count");
        assert_eq!(
            declared.value,
            target.soc.cpu().interrupt_lines.to_string(),
            "the count the engine is given is the count the SoC has"
        );
    }
}