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 blocks an ARMv7-M core puts in its own peripheral bus.
//!
//! One of them 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
//!
//! Neither ST manual documents this block. PM0214 rev 10 table 37 gives the address map of the private
//! peripheral bus and covers `0xE000E010` to `0xE000EF44` — the system timer, the interrupt controller,
//! the system control block, the memory protection unit, the floating point unit — and nothing below
//! `0xE000E000`; RM0090 rev 22 does not contain the address at all. 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, which is
//!   the core this part carries. 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 ADR-0010's 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, ADR-0025 | 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 — its purpose is to "show or set the value of the processor cycle counter" — and 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 | ChibiOS's `chSysPolledDelayX`, 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` |
//! | ES0206 rev 24 | The errata sheet describes what ST built around the core and names no block of the core itself, so there is nothing here to model and nothing left unmodelled | not applicable | |
//!
//! # What is not, and why
//!
//! | Departure | Reason |
//! |---|---|
//! | `DEMCR.TRCENA`, the global enable for the whole unit, is not consulted | That register is at `0xE000EDFC`, inside the system control space the emulator implements, and never reaches the runtime, 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 suspends `CYCCNT` while the processor is halted. The runtime already holds virtual time still for a stopped debugger, so the counter does not advance then either; what is missing is only a debugger halting the core without the runtime knowing, which this seam does not have |
//! | The comparators and their mask and function registers, the profiling counters, the program counter sampler, and the identification registers | Nothing has asked for them. The instance covers the eight bytes that are modelled, so each of the rest keeps reading the way the emulator answers it instead of reaching a model with nothing to say |

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

/// 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.
const PPB_BASE: u64 = 0xE000_0000;
const PPB_SIZE: u64 = 0x0010_0000;

/// One stretch of the processor's peripheral bus the runtime answers for.
///
/// The emulator 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 description 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 emulator 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 emulator then answers the whole bus the way it always has.
#[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 are what the instance covers and what a machine description claims a window for;
/// the rest of the page stays the emulator's to answer.
pub const DWT_BASE: u64 = 0xE000_1000;

/// 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; 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 ADR-0010's single clock: an instruction is four nanoseconds and
/// nothing here keeps a second count of them. What the model holds is where the current run of counting
/// started and what was on the clock before it.
#[derive(Debug)]
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,
        }
    }

    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);
                }
            }
        }
    }
}