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 general-purpose I/O port of RM0090 rev 22 chapter 8, written against the `gpioa` layout.
//!
//! Every register of section 8.4 and every configuration of section 8.3 is implemented. A pin's electrical level
//! is what the outside world would see on it; the outside world drives a pin through [`Gpio::drive`] and a
//! peripheral drives its alternate function through [`Gpio::drive_function`]. Nothing here is register-only.
//!
//! | Register or section | Behaviour | Source | Test |
//! |---|---|---|---|
//! | Reset state | Every pin an input, floating; port A's debug pins in their alternate function with their pulls | 8.3.1, 8.4.1 to 8.4.4 | `port_a_resets_with_the_debug_pins_pulled` |
//! | `MODER` | Input, output, alternate function, or analog per pin | 8.4.1, table 36 | `a_pin_is_what_its_mode_says` |
//! | `OTYPER` | Push-pull drives both levels; open-drain drives low and releases high | 8.3.10, 8.4.2 | `open_drain_drives_low_and_releases_high` |
//! | `OSPEEDR` | Slew rate, with no observable value in the emulator; stored | 8.4.3 | `speed_is_stored_and_changes_nothing` |
//! | `PUPDR` | A released pin reads its pull; a floating pin keeps its last level | 8.3.9, 8.4.4 | `a_released_pin_reads_its_pull` |
//! | `IDR` | Every pin's level, sampled at the read; zero in analog mode | 8.3.9, 8.3.12, 8.4.5 | `idr_reads_the_pins_and_zero_for_analog` |
//! | `ODR` | The level an output drives; a push-pull output beats what drives the pin | 8.3.10, 8.4.6 | `odr_drives_a_push_pull_output` |
//! | `BSRR` | Sets the low half's bits and clears the high half's, the set winning; reads zero | 8.3.5, 8.4.7 | `bsrr_sets_and_clears_and_the_set_wins` |
//! | `LCKR` | The lock key sequence freezes the six configuration registers for the locked pins until reset | 8.3.6, 8.4.8 | `the_lock_sequence_freezes_the_configuration`, `a_broken_lock_sequence_aborts` |
//! | `AFRL`, `AFRH` | The alternate function a pin carries; its peripheral drives the pin | 8.3.7, 8.3.11, 8.4.9, 8.4.10 | `an_alternate_function_pin_is_driven_by_its_peripheral` |
//! | Input | An input follows what drives it and keeps the last level when released | 8.3.9 | `an_input_follows_what_drives_it` |
//! | Narrow access | Byte, half-word, and word access lands in its lane; on the SoC `IDR` is read and `LCKR` written by word only, which the model cannot see, the width staying on the bus | 8.4, 8.3.6, 8.4.5, 8.4.8 | `the_registers_accept_narrow_access`; the word-only rule pending, RL-102 |
//! | 8.3.2, 8.3.7 | The pin multiplexer: `AFRL` and `AFRH` above choose the function, and the board wires the peripheral to the pin | 8.3.2, 8.3.7 | see `AFRL`, `AFRH` |
//! | 8.3.8 | External interrupt and wake-up lines | not applicable, the EXTI is its own model | |
//! | 8.3.13, 8.3.14 | The oscillator pins as GPIO, PC14, PC15, PH0, PH1 | not applicable, port A has none; the RCC model of 0.2 decides them, RL-8 | |
//! | ES0206 rev 24, 2.2.7 | A register write within the clock-enable delay is lost | pending, the RCC model of 0.2, RL-8 | |
//! | ES0206 rev 24, 2.2.14 | Flash reads corrupt while PA12 is a GPIO under dual-bank conditions | pending, flash bank 2 is not modeled, RL-99 | |
//! | ES0206 rev 24, 2.2.16 | PC13 as the RTC alarm open-drain output | pending, port C is not bound in 0.1, RL-100 | |
//! | ES0206 rev 24, 2.2.10 | PC13 transitions disturb the LSE | pending, the LSE of the RCC model of 0.2, RL-8 | |
//! | ES0206 rev 24, 2.2.5 | PB4 as a GPIO breaks a four-wire JTAG debug session | not applicable, the debugger reaches the core through the sidecar and not a JTAG port | |
//!
//! Where the manual leaves a value undefined, an input pin that floats with nothing driving it, section 8.4.5's
//! reset value, the model keeps the last level the pin had, low at reset, and says so here.

