Add a native-Rust model of the STM32F1xx GPIO port (RM0008). The device
implements the CRL/CRH mode/config decode and the IDR update algorithm,
driving its 16 gpio-out lines from ODR according to each pin's push-pull,
open-drain or input pull configuration.

The pure register logic lives in GpioRegisters with no QOM/MemoryRegion
coupling so it can be unit-tested natively.

Signed-off-by: Jack Wang <[email protected]>
---
 MAINTAINERS                  |   7 +
 hw/gpio/Kconfig              |   5 +
 hw/gpio/trace-events         |   7 +
 rust/hw/gpio/Kconfig         |   2 +
 rust/hw/gpio/src/bindings.rs |  19 ++
 rust/hw/gpio/src/gpio.rs     | 539 +++++++++++++++++++++++++++++++++++
 rust/hw/gpio/src/lib.rs      |   3 +
 7 files changed, 582 insertions(+)
 create mode 100644 rust/hw/gpio/Kconfig
 create mode 100644 rust/hw/gpio/src/bindings.rs
 create mode 100644 rust/hw/gpio/src/gpio.rs
 create mode 100644 rust/hw/gpio/src/lib.rs

diff --git a/MAINTAINERS b/MAINTAINERS
index 72357df7c4..4870da54ab 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1184,6 +1184,13 @@ L: [email protected]
 S: Maintained
 F: hw/arm/stm32f100_soc.c
 
+STM32F1xx GPIO (Rust)
+M: Jack Wang <[email protected]>
+L: [email protected]
+S: Maintained
+F: rust/hw/gpio/
+F: tests/qtest/stm32f103_gpio-test.c
+
 STM32F205
 M: Alistair Francis <[email protected]>
 M: Peter Maydell <[email protected]>
diff --git a/hw/gpio/Kconfig b/hw/gpio/Kconfig
index a209294c20..3762135114 100644
--- a/hw/gpio/Kconfig
+++ b/hw/gpio/Kconfig
@@ -16,6 +16,11 @@ config SIFIVE_GPIO
 config STM32L4X5_GPIO
     bool
 
+config STM32F1XX_GPIO
+    bool
+    depends on HAVE_RUST
+    select X_STM32F1XX_GPIO_RUST
+
 config PCA9552
     bool
     depends on I2C
diff --git a/hw/gpio/trace-events b/hw/gpio/trace-events
index cea896b28f..c77c6e24a3 100644
--- a/hw/gpio/trace-events
+++ b/hw/gpio/trace-events
@@ -46,3 +46,10 @@ stm32l4x5_gpio_read(char *gpio, uint64_t addr) "GPIO%s addr: 
0x%" PRIx64 " "
 stm32l4x5_gpio_write(char *gpio, uint64_t addr, uint64_t data) "GPIO%s addr: 
0x%" PRIx64 " val: 0x%" PRIx64 ""
 stm32l4x5_gpio_update_idr(char *gpio, uint32_t old_idr, uint32_t new_idr) 
"GPIO%s from: 0x%x to: 0x%x"
 stm32l4x5_gpio_pins(char *gpio, uint16_t disconnected, uint16_t high) "GPIO%s 
disconnected pins: 0x%x levels: 0x%x"
+
+# stm32f1xx GPIO Rust device
+stm32f1xx_gpio_instance_init(void) "instance init"
+stm32f1xx_gpio_reset(void) "reset"
+stm32f1xx_gpio_read(uint64_t offset, uint64_t value, unsigned size) "offset 
0x%" PRIx64 " value 0x%" PRIx64 " size %u"
+stm32f1xx_gpio_write(uint64_t offset, uint64_t value, unsigned size) "offset 
0x%" PRIx64 " value 0x%" PRIx64 " size %u"
+stm32f1xx_gpio_irq(unsigned line, int level) "line %u level %d"
diff --git a/rust/hw/gpio/Kconfig b/rust/hw/gpio/Kconfig
new file mode 100644
index 0000000000..1f2bc9e0e4
--- /dev/null
+++ b/rust/hw/gpio/Kconfig
@@ -0,0 +1,2 @@
+config X_STM32F1XX_GPIO_RUST
+    bool
diff --git a/rust/hw/gpio/src/bindings.rs b/rust/hw/gpio/src/bindings.rs
new file mode 100644
index 0000000000..b0189065b4
--- /dev/null
+++ b/rust/hw/gpio/src/bindings.rs
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#![allow(
+    dead_code,
+    improper_ctypes_definitions,
+    improper_ctypes,
+    non_camel_case_types,
+    non_snake_case,
+    non_upper_case_globals,
+    unnecessary_transmutes,
+    unsafe_op_in_unsafe_fn,
+    clippy::pedantic,
+    clippy::restriction,
+    clippy::style,
+    clippy::missing_const_for_fn,
+    clippy::ptr_offset_with_cast,
+    clippy::useless_transmute,
+    clippy::missing_safety_doc,
+    clippy::too_many_arguments
+)]
diff --git a/rust/hw/gpio/src/gpio.rs b/rust/hw/gpio/src/gpio.rs
new file mode 100644
index 0000000000..a8197d003d
--- /dev/null
+++ b/rust/hw/gpio/src/gpio.rs
@@ -0,0 +1,539 @@
+// STM32F1xx GPIO port device model (Rust)
+// Copyright 2026, Jack Wang
+// Author(s): Jack Wang <[email protected]>
+// SPDX-License-Identifier: GPL-2.0-or-later
+//
+// Reference: ST RM0008, GPIO and AFIO registers.
+// The register model deliberately follows the C stm32l4x5_gpio.c IDR
+// algorithm, adapted to the F1 CRL/CRH (MODE[1:0]+CNF[1:0]) layout.
+//
+// The register state and all of its logic live in `GpioRegisters`, a plain
+// struct with no QOM/MMIO/IRQ coupling. `Stm32f1xxGpioState` is a thin QOM
+// device shell that owns a `GpioRegisters` plus the GPIO-out lines, forwards
+// MMIO/pin callbacks into it, and propagates the resulting pin-level changes
+// onto the out lines. Keeping the logic free of C dependencies lets the unit
+// tests in tests/tests.rs exercise it natively, without booting a machine.
+
+use std::ffi::CStr;
+
+use bql::prelude::*;
+use common::prelude::*;
+use hwcore::prelude::*;
+use migration::prelude::*;
+use qom::prelude::*;
+use system::prelude::*;
+use util::prelude::*;
+
+pub const GPIO_NUM_PINS: usize = 16;
+
+pub const TYPE_STM32F1XX_GPIO_RUST: &CStr = c"stm32f1xx-gpio-rust";
+qom_isa!(Stm32f1xxGpioState: SysBusDevice, DeviceState, Object);
+
+unsafe impl ObjectType for Stm32f1xxGpioState {
+    type Class = <SysBusDevice as ObjectType>::Class;
+    const TYPE_NAME: &'static CStr = TYPE_STM32F1XX_GPIO_RUST;
+}
+
+// Trace bindings are generated from hw/gpio/trace-events (group "hw_gpio").
+// Each `stm32f1xx_gpio_<name>` event yields a
+// `trace::trace_stm32f1xx_gpio_<name>` function whose argument types match the
+// trace-events declaration verbatim.
+::trace::include_trace!("hw_gpio");
+
+/// Register offsets (RM0008), shared by the device and its tests.
+pub const REG_CRL: u64 = 0x00;
+pub const REG_CRH: u64 = 0x04;
+pub const REG_IDR: u64 = 0x08;
+pub const REG_ODR: u64 = 0x0C;
+pub const REG_BSRR: u64 = 0x10;
+pub const REG_BRR: u64 = 0x14;
+pub const REG_LCKR: u64 = 0x18;
+
+/// RM0008 reset values: CRL/CRH = 0x44444444 (all pins floating input).
+pub const RESET_CRL: u32 = 0x4444_4444;
+pub const RESET_CRH: u32 = 0x4444_4444;
+
+/// Pure register state + behaviour of one GPIO port.
+///
+/// This holds no QOM object, no `MemoryRegion` and no `InterruptSource`, so it
+/// can be constructed and driven directly from unit tests. Methods that can
+/// change pin levels return a [`PinUpdate`] describing which output lines
+/// flipped; the QOM shell turns those into `InterruptSource` edges.
+#[repr(C)]
+#[derive(Debug)]
+pub struct GpioRegisters {
+    crl: BqlCell<u32>,
+    crh: BqlCell<u32>,
+    idr: BqlCell<u32>,
+    odr: BqlCell<u32>,
+    lckr: BqlCell<u32>,
+
+    // Bit i set  => pin i is not driven by an external source.
+    // Bit i clear => pin i is driven, and `pins_connected_high` bit i
+    //                gives the level driven.
+    disconnected_pins: BqlCell<u16>,
+    pins_connected_high: BqlCell<u16>,
+}
+
+impl Default for GpioRegisters {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// Which output pins changed level as a result of an operation.
+///
+/// `mask` bit i set means pin i has a determined level in `levels` bit i and
+/// that level differs from before the operation.
+#[derive(Debug, Default, Clone, Copy)]
+pub struct PinUpdate {
+    pub mask: u32,
+    pub levels: u32,
+}
+
+impl GpioRegisters {
+    pub const fn new() -> Self {
+        Self {
+            crl: BqlCell::new(RESET_CRL),
+            crh: BqlCell::new(RESET_CRH),
+            idr: BqlCell::new(0),
+            odr: BqlCell::new(0),
+            lckr: BqlCell::new(0),
+            disconnected_pins: BqlCell::new(0xFFFF),
+            pins_connected_high: BqlCell::new(0),
+        }
+    }
+
+    // ---- register accessors (for tests / vmstate / shell) ----
+
+    pub fn idr(&self) -> u32 {
+        self.idr.get()
+    }
+    pub fn odr(&self) -> u32 {
+        self.odr.get()
+    }
+    pub fn crl(&self) -> u32 {
+        self.crl.get()
+    }
+    pub fn crh(&self) -> u32 {
+        self.crh.get()
+    }
+    pub fn disconnected_pins(&self) -> u16 {
+        self.disconnected_pins.get()
+    }
+
+    // ---- pin configuration helpers (decode CRL/CRH nibbles) ----
+
+    fn extract(value: u32, start: u32, length: u32) -> u32 {
+        assert!(length > 0 && length <= 32 - start);
+        (value >> start) & (!0u32 >> (32 - length))
+    }
+
+    // 4-bit MODE[1:0]+CNF[1:0] field for `pin`.
+    fn get_modecnf(&self, pin: usize) -> u8 {
+        let reg = if pin < 8 {
+            self.crl.get()
+        } else {
+            self.crh.get()
+        };
+        let shift = ((pin % 8) * 4) as u32;
+        Self::extract(reg, shift, 4) as u8
+    }
+
+    // MODE != 0 means output (any speed).
+    fn is_output(&self, pin: usize) -> bool {
+        (self.get_modecnf(pin) & 0x03) != 0
+    }
+
+    fn is_alternate_open_drain(&self, pin: usize) -> bool {
+        let modecnf = self.get_modecnf(pin);
+        ((modecnf & 0x03) != 0) && ((modecnf >> 2) == 0x03)
+    }
+
+    fn is_alternate_push_pull(&self, pin: usize) -> bool {
+        let modecnf = self.get_modecnf(pin);
+        ((modecnf & 0x03) != 0) && ((modecnf >> 2) == 0x02)
+    }
+
+    fn is_general_push_pull(&self, pin: usize) -> bool {
+        let modecnf = self.get_modecnf(pin);
+        ((modecnf & 0x03) != 0) && ((modecnf >> 2) == 0x00)
+    }
+
+    fn is_general_open_drain(&self, pin: usize) -> bool {
+        let modecnf = self.get_modecnf(pin);
+        ((modecnf & 0x03) != 0) && ((modecnf >> 2) == 0x01)
+    }
+
+    // Input with pull-up/pull-down selected: MODE=00, CNF=10 (0b1000).
+    fn is_input_pull_up_down_mode(&self, pin: usize) -> bool {
+        self.get_modecnf(pin) == 0b1000
+    }
+
+    // In pull mode, ODR bit selects pull-up (1) vs pull-down (0).
+    fn is_input_pullup(&self, pin: usize) -> bool {
+        self.is_input_pull_up_down_mode(pin) && ((self.odr.get() & (1 << pin)) 
!= 0)
+    }
+
+    fn is_input_pulldown(&self, pin: usize) -> bool {
+        self.is_input_pull_up_down_mode(pin) && ((self.odr.get() & (1 << pin)) 
== 0)
+    }
+
+    fn check_and_warn_af(&self, start_pin: usize) {
+        for i in 0..8 {
+            let pin = start_pin + i;
+            if self.is_alternate_push_pull(pin) || 
self.is_alternate_open_drain(pin) {
+                log_mask_ln!(
+                    Log::Unimp,
+                    "stm32f1xx_gpio: AF mode configured on pin {}. Downgrading 
to General Output \
+                     to keep simulation running.",
+                    pin
+                );
+            }
+        }
+    }
+
+    // ---- operations (all return which out pins changed) ----
+
+    /// Restore the RM0008 documented reset state.
+    pub fn reset(&self) -> PinUpdate {
+        self.crl.set(RESET_CRL);
+        self.crh.set(RESET_CRH);
+        self.idr.set(0);
+        self.odr.set(0);
+        self.lckr.set(0);
+        self.disconnected_pins.set(0xFFFF);
+        self.pins_connected_high.set(0);
+        self.update_gpio_idr()
+    }
+
+    /// External source drives pin `line` to `level`.
+    pub fn gpio_set(&self, line: usize, level: u32) -> PinUpdate {
+        // A push-pull output (or an open-drain output driven high) cannot be
+        // driven externally without a short-circuit; refuse it. An open-drain
+        // output driven low is allowed.
+        if self.is_output(line)
+            && !((self.is_alternate_open_drain(line) || 
self.is_general_open_drain(line))
+                && (level == 0))
+        {
+            log_mask_ln!(
+                Log::GuestError,
+                "stm32f1xx_gpio: Line {} can't be driven externally",
+                line
+            );
+            return PinUpdate::default();
+        }
+
+        self.disconnected_pins
+            .set(self.disconnected_pins.get() & !(1 << line));
+
+        if level != 0 {
+            self.pins_connected_high
+                .set(self.pins_connected_high.get() | (1 << line));
+        } else {
+            self.pins_connected_high
+                .set(self.pins_connected_high.get() & !(1 << line));
+        }
+
+        self.update_gpio_idr()
+    }
+
+    // Return mask of pins that are both configured in output mode and
+    // externally driven (except open-drain pins externally set to 0), which
+    // must be disconnected to avoid a short circuit.
+    fn get_gpio_pinmask_to_disconnect(&self) -> u16 {
+        let mut pins_to_disconnect: u16 = 0;
+        for i in 0..GPIO_NUM_PINS {
+            if (self.disconnected_pins.get() & (1 << i) == 0) && 
self.is_output(i) {
+                if self.is_general_push_pull(i)
+                    || self.is_alternate_push_pull(i)
+                    || (self.pins_connected_high.get() & (1 << i) != 0)
+                {
+                    pins_to_disconnect |= 1 << i;
+                    log_mask_ln!(
+                        Log::GuestError,
+                        "stm32f1xx_gpio: Line {} can't be driven externally",
+                        i
+                    );
+                }
+            }
+        }
+        pins_to_disconnect
+    }
+
+    fn disconnect_gpio_pins(&self, lines: u16) -> PinUpdate {
+        self.disconnected_pins
+            .set(self.disconnected_pins.get() | lines);
+        self.update_gpio_idr()
+    }
+
+    /// MMIO write. Returns the resulting output-pin changes.
+    pub fn write(&self, offset: u64, data: u32) -> PinUpdate {
+        match offset {
+            REG_CRL => {
+                self.crl.set(data);
+                self.check_and_warn_af(0);
+                // A pin reconfigured to output can no longer be driven.
+                
self.disconnect_gpio_pins(self.get_gpio_pinmask_to_disconnect())
+            }
+            REG_CRH => {
+                self.crh.set(data);
+                self.check_and_warn_af(8);
+                
self.disconnect_gpio_pins(self.get_gpio_pinmask_to_disconnect())
+            }
+            REG_ODR => {
+                self.odr.set(data & 0xFFFF);
+                self.update_gpio_idr()
+            }
+            REG_BRR => {
+                let bits_to_reset = data & 0xFFFF;
+                self.odr.set(self.odr.get() & !bits_to_reset);
+                self.update_gpio_idr()
+            }
+            REG_BSRR => {
+                let bits_to_set = data & 0xFFFF;
+                let bits_to_reset = (data >> 16) & 0xFFFF;
+
+                // If both BSx and BRx are set, BSx has priority.
+                let mut current_odr = self.odr.get();
+                current_odr &= !bits_to_reset;
+                current_odr |= bits_to_set;
+                self.odr.set(current_odr);
+
+                self.update_gpio_idr()
+            }
+            REG_LCKR => {
+                self.lckr.set(data);
+                log_mask_ln!(
+                    Log::Unimp,
+                    "stm32f1xx_gpio: Locking port bits configuration isn't 
supported"
+                );
+                PinUpdate::default()
+            }
+            _ => {
+                log_mask_ln!(
+                    Log::GuestError,
+                    "stm32f1xx_gpio_write: Bad offset 0x{:x}",
+                    offset
+                );
+                PinUpdate::default()
+            }
+        }
+    }
+
+    /// MMIO read.
+    pub fn read(&self, offset: u64) -> u32 {
+        match offset {
+            REG_CRL => self.crl.get(),
+            REG_CRH => self.crh.get(),
+            REG_IDR => self.idr.get(),
+            REG_ODR => self.odr.get(),
+            REG_BSRR => 0,
+            REG_BRR => 0,
+            REG_LCKR => self.lckr.get(),
+            _ => {
+                log_mask_ln!(
+                    Log::GuestError,
+                    "stm32f1xx_gpio_read: Bad offset 0x{:x}",
+                    offset
+                );
+                0
+            }
+        }
+    }
+
+    /// Recompute IDR from the pin configuration and external drive state.
+    /// Returns which determined output pins changed level.
+    fn update_gpio_idr(&self) -> PinUpdate {
+        // new_idr_mask bit i => pin i has a determined level in new_idr.
+        let mut new_idr_mask = 0u32;
+        let mut new_idr = self.odr.get();
+        let old_idr = self.idr.get();
+
+        for i in 0..GPIO_NUM_PINS {
+            if self.is_output(i) {
+                if self.is_general_push_pull(i) || 
self.is_alternate_push_pull(i) {
+                    // push-pull output: pin follows ODR
+                    new_idr_mask |= 1 << i;
+                } else if (self.odr.get() & (1 << i)) == 0 {
+                    // open-drain, ODR 0: pin driven low
+                    new_idr_mask |= 1 << i;
+                } else if (self.disconnected_pins.get() & (1 << i)) == 0
+                    && (self.pins_connected_high.get() & (1 << i)) == 0
+                {
+                    // open-drain, ODR 1, externally pulled low
+                    new_idr &= !(1 << i);
+                    new_idr_mask |= 1 << i;
+                } else if self.is_input_pullup(i) {
+                    new_idr_mask |= 1 << i;
+                } else if self.is_input_pulldown(i) {
+                    new_idr &= !(1 << i);
+                    new_idr_mask |= 1 << i;
+                }
+                // otherwise open-drain ODR 1 floating: keep old value
+            } else if (self.disconnected_pins.get() & (1 << i)) == 0 {
+                // input/analog, externally driven
+                if (self.pins_connected_high.get() & (1 << i)) != 0 {
+                    new_idr_mask |= 1 << i;
+                    new_idr |= 1 << i;
+                } else {
+                    new_idr_mask |= 1 << i;
+                    new_idr &= !(1 << i);
+                }
+            } else {
+                // input/analog, not driven: pull-up/down or floating
+                if self.is_input_pullup(i) {
+                    new_idr |= 1 << i;
+                    new_idr_mask |= 1 << i;
+                } else if self.is_input_pulldown(i) {
+                    new_idr &= !(1 << i);
+                    new_idr_mask |= 1 << i;
+                }
+            }
+        }
+
+        self.idr
+            .set((old_idr & !new_idr_mask) | (new_idr & new_idr_mask));
+
+        // Report which determined pins actually flipped level.
+        let mut changed = 0u32;
+        for i in 0..GPIO_NUM_PINS {
+            if (new_idr_mask & (1 << i)) != 0
+                && ((new_idr & (1 << i)) != 0) != ((old_idr & (1 << i)) != 0)
+            {
+                changed |= 1 << i;
+            }
+        }
+        PinUpdate {
+            mask: changed,
+            levels: new_idr,
+        }
+    }
+}
+
+impl_vmstate_struct!(
+    GpioRegisters,
+    VMStateDescriptionBuilder::<GpioRegisters>::new()
+        .name(c"stm32f1xx-gpio/regs")
+        .version_id(1)
+        .minimum_version_id(1)
+        .fields(vmstate_fields! {
+            vmstate_of!(GpioRegisters, crl),
+            vmstate_of!(GpioRegisters, crh),
+            vmstate_of!(GpioRegisters, idr),
+            vmstate_of!(GpioRegisters, odr),
+            vmstate_of!(GpioRegisters, lckr),
+            vmstate_of!(GpioRegisters, disconnected_pins),
+            vmstate_of!(GpioRegisters, pins_connected_high),
+        })
+        .build()
+);
+
+#[repr(C)]
+#[derive(qom::Object, hwcore::Device)]
+pub struct Stm32f1xxGpioState {
+    parent_obj: ParentField<SysBusDevice>,
+    mmio: MemoryRegion,
+
+    regs: GpioRegisters,
+
+    // One output line per pin, exposed as GPIO-out (to EXTI/board).
+    outlines: [InterruptSource; GPIO_NUM_PINS],
+}
+
+impl Stm32f1xxGpioState {
+    unsafe fn init(mut this: ParentInit<Self>) {
+        static STM32F1XX_GPIO_OPS: MemoryRegionOps<Stm32f1xxGpioState> =
+            MemoryRegionOpsBuilder::<Stm32f1xxGpioState>::new()
+                .read(&Stm32f1xxGpioState::read)
+                .write(&Stm32f1xxGpioState::write)
+                .little_endian()
+                .impl_sizes(4, 4)
+                .build();
+
+        MemoryRegion::init_io(
+            &mut uninit_field_mut!(*this, mmio),
+            &STM32F1XX_GPIO_OPS,
+            "stm32f1xx-gpio-rust",
+            0x400,
+        );
+
+        uninit_field_mut!(*this, regs).write(GpioRegisters::new());
+        uninit_field_mut!(*this, outlines).write(Default::default());
+
+        trace::trace_stm32f1xx_gpio_instance_init();
+    }
+
+    fn post_init(&self) {
+        self.init_mmio(&self.mmio);
+        // Pin-change interrupts on F1 are delivered through EXTI, not a
+        // dedicated NVIC line, so the port only exposes GPIO in/out lines
+        // (no sysbus IRQ).
+        self.init_gpio_in(GPIO_NUM_PINS as u32, Self::gpio_set);
+        self.init_gpio_out(&self.outlines);
+    }
+
+    /// Drive the out lines according to a [`PinUpdate`].
+    fn apply_pin_update(&self, upd: PinUpdate) {
+        for i in 0..GPIO_NUM_PINS {
+            if (upd.mask & (1 << i)) != 0 {
+                let level = (upd.levels & (1 << i)) != 0;
+                self.outlines[i].set(level);
+                trace::trace_stm32f1xx_gpio_irq(i as u32, i32::from(level));
+            }
+        }
+    }
+
+    fn reset_hold(&self, _type: ResetType) {
+        trace::trace_stm32f1xx_gpio_reset();
+        let upd = self.regs.reset();
+        self.apply_pin_update(upd);
+    }
+
+    // GPIO-in callback: external source drives a pin.
+    fn gpio_set(&self, line: u32, level: u32) {
+        let upd = self.regs.gpio_set(line as usize, level);
+        self.apply_pin_update(upd);
+    }
+
+    fn write(&self, offset: hwaddr, data: u64, size: u32) {
+        trace::trace_stm32f1xx_gpio_write(offset, data, size);
+        let upd = self.regs.write(offset, data as u32);
+        self.apply_pin_update(upd);
+    }
+
+    fn read(&self, offset: hwaddr, size: u32) -> u64 {
+        let value = self.regs.read(offset);
+        trace::trace_stm32f1xx_gpio_read(offset, u64::from(value), size);
+        u64::from(value)
+    }
+}
+
+impl ObjectImpl for Stm32f1xxGpioState {
+    type ParentType = SysBusDevice;
+
+    const CLASS_INIT: fn(&mut Self::Class) = Self::Class::class_init::<Self>;
+    const INSTANCE_INIT: Option<unsafe fn(ParentInit<Self>)> = 
Some(Self::init);
+    const INSTANCE_POST_INIT: Option<fn(&Self)> = Some(Self::post_init);
+}
+
+impl DeviceImpl for Stm32f1xxGpioState {
+    const VMSTATE: Option<VMStateDescription<Self>> = 
Some(VMSTATE_STM32F1XX_GPIO);
+}
+
+impl SysBusDeviceImpl for Stm32f1xxGpioState {}
+
+impl ResettablePhasesImpl for Stm32f1xxGpioState {
+    const HOLD: Option<fn(&Self, ResetType)> = Some(Self::reset_hold);
+}
+
+// Migration: register block only; IRQ links are reconstructed on wire-up.
+const VMSTATE_STM32F1XX_GPIO: VMStateDescription<Stm32f1xxGpioState> =
+    VMStateDescriptionBuilder::<Stm32f1xxGpioState>::new()
+        .name(c"stm32f1xx-gpio")
+        .version_id(1)
+        .minimum_version_id(1)
+        .fields(vmstate_fields! {
+            vmstate_of!(Stm32f1xxGpioState, regs),
+        })
+        .build();
diff --git a/rust/hw/gpio/src/lib.rs b/rust/hw/gpio/src/lib.rs
new file mode 100644
index 0000000000..57a1420bb1
--- /dev/null
+++ b/rust/hw/gpio/src/lib.rs
@@ -0,0 +1,3 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pub mod bindings;
+pub mod gpio;
-- 
2.53.0


Reply via email to