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 microcontroller debug unit of RM0433 rev 9 section 60.5.8, written against the `dbgmcu` layout.
//! Every citation is RM0433 rev 9 unless it names another document.
//!
//! The `DBGMCU` is ST's own extension of the Cortex-M7's debug infrastructure. The core reaches it at
//! `0x5C00_1000`, and a debugger reaches the same registers on the APB-D bus at `0xE00E_1000`, which
//! the emulator's debug stub stands in for. It controls the clocks while a core sleeps, stops or
//! stands by under debug, and which peripherals freeze when the core halts in debug. This is the
//! same part of the chip as the F427's, and the model is its kind: the registers store, and nothing
//! behind them can act, see the second table.
//!
//! **What the model handles and what falls to the register file.** It has no behavior beyond the file's.
//! The registers answer from the values the model keeps in the file: the file applies the reset values,
//! which `memory_map.rs` corrects for the identity code, and the access types.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Reset state | Every register at its manual value, `DBGMCU_IDC` corrected to this part's revision, and the other five at zero. `memory_map.rs` carries the correction, since a reset value belongs to the instance | 60.5.8 | `every_register_answers_its_manual_reset_value` |
//! | `DBGMCU_IDC` `DEV_ID` | The device identifier `0x450` of the STM32H742, H743, H753 and H750, read-only | 60.5.8 | `every_register_answers_its_manual_reset_value`, `software_cannot_write_the_read_only_idcode` |
//! | `DBGMCU_IDC` `REV_ID` | The silicon revision, `0x2003` for revision V, picked in `memory_map.rs` since a reset value belongs to the instance. ChibiOS and ArduPilot read none of it on this part | 60.5.8, ES0392 rev 15 Table 2 | `every_register_answers_its_manual_reset_value` |
//! | `DBGMCU_CR` `DBGSLEEP_D1`, `DBGSTOP_D1`, `DBGSTBY_D1` | Store what a debugger or the firmware writes. What they would do, keep the clocks running while the core is in a low-power mode under debug, is a mode this model has no way to enter, see the second table | 60.5.8 | `dbgmcu_cr_stores_every_defined_bit` |
//! | `DBGMCU_CR` `TRACECLKEN`, `D1DBGCKEN`, `D3DBGCKEN`, `TRGOEN` | Store what is written. No trace port, debug clock domain or trigger pin answers behind them, see the second table | 60.5.8 | `dbgmcu_cr_stores_every_defined_bit` |
//! | `DBGMCU_APB3FZ1` | The `WWDG1` freeze bit stores what is written | 60.5.8 | `every_freeze_register_stores_the_bits_the_manual_gives_it` |
//! | `DBGMCU_APB1LFZ1` | Every freeze bit stores what is written, `DBG_TIM12` among them, which ChibiOS sets when TIM12 is the system timer | 60.5.8 | `every_freeze_register_stores_the_bits_the_manual_gives_it`, `chibios_freezes_its_system_timer_in_debug` |
//! | `DBGMCU_APB2FZ1` | Every freeze bit stores what is written | 60.5.8 | `every_freeze_register_stores_the_bits_the_manual_gives_it` |
//! | `DBGMCU_APB4FZ1` | Every freeze bit stores what is written | 60.5.8 | `every_freeze_register_stores_the_bits_the_manual_gives_it` |
//! | System reset | The registers of `DBGMCU_CR` and the four freeze registers are not reset by a system reset, "only by a power on reset". [`KEPT_BY_A_SYSTEM_RESET`] lists them, for the part's reset to carry over | 60.5.8 | the part's reset |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | `DBGSLEEP_D1`, `DBGSTOP_D1` and `DBGSTBY_D1` keeping the clocks and the supply while the core sleeps, stops or stands by | 60.5.8 | The core enters Sleep, Stop or Standby inside the emulator, which sends the runtime no event for it, as `pwr.rs` of the STM32F427 records for the same reason. The bits store and read back |
//! | A freeze bit keeping its peripheral counting, or stopping it, while a debugger halts the core | 60.5.8 | Virtual time is one clock the whole target shares, driven by the instruction count. A debugger halt stops that count for every model at once, so every counter is frozen for the whole halt whatever its bit says. At power-on every bit is clear, which says the peripherals keep running under a halt, so the halt itself is a departure from the reset default. Letting an unset bit mean something would need a second, per-instance clock |
//! | A timer's outputs disabled when `DBG_TIMx` freezes it | 60.5.8, 17.3.21 | The same gap: the halt isn't modeled, so nothing this would gate is either |
//! | The trace clock, the D1 and D3 debug clock domains and the bidirectional trigger pin | 60.5.8 | No trace port, no debug domain split and no trigger pin exist behind the bits. They are stored |
//! | `DBGSLEEP_D2`, `DBGSTOP_D2`, `DBGSTBY_D2`, `DBGSTOP_D3` and `DBGSTBY_D3` of the file | 60.5.8 | ST's file names bits the manual reserves, 60.5.8 giving `DBGMCU_CR` bits 19:3 as reserved. They belong to the dual-core parts, and are stored like any reserved bit |
//! | The page's typos: `DBG_IIWDG1` for `DBG_IWDG1`, and `DBG_LPTIM3`'s text naming LPTIM2 | 60.5.8 | The file's field names are kept. Bit 10 of `DBGMCU_APB4FZ1` is LPTIM3's |
//! | Reserved bits of the registers reading back what was written | 60.5.8 | No field covers a reserved bit in any register of this part, so a register's whole-register write rule is what lands there. It's the same for every reserved bit of every register this part models, and ArduPilot writes none of these |
//! | A narrower access than the word | 60.5.8 | The register file lands a byte or half-word store in its lane regardless |
//! | The debugger's own address, `0xE00E_1000` | 60.5.8 | The debug stub reaches the core, and what it asks of this unit is the bus's |
//! | `DBGMCU_IDC` readable and `DBGMCU_CR` writable "under system reset" | 60.5.8 | Already true without a line of code. A rigless reset restarts the core between one access and the next rather than holding it through an interval |
//! | ES0392 rev 15 | ES0392 rev 15 | No item touches this instance. The whole errata chapter was read for it. Its one mention of the `DBG_RTC` effect, the RTC's alarm flag set again when the core stops in debug, is an item of the RTC |
//!
//! **The revision this part reports.** `memory_map.rs` corrects `DBGMCU_IDC`'s reset value against
//! section 60.5.8 and gives its reason there, since a reset value belongs to the instance.

