All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/3] hw/misc: QUP UART device
@ 2026-10-27 18:38 Brian Cain
  2026-10-27 18:38 ` [PATCH 1/3] hw/misc: add the QUP wrapper device Brian Cain
                   ` (3 more replies)
  0 siblings, 4 replies; 11+ messages in thread
From: Brian Cain @ 2026-10-27 18:38 UTC (permalink / raw)
  To: qemu-devel
  Cc: Manos Pitsidianakis, qemu-rust, Brian Cain, Paolo Bonzini,
	Pierrick Bouvier

Add the QUP + GENI uart device models.  The QUP is a "Universal Peripheral"
present on Qualcomm SoCs, with connections for SPI, UART, I2C, I3C.  For
now we're only interested in UART.

Will follow up with a v2 including wiring into machines + qtest after
https://lore.kernel.org/qemu-devel/20261027143721.4125584-1-brian.cain@oss.qualcomm.com/
lands.

Brian Cain (3):
  hw/misc: add the QUP wrapper device
  rust/chardev: fix write errors and add write watches
  rust/hw/char: add QUP GENI UART device model

 MAINTAINERS                                 |    3 +
 docs/system/device-emulation.rst            |    1 +
 docs/system/devices/qup.rst                 |   10 +
 include/hw/misc/qup-wrapper.h               |   27 +
 hw/misc/qup-wrapper.c                       |   90 ++
 Cargo.lock                                  |   15 +
 Cargo.toml                                  |    1 +
 hw/misc/Kconfig                             |    3 +
 hw/misc/meson.build                         |    3 +
 rust/chardev/src/chardev.rs                 |   82 +-
 rust/hw/char/Kconfig                        |    4 +
 rust/hw/char/meson.build                    |    1 +
 rust/hw/char/qup_geni_uart/Cargo.toml       |   27 +
 rust/hw/char/qup_geni_uart/meson.build      |   28 +
 rust/hw/char/qup_geni_uart/src/device.rs    | 1293 +++++++++++++++++++
 rust/hw/char/qup_geni_uart/src/lib.rs       |    9 +
 rust/hw/char/qup_geni_uart/src/registers.rs |  160 +++
 17 files changed, 1741 insertions(+), 16 deletions(-)
 create mode 100644 docs/system/devices/qup.rst
 create mode 100644 include/hw/misc/qup-wrapper.h
 create mode 100644 hw/misc/qup-wrapper.c
 create mode 100644 rust/hw/char/qup_geni_uart/Cargo.toml
 create mode 100644 rust/hw/char/qup_geni_uart/meson.build
 create mode 100644 rust/hw/char/qup_geni_uart/src/device.rs
 create mode 100644 rust/hw/char/qup_geni_uart/src/lib.rs
 create mode 100644 rust/hw/char/qup_geni_uart/src/registers.rs

-- 
2.34.1


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 1/3] hw/misc: add the QUP wrapper device
  2026-10-27 18:38 [PATCH 0/3] hw/misc: QUP UART device Brian Cain
@ 2026-10-27 18:38 ` Brian Cain
  2026-10-27 18:38 ` [PATCH 2/3] rust/chardev: fix write errors and add write watches Brian Cain
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 11+ messages in thread
From: Brian Cain @ 2026-10-27 18:38 UTC (permalink / raw)
  To: qemu-devel
  Cc: Manos Pitsidianakis, qemu-rust, Brian Cain, Paolo Bonzini,
	Pierrick Bouvier

The Linux qcom_geni_serial driver expects a GENI UART to be a child of
a QUP wrapper.

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
---
 MAINTAINERS                   |  2 +
 include/hw/misc/qup-wrapper.h | 27 +++++++++++
 hw/misc/qup-wrapper.c         | 90 +++++++++++++++++++++++++++++++++++
 hw/misc/Kconfig               |  3 ++
 hw/misc/meson.build           |  3 ++
 5 files changed, 125 insertions(+)
 create mode 100644 include/hw/misc/qup-wrapper.h
 create mode 100644 hw/misc/qup-wrapper.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 4ca1751e30f..064d9531d3e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1383,7 +1383,9 @@ M: Brian Cain <brian.cain@oss.qualcomm.com>
 R: Pierrick Bouvier <pierrick.bouvier@oss.qualcomm.com>
 S: Supported
 F: hw/hexagon/
+F: hw/misc/qup-wrapper.c
 F: include/hw/hexagon/
+F: include/hw/misc/qup-wrapper.h
 F: configs/devices/hexagon-softmmu/default.mak
 F: docs/system/hexagon/
 F: tests/functional/hexagon/
