rigless 0.1.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 core, which [`processor`] makes, and the blocks it puts in its own peripheral bus.
//!
//! One block so far: the cycle counter of the data watchpoint and trace unit, which is what a
//! firmware spins on for a delay shorter than its tick.
//!
//! # The manuals
//!
//! A part's own manuals don't document this block. It's the core's, so two of ARM's manuals are the
//! reference here, and both are needed:
//!
//! * **100166\_0001\_04**, the Cortex-M4 processor technical reference manual at revision r0p1.
//!   Section 9.2 table 9-1 is the register map: where each register is, what it resets to, and
//!   which direction the bus allows. It then says, of the bit assignments, "DWT registers are
//!   described in the Arm®v7M Architecture Reference Manual".
//! * **ARM DDI 0403E.b**, the ARMv7-M architecture reference manual. Section C1.8.7 is the control
//!   register and C1.8.8 the cycle counter.
//!
//! # What is modelled
//!
//! | Register or section | Behaviour | Source | Test |
//! |---|---|---|---|
//! | `DWT_CYCCNT` | Counts the core clock: "When enabled, CYCCNT increments on each processor clock cycle." The count is not stored but derived from elapsed virtual time at the core clock, so the single clock stays the only one | DDI 0403E.b C1.8.8 | `the_counter_advances_with_the_clock` |
//! | The core clock | The rate the part's tree feeds the processor, followed as it changes: what was counted stands, and from the change the counter runs at the new rate | DDI 0403E.b C1.8.8, the part's manual for the tree | `the_counter_carries_across_a_change_of_clock` |
//! | `DWT_CYCCNT` | Wraps rather than saturates: "On overflow, CYCCNT wraps to zero" | DDI 0403E.b C1.8.8 | `the_counter_wraps_where_it_runs_out` |
//! | `DWT_CYCCNT` | Written as well as read, since its purpose is to "show or set the value of the processor cycle counter". A run already under way continues from what was written | DDI 0403E.b C1.8.8 | `a_write_sets_the_counter`, `a_write_while_it_runs_carries_on_from_there` |
//! | `DWT_CTRL.CYCCNTENA` | Enables the counter, and resets to 0, so a counter nobody enabled reads the same value twice however long passes. Disabling keeps what was counted rather than clearing it | DDI 0403E.b C1.8.7 | `a_counter_nobody_enabled_does_not_run`, `the_counter_holds_its_value_while_disabled` |
//! | `DWT_CTRL.NOCYCCNT` | Reads clear, which is what says the counter exists, and is read-only so a firmware cannot claim otherwise | DDI 0403E.b C1.8.7 | `the_part_says_it_has_a_cycle_counter` |
//! | Reset state | `0x4000_0000`, the value table 9-1 gives for the four comparators for watchpoints and triggers this core carries | 100166 table 9-1 | `the_control_register_resets_to_the_configuration_this_core_has` |
//! | The driver's whole sequence | A kernel's polled delay, run as a firmware runs it: read, read again, subtract, compare. Against a counter that never moves this never ends, which is the whole reason the block is modelled | not applicable | `the_polled_delay_of_a_real_driver_leaves_its_loop` |
//! | Core errata | A part's errata sheet can list this core's own errata. The one part the crate has lists three, marked on every revision, and all three are in instruction execution and exception entry, which the part's own module answers for. None names the watchpoint unit, so there's nothing here to model | not applicable | |
//!
//! **What the model leaves out, and why.**
//!
//! | Behaviour | Source | Reason |
//! |---|---|---|
//! | `DEMCR.TRCENA`, the global enable for the whole unit, is not consulted | DDI 0403E.b C1.6.5 | That register is at `0xE000EDFC`, inside the system control space the core's family module answers rather than the bus this block is on, so it is not reachable from here. DDI 0403E.b makes reads with `TRCENA` clear UNKNOWN, so answering with the running counter is inside what the architecture allows rather than a divergence from it |
//! | Counting is not suspended in debug state | DDI 0403E.b C1.8.8 | DDI 0403E.b C1.8.8 suspends `CYCCNT` while the processor is halted. The runtime already holds virtual time still for a stopped debugger, so the counter doesn't advance then either. What's missing is only a debugger halting the core without the runtime knowing, which this seam doesn't have |
//! | The comparators and their mask and function registers, the profiling counters, the program counter sampler, and the identification registers | 100166 table 9-1 | Nothing has asked for them. The instance covers the eight bytes that are modelled, so each of the rest keeps reading the way the core answers it instead of reaching a model with nothing to say |

