rigless 0.2.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 ARMv7-M machine, what a target of that core family is.
//!
//! This is what an ARMv7-M target says about itself: a typed field for each of its properties, and
//! one each for its variant, its instruction rate, and the region an SoC boots from. A [`Config`]
//! is that [`Core`] with the windows of the processor's own bus the runtime answers and the units
//! the part has, which the family's [`processor`](crate::cpu::armv7m::processor) takes as it is.
//! [`Config::spec`] makes the spec whose constructor hands it over. A machine module fills the
//! fields with its SoC's numbers and calls [`Core::spec`] or [`Core::spec_with_parts`].

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

use crate::cpu::armv7m::Window;
use crate::elf::Machine;
use crate::emulator::{Alias, Boot, CoreSpec};

/// The core this module describes, as the family's refusals name it.
#[cfg(not(emulator_absent))]
pub(crate) const CORE: &str = "the ARMv7-M core";

/// The machine of the ELF files the core runs: `EM_ARM`, which is 40, in 32-bit files, as the
/// toolchain writes every Thumb firmware's header.
const ELF: Machine = Machine::new(40, false);

/// Where the core reads the address it starts from.
const RESET_VECTOR: u64 = 0;
/// The exceptions every core has before its first interrupt line, reset among them.
const SYSTEM_EXCEPTIONS: u64 = 16;
/// The bytes of one entry in the vector table.
const VECTOR_BYTES: u64 = 4;
/// The least a vector table is aligned to, however few exceptions it has.
const TABLE_ALIGNMENT: u64 = 128;

// ceiling: ARMv7-M cores only; upgrade when the family models v8-M's system control space.
/// A core the machine runs, by the name it takes for it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Variant {
    /// Cortex-M3.
    M3,
    /// Cortex-M4.
    M4,
    /// Cortex-M7.
    M7,
}

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

    /// 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",
        }
    }

    // ceiling: the Cortex-M3's and M7's options below come from no manual cited here; upgrade when
    // their technical reference manuals are cited.
    /// Whether the core has the DSP extension, which makes it ARMv7E-M, DDI 0403E.b A1.3. The M4
    /// has it, 100166 section 3.3.2.
    #[must_use]
    pub(crate) const fn dsp(self) -> bool {
        matches!(self, Self::M4 | Self::M7)
    }

    /// Every floating-point unit a part on this core may have. The M4's is FPv4-SP, and it's
    /// optional, 100166 sections 1.4 and 7.1.
    #[must_use]
    pub(crate) const fn fpus(self) -> &'static [Fpu] {
        match self {
            Self::M3 => &[Fpu::None],
            Self::M4 => &[Fpu::None, Fpu::FpV4Sp],
            Self::M7 => &[Fpu::None, Fpu::FpV5Sp, Fpu::FpV5Dp],
        }
    }

    /// Every MPU a part on this core may have. The M4's has eight regions, and it's optional,
    /// 100166 sections 1.4 and 2.2.
    #[must_use]
    pub(crate) const fn mpus(self) -> &'static [MpuRegions] {
        match self {
            Self::M3 | Self::M4 => &[MpuRegions::None, MpuRegions::Eight],
            Self::M7 => &[MpuRegions::None, MpuRegions::Eight, MpuRegions::Sixteen],
        }
    }

    /// The unit a board that doesn't name one gets: the fullest the core may have, so a board from
    /// before units were named keeps one. That's the Cortex-M4F's on an M4 and double-precision
    /// `FPv5` on an M7. An M3 can't have one.
    #[must_use]
    pub(crate) const fn default_fpu(self) -> Fpu {
        match self {
            Self::M3 => Fpu::None,
            Self::M4 => Fpu::FpV4Sp,
            Self::M7 => Fpu::FpV5Dp,
        }
    }
}

