// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The general-purpose I/O port of RM0090 rev 22 chapter 8, written against the `gpioa` layout,
//! which is the layout of all eleven ports. One of these answers for each of them.
//!
//! 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 | Behavior | Source | Test |
//! |---|---|---|---|
//! | Reset state | Every pin an input, floating, except port A's debug pins, in their alternate function with their pulls, and port B's one of them. What a port wakes up as is its own, held in the part's corrections rather than here | 8.3.1, 8.4.1 to 8.4.4 | `port_a_resets_with_the_debug_pins_pulled`, `every_port_comes_out_of_reset_as_the_manual_gives_it` |
//! | `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, and 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. It's stored | 8.4.3 | `speed_is_stored_and_changes_nothing` |
//! | `PUPDR` | A released pin reads its pull, and 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` |
//! | Wire | A pin whose level on the wire changes announces it as the port's request line, numbered by the pin: what a chip select on a serial interface reads. Announced after the write that changed it, never on a read, and only while something holds the pin (an output stage, a function's driver, the outside world, or a pull). A floating pin announces nothing, because the level it keeps is section 8.4.5's convention rather than a wire | 8.3.9, 8.4.4 | `a_pin_announces_its_level_when_it_changes`, `nothing_is_announced_at_reset`, `a_pin_announces_the_level_it_becomes_an_output_at`, `a_read_announces_nothing`, `a_floating_pin_announces_nothing_and_a_pulled_one_its_pull` |
//! | `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 can't see, the width staying on the bus | 8.4, 8.3.6, 8.4.5, 8.4.8 | `the_registers_accept_narrow_access`, with the word-only rule pending |
//! | 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 | `an_alternate_function_pin_is_driven_by_its_peripheral` |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | External interrupt and wake-up lines | 8.3.8 | Not applicable. The EXTI is its own instance, and the part has no model for it yet |
//! | The oscillator pins as GPIO, PC14, PC15, PH0, PH1 | 8.3.13, 8.3.14 | Not modeled. The oscillator takes the pins while `LSEON` or `HSEON` stands, and the port here never gives them up. Ports C and H are bound, so the pins exist. What's on the other end of them is the RCC's |
//! | A register write within the clock-enable delay is lost | ES0206 rev 24, 2.2.7 | Not modeled, as `rcc.rs` records for the same erratum: a gate opens at the instant its bit is written |
//! | Flash reads corrupt while PA12 is a GPIO under dual-bank conditions | ES0206 rev 24, 2.2.14 | Not applicable. Table 3 marks it for revisions A, Y and 1 only, and `memory_map.rs` gives `DBGMCU_IDCODE` a `REV_ID` of revision 4, 5 or B with its reason |
//! | PC13 as the RTC alarm open-drain output | ES0206 rev 24, 2.2.16 | Pending. The RTC's alarm isn't modeled |
//! | PC13 transitions disturb the LSE | ES0206 rev 24, 2.2.10 | Not modeled. It's an analog effect with no register behavior, as `rcc.rs` records |
//! | A level put on a pin from outside, announced at once | not applicable | `drive` and `drive_function` announce it at the next register write instead. They take no context, and a chip select is a firmware output that nothing outside the part drives |
//! | PB4 as a GPIO breaking a four-wire JTAG debug session | ES0206 rev 24, 2.2.5 | Unreachable here, though Table 3 marks it on every revision. The debugger reaches the core through the emulator's stub 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::time::VirtualNanos;
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, serde::Serialize, serde::Deserialize,
)]
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)
}
/// The pin's number, which is also its request line's number on the port.
pub 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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
struct Frozen {
moder: u64,
otyper: u64,
ospeedr: u64,
pupdr: u64,
afrl: u64,
afrh: u64,
}
/// A port of sixteen pins.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Gpio {
external: [Option<Level>; PINS],
function: [Option<Level>; PINS],
sampled: [Level; PINS],
/// The level each pin last announced on its request line, or `None` before its first.
announced: [Option<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],
announced: [None; 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's 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
}
/// The wire's level where something holds the pin (an output stage, a function's driver, the
/// outside world, or a pull), and `None` where it floats.
fn held(&self, pin: Pin, registers: &RegisterFile) -> Option<Level> {
let index = pin.index();
let held = match mode(pin, registers) {
MODE_OUTPUT => true,
MODE_ALTERNATE => self.function[index].is_some(),
_ => false,
} || self.external[index].is_some()
|| pull(pin, registers).is_some();
held.then(|| self.level(pin, registers))
}
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 {
input_bits(|pin| self.sample(pin, registers))
}
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))
}
/// `IDR` as a read would sample it, with no sample kept.
fn peek(
&self,
register: Register,
registers: &RegisterFile,
_now: VirtualNanos,
) -> Option<u64> {
(register == Register::Idr).then(|| input_bits(|pin| self.input_level(pin, 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);
// What a wire soldered to the pin sees is what a chip select reads, and the runtime carries a
// level one model drives to another as a request line. Only a change is one, and a floating
// pin has no level to change to. The last it had is a convention of section 8.4.5's reset
// value, not a wire.
for pin in Pin::all() {
let Some(level) = self.held(pin, registers) else {
continue;
};
if self.announced[pin.index()] != Some(level) {
self.announced[pin.index()] = Some(level);
context.request(pin.index(), level);
}
}
}
}
// `IDR` with each pin's bit set where `level` samples it high.
fn input_bits(mut level: impl FnMut(Pin) -> Option<Level>) -> u64 {
Pin::all()
.filter(|&pin| level(pin) == Some(Level::High))
.map(|pin| field_at(Register::Idr, pin.bit()).mask())
.sum()
}
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,
}
}