rigless 0.0.5

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; a fork of QEMU is built in and executes the instructions
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The FM25V02, a 256-kilobit ferroelectric memory on a serial interface: the parameter store of a
//! flight controller, read and written a word at a time behind a chip select.
//!
//! Written against Cypress document 001-84494 Rev. \*G, "FM25V02, 256-Kbit (32 K × 8) Serial (SPI) F-RAM",
//! May 2, 2017. Every section and table below is that document's. It is the first chip behind a chip
//! select, and the select is the interface's business: this chip sees `select`, words, and `deselect`,
//! which is the whole of what a chip on such a bus sees.
//!
//! The part is simpler than the serial flash and EEPROM it replaces, and the datasheet says why on its
//! first page: `NoDelay`™ writes, "no page register is needed and any number of sequential writes may be
//! performed". There is no busy flag to poll and no page to wrap. What there is, is a command state
//! machine, a write-enable latch with three ways to clear it, and a device ID a driver recognises before
//! it trusts a byte. The array and the status register are its whole account of itself; a test asks
//! [`Fm25v02::stored`] what landed rather than the chip what it heard.
//!
//! | Register or section | Behaviour | Source | Test |
//! |---|---|---|---|
//! | Opcodes | `WREN 06h`, `WRDI 04h`, `RDSR 05h`, `WRSR 01h`, `READ 03h`, `FSTRD 0Bh`, `WRITE 02h`, `SLEEP B9h`, `RDID 9Fh`; the first byte after the select falls | Table 1 | `an_unknown_opcode_is_ignored_until_the_select_rises` |
//! | Device ID | Nine bytes: six of `7Fh`, then `C2h`, then `22h 00h` — family, density, sub, revision. Read-only, and what a driver compares before it trusts the chip | Table 6, Figure 16 | `the_device_id_is_nine_bytes_the_driver_recognises` |
//! | `WREN` | "The FM25V02 will power up with writes disabled. The WREN command must be issued before any write operation." Sets the latch; `WEL` reads set | WREN, p. 6 | `a_write_without_the_latch_is_ignored` |
//! | `WEL` clearing | "The WEL bit will be automatically cleared on the rising edge of CS following a WRDI, a WRSR, or a WRITE operation." The operation, not a byte landing: a `WRITE` the block protection refused clears it too. A `RDSR` leaves it standing, and a `WRSR` cannot set or clear it: "Attempting to write the WEL bit in the Status Register has no effect" | WREN, p. 6; Table 3 | `the_latch_clears_on_the_rising_edge_after_a_write_a_wrdi_or_a_wrsr`, `a_refused_write_still_clears_the_latch` |
//! | Status register | `WPEN` bit 7, `BP1` bit 3, `BP0` bit 2, `WEL` bit 1; bits 0 and 4 to 6 "are non-writable and always return '0' upon read". Bit 0 is the busy bit other parts have: "the F-RAM writes in real-time and is never busy" | Table 2, Table 3 | `the_status_register_writes_only_its_three_bits` |
//! | `RDSR` | "Will return one byte with the contents of the Status Register" | RDSR, p. 8 | `the_status_register_repeats_while_clocked` |
//! | Block protection | `BP1:BP0` — `00` none, `01` `6000h` to `7FFFh`, `10` `4000h` to `7FFFh`, `11` all. Nonvolatile | Table 4, Table 3 | `each_block_protect_range_refuses_writes` |
//! | `WRITE` | Two address bytes carry `A14` to `A0`, "the upper bit of the two-byte address is ignored"; bytes land as they arrive; "if the last address of 7FFFh is reached, the counter will roll over to 0000h"; the rising edge of `CS` ends it | Write Operation, p. 8 | `a_read_answers_what_was_written`, `the_address_rolls_over_at_the_top`, `the_upper_address_bit_is_ignored` |
//! | A write reaching protection | "The automatic address increment stops and all the subsequent data bytes received for write will be ignored by the device" | Write Operation, note, p. 8 | `a_burst_write_stops_at_a_protected_block` |
//! | `READ` | Two address bytes then data on every following eight clocks, "the SI input is ignored during read data bytes", incrementing and rolling over the same way | Read Operation, p. 9 | `a_read_answers_what_was_written`, `mosi_is_ignored_during_read_data` |
//! | `FSTRD` | `READ` with one dummy byte between the address and the data, "provided for code compatibility with serial flash devices" | Fast Read Operation, p. 9 | `a_fast_read_skips_one_dummy_byte` |
//! | `SLEEP` | Entered "when the SLEEP opcode B9h is clocked in and a rising edge of CS is applied"; asleep, "the SCK and SI pins are ignored and SO will be HI-Z"; the next falling edge of `CS` wakes it, and it does not "respond to an opcode within the wakeup period", `tREC`, 400 µs at most | Sleep Mode, p. 11; AC table | `sleep_is_entered_on_the_rising_edge_and_answers_nothing`, `a_command_inside_the_wake_up_time_is_ignored` |
//! | Power-up | "Not accessible for a tPU time after power-up", 250 µs from `VDD(min)` to the first `CS` low | Power Up to First Access, p. 6; AC table | `nothing_answers_before_the_power_up_time` |
//!
//! # What is not, and why
//!
//! | Departure | Reason |
//! |---|---|
//! | The `WP` pin | Not wired; nothing on the board drives it. Treated as high, so `WPEN` has nothing to act on and the status register is writable whenever `WEL` is; the rows of Table 5 that need `WP` low are unreachable |
//! | The `HOLD` pin | Not wired. "The HOLD pin can be used to interrupt a serial operation without aborting it"; with nothing to pull it, no operation pauses |
//! | What `RDSR` and `RDID` answer past their last byte | The datasheet shows one byte and nine and says nothing after. The status byte repeats and the ID answers zero past the ninth, and this row is here so a reader knows those are choices |
//! | An opcode Table 1 does not list | Not specified. Ignored until `CS` rises |
//! | The 40 MHz ceiling on `SCK` | The seam carries words, not a clock, so the chip cannot see the rate; a controller run faster than the part allows is the controller's to refuse |
//! | A sixteen-bit frame | The chip is eight bits wide and the controller may frame sixteen; the low byte is taken and the departure is the controller's, which frames what the firmware asked |
//! | Endurance and retention | 10¹⁴ cycles and 151 years. A counter would be honest and would never be read |
//! | What the memory holds at power-up | The datasheet is silent; the array is nonvolatile and holds whatever it held. It starts at zero here, and carrying contents across runs is its own change |

