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 ARMv7-M machine of the sidecar, mirrored field for field.
//!
//! The sidecar carries one machine per core family, and this is the Rust side of the ARMv7-M one: a typed
//! field for each property that machine declares, listed in [`PROPERTIES`], and one each for its model, its
//! instruction rate, and the region an SoC boots from. `machine_matches_the_fork` asks the sidecar itself
//! whether the properties and the models still agree with what is here. A machine module fills the fields
//! with its SoC's numbers and calls [`Core::command`]; no machine module spells a property name.

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

use crate::sidecar::{Alias, Property, SidecarCommand};

/// The binary the sidecar package installs.
pub const BINARY: &str = "rigless-sidecar";
/// The machine this module mirrors.
pub const MACHINE: &str = "rigless-armv7m";

/// The properties the machine declares, with the type each takes, less the one the core sets itself.
pub const PROPERTIES: [(&str, &str); 4] = [
    (CLOCK_HZ, "size"),
    (INTERRUPT_LINES, "uint32"),
    (PRIORITY_BITS, "uint8"),
    (REFCLK_DIVIDER, "uint32"),
];

/// Where the core reads the address it starts from.
const RESET_VECTOR: u64 = 0;

const CLOCK_HZ: &str = "clock-hz";
const INTERRUPT_LINES: &str = "num-irq";
const PRIORITY_BITS: &str = "num-prio-bits";
const REFCLK_DIVIDER: &str = "refclk-divider";

/// A core the machine runs, by the name it takes for it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Model {
    /// Cortex-M3.
    M3,
    /// Cortex-M4.
    M4,
    /// Cortex-M7.
    M7,
    /// Cortex-M33.
    M33,
}

impl Model {
    /// Every core the machine accepts, in the order it lists them.
    pub const ALL: [Self; 4] = [Self::M3, Self::M4, Self::M7, Self::M33];

    /// The name the machine takes for this core.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::M3 => "cortex-m3",
            Self::M4 => "cortex-m4",
            Self::M7 => "cortex-m7",
            Self::M33 => "cortex-m33",
        }
    }
}

/// The ARMv7-M core as a machine module configures it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Core {
    /// Which core.
    pub model: Model,
    /// The core clock, which drives the timers inside the core.
    pub clock_hz: NonZeroU64,
    /// The interrupt lines the controller has, each one a line the runtime can raise.
    pub interrupt_lines: NonZeroU32,
    /// The priority bits the controller implements; zero leaves the count to the core, as the machine spells it.
    pub priority_bits: u8,
    /// The reference clock is the core clock divided by this.
    pub refclk_divider: NonZeroU32,
    /// Every instruction takes `2 ^ icount_shift` nanoseconds.
    pub icount_shift: u32,
    /// The memory region the SoC's boot configuration puts where the core fetches its reset vector.
    pub boot: &'static str,
}

impl Core {
    /// What a target on this machine is started as.
    #[must_use]
    pub fn command(self) -> SidecarCommand {
        SidecarCommand {
            binary: BINARY,
            machine: MACHINE,
            cpu: self.model.name(),
            properties: vec![
                Property::new(CLOCK_HZ, self.clock_hz),
                Property::new(INTERRUPT_LINES, self.interrupt_lines),
                Property::new(PRIORITY_BITS, self.priority_bits),
                Property::new(REFCLK_DIVIDER, self.refclk_divider),
            ],
            icount_shift: self.icount_shift,
            // The core fetches the reset vector from address zero, so the boot region answers there too.
            aliases: vec![Alias {
                region: self.boot,
                at: RESET_VECTOR,
            }],
        }
    }
}

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

    fn core() -> Core {
        Core {
            model: Model::M4,
            clock_hz: NonZeroU64::new(168_000_000).unwrap(),
            interrupt_lines: NonZeroU32::new(91).unwrap(),
            priority_bits: 4,
            refclk_divider: NonZeroU32::new(8).unwrap(),
            icount_shift: 2,
            boot: "flash",
        }
    }

    fn rendered(core: Core) -> Vec<String> {
        core.command()
            .properties
            .iter()
            .map(ToString::to_string)
            .collect()
    }

    #[test]
    fn the_properties_render_as_the_machine_spells_them() {
        assert_eq!(
            rendered(core()),
            [
                "clock-hz=168000000",
                "num-irq=91",
                "num-prio-bits=4",
                "refclk-divider=8"
            ]
        );
        let command = core().command();
        assert_eq!(command.machine, MACHINE);
        assert_eq!(command.cpu, "cortex-m4");
        assert_eq!(
            command.properties.len(),
            PROPERTIES.len(),
            "every property the machine declares is set"
        );
    }

    #[test]
    fn the_boot_region_is_the_one_the_core_also_finds_at_its_reset_vector() {
        assert_eq!(
            core().command().aliases,
            [Alias {
                region: "flash",
                at: 0
            }]
        );
        let from_system = Core {
            boot: "system",
            ..core()
        };
        assert_eq!(
            from_system.command().aliases,
            [Alias {
                region: "system",
                at: 0
            }],
            "the SoC's boot configuration says which region, not the family"
        );
    }
}