use crate::device::{Device, DeviceContext, Level, RegisterFile};
use crate::soc::{FieldSpec, Layout};

use crate::soc::stm32f427::generated::gpioa::{Register, lckr};

const PIN_COUNT: u8 = 16;
const PINS: usize = PIN_COUNT as usize;
const MODE_INPUT: u64 = 0b00;
const MODE_OUTPUT: u64 = 0b01;
const MODE_ALTERNATE: u64 = 0b10;
const MODE_ANALOG: u64 = 0b11;
const PULL_UP: u64 = 0b01;
const PULL_DOWN: u64 = 0b10;
const OPEN_DRAIN: u64 = 1;
const MODE_BITS: u32 = 2;
const FUNCTION_BITS: u32 = 4;
const FUNCTIONS_PER_REGISTER: u8 = 8;

/// One of the port's sixteen pins.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Pin(u8);

impl Pin {
    /// The pin at `index`, 0 to 15.
    #[must_use]
    pub const fn new(index: u8) -> Option<Self> {
        if index < PIN_COUNT {
            Some(Self(index))
        } else {
            None
        }
    }

    fn all() -> impl Iterator<Item = Self> {
        (0..PIN_COUNT).map(Self)
    }

    const fn index(self) -> usize {
        self.0 as usize
    }

    /// The pin's number, its bit in every one-bit-per-pin register, 8.4.
    #[must_use]
    pub const fn bit(self) -> u32 {
        self.0 as u32
    }
}

// Where the lock key write sequence of section 8.4.8 stands, carrying the `LCK[15:0]` value it was started with.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum Lock {
    #[default]
    Open,
    KeyWritten(u64),
    KeyCleared(u64),
    Locked(u64),
}

// The six configuration registers as they were when the lock took hold, section 8.3.6.
#[derive(Debug, Clone, Copy)]
struct Frozen {
    moder: u64,
    otyper: u64,
    ospeedr: u64,
    pupdr: u64,
    afrl: u64,
    afrh: u64,
}

/// A port of sixteen pins.
#[derive(Debug)]
pub struct Gpio {
    external: [Option<Level>; PINS],
    function: [Option<Level>; PINS],
    sampled: [Level; PINS],
    lock: Lock,
    frozen: Option<Frozen>,
}

impl Default for Gpio {
    fn default() -> Self {
        Self {
            external: [None; PINS],
            function: [None; PINS],
            sampled: [Level::Low; PINS],
            lock: Lock::Open,
            frozen: None,
        }
    }
}

impl Gpio {
    /// What the outside world drives on `pin`, or `None` when nothing does.
    pub fn drive(&mut self, pin: Pin, level: Option<Level>) {
        self.external[pin.index()] = level;
        self.remember(pin, level);
    }

    /// What the peripheral selected by the alternate function drives on `pin`, or `None` when it drives nothing;
    /// section 8.3.11.
    pub fn drive_function(&mut self, pin: Pin, level: Option<Level>) {
        self.function[pin.index()] = level;
        self.remember(pin, level);
    }

    // A level put on a pin is the level a floating pin keeps once it is released; the input stage samples every
    // clock, so the pin saw it whether or not the firmware read it.
    fn remember(&mut self, pin: Pin, level: Option<Level>) {
        if let Some(level) = level {
            self.sampled[pin.index()] = level;
        }
    }

    /// The electrical level on `pin` now, what a wire soldered to it would see.
    #[must_use]
    pub fn level(&self, pin: Pin, registers: &RegisterFile) -> Level {
        self.input_level(pin, registers)
            .unwrap_or_else(|| self.external[pin.index()].unwrap_or(self.sampled[pin.index()]))
    }