/// The floating-point unit a part has, DDI 0403E.b A1.3 and Table A1-1.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Fpu {
    /// No floating-point extension, so CP10 and CP11 don't exist.
    None,
    /// FPv4-SP, single precision only: the Cortex-M4F's.
    FpV4Sp,
    /// `FPv5`, single precision only.
    FpV5Sp,
    /// `FPv5` with double precision.
    FpV5Dp,
}

impl Fpu {
    /// The name the machine takes for this unit.
    #[must_use]
    pub(crate) const fn name(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::FpV4Sp => "fpv4-sp",
            Self::FpV5Sp => "fpv5-sp",
            Self::FpV5Dp => "fpv5-dp",
        }
    }
}

impl std::fmt::Display for Fpu {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(self.name())
    }
}

/// How many regions a part's MPU has, which `MPU_TYPE.DREGION` reads, DDI 0403E.b B3.5.5.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum MpuRegions {
    /// No MPU at all.
    None = 0,
    /// Eight regions: the Cortex-M4's, and what a board that doesn't say takes.
    #[default]
    Eight = 8,
    /// Sixteen regions.
    Sixteen = 16,
}

impl MpuRegions {
    /// The regions, as `MPU_TYPE.DREGION` reads them.
    #[must_use]
    pub(crate) const fn count(self) -> u8 {
        self as u8
    }
}

/// The ARMv7-M core as a machine module configures it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Core {
    /// Which core.
    pub variant: Variant,
    /// 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 interrupt controller implements, which the part's manual gives in its
    /// section on the controller. A firmware that reads back a priority it wrote sees this many bits
    /// of it. Zero leaves the count to the core's own default, as the machine spells it.
    pub priority_bits: u8,
    /// The reference clock of the core's own timer is the core clock divided by this. The part's
    /// manual gives the divider in its section on that timer's clock source, and eight is common. A
    /// wrong one makes a kernel's tick built on that timer run fast or slow by the same ratio.
    pub refclk_divider: NonZeroU32,
    /// How many instructions the core retires in a second of virtual time. It's a choice: every
    /// target this crate carries retires 250 million, four nanoseconds each, which is about what a
    /// core at a few hundred megahertz does. The part's own clock, one instruction a cycle, is the
    /// other natural pick. A lower rate makes the emulated core slower against its own peripherals
    /// and the plant, a higher one faster, and nothing else depends on it.
    pub instructions_per_second: NonZeroU64,
    /// The memory region the SoC's boot configuration puts where the core fetches its reset vector.
    ///
    /// The core answers at address zero with that region's bytes, so a vector table linked at the
    /// start of the flash region is what the core finds at its reset vector. A name the MCU's memory
    /// map does not have is refused when the session opens, before the core fetches from it.
    pub boot: &'static str,
}

impl Core {
    /// The description of a target's core on this machine, answering for `ppb` of the processor's
    /// own bus, with the variant's fullest floating-point unit and eight MPU regions.
    ///
    /// The windows are the MCU's, from [`crate::cpu::armv7m::windows`], rather than anything this
    /// module decides: the core answers the processor's peripheral bus itself unless told which
    /// stretches of it the runtime has a model behind, and the runtime's own bus is what knows.
    #[must_use]
    pub fn spec(self, ppb: &[Window]) -> CoreSpec {
        Config::new(self, ppb).spec()
    }

    /// [`Core::spec`] for a part whose manual names its floating-point unit and its MPU. One the
    /// variant can't have is refused when the run starts. A board that calls [`Core::spec`] gets an
    /// MPU of eight regions, and the Cortex-M4F's unit on an M4.
    #[must_use]
    pub fn spec_with_parts(self, ppb: &[Window], fpu: Fpu, mpu_regions: MpuRegions) -> CoreSpec {
        let mut config = Config::new(self, ppb);
        config.fpu = fpu;
        config.mpu_regions = mpu_regions;
        config.spec()
    }