use crate::device::Device;

use crate::soc::stm32h743::generated::dbgmcu::Register;

/// The registers a system reset leaves alone: all but the identity code. Section 60.5.8: "The DBGMCU
/// registers are not reset by a system reset, only by a power on reset." The part copies their values
/// across a reset, as the identity code reads the same in any case.
pub const KEPT_BY_A_SYSTEM_RESET: [Register; 5] = [
    Register::Cr,
    Register::Apb3fz1,
    Register::Apb1lfz1,
    Register::Apb2fz1,
    Register::Apb4fz1,
];

/// The microcontroller debug unit of one STM32H743.
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Dbg;

impl Device for Dbg {
    type Register = Register;
}

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

    const NOW: VirtualNanos = VirtualNanos(0);

    fn dbgmcu() -> Single<Dbg> {
        Single::new(builder(), "DBGMCU", Dbg).expect("the bus builds")
    }

    fn address(register: Register) -> u64 {
        DBGMCU.base + u64::from(register.spec().offset)
    }

    fn read(soc: &mut Single<Dbg>, register: Register) -> u64 {
        soc.read(NOW, address(register), Width::W32).unwrap()
    }

    fn write(soc: &mut Single<Dbg>, register: Register, value: u64) {
        soc.write(NOW, address(register), Width::W32, value)
            .unwrap();
    }

    /// The bits of every field of `register` the file defines.
    fn defined(register: Register) -> u64 {
        register
            .spec()
            .fields
            .iter()
            .fold(0, |bits, field| bits | field.mask())
    }

    #[test]
    fn every_register_answers_its_manual_reset_value() {
        let mut soc = dbgmcu();
        assert_eq!(
            read(&mut soc, Register::Idc),
            0x2003_6450,
            "revision V's REV_ID and 0x450's DEV_ID, 60.5.8"
        );
        assert_eq!(
            Register::Idc.spec().fields[0].get(read(&mut soc, Register::Idc)),
            0x450,
            "DEV_ID"
        );
        for register in KEPT_BY_A_SYSTEM_RESET {
            assert_eq!(read(&mut soc, register), 0, "{}", register.spec().name);
        }
    }

    #[test]
    fn software_cannot_write_the_read_only_idcode() {
        let mut soc = dbgmcu();
        write(&mut soc, Register::Idc, 0);
        assert_eq!(read(&mut soc, Register::Idc), 0x2003_6450);
    }

    #[test]
    fn dbgmcu_cr_stores_every_defined_bit() {
        let mut soc = dbgmcu();
        let every_bit = defined(Register::Cr);
        write(&mut soc, Register::Cr, every_bit);
        assert_eq!(read(&mut soc, Register::Cr), every_bit);
        for field in [
            cr::DBGSLEEP_D1,
            cr::DBGSTOP_D1,
            cr::DBGSTBY_D1,
            cr::TRACECLKEN,
            cr::D1DBGCKEN,
            cr::D3DBGCKEN,
            cr::TRGOEN,
        ] {
            assert!(field.is_set(read(&mut soc, Register::Cr)), "{}", field.name);
        }
        write(&mut soc, Register::Cr, 0);
        assert_eq!(read(&mut soc, Register::Cr), 0);
    }

    #[test]
    fn every_freeze_register_stores_the_bits_the_manual_gives_it() {
        let mut soc = dbgmcu();
        for register in [
            Register::Apb3fz1,
            Register::Apb1lfz1,
            Register::Apb2fz1,
            Register::Apb4fz1,
        ] {
            let every_bit = defined(register);
            assert_ne!(every_bit, 0, "{}", register.spec().name);
            write(&mut soc, register, every_bit);
            assert_eq!(
                read(&mut soc, register),
                every_bit,
                "{}",
                register.spec().name
            );
            for field in register.spec().fields {
                assert!(field.is_set(read(&mut soc, register)), "{}", field.name);
            }
            write(&mut soc, register, 0);
            assert_eq!(read(&mut soc, register), 0);
        }
    }

    /// ChibiOS's `STM32_DBGMCU_TIM12_STOP()` is `DBGMCU->APB1LFZ1 |= DBGMCU_APB1LFZ1_DBG_TIM12`, which the
    /// system timer's driver runs when the board makes TIM12 the timer, as the `MatekH743` does.
    #[test]
    fn chibios_freezes_its_system_timer_in_debug() {
        let mut soc = dbgmcu();
        let current = read(&mut soc, Register::Apb1lfz1);
        write(
            &mut soc,
            Register::Apb1lfz1,
            current | apb1lfz1::DBG_TIM12.mask(),
        );
        let frozen = read(&mut soc, Register::Apb1lfz1);
        assert!(apb1lfz1::DBG_TIM12.is_set(frozen), "DBG_TIM12, 60.5.8");
        assert_eq!(
            frozen,
            apb1lfz1::DBG_TIM12.mask(),
            "and no other timer is frozen"
        );
    }
}