rigless 0.0.2

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; needs the rigless-sidecar package from rigless-labs/qemu
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: Apache-2.0
//! The memory map of the STM32F427 and every correction of the vendor's file against RM0090 rev 22.

use crate::soc::{Builder, Cpu, Error, Peripheral, Region, Soc, WriteModify};

use super::generated;

/// The memory map from RM0090 rev 22, section 2.3.
///
/// The system region covers system memory, the OTP area, and the unique device ID and flash
/// size words at `0x1FFF_7A10` and `0x1FFF_7A22` that ChibiOS reads.
pub static MEMORY: [Region; 5] = [
    Region::flash("flash", 0x0800_0000, 2 << 20),
    Region::ram("ccm", 0x1000_0000, 64 << 10),
    Region::system("system", 0x1FFF_0000, 32 << 10),
    Region::ram("sram", 0x2000_0000, 192 << 10),
    Region::ram("backup_sram", 0x4002_4000, 4 << 10),
];

/// Interrupt lines on the STM32F427, RM0090 rev 22, table 63; ST's file names no count.
const INTERRUPT_LINES: u16 = 91;
/// The SAI1 interrupt line, RM0090 rev 22, table 63; ST's file omits it.
const SAI1_INTERRUPT: u16 = 87;
/// The DMA2D interrupt line, RM0090 rev 22, table 63; ST's file omits it.
const DMA2D_INTERRUPT: u16 = 90;

/// The CPU, with the interrupt count the reference manual gives.
pub static CPU: Cpu = Cpu {
    interrupt_lines: INTERRUPT_LINES,
    ..generated::CPU
};

/// Every instance, with the two interrupt lines ST's file omits filled in.
fn instances() -> impl Iterator<Item = Peripheral> {
    generated::PERIPHERALS
        .iter()
        .map(|&peripheral| match peripheral.name {
            "SAI1" => Peripheral {
                interrupts: &[SAI1_INTERRUPT],
                ..peripheral
            },
            "DMA2D" => Peripheral {
                interrupts: &[DMA2D_INTERRUPT],
                ..peripheral
            },
            _ => peripheral,
        })
}

/// Every timer: the manual types every flag of each one's status register `rc_w0`, cleared by writing zero and
/// left alone by writing one, and ST's file carries no write rules at all. RM0090 rev 22 sections 17.4.5 (TIM1
/// and TIM8), 18.4.5 (TIM2 to TIM5), 19.4.4 (TIM9 and TIM12), 19.5.3 (TIM10, TIM11, TIM13, TIM14), and 20.4.4
/// (TIM6 and TIM7).
const TIMERS: [&str; 14] = [
    "TIM1", "TIM2", "TIM3", "TIM4", "TIM5", "TIM6", "TIM7", "TIM8", "TIM9", "TIM10", "TIM11",
    "TIM12", "TIM13", "TIM14",
];

/// The STM32F427 with its memory map and every correction of the file against the manual, ready for a machine
/// to bind models to.
pub fn builder() -> Builder {
    TIMERS
        .iter()
        .fold(Soc::builder(CPU, &MEMORY, instances()), |builder, timer| {
            builder.write_modify(timer, "SR", WriteModify::ZeroToClear)
        })
}

/// The STM32F427 with every instance register-only.
pub fn soc() -> Result<Soc, Error> {
    builder().build()
}

#[cfg(test)]
mod tests {
    use crate::soc::{Layout, Width};

    use super::generated::spi1;
    use super::*;
    use crate::time::VirtualNanos;

    #[test]
    fn the_soc_constructs_with_every_instance() {
        let soc = soc().unwrap();
        assert_eq!(soc.peripherals().count(), generated::PERIPHERALS.len());
        assert_eq!(soc.cpu().interrupt_lines, INTERRUPT_LINES);
        assert!(
            soc.memory()
                .iter()
                .any(|region| region.name == "backup_sram")
        );
    }

    #[test]
    fn the_lines_the_vendor_omitted_are_filled_in() {
        let soc = soc().unwrap();
        assert_eq!(
            soc.peripheral("SAI1")
                .map(|peripheral| peripheral.interrupts),
            Some(&[SAI1_INTERRUPT][..])
        );
        assert_eq!(
            soc.peripheral("DMA2D")
                .map(|peripheral| peripheral.interrupts),
            Some(&[DMA2D_INTERRUPT][..])
        );
        assert!(
            generated::SAI1.interrupts.is_empty(),
            "the generated table is the vendor's, untouched"
        );
    }

    #[test]
    fn instances_derived_from_a_sibling_share_its_table() {
        let soc = soc().unwrap();
        let (spi1, spi2) = (
            soc.peripheral("SPI1").unwrap(),
            soc.peripheral("SPI2").unwrap(),
        );
        assert!(std::ptr::eq(spi1.registers, spi2.registers));
        assert_eq!(spi2.base, 0x4000_3800);
    }

    #[test]
    fn a_register_reads_its_reset_value_through_the_bus() {
        let mut soc = soc().unwrap();
        let status = generated::SPI1.base + u64::from(spi1::Register::Sr.spec().offset);
        assert_eq!(
            soc.read(VirtualNanos(0), status, Width::W32),
            Ok(0x2),
            "SPI_SR resets with TXE set, RM0090 28.5.3"
        );
        assert_eq!(spi1::cr1::SPE.bit, 6);
    }
}