diff --git a/include/hw/misc/qup-wrapper.h b/include/hw/misc/qup-wrapper.h
new file mode 100644
index 00000000000..6180092b799
--- /dev/null
+++ b/include/hw/misc/qup-wrapper.h
@@ -0,0 +1,27 @@
+/*
+ * QUP wrapper (GENI SE QUP) device
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_MISC_QUP_WRAPPER_H
+#define HW_MISC_QUP_WRAPPER_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+
+#define TYPE_QUP_WRAPPER "qup-wrapper"
+OBJECT_DECLARE_SIMPLE_TYPE(QupWrapperState, QUP_WRAPPER)
+
+#define QUP_WRAPPER_MMIO_SIZE 0x6000
+
+struct QupWrapperState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion iomem;
+
+    uint32_t hw_version;
+};
+
+#endif /* HW_MISC_QUP_WRAPPER_H */
diff --git a/hw/misc/qup-wrapper.c b/hw/misc/qup-wrapper.c
new file mode 100644
index 00000000000..40c79dc66e6
--- /dev/null
+++ b/hw/misc/qup-wrapper.c
@@ -0,0 +1,90 @@
+/*
+ * QUP wrapper device
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/misc/qup-wrapper.h"
+#include "qemu/log.h"
+
+#define REG_QUPV3_HW_VER 0x4
+
+static uint64_t qup_wrapper_read(void *opaque, hwaddr offset, unsigned size)
+{
+    QupWrapperState *s = QUP_WRAPPER(opaque);
+    uint64_t value = 0;
+
+    switch (offset) {
+    case REG_QUPV3_HW_VER:
+        value = s->hw_version;
+        break;
+    default:
+        qemu_log_mask(LOG_UNIMP, "qup-wrapper: read from unknown offset 0x%"
+                      HWADDR_PRIx "\n", offset);
+        break;
+    }
+
+    return value;
+}
+
+static void qup_wrapper_write(void *opaque, hwaddr offset, uint64_t value,
+                              unsigned size)
+{
+    qemu_log_mask(LOG_UNIMP, "qup-wrapper: write to offset 0x%"
+                  HWADDR_PRIx " value 0x%" PRIx64 "\n", offset, value);
+}
+
+static const MemoryRegionOps qup_wrapper_ops = {
+    .read = qup_wrapper_read,
+    .write = qup_wrapper_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+    },
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+};
+
+static void qup_wrapper_init(Object *obj)
+{
+    QupWrapperState *s = QUP_WRAPPER(obj);
+
+    memory_region_init_io(&s->iomem, obj, &qup_wrapper_ops, s,
+                          TYPE_QUP_WRAPPER, QUP_WRAPPER_MMIO_SIZE);
+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->iomem);
+}
+
+static const Property qup_wrapper_properties[] = {
+    DEFINE_PROP_UINT32("hw-version", QupWrapperState, hw_version,
+                       0x30100000),
+};
+
+static void qup_wrapper_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    device_class_set_props(dc, qup_wrapper_properties);
+    dc->desc = "Qualcomm QUP wrapper";
+}
+
+static const TypeInfo qup_wrapper_info = {
+    .name          = TYPE_QUP_WRAPPER,
+    .parent        = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(QupWrapperState),
+    .instance_init = qup_wrapper_init,
+    .class_init    = qup_wrapper_class_init,
+};
+
+static void qup_wrapper_register_types(void)
+{
+    type_register_static(&qup_wrapper_info);
+}
+
+type_init(qup_wrapper_register_types)
diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index 7abd22869a4..aa76a2398a9 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -266,4 +266,7 @@ config K230_DECOMP_GZIP
 config SOPHGO_CV1800B_CLK
     bool
 
+config QUP_WRAPPER
+    bool
+
 source macio/Kconfig
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 90f3f89de70..9cf897abc76 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -181,3 +181,6 @@ system_ss.add(when: 'CONFIG_SBSA_REF', if_true: files('sbsa_ec.c'))
 system_ss.add(when: 'CONFIG_LASI', if_true: files('lasi.c'))
 
 system_ss.add(when: 'CONFIG_AXIADO_CLK', if_true: files('axiado_clk.c'))
+
+# Qualcomm QUP devices
+system_ss.add(when: 'CONFIG_QUP_WRAPPER', if_true: files('qup-wrapper.c'))
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 2/3] rust/chardev: fix write errors and add write watches
  2026-10-27 18:38 [PATCH 0/3] hw/misc: QUP UART device Brian Cain
  2026-10-27 18:38 ` [PATCH 1/3] hw/misc: add the QUP wrapper device Brian Cain
@ 2026-10-27 18:38 ` Brian Cain
  2026-10-28  7:03   ` Philippe Mathieu-Daudé
  2026-10-27 18:38 ` [PATCH 3/3] rust/hw/char: add QUP GENI UART device model Brian Cain
  2026-10-27 18:43 ` [PATCH 0/3] hw/misc: QUP UART device Pierrick Bouvier
  3 siblings, 1 reply; 11+ messages in thread
From: Brian Cain @ 2026-10-27 18:38 UTC (permalink / raw)
  To: qemu-devel
  Cc: Manos Pitsidianakis, qemu-rust, Brian Cain, Paolo Bonzini,
	Pierrick Bouvier

qemu_chr_fe_write() reports failure as -1 with the cause in errno, not
as a negated errno, so errno::into_io_result() turned every failure into
EPERM and a frontend could never see EAGAIN.  Build the error from
errno instead, in both CharFrontend::write() and the BQL-free variant.

Also add CharFrontend::add_write_watch() and remove_watch(), so that a
device whose backend would block can retry once it is writable again.
remove_watch() tolerates a source that the backend has already torn
down.

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
---
 rust/chardev/src/chardev.rs | 82 +++++++++++++++++++++++++++++--------
 1 file changed, 66 insertions(+), 16 deletions(-)

diff --git a/rust/chardev/src/chardev.rs b/rust/chardev/src/chardev.rs
index fb9674b3fb4..8a3836d78e4 100644
--- a/rust/chardev/src/chardev.rs
+++ b/rust/chardev/src/chardev.rs
@@ -54,7 +54,11 @@ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
 
         let len = buf.len().try_into().unwrap();
         let r = unsafe { bindings::qemu_chr_fe_write(addr_of_mut!(*chr), buf.as_ptr(), len) };
-        errno::into_io_result(r).map(|cnt| cnt as usize)
+        if r < 0 {
+            Err(io::Error::last_os_error())
+        } else {
+            Ok(r as usize)
+        }
     }
 
     fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
@@ -62,13 +66,13 @@ fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
 
         let len = buf.len().try_into().unwrap();
         let r = unsafe { bindings::qemu_chr_fe_write_all(addr_of_mut!(*chr), buf.as_ptr(), len) };
-        errno::into_io_result(r).and_then(|cnt| {
-            if cnt as usize == buf.len() {
-                Ok(())
-            } else {
-                Err(ErrorKind::WriteZero.into())
-            }
-        })
+        if r < 0 {
+            Err(io::Error::last_os_error())
+        } else if r as usize == buf.len() {
+            Ok(())
+        } else {
+            Err(ErrorKind::WriteZero.into())
+        }
     }
 }
 
@@ -193,6 +197,48 @@ pub fn accept_input(&self) {
         unsafe { bindings::qemu_chr_fe_accept_input(addr_of_mut!(*chr)) }
     }
 
+    /// Watch for output becoming writable or disconnected.
+    ///
+    /// # Safety
+    /// The caller must keep `opaque` valid until the source is removed, and
+    /// must remove the source before the frontend is destroyed.
+    pub unsafe fn add_write_watch(
+        &self,
+        callback: bindings::FEWatchFunc,
+        opaque: *mut c_void,
+    ) -> u32 {
+        let mut chr = self.inner.borrow_mut();
+        unsafe {
+            bindings::qemu_chr_fe_add_watch(
+                addr_of_mut!(*chr),
+                glib_sys::G_IO_OUT | glib_sys::G_IO_HUP,
+                callback,
+                opaque,
+            )
+        }
+    }
+
+    /// Remove a watch previously returned by `add_write_watch`.
+    pub fn remove_watch(&self, tag: u32) {
+        if tag == 0 {
+            return;
+        }
+        let chr = self.inner.borrow();
+        if chr.chr.is_null() {
+            return;
+        }
+        // SAFETY: the backend and its context remain valid while borrowed.
+        // Looking up the source handles watches already removed by the backend
+        // and also works when NULL selects the default context.
+        unsafe {
+            let context = (*chr.chr).gcontext;
+            let source = glib_sys::g_main_context_find_source_by_id(context, tag);
+            if !source.is_null() {
+                glib_sys::g_source_destroy(source);
+            }
+        }
+    }
+
     /// Temporarily borrow the character device, allowing it to be used
     /// as an implementor of `Write`.  Note that it is not valid to drop
     /// the big QEMU lock while the character device is borrowed, as
@@ -229,7 +275,11 @@ pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
         let len = buf.len().try_into().unwrap();
         // SAFETY: qemu_chr_fe_write is thread-safe
         let r = unsafe { bindings::qemu_chr_fe_write(self.inner.as_ptr(), buf.as_ptr(), len) };
-        errno::into_io_result(r).map(|cnt| cnt as usize)
+        if r < 0 {
+            Err(io::Error::last_os_error())
+        } else {
+            Ok(r as usize)
+        }
     }
 
     /// Write data to a character backend from the front end.  This function
@@ -243,13 +293,13 @@ pub fn write_all(&self, buf: &[u8]) -> io::Result<()> {
         let len = buf.len().try_into().unwrap();
         // SAFETY: qemu_chr_fe_write_all is thread-safe
         let r = unsafe { bindings::qemu_chr_fe_write_all(self.inner.as_ptr(), buf.as_ptr(), len) };
-        errno::into_io_result(r).and_then(|cnt| {
-            if cnt as usize == buf.len() {
-                Ok(())
-            } else {
-                Err(ErrorKind::WriteZero.into())
-            }
-        })
+        if r < 0 {
+            Err(io::Error::last_os_error())
+        } else if r as usize == buf.len() {
+            Ok(())
+        } else {
+            Err(ErrorKind::WriteZero.into())
+        }
     }
 }
 
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 3/3] rust/hw/char: add QUP GENI UART device model
  2026-10-27 18:38 [PATCH 0/3] hw/misc: QUP UART device Brian Cain
  2026-10-27 18:38 ` [PATCH 1/3] hw/misc: add the QUP wrapper device Brian Cain
  2026-10-27 18:38 ` [PATCH 2/3] rust/chardev: fix write errors and add write watches Brian Cain
@ 2026-10-27 18:38 ` Brian Cain
  2026-10-27 19:05   ` Philippe Mathieu-Daudé
  2026-10-27 18:43 ` [PATCH 0/3] hw/misc: QUP UART device Pierrick Bouvier
  3 siblings, 1 reply; 11+ messages in thread
From: Brian Cain @ 2026-10-27 18:38 UTC (permalink / raw)
  To: qemu-devel
  Cc: Manos Pitsidianakis, qemu-rust, Brian Cain, Paolo Bonzini,
	Pierrick Bouvier

Add a model of the QUP GENI serial engine in UART mode.

Supports FIFO, DMA, loopback modes - DMA via memory link.

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
---
 MAINTAINERS                                 |    1 +
 docs/system/device-emulation.rst            |    1 +
 docs/system/devices/qup.rst                 |   10 +
 Cargo.lock                                  |   15 +
 Cargo.toml                                  |    1 +
 rust/hw/char/Kconfig                        |    4 +
 rust/hw/char/meson.build                    |    1 +
 rust/hw/char/qup_geni_uart/Cargo.toml       |   27 +
 rust/hw/char/qup_geni_uart/meson.build      |   28 +
 rust/hw/char/qup_geni_uart/src/device.rs    | 1293 +++++++++++++++++++
 rust/hw/char/qup_geni_uart/src/lib.rs       |    9 +
 rust/hw/char/qup_geni_uart/src/registers.rs |  160 +++
 12 files changed, 1550 insertions(+)
 create mode 100644 docs/system/devices/qup.rst
 create mode 100644 rust/hw/char/qup_geni_uart/Cargo.toml
 create mode 100644 rust/hw/char/qup_geni_uart/meson.build
 create mode 100644 rust/hw/char/qup_geni_uart/src/device.rs
 create mode 100644 rust/hw/char/qup_geni_uart/src/lib.rs
 create mode 100644 rust/hw/char/qup_geni_uart/src/registers.rs

diff --git a/MAINTAINERS b/MAINTAINERS
index 064d9531d3e..24f7b127c77 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1386,6 +1386,7 @@ F: hw/hexagon/
 F: hw/misc/qup-wrapper.c
 F: include/hw/hexagon/
 F: include/hw/misc/qup-wrapper.h
+F: rust/hw/char/qup_geni_uart/
 F: configs/devices/hexagon-softmmu/default.mak
 F: docs/system/hexagon/
 F: tests/functional/hexagon/
diff --git a/docs/system/device-emulation.rst b/docs/system/device-emulation.rst
index a7d247a270e..e26e6aef3bc 100644
--- a/docs/system/device-emulation.rst
+++ b/docs/system/device-emulation.rst
@@ -95,6 +95,7 @@ Emulated Devices
    devices/keyboard.rst
    devices/net.rst
    devices/nvme.rst
+   devices/qup.rst
    devices/scsi/index.rst
    devices/ufs.rst
    devices/usb-u2f.rst
diff --git a/docs/system/devices/qup.rst b/docs/system/devices/qup.rst
new file mode 100644
index 00000000000..2e12be233a3
--- /dev/null
+++ b/docs/system/devices/qup.rst
@@ -0,0 +1,10 @@
+.. Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+Qualcomm Universal Peripheral
+=============================
+
+QEMU provides models for the QUP wrapper and its GENI UART serial engine.
+See the `Qualcomm QUP documentation
+<https://docs.qualcomm.com/doc/80-70014-8/>`_ for the hardware programming
+model.
diff --git a/Cargo.lock b/Cargo.lock
index 0dd16cdb68a..a9fa55539d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -280,6 +280,21 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "qup_geni_uart"
+version = "0.1.0"
+dependencies = [
+ "bql",
+ "chardev",
+ "common",
+ "hwcore",
+ "migration",
+ "qemu_macros",
+ "qom",
+ "system",
+ "util",
+]
+
 [[package]]
 name = "rustc_version"
 version = "0.4.1"
diff --git a/Cargo.toml b/Cargo.toml
index b6b6d97dfec..a91a21c539c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,7 @@
 resolver = "2"
 members = [
     "rust/hw/char/pl011",
+    "rust/hw/char/qup_geni_uart",
     "rust/hw/timer/hpet",
     "rust/tests",
 ]
diff --git a/rust/hw/char/Kconfig b/rust/hw/char/Kconfig
index 5fe800c4806..488ec0a40c9 100644
--- a/rust/hw/char/Kconfig
+++ b/rust/hw/char/Kconfig
@@ -1,2 +1,6 @@
 config X_PL011_RUST
     bool
+
+config X_QUP_GENI_UART_RUST
+    bool
+    select QUP_WRAPPER
diff --git a/rust/hw/char/meson.build b/rust/hw/char/meson.build
index 5716dc43ef6..a220a74a37b 100644
--- a/rust/hw/char/meson.build
+++ b/rust/hw/char/meson.build
@@ -1 +1,2 @@
 subdir('pl011')
+subdir('qup_geni_uart')
diff --git a/rust/hw/char/qup_geni_uart/Cargo.toml b/rust/hw/char/qup_geni_uart/Cargo.toml
new file mode 100644
index 00000000000..1285a18d5b6
--- /dev/null
+++ b/rust/hw/char/qup_geni_uart/Cargo.toml
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+[package]
+name = "qup_geni_uart"
+version = "0.1.0"
+authors = ["Brian Cain <brian.cain@oss.qualcomm.com>"]
+description = "Qualcomm QUP GENI UART device model for QEMU"
+publish = false
+
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+common = { path = "../../../common" }
+util = { path = "../../../util" }
+bql = { path = "../../../bql" }
+migration = { path = "../../../migration" }
+qom = { path = "../../../qom" }
+chardev = { path = "../../../chardev" }
+system = { path = "../../../system" }
+hwcore = { path = "../../../hw/core" }
+qemu_macros = { path = "../../../qemu-macros" }
+
+[lints]
+workspace = true
diff --git a/rust/hw/char/qup_geni_uart/meson.build b/rust/hw/char/qup_geni_uart/meson.build
new file mode 100644
index 00000000000..e2530e6b4a9
--- /dev/null
+++ b/rust/hw/char/qup_geni_uart/meson.build
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+_libqup_geni_uart_rs = static_library(
+  'qup_geni_uart',
+  files('src/lib.rs'),
+  override_options: ['rust_std=2021', 'build.rust_std=2021'],
+  rust_abi: 'rust',
+  link_with: [
+    _util_rs,
+    _migration_rs,
+    _bql_rs,
+    _qom_rs,
+    _chardev_rs,
+    _system_rs,
+    _hwcore_rs,
+  ],
+  dependencies: [
+    common_rs,
+    qemu_macros,
+  ],
+)
+
+rust_devices_ss.add(when: 'CONFIG_X_QUP_GENI_UART_RUST', if_true: [declare_dependency(
+  link_whole: [_libqup_geni_uart_rs],
+  # Putting proc macro crates in `dependencies` is necessary for Meson to find
+  # them when compiling the root per-target static rust lib.
+  dependencies: [qemu_macros],
+  variables: {'crate': 'qup_geni_uart'},
+)])
diff --git a/rust/hw/char/qup_geni_uart/src/device.rs b/rust/hw/char/qup_geni_uart/src/device.rs
new file mode 100644
index 00000000000..759ce8cf595
--- /dev/null
+++ b/rust/hw/char/qup_geni_uart/src/device.rs
@@ -0,0 +1,1293 @@
+// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+use std::{
+    ffi::{c_void, CStr},
+    io::ErrorKind,
+    ptr,
+};
+
+use bql::prelude::*;
+use chardev::prelude::*;
+use common::{prelude::*, Opaque};
+use hwcore::{bindings::qdev_prop_allow_set_link_before_realize, prelude::*};
+use migration::{self, prelude::*};
+use qom::{
+    bindings::{object_property_add_link, object_ref, object_unref, OBJ_PROP_LINK_STRONG},
+    prelude::*,
+};
+use system::{
+    bindings::{
+        address_space_destroy, address_space_init, address_space_rw, get_system_memory,
+        AddressSpace,
+    },
+    prelude::*,
+    MEMTXATTRS_UNSPECIFIED,
+};
+use util::{self, log::Log, log_mask_ln};
+
+use crate::registers::{self, RegisterOffset, UART_FIFO_DEPTH_WORDS};
+
+pub const QUP_FIFO_DEPTH: u32 = UART_FIFO_DEPTH_WORDS;
+
+const QUP_GENI_UART_MMIO_SIZE: u64 = 0x6000;
+
+const DMA_CHUNK_SIZE: usize = 4096;
+
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct Fifo([u32; QUP_FIFO_DEPTH as usize]);
+
+impl Default for Fifo {
+    fn default() -> Self {
+        Self([0u32; QUP_FIFO_DEPTH as usize])
+    }
+}
+impl_vmstate_forward!(Fifo);
+
+impl std::ops::IndexMut<u32> for Fifo {
+    fn index_mut(&mut self, idx: u32) -> &mut Self::Output {
+        &mut self.0[idx as usize]
+    }
+}
+
+impl std::ops::Index<u32> for Fifo {
+    type Output = u32;
+
+    fn index(&self, idx: u32) -> &Self::Output {
+        &self.0[idx as usize]
+    }
+}
+
+#[repr(C)]
+#[derive(Debug, Default)]
+pub struct QupGeniUartRegs {
+    // Common GENI registers
+    pub geni_status: u32,
+    pub geni_m_cmd0: u32,
+    pub geni_m_irq_status: u32,
+    pub geni_m_irq_en: u32,
+    pub geni_s_cmd0: u32,
+    pub geni_s_irq_status: u32,
+    pub geni_s_irq_en: u32,
+    pub geni_tx_watermark: u32,
+    pub geni_rx_watermark: u32,
+    pub geni_rx_rfr_watermark: u32,
+    pub geni_m_gp_length: u32,
+    pub geni_s_gp_length: u32,
+    pub geni_tx_packing_cfg0: u32,
+    pub geni_tx_packing_cfg1: u32,
+    pub geni_rx_packing_cfg0: u32,
+    pub geni_rx_packing_cfg1: u32,
+
+    // UART specific registers
+    pub uart_tx_trans_cfg: u32,
+    pub uart_rx_trans_cfg: u32,
+    pub uart_tx_word_len: u32,
+    pub uart_rx_word_len: u32,
+    pub uart_tx_stop_bit_len: u32,
+    pub uart_rx_stale_cnt: u32,
+    pub uart_tx_parity_cfg: u32,
+    pub uart_rx_parity_cfg: u32,
+    pub uart_loopback_cfg: u32,
+    pub uart_io_macro_ctrl: u32,
+    pub uart_manual_rfr: u32,
+    pub uart_tx_trans_len: u32,
+
+    // TX FIFO writes go directly to the backend, so only RX needs storage.
+    pub rx_fifo: Fifo,
+    pub rx_fifo_len: u32,
+    // Valid bytes in the final RX FIFO word; 0 means the word is full.
+    pub rx_last_byte_cnt: u32,
+    // Set once the guest observes the size of a short final word.
+    pub rx_tail_closed: bool,
+    pub tx_remaining: u32,
+
+    // DMA registers
+    pub dma_tx_ptr_l: u32,
+    pub dma_tx_ptr_h: u32,
+    pub dma_tx_attr: u32,
+    pub dma_tx_len: u32,
+    pub dma_tx_sent: u32,
+    pub dma_tx_irq_en: u32,
+    pub dma_tx_irq_stat: u32,
+    pub dma_rx_ptr_l: u32,
+    pub dma_rx_ptr_h: u32,
+    pub dma_rx_attr: u32,
+    pub dma_rx_len: u32,
+    pub dma_rx_len_in: u32,
+    pub dma_rx_irq_en: u32,
+    pub dma_rx_irq_stat: u32,
+
+    // Runtime state
+    pub tx_enabled: bool,
+    pub rx_enabled: bool,
+    pub dma_mode_enabled: bool,
+    // A TX DMA may be configured before its TX command is started.
+    pub dma_tx_armed: bool,
+    pub dma_tx_active: bool,
+    pub dma_rx_active: bool,
+}
+
+/// Target of the device's "memory" link property.
+#[derive(Clone, Copy)]
+#[repr(transparent)]
+pub struct DmaRegion(*mut system::bindings::MemoryRegion);
+
+// SAFETY: the pointer is only used under the BQL
+unsafe impl Send for DmaRegion {}
+
+struct DmaAddressSpace(Opaque<AddressSpace>);
+
+// SAFETY: address_space_rw() may be called from any thread
+unsafe impl Sync for DmaAddressSpace {}
+
+#[repr(C)]
+#[derive(qemu_macros::Object, hwcore::Device)]
+pub struct QupGeniUartState {
+    pub parent_obj: ParentField<SysBusDevice>,
+    pub iomem: MemoryRegion,
+    #[property(rename = "chardev")]
+    pub char_frontend: CharFrontend,
+    pub regs: BqlRefCell<QupGeniUartRegs>,
+    pub irq: InterruptSource,
+    pub tx_watch: BqlCell<u32>,
+    pub dma_mr: BqlCell<DmaRegion>,
+    dma_as: DmaAddressSpace,
+    dma_as_ready: BqlCell<bool>,
+    // Prevent DMA to this device's MMIO from recursively starting TX DMA.
+    dma_tx_busy: BqlCell<bool>,
+}
+
+qom_isa!(QupGeniUartState : SysBusDevice, DeviceState, Object);
+
+unsafe impl ObjectType for QupGeniUartState {
+    type Class = <SysBusDevice as ObjectType>::Class;
+    const TYPE_NAME: &'static CStr = crate::TYPE_QUP_GENI_UART;
+}
+
+impl ObjectImpl for QupGeniUartState {
+    type ParentType = SysBusDevice;
+
+    const INSTANCE_INIT: Option<unsafe fn(ParentInit<Self>)> = Some(Self::init);
+    const INSTANCE_POST_INIT: Option<fn(&Self)> = Some(Self::post_init);
+    const CLASS_INIT: fn(&mut Self::Class) = Self::Class::class_init::<Self>;
+}
+
+impl DeviceImpl for QupGeniUartState {
+    const VMSTATE: Option<VMStateDescription<Self>> = Some(VMSTATE_QUP_GENI_UART);
+    const REALIZE: Option<fn(&Self) -> util::Result<()>> = Some(Self::realize);
+}
+
+impl ResettablePhasesImpl for QupGeniUartState {
+    const HOLD: Option<fn(&Self, ResetType)> = Some(Self::reset_hold);
+}
+
+impl SysBusDeviceImpl for QupGeniUartState {}
+
+impl QupGeniUartRegs {
+    // The model has no TX FIFO, so any nonzero watermark is always reached.
+    fn tx_watermark_latch(&mut self) {
+        if self.geni_tx_watermark > 0 {
+            self.geni_m_irq_status |= registers::M_TX_FIFO_WATERMARK;
+        } else {
+            self.geni_m_irq_status &= !registers::M_TX_FIFO_WATERMARK;
+        }
+    }
+
+    pub fn read(&mut self, offset: RegisterOffset) -> (bool, u32) {
+        use RegisterOffset::*;
+
+        let mut update_irq = false;
+        let result = match offset {
+            SE_GENI_STATUS => self.geni_status,
+            GENI_FW_REVISION_RO => {
+                (registers::GENI_SE_UART << registers::FW_REV_PROTOCOL_SHFT) | 0x0001
+            }
+            SE_GENI_M_IRQ_STATUS => self.geni_m_irq_status,
+            SE_GENI_M_IRQ_EN => self.geni_m_irq_en,
+            SE_GENI_M_CMD0 => self.geni_m_cmd0,
+            SE_GENI_S_CMD0 => self.geni_s_cmd0,
+            SE_GENI_TX_WATERMARK_REG => self.geni_tx_watermark,
+            SE_GENI_RX_WATERMARK_REG => self.geni_rx_watermark,
+            SE_GENI_RX_RFR_WATERMARK_REG => self.geni_rx_rfr_watermark,
+            SE_UART_TX_WORD_LEN => self.uart_tx_word_len,
+            SE_UART_RX_WORD_LEN => self.uart_rx_word_len,
+            SE_UART_TX_STOP_BIT_LEN => self.uart_tx_stop_bit_len,
+            SE_UART_RX_STALE_CNT => self.uart_rx_stale_cnt,
+            SE_UART_TX_TRANS_CFG => self.uart_tx_trans_cfg,
+            SE_UART_RX_TRANS_CFG => self.uart_rx_trans_cfg,
+            SE_UART_LOOPBACK_CFG => self.uart_loopback_cfg,
+            SE_UART_IO_MACRO_CTRL => self.uart_io_macro_ctrl,
+            SE_UART_MANUAL_RFR => self.uart_manual_rfr,
+            SE_GENI_TX_PACKING_CFG0 => self.geni_tx_packing_cfg0,
+            SE_GENI_TX_PACKING_CFG1 => self.geni_tx_packing_cfg1,
+            SE_GENI_RX_PACKING_CFG0 => self.geni_rx_packing_cfg0,
+            SE_GENI_RX_PACKING_CFG1 => self.geni_rx_packing_cfg1,
+            SE_GENI_DMA_MODE_EN => self.dma_mode_enabled as u32,
+            SE_GENI_TX_FIFO_STATUS => 0,
+            SE_GENI_RX_FIFO_STATUS => {
+                let mut status = self.rx_fifo_len & registers::RX_FIFO_WC_MSK;
+                // Without RX_LAST, the guest treats a short word as four bytes.
+                if self.rx_fifo_len > 0 && self.rx_last_byte_cnt > 0 {
+                    self.rx_tail_closed = true;
+                    status |= registers::RX_LAST
+                        | (self.rx_last_byte_cnt << registers::RX_LAST_BYTE_VALID_SHFT)
+                            & registers::RX_LAST_BYTE_VALID_MSK;
+                }
+                status
+            }
+            SE_GENI_TX_FIFO_N => {
+                log_mask_ln!(Log::GuestError, "QUP GENI UART: TX FIFO read not supported");
+                0
+            }
+            SE_GENI_RX_FIFO_N => {
+                if self.rx_fifo_len > 0 {
+                    let data = self.rx_fifo[0];
+                    for i in 0..self.rx_fifo_len - 1 {
+                        self.rx_fifo[i] = self.rx_fifo[i + 1];
+                    }
+                    self.rx_fifo_len -= 1;
+                    if self.rx_fifo_len < self.geni_rx_watermark {
+                        self.geni_s_irq_status &= !registers::S_RX_FIFO_WATERMARK;
+                    }
+                    if self.rx_fifo_len == 0 {
+                        self.geni_s_irq_status &= !registers::S_RX_FIFO_LAST;
+                        self.rx_last_byte_cnt = 0;
+                        self.rx_tail_closed = false;
+                    }
+                    update_irq = true;
+                    data
+                } else {
+                    0
+                }
+            }
+            SE_GENI_S_IRQ_STATUS => self.geni_s_irq_status,
+            SE_GENI_S_IRQ_EN => self.geni_s_irq_en,
+            GENI_SER_M_CLK_CFG | GENI_SER_S_CLK_CFG | SE_GENI_CLK_SEL => 0,
+            GENI_OUTPUT_CTRL | GENI_FORCE_DEFAULT => 0,
+            GENI_IF_DISABLE_RO => 0,
+            SE_GENI_IOS => 0x3, // TX/RX lines idle high
+            SE_GENI_M_GP_LENGTH => self.geni_m_gp_length,
+            SE_GENI_S_GP_LENGTH => self.geni_s_gp_length,
+            SE_UART_TX_TRANS_LEN => self.uart_tx_trans_len,
+            SE_UART_TX_PARITY_CFG => self.uart_tx_parity_cfg,
+            SE_UART_RX_PARITY_CFG => self.uart_rx_parity_cfg,
+            SE_GSI_EVENT_EN | SE_IRQ_EN => 0,
+            SE_HW_PARAM0 => {
+                (registers::UART_FIFO_WIDTH_BITS << registers::TX_FIFO_WIDTH_SHFT)
+                    | (registers::UART_FIFO_DEPTH_WORDS << registers::TX_FIFO_DEPTH_SHFT)
+            }
+            SE_HW_PARAM1 => {
+                (registers::UART_FIFO_WIDTH_BITS << registers::RX_FIFO_WIDTH_SHFT)
+                    | (registers::UART_FIFO_DEPTH_WORDS << registers::RX_FIFO_DEPTH_SHFT)
+            }
+            SE_DMA_TX_PTR_L => self.dma_tx_ptr_l,
+            SE_DMA_TX_PTR_H => self.dma_tx_ptr_h,
+            SE_DMA_TX_ATTR => self.dma_tx_attr,
+            SE_DMA_TX_LEN => self.dma_tx_len,
+            SE_DMA_TX_IRQ_EN => self.dma_tx_irq_en,
+            SE_DMA_TX_IRQ_STAT => self.dma_tx_irq_stat,
+            SE_DMA_RX_PTR_L => self.dma_rx_ptr_l,
+            SE_DMA_RX_PTR_H => self.dma_rx_ptr_h,
+            SE_DMA_RX_ATTR => self.dma_rx_attr,
+            SE_DMA_RX_LEN => self.dma_rx_len,
+            SE_DMA_RX_LEN_IN => self.dma_rx_len_in,
+            SE_DMA_RX_IRQ_EN => self.dma_rx_irq_en,
+            SE_DMA_RX_IRQ_STAT => self.dma_rx_irq_stat,
+            _ => {
+                log_mask_ln!(
+                    Log::Unimp,
+                    "QUP GENI UART: Unimplemented read from offset {:x}",
+                    offset as u32
+                );
+                0
+            }
+        };
+        (update_irq, result)
+    }
+
+    pub fn write(
+        &mut self,
+        offset: RegisterOffset,
+        value: u32,
+        char_frontend: &CharFrontend,
+    ) -> bool {
+        use RegisterOffset::*;
+
+        match offset {
+            SE_GENI_M_IRQ_CLEAR => {
+                self.geni_m_irq_status &= !value;
+                return true;
+            }
+            SE_GENI_M_IRQ_EN => {
+                self.geni_m_irq_en = value;
+                return true;
+            }
+            SE_GENI_TX_WATERMARK_REG => {
+                self.geni_tx_watermark = value;
+                self.tx_watermark_latch();
+                return true;
+            }
+            SE_GENI_RX_WATERMARK_REG => {
+                self.geni_rx_watermark = value;
+                if value > 0 && self.rx_fifo_len >= value {
+                    self.geni_s_irq_status |= registers::S_RX_FIFO_WATERMARK;
+                } else {
+                    self.geni_s_irq_status &= !registers::S_RX_FIFO_WATERMARK;
+                }
+                return true;
+            }
+            SE_GENI_RX_RFR_WATERMARK_REG => {
+                self.geni_rx_rfr_watermark = value;
+            }
+            SE_UART_TX_WORD_LEN => {
+                self.uart_tx_word_len = value;
+            }
+            SE_UART_RX_WORD_LEN => {
+                self.uart_rx_word_len = value;
+            }
+            SE_UART_TX_STOP_BIT_LEN => {
+                self.uart_tx_stop_bit_len = value;
+            }
+            SE_UART_RX_STALE_CNT => {
+                self.uart_rx_stale_cnt = value;
+            }
+            SE_UART_TX_TRANS_CFG => {
+                self.uart_tx_trans_cfg = value;
+            }
+            SE_UART_RX_TRANS_CFG => {
+                self.uart_rx_trans_cfg = value;
+            }
+            SE_UART_LOOPBACK_CFG => {
+                self.uart_loopback_cfg = value;
+            }
+            SE_UART_IO_MACRO_CTRL => {
+                self.uart_io_macro_ctrl = value;
+            }
+            SE_UART_MANUAL_RFR => {
+                self.uart_manual_rfr = value;
+            }
+            SE_GENI_TX_PACKING_CFG0 => {
+                self.geni_tx_packing_cfg0 = value;
+            }
+            SE_GENI_TX_PACKING_CFG1 => {
+                self.geni_tx_packing_cfg1 = value;
+            }
+            SE_GENI_RX_PACKING_CFG0 => {
+                self.geni_rx_packing_cfg0 = value;
+            }
+            SE_GENI_RX_PACKING_CFG1 => {
+                self.geni_rx_packing_cfg1 = value;
+            }
+            SE_UART_TX_TRANS_LEN => {
+                self.uart_tx_trans_len = value;
+            }
+            GENI_SER_M_CLK_CFG | GENI_SER_S_CLK_CFG | SE_GENI_CLK_SEL | GENI_OUTPUT_CTRL
+            | GENI_FORCE_DEFAULT | SE_GSI_EVENT_EN | SE_IRQ_EN => {}
+            SE_GENI_M_IRQ_EN_SET => {
+                self.geni_m_irq_en |= value;
+                return true;
+            }
+            SE_GENI_M_IRQ_EN_CLEAR => {
+                self.geni_m_irq_en &= !value;
+                return true;
+            }
+            SE_GENI_S_IRQ_STATUS => {}
+            SE_GENI_S_IRQ_EN => {
+                self.geni_s_irq_en = value;
+                return true;
+            }
+            SE_GENI_S_IRQ_CLEAR => {
+                self.geni_s_irq_status &= !value;
+                return true;
+            }
+            SE_GENI_S_IRQ_EN_SET => {
+                self.geni_s_irq_en |= value;
+                return true;
+            }
+            SE_GENI_S_IRQ_EN_CLEAR => {
+                self.geni_s_irq_en &= !value;
+                return true;
+            }
+            SE_GENI_M_GP_LENGTH => {
+                self.geni_m_gp_length = value;
+            }
+            SE_GENI_S_GP_LENGTH => {
+                self.geni_s_gp_length = value;
+            }
+            SE_UART_TX_PARITY_CFG => {
+                self.uart_tx_parity_cfg = value;
+            }
+            SE_UART_RX_PARITY_CFG => {
+                self.uart_rx_parity_cfg = value;
+            }
+            SE_GENI_CFG_SEQ_START => {
+                if value & 1 != 0 {
+                    log_mask_ln!(
+                        Log::Unimp,
+                        "QUP GENI UART: Configuration sequence start attempted - protocol \
+                         switching not supported"
+                    );
+                }
+            }
+            SE_GENI_DMA_MODE_EN => {
+                self.dma_mode_enabled = (value & 1) != 0;
+            }
+            SE_GENI_M_CMD0 => {
+                self.geni_m_cmd0 = value;
+                let opcode = (value & registers::M_OPCODE_MSK) >> registers::M_OPCODE_SHFT;
+
+                match opcode {
+                    registers::UART_START_TX => {
+                        self.geni_status |= registers::M_GENI_CMD_ACTIVE;
+                        self.tx_enabled = true;
+                        self.tx_watermark_latch();
+                        self.tx_remaining = self.uart_tx_trans_len;
+                        if self.tx_remaining == 0 {
+                            self.geni_status &= !registers::M_GENI_CMD_ACTIVE;
+                            self.tx_enabled = false;
+                            self.geni_m_irq_status |= registers::M_CMD_DONE;
+                        } else if self.dma_tx_armed {
+                            self.dma_tx_active = true;
+                        }
+                        return true;
+                    }
+                    registers::UART_ABORT => {
+                        self.m_cmd_stop(registers::M_CMD_DONE);
+                        return true;
+                    }
+                    _ => {
+                        log_mask_ln!(
+                            Log::Unimp,
+                            "QUP GENI UART: Unsupported command opcode 0x{:x} - device \
+                             only supports UART mode",
+                            opcode
+                        );
+                    }
+                }
+            }
+            SE_GENI_M_CMD_CTRL_REG => {
+                if value & registers::M_GENI_CMD_ABORT != 0 {
+                    self.m_cmd_stop(registers::M_CMD_ABORT);
+                    return true;
+                }
+                if value & registers::M_GENI_CMD_CANCEL != 0 {
+                    self.m_cmd_stop(registers::M_CMD_CANCEL);
+                    return true;
+                }
+            }
+            SE_GENI_S_CMD_CTRL_REG => {
+                if value & registers::S_GENI_CMD_ABORT != 0 {
+                    self.s_cmd_stop(registers::S_CMD_ABORT);
+                    return true;
+                }
+                if value & registers::S_GENI_CMD_CANCEL != 0 {
+                    self.s_cmd_stop(registers::S_CMD_CANCEL);
+                    if self.dma_mode_enabled {
+                        self.dma_rx_irq_stat |= registers::DMA_EOT;
+                    }
+                    return true;
+                }
+            }
+            SE_GENI_S_CMD0 => {
+                self.geni_s_cmd0 = value;
+                let opcode = (value & registers::M_OPCODE_MSK) >> registers::M_OPCODE_SHFT;
+
+                match opcode {
+                    registers::UART_START_READ => {
+                        self.geni_status |= registers::S_GENI_CMD_ACTIVE;
+                        self.rx_enabled = true;
+                    }
+                    registers::UART_ABORT_READ => {
+                        self.s_cmd_stop(registers::S_CMD_DONE);
+                        return true;
+                    }
+                    _ => {
+                        log_mask_ln!(
+                            Log::Unimp,
+                            "QUP GENI UART: Unsupported secondary command opcode 0x{:x} - \
+                             device only supports UART mode",
+                            opcode
+                        );
+                    }
+                }
+            }
+            SE_GENI_TX_FIFO_N => {
+                if self.tx_enabled {
+                    let tx_data = value.to_le_bytes();
+                    let len = self.tx_remaining.min(registers::BYTES_PER_FIFO_WORD) as usize;
+                    if self.uart_loopback_cfg & 0x1 != 0 {
+                        let mut pushed = false;
+                        for &byte in &tx_data[..len] {
+                            if !self.rx_enabled {
+                                break;
+                            }
+                            if !self.rx_push_byte(byte) {
+                                self.geni_s_irq_status |= registers::S_RX_FIFO_WR_ERR;
+                                break;
+                            }
+                            pushed = true;
+                        }
+                        if pushed {
+                            self.rx_data_pushed();
+                        }
+                    } else {
+                        // FIFO writes cannot wait for a blocked backend while
+                        // holding the BQL, so drop any unwritten bytes.
+                        match char_frontend.write(&tx_data[..len]) {
+                            Ok(n) if n == len => {}
+                            _ => log_mask_ln!(
+                                Log::GuestError,
+                                "QUP GENI UART: backend busy, TX FIFO data dropped"
+                            ),
+                        }
+                    }
+                    self.tx_remaining -= len as u32;
+                    self.tx_watermark_latch();
+                    if self.tx_remaining == 0 {
+                        self.geni_status &= !registers::M_GENI_CMD_ACTIVE;
+                        self.tx_enabled = false;
+                        self.geni_m_irq_status |= registers::M_CMD_DONE;
+                    }
+                    return true;
+                } else {
+                    log_mask_ln!(
+                        Log::GuestError,
+                        "QUP GENI UART: TX FIFO write with no TX command active"
+                    );
+                }
+            }
+            SE_DMA_TX_PTR_L => {
+                self.dma_tx_ptr_l = value;
+            }
+            SE_DMA_TX_PTR_H => {
+                self.dma_tx_ptr_h = value;
+            }
+            SE_DMA_TX_ATTR => {
+                self.dma_tx_attr = value;
+            }
+            SE_DMA_TX_LEN => {
+                self.dma_tx_len = value;
+                self.dma_tx_sent = 0;
+                self.dma_tx_armed = self.dma_mode_enabled && value > 0;
+                self.dma_tx_active = self.dma_tx_armed && self.tx_enabled;
+            }
+            SE_DMA_TX_IRQ_EN => {
+                self.dma_tx_irq_en = value;
+                return true;
+            }
+            SE_DMA_TX_IRQ_EN_SET => {
+                self.dma_tx_irq_en |= value;
+                return true;
+            }
+            SE_DMA_TX_IRQ_EN_CLR => {
+                self.dma_tx_irq_en &= !value;
+                return true;
+            }
+            SE_DMA_TX_IRQ_CLR => {
+                self.dma_tx_irq_stat &= !value;
+                return true;
+            }
+            SE_DMA_TX_FSM_RST => {
+                if value & 1 != 0 {
+                    self.dma_tx_stop();
+                    self.dma_tx_irq_stat |= registers::DMA_RESET_DONE;
+                    return true;
+                }
+            }
+            SE_DMA_RX_PTR_L => {
+                self.dma_rx_ptr_l = value;
+            }
+            SE_DMA_RX_PTR_H => {
+                self.dma_rx_ptr_h = value;
+            }
+            SE_DMA_RX_ATTR => {
+                self.dma_rx_attr = value;
+            }
+            SE_DMA_RX_LEN => {
+                self.dma_rx_len = value;
+                self.dma_rx_len_in = 0;
+                if self.dma_mode_enabled && value > 0 {
+                    self.dma_rx_active = true;
+                }
+            }
+            SE_DMA_RX_FSM_RST => {
+                if value & 1 != 0 {
+                    self.dma_rx_active = false;
+                    self.dma_rx_irq_stat |= registers::DMA_RESET_DONE;
+                    return true;
+                }
+            }
+            SE_DMA_RX_IRQ_EN => {
+                self.dma_rx_irq_en = value;
+                return true;
+            }
+            SE_DMA_RX_IRQ_EN_SET => {
+                self.dma_rx_irq_en |= value;
+                return true;
+            }
+            SE_DMA_RX_IRQ_EN_CLR => {
+                self.dma_rx_irq_en &= !value;
+                return true;
+            }
+            SE_DMA_RX_IRQ_CLR => {
+                self.dma_rx_irq_stat &= !value;
+                return true;
+            }
+            _ => {
+                log_mask_ln!(
+                    Log::Unimp,
+                    "QUP GENI UART: Unimplemented write to offset {:x} value {:x}",
+                    offset as u32,
+                    value
+                );
+            }
+        }
+        false
+    }
+
+    // Guest memory may map this device, so the caller performs the access
+    // after releasing the register borrow.
+    fn dma_tx_plan(&mut self) -> Option<(u64, usize)> {
+        let len = self.dma_tx_len.min(self.uart_tx_trans_len);
+
+        if !self.dma_tx_active {
+            return None;
+        }
+        if self.dma_tx_sent >= len {
+            self.dma_tx_stop();
+            self.dma_tx_irq_stat |= registers::DMA_DONE;
+            if self.tx_remaining == 0 {
+                self.tx_enabled = false;
+                self.geni_status &= !registers::M_GENI_CMD_ACTIVE;
+                self.geni_m_irq_status |= registers::M_CMD_DONE;
+            }
+            return None;
+        }
+
+        let mut count = (len - self.dma_tx_sent).min(DMA_CHUNK_SIZE as u32) as usize;
+        if self.uart_loopback_cfg & 1 != 0 && self.rx_enabled && self.dma_rx_active {
+            count = count.min(self.dma_rx_len as usize);
+        }
+        let base = (u64::from(self.dma_tx_ptr_h) << 32) | u64::from(self.dma_tx_ptr_l);
+        match base.checked_add(u64::from(self.dma_tx_sent)) {
+            Some(addr) if count > 0 => Some((addr, count)),
+            Some(_) => None,
+            None => {
+                self.dma_tx_irq_stat |= registers::DMA_AHB_ERR;
+                self.dma_tx_stop();
+                None
+            }
+        }
+    }
+
+    fn dma_tx_stop(&mut self) {
+        self.dma_tx_armed = false;
+        self.dma_tx_active = false;
+    }
+
+    fn dma_rx_addr(&self) -> u64 {
+        (u64::from(self.dma_rx_ptr_h) << 32) | u64::from(self.dma_rx_ptr_l)
+    }
+
+    fn dma_rx_finish(&mut self, len: usize, ok: bool) {
+        self.dma_rx_active = false;
+        if ok {
+            self.dma_rx_len_in = len as u32;
+            self.dma_rx_irq_stat |= registers::DMA_DONE;
+        } else {
+            self.dma_rx_irq_stat |= registers::DMA_AHB_ERR;
+        }
+    }
+
+    fn m_cmd_stop(&mut self, irq: u32) {
+        self.geni_status &= !registers::M_GENI_CMD_ACTIVE;
+        self.tx_enabled = false;
+        self.dma_tx_stop();
+        self.tx_remaining = 0;
+        self.geni_m_irq_status |= irq;
+    }
+
+    fn s_cmd_stop(&mut self, irq: u32) {
+        self.geni_status &= !registers::S_GENI_CMD_ACTIVE;
+        self.rx_enabled = false;
+        self.dma_rx_active = false;
+        self.rx_fifo_len = 0;
+        self.rx_last_byte_cnt = 0;
+        self.rx_tail_closed = false;
+        self.geni_s_irq_status &= !(registers::S_RX_FIFO_WATERMARK | registers::S_RX_FIFO_LAST);
+        self.geni_s_irq_status |= irq;
+    }
+
+    // RX_LAST_BYTE_VALID can describe only the last FIFO word, so fill a
+    // partial word before appending another one.
+    fn rx_push_byte(&mut self, byte: u8) -> bool {
+        if self.rx_last_byte_cnt > 0 && self.rx_fifo_len > 0 {
+            if self.rx_tail_closed {
+                return false;
+            }
+            let idx = self.rx_fifo_len - 1;
+            self.rx_fifo[idx] |= u32::from(byte) << (self.rx_last_byte_cnt * 8);
+            self.rx_last_byte_cnt = (self.rx_last_byte_cnt + 1) % registers::BYTES_PER_FIFO_WORD;
+            return true;
+        }
+        if self.rx_fifo_len >= QUP_FIFO_DEPTH {
+            return false;
+        }
+        let idx = self.rx_fifo_len;
+        self.rx_fifo[idx] = u32::from(byte);
+        self.rx_fifo_len += 1;
+        self.rx_last_byte_cnt = 1;
+        true
+    }
+
+    fn rx_data_pushed(&mut self) {
+        if self.geni_rx_watermark > 0 && self.rx_fifo_len >= self.geni_rx_watermark {
+            self.geni_s_irq_status |= registers::S_RX_FIFO_WATERMARK;
+        }
+        if self.rx_last_byte_cnt > 0 {
+            self.geni_s_irq_status |= registers::S_RX_FIFO_LAST;
+        }
+    }
+
+    pub fn reset(&mut self) {
+        self.geni_status = 0;
+        self.geni_m_cmd0 = 0;
+        self.geni_m_irq_status = 0;
+        self.geni_m_irq_en = 0;
+        self.geni_s_cmd0 = 0;
+        self.geni_s_irq_status = 0;
+        self.geni_s_irq_en = 0;
+        self.geni_tx_watermark = registers::DEF_TX_WM;
+        self.geni_rx_watermark = registers::UART_RX_WM;
+        self.geni_rx_rfr_watermark = registers::UART_RX_WM;
+        self.tx_watermark_latch();
+        self.geni_m_gp_length = 0;
+        self.geni_s_gp_length = 0;
+        self.geni_tx_packing_cfg0 = 0;
+        self.geni_tx_packing_cfg1 = 0;
+        self.geni_rx_packing_cfg0 = 0;
+        self.geni_rx_packing_cfg1 = 0;
+
+        self.uart_tx_trans_cfg = 0;
+        self.uart_rx_trans_cfg = 0;
+        self.uart_tx_word_len = 0;
+        self.uart_rx_word_len = 0;
+        self.uart_tx_stop_bit_len = 0;
+        self.uart_rx_stale_cnt = registers::STALE_TIMEOUT;
+        self.uart_tx_parity_cfg = 0;
+        self.uart_rx_parity_cfg = 0;
+        self.uart_loopback_cfg = 0;
+        self.uart_io_macro_ctrl = 0;
+        self.uart_manual_rfr = 0;
+        self.uart_tx_trans_len = 0;
+
+        self.rx_fifo_len = 0;
+        self.rx_last_byte_cnt = 0;
+        self.rx_tail_closed = false;
+        self.tx_remaining = 0;
+        self.tx_enabled = false;
+        self.rx_enabled = false;
+        self.dma_mode_enabled = false;
+
+        self.dma_tx_ptr_l = 0;
+        self.dma_tx_ptr_h = 0;
+        self.dma_tx_attr = 0;
+        self.dma_tx_len = 0;
+        self.dma_tx_sent = 0;
+        self.dma_tx_irq_en = 0;
+        self.dma_tx_irq_stat = 0;
+        self.dma_rx_ptr_l = 0;
+        self.dma_rx_ptr_h = 0;
+        self.dma_rx_attr = 0;
+        self.dma_rx_len = 0;
+        self.dma_rx_len_in = 0;
+        self.dma_rx_irq_en = 0;
+        self.dma_rx_irq_stat = 0;
+        self.dma_tx_armed = false;
+        self.dma_tx_active = false;
+        self.dma_rx_active = false;
+    }
+}
+
+impl QupGeniUartState {
+    /// # Safety
+    ///
+    /// `this` must refer to a new, correctly sized and aligned instance whose
+    /// fields other than `parent_obj` are uninitialized.
+    unsafe fn init(mut this: ParentInit<Self>) {
+        static QUP_GENI_UART_OPS: MemoryRegionOps<QupGeniUartState> =
+            MemoryRegionOpsBuilder::<QupGeniUartState>::new()
+                .read(&QupGeniUartState::read)
+                .write(&QupGeniUartState::write)
+                .little_endian()
+                .impl_sizes(4, 4)
+                .build();
+
+        // SAFETY: this and this.iomem are guaranteed to be valid at this point
+        MemoryRegion::init_io(
+            &mut uninit_field_mut!(*this, iomem),
+            &QUP_GENI_UART_OPS,
+            "qup-geni-uart",
+            QUP_GENI_UART_MMIO_SIZE,
+        );
+
+        uninit_field_mut!(*this, regs).write(Default::default());
+        uninit_field_mut!(*this, tx_watch).write(Default::default());
+        uninit_field_mut!(*this, dma_mr).write(BqlCell::new(DmaRegion(ptr::null_mut())));
+        // SAFETY: address_space_init() fills it in when the device is realized
+        uninit_field_mut!(*this, dma_as).write(unsafe { DmaAddressSpace(Opaque::zeroed()) });
+        uninit_field_mut!(*this, dma_as_ready).write(Default::default());
+        uninit_field_mut!(*this, dma_tx_busy).write(Default::default());
+    }
+
+    fn post_init(&self) {
+        self.init_mmio(&self.iomem);
+        self.init_irq(&self.irq);
+
+        // SAFETY: dma_mr lives as long as the object, and DmaRegion is a
+        // transparent wrapper of the pointer that the link property stores.
+        // The property keeps a reference to what it is set to.
+        unsafe {
+            object_property_add_link(
+                self.as_object_mut_ptr(),
+                c"memory".as_ptr(),
+                c"memory-region".as_ptr(),
+                self.dma_mr.as_ptr().cast(),
+                Some(qdev_prop_allow_set_link_before_realize),
+                OBJ_PROP_LINK_STRONG,
+            );
+        }
+    }
+
+    fn read(&self, offset: hwaddr, _size: u32) -> u64 {
+        match RegisterOffset::try_from(offset) {
+            Ok(reg) => {
+                let (update_irq, result) = self.regs.borrow_mut().read(reg);
+                if update_irq {
+                    self.update_irq();
+                    self.char_frontend.accept_input();
+                }
+                result.into()
+            }
+            Err(_) => {
+                log_mask_ln!(
+                    Log::GuestError,
+                    "QupGeniUartState::read: Bad offset {:x}",
+                    offset
+                );
+                0
+            }
+        }
+    }
+
+    fn write(&self, offset: hwaddr, value: u64, _size: u32) {
+        if let Ok(reg) = RegisterOffset::try_from(offset) {
+            let update_irq = self
+                .regs
+                .borrow_mut()
+                .write(reg, value as u32, &self.char_frontend);
+            if update_irq {
+                self.update_irq();
+            }
+            if matches!(
+                reg,
+                RegisterOffset::SE_GENI_M_CMD0
+                    | RegisterOffset::SE_DMA_TX_LEN
+                    | RegisterOffset::SE_DMA_RX_LEN
+            ) {
+                self.run_dma_tx();
+                self.update_irq();
+            }
+            self.update_tx_watch();
+            if matches!(
+                reg,
+                RegisterOffset::SE_GENI_S_CMD0
+                    | RegisterOffset::SE_GENI_S_CMD_CTRL_REG
+                    | RegisterOffset::SE_DMA_RX_LEN
+            ) {
+                self.char_frontend.accept_input();
+            }
+        } else {
+            log_mask_ln!(
+                Log::GuestError,
+                "QupGeniUartState::write: Bad offset {:x} value {:x}",
+                offset,
+                value
+            );
+        }
+    }
+
+    fn dma_read(&self, addr: u64, buf: &mut [u8]) -> bool {
+        // SAFETY: buf is writable for its length and dma_as was initialized
+        // by realize, before any register could be accessed.
+        let result = unsafe {
+            address_space_rw(
+                self.dma_as.0.as_ptr(),
+                addr,
+                MEMTXATTRS_UNSPECIFIED,
+                buf.as_mut_ptr().cast(),
+                buf.len() as u64,
+                false,
+            )
+        };
+        result == 0
+    }
+
+    fn dma_write(&self, addr: u64, data: &[u8]) -> bool {
+        // SAFETY: data is readable for its length and dma_as was initialized
+        // by realize, before any register could be accessed.
+        let result = unsafe {
+            address_space_rw(
+                self.dma_as.0.as_ptr(),
+                addr,
+                MEMTXATTRS_UNSPECIFIED,
+                data.as_ptr().cast_mut().cast(),
+                data.len() as u64,
+                true,
+            )
+        };
+        result == 0
+    }
+
+    // Complete RX DMA on each delivery, as if the stale timeout expired.
+    // Release the register borrow in case the DMA target is this device.
+    fn dma_rx_deliver(&self, data: &[u8]) {
+        let (addr, data) = {
+            let mut regs = self.regs.borrow_mut();
+            regs.dma_rx_active = false;
+            let len = data.len().min(regs.dma_rx_len as usize);
+            (regs.dma_rx_addr(), &data[..len])
+        };
+        let ok = self.dma_write(addr, data);
+        self.regs.borrow_mut().dma_rx_finish(data.len(), ok);
+    }
+
+    fn run_dma_tx(&self) {
+        if self.dma_tx_busy.replace(true) {
+            return;
+        }
+        self.run_dma_tx_chunks();
+        self.dma_tx_busy.set(false);
+    }
+
+    fn run_dma_tx_chunks(&self) {
+        let mut buf = [0u8; DMA_CHUNK_SIZE];
+
+        loop {
+            let plan = self.regs.borrow_mut().dma_tx_plan();
+            let Some((addr, count)) = plan else {
+                break;
+            };
+            let ok = self.dma_read(addr, &mut buf[..count]);
+
+            let mut regs = self.regs.borrow_mut();
+            if !regs.dma_tx_active {
+                break;
+            }
+            if !ok {
+                regs.dma_tx_irq_stat |= registers::DMA_AHB_ERR;
+                regs.dma_tx_stop();
+                break;
+            }
+            let sent = if regs.uart_loopback_cfg & 1 != 0 {
+                let deliver = regs.rx_enabled && regs.dma_rx_active;
+                drop(regs);
+                if deliver {
+                    self.dma_rx_deliver(&buf[..count]);
+                }
+                regs = self.regs.borrow_mut();
+                count
+            } else {
+                match self.char_frontend.write(&buf[..count]) {
+                    // There is no source to wake us when no backend is present.
+                    Ok(0) => count,
+                    Ok(sent) => sent,
+                    Err(err) if err.kind() == ErrorKind::WouldBlock => break,
+                    Err(_) => count,
+                }
+            };
+            regs.dma_tx_sent = regs.dma_tx_sent.saturating_add(sent as u32);
+            regs.tx_remaining = regs.tx_remaining.saturating_sub(sent as u32);
+        }
+    }
+
+    fn can_receive(&self) -> u32 {
+        let regs = self.regs.borrow();
+        if regs.uart_loopback_cfg & 1 != 0 {
+            0
+        } else if regs.rx_enabled && regs.dma_mode_enabled {
+            if regs.dma_rx_active {
+                regs.dma_rx_len.min(DMA_CHUNK_SIZE as u32)
+            } else {
+                0
+            }
+        } else if regs.rx_enabled {
+            if regs.rx_tail_closed {
+                0
+            } else {
+                let words = QUP_FIFO_DEPTH - regs.rx_fifo_len;
+                let tail = if regs.rx_last_byte_cnt > 0 {
+                    registers::BYTES_PER_FIFO_WORD - regs.rx_last_byte_cnt
+                } else {
+                    0
+                };
+                words * registers::BYTES_PER_FIFO_WORD + tail
+            }
+        } else {
+            0
+        }
+    }
+
+    fn receive(&self, buf: &[u8]) {
+        let mut regs = self.regs.borrow_mut();
+        if !regs.rx_enabled || (regs.uart_loopback_cfg & 0x1 != 0) {
+            return;
+        }
+
+        if regs.dma_mode_enabled {
+            let dma_rx_active = regs.dma_rx_active;
+            drop(regs);
+            if dma_rx_active {
+                self.dma_rx_deliver(buf);
+            }
+            self.update_irq();
+            return;
+        }
+
+        let mut pushed = false;
+        let mut overflow = false;
+        for &byte in buf {
+            if !regs.rx_push_byte(byte) {
+                overflow = true;
+                break;
+            }
+            pushed = true;
+        }
+        if overflow {
+            regs.geni_s_irq_status |= registers::S_RX_FIFO_WR_ERR;
+        }
+        if pushed {
+            regs.rx_data_pushed();
+        }
+
+        drop(regs);
+        if pushed || overflow {
+            self.update_irq();
+        }
+    }
+
+    fn event(&self, _event: Event) {}
+
+    fn realize(&self) -> util::Result<()> {
+        let mut dma_mr = self.dma_mr.get().0;
+        if dma_mr.is_null() {
+            // SAFETY: the system memory exists for the life of the machine.
+            // The link property is strong and unrefs its target when the
+            // device is finalized, so it needs a reference of its own here.
+            dma_mr = unsafe {
+                let mr = get_system_memory();
+                object_ref(mr.cast());
+                mr
+            };
+            self.dma_mr.set(DmaRegion(dma_mr));
+        }
+        // SAFETY: dma_as is not initialized yet and dma_mr is a valid region.
+        // The link property drops its reference before Drop runs, so keep
+        // one that lasts as long as the address space.
+        unsafe {
+            object_ref(dma_mr.cast());
+            address_space_init(
+                self.dma_as.0.as_mut_ptr(),
+                dma_mr,
+                c"qup-geni-uart-dma".as_ptr(),
+            );
+        }
+        self.dma_as_ready.set(true);
+
+        self.char_frontend
+            .enable_handlers(self, Self::can_receive, Self::receive, Self::event);
+        Ok(())
+    }
+
+    fn reset_hold(&self, _type: ResetType) {
+        self.clear_tx_watch();
+        self.dma_tx_busy.set(false);
+        self.regs.borrow_mut().reset();
+        self.update_irq();
+        self.char_frontend.accept_input();
+    }
+
+    fn clear_tx_watch(&self) {
+        let tag = self.tx_watch.replace(0);
+        self.char_frontend.remove_watch(tag);
+    }
+
+    fn update_tx_watch(&self) {
+        if !self.regs.borrow().dma_tx_active || self.regs.borrow().uart_loopback_cfg & 1 != 0 {
+            self.clear_tx_watch();
+        } else if self.tx_watch.get() == 0 {
+            // SAFETY: the source is removed on reset and before object drop.
+            let tag = unsafe {
+                self.char_frontend.add_write_watch(
+                    Some(Self::tx_ready),
+                    (self as *const Self).cast_mut().cast(),
+                )
+            };
+            self.tx_watch.set(tag);
+            if tag == 0 {
+                log_mask_ln!(
+                    Log::Unimp,
+                    "QUP GENI UART: backend cannot watch for writability, \
+                     assuming the TX DMA data was accepted"
+                );
+                let mut regs = self.regs.borrow_mut();
+                let pending = regs
+                    .dma_tx_len
+                    .min(regs.uart_tx_trans_len)
+                    .saturating_sub(regs.dma_tx_sent);
+                regs.tx_remaining = regs.tx_remaining.saturating_sub(pending);
+                regs.dma_tx_stop();
+                regs.dma_tx_irq_stat |= registers::DMA_DONE;
+                if regs.tx_remaining == 0 {
+                    regs.tx_enabled = false;
+                    regs.geni_status &= !registers::M_GENI_CMD_ACTIVE;
+                    regs.geni_m_irq_status |= registers::M_CMD_DONE;
+                }
+                drop(regs);
+                self.update_irq();
+            }
+        }
+    }
+
+    unsafe extern "C" fn tx_ready(
+        _unused: *mut c_void,
+        _condition: u32,
+        opaque: *mut c_void,
+    ) -> i32 {
+        // SAFETY: the watch holds a pointer to this live QOM object.
+        let this = unsafe { &*opaque.cast::<Self>() };
+        this.tx_watch.set(0);
+        this.run_dma_tx();
+        this.update_irq();
+        this.update_tx_watch();
+        0
+    }
+
+    fn post_load(&self, _version: u8) -> Result<(), migration::InvalidError> {
+        {
+            // Reject invalid array indices and shift counts before use.
+            let regs = self.regs.borrow();
+            if regs.rx_fifo_len > QUP_FIFO_DEPTH
+                || regs.rx_last_byte_cnt >= registers::BYTES_PER_FIFO_WORD
+                || (regs.rx_last_byte_cnt > 0 && regs.rx_fifo_len == 0)
+                || (regs.rx_tail_closed && regs.rx_last_byte_cnt == 0)
+                || regs.dma_tx_sent > regs.dma_tx_len
+                || regs.dma_rx_len_in > regs.dma_rx_len
+            {
+                return Err(migration::InvalidError);
+            }
+        }
+        self.update_irq();
+        self.update_tx_watch();
+        Ok(())
+    }
+
+    fn update_irq(&self) {
+        let regs = self.regs.borrow();
+        let irq_active = (regs.geni_m_irq_status & regs.geni_m_irq_en) != 0
+            || (regs.geni_s_irq_status & regs.geni_s_irq_en) != 0
+            || (regs.dma_tx_irq_stat & regs.dma_tx_irq_en) != 0
+            || (regs.dma_rx_irq_stat & regs.dma_rx_irq_en) != 0;
+        self.irq.set(irq_active);
+    }
+}
+
+impl Drop for QupGeniUartState {
+    fn drop(&mut self) {
+        self.clear_tx_watch();
+        if self.dma_as_ready.get() {
+            // SAFETY: the address space was initialized by realize
+            unsafe {
+                address_space_destroy(self.dma_as.0.as_mut_ptr());
+                object_unref(self.dma_mr.get().0.cast());
+            }
+        }
+    }
+}
+
+impl_vmstate_struct!(
+    QupGeniUartRegs,
+    VMStateDescriptionBuilder::<QupGeniUartRegs>::new()
+        .name(c"qup_geni_uart/regs")
+        .version_id(1)
+        .minimum_version_id(1)
+        .fields(vmstate_fields! {
+        vmstate_of!(QupGeniUartRegs, geni_status),
+        vmstate_of!(QupGeniUartRegs, geni_m_cmd0),
+        vmstate_of!(QupGeniUartRegs, geni_m_irq_status),
+        vmstate_of!(QupGeniUartRegs, geni_m_irq_en),
+        vmstate_of!(QupGeniUartRegs, geni_s_cmd0),
+        vmstate_of!(QupGeniUartRegs, geni_s_irq_status),
+        vmstate_of!(QupGeniUartRegs, geni_s_irq_en),
+        vmstate_of!(QupGeniUartRegs, geni_tx_watermark),
+        vmstate_of!(QupGeniUartRegs, geni_rx_watermark),
+        vmstate_of!(QupGeniUartRegs, geni_rx_rfr_watermark),
+        vmstate_of!(QupGeniUartRegs, geni_m_gp_length),
+        vmstate_of!(QupGeniUartRegs, geni_s_gp_length),
+        vmstate_of!(QupGeniUartRegs, geni_tx_packing_cfg0),
+        vmstate_of!(QupGeniUartRegs, geni_tx_packing_cfg1),
+        vmstate_of!(QupGeniUartRegs, geni_rx_packing_cfg0),
+        vmstate_of!(QupGeniUartRegs, geni_rx_packing_cfg1),
+        vmstate_of!(QupGeniUartRegs, uart_tx_trans_cfg),
+        vmstate_of!(QupGeniUartRegs, uart_rx_trans_cfg),
+        vmstate_of!(QupGeniUartRegs, uart_tx_word_len),
+        vmstate_of!(QupGeniUartRegs, uart_rx_word_len),
+        vmstate_of!(QupGeniUartRegs, uart_tx_stop_bit_len),
+        vmstate_of!(QupGeniUartRegs, uart_rx_stale_cnt),
+        vmstate_of!(QupGeniUartRegs, uart_tx_parity_cfg),
+        vmstate_of!(QupGeniUartRegs, uart_rx_parity_cfg),
+        vmstate_of!(QupGeniUartRegs, uart_loopback_cfg),
+        vmstate_of!(QupGeniUartRegs, uart_io_macro_ctrl),
+        vmstate_of!(QupGeniUartRegs, uart_manual_rfr),
+        vmstate_of!(QupGeniUartRegs, uart_tx_trans_len),
+        vmstate_of!(QupGeniUartRegs, rx_fifo),
+        vmstate_of!(QupGeniUartRegs, rx_fifo_len),
+        vmstate_of!(QupGeniUartRegs, rx_last_byte_cnt),
+        vmstate_of!(QupGeniUartRegs, rx_tail_closed),
+        vmstate_of!(QupGeniUartRegs, tx_remaining),
+        vmstate_of!(QupGeniUartRegs, tx_enabled),
+        vmstate_of!(QupGeniUartRegs, rx_enabled),
+        vmstate_of!(QupGeniUartRegs, dma_mode_enabled),
+        vmstate_of!(QupGeniUartRegs, dma_tx_ptr_l),
+        vmstate_of!(QupGeniUartRegs, dma_tx_ptr_h),
+        vmstate_of!(QupGeniUartRegs, dma_tx_attr),
+        vmstate_of!(QupGeniUartRegs, dma_tx_len),
+        vmstate_of!(QupGeniUartRegs, dma_tx_sent),
+        vmstate_of!(QupGeniUartRegs, dma_tx_irq_en),
+        vmstate_of!(QupGeniUartRegs, dma_tx_irq_stat),
+        vmstate_of!(QupGeniUartRegs, dma_rx_ptr_l),
+        vmstate_of!(QupGeniUartRegs, dma_rx_ptr_h),
+        vmstate_of!(QupGeniUartRegs, dma_rx_attr),
+        vmstate_of!(QupGeniUartRegs, dma_rx_len),
+        vmstate_of!(QupGeniUartRegs, dma_rx_len_in),
+        vmstate_of!(QupGeniUartRegs, dma_rx_irq_en),
+        vmstate_of!(QupGeniUartRegs, dma_rx_irq_stat),
+        vmstate_of!(QupGeniUartRegs, dma_tx_armed),
+        vmstate_of!(QupGeniUartRegs, dma_tx_active),
+        vmstate_of!(QupGeniUartRegs, dma_rx_active),
+        })
+        .build()
+);
+
+pub const VMSTATE_QUP_GENI_UART: VMStateDescription<QupGeniUartState> =
+    VMStateDescriptionBuilder::<QupGeniUartState>::new()
+        .name(c"qup_geni_uart")
+        .post_load(&QupGeniUartState::post_load)
+        .version_id(1)
+        .minimum_version_id(1)
+        .fields(vmstate_fields! {
+            vmstate_of!(QupGeniUartState, regs),
+        })
+        .build();
diff --git a/rust/hw/char/qup_geni_uart/src/lib.rs b/rust/hw/char/qup_geni_uart/src/lib.rs
new file mode 100644
index 00000000000..209739867f6
--- /dev/null
+++ b/rust/hw/char/qup_geni_uart/src/lib.rs
@@ -0,0 +1,9 @@
+// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+//! Qualcomm QUP GENI UART device model
+
+mod device;
+mod registers;
+
+pub const TYPE_QUP_GENI_UART: &::std::ffi::CStr = c"qup-geni-uart";
diff --git a/rust/hw/char/qup_geni_uart/src/registers.rs b/rust/hw/char/qup_geni_uart/src/registers.rs
new file mode 100644
index 00000000000..571b357ea24
--- /dev/null
+++ b/rust/hw/char/qup_geni_uart/src/registers.rs
@@ -0,0 +1,160 @@
+// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+//! Register offsets and bit fields of the QUP GENI UART
+
+#[allow(non_camel_case_types)]
+#[repr(u64)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, common::TryInto)]
+pub enum RegisterOffset {
+    // Common SE registers
+    GENI_FORCE_DEFAULT = 0x020,
+    GENI_OUTPUT_CTRL = 0x024,
+    SE_GENI_STATUS = 0x040,
+    GENI_SER_M_CLK_CFG = 0x048,
+    GENI_SER_S_CLK_CFG = 0x04c,
+    GENI_IF_DISABLE_RO = 0x064,
+    GENI_FW_REVISION_RO = 0x068,
+    SE_GENI_CLK_SEL = 0x07c,
+    SE_GENI_CFG_SEQ_START = 0x084,
+    SE_GENI_DMA_MODE_EN = 0x258,
+    SE_GENI_TX_PACKING_CFG0 = 0x260,
+    SE_GENI_TX_PACKING_CFG1 = 0x264,
+    SE_GENI_RX_PACKING_CFG0 = 0x284,
+    SE_GENI_RX_PACKING_CFG1 = 0x288,
+    SE_GENI_M_CMD0 = 0x600,
+    SE_GENI_M_CMD_CTRL_REG = 0x604,
+    SE_GENI_M_IRQ_STATUS = 0x610,
+    SE_GENI_M_IRQ_EN = 0x614,
+    SE_GENI_M_IRQ_CLEAR = 0x618,
+    SE_GENI_M_IRQ_EN_SET = 0x61c,
+    SE_GENI_M_IRQ_EN_CLEAR = 0x620,
+    SE_GENI_S_CMD0 = 0x630,
+    SE_GENI_S_CMD_CTRL_REG = 0x634,
+    SE_GENI_S_IRQ_STATUS = 0x640,
+    SE_GENI_S_IRQ_EN = 0x644,
+    SE_GENI_S_IRQ_CLEAR = 0x648,
+    SE_GENI_S_IRQ_EN_SET = 0x64c,
+    SE_GENI_S_IRQ_EN_CLEAR = 0x650,
+    SE_GENI_TX_FIFO_N = 0x700,
+    SE_GENI_RX_FIFO_N = 0x780,
+    SE_GENI_TX_FIFO_STATUS = 0x800,
+    SE_GENI_RX_FIFO_STATUS = 0x804,
+    SE_GENI_TX_WATERMARK_REG = 0x80c,
+    SE_GENI_RX_WATERMARK_REG = 0x810,
+    SE_GENI_RX_RFR_WATERMARK_REG = 0x814,
+    SE_GENI_IOS = 0x908,
+    SE_GENI_M_GP_LENGTH = 0x910,
+    SE_GENI_S_GP_LENGTH = 0x914,
+    SE_GSI_EVENT_EN = 0xe18,
+    SE_IRQ_EN = 0xe1c,
+    SE_HW_PARAM0 = 0xe24,
+    SE_HW_PARAM1 = 0xe28,
+
+    // DMA registers
+    SE_DMA_TX_PTR_L = 0xc30,
+    SE_DMA_TX_PTR_H = 0xc34,
+    SE_DMA_TX_ATTR = 0xc38,
+    SE_DMA_TX_LEN = 0xc3c,
+    SE_DMA_TX_IRQ_EN = 0xc48,
+    SE_DMA_TX_IRQ_EN_SET = 0xc4c,
+    SE_DMA_TX_IRQ_EN_CLR = 0xc50,
+    SE_DMA_TX_IRQ_STAT = 0xc40,
+    SE_DMA_TX_IRQ_CLR = 0xc44,
+    SE_DMA_TX_FSM_RST = 0xc58,
+    SE_DMA_RX_PTR_L = 0xd30,
+    SE_DMA_RX_PTR_H = 0xd34,
+    SE_DMA_RX_ATTR = 0xd38,
+    SE_DMA_RX_LEN = 0xd3c,
+    SE_DMA_RX_IRQ_EN = 0xd48,
+    SE_DMA_RX_IRQ_EN_SET = 0xd4c,
+    SE_DMA_RX_IRQ_EN_CLR = 0xd50,
+    SE_DMA_RX_IRQ_STAT = 0xd40,
+    SE_DMA_RX_IRQ_CLR = 0xd44,
+    SE_DMA_RX_LEN_IN = 0xd54,
+    SE_DMA_RX_FSM_RST = 0xd58,
+
+    // UART specific registers
+    SE_UART_LOOPBACK_CFG = 0x22c,
+    SE_UART_IO_MACRO_CTRL = 0x240,
+    SE_UART_TX_TRANS_CFG = 0x25c,
+    SE_UART_TX_WORD_LEN = 0x268,
+    SE_UART_TX_STOP_BIT_LEN = 0x26c,
+    SE_UART_TX_TRANS_LEN = 0x270,
+    SE_UART_RX_TRANS_CFG = 0x280,
+    SE_UART_RX_WORD_LEN = 0x28c,
+    SE_UART_RX_STALE_CNT = 0x294,
+    SE_UART_TX_PARITY_CFG = 0x2a4,
+    SE_UART_RX_PARITY_CFG = 0x2a8,
+    SE_UART_MANUAL_RFR = 0x2ac,
+}
+
+// Status register fields
+pub const M_GENI_CMD_ACTIVE: u32 = 1 << 0;
+pub const S_GENI_CMD_ACTIVE: u32 = 1 << 12;
+
+// GENI_M_CMD_CTRL_REG fields
+pub const M_GENI_CMD_ABORT: u32 = 1 << 1;
+pub const M_GENI_CMD_CANCEL: u32 = 1 << 2;
+
+// GENI_S_CMD_CTRL_REG fields
+pub const S_GENI_CMD_ABORT: u32 = 1 << 1;
+pub const S_GENI_CMD_CANCEL: u32 = 1 << 2;
+
+// FW Revision fields
+pub const FW_REV_PROTOCOL_SHFT: u32 = 8;
+
+// Protocol constants
+pub const GENI_SE_UART: u32 = 2;
+
+// M_CMD0 fields
+pub const M_OPCODE_MSK: u32 = 0xf800_0000;
+pub const M_OPCODE_SHFT: u32 = 27;
+
+// UART M_CMD OP codes
+pub const UART_START_TX: u32 = 0x1;
+pub const UART_ABORT: u32 = 0x2;
+
+// UART S_CMD OP codes
+pub const UART_START_READ: u32 = 0x1;
+pub const UART_ABORT_READ: u32 = 0x2;
+
+// Main sequencer interrupt bits (same positions in IRQ_STATUS, IRQ_EN and
+// IRQ_CLEAR)
+pub const M_CMD_DONE: u32 = 1 << 0;
+pub const M_CMD_CANCEL: u32 = 1 << 4;
+pub const M_CMD_ABORT: u32 = 1 << 5;
+pub const M_TX_FIFO_WATERMARK: u32 = 1 << 30;
+
+// Secondary sequencer interrupt bits
+pub const S_CMD_DONE: u32 = 1 << 0;
+pub const S_CMD_CANCEL: u32 = 1 << 4;
+pub const S_CMD_ABORT: u32 = 1 << 5;
+pub const S_RX_FIFO_WR_ERR: u32 = 1 << 25;
+pub const S_RX_FIFO_WATERMARK: u32 = 1 << 26;
+pub const S_RX_FIFO_LAST: u32 = 1 << 27;
+
+// RX FIFO status fields
+pub const RX_LAST: u32 = 1 << 31;
+pub const RX_LAST_BYTE_VALID_MSK: u32 = 0x7000_0000;
+pub const RX_LAST_BYTE_VALID_SHFT: u32 = 28;
+pub const RX_FIFO_WC_MSK: u32 = 0x01ff_ffff;
+
+// Hardware parameter register fields
+pub const TX_FIFO_WIDTH_SHFT: u32 = 24;
+pub const TX_FIFO_DEPTH_SHFT: u32 = 16;
+pub const RX_FIFO_WIDTH_SHFT: u32 = 24;
+pub const RX_FIFO_DEPTH_SHFT: u32 = 16;
+
+pub const BYTES_PER_FIFO_WORD: u32 = 4;
+pub const UART_FIFO_DEPTH_WORDS: u32 = 64;
+pub const UART_FIFO_WIDTH_BITS: u32 = 32;
+pub const STALE_TIMEOUT: u32 = 160;
+pub const DEF_TX_WM: u32 = 2;
+pub const UART_RX_WM: u32 = 2;
+
+// DMA interrupt status fields
+pub const DMA_DONE: u32 = 1 << 0;
+pub const DMA_EOT: u32 = 1 << 1;
+pub const DMA_AHB_ERR: u32 = 1 << 2;
+pub const DMA_RESET_DONE: u32 = 1 << 3;
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH 0/3] hw/misc: QUP UART device
  2026-10-27 18:38 [PATCH 0/3] hw/misc: QUP UART device Brian Cain
                   ` (2 preceding siblings ...)
  2026-10-27 18:38 ` [PATCH 3/3] rust/hw/char: add QUP GENI UART device model Brian Cain
@ 2026-10-27 18:43 ` Pierrick Bouvier
  2026-10-27 20:02   ` Brian Cain
  3 siblings, 1 reply; 11+ messages in thread
From: Pierrick Bouvier @ 2026-10-27 18:43 UTC (permalink / raw)
  To: Brian Cain, qemu-devel; +Cc: Manos Pitsidianakis, qemu-rust, Paolo Bonzini

On 10/27/2026 10:38 AM, Brian Cain wrote:
> Add the QUP + GENI uart device models.  The QUP is a "Universal Peripheral"
> present on Qualcomm SoCs, with connections for SPI, UART, I2C, I3C.  For
> now we're only interested in UART.
> 
> Will follow up with a v2 including wiring into machines + qtest after
> https://lore.kernel.org/qemu-devel/20261027143721.4125584-1-brian.cain@oss.qualcomm.com/
> lands.
> 
> Brian Cain (3):
>   hw/misc: add the QUP wrapper device
>   rust/chardev: fix write errors and add write watches
>   rust/hw/char: add QUP GENI UART device model
> 
>  MAINTAINERS                                 |    3 +
>  docs/system/device-emulation.rst            |    1 +
>  docs/system/devices/qup.rst                 |   10 +
>  include/hw/misc/qup-wrapper.h               |   27 +
>  hw/misc/qup-wrapper.c                       |   90 ++
>  Cargo.lock                                  |   15 +
>  Cargo.toml                                  |    1 +
>  hw/misc/Kconfig                             |    3 +
>  hw/misc/meson.build                         |    3 +
>  rust/chardev/src/chardev.rs                 |   82 +-
>  rust/hw/char/Kconfig                        |    4 +
>  rust/hw/char/meson.build                    |    1 +
>  rust/hw/char/qup_geni_uart/Cargo.toml       |   27 +
>  rust/hw/char/qup_geni_uart/meson.build      |   28 +
>  rust/hw/char/qup_geni_uart/src/device.rs    | 1293 +++++++++++++++++++
>  rust/hw/char/qup_geni_uart/src/lib.rs       |    9 +
>  rust/hw/char/qup_geni_uart/src/registers.rs |  160 +++
>  17 files changed, 1741 insertions(+), 16 deletions(-)
>  create mode 100644 docs/system/devices/qup.rst
>  create mode 100644 include/hw/misc/qup-wrapper.h
>  create mode 100644 hw/misc/qup-wrapper.c
>  create mode 100644 rust/hw/char/qup_geni_uart/Cargo.toml
>  create mode 100644 rust/hw/char/qup_geni_uart/meson.build
>  create mode 100644 rust/hw/char/qup_geni_uart/src/device.rs
>  create mode 100644 rust/hw/char/qup_geni_uart/src/lib.rs
>  create mode 100644 rust/hw/char/qup_geni_uart/src/registers.rs
> 

Any specific reason to have this implemented in Rust?

Not a bad thing in itself, but it means it's only available with rust
builds, which can be a problem if a machine defined in C wants to use it
unconditionally. Rust is not (yet) a mandatory dep for QEMU.

Regards,
Pierrick


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 3/3] rust/hw/char: add QUP GENI UART device model
  2026-10-27 18:38 ` [PATCH 3/3] rust/hw/char: add QUP GENI UART device model Brian Cain
@ 2026-10-27 19:05   ` Philippe Mathieu-Daudé
  0 siblings, 0 replies; 11+ messages in thread
From: Philippe Mathieu-Daudé @ 2026-10-27 19:05 UTC (permalink / raw)
  To: Brian Cain, qemu-devel
  Cc: Manos Pitsidianakis, qemu-rust, Paolo Bonzini, Pierrick Bouvier

On 27/10/26 19:38, Brian Cain wrote:
> Add a model of the QUP GENI serial engine in UART mode.
> 
> Supports FIFO, DMA, loopback modes - DMA via memory link.
> 
> Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
> ---
>   MAINTAINERS                                 |    1 +
>   docs/system/device-emulation.rst            |    1 +
>   docs/system/devices/qup.rst                 |   10 +
>   Cargo.lock                                  |   15 +
>   Cargo.toml                                  |    1 +
>   rust/hw/char/Kconfig                        |    4 +
>   rust/hw/char/meson.build                    |    1 +
>   rust/hw/char/qup_geni_uart/Cargo.toml       |   27 +
>   rust/hw/char/qup_geni_uart/meson.build      |   28 +
>   rust/hw/char/qup_geni_uart/src/device.rs    | 1293 +++++++++++++++++++

Too big for me to review (not sure if easily splittable), I'll skip.

>   rust/hw/char/qup_geni_uart/src/lib.rs       |    9 +
>   rust/hw/char/qup_geni_uart/src/registers.rs |  160 +++
>   12 files changed, 1550 insertions(+)
>   create mode 100644 docs/system/devices/qup.rst
>   create mode 100644 rust/hw/char/qup_geni_uart/Cargo.toml
>   create mode 100644 rust/hw/char/qup_geni_uart/meson.build
>   create mode 100644 rust/hw/char/qup_geni_uart/src/device.rs
>   create mode 100644 rust/hw/char/qup_geni_uart/src/lib.rs
>   create mode 100644 rust/hw/char/qup_geni_uart/src/registers.rs



^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 0/3] hw/misc: QUP UART device
  2026-10-27 18:43 ` [PATCH 0/3] hw/misc: QUP UART device Pierrick Bouvier
@ 2026-10-27 20:02   ` Brian Cain
  2026-10-27 20:12     ` Pierrick Bouvier
  0 siblings, 1 reply; 11+ messages in thread
From: Brian Cain @ 2026-10-27 20:02 UTC (permalink / raw)
  To: Pierrick Bouvier, qemu-devel
  Cc: Manos Pitsidianakis, qemu-rust, Paolo Bonzini


On 10/27/2026 1:43 PM, Pierrick Bouvier wrote:
> On 10/27/2026 10:38 AM, Brian Cain wrote:
>> Add the QUP + GENI uart device models.  The QUP is a "Universal Peripheral"
>> present on Qualcomm SoCs, with connections for SPI, UART, I2C, I3C.  For
>> now we're only interested in UART.
>>
>> Will follow up with a v2 including wiring into machines + qtest after
>> https://lore.kernel.org/qemu-devel/20261027143721.4125584-1-brian.cain@oss.qualcomm.com/
>> lands.
>>
>> Brian Cain (3):
>>    hw/misc: add the QUP wrapper device
>>    rust/chardev: fix write errors and add write watches
>>    rust/hw/char: add QUP GENI UART device model
>>
>>   MAINTAINERS                                 |    3 +
>>   docs/system/device-emulation.rst            |    1 +
>>   docs/system/devices/qup.rst                 |   10 +
>>   include/hw/misc/qup-wrapper.h               |   27 +
>>   hw/misc/qup-wrapper.c                       |   90 ++
>>   Cargo.lock                                  |   15 +
>>   Cargo.toml                                  |    1 +
>>   hw/misc/Kconfig                             |    3 +
>>   hw/misc/meson.build                         |    3 +
>>   rust/chardev/src/chardev.rs                 |   82 +-
>>   rust/hw/char/Kconfig                        |    4 +
>>   rust/hw/char/meson.build                    |    1 +
>>   rust/hw/char/qup_geni_uart/Cargo.toml       |   27 +
>>   rust/hw/char/qup_geni_uart/meson.build      |   28 +
>>   rust/hw/char/qup_geni_uart/src/device.rs    | 1293 +++++++++++++++++++
>>   rust/hw/char/qup_geni_uart/src/lib.rs       |    9 +
>>   rust/hw/char/qup_geni_uart/src/registers.rs |  160 +++
>>   17 files changed, 1741 insertions(+), 16 deletions(-)
>>   create mode 100644 docs/system/devices/qup.rst
>>   create mode 100644 include/hw/misc/qup-wrapper.h
>>   create mode 100644 hw/misc/qup-wrapper.c
>>   create mode 100644 rust/hw/char/qup_geni_uart/Cargo.toml
>>   create mode 100644 rust/hw/char/qup_geni_uart/meson.build
>>   create mode 100644 rust/hw/char/qup_geni_uart/src/device.rs
>>   create mode 100644 rust/hw/char/qup_geni_uart/src/lib.rs
>>   create mode 100644 rust/hw/char/qup_geni_uart/src/registers.rs
>>
> Any specific reason to have this implemented in Rust?
>
> Not a bad thing in itself, but it means it's only available with rust
> builds, which can be a problem if a machine defined in C wants to use it
> unconditionally. Rust is not (yet) a mandatory dep for QEMU.

A new device model -> a brand new day -> a new frontier?

It was an interesting challenge and seemed appropriate given the nice 
reference pl011 device.  But I will concede it can be impractical.  
Making this device conditionally enabled in the machine that uses it 
felt kinda "lame" and made me long for a day when it *was* a mandatory 
dependency.

Seeing as how we have a pl011 device in C, maybe it's better to start 
with a C version of the QUP?  If so, that's okay with me.

> Regards,
> Pierrick


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 0/3] hw/misc: QUP UART device
  2026-10-27 20:02   ` Brian Cain
@ 2026-10-27 20:12     ` Pierrick Bouvier
  2026-10-29  0:21       ` Brian Cain
  0 siblings, 1 reply; 11+ messages in thread
From: Pierrick Bouvier @ 2026-10-27 20:12 UTC (permalink / raw)
  To: Brian Cain, qemu-devel; +Cc: Manos Pitsidianakis, qemu-rust, Paolo Bonzini

On 10/27/2026 12:02 PM, Brian Cain wrote:
> 
> On 10/27/2026 1:43 PM, Pierrick Bouvier wrote:
>> On 10/27/2026 10:38 AM, Brian Cain wrote:
>>> Add the QUP + GENI uart device models.  The QUP is a "Universal
>>> Peripheral"
>>> present on Qualcomm SoCs, with connections for SPI, UART, I2C, I3C.  For
>>> now we're only interested in UART.
>>>
>>> Will follow up with a v2 including wiring into machines + qtest after
>>> https://lore.kernel.org/qemu-devel/20261027143721.4125584-1-
>>> brian.cain@oss.qualcomm.com/
>>> lands.
>>>
>>> Brian Cain (3):
>>>    hw/misc: add the QUP wrapper device
>>>    rust/chardev: fix write errors and add write watches
>>>    rust/hw/char: add QUP GENI UART device model
>>>
>>>   MAINTAINERS                                 |    3 +
>>>   docs/system/device-emulation.rst            |    1 +
>>>   docs/system/devices/qup.rst                 |   10 +
>>>   include/hw/misc/qup-wrapper.h               |   27 +
>>>   hw/misc/qup-wrapper.c                       |   90 ++
>>>   Cargo.lock                                  |   15 +
>>>   Cargo.toml                                  |    1 +
>>>   hw/misc/Kconfig                             |    3 +
>>>   hw/misc/meson.build                         |    3 +
>>>   rust/chardev/src/chardev.rs                 |   82 +-
>>>   rust/hw/char/Kconfig                        |    4 +
>>>   rust/hw/char/meson.build                    |    1 +
>>>   rust/hw/char/qup_geni_uart/Cargo.toml       |   27 +
>>>   rust/hw/char/qup_geni_uart/meson.build      |   28 +
>>>   rust/hw/char/qup_geni_uart/src/device.rs    | 1293 +++++++++++++++++++
>>>   rust/hw/char/qup_geni_uart/src/lib.rs       |    9 +
>>>   rust/hw/char/qup_geni_uart/src/registers.rs |  160 +++
>>>   17 files changed, 1741 insertions(+), 16 deletions(-)
>>>   create mode 100644 docs/system/devices/qup.rst
>>>   create mode 100644 include/hw/misc/qup-wrapper.h
>>>   create mode 100644 hw/misc/qup-wrapper.c
>>>   create mode 100644 rust/hw/char/qup_geni_uart/Cargo.toml
>>>   create mode 100644 rust/hw/char/qup_geni_uart/meson.build
>>>   create mode 100644 rust/hw/char/qup_geni_uart/src/device.rs
>>>   create mode 100644 rust/hw/char/qup_geni_uart/src/lib.rs
>>>   create mode 100644 rust/hw/char/qup_geni_uart/src/registers.rs
>>>
>> Any specific reason to have this implemented in Rust?
>>
>> Not a bad thing in itself, but it means it's only available with rust
>> builds, which can be a problem if a machine defined in C wants to use it
>> unconditionally. Rust is not (yet) a mandatory dep for QEMU.
> 
> A new device model -> a brand new day -> a new frontier?
> 
> It was an interesting challenge and seemed appropriate given the nice
> reference pl011 device.  But I will concede it can be impractical. 
> Making this device conditionally enabled in the machine that uses it
> felt kinda "lame" and made me long for a day when it *was* a mandatory
> dependency.
> 
> Seeing as how we have a pl011 device in C, maybe it's better to start
> with a C version of the QUP?  If so, that's okay with me.
>

If it's a mandatory device as part of a machine definition, it would
probably be better to stick to C for now.
I hope we can get Rust as a main dep at some point. I'm not sure what is
the criteria to do this change, out of deciding "let's do this".

Regards,
Pierrick


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 2/3] rust/chardev: fix write errors and add write watches
  2026-10-27 18:38 ` [PATCH 2/3] rust/chardev: fix write errors and add write watches Brian Cain
@ 2026-10-28  7:03   ` Philippe Mathieu-Daudé
  0 siblings, 0 replies; 11+ messages in thread
From: Philippe Mathieu-Daudé @ 2026-10-28  7:03 UTC (permalink / raw)
  To: Brian Cain, qemu-devel
  Cc: Manos Pitsidianakis, qemu-rust, Paolo Bonzini, Pierrick Bouvier

On 27/10/26 19:38, Brian Cain wrote:
> qemu_chr_fe_write() reports failure as -1 with the cause in errno, not
> as a negated errno, so errno::into_io_result() turned every failure into
> EPERM and a frontend could never see EAGAIN.  Build the error from
> errno instead, in both CharFrontend::write() and the BQL-free variant.
> 
> Also add CharFrontend::add_write_watch() and remove_watch(), so that a
> device whose backend would block can retry once it is writable again.
> remove_watch() tolerates a source that the backend has already torn
> down.

Please split as 2 distinct patches. With that:

Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>

> Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
> ---
>   rust/chardev/src/chardev.rs | 82 +++++++++++++++++++++++++++++--------
>   1 file changed, 66 insertions(+), 16 deletions(-)



^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 0/3] hw/misc: QUP UART device
  2026-10-27 20:12     ` Pierrick Bouvier
@ 2026-10-29  0:21       ` Brian Cain
  2026-10-29  0:31         ` Pierrick Bouvier
  0 siblings, 1 reply; 11+ messages in thread
From: Brian Cain @ 2026-10-29  0:21 UTC (permalink / raw)
  To: Pierrick Bouvier, qemu-devel
  Cc: Manos Pitsidianakis, qemu-rust, Paolo Bonzini


On 10/27/2026 3:12 PM, Pierrick Bouvier wrote:
> On 10/27/2026 12:02 PM, Brian Cain wrote:
>> On 10/27/2026 1:43 PM, Pierrick Bouvier wrote:
>>> On 10/27/2026 10:38 AM, Brian Cain wrote:
>>>> Add the QUP + GENI uart device models.  The QUP is a "Universal
>>>> Peripheral"
>>>> present on Qualcomm SoCs, with connections for SPI, UART, I2C, I3C.  For
>>>> now we're only interested in UART.
>>>>
>>>> Will follow up with a v2 including wiring into machines + qtest after
>>>> https://lore.kernel.org/qemu-devel/20261027143721.4125584-1-
>>>> brian.cain@oss.qualcomm.com/
>>>> lands.
>>>>
>>>> Brian Cain (3):
>>>>     hw/misc: add the QUP wrapper device
>>>>     rust/chardev: fix write errors and add write watches
>>>>     rust/hw/char: add QUP GENI UART device model
>>>>
>>>>    MAINTAINERS                                 |    3 +
>>>>    docs/system/device-emulation.rst            |    1 +
>>>>    docs/system/devices/qup.rst                 |   10 +
>>>>    include/hw/misc/qup-wrapper.h               |   27 +
>>>>    hw/misc/qup-wrapper.c                       |   90 ++
>>>>    Cargo.lock                                  |   15 +
>>>>    Cargo.toml                                  |    1 +
>>>>    hw/misc/Kconfig                             |    3 +
>>>>    hw/misc/meson.build                         |    3 +
>>>>    rust/chardev/src/chardev.rs                 |   82 +-
>>>>    rust/hw/char/Kconfig                        |    4 +
>>>>    rust/hw/char/meson.build                    |    1 +
>>>>    rust/hw/char/qup_geni_uart/Cargo.toml       |   27 +
>>>>    rust/hw/char/qup_geni_uart/meson.build      |   28 +
>>>>    rust/hw/char/qup_geni_uart/src/device.rs    | 1293 +++++++++++++++++++
>>>>    rust/hw/char/qup_geni_uart/src/lib.rs       |    9 +
>>>>    rust/hw/char/qup_geni_uart/src/registers.rs |  160 +++
>>>>    17 files changed, 1741 insertions(+), 16 deletions(-)
>>>>    create mode 100644 docs/system/devices/qup.rst
>>>>    create mode 100644 include/hw/misc/qup-wrapper.h
>>>>    create mode 100644 hw/misc/qup-wrapper.c
>>>>    create mode 100644 rust/hw/char/qup_geni_uart/Cargo.toml
>>>>    create mode 100644 rust/hw/char/qup_geni_uart/meson.build
>>>>    create mode 100644 rust/hw/char/qup_geni_uart/src/device.rs
>>>>    create mode 100644 rust/hw/char/qup_geni_uart/src/lib.rs
>>>>    create mode 100644 rust/hw/char/qup_geni_uart/src/registers.rs
>>>>
>>> Any specific reason to have this implemented in Rust?
>>>
>>> Not a bad thing in itself, but it means it's only available with rust
>>> builds, which can be a problem if a machine defined in C wants to use it
>>> unconditionally. Rust is not (yet) a mandatory dep for QEMU.
>> A new device model -> a brand new day -> a new frontier?
>>
>> It was an interesting challenge and seemed appropriate given the nice
>> reference pl011 device.  But I will concede it can be impractical.
>> Making this device conditionally enabled in the machine that uses it
>> felt kinda "lame" and made me long for a day when it *was* a mandatory
>> dependency.
>>
>> Seeing as how we have a pl011 device in C, maybe it's better to start
>> with a C version of the QUP?  If so, that's okay with me.
>>
> If it's a mandatory device as part of a machine definition, it would
> probably be better to stick to C for now.
> I hope we can get Rust as a main dep at some point. I'm not sure what is
> the criteria to do this change, out of deciding "let's do this".

Hmm - so ... can I put in a vote for this, then?  Can "at some point" be 
now?  What are the criteria?


> Regards,
> Pierrick


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 0/3] hw/misc: QUP UART device
  2026-10-29  0:21       ` Brian Cain
@ 2026-10-29  0:31         ` Pierrick Bouvier
  0 siblings, 0 replies; 11+ messages in thread
From: Pierrick Bouvier @ 2026-10-29  0:31 UTC (permalink / raw)
  To: Brian Cain, qemu-devel; +Cc: Manos Pitsidianakis, qemu-rust, Paolo Bonzini

On 10/28/2026 4:21 PM, Brian Cain wrote:
> 
> On 10/27/2026 3:12 PM, Pierrick Bouvier wrote:
>> On 10/27/2026 12:02 PM, Brian Cain wrote:
>>> On 10/27/2026 1:43 PM, Pierrick Bouvier wrote:
>>>> On 10/27/2026 10:38 AM, Brian Cain wrote:
>>>>> Add the QUP + GENI uart device models.  The QUP is a "Universal
>>>>> Peripheral"
>>>>> present on Qualcomm SoCs, with connections for SPI, UART, I2C,
>>>>> I3C.  For
>>>>> now we're only interested in UART.
>>>>>
>>>>> Will follow up with a v2 including wiring into machines + qtest after
>>>>> https://lore.kernel.org/qemu-devel/20261027143721.4125584-1-
>>>>> brian.cain@oss.qualcomm.com/
>>>>> lands.
>>>>>
>>>>> Brian Cain (3):
>>>>>     hw/misc: add the QUP wrapper device
>>>>>     rust/chardev: fix write errors and add write watches
>>>>>     rust/hw/char: add QUP GENI UART device model
>>>>>
>>>>>    MAINTAINERS                                 |    3 +
>>>>>    docs/system/device-emulation.rst            |    1 +
>>>>>    docs/system/devices/qup.rst                 |   10 +
>>>>>    include/hw/misc/qup-wrapper.h               |   27 +
>>>>>    hw/misc/qup-wrapper.c                       |   90 ++
>>>>>    Cargo.lock                                  |   15 +
>>>>>    Cargo.toml                                  |    1 +
>>>>>    hw/misc/Kconfig                             |    3 +
>>>>>    hw/misc/meson.build                         |    3 +
>>>>>    rust/chardev/src/chardev.rs                 |   82 +-
>>>>>    rust/hw/char/Kconfig                        |    4 +
>>>>>    rust/hw/char/meson.build                    |    1 +
>>>>>    rust/hw/char/qup_geni_uart/Cargo.toml       |   27 +
>>>>>    rust/hw/char/qup_geni_uart/meson.build      |   28 +
>>>>>    rust/hw/char/qup_geni_uart/src/device.rs    | 1293 +++++++++++++
>>>>> ++++++
>>>>>    rust/hw/char/qup_geni_uart/src/lib.rs       |    9 +
>>>>>    rust/hw/char/qup_geni_uart/src/registers.rs |  160 +++
>>>>>    17 files changed, 1741 insertions(+), 16 deletions(-)
>>>>>    create mode 100644 docs/system/devices/qup.rst
>>>>>    create mode 100644 include/hw/misc/qup-wrapper.h
>>>>>    create mode 100644 hw/misc/qup-wrapper.c
>>>>>    create mode 100644 rust/hw/char/qup_geni_uart/Cargo.toml
>>>>>    create mode 100644 rust/hw/char/qup_geni_uart/meson.build
>>>>>    create mode 100644 rust/hw/char/qup_geni_uart/src/device.rs
>>>>>    create mode 100644 rust/hw/char/qup_geni_uart/src/lib.rs
>>>>>    create mode 100644 rust/hw/char/qup_geni_uart/src/registers.rs
>>>>>
>>>> Any specific reason to have this implemented in Rust?
>>>>
>>>> Not a bad thing in itself, but it means it's only available with rust
>>>> builds, which can be a problem if a machine defined in C wants to
>>>> use it
>>>> unconditionally. Rust is not (yet) a mandatory dep for QEMU.
>>> A new device model -> a brand new day -> a new frontier?
>>>
>>> It was an interesting challenge and seemed appropriate given the nice
>>> reference pl011 device.  But I will concede it can be impractical.
>>> Making this device conditionally enabled in the machine that uses it
>>> felt kinda "lame" and made me long for a day when it *was* a mandatory
>>> dependency.
>>>
>>> Seeing as how we have a pl011 device in C, maybe it's better to start
>>> with a C version of the QUP?  If so, that's okay with me.
>>>
>> If it's a mandatory device as part of a machine definition, it would
>> probably be better to stick to C for now.
>> I hope we can get Rust as a main dep at some point. I'm not sure what is
>> the criteria to do this change, out of deciding "let's do this".
> 
> Hmm - so ... can I put in a vote for this, then?  Can "at some point" be
> now?  What are the criteria?
>

@Manos, Paolo: any idea on this?
Could 12.0 be the first version to make rust a mandatory dep?

Regards,
Pierrick


^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-10-29  0:31 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-27 18:38 [PATCH 0/3] hw/misc: QUP UART device Brian Cain
2026-10-27 18:38 ` [PATCH 1/3] hw/misc: add the QUP wrapper device Brian Cain
2026-10-27 18:38 ` [PATCH 2/3] rust/chardev: fix write errors and add write watches Brian Cain
2026-10-28  7:03   ` Philippe Mathieu-Daudé
2026-10-27 18:38 ` [PATCH 3/3] rust/hw/char: add QUP GENI UART device model Brian Cain
2026-10-27 19:05   ` Philippe Mathieu-Daudé
2026-10-27 18:43 ` [PATCH 0/3] hw/misc: QUP UART device Pierrick Bouvier
2026-10-27 20:02   ` Brian Cain
2026-10-27 20:12     ` Pierrick Bouvier
2026-10-29  0:21       ` Brian Cain
2026-10-29  0:31         ` Pierrick Bouvier

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.