use crate::chip::spi::Peer;
use crate::device::DeviceContext;
use crate::time::VirtualNanos;

/// Thirty-two kilobytes, "256-Kbit (32 K × 8)".
pub const SIZE: usize = 32 * 1024;
/// The address bits the part decodes, `A14` to `A0`; the upper bit of the two address bytes is ignored.
const ADDRESS_MASK: u16 = 0x7FFF;

/// Table 1.
const WREN: u8 = 0x06;
const WRDI: u8 = 0x04;
const RDSR: u8 = 0x05;
const WRSR: u8 = 0x01;
const READ: u8 = 0x03;
const FSTRD: u8 = 0x0B;
const WRITE: u8 = 0x02;
const SLEEP: u8 = 0xB9;
const RDID: u8 = 0x9F;

/// Table 6: six bytes of the JEDEC continuation code, the Cypress (Ramtron) identifier in bank 7, then the
/// family, density, sub and revision codes of this part.
pub const DEVICE_ID: [u8; 9] = [0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0xC2, 0x22, 0x00];

/// Table 2.
const WEL: u8 = 1 << 1;
const BP0: u8 = 1 << 2;
const BP1: u8 = 1 << 3;
const WPEN: u8 = 1 << 7;
/// The bits a `WRSR` lands: bits 0 and 4 to 6 are fixed and `WEL` is the latch's, Table 3.
const WRITABLE: u8 = WPEN | BP1 | BP0;

/// `tPU`, power-up to the first `CS` low, AC table.
pub const POWER_UP: VirtualNanos = VirtualNanos(250_000);
/// `tREC`, recovery from sleep mode, AC table.
pub const RECOVERY: VirtualNanos = VirtualNanos(400_000);

/// Where a select cycle is, from the opcode to the rising edge that ends it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
    /// The first byte after the select fell is the opcode.
    Opcode,
    /// Two address bytes follow `READ`, `FSTRD` and `WRITE`, the high one first.
    Address {
        opcode: u8,
        high: Option<u8>,
    },
    /// A fast read's one dummy byte before the data.
    Dummy(u16),
    Read(u16),
    /// `stopped` once a byte reached a protected block: the increment has stopped and the rest are ignored.
    Write {
        address: u16,
        stopped: bool,
    },
    Id(usize),
    Status,
    WriteStatus,
    /// The rising edge that ends this cycle puts the part to sleep.
    ToSleep,
    /// The rest of the cycle means nothing: after a command that takes no more bytes, or an opcode the
    /// part does not know, or a cycle that began while it could not answer.
    Ignored,
}

/// One FM25V02.
///
/// Its state is the array, the status register, and the latch's pending clear; where it is in a command
/// is the phase, which every falling edge of the select resets. Time enters only at the select: whether
/// the part is ready to answer is decided when `CS` falls, which is when the datasheet decides it too.
#[derive(Debug)]
pub struct Fm25v02 {
    memory: Box<[u8]>,
    status: u8,
    phase: Phase,
    selected: bool,
    asleep: bool,
    /// The instant before which a select finds the part unable to answer: `tPU` from power-up, then
    /// `tREC` from a wake.
    ready_at: VirtualNanos,
    /// `WEL` clears on the rising edge that ends a `WRDI`, a `WRSR`, or a `WRITE`, not before. It is the
    /// operation that clears it, so a `WRITE` aimed at a protected block clears it as one that landed does.
    clear_latch_on_rise: bool,
}

impl Default for Fm25v02 {
    fn default() -> Self {
        Self::new()
    }
}

