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 system configuration controller of RM0433 rev 9 chapter 12, written against the `syscfg`
//! layout. Every citation is RM0433 rev 9 unless it names another document.
//!
//! The controller is configuration the rest of the part reads. Most of what it holds has no consumer
//! in the emulator yet, and the model keeps each register as the firmware wrote it. One behavior is its
//! own: the compensation cell's `READY` flag, which follows the enable.
//!
//! **What the model handles and what falls to the register file.** The model has an arm for every
//! register of the chapter. Only `SYSCFG_CCCSR` does anything beyond the file, which
//! applies the access types, the resets and the stores. The external interrupt controller reads
//! `SYSCFG_EXTICR1` to `SYSCFG_EXTICR4` through a requested access, so the registers stay the single
//! place the routing lives, and nothing here is told when the firmware writes them.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Reset state | Every register at the value the file gives, zero. The manual gives `X` to the package-dependent bits and to every user register, see the second table | 12.3.1 to 12.3.28 | `every_register_resets_to_the_files_value` |
//! | `SYSCFG_PMCR` | Stored: the Fm+ bits of the four I2C instances and of PB6 to PB9, the booster, the Ethernet PHY interface and the four analog switches. What they act on has no model, see the second table | 12.3.1 | `pmcr_stores_every_defined_field` |
//! | `SYSCFG_EXTICR1` to `SYSCFG_EXTICR4` | Stored: four bits per EXTI line choosing the port whose pin feeds it, 0 for port A to 10 for port K. The external interrupt controller reads them, and its own tests route an edge by them | 12.3.2 to 12.3.5, 20.4 | `each_exti_line_stores_its_port_selection` |
//! | `SYSCFG_CCCSR` `EN`, `CS`, `HSLV` | Stored. `CS` chooses the code the cell gives or `SYSCFG_CCCR` does, and `HSLV` is for supplies under 2.7 V, with no effect on anything modeled | 12.3.7, 11.3.11 | `the_compensation_cell_control_bits_are_stored` |
//! | `SYSCFG_CCCSR` `READY` | Read-only, and set by the hardware: it follows `EN`. The manual gives no start-up time, so the cell is ready at the write that enables it, and not ready again when it is disabled | 12.3.7, 11.3.11 | `ready_follows_the_enable`, `ready_is_read_only` |
//! | `SYSCFG_CCVR` | Read-only, the code the cell measures. It reads zero, since the cell has no model | 12.3.8 | `ccvr_pkgr_and_the_user_registers_ignore_writes` |
//! | `SYSCFG_CCCR` | Stored: the code that applies when `CS` is set | 12.3.9 | `the_compensation_code_is_stored` |
//! | `SYSCFG_PWRCR` `ODEN` | Stored, one bit wide. It lets the regulator generate VOS0, see the second table | 12.3.10 | `oden_is_stored_as_one_bit` |
//! | `SYSCFG_PKGR` | Read-only. The file gives zero, which is `LQFP100` | 12.3.11 | `ccvr_pkgr_and_the_user_registers_ignore_writes` |
//! | `SYSCFG_UR0` to `SYSCFG_UR17` | Read as the file types them and give zero. `UR2`, `UR3` and `UR14` take writes and the others ignore them | 12.3.12 to 12.3.28 | `ccvr_pkgr_and_the_user_registers_ignore_writes`, `the_writable_user_registers_are_stored` |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | `SYSCFG_CFGR`, the double ECC error lock bits `AXISRAML` to `PVDL` | 12.3.6 | ST's file has no register at `0x18`. The bus has nothing to give a read or a write there, and the layout has no row to match. Neither ChibiOS nor ArduPilot touches it. The lock bits connect memory error flags to the timer break inputs, which no model carries |
//! | What `PMCR` acts on: the Fm+ drive of I2C and PB6 to PB9, the booster, the Ethernet PHY interface, and the analog switches of PA0, PA1, PC2 and PC3 | 12.3.1 | The I2C, Ethernet and ADC models have no input for them. The bits are stored and each reads back |
//! | The reset value's `X`: `PMCR` bits 27 to 24 by package, `PKGR`, and every user register from the option bytes | 12.3.1, 12.3.11 to 12.3.28 | The file gives zero. The package isn't named by the part, and the option bytes' values live in the flash model, with no seam for a model to read another's at reset. No firmware in ChibiOS or ArduPilot reads any of them. They read as the file gives them, so `RDP` reads 0, which the manual calls level 1 |
//! | The cell's measured code in `SYSCFG_CCVR`, and the `READY` flag's dependence on the CSI clock and on a start-up time | 12.3.7, 12.3.8, 11.3.11 | The manual says the cell is not ready without the CSI clock and gives no time for it to become so. The RCC is another model, so `READY` follows `EN` alone. Nothing in ChibiOS or ArduPilot enables the cell or reads `READY` |
//! | The user registers' access types | 12.3.12 to 12.3.28 | The pages type `BOOT_ADD0` of `UR2`, all of `UR3`, `D1SBRST` of `UR13` and `D1STPRST` of `UR14` read-write and every other bit read-only. The file types `UR2`, `UR3` and `UR14` whole read-write and `UR13` whole read-only, so `BORH` takes a write here that the page forbids and `D1SBRST` refuses one that it allows. The bits mirror option bytes nothing reads, so they're left as the file has them |
//! | `ODEN` acting on the regulator | 12.3.10 | The voltage regulator is the power controller's, which has no input for it yet. The bit is stored and reads back |
//! | A routing change moving a line | 12.3.2 | A write to `SYSCFG_EXTICRn` that changes which pin feeds a line can change the level the line sees. The edge detector would see it. Nothing here tells the external interrupt controller of the write, which finds the routing only when a pin changes |
//! | ES0392 rev 15 2.11.3: the VREFBUF trimming code isn't loaded after reset, and the workaround reads it at `0x5800 0574` | ES0392 rev 15 | Not applicable. Table 3 marks it for revisions Y and W alone, and this part is revision V. The address lies inside this instance's window, and no register is there |
//! | No other ES0392 rev 15 item touches this instance | ES0392 rev 15 | The whole errata chapter was read for it |
//!
//! **Where this model departs from the vendor's file.** `SYSCFG_PWRCR`'s `ODEN` is four bits wide in
//! the file where section 12.3.10 gives it one, so [`ODEN`] restates it. `SYSCFG_CCCSR`'s `READY` is
//! read-write in the file where section 12.3.7 types it `r`, so [`READY`] restates it.

