rigless 0.3.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 reset register of the two USB on-the-go cores, RM0433 rev 9 chapter 57, written against the
//! `otg1_hs_global` layout both cores' global blocks share.
//!
//! **This is one register of the global block, and a deliberate partial.** It covers `OTG_GRSTCTL`
//! and nothing else, because a firmware can't get past the core's reset without it and can get what
//! else it needs from a register file. ChibiOS's `otg_core_reset` waits on this register for the
//! master to go idle and for each reset and flush it asks for to clear, and against a register file
//! nothing it waits on moves. When the global block is modeled whole, that model takes this register
//! over.
//!
//! Nothing enumerates, no endpoint carries a byte, and no host is attached. A firmware brings its
//! console up and writes into it whether or not anyone is listening, which is how the same firmware
//! behaves on a bench with no cable in it.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Reset state | `0x8000 0000`: the master idle and nothing being reset. ST's file gives `0x2000_0000`, which sets reserved bit 29 and leaves `AHBIDL` clear, and `memory_map.rs` corrects it | 57.14.5 | `the_core_comes_out_of_reset_with_its_master_idle` |
//! | `AHBIDL` | Reads set: "Indicates that the AHB master state machine is in the Idle condition", and with no transfer it always is. The field is read-only, so a write can't clear it | 57.14.5 | `a_write_cannot_clear_the_idle_flag` |
//! | `CSRST`, `FCRST`, `RXFFLSH`, `TXFFLSH` | Clear as the write lands. `CSRST` is "a self-clearing bit and the core clears this bit", the two flushes are bits "the core clears", and `FCRST` is "cleared by the core in a few clock cycles" | 57.14.5 | `every_bit_the_core_clears_for_itself_reads_back_clear` |
//! | `PSRST`, which ST's file calls `HSRST`, and `TXFNUM` | Stored and read back as written. The page doesn't say the core clears `PSRST` | 57.14.5 | `the_partial_reset_and_the_fifo_number_are_stored` |
//! | The driver's sequence | ChibiOS's `otg_core_reset`: idle, core soft reset, idle again, then the two flushes, each wait left on its first read | 57.14.5 | `the_drivers_reset_sequence_leaves_every_one_of_its_waits` |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | The global block's other registers, and the device, host and power and clock gating blocks | 57.14 | Register-only: each answers its reset value and keeps what's written. Enumeration, endpoints and the interrupt hierarchy of `OTG_GINTSTS` are the substance of the core, and half of them would be worse than none |
//! | The interrupt lines | Table 147 | This model raises nothing, since the one register it answers for raises nothing |
//! | The eight clocks a flush takes to clear, and the clocks a core soft reset takes | 57.14.5 | The model clears them inside the write, which errs in the direction that can't hang a firmware |
//! | What a core soft reset resets | 57.14.5 | The registers it would clear are register-only, and nothing yet asks for them cleared |
//! | ES0392 rev 15 2.25.6: a false `OTG_GINTSTS.CIDSCHG` interrupt after a reset, `OTG_GRSTCTL.CSRST`'s among them | 2.25.6 | Applicable to revision V. `OTG_GINTSTS` is register-only in this partial, so no reset sets the flag |
//! | ES0392 rev 15 section 2.25: the other eight errata on this core | 2.25.1 to 2.25.5, 2.25.7 to 2.25.9 | Each concerns a transfer, a hub, the battery charger or the PHY, and none touches this register |

use crate::device::{Device, DeviceContext, RegisterFile};
use crate::soc::stm32h743::generated::otg1_hs_global::{Register, otg_hs_grstctl};

/// The two cores' global blocks, in the order a part holds them.
pub const INSTANCES: [&str; 2] = ["OTG1_HS_GLOBAL", "OTG2_HS_GLOBAL"];

/// `OTG_GRSTCTL` out of reset, 57.14.5: the master idle.
pub const GRSTCTL_RESET: u64 = otg_hs_grstctl::AHBIDL.mask();

/// Every bit of `OTG_GRSTCTL` the core clears for itself, 57.14.5.
const SELF_CLEARING: u64 = otg_hs_grstctl::CSRST.mask()
    | otg_hs_grstctl::FCRST.mask()
    | otg_hs_grstctl::RXFFLSH.mask()
    | otg_hs_grstctl::TXFFLSH.mask();

/// One USB core, as far as its reset register.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Otg;

impl Device for Otg {
    type Register = Register;

    fn write(
        &mut self,
        register: Register,
        _written: u64,
        registers: &mut RegisterFile,
        _context: &mut DeviceContext<'_>,
    ) {
        if register == Register::OtgHsGrstctl {
            let held = registers.get(Register::OtgHsGrstctl);
            registers.set(Register::OtgHsGrstctl, held & !SELF_CLEARING);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mcu::{Operate, Single};
    use crate::soc::stm32h743::builder;
    use crate::soc::stm32h743::generated::OTG1_HS_GLOBAL;
    use crate::soc::{Layout, Width};
    use crate::time::VirtualNanos;

    const AT: VirtualNanos = VirtualNanos(0);
    fn grstctl() -> u64 {
        OTG1_HS_GLOBAL.base + u64::from(Register::OtgHsGrstctl.spec().offset)
    }

    fn core() -> Single<Otg> {
        Single::new(builder(), INSTANCES[0], Otg).unwrap()
    }

    #[test]
    fn the_core_comes_out_of_reset_with_its_master_idle() {
        assert_eq!(core().read(AT, grstctl(), Width::W32), Ok(0x8000_0000));
    }

    #[test]
    fn a_write_cannot_clear_the_idle_flag() {
        let mut core = core();
        core.write(AT, grstctl(), Width::W32, 0).unwrap();
        assert_eq!(core.read(AT, grstctl(), Width::W32), Ok(GRSTCTL_RESET));
    }

    #[test]
    fn every_bit_the_core_clears_for_itself_reads_back_clear() {
        let mut core = core();
        for bit in [
            otg_hs_grstctl::CSRST,
            otg_hs_grstctl::FCRST,
            otg_hs_grstctl::RXFFLSH,
            otg_hs_grstctl::TXFFLSH,
        ] {
            core.write(AT, grstctl(), Width::W32, bit.mask()).unwrap();
            assert_eq!(
                core.read(AT, grstctl(), Width::W32),
                Ok(GRSTCTL_RESET),
                "{}",
                bit.name
            );
        }
    }

    #[test]
    fn the_partial_reset_and_the_fifo_number_are_stored() {
        let mut core = core();
        let written = otg_hs_grstctl::HSRST.mask() | otg_hs_grstctl::TXFNUM.set(0, 0x10);
        core.write(AT, grstctl(), Width::W32, written).unwrap();
        assert_eq!(
            core.read(AT, grstctl(), Width::W32),
            Ok(GRSTCTL_RESET | written)
        );
    }

    /// ChibiOS's `otg_core_reset`: wait for the master to go idle, reset the core and wait for the
    /// reset to clear, wait for idle again, then flush every transmit FIFO and the receive FIFO, each
    /// waited on until it clears.
    #[test]
    fn the_drivers_reset_sequence_leaves_every_one_of_its_waits() {
        let mut core = core();
        let read = |core: &mut Single<Otg>| core.read(AT, grstctl(), Width::W32).unwrap();
        assert_ne!(read(&mut core) & otg_hs_grstctl::AHBIDL.mask(), 0);
        core.write(AT, grstctl(), Width::W32, otg_hs_grstctl::CSRST.mask())
            .unwrap();
        assert_eq!(read(&mut core) & otg_hs_grstctl::CSRST.mask(), 0);
        assert_ne!(read(&mut core) & otg_hs_grstctl::AHBIDL.mask(), 0);
        let flush_all = otg_hs_grstctl::TXFNUM.set(0, 0x10) | otg_hs_grstctl::TXFFLSH.mask();
        core.write(AT, grstctl(), Width::W32, flush_all).unwrap();
        assert_eq!(read(&mut core) & otg_hs_grstctl::TXFFLSH.mask(), 0);
        core.write(AT, grstctl(), Width::W32, otg_hs_grstctl::RXFFLSH.mask())
            .unwrap();
        assert_eq!(read(&mut core) & otg_hs_grstctl::RXFFLSH.mask(), 0);
    }
}