    /// What a vector table's address is a multiple of: the table's size rounded up to a power of two,
    /// and never less than 128 bytes (ARMv7-M ARM B1.5.3).
    fn table_alignment(self) -> NonZeroU64 {
        let size = (SYSTEM_EXCEPTIONS + u64::from(self.interrupt_lines.get())) * VECTOR_BYTES;
        NonZeroU64::new(size.next_power_of_two().max(TABLE_ALIGNMENT))
            .expect("a power of two is above zero")
    }
}

/// What the family makes a core from: the machine's [`Core`], the windows of the processor's own
/// bus the runtime answers, and the units the part has.
///
/// [`Config::new`] makes one with the variant's fullest floating-point unit and eight MPU regions,
/// and each field is yours to set after.
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Config {
    /// The core.
    pub core: Core,
    /// The stretches of the processor's own bus the runtime has a model behind.
    pub ppb: Vec<Window>,
    /// The floating-point unit.
    pub fpu: Fpu,
    /// The MPU's regions.
    pub mpu_regions: MpuRegions,
}

impl Config {
    /// `core` answering for `ppb` of the processor's own bus, with the fullest floating-point unit
    /// its variant may have and an MPU of eight regions.
    #[must_use]
    pub fn new(core: Core, ppb: &[Window]) -> Self {
        Self {
            core,
            ppb: ppb.to_vec(),
            fpu: core.variant.default_fpu(),
            mpu_regions: MpuRegions::default(),
        }
    }

    /// The description of a target's core made as this configuration says, which hands it to
    /// the family's [`processor`](crate::cpu::armv7m::processor).
    #[must_use]
    pub fn spec(self) -> CoreSpec {
        let core = self.core;
        let mut spec = CoreSpec::new(
            Arc::new(move |endian| crate::cpu::armv7m::processor(&self, endian)),
            core.instructions_per_second,
            Boot::new(RESET_VECTOR, core.table_alignment()),
        );
        // The core fetches the reset vector from address zero, so the boot region answers there too.
        spec.aliases = vec![Alias {
            region: core.boot,
            at: RESET_VECTOR,
        }];
        spec.elf = Some(ELF);
        spec.code_address = crate::cpu::armv7m::code_address;
        spec
    }
}

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

    fn core() -> Core {
        Core {
            variant: Variant::M4,
            clock_hz: NonZeroU64::new(168_000_000).unwrap(),
            interrupt_lines: NonZeroU32::new(91).unwrap(),
            priority_bits: 4,
            refclk_divider: NonZeroU32::new(8).unwrap(),
            instructions_per_second: NonZeroU64::new(250_000_000).unwrap(),
            boot: "flash",
        }
    }

    const WINDOWS: [Window; 2] = [
        Window {
            base: 0xE000_0000,
            size: 0x1000,
        },
        Window {
            base: 0xE000_1000,
            size: 0x8,
        },
    ];

    #[test]
    fn a_config_takes_the_variants_fullest_unit_and_eight_regions_unless_a_part_names_them() {
        let config = Config::new(core(), &WINDOWS);
        assert_eq!(
            (config.fpu, config.mpu_regions, &config.ppb[..]),
            (Fpu::FpV4Sp, MpuRegions::Eight, &WINDOWS[..])
        );
        let m3 = Config::new(
            Core {
                variant: Variant::M3,
                ..core()
            },
            &[],
        );
        assert_eq!(m3.fpu, Fpu::None);
    }

    #[test]
    fn a_vector_table_is_aligned_to_its_size_and_never_less_than_128_bytes() {
        let alignment = |lines| {
            Core {
                interrupt_lines: NonZeroU32::new(lines).unwrap(),
                ..core()
            }
            .spec(&WINDOWS)
            .boot
            .alignment
            .get()
        };
        assert_eq!(alignment(91), 512, "107 entries are 428 bytes");
        assert_eq!(alignment(1), 128, "17 entries are 68 bytes");
        assert_eq!(alignment(16), 128, "32 entries are 128 bytes");
        assert_eq!(alignment(17), 256);
    }

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