use crate::device::{Device, DeviceContext, RegisterFile};
use crate::soc::stm32h743::generated::syscfg::{Register, cccsr, pwrcr};
use crate::soc::{Access, FieldSpec, WriteModify};

/// `SYSCFG_PWRCR`'s `ODEN`, section 12.3.10: bit 0 alone. The file has it four bits wide.
pub static ODEN: FieldSpec = FieldSpec {
    width: 1,
    ..*pwrcr::ODEN
};

/// `SYSCFG_CCCSR`'s `READY`, section 12.3.7: type `r`, set by the hardware and never by a write. The
/// file types it read-write.
pub static READY: FieldSpec = cccsr::READY
    .with_access(Access::ReadOnly)
    .with_write(WriteModify::None);

/// The system configuration controller.
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Syscfg;

impl Device for Syscfg {
    type Register = Register;

    fn write(
        &mut self,
        register: Register,
        _value: u64,
        registers: &mut RegisterFile,
        _context: &mut DeviceContext<'_>,
    ) {
        match register {
            // The manual states no start-up time, so ready follows the enable at the write, 12.3.7.
            Register::Cccsr => {
                let control = registers.get(Register::Cccsr);
                let ready = cccsr::EN.is_set(control);
                registers.set(Register::Cccsr, READY.set(control, u64::from(ready)));
            }
            // 12.3.1: peripheral mode, stored.
            Register::Pmcr
            // 12.3.10: the overdrive enable, stored.
            | Register::Pwrcr
            // 12.3.2 to 12.3.5: the routing of EXTI lines to ports, which the external interrupt controller reads.
            | Register::Exticr1
            | Register::Exticr2
            | Register::Exticr3
            | Register::Exticr4
            // 12.3.8, 12.3.9: the compensation cell's value and code.
            | Register::Ccvr
            | Register::Cccr
            // 12.3.11 to 12.3.28: the package and the option bytes' mirror.
            | Register::Pkgr
            | Register::Ur0
            | Register::Ur2
            | Register::Ur3
            | Register::Ur4
            | Register::Ur5
            | Register::Ur6
            | Register::Ur7
            | Register::Ur8
            | Register::Ur9
            | Register::Ur10
            | Register::Ur11
            | Register::Ur12
            | Register::Ur13
            | Register::Ur14
            | Register::Ur15
            | Register::Ur16
            | Register::Ur17 => {}
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mcu::{Operate, Single};
    use crate::soc::stm32h743::builder;
    use crate::soc::stm32h743::generated::SYSCFG;
    use crate::soc::stm32h743::generated::syscfg::{
        cccr, exticr1, exticr2, exticr4, pmcr, ur2, ur14,
    };
    use crate::soc::{Layout, Width};
    use crate::time::VirtualNanos;

    const NOW: VirtualNanos = VirtualNanos(0);

    fn syscfg() -> Single<Syscfg> {
        Single::new(builder(), "SYSCFG", Syscfg).expect("the bus builds")
    }

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

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

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

    #[test]
    fn every_register_resets_to_the_files_value() {
        let mut soc = syscfg();
        for register in Register::ALL {
            assert_eq!(
                read(&mut soc, register),
                register.spec().reset,
                "{}",
                register.spec().name
            );
        }
    }

    #[test]
    fn pmcr_stores_every_defined_field() {
        let mut soc = syscfg();
        let every_bit = Register::Pmcr
            .spec()
            .fields
            .iter()
            .fold(0, |bits, field| bits | field.mask());
        write(&mut soc, Register::Pmcr, every_bit);
        assert_eq!(read(&mut soc, Register::Pmcr), every_bit, "12.3.1");
        assert_eq!(
            pmcr::EPIS.get(read(&mut soc, Register::Pmcr)),
            0b111,
            "the PHY interface selection is three bits"
        );
        write(&mut soc, Register::Pmcr, 0);
        assert_eq!(read(&mut soc, Register::Pmcr), 0);
    }

    #[test]
    fn each_exti_line_stores_its_port_selection() {
        let mut soc = syscfg();
        let first = [
            exticr1::EXTI0,
            exticr1::EXTI1,
            exticr1::EXTI2,
            exticr1::EXTI3,
        ];
        let value = first
            .iter()
            .zip([0, 4, 10, 7])
            .fold(0, |value, (field, port)| field.set(value, port));
        write(&mut soc, Register::Exticr1, value);
        let stored = read(&mut soc, Register::Exticr1);
        for (field, port) in first.iter().zip([0, 4, 10, 7]) {
            assert_eq!(field.get(stored), port, "{}, 12.3.2", field.name);
        }
        write(&mut soc, Register::Exticr4, exticr4::EXTI15.set(0, 2));
        assert_eq!(
            exticr4::EXTI15.get(read(&mut soc, Register::Exticr4)),
            2,
            "12.3.5"
        );
        write(&mut soc, Register::Exticr2, exticr2::EXTI5.set(0, 0xB));
        assert_eq!(
            read(&mut soc, Register::Exticr2),
            exticr2::EXTI5.set(0, 0xB),
            "a reserved selection is stored, and selects no port, 12.3.3"
        );
    }

    #[test]
    fn the_compensation_cell_control_bits_are_stored() {
        let mut soc = syscfg();
        write(
            &mut soc,
            Register::Cccsr,
            cccsr::CS.mask() | cccsr::HSLV.mask(),
        );
        let stored = read(&mut soc, Register::Cccsr);
        assert!(cccsr::CS.is_set(stored), "12.3.7");
        assert!(cccsr::HSLV.is_set(stored));
        assert!(!cccsr::EN.is_set(stored));
        assert!(!cccsr::READY.is_set(stored), "not enabled, not ready");
    }

    #[test]
    fn ready_follows_the_enable() {
        let mut soc = syscfg();
        write(&mut soc, Register::Cccsr, cccsr::EN.mask());
        assert!(
            cccsr::READY.is_set(read(&mut soc, Register::Cccsr)),
            "enabled, the cell is ready, 12.3.7"
        );
        write(
            &mut soc,
            Register::Cccsr,
            cccsr::EN.mask() | cccsr::CS.mask(),
        );
        assert!(
            cccsr::READY.is_set(read(&mut soc, Register::Cccsr)),
            "a write that leaves it enabled leaves it ready"
        );
        write(&mut soc, Register::Cccsr, 0);
        assert_eq!(
            read(&mut soc, Register::Cccsr),
            0,
            "disabled, it isn't ready"
        );
    }

    #[test]
    fn ready_is_read_only() {
        let mut soc = syscfg();
        write(&mut soc, Register::Cccsr, cccsr::READY.mask());
        assert!(
            !cccsr::READY.is_set(read(&mut soc, Register::Cccsr)),
            "software can't make the cell ready, 12.3.7"
        );
    }

    #[test]
    fn the_compensation_code_is_stored() {
        let mut soc = syscfg();
        write(
            &mut soc,
            Register::Cccr,
            cccr::PCC.set(cccr::NCC.set(0, 0x3), 0xC),
        );
        let stored = read(&mut soc, Register::Cccr);
        assert_eq!(cccr::NCC.get(stored), 0x3, "12.3.9");
        assert_eq!(cccr::PCC.get(stored), 0xC);
    }

    #[test]
    fn ccvr_pkgr_and_the_user_registers_ignore_writes() {
        let mut soc = syscfg();
        for register in [
            Register::Ccvr,
            Register::Pkgr,
            Register::Ur0,
            Register::Ur4,
            Register::Ur5,
            Register::Ur6,
            Register::Ur7,
            Register::Ur8,
            Register::Ur9,
            Register::Ur10,
            Register::Ur11,
            Register::Ur12,
            Register::Ur13,
            Register::Ur15,
            Register::Ur16,
            Register::Ur17,
        ] {
            write(&mut soc, register, 0xFFFF_FFFF);
            assert_eq!(
                read(&mut soc, register),
                register.spec().reset,
                "{} is read-only, 12.3.8 to 12.3.28",
                register.spec().name
            );
        }
    }

    #[test]
    fn the_writable_user_registers_are_stored() {
        let mut soc = syscfg();
        write(&mut soc, Register::Ur2, ur2::BOOT_ADD0.set(0, 0x0800));
        assert_eq!(
            ur2::BOOT_ADD0.get(read(&mut soc, Register::Ur2)),
            0x0800,
            "12.3.13"
        );
        write(&mut soc, Register::Ur14, ur14::D1STPRST.mask());
        assert!(
            ur14::D1STPRST.is_set(read(&mut soc, Register::Ur14)),
            "the file types it read-write, and it is stored as such"
        );
    }

    #[test]
    fn oden_is_stored_as_one_bit() {
        let mut soc = syscfg();
        write(&mut soc, Register::Pwrcr, 1);
        assert_eq!(read(&mut soc, Register::Pwrcr), 1, "12.3.10");
        assert_eq!(ODEN.width, 1, "one bit, where the file has four");
        let row = Register::Pwrcr.index();
        let oden = Operate::registers(&soc, "SYSCFG")
            .expect("an instance")
            .fields(row)
            .iter()
            .find(|field| field.name == "ODEN")
            .copied()
            .expect("the register has ODEN");
        assert_eq!(oden.width, 1, "the part carries the correction");
    }
}