rigless 0.0.4

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, Bus, Cpu, Peripheral, Region, 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",
];

/// Every instance of the two U(S)ART layouts, whose status register ST's file gets wrong; see `builder`.
const UARTS: [&str; 8] = [
    "USART1", "USART2", "USART3", "USART6", "UART4", "UART5", "UART7", "UART8",
];

/// Every instance of the `SPI1` layout, whose prescaler register ST's file gets wrong; see `builder`.
const SPIS: [&str; 8] = [
    "SPI1", "SPI2", "SPI3", "SPI4", "SPI5", "SPI6", "I2S2ext", "I2S3ext",
];

/// The part's tables and every correction to them. [`Stm32f427::new`](super::Stm32f427::new) builds
/// the bus from this and puts the models on its own fields; this is here for a test that wants one
/// model on a bus of its own.
pub fn builder() -> Builder {
    // Section 6.3.23 drops bit 26 of PLLI2SQ in ST's file. Sections 6.3.15, 6.3.18, and 6.3.19 give every
    // LPEN bit set out of reset, and ST leaves clear exactly the peripherals this part adds over the
    // STM32F405; 6.3.19's printed value carries a typo its own bit map settles. RM0090 rev 22.
    let builder = Bus::builder(CPU, &MEMORY, instances())
        .reset("RCC", "PLLI2SCFGR", 0x2400_3000)
        // DMA2D, SRAM3, GPIOK, and GPIOJ.
        .reset("RCC", "AHB1LPENR", 0x7EEF_97FF)
        // UART8 and UART7.
        .reset("RCC", "APB1LPENR", 0xF6FE_C9FF)
        // LTDC, SAI1, SPI6, SPI5, and SPI4.
        .reset("RCC", "APB2LPENR", 0x0477_7F33)
        // Section 38.6.1 gives this family 0x419 and the STM32F405 the only other identifier it names,
        // 0x413; ST says 0x6411, which is neither, with a reserved nibble set. It leaves the revision to
        // the product and lists 0x1007 as revision 1, which is taken because ES0206 rev 24 section 2.2.12
        // has no over-drive on the revision A that ST's value claims, and this SoC models over-drive.
        .reset("DBG", "DBGMCU_IDCODE", 0x1007_0419);
    // Section 30.6.1 prints USART_SR's reset value with a digit too many and ST read it literally, which
    // leaves TXE clear; its own bit map gives 0x0000_00C0, so section 30.3.2's first polled write ends.
    let builder = UARTS.iter().fold(builder, |builder, instance| {
        builder.reset(instance, "SR", 0x0000_00C0)
    });
    // Section 28.5.9 and table 129 give SPI_I2SPR 0x0002; ST's file says 0x000A.
    let builder = SPIS.iter().fold(builder, |builder, instance| {
        builder.reset(instance, "I2SPR", 0x0000_0002)
    });
    TIMERS.iter().fold(builder, |builder, timer| {
        builder.write_modify(timer, "SR", WriteModify::ZeroToClear)
    })
}

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

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

    #[test]
    fn the_bus_carries_every_instance() {
        let soc = builder().build().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")
        );
    }

    /// RM0090 rev 22 section 30.6.1's bit map, against ST's reading of its typo'd reset value. A polled
    /// write waits for TXE before it writes, section 30.3.2, so with the vendor's value that wait never
    /// ends on any of the eight instances.
    #[test]
    fn every_uart_comes_out_of_reset_ready_to_be_written_to() {
        use super::generated::{uart4, usart6};

        let soc = builder().build().unwrap();
        // The two layouts put TXE and TC in the same places, which is what the shared value asserts.
        assert_eq!(usart6::sr::TXE.bit, uart4::sr::TXE.bit);
        assert_eq!(usart6::sr::TC.bit, uart4::sr::TC.bit);
        let expected = usart6::sr::TXE.mask() | usart6::sr::TC.mask();
        assert_eq!(expected, 0x0000_00C0, "30.6.1: TXE is bit 7 and TC bit 6");

        for instance in UARTS {
            let registers = soc
                .registers(instance)
                .unwrap_or_else(|| panic!("{instance} is an instance"));
            let value = match instance {
                "UART4" | "UART5" => registers.get(uart4::Register::Sr),
                _ => registers.get(usart6::Register::Sr),
            };
            assert_eq!(value, expected, "30.6.1: {instance} out of reset");
        }
    }

    #[test]
    fn the_lines_the_vendor_omitted_are_filled_in() {
        let soc = builder().build().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 = builder().build().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 = crate::soc::stm32f427::Stm32f427::new().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);
    }
}