#[cfg(not(emulator_absent))]
mod exception;
#[cfg(not(emulator_absent))]
pub(crate) mod family;
#[cfg(not(emulator_absent))]
mod fp;
#[cfg(not(emulator_absent))]
mod itm;
#[cfg(not(emulator_absent))]
mod mpu;
#[cfg_attr(
    emulator_absent,
    expect(
        dead_code,
        reason = "only the core uses most of it, and Miri builds the crate without one"
    )
)]
pub(crate) mod registers;
#[cfg_attr(
    all(emulator_absent, not(test)),
    expect(
        dead_code,
        reason = "only the core uses it, and Miri builds the crate without one"
    )
)]
pub(crate) mod scs;
#[cfg(not(emulator_absent))]
mod semihosting;
#[cfg(not(emulator_absent))]
mod state;
#[cfg(not(emulator_absent))]
mod translate;
#[cfg(fuzzing)]
pub(crate) use translate::testing;

use crate::device::{Device, DeviceContext, RegisterFile};
use crate::emulator::{CoreSpec, Error, Processor};
use crate::soc::{
    Access, Endian, FieldSpec, Layout, Peripheral, ReadAction, RegisterSpec, Width, WriteModify,
};
use crate::time::VirtualNanos;

/// The ARMv7-M core `spec` describes, for a part that keeps its data `endian`, with nothing
/// mapped.
///
/// It's what [`Core::spec`](crate::machine::armv7m::Core::spec) puts in
/// [`CoreSpec::processor`], and how you run the core on a [`CoreBus`](crate::emulator::CoreBus) of
/// your own: map your memory into it, boot it and run it.
///
/// # Errors
///
/// [`Error::Machine`] for a core, a unit, an MPU or a property the family can't make, or a host
/// the code generator can't generate code for.
#[cfg(not(emulator_absent))]
pub fn processor(spec: &CoreSpec, endian: Endian) -> Result<Box<dyn Processor>, Error> {
    Ok(family::Family::new(spec, endian)?)
}

/// The ARMv7-M core `spec` describes, and [`Error::NoEmulator`] because this build has no core.
///
/// Only Miri reaches this, which can't execute the code the core generates.
#[cfg(emulator_absent)]
pub fn processor(spec: &CoreSpec, endian: Endian) -> Result<Box<dyn Processor>, Error> {
    let _ = (spec, endian);
    Err(Error::NoEmulator)
}

/// The instance name the bus knows the unit by.
pub const DWT: &str = "DWT";

/// The processor's own peripheral bus: one megabyte at the top of the map, DDI 0403E.b B3.1.
pub(crate) const PPB_BASE: u64 = 0xE000_0000;
pub(crate) const PPB_SIZE: u64 = 0x0010_0000;

/// One stretch of the processor's peripheral bus the runtime answers for.
///
/// The core's family module implements the interrupt controller and the system timer in that
/// megabyte and answers the rest of it itself, so a block modelled here is reached only where the
/// machine module asks for it by name. Rendering is what the machine's `ppb-windows` property takes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Window {
    /// First address of the window.
    pub base: u64,
    /// Bytes it covers.
    pub size: u64,
}

impl std::fmt::Display for Window {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(formatter, "{:#x}:{:#x}", self.base, self.size)
    }
}

/// Every window a bus wants, which is every instance it carries inside the processor's own bus.
///
/// Asked of the bus rather than listed, so what the core is told to forward and what the runtime
/// models are one fact rather than two that can drift. A part with no such instance wants no window,
/// and the core then answers the whole bus itself.
#[must_use]
pub fn windows(bus: &crate::soc::Bus) -> Vec<Window> {
    let mut windows: Vec<Window> = bus
        .peripherals()
        .filter(|peripheral| peripheral.base >= PPB_BASE && peripheral.base < PPB_BASE + PPB_SIZE)
        .map(|peripheral| Window {
            base: peripheral.base,
            size: peripheral.size,
        })
        .collect();
    windows.sort();
    windows
}