impl Fm25v02 {
    /// A part at power-up: the array at zero, the status register at its factory default of zero, writes
    /// disabled, and not answering until `tPU` has passed.
    #[must_use]
    pub fn new() -> Self {
        Self {
            memory: vec![0; SIZE].into_boxed_slice(),
            status: 0,
            phase: Phase::Opcode,
            selected: false,
            asleep: false,
            ready_at: POWER_UP,
            clear_latch_on_rise: false,
        }
    }

    /// What the array holds, for a test to assert a write landed where a read would find it.
    #[must_use]
    pub fn stored(&self) -> &[u8] {
        &self.memory
    }

    /// Table 4: whether `address` is in a block the status register protects.
    fn protected(&self, address: u16) -> bool {
        let from = match (self.status & BP1 != 0, self.status & BP0 != 0) {
            (false, false) => return false,
            (false, true) => 0x6000,
            (true, false) => 0x4000,
            (true, true) => 0x0000,
        };
        address >= from
    }

    /// One byte of a cycle that began ready: the phase decides what it means and what goes back.
    fn byte(&mut self, byte: u8) -> u8 {
        match self.phase {
            Phase::Opcode => {
                self.phase = match byte {
                    WREN => {
                        self.status |= WEL;
                        Phase::Ignored
                    }
                    WRDI => {
                        self.clear_latch_on_rise = true;
                        Phase::Ignored
                    }
                    RDSR => Phase::Status,
                    WRSR if self.status & WEL != 0 => {
                        self.clear_latch_on_rise = true;
                        Phase::WriteStatus
                    }
                    READ | FSTRD => Phase::Address {
                        opcode: byte,
                        high: None,
                    },
                    WRITE if self.status & WEL != 0 => {
                        self.clear_latch_on_rise = true;
                        Phase::Address {
                            opcode: byte,
                            high: None,
                        }
                    }
                    RDID => Phase::Id(0),
                    SLEEP => Phase::ToSleep,
                    _ => Phase::Ignored,
                };
                0
            }
            Phase::Address { opcode, high: None } => {
                self.phase = Phase::Address {
                    opcode,
                    high: Some(byte),
                };
                0
            }
            Phase::Address {
                opcode,
                high: Some(high),
            } => {
                let address = (u16::from(high) << 8 | u16::from(byte)) & ADDRESS_MASK;
                self.phase = match opcode {
                    READ => Phase::Read(address),
                    FSTRD => Phase::Dummy(address),
                    _ => Phase::Write {
                        address,
                        stopped: false,
                    },
                };
                0
            }
            Phase::Dummy(address) => {
                self.phase = Phase::Read(address);
                0
            }
            Phase::Read(address) => {
                self.phase = Phase::Read((address + 1) & ADDRESS_MASK);
                self.memory[usize::from(address)]
            }
            Phase::Write { address, stopped } => {
                if !stopped && !self.protected(address) {
                    self.memory[usize::from(address)] = byte;
                    self.phase = Phase::Write {
                        address: (address + 1) & ADDRESS_MASK,
                        stopped: false,
                    };
                } else {
                    self.phase = Phase::Write {
                        address,
                        stopped: true,
                    };
                }
                0
            }
            Phase::Id(index) => {
                self.phase = Phase::Id(index + 1);
                DEVICE_ID.get(index).copied().unwrap_or(0)
            }
            Phase::Status => self.status,
            Phase::WriteStatus => {
                self.status = (self.status & WEL) | (byte & WRITABLE);
                self.phase = Phase::Ignored;
                0
            }
            Phase::ToSleep | Phase::Ignored => 0,
        }
    }
}

impl Peer for Fm25v02 {
    /// The falling edge of `CS`. A part asleep begins waking here and is not ready for `tREC`; a part
    /// not yet ready — waking, or within `tPU` of power-up — takes nothing from this cycle at all.
    fn select(&mut self, context: &DeviceContext<'_>) {
        self.selected = true;
        let now = context.now();
        if self.asleep {
            self.asleep = false;
            self.ready_at = VirtualNanos(now.0 + RECOVERY.0);
        }
        self.phase = if now.0 < self.ready_at.0 {
            Phase::Ignored
        } else {
            Phase::Opcode
        };
    }

    /// The rising edge of `CS`: the latch clears if this cycle was one of the three that clear it, and the
    /// part sleeps if this cycle asked it to.
    fn deselect(&mut self, _context: &DeviceContext<'_>) {
        self.selected = false;
        if self.clear_latch_on_rise {
            self.status &= !WEL;
            self.clear_latch_on_rise = false;
        }
        if self.phase == Phase::ToSleep {
            self.asleep = true;
        }
        self.phase = Phase::Opcode;
    }

    /// One word on the wire while selected. The chip is eight bits wide; the low byte is what it sees.
    fn transfer(&mut self, mosi: u16, _context: &DeviceContext<'_>) -> u16 {
        if !self.selected {
            return 0;
        }
        u16::from(self.byte((mosi & 0xFF) as u8))
    }
}