    /// The alternate function number `pin` is configured to carry, when its mode is alternate function.
    #[must_use]
    pub fn function(pin: Pin, registers: &RegisterFile) -> Option<u8> {
        (mode(pin, registers) == MODE_ALTERNATE).then(|| {
            let (register, bit) = function_field(pin);
            let number = field_at(register, bit).get(registers.get(register));
            u8::try_from(number).expect("a four-bit field fits a byte")
        })
    }

    // The level the input stage samples on `pin`, sections 8.3.9 to 8.3.12, or `None` in analog mode, where
    // the Schmitt trigger is off and the input data register reads zero.
    fn sample(&mut self, pin: Pin, registers: &RegisterFile) -> Option<Level> {
        let level = self.input_level(pin, registers);
        if let Some(level) = level {
            self.sampled[pin.index()] = level;
        }
        level
    }

    fn input_level(&self, pin: Pin, registers: &RegisterFile) -> Option<Level> {
        let index = pin.index();
        let driven = match mode(pin, registers) {
            MODE_ANALOG => return None,
            MODE_OUTPUT => Some(Level::from(
                field_at(Register::Odr, pin.bit()).is_set(registers.get(Register::Odr)),
            )),
            MODE_ALTERNATE => self.function[index],
            MODE_INPUT => None,
            unknown => unreachable!("a two-bit field holds {unknown}"),
        };
        let open_drain = field_at(Register::Otyper, pin.bit()).get(registers.get(Register::Otyper))
            == OPEN_DRAIN;
        let level = match driven {
            Some(Level::Low) => Level::Low,
            Some(Level::High) if !open_drain => Level::High,
            _ => self.external[index]
                .or_else(|| pull(pin, registers))
                .unwrap_or(self.sampled[index]),
        };
        Some(level)
    }

    fn input_data(&mut self, registers: &RegisterFile) -> u64 {
        Pin::all()
            .filter(|&pin| self.sample(pin, registers) == Some(Level::High))
            .map(|pin| field_at(Register::Idr, pin.bit()).mask())
            .sum()
    }

    fn lock(&mut self, value: u64, registers: &mut RegisterFile) {
        let key = lckr::LCKK.is_set(value);
        let bits = lock_bits(value);
        self.lock = match (self.lock, key) {
            (Lock::Locked(mask), _) => Lock::Locked(mask),
            (Lock::KeyCleared(same), true) if same == bits => {
                self.frozen = Some(Frozen {
                    moder: registers.get(Register::Moder),
                    otyper: registers.get(Register::Otyper),
                    ospeedr: registers.get(Register::Ospeedr),
                    pupdr: registers.get(Register::Pupdr),
                    afrl: registers.get(Register::Afrl),
                    afrh: registers.get(Register::Afrh),
                });
                Lock::Locked(same)
            }
            (_, true) => Lock::KeyWritten(bits),
            (Lock::KeyWritten(same), false) if same == bits => Lock::KeyCleared(same),
            (_, false) => Lock::Open,
        };
        let shown = match self.lock {
            Lock::Locked(mask) => lckr::LCKK.set(mask, 1),
            _ => bits,
        };
        registers.set(Register::Lckr, shown);
    }

    // Puts the locked pins' bits of a configuration register back after a write, section 8.3.6.
    fn hold_frozen(&self, register: Register, registers: &mut RegisterFile) {
        let (Lock::Locked(mask), Some(frozen)) = (self.lock, self.frozen) else {
            return;
        };
        let held = match register {
            Register::Moder => frozen.moder,
            Register::Ospeedr => frozen.ospeedr,
            Register::Pupdr => frozen.pupdr,
            Register::Otyper => frozen.otyper,
            Register::Afrl => frozen.afrl,
            Register::Afrh => frozen.afrh,
            _ => return,
        };
        let bits = Pin::all()
            .filter(|pin| field_at(Register::Lckr, pin.bit()).is_set(mask))
            .filter_map(|pin| configuration_field(register, pin))
            .fold(0, |bits, field| bits | field.mask());
        let written = registers.get(register);
        registers.set(register, (written & !bits) | (held & bits));
    }
}

