rigless 0.2.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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
// 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,
    }
}