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 command path of the two SD and MMC card interfaces with nothing in the slot, RM0433 rev 9
//! chapter 55, written against the `sdmmc1` layout both share.
//!
//! **This is the empty slot, and a deliberate partial.** A firmware probing for a card sends commands
//! and waits for each to finish, and ChibiOS's `sdc_lld_send_cmd_none` and its siblings spin on
//! `SDMMC_STAR` with no timeout. Against a register file nothing they wait on ever sets. With no card
//! in the slot a command needing no response is sent, and every other one times out, which is what a
//! board with an empty slot gives a firmware. The data path and a card are the whole model's, later.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | `CMDR` `CPSMEN` with `WAITRESP` 00 | The command is sent and `CMDSENT` sets: "If no response is expected (WAITRESP = 00) the CPSM moves to the Idle state and the CMDSENT flag is set" | 55.5.4 | `a_command_needing_no_response_is_sent` |
//! | `CMDR` `CPSMEN` with a response expected | Nothing answers in an empty slot, so `CTIMEOUT` sets: the CPSM waits for a start bit and "The Command Timeout period has a fixed value of 64 `SDMMC_CK` clock periods" | 55.5.4, 55.10.11 | `a_command_expecting_a_response_times_out` |
//! | `CPSMEN` | Clears as the command ends: "cleared by hardware when the CPSM enters the Idle state" | 55.10.4 | `a_command_needing_no_response_is_sent` |
//! | `CPSMEN` with `DTEN` set | No command is sent and `CPSMEN` clears | 55.10.4 | `no_command_goes_out_while_a_data_transfer_is_enabled` |
//! | `ICR` | A written one clears the matching `STAR` flag: each flag "is cleared by writing corresponding interrupt clear bit in `SDMMC_ICR`" | 55.10.11, 55.10.12 | `the_clear_register_clears_the_flags_it_names` |
//! | The interrupt | Raised while a flag stands that `MASKR` enables | 55.10.13 | `an_enabled_flag_raises_the_interrupt` |
//! | The driver's probe | ChibiOS's card initialization: `CMD0` sent, then `CMD8` and `CMD55` time out, which is how it learns the slot is empty | 55.5.4 | `chibios_finds_the_slot_empty` |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | A card, responses, the data path, the internal DMA, `DATAEND` and every data flag | 55.5 | The slot is empty. A card on this interface is the whole model's |
//! | The time a command and its timeout take: the command's 48 bits and 64 `SDMMC_CK` periods | 55.5.4 | The flags set inside the write, which errs in the direction that can't hang a firmware |
//! | Power, clock, boot, voltage switching, suspend and read wait | 55.10.1, 55.10.2, 55.5 | Stored, and nothing a probe of an empty slot waits on |
//! | ES0392 rev 15 section 2.8: six errata on this interface | 2.8.1 to 2.8.6 | Each concerns a card's data transfer or its response, and an empty slot has neither |

use crate::device::{Device, DeviceContext, Level, RegisterFile};
use crate::soc::Layout;
use crate::soc::stm32h743::generated::sdmmc1::{Register, cmdr, dctrl, star};

/// The two interfaces, in the order a part holds them.
pub const INSTANCES: [&str; 2] = ["SDMMC1", "SDMMC2"];

/// The interface's interrupt, position zero of its table.
const IRQ: usize = 0;

/// The command path of one interface with an empty slot.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Sdmmc {
    interrupt: bool,
}

impl Sdmmc {
    fn refresh(&mut self, registers: &RegisterFile, context: &mut DeviceContext<'_>) {
        let raised = registers.get(Register::Star) & registers.get(Register::Maskr) != 0;
        if raised != self.interrupt {
            self.interrupt = raised;
            context.interrupt(IRQ, Level::from(raised));
        }
    }
}

impl Device for Sdmmc {
    type Register = Register;

    fn write(
        &mut self,
        register: Register,
        written: u64,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        match register {
            Register::Cmdr if cmdr::CPSMEN.get(written) != 0 => {
                registers.set(Register::Cmdr, cmdr::CPSMEN.set(written, 0));
                if dctrl::DTEN.get(registers.get(Register::Dctrl)) == 0 {
                    let flag = if cmdr::WAITRESP.get(written) == 0 {
                        star::CMDSENT
                    } else {
                        star::CTIMEOUT
                    };
                    let flags = registers.get(Register::Star) | flag.mask();
                    registers.set(Register::Star, flags);
                }
            }
            Register::Icr => {
                // Section 55.10.12: a clear bit for each flag software clears, and none for the
                // state flags `CPSMACT`, `DPSMACT`, `BUSYD0` and the FIFO's.
                let clear_bits = Register::Icr
                    .spec()
                    .fields
                    .iter()
                    .fold(0, |mask, field| mask | field.mask());
                let cleared = written & clear_bits;
                let flags = registers.get(Register::Star) & !cleared;
                registers.set(Register::Star, flags);
                registers.set(Register::Icr, 0);
            }
            _ => {}
        }
        self.refresh(registers, context);
    }