impl Device for Gpio {
    type Register = Register;

    fn read(
        &mut self,
        register: Register,
        registers: &mut RegisterFile,
        _context: &mut DeviceContext<'_>,
    ) -> Option<u64> {
        (register == Register::Idr).then(|| self.input_data(registers))
    }

    fn write(
        &mut self,
        register: Register,
        value: u64,
        registers: &mut RegisterFile,
        _context: &mut DeviceContext<'_>,
    ) {
        match register {
            Register::Bsrr => {
                // BSx sets and BRx resets, and BSx wins when both are written, 8.4.7.
                let output = Pin::all().fold(registers.get(Register::Odr), |output, pin| {
                    let set = field_at(Register::Bsrr, pin.bit()).is_set(value);
                    let reset =
                        field_at(Register::Bsrr, u32::from(PIN_COUNT) + pin.bit()).is_set(value);
                    match (set, reset) {
                        (true, _) => field_at(Register::Odr, pin.bit()).set(output, 1),
                        (false, true) => field_at(Register::Odr, pin.bit()).set(output, 0),
                        (false, false) => output,
                    }
                });
                registers.set(Register::Odr, output);
            }
            Register::Lckr => self.lock(value, registers),
            Register::Moder
            | Register::Otyper
            | Register::Ospeedr
            | Register::Pupdr
            | Register::Afrl
            | Register::Afrh => self.hold_frozen(register, registers),
            _ => {}
        }
        // The input stage samples every AHB1 clock, section 8.3.9, so a configuration change reaches the pins now.
        self.input_data(registers);
    }
}

fn mode(pin: Pin, registers: &RegisterFile) -> u64 {
    field_at(Register::Moder, pin.bit() * MODE_BITS).get(registers.get(Register::Moder))
}

fn pull(pin: Pin, registers: &RegisterFile) -> Option<Level> {
    match field_at(Register::Pupdr, pin.bit() * MODE_BITS).get(registers.get(Register::Pupdr)) {
        PULL_UP => Some(Level::High),
        PULL_DOWN => Some(Level::Low),
        _ => None,
    }
}

fn function_field(pin: Pin) -> (Register, u32) {
    if pin.0 < FUNCTIONS_PER_REGISTER {
        (Register::Afrl, pin.bit() * FUNCTION_BITS)
    } else {
        (
            Register::Afrh,
            (pin.bit() - u32::from(FUNCTIONS_PER_REGISTER)) * FUNCTION_BITS,
        )
    }
}

// The field of `register` that starts at `bit`, as the generated table lists it.
fn field_at(register: Register, bit: u32) -> &'static FieldSpec {
    register
        .spec()
        .fields
        .iter()
        .find(|field| u32::from(field.bit) == bit)
        .expect("every pin has its field in every GPIO register")
}

// The `LCKx` bits of a written `LCKR` value, without the key.
fn lock_bits(value: u64) -> u64 {
    Pin::all().fold(0, |bits, pin| {
        bits | (value & field_at(Register::Lckr, pin.bit()).mask())
    })
}

// The field of `register` that configures `pin`, or `None` when the register holds other pins' functions.
fn configuration_field(register: Register, pin: Pin) -> Option<&'static FieldSpec> {
    match register {
        Register::Moder | Register::Ospeedr | Register::Pupdr => {
            Some(field_at(register, pin.bit() * MODE_BITS))
        }
        Register::Otyper => Some(field_at(register, pin.bit())),
        Register::Afrl | Register::Afrh => {
            let (holder, bit) = function_field(pin);
            (holder == register).then(|| field_at(holder, bit))
        }
        _ => None,
    }
}