rigless 0.0.6

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 fork of QEMU built in to execute 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's the first chip
//! behind a chip select, and the select is the interface's business. This chip sees `select`, words
//! and `deselect`, and that's all 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's no busy flag to poll and no page to wrap. What it has 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 instead of asking 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 opcode is 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` for family, density, sub and 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, and `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 clears it, not a byte landing, so a `WRITE` the block protection refused clears it too. A `RDSR` leaves it standing, and a `WRSR` can't 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`, and "the upper bit of the two-byte address is ignored". Bytes land as they arrive, and "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 doesn't "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, and nothing on the board drives it. It's 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 you know those are choices |
//! | An opcode Table 1 doesn't list | Not specified. Ignored until `CS` rises |
//! | The 40 MHz ceiling on `SCK` | The seam carries words, not a clock, so the chip can't 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, a `WRSR` or a
    /// `WRITE` without the latch, an opcode the part doesn't know, or a cycle that began while it
    /// couldn't 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's
    /// 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 isn't ready for `tREC`. A
    /// part that isn't ready yet, because it's 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, and 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))
    }
}