    fn reset(&mut self) {
        self.interrupt = false;
    }
}

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

    const AT: VirtualNanos = VirtualNanos(0);

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

    fn slot() -> Single<Sdmmc> {
        Single::new(builder(), INSTANCES[0], Sdmmc::default()).unwrap()
    }

    /// `CMDR` for command `index`, with the response `WAITRESP` gives, the way ChibiOS writes it.
    fn command(index: u64, waitresp: u64) -> u64 {
        cmdr::CPSMEN.set(cmdr::WAITRESP.set(index, waitresp), 1)
    }

    fn status(slot: &mut Single<Sdmmc>) -> u64 {
        slot.read(AT, at(Register::Star), Width::W32).unwrap()
    }

    #[test]
    fn a_command_needing_no_response_is_sent() {
        let mut slot = slot();
        slot.write(AT, at(Register::Cmdr), Width::W32, command(0, 0))
            .unwrap();
        assert_eq!(status(&mut slot), star::CMDSENT.mask());
        let held = slot.read(AT, at(Register::Cmdr), Width::W32).unwrap();
        assert_eq!(cmdr::CPSMEN.get(held), 0, "the CPSM is idle again");
    }

    #[test]
    fn a_command_expecting_a_response_times_out() {
        let mut slot = slot();
        slot.write(AT, at(Register::Cmdr), Width::W32, command(8, 0b01))
            .unwrap();
        assert_eq!(status(&mut slot), star::CTIMEOUT.mask());
    }

    #[test]
    fn no_command_goes_out_while_a_data_transfer_is_enabled() {
        let mut slot = slot();
        slot.write(AT, at(Register::Dctrl), Width::W32, dctrl::DTEN.mask())
            .unwrap();
        slot.write(AT, at(Register::Cmdr), Width::W32, command(0, 0))
            .unwrap();
        assert_eq!(status(&mut slot), 0);
    }

    #[test]
    fn the_clear_register_clears_the_flags_it_names() {
        let mut slot = slot();
        slot.write(AT, at(Register::Cmdr), Width::W32, command(0, 0))
            .unwrap();
        slot.write(AT, at(Register::Cmdr), Width::W32, command(55, 0b01))
            .unwrap();
        slot.write(AT, at(Register::Icr), Width::W32, star::CMDSENT.mask())
            .unwrap();
        assert_eq!(status(&mut slot), star::CTIMEOUT.mask());
        slot.write(AT, at(Register::Icr), Width::W32, 0xFFFF_FFFF)
            .unwrap();
        assert_eq!(status(&mut slot), 0);
    }

    #[test]
    fn an_enabled_flag_raises_the_interrupt() {
        let mut slot = slot();
        slot.write(
            AT,
            at(Register::Maskr),
            Width::W32,
            maskr::CTIMEOUTIE.mask(),
        )
        .unwrap();
        slot.write(AT, at(Register::Cmdr), Width::W32, command(8, 0b01))
            .unwrap();
        assert!(slot.model.interrupt);
        slot.write(AT, at(Register::Icr), Width::W32, star::CTIMEOUT.mask())
            .unwrap();
        assert!(!slot.model.interrupt);
    }

    /// ChibiOS's `sdc_lld_send_cmd_none` for `CMD0`, then `sdc_lld_send_cmd_short` for `CMD8` and
    /// `CMD55`: each waits on its flag and clears it, and the two that expect an answer time out.
    #[test]
    fn chibios_finds_the_slot_empty() {
        let mut slot = slot();
        slot.write(AT, at(Register::Icr), Width::W32, 0xFFFF_FFFF)
            .unwrap();
        slot.write(AT, at(Register::Cmdr), Width::W32, command(0, 0))
            .unwrap();
        assert_ne!(status(&mut slot) & star::CMDSENT.mask(), 0);
        slot.write(AT, at(Register::Icr), Width::W32, star::CMDSENT.mask())
            .unwrap();
        for index in [8, 55] {
            slot.write(AT, at(Register::Cmdr), Width::W32, command(index, 0b01))
                .unwrap();
            let waited = star::CMDREND.mask() | star::CTIMEOUT.mask() | star::CCRCFAIL.mask();
            let seen = status(&mut slot) & waited;
            assert_eq!(seen, star::CTIMEOUT.mask(), "CMD{index}");
            slot.write(AT, at(Register::Icr), Width::W32, seen).unwrap();
        }
        assert_eq!(status(&mut slot), 0);
    }
}