/// Where the unit answers, and how much of it this model claims.
///
/// 100166 table 9-1 puts the whole unit in the page at `0xE0001000`. The two registers below are
/// what is modelled, so they're what the instance covers and what a machine module claims a
/// window for. The rest of the page stays the core's to answer.
pub const DWT_BASE: u64 = 0xE000_1000;

/// Where the system control space starts, B3.2, which the family answers itself: the NVIC, the
/// SCB and SysTick.
pub const SCS_BASE: u64 = scs::SCS_BASE;

/// The exception number of external interrupt 0, B1.5.2. Interrupt `n`'s vector is the word at
/// `4 * (FIRST_INTERRUPT + n)` in the vector table.
pub const FIRST_INTERRUPT: u16 = scs::exception::FIRST_INTERRUPT;

/// The instance, for a bus builder to place.
pub const PERIPHERAL: Peripheral = Peripheral {
    name: DWT,
    base: DWT_BASE,
    size: 0x8,
    interrupts: &[],
    registers: &REGISTERS,
};

/// The registers by name, where a variant's index is its row in [`REGISTERS`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Register {
    /// `DWT_CTRL` at offset `0x0`, resets to `0x4000_0000`, read-write.
    Ctrl = 0,
    /// `DWT_CYCCNT` at offset `0x4`, resets to `0x0000_0000`, read-write.
    Cyccnt = 1,
}

impl Register {
    /// Every register, in table order.
    pub const ALL: [Self; 2] = [Self::Ctrl, Self::Cyccnt];
}

impl Layout for Register {
    const REGISTERS: &'static [RegisterSpec] = &REGISTERS;
    fn from_index(index: usize) -> Option<Self> {
        Self::ALL.get(index).copied()
    }
    fn index(self) -> usize {
        self as usize
    }
}

/// The fields of `DWT_CTRL`.
pub mod dwt_ctrl {
    use super::FieldSpec;
    /// `CYCCNTENA`, bit 0: enables `CYCCNT`, DDI 0403E.b C1.8.7.
    pub const CYCCNTENA: &FieldSpec = &super::REGISTERS[0].fields[0];
    /// `NOCYCCNT`, bit 25: set when the implementation has no cycle counter, DDI 0403E.b C1.8.7.
    pub const NOCYCCNT: &FieldSpec = &super::REGISTERS[0].fields[1];
}

/// The fields of `DWT_CYCCNT`.
pub mod dwt_cyccnt {
    use super::FieldSpec;
    /// `CYCCNT`, bits 31 to 0, DDI 0403E.b C1.8.8.
    pub const CYCCNT: &FieldSpec = &super::REGISTERS[1].fields[0];
}

/// The register table.
///
/// `DWT_CTRL`'s reset value is the implementation's: 100166 table 9-1 gives `0x4000_0000` for the four
/// comparators for watchpoints and triggers this core has. Its `NUMCOMP` field is only read by a
/// firmware programming a comparator, which this model does not carry, so the value is here rather than
/// the field.
pub static REGISTERS: [RegisterSpec; 2] = [
    RegisterSpec {
        name: "DWT_CTRL",
        offset: 0x0,
        width: Width::W32,
        reset: 0x4000_0000,
        access: Access::ReadWrite,
        read: ReadAction::None,
        write: WriteModify::None,
        fields: &[
            FieldSpec {
                name: "CYCCNTENA",
                bit: 0,
                width: 1,
                access: Access::ReadWrite,
                read: ReadAction::None,
                write: WriteModify::None,
            },
            FieldSpec {
                name: "NOCYCCNT",
                bit: 25,
                width: 1,
                access: Access::ReadOnly,
                read: ReadAction::None,
                write: WriteModify::None,
            },
        ],
    },
    RegisterSpec {
        name: "DWT_CYCCNT",
        offset: 0x4,
        width: Width::W32,
        reset: 0x0000_0000,
        access: Access::ReadWrite,
        read: ReadAction::None,
        write: WriteModify::None,
        fields: &[FieldSpec {
            name: "CYCCNT",
            bit: 0,
            width: 32,
            access: Access::ReadWrite,
            read: ReadAction::None,
            write: WriteModify::None,
        }],
    },
];

