Re: [RFC PATCH v1 1/5] hw/gpio: add stm32f1xx GPIO device model (Rust)
Alistair <[email protected]>
| Newsgroups | org.nongnu.qemu-arm,org.nongnu.qemu-devel,org.nongnu.qemu-rust |
|---|---|
| Message-ID | <[email protected]> |
On Sun, 2026-07-26 at 22:51 +0800, Jack Wang wrote: > 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 Oh cool! I don't know the QEMU Rust style, but from a quick look at this it seems sane. So overall looks good. Although I haven't looked too closely or checked the data sheet. When you hook it up to an upstream machine I can check a bit more Alistair > > 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;