* [RFC PATCH v1 0/5] hw/gpio: STM32F1xx GPIO controller in Rust
@ 2026-07-26 14:51 Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 1/5] hw/gpio: add stm32f1xx GPIO device model (Rust) Jack Wang
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Jack Wang @ 2026-07-26 14:51 UTC (permalink / raw)
To: qemu-devel
Cc: Paolo Bonzini, Peter Maydell, Alistair Francis, Fabiano Rosas,
Laurent Vivier, qemu-arm, qemu-rust, caojunze424, Jack Wang
Hi all,
This RFC adds an STM32F1xx GPIO controller modelled in Rust, following the
existing rust/hw/char/pl011 device as a template. The register logic (CRL/CRH
mode+config decode and the IDR update algorithm) is a direct port of the C
model in hw/gpio/stm32l4x5_gpio.c; the STM32F1xx GPIO block (RM0008) is the
same IP used by the upstream stm32f100_soc.
This series is currently stacked on an out-of-tree STM32F103 SoC and board
that are not yet upstream which is https://github.com/shandianchengzi/qemu
But the commit(the initialize logic and gpio architecture) is both capable
for stm32f100.This is the reason that struct name is stm32f1xx. Patch 3
("hw/arm/stm32f103: instantiate and mapGPIO ports") therefore touches files
that do not exist in mainline and willnot apply cleanly on its own.
I am sending this as an RFC to get feedback onthe Rust device model and
its integration now, before the SoC lands, rather than as an apply-ready series
. Once the SoC is upstream (or if you I
can retarget the device at the existing stm32f100_soc), I will resend a
non-RFC series.
checkpatch reports SPDX warnings on Kconfig/trace-events/Cargo.toml/
meson.build and "MAINTAINERS need updating" on the new files. These are
false positives: matching rust/hw/char/pl011, these file types carry no SPDX
header in-tree, and MAINTAINERS already gains an entry covering rust/hw/gpio/
and the qtest in this series.
Anyway this is my first time to generate rust model build so sorry for some
mistakes. And any help or tip are welcomed
Jack Wang (5):
hw/gpio: add stm32f1xx GPIO device model (Rust)
rust: add hw/gpio crate to the workspace
hw/arm/stm32f103: instantiate and map GPIO ports
tests/qtest: add stm32f103 GPIO test
rust/hw/gpio: add native unit tests
MAINTAINERS | 7 +
hw/arm/Kconfig | 1 +
hw/arm/stm32f103_board.c | 2 +-
hw/arm/stm32f103_soc.c | 54 ++-
hw/gpio/Kconfig | 5 +
hw/gpio/trace-events | 7 +
include/hw/arm/stm32f103_soc.h | 9 +
rust/Cargo.lock | 17 +-
rust/Cargo.toml | 18 +
rust/hw/Kconfig | 1 +
rust/hw/gpio/Cargo.toml | 28 ++
rust/hw/gpio/Kconfig | 2 +
rust/hw/gpio/meson.build | 54 +++
rust/hw/gpio/src/bindings.rs | 19 ++
rust/hw/gpio/src/gpio.rs | 539 ++++++++++++++++++++++++++++++
rust/hw/gpio/src/lib.rs | 3 +
rust/hw/gpio/tests/tests.rs | 191 +++++++++++
rust/hw/meson.build | 1 +
tests/qtest/meson.build | 4 +
tests/qtest/stm32f103_gpio-test.c | 328 ++++++++++++++++++
20 files changed, 1281 insertions(+), 9 deletions(-)
create mode 100644 rust/hw/gpio/Cargo.toml
create mode 100644 rust/hw/gpio/Kconfig
create mode 100644 rust/hw/gpio/meson.build
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
create mode 100644 rust/hw/gpio/tests/tests.rs
create mode 100644 tests/qtest/stm32f103_gpio-test.c
--
2.53.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [RFC PATCH v1 1/5] hw/gpio: add stm32f1xx GPIO device model (Rust)
2026-07-26 14:51 [RFC PATCH v1 0/5] hw/gpio: STM32F1xx GPIO controller in Rust Jack Wang
@ 2026-07-26 14:51 ` Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 2/5] rust: add hw/gpio crate to the workspace Jack Wang
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Jack Wang @ 2026-07-26 14:51 UTC (permalink / raw)
To: qemu-devel
Cc: Paolo Bonzini, Peter Maydell, Alistair Francis, Fabiano Rosas,
Laurent Vivier, qemu-arm, qemu-rust, caojunze424, Jack Wang
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 <163wangjack@gmail.com>
---
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: qemu-arm@nongnu.org
S: Maintained
F: hw/arm/stm32f100_soc.c
+STM32F1xx GPIO (Rust)
+M: Jack Wang <163wangjack@gmail.com>
+L: qemu-arm@nongnu.org
+S: Maintained
+F: rust/hw/gpio/
+F: tests/qtest/stm32f103_gpio-test.c
+
STM32F205
M: Alistair Francis <alistair@alistair23.me>
M: Peter Maydell <peter.maydell@linaro.org>
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 <163wangjack@gmail.com>
+// 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
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [RFC PATCH v1 2/5] rust: add hw/gpio crate to the workspace
2026-07-26 14:51 [RFC PATCH v1 0/5] hw/gpio: STM32F1xx GPIO controller in Rust Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 1/5] hw/gpio: add stm32f1xx GPIO device model (Rust) Jack Wang
@ 2026-07-26 14:51 ` Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 3/5] hw/arm/stm32f103: instantiate and map GPIO ports Jack Wang
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Jack Wang @ 2026-07-26 14:51 UTC (permalink / raw)
To: qemu-devel
Cc: Paolo Bonzini, Peter Maydell, Alistair Francis, Fabiano Rosas,
Laurent Vivier, qemu-arm, qemu-rust, caojunze424, Jack Wang
Register the new stm32f1xx_gpio crate in the Rust workspace and hook its
meson build and Kconfig into rust/hw so it is compiled into the softmmu
build when Rust is enabled.
Signed-off-by: Jack Wang <163wangjack@gmail.com>
---
rust/Cargo.lock | 17 ++++++++++++-
rust/Cargo.toml | 18 ++++++++++++++
rust/hw/Kconfig | 1 +
rust/hw/gpio/Cargo.toml | 28 +++++++++++++++++++++
rust/hw/gpio/meson.build | 54 ++++++++++++++++++++++++++++++++++++++++
rust/hw/meson.build | 1 +
6 files changed, 118 insertions(+), 1 deletion(-)
create mode 100644 rust/hw/gpio/Cargo.toml
create mode 100644 rust/hw/gpio/meson.build
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index cbb3ca15f7..2daadaaf15 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -184,7 +184,6 @@ dependencies = [
"glib-sys",
"migration-sys",
"qom-sys",
- "system-sys",
"util-sys",
]
@@ -387,6 +386,21 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+[[package]]
+name = "stm32f1xx_gpio"
+version = "0.1.0"
+dependencies = [
+ "bql",
+ "common",
+ "glib-sys",
+ "hwcore",
+ "migration",
+ "qom",
+ "system",
+ "trace",
+ "util",
+]
+
[[package]]
name = "syn"
version = "2.0.104"
@@ -431,6 +445,7 @@ version = "0.1.0"
dependencies = [
"common",
"glib-sys",
+ "hwcore-sys",
"migration-sys",
"qom-sys",
"util-sys",
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 0d24eb84e1..7661205d3c 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -1,9 +1,27 @@
[workspace]
resolver = "2"
members = [
+ "bindings/chardev-sys",
+ "bindings/hwcore-sys",
+ "bindings/migration-sys",
+ "bindings/qom-sys",
+ "bindings/system-sys",
+ "bindings/util-sys",
+ "bits",
+ "bql",
+ "chardev",
+ "common",
"hw/char/pl011",
+ "hw/core",
+ "hw/gpio",
"hw/timer/hpet",
+ "migration",
+ "qemu-macros",
+ "qom",
+ "system",
"tests",
+ "trace",
+ "util",
]
[workspace.package]
diff --git a/rust/hw/Kconfig b/rust/hw/Kconfig
index 36f92ec028..ba1297ca2d 100644
--- a/rust/hw/Kconfig
+++ b/rust/hw/Kconfig
@@ -1,3 +1,4 @@
# devices Kconfig
source char/Kconfig
+source gpio/Kconfig
source timer/Kconfig
diff --git a/rust/hw/gpio/Cargo.toml b/rust/hw/gpio/Cargo.toml
new file mode 100644
index 0000000000..5c6eade306
--- /dev/null
+++ b/rust/hw/gpio/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "stm32f1xx_gpio"
+version = "0.1.0"
+description = "STM32F1xx GPIO device model for QEMU"
+resolver = "2"
+publish = false
+
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+glib-sys.workspace = true
+
+# QEMU Rust foundations crate
+common = { path = "../../common" }
+util = { path = "../../util" }
+bql = { path = "../../bql" }
+migration = { path = "../../migration" }
+qom = { path = "../../qom" }
+system = { path = "../../system" }
+hwcore = { path = "../../hw/core" }
+trace = { path = "../../trace" }
+
+[lints]
+workspace = true
diff --git a/rust/hw/gpio/meson.build b/rust/hw/gpio/meson.build
new file mode 100644
index 0000000000..35da59991f
--- /dev/null
+++ b/rust/hw/gpio/meson.build
@@ -0,0 +1,54 @@
+_libstm32f1xx_gpio_rs = static_library(
+ 'stm32f1xx_gpio',
+ files(
+ 'src/lib.rs',
+ 'src/gpio.rs',
+ 'src/bindings.rs',
+ ),
+ override_options: ['rust_std=2021', 'build.rust_std=2021'],
+ rust_abi: 'rust',
+ link_with: [
+ _util_rs,
+ _migration_rs,
+ _bql_rs,
+ _qom_rs,
+ _system_rs,
+ _hwcore_rs,
+ _trace_rs,
+ ],
+ dependencies: [
+ common_rs,
+ glib_sys_rs,
+ ],
+)
+
+rust_devices_ss.add(when: 'CONFIG_X_STM32F1XX_GPIO_RUST', if_true: [declare_dependency(
+ link_whole: [_libstm32f1xx_gpio_rs],
+ variables: {'crate': 'stm32f1xx_gpio'},
+)])
+
+# Dependency usable by the in-crate integration test below.
+stm32f1xx_gpio_rs = declare_dependency(link_with: [_libstm32f1xx_gpio_rs],
+ dependencies: [common_rs, qom_rs, hwcore_rs, bql_rs, migration_rs,
+ system_rs, util_rs])
+
+# Native Rust unit tests for the pure register logic (GpioRegisters). These
+# need no qtest and no machine boot: they construct GpioRegisters directly and
+# only require the BQL mock (bql::start_test). Run with:
+# meson test --suite rust rust-stm32f1xx-gpio-integration
+test('rust-stm32f1xx-gpio-integration',
+ executable(
+ 'rust-stm32f1xx-gpio-integration',
+ files('tests/tests.rs'),
+ override_options: ['rust_std=2021', 'build.rust_std=2021'],
+ rust_args: ['--test'],
+ install: false,
+ dependencies: [stm32f1xx_gpio_rs, common_rs, qom_rs, hwcore_rs,
+ bql_rs, migration_rs, system_rs, util_rs]),
+ args: [
+ '--test', '--test-threads', '1',
+ '--format', 'pretty',
+ ],
+ protocol: 'rust',
+ suite: ['unit', 'rust'])
+
diff --git a/rust/hw/meson.build b/rust/hw/meson.build
index 9749d4adfc..d6b273170e 100644
--- a/rust/hw/meson.build
+++ b/rust/hw/meson.build
@@ -1,2 +1,3 @@
subdir('char')
+subdir('gpio')
subdir('timer')
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [RFC PATCH v1 3/5] hw/arm/stm32f103: instantiate and map GPIO ports
2026-07-26 14:51 [RFC PATCH v1 0/5] hw/gpio: STM32F1xx GPIO controller in Rust Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 1/5] hw/gpio: add stm32f1xx GPIO device model (Rust) Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 2/5] rust: add hw/gpio crate to the workspace Jack Wang
@ 2026-07-26 14:51 ` Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 4/5] tests/qtest: add stm32f103 GPIO test Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 5/5] rust/hw/gpio: add native unit tests Jack Wang
4 siblings, 0 replies; 6+ messages in thread
From: Jack Wang @ 2026-07-26 14:51 UTC (permalink / raw)
To: qemu-devel
Cc: Paolo Bonzini, Peter Maydell, Alistair Francis, Fabiano Rosas,
Laurent Vivier, qemu-arm, qemu-rust, caojunze424, Jack Wang
Create the seven GPIO ports GPIOA..GPIOG by name via qdev_new() and map
them at their APB2 base addresses, removing the corresponding
unimplemented-device stubs so the real devices back that range.
The Rust GPIO type is only present in a Rust-enabled build, so the ports
are created behind an object_class_by_name() check; with --disable-rust
the type is absent and the range is simply left unbacked.
Signed-off-by: Jack Wang <163wangjack@gmail.com>
---
hw/arm/Kconfig | 1 +
hw/arm/stm32f103_board.c | 2 +-
hw/arm/stm32f103_soc.c | 54 +++++++++++++++++++++++++++++-----
include/hw/arm/stm32f103_soc.h | 9 ++++++
4 files changed, 58 insertions(+), 8 deletions(-)
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 66aed6edd7..7aa194c64a 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -410,6 +410,7 @@ config STM32F103_SOC
select STM32F2XX_USART
select STM32F2XX_SPI
select STM32F1XX_ADC
+ select STM32F1XX_GPIO if HAVE_RUST
config B_L475E_IOT01A
bool
diff --git a/hw/arm/stm32f103_board.c b/hw/arm/stm32f103_board.c
index a740483575..fb5041e484 100644
--- a/hw/arm/stm32f103_board.c
+++ b/hw/arm/stm32f103_board.c
@@ -26,7 +26,7 @@
#include "qemu/osdep.h"
#include "qapi/error.h"
-#include "hw/boards.h"
+#include "hw/core/boards.h"
#include "hw/core/qdev-properties.h"
#include "hw/core/qdev-clock.h"
#include "qemu/error-report.h"
diff --git a/hw/arm/stm32f103_soc.c b/hw/arm/stm32f103_soc.c
index e76640ec03..75fc99a554 100644
--- a/hw/arm/stm32f103_soc.c
+++ b/hw/arm/stm32f103_soc.c
@@ -24,12 +24,16 @@
#include "qemu/osdep.h"
#include "qapi/error.h"
+#include "qom/object.h"
#include "system/address-spaces.h"
#include "system/system.h"
#include "hw/arm/stm32f103_soc.h"
#include "hw/core/qdev-clock.h"
#include "hw/misc/unimp.h"
+/* QOM type name of the Rust GPIO device (rust/hw/gpio/src/gpio.rs) */
+#define TYPE_STM32F1XX_GPIO_RUST "stm32f1xx-gpio-rust"
+
#define RCC_ADDR 0x40021000
/* USART addresses: USART1, USART2, USART3, UART4, UART5 */
@@ -47,6 +51,12 @@ static const uint32_t adc_addr[] = {
0x40012400, 0x40012800, 0x40013C00
};
+/* GPIO addresses: GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG */
+static const uint32_t gpio_addr[] = {
+ 0x40010800, 0x40010C00, 0x40011000, 0x40011400, 0x40011800, 0x40011C00,
+ 0x40012000
+};
+
static const int usart_irq[] = { 37, 38, 39, 52, 53 };
static const int spi_irq[] = { 35, 36, 51 };
#define ADC_IRQ 18
@@ -199,6 +209,37 @@ static void stm32f103_soc_realize(DeviceState *dev_soc, Error **errp)
qdev_get_gpio_in(DEVICE(&s->adc_irqs), i));
}
+ /*
+ * GPIO ports GPIOA..GPIOG.
+ *
+ * These are implemented by the Rust device "stm32f1xx-gpio-rust", created
+ * by name with qdev_new() and mapped at its APB2 base address. On STM32F1
+ * a GPIO port has no dedicated NVIC line of its own: pin-change interrupts
+ * are delivered through the EXTI controller (not yet modelled), so no
+ * sysbus_connect_irq() to the NVIC is done here.
+ *
+ * The GPIO port is only provided by the Rust build; when QEMU is built
+ * with --disable-rust the type is not registered, so skip creating the
+ * ports and leave the address range unbacked.
+ */
+ if (object_class_by_name(TYPE_STM32F1XX_GPIO_RUST)) {
+ for (i = 0; i < STM32F103_NUM_GPIOS; i++) {
+ g_autofree char *name = g_strdup_printf("gpio%c", 'a' + i);
+ s->gpio[i] = qdev_new(TYPE_STM32F1XX_GPIO_RUST);
+ /*
+ * Give each port a stable QOM name ("/machine/soc/gpioa"..) so
+ * tests and the monitor can reach it and its gpio-out lines by
+ * path.
+ */
+ object_property_add_child(OBJECT(s), name, OBJECT(s->gpio[i]));
+ busdev = SYS_BUS_DEVICE(s->gpio[i]);
+ if (!sysbus_realize_and_unref(busdev, errp)) {
+ return;
+ }
+ sysbus_mmio_map(busdev, 0, gpio_addr[i]);
+ }
+ }
+
/*
* Unimplemented peripherals -- STM32F103 devices that do not yet
* have QEMU models.
@@ -231,13 +272,12 @@ static void stm32f103_soc_realize(DeviceState *dev_soc, Error **errp)
/* APB2 peripherals */
create_unimplemented_device("AFIO", 0x40010000, 0x400);
create_unimplemented_device("EXTI", 0x40010400, 0x400);
- create_unimplemented_device("GPIOA", 0x40010800, 0x400);
- create_unimplemented_device("GPIOB", 0x40010C00, 0x400);
- create_unimplemented_device("GPIOC", 0x40011000, 0x400);
- create_unimplemented_device("GPIOD", 0x40011400, 0x400);
- create_unimplemented_device("GPIOE", 0x40011800, 0x400);
- create_unimplemented_device("GPIOF", 0x40011C00, 0x400);
- create_unimplemented_device("GPIOG", 0x40012000, 0x400);
+ /*
+ * GPIOA..GPIOG (0x40010800..0x40012000) are real devices mapped in the
+ * GPIO loop above when the Rust build is used, so they must NOT be
+ * registered as unimplemented devices here: overlapping MemoryRegions at
+ * the same address abort at runtime.
+ */
create_unimplemented_device("timer[1]", 0x40012C00, 0x400);
create_unimplemented_device("timer[8]", 0x40013400, 0x400);
create_unimplemented_device("timer[9]", 0x40014C00, 0x400);
diff --git a/include/hw/arm/stm32f103_soc.h b/include/hw/arm/stm32f103_soc.h
index 260f3695c0..8d78360ce0 100644
--- a/include/hw/arm/stm32f103_soc.h
+++ b/include/hw/arm/stm32f103_soc.h
@@ -40,6 +40,7 @@ OBJECT_DECLARE_SIMPLE_TYPE(STM32F103State, STM32F103_SOC)
#define STM32F103_NUM_USARTS 5
#define STM32F103_NUM_SPIS 3
#define STM32F103_NUM_ADCS 3
+#define STM32F103_NUM_GPIOS 7
/* High-density STM32F103: 512 KB Flash, 64 KB SRAM */
#define STM32F103_FLASH_BASE_ADDRESS 0x08000000
@@ -58,6 +59,14 @@ struct STM32F103State {
OrIRQState adc_irqs;
STM32F1XXADCState adc[STM32F103_NUM_ADCS];
+ /*
+ * GPIOA..GPIOG are implemented by the Rust device "stm32f1xx-gpio-rust".
+ * That device is created by name via qdev_new() (pointer-based mounting)
+ * rather than embedded by value, so no C struct definition is required
+ * here and no struct-size coupling with the Rust side is needed.
+ */
+ DeviceState *gpio[STM32F103_NUM_GPIOS];
+
MemoryRegion sram;
MemoryRegion flash;
MemoryRegion flash_alias;
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [RFC PATCH v1 4/5] tests/qtest: add stm32f103 GPIO test
2026-07-26 14:51 [RFC PATCH v1 0/5] hw/gpio: STM32F1xx GPIO controller in Rust Jack Wang
` (2 preceding siblings ...)
2026-07-26 14:51 ` [RFC PATCH v1 3/5] hw/arm/stm32f103: instantiate and map GPIO ports Jack Wang
@ 2026-07-26 14:51 ` Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 5/5] rust/hw/gpio: add native unit tests Jack Wang
4 siblings, 0 replies; 6+ messages in thread
From: Jack Wang @ 2026-07-26 14:51 UTC (permalink / raw)
To: qemu-devel
Cc: Paolo Bonzini, Peter Maydell, Alistair Francis, Fabiano Rosas,
Laurent Vivier, qemu-arm, qemu-rust, caojunze424, Jack Wang
Add a qtest that drives the STM32F103 GPIO ports over the qtest socket,
covering the RM0008 reset state, BSRR/BRR set/reset semantics and the
output-line behaviour for push-pull and open-drain configurations.
Signed-off-by: Jack Wang <163wangjack@gmail.com>
---
tests/qtest/meson.build | 4 +
tests/qtest/stm32f103_gpio-test.c | 328 ++++++++++++++++++++++++++++++
2 files changed, 332 insertions(+)
create mode 100644 tests/qtest/stm32f103_gpio-test.c
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index e154abb05d..4f407b68fe 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -235,6 +235,9 @@ qtests_stm32l4x5 = \
'stm32l4x5_gpio-test',
'stm32l4x5_usart-test']
+qtests_stm32f103 = \
+ ['stm32f103_gpio-test']
+
qtests_arm = \
(config_all_devices.has_key('CONFIG_MPS2') ? ['sse-timer-test'] : []) + \
(config_all_devices.has_key('CONFIG_CMSDK_APB_DUALTIMER') ? ['cmsdk-apb-dualtimer-test'] : []) + \
@@ -250,6 +253,7 @@ qtests_arm = \
(config_all_devices.has_key('CONFIG_VEXPRESS') ? ['test-arm-mptimer'] : []) + \
(config_all_devices.has_key('CONFIG_MICROBIT') ? ['microbit-test'] : []) + \
(config_all_devices.has_key('CONFIG_STM32L4X5_SOC') ? qtests_stm32l4x5 : []) + \
+ (config_all_devices.has_key('CONFIG_STM32F103_SOC') ? qtests_stm32f103 : []) + \
(config_all_devices.has_key('CONFIG_FSI_APB2OPB_ASPEED') ? ['aspeed_fsi-test'] : []) + \
(config_all_devices.has_key('CONFIG_STM32L4X5_SOC') and
config_all_devices.has_key('CONFIG_DM163')? ['dm163-test'] : []) + \
diff --git a/tests/qtest/stm32f103_gpio-test.c b/tests/qtest/stm32f103_gpio-test.c
new file mode 100644
index 0000000000..97ef7dd3e8
--- /dev/null
+++ b/tests/qtest/stm32f103_gpio-test.c
@@ -0,0 +1,328 @@
+/*
+ * QTest testcase for STM32F103 GPIO (Rust device "stm32f1xx-gpio-rust")
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Copyright (c) 2026 QEMU contributors
+ *
+ * The STM32F1 GPIO differs from the L4 family: each pin is configured by a
+ * 4-bit field MODE[1:0]+CNF[1:0] packed into CRL (pins 0-7) and CRH (pins
+ * 8-15), instead of the separate MODER/OTYPER/PUPDR registers of the L4.
+ * See ST RM0008 for the register map and reset values.
+ */
+
+#include "qemu/osdep.h"
+#include "libqtest-single.h"
+
+#define GPIO_BASE_ADDR 0x40010800
+#define GPIO_SIZE 0x400
+#define NUM_GPIOS 7
+#define NUM_GPIO_PINS 16
+
+#define GPIO_A 0x40010800
+#define GPIO_B 0x40010C00
+#define GPIO_C 0x40011000
+#define GPIO_D 0x40011400
+#define GPIO_E 0x40011800
+#define GPIO_F 0x40011C00
+#define GPIO_G 0x40012000
+
+/* Register offsets (RM0008). */
+#define CRL 0x00
+#define CRH 0x04
+#define IDR 0x08
+#define ODR 0x0C
+#define BSRR 0x10
+#define BRR 0x14
+#define LCKR 0x18
+
+/* 4-bit MODE[1:0]+CNF[1:0] pin configurations. */
+#define CFG_INPUT_FLOATING 0x4 /* MODE=00 CNF=01 (reset default) */
+#define CFG_INPUT_PULL 0x8 /* MODE=00 CNF=10, ODR selects up/down */
+#define CFG_OUTPUT_PP 0x1 /* MODE=01 CNF=00, general push-pull */
+#define CFG_OUTPUT_OD 0x5 /* MODE=01 CNF=01, general open-drain */
+
+/* CRL/CRH reset value: every pin floating input. */
+#define CR_RESET 0x44444444
+
+static uint32_t gpio_readl(unsigned int gpio, unsigned int offset)
+{
+ return readl(gpio + offset);
+}
+
+static void gpio_writel(unsigned int gpio, unsigned int offset, uint32_t value)
+{
+ writel(gpio + offset, value);
+}
+
+/* Set the 4-bit config nibble for `pin` in CRL/CRH. */
+static void gpio_set_config(unsigned int gpio, unsigned int pin, uint8_t cfg)
+{
+ unsigned int reg = (pin < 8) ? CRL : CRH;
+ unsigned int shift = (pin % 8) * 4;
+ uint32_t mask = ~(0xFu << shift);
+ uint32_t val = (gpio_readl(gpio, reg) & mask) | ((uint32_t)cfg << shift);
+ gpio_writel(gpio, reg, val);
+}
+
+static void gpio_set_odr_bit(unsigned int gpio, unsigned int pin,
+ uint32_t value)
+{
+ uint32_t mask = ~(0x1u << pin);
+ gpio_writel(gpio, ODR, (gpio_readl(gpio, ODR) & mask) | (value << pin));
+}
+
+static unsigned int get_gpio_id(uint32_t gpio_addr)
+{
+ return (gpio_addr - GPIO_BASE_ADDR) / GPIO_SIZE;
+}
+
+static const char *gpio_path(uint32_t gpio)
+{
+ static char buf[32];
+ snprintf(buf, sizeof(buf), "/machine/soc/gpio%c", 'a' + get_gpio_id(gpio));
+ return buf;
+}
+
+/* Drive pin `num` of `gpio` to `level` from an external source. */
+static void gpio_set_irq(unsigned int gpio, int num, int level)
+{
+ qtest_set_irq_in(global_qtest, gpio_path(gpio), NULL, num, level);
+}
+
+/*
+ * Intercept this port's own gpio-out lines so get_irq(pin) observes them.
+ *
+ * qtest only supports intercepting one device's out-GPIOs per session (the
+ * interception device is global), so every test that observes gpio-out via
+ * get_irq() must target the SAME port. We use GPIO_A for all of them. Tests
+ * that only read IDR/ODR over MMIO can use any port and skip interception.
+ */
+static void intercept_out(unsigned int gpio)
+{
+ qtest_irq_intercept_out(global_qtest, gpio_path(gpio));
+}
+
+static void reset_gpio(unsigned int gpio)
+{
+ gpio_writel(gpio, CRL, CR_RESET);
+ gpio_writel(gpio, CRH, CR_RESET);
+ gpio_writel(gpio, ODR, 0);
+}
+
+static void test_reset_values(void)
+{
+ /*
+ * Scribble over the config/data registers, issue a system reset, and
+ * check the RM0008 documented reset state is restored: CRL/CRH back to
+ * 0x44444444 (floating input) and IDR/ODR back to 0.
+ */
+ gpio_writel(GPIO_A, CRL, 0xDEADBEEF);
+ gpio_writel(GPIO_A, CRH, 0xDEADBEEF);
+ gpio_writel(GPIO_A, ODR, 0xDEADBEEF);
+
+ gpio_writel(GPIO_C, CRL, 0x12345678);
+ gpio_writel(GPIO_C, ODR, 0x0000FFFF);
+
+ qtest_system_reset(global_qtest);
+
+ g_assert_cmphex(gpio_readl(GPIO_A, CRL), ==, CR_RESET);
+ g_assert_cmphex(gpio_readl(GPIO_A, CRH), ==, CR_RESET);
+ g_assert_cmphex(gpio_readl(GPIO_A, IDR), ==, 0);
+ g_assert_cmphex(gpio_readl(GPIO_A, ODR), ==, 0);
+
+ g_assert_cmphex(gpio_readl(GPIO_C, CRL), ==, CR_RESET);
+ g_assert_cmphex(gpio_readl(GPIO_C, IDR), ==, 0);
+ g_assert_cmphex(gpio_readl(GPIO_C, ODR), ==, 0);
+}
+
+static void test_output_mode(const void *data)
+{
+ /*
+ * Configure a pin as push-pull output and check ODR drives IDR and the
+ * gpio-out line. Also check ODR writes made while the pin is still an
+ * input are latched (not discarded), taking effect once switched.
+ */
+ unsigned int pin = (uintptr_t)data & 0xF;
+ uint32_t gpio = (uintptr_t)data & ~(GPIO_SIZE - 1);
+ bool observe = (gpio == GPIO_A);
+
+ reset_gpio(gpio);
+ if (observe) {
+ intercept_out(gpio);
+ }
+
+ /* Set ODR while floating input: IDR unaffected, no output edge. */
+ gpio_set_odr_bit(gpio, pin, 1);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, 0);
+ if (observe) {
+ g_assert_false(get_irq(pin));
+ }
+
+ /* Switch to push-pull output: pin now follows ODR (high). */
+ gpio_set_config(gpio, pin, CFG_OUTPUT_PP);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, (1u << pin));
+ if (observe) {
+ g_assert_true(get_irq(pin));
+ }
+
+ /* Clear ODR bit: pin goes low. */
+ gpio_set_odr_bit(gpio, pin, 0);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, 0);
+ if (observe) {
+ g_assert_false(get_irq(pin));
+ }
+
+ reset_gpio(gpio);
+}
+
+static void test_input_mode(const void *data)
+{
+ /*
+ * Configure a pin as floating input and check that an external drive
+ * (via the gpio-in line) is reflected in IDR and on the gpio-out line.
+ */
+ unsigned int pin = (uintptr_t)data & 0xF;
+ uint32_t gpio = (uintptr_t)data & ~(GPIO_SIZE - 1);
+ bool observe = (gpio == GPIO_A);
+
+ reset_gpio(gpio);
+ if (observe) {
+ intercept_out(gpio);
+ }
+
+ gpio_set_config(gpio, pin, CFG_INPUT_FLOATING);
+
+ gpio_set_irq(gpio, pin, 1);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, (1u << pin));
+ if (observe) {
+ g_assert_true(get_irq(pin));
+ }
+
+ gpio_set_irq(gpio, pin, 0);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, 0);
+ if (observe) {
+ g_assert_false(get_irq(pin));
+ }
+
+ reset_gpio(gpio);
+}
+
+static void test_pull_up_down(const void *data)
+{
+ /*
+ * In input pull mode (CNF=10), ODR selects the resistor: ODR=1 pulls the
+ * pin up (IDR=1) and ODR=0 pulls it down (IDR=0), even with no external
+ * driver connected.
+ */
+ unsigned int pin = (uintptr_t)data & 0xF;
+ uint32_t gpio = (uintptr_t)data & ~(GPIO_SIZE - 1);
+
+ reset_gpio(gpio);
+ intercept_out(gpio);
+
+ /* Pull-up. */
+ gpio_set_odr_bit(gpio, pin, 1);
+ gpio_set_config(gpio, pin, CFG_INPUT_PULL);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, (1u << pin));
+ g_assert_true(get_irq(pin));
+
+ /* Pull-down. */
+ gpio_set_odr_bit(gpio, pin, 0);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, 0);
+ g_assert_false(get_irq(pin));
+
+ reset_gpio(gpio);
+}
+
+static void test_bsrr_brr(const void *data)
+{
+ /*
+ * BSRR: low 16 bits set ODR bits, high 16 bits reset them, with set
+ * taking priority. BRR resets ODR bits.
+ */
+ unsigned int pin = (uintptr_t)data & 0xF;
+ uint32_t gpio = (uintptr_t)data & ~(GPIO_SIZE - 1);
+
+ reset_gpio(gpio);
+
+ gpio_writel(gpio, BSRR, (1u << pin));
+ g_assert_cmphex(gpio_readl(gpio, ODR), ==, (1u << pin));
+
+ gpio_writel(gpio, BSRR, (1u << (pin + 16)));
+ g_assert_cmphex(gpio_readl(gpio, ODR), ==, 0);
+
+ gpio_writel(gpio, BSRR, (1u << pin));
+ g_assert_cmphex(gpio_readl(gpio, ODR), ==, (1u << pin));
+
+ gpio_writel(gpio, BRR, (1u << pin));
+ g_assert_cmphex(gpio_readl(gpio, ODR), ==, 0);
+
+ /* Set has priority over reset in BSRR. */
+ gpio_writel(gpio, BSRR, (1u << pin) | (1u << (pin + 16)));
+ g_assert_cmphex(gpio_readl(gpio, ODR), ==, (1u << pin));
+
+ reset_gpio(gpio);
+}
+
+static void test_push_pull_disconnect(const void *data)
+{
+ /*
+ * A pin driven externally, then reconfigured as push-pull output, must be
+ * disconnected from the external driver: its IDR follows ODR, not the
+ * stale external level.
+ */
+ unsigned int pin = (uintptr_t)data & 0xF;
+ uint32_t gpio = (uintptr_t)data & ~(GPIO_SIZE - 1);
+
+ reset_gpio(gpio);
+
+ /* Drive high externally as input. */
+ gpio_set_config(gpio, pin, CFG_INPUT_FLOATING);
+ gpio_set_irq(gpio, pin, 1);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, (1u << pin));
+
+ /* Reconfigure as push-pull output with ODR=0: pin must drop to 0. */
+ gpio_set_odr_bit(gpio, pin, 0);
+ gpio_set_config(gpio, pin, CFG_OUTPUT_PP);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, 0);
+
+ /* Further external drives are ignored while output push-pull. */
+ gpio_set_irq(gpio, pin, 1);
+ g_assert_cmphex(gpio_readl(gpio, IDR), ==, 0);
+
+ reset_gpio(gpio);
+}
+
+int main(int argc, char **argv)
+{
+ int ret;
+
+ g_test_init(&argc, &argv, NULL);
+ g_test_set_nonfatal_assertions();
+
+ qtest_add_func("stm32f103/gpio/reset_values", test_reset_values);
+ qtest_add_data_func("stm32f103/gpio/output_mode_a5",
+ (void *)(uintptr_t)(GPIO_A | 5), test_output_mode);
+ qtest_add_data_func("stm32f103/gpio/output_mode_c13",
+ (void *)(uintptr_t)(GPIO_C | 13), test_output_mode);
+ qtest_add_data_func("stm32f103/gpio/input_mode_b6",
+ (void *)(uintptr_t)(GPIO_B | 6), test_input_mode);
+ qtest_add_data_func("stm32f103/gpio/input_mode_d10",
+ (void *)(uintptr_t)(GPIO_D | 10), test_input_mode);
+ qtest_add_data_func("stm32f103/gpio/pull_up_down_a0",
+ (void *)(uintptr_t)(GPIO_A | 0), test_pull_up_down);
+ qtest_add_data_func("stm32f103/gpio/bsrr_brr_a1",
+ (void *)(uintptr_t)(GPIO_A | 1), test_bsrr_brr);
+ qtest_add_data_func("stm32f103/gpio/bsrr_brr_e12",
+ (void *)(uintptr_t)(GPIO_E | 12), test_bsrr_brr);
+ qtest_add_data_func("stm32f103/gpio/push_pull_disconnect_g7",
+ (void *)(uintptr_t)(GPIO_G | 7),
+ test_push_pull_disconnect);
+
+ qtest_start("-machine stm32f103");
+ ret = g_test_run();
+ qtest_end();
+
+ return ret;
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [RFC PATCH v1 5/5] rust/hw/gpio: add native unit tests
2026-07-26 14:51 [RFC PATCH v1 0/5] hw/gpio: STM32F1xx GPIO controller in Rust Jack Wang
` (3 preceding siblings ...)
2026-07-26 14:51 ` [RFC PATCH v1 4/5] tests/qtest: add stm32f103 GPIO test Jack Wang
@ 2026-07-26 14:51 ` Jack Wang
4 siblings, 0 replies; 6+ messages in thread
From: Jack Wang @ 2026-07-26 14:51 UTC (permalink / raw)
To: qemu-devel
Cc: Paolo Bonzini, Peter Maydell, Alistair Francis, Fabiano Rosas,
Laurent Vivier, qemu-arm, qemu-rust, caojunze424, Jack Wang
Add native Rust unit tests that link GpioRegisters directly and exercise
the CRL/CRH decode and IDR update algorithm in-process, without booting a
machine or pulling in the softmmu memory subsystem.
Signed-off-by: Jack Wang <163wangjack@gmail.com>
---
rust/hw/gpio/tests/tests.rs | 191 ++++++++++++++++++++++++++++++++++++
1 file changed, 191 insertions(+)
create mode 100644 rust/hw/gpio/tests/tests.rs
diff --git a/rust/hw/gpio/tests/tests.rs b/rust/hw/gpio/tests/tests.rs
new file mode 100644
index 0000000000..ca4d5ed4d6
--- /dev/null
+++ b/rust/hw/gpio/tests/tests.rs
@@ -0,0 +1,191 @@
+// Native Rust unit tests for the STM32F1xx GPIO register logic.
+//
+// Copyright 2026, Jack Wang
+// Author(s): Jack Wang <163wangjack@gmail.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+//
+// Unlike tests/qtest/stm32f103_gpio-test.c (which drives the device over a
+// socket from an external qemu-system-arm process), these tests link the
+// device crate directly and exercise `GpioRegisters` in-process. This gives
+// fast, native Rust coverage of the CRL/CRH decode + IDR algorithm without
+// booting a machine or pulling in the softmmu memory subsystem.
+//
+// `GpioRegisters` deliberately has no QOM object, no MemoryRegion and no
+// InterruptSource, so it can be constructed with `GpioRegisters::new()` and
+// called directly. Only the BQL mock (bql::start_test) is required, because
+// the register cells are `BqlCell`s.
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+use stm32f1xx_gpio::gpio::{
+ GpioRegisters, PinUpdate, REG_BRR, REG_BSRR, REG_CRH, REG_CRL, REG_IDR, REG_ODR, RESET_CRH,
+ RESET_CRL,
+};
+
+/// Take the mock BQL so `BqlCell` accesses are permitted. Integration tests
+/// run with `--test-threads=1`, so this is safe to call from every test.
+fn regs() -> GpioRegisters {
+ bql::start_test();
+ GpioRegisters::new()
+}
+
+// 4-bit MODE[1:0]+CNF[1:0] pin configurations (RM0008).
+const CFG_INPUT_FLOATING: u32 = 0x4; // MODE=00 CNF=01 (reset default)
+const CFG_INPUT_PULL: u32 = 0x8; // MODE=00 CNF=10, ODR selects up/down
+const CFG_OUTPUT_PP: u32 = 0x1; // MODE=01 CNF=00, general push-pull
+
+/// Set the 4-bit config nibble for `pin` in CRL/CRH via an MMIO write.
+fn set_config(r: &GpioRegisters, pin: usize, cfg: u32) {
+ let reg = if pin < 8 { REG_CRL } else { REG_CRH };
+ let shift = ((pin % 8) * 4) as u32;
+ let old = r.read(reg);
+ r.write(reg, (old & !(0xF << shift)) | (cfg << shift));
+}
+
+fn set_odr_bit(r: &GpioRegisters, pin: usize, value: u32) {
+ let old = r.read(REG_ODR);
+ r.write(REG_ODR, (old & !(1 << pin)) | (value << pin));
+}
+
+/// True if pin `pin` is set in a [`PinUpdate`] and its reported level is
+/// `level`.
+fn pin_changed_to(upd: PinUpdate, pin: usize, level: bool) -> bool {
+ (upd.mask & (1 << pin)) != 0 && (((upd.levels >> pin) & 1) != 0) == level
+}
+
+#[test]
+/// A freshly created register block holds the RM0008 reset values.
+fn test_reset_values() {
+ let r = regs();
+ assert_eq!(r.read(REG_CRL), RESET_CRL);
+ assert_eq!(r.read(REG_CRH), RESET_CRH);
+ assert_eq!(r.read(REG_IDR), 0);
+ assert_eq!(r.read(REG_ODR), 0);
+}
+
+#[test]
+/// `reset` restores the reset state after the registers are dirtied.
+fn test_reset_restores() {
+ let r = regs();
+ r.write(REG_CRL, 0xDEAD_BEEF);
+ r.write(REG_ODR, 0x0000_FFFF);
+ assert_ne!(r.read(REG_CRL), RESET_CRL);
+
+ r.reset();
+
+ assert_eq!(r.read(REG_CRL), RESET_CRL);
+ assert_eq!(r.read(REG_CRH), RESET_CRH);
+ assert_eq!(r.read(REG_IDR), 0);
+ assert_eq!(r.read(REG_ODR), 0);
+}
+
+#[test]
+/// Push-pull output: IDR follows ODR; ODR written while input is latched,
+/// and the out-pin change is reported.
+fn test_output_mode() {
+ let r = regs();
+ let pin = 5;
+
+ // Set ODR while still a floating input: IDR unaffected, no out change.
+ let idle = set_odr_and_report(&r, pin, 1);
+ assert_eq!(r.read(REG_IDR), 0);
+ assert_eq!(idle.mask & (1 << pin), 0);
+
+ // Switch to push-pull output: pin now follows ODR (high), and flips.
+ let old = r.read(REG_CRL);
+ let to_output = r.write(
+ REG_CRL,
+ (old & !(0xF << (pin * 4))) | (CFG_OUTPUT_PP << (pin * 4)),
+ );
+ assert_eq!(r.read(REG_IDR), 1 << pin);
+ assert!(pin_changed_to(to_output, pin, true));
+
+ // Clear ODR bit: pin goes low, and flips back.
+ let to_low = set_odr_and_report(&r, pin, 0);
+ assert_eq!(r.read(REG_IDR), 0);
+ assert!(pin_changed_to(to_low, pin, false));
+}
+
+/// Helper: write one ODR bit and return the resulting [`PinUpdate`].
+fn set_odr_and_report(r: &GpioRegisters, pin: usize, value: u32) -> PinUpdate {
+ let old = r.read(REG_ODR);
+ r.write(REG_ODR, (old & !(1 << pin)) | (value << pin))
+}
+
+#[test]
+/// Floating input: an external drive is reflected in IDR and reported.
+fn test_input_mode() {
+ let r = regs();
+ let pin = 6usize;
+
+ set_config(&r, pin, CFG_INPUT_FLOATING);
+
+ let drive_high = r.gpio_set(pin, 1);
+ assert_eq!(r.read(REG_IDR), 1 << pin);
+ assert!(pin_changed_to(drive_high, pin, true));
+
+ let drive_low = r.gpio_set(pin, 0);
+ assert_eq!(r.read(REG_IDR), 0);
+ assert!(pin_changed_to(drive_low, pin, false));
+}
+
+#[test]
+/// Input pull mode: ODR selects pull-up (IDR=1) vs pull-down (IDR=0),
+/// with no external driver connected.
+fn test_pull_up_down() {
+ let r = regs();
+ let pin = 0;
+
+ set_odr_bit(&r, pin, 1);
+ set_config(&r, pin, CFG_INPUT_PULL);
+ assert_eq!(r.read(REG_IDR), 1 << pin);
+
+ set_odr_bit(&r, pin, 0);
+ assert_eq!(r.read(REG_IDR), 0);
+}
+
+#[test]
+/// BSRR sets/resets ODR bits (set has priority); BRR resets ODR bits.
+fn test_bsrr_brr() {
+ let r = regs();
+ let pin = 1;
+
+ r.write(REG_BSRR, 1 << pin);
+ assert_eq!(r.read(REG_ODR), 1 << pin);
+
+ r.write(REG_BSRR, 1 << (pin + 16));
+ assert_eq!(r.read(REG_ODR), 0);
+
+ r.write(REG_BSRR, 1 << pin);
+ r.write(REG_BRR, 1 << pin);
+ assert_eq!(r.read(REG_ODR), 0);
+
+ // Set has priority over reset within one BSRR write.
+ r.write(REG_BSRR, (1 << pin) | (1 << (pin + 16)));
+ assert_eq!(r.read(REG_ODR), 1 << pin);
+}
+
+#[test]
+/// A pin driven externally, then reconfigured as push-pull output, is
+/// disconnected: IDR follows ODR, not the stale external level.
+fn test_push_pull_disconnect() {
+ let r = regs();
+ let pin = 7usize;
+
+ // Drive high externally as input.
+ set_config(&r, pin, CFG_INPUT_FLOATING);
+ r.gpio_set(pin, 1);
+ assert_eq!(r.read(REG_IDR), 1 << pin);
+ assert_eq!(r.disconnected_pins() & (1 << pin), 0); // now connected
+
+ // Reconfigure as push-pull output with ODR=0: pin must drop to 0,
+ // and the external driver must be disconnected.
+ set_odr_bit(&r, pin, 0);
+ set_config(&r, pin, CFG_OUTPUT_PP);
+ assert_eq!(r.read(REG_IDR), 0);
+ assert_ne!(r.disconnected_pins() & (1 << pin), 0); // disconnected again
+
+ // Further external drives are ignored while output push-pull.
+ r.gpio_set(pin, 1);
+ assert_eq!(r.read(REG_IDR), 0);
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-07-26 14:53 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-26 14:51 [RFC PATCH v1 0/5] hw/gpio: STM32F1xx GPIO controller in Rust Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 1/5] hw/gpio: add stm32f1xx GPIO device model (Rust) Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 2/5] rust: add hw/gpio crate to the workspace Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 3/5] hw/arm/stm32f103: instantiate and map GPIO ports Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 4/5] tests/qtest: add stm32f103 GPIO test Jack Wang
2026-07-26 14:51 ` [RFC PATCH v1 5/5] rust/hw/gpio: add native unit tests Jack Wang
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.