/// One cycle counter, counting at the core clock.
///
/// The counter is not stored. It is what the elapsed virtual time comes to at the core clock, which is
/// the only way it stays consistent with the single clock: an instruction's time is the target's
/// to set, and nothing here keeps a second count of instructions. What the model holds is where the
/// current run of counting started and what was on the clock before it.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Dwt {
    /// What the core clock runs at, which is what the counter counts.
    clock_hz: u64,
    /// Cycles counted before the current run, from earlier runs and from a firmware's own writes.
    held: u64,
    /// When the current run began, or `None` while the counter is disabled.
    since: Option<VirtualNanos>,
}

impl Dwt {
    /// A unit whose core clock runs at `clock_hz` until the part's tree drives it otherwise.
    #[must_use]
    pub fn with_clock(clock_hz: u64) -> Self {
        Self {
            clock_hz,
            held: 0,
            since: None,
        }
    }

    /// Cycles at the core clock over a span of virtual time.
    ///
    /// DDI 0403E.b C1.8.8: "When enabled, CYCCNT increments on each processor clock cycle." The
    /// rate is the model's, and the span is the runtime's. Nothing here counts instructions of its
    /// own.
    fn cycles(&self, span: VirtualNanos) -> u64 {
        u128::from(span.0)
            .saturating_mul(u128::from(self.clock_hz))
            .checked_div(1_000_000_000)
            .and_then(|cycles| u64::try_from(cycles).ok())
            .unwrap_or(u64::MAX)
    }

    /// What the counter reads at `now`, wrapped to its width.
    ///
    /// DDI 0403E.b C1.8.8: "On overflow, CYCCNT wraps to zero."
    fn counter(&self, now: VirtualNanos) -> u64 {
        let running = match self.since {
            Some(started) => self.cycles(VirtualNanos(now.0.saturating_sub(started.0))),
            None => 0,
        };
        self.held.wrapping_add(running) & dwt_cyccnt::CYCCNT.mask()
    }

    /// Stops the current run, keeping what it counted, so the next enable starts from there.
    fn settle(&mut self, now: VirtualNanos) {
        self.held = self.counter(now);
        self.since = None;
    }
}

impl Device for Dwt {
    type Register = Register;

    /// The core clock has changed: the run so far is banked at the old rate and a new one begins here.
    fn on_clock(
        &mut self,
        hz: u64,
        _registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        let now = context.now();
        if self.since.is_some() {
            self.settle(now);
            self.since = Some(now);
        }
        self.clock_hz = hz;
    }

    fn read(
        &mut self,
        register: Register,
        _registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) -> Option<u64> {
        match register {
            // The file holds what the firmware wrote, which for the counter is only ever where a
            // run started from. What it reads is the clock.
            Register::Cyccnt => Some(self.counter(context.now())),
            Register::Ctrl => None,
        }
    }

    /// `CYCCNT` is the clock's count at `now`, as a read answers it.
    fn peek(
        &self,
        register: Register,
        _registers: &RegisterFile,
        now: VirtualNanos,
    ) -> Option<u64> {
        (register == Register::Cyccnt).then(|| self.counter(now))
    }

    fn write(
        &mut self,
        register: Register,
        _value: u64,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        let now = context.now();
        match register {
            Register::Ctrl => {
                let enabled = dwt_ctrl::CYCCNTENA.is_set(registers.get(Register::Ctrl));
                match (enabled, self.since) {
                    (true, None) => self.since = Some(now),
                    (false, Some(_)) => self.settle(now),
                    _ => {}
                }
            }
            // C1.8.8's purpose is "Shows or sets the value of the processor cycle counter".
            Register::Cyccnt => {
                self.held = registers.get(Register::Cyccnt) & dwt_cyccnt::CYCCNT.mask();
                if self.since.is_some() {
                    self.since = Some(now);
                }
            }
        }
    }
}