[PATCH 4/5] hw/gpio/npcm8xx: Implement SIOX (SPGIO) device for NPCM without input pin logic

Coco Li posted 5 patches 2 days, 17 hours ago
Maintainers: Tyrone Ting <kfting@nuvoton.com>, Hao Wu <wuhaotsh@google.com>, Peter Maydell <peter.maydell@linaro.org>, "Cédric Le Goater" <clg@kaod.org>, Steven Lee <steven_lee@aspeedtech.com>, Troy Lee <leetroy@gmail.com>, Jamin Lin <jamin_lin@aspeedtech.com>, Andrew Jeffery <andrew@codeconstruct.com.au>, Joel Stanley <joel@jms.id.au>, Markus Armbruster <armbru@redhat.com>, Fabiano Rosas <farosas@suse.de>, Laurent Vivier <lvivier@redhat.com>, Paolo Bonzini <pbonzini@redhat.com>
[PATCH 4/5] hw/gpio/npcm8xx: Implement SIOX (SPGIO) device for NPCM without input pin logic
Posted by Coco Li 2 days, 17 hours ago
Signed-off-by: Coco Li <lixiaoyan@google.com>
Reviewed-by: Hao Wu <wuhaotsh@google.com>
---
 hw/arm/npcm8xx.c                 |  23 +-
 hw/gpio/meson.build              |   1 +
 hw/gpio/npcm8xx_sgpio.c          | 425 +++++++++++++++++++++++++++++++
 hw/gpio/trace-events             |   4 +
 include/hw/arm/npcm8xx.h         |   2 +
 include/hw/gpio/npcm8xx_sgpio.h  |  45 ++++
 tests/qtest/meson.build          |   3 +-
 tests/qtest/npcm8xx_sgpio-test.c | 100 ++++++++
 8 files changed, 600 insertions(+), 3 deletions(-)
 create mode 100644 hw/gpio/npcm8xx_sgpio.c
 create mode 100644 include/hw/gpio/npcm8xx_sgpio.h
 create mode 100644 tests/qtest/npcm8xx_sgpio-test.c

diff --git a/hw/arm/npcm8xx.c b/hw/arm/npcm8xx.c
index 10887d07fa..ae72c6d54b 100644
--- a/hw/arm/npcm8xx.c
+++ b/hw/arm/npcm8xx.c
@@ -369,6 +369,11 @@ static const struct {
     },
 };
 
+static const hwaddr npcm8xx_sgpio_addr[] = {
+    0xf0101000,
+    0xf0102000,
+};
+
 static const struct {
     const char *name;
     hwaddr regs_addr;
@@ -474,6 +479,11 @@ static void npcm8xx_init(Object *obj)
     }
 
 
+    for (i = 0; i < ARRAY_SIZE(s->sgpio); i++) {
+        object_initialize_child(obj, "sgpio[*]",
+                                &s->sgpio[i], TYPE_NPCM8XX_SGPIO);
+    }
+
     for (i = 0; i < ARRAY_SIZE(s->smbus); i++) {
         object_initialize_child(obj, "smbus[*]", &s->smbus[i],
                                 TYPE_NPCM7XX_SMBUS);
@@ -671,6 +681,17 @@ static void npcm8xx_realize(DeviceState *dev, Error **errp)
                            npcm8xx_irq(s, NPCM8XX_GPIO0_IRQ + i));
     }
 
+    /* Serial SIOX modules. Cannot fail. */
+    QEMU_BUILD_BUG_ON(ARRAY_SIZE(npcm8xx_sgpio_addr) != ARRAY_SIZE(s->sgpio));
+    for (i = 0; i < ARRAY_SIZE(s->sgpio); i++) {
+        Object *obj = OBJECT(&s->sgpio[i]);
+
+        sysbus_realize(SYS_BUS_DEVICE(obj), &error_abort);
+        sysbus_mmio_map(SYS_BUS_DEVICE(obj), 0, npcm8xx_sgpio_addr[i]);
+        sysbus_connect_irq(SYS_BUS_DEVICE(obj), 0,
+                           npcm8xx_irq(s, NPCM8XX_SIOX0_IRQ + i));
+    }
+
     /* SMBus modules. Cannot fail. */
     QEMU_BUILD_BUG_ON(ARRAY_SIZE(npcm8xx_smbus_addr) != ARRAY_SIZE(s->smbus));
     for (i = 0; i < ARRAY_SIZE(s->smbus); i++) {
@@ -818,8 +839,6 @@ static void npcm8xx_realize(DeviceState *dev, Error **errp)
     create_unimplemented_device("npcm8xx.bt",           0xf0030000,   4 * KiB);
     create_unimplemented_device("npcm8xx.espi",         0xf009f000,   4 * KiB);
     create_unimplemented_device("npcm8xx.peci",         0xf0100000,   4 * KiB);
-    create_unimplemented_device("npcm8xx.siox[1]",      0xf0101000,   4 * KiB);
-    create_unimplemented_device("npcm8xx.siox[2]",      0xf0102000,   4 * KiB);
     create_unimplemented_device("npcm8xx.tmps",         0xf0188000,   4 * KiB);
     create_unimplemented_device("npcm8xx.viru1",        0xf0204000,   4 * KiB);
     create_unimplemented_device("npcm8xx.viru2",        0xf0205000,   4 * KiB);
diff --git a/hw/gpio/meson.build b/hw/gpio/meson.build
index 74840619c0..8a3879983c 100644
--- a/hw/gpio/meson.build
+++ b/hw/gpio/meson.build
@@ -18,3 +18,4 @@ system_ss.add(when: 'CONFIG_STM32L4X5_SOC', if_true: files('stm32l4x5_gpio.c'))
 system_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: files('aspeed_gpio.c'))
 system_ss.add(when: 'CONFIG_SIFIVE_GPIO', if_true: files('sifive_gpio.c'))
 system_ss.add(when: 'CONFIG_PCF8574', if_true: files('pcf8574.c'))
+system_ss.add(when: 'CONFIG_NPCM8XX', if_true: files('npcm8xx_sgpio.c'))
diff --git a/hw/gpio/npcm8xx_sgpio.c b/hw/gpio/npcm8xx_sgpio.c
new file mode 100644
index 0000000000..f7d6bbf672
--- /dev/null
+++ b/hw/gpio/npcm8xx_sgpio.c
@@ -0,0 +1,425 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * Nuvoton Serial I/O EXPANSION INTERFACE (SOIX)
+ *
+ * Copyright 2025 Google LLC
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include "qemu/osdep.h"
+
+#include "hw/gpio/npcm8xx_sgpio.h"
+#include "hw/irq.h"
+#include "hw/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "qemu/units.h"
+#include "trace.h"
+
+#define NPCM8XX_SGPIO_RD_MODE_MASK      0x6
+#define NPCM8XX_SGPIO_RD_MODE_PERIODIC  0x4
+#define NPCM8XX_SGPIO_RD_MODE_ON_DEMAND 0x0
+#define NPCM8XX_SGPIO_IOXCTS_IOXIF_EN   BIT(7)
+#define NPCM8XX_SGPIO_IOXCTS_WR_PEND    BIT(6)
+#define NPCM8XX_SGPIO_IOXCTS_DATA16W    BIT(3)
+#define NPCM8XX_SGPIO_REGS_SIZE         (4 * KiB)
+
+#define  NPCM8XX_SGPIO_IXOEVCFG_FALLING BIT(1)
+#define  NPCM8XX_SGPIO_IXOEVCFG_RISING  BIT(0)
+#define  NPCM8XX_SGPIO_IXOEVCFG_BOTH    (NPCM8XX_SGPIO_IXOEVCFG_FALLING | \
+                                         NPCM8XX_SGPIO_IXOEVCFG_RISING)
+
+#define  IXOEVCFG_MASK 0x3
+
+/* 8-bit register indices, with the event config registers being 16-bit */
+enum NPCM8xxSGPIORegister {
+    NPCM8XX_SGPIO_XDOUT0,
+    NPCM8XX_SGPIO_XDOUT1,
+    NPCM8XX_SGPIO_XDOUT2,
+    NPCM8XX_SGPIO_XDOUT3,
+    NPCM8XX_SGPIO_XDOUT4,
+    NPCM8XX_SGPIO_XDOUT5,
+    NPCM8XX_SGPIO_XDOUT6,
+    NPCM8XX_SGPIO_XDOUT7,
+    NPCM8XX_SGPIO_XDIN0,
+    NPCM8XX_SGPIO_XDIN1,
+    NPCM8XX_SGPIO_XDIN2,
+    NPCM8XX_SGPIO_XDIN3,
+    NPCM8XX_SGPIO_XDIN4,
+    NPCM8XX_SGPIO_XDIN5,
+    NPCM8XX_SGPIO_XDIN6,
+    NPCM8XX_SGPIO_XDIN7,
+    NPCM8XX_SGPIO_XEVCFG0 = 0x10,
+    NPCM8XX_SGPIO_XEVCFG1 = 0x12,
+    NPCM8XX_SGPIO_XEVCFG2 = 0x14,
+    NPCM8XX_SGPIO_XEVCFG3 = 0x16,
+    NPCM8XX_SGPIO_XEVCFG4 = 0x18,
+    NPCM8XX_SGPIO_XEVCFG5 = 0x1a,
+    NPCM8XX_SGPIO_XEVCFG6 = 0x1c,
+    NPCM8XX_SGPIO_XEVCFG7 = 0x1e,
+    NPCM8XX_SGPIO_XEVSTS0 = 0x20,
+    NPCM8XX_SGPIO_XEVSTS1,
+    NPCM8XX_SGPIO_XEVSTS2,
+    NPCM8XX_SGPIO_XEVSTS3,
+    NPCM8XX_SGPIO_XEVSTS4,
+    NPCM8XX_SGPIO_XEVSTS5,
+    NPCM8XX_SGPIO_XEVSTS6,
+    NPCM8XX_SGPIO_XEVSTS7,
+    NPCM8XX_SGPIO_IOXCTS,
+    NPCM8XX_SGPIO_IOXINDR,
+    NPCM8XX_SGPIO_IOXCFG1,
+    NPCM8XX_SGPIO_IOXCFG2,
+    NPCM8XX_SGPIO_IOXDATR = 0x2d,
+    NPCM8XX_SGPIO_REGS_END,
+};
+
+static uint8_t npcm8xx_sgpio_get_in_port(NPCM8xxSGPIOState *s)
+{
+    uint8_t p;
+
+    p = s->regs[NPCM8XX_SGPIO_IOXCFG2] & 0xf;
+    if (p > 8) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                     "%s: Trying to set more the allowed input ports %d\n",
+                     DEVICE(s)->canonical_path, p);
+    }
+
+    return p;
+}
+
+static uint8_t npcm8xx_sgpio_get_out_port(NPCM8xxSGPIOState *s)
+{
+    uint8_t p;
+
+    p = (s->regs[NPCM8XX_SGPIO_IOXCFG2] >> 4) & 0xf;
+    if (p > 8) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                     "%s: Trying to set more the allowed output ports %d\n",
+                     DEVICE(s)->canonical_path, p);
+    }
+
+    return p;
+}
+
+static bool npcm8xx_sgpio_is_16bit(NPCM8xxSGPIOState *s)
+{
+    return s->regs[NPCM8XX_SGPIO_IOXCTS] & NPCM8XX_SGPIO_IOXCTS_DATA16W;
+}
+
+static uint64_t npcm8xx_sgpio_regs_read_with_cfg(NPCM8xxSGPIOState *s,
+                                                 hwaddr reg)
+{
+    bool rd_word = npcm8xx_sgpio_is_16bit(s);
+    uint64_t value;
+
+    if (rd_word) {
+        value = ((uint16_t)s->regs[reg] << 8) | s->regs[reg + 1];
+    } else {
+        value = s->regs[reg];
+    }
+
+    return value;
+}
+
+static void npcm8xx_sgpio_update_event(NPCM8xxSGPIOState *s, uint64_t diff)
+{
+    /* TODO in upcoming patch */
+}
+
+static void npcm8xx_sgpio_update_pins_in(NPCM8xxSGPIOState *s, uint64_t diff)
+{
+    /* TODO in upcoming patch */
+}
+
+static void npcm8xx_sgpio_update_pins_out(NPCM8xxSGPIOState *s, hwaddr reg)
+{
+    uint8_t nout, dout;
+
+    if (~(s->regs[NPCM8XX_SGPIO_IOXCTS] & NPCM8XX_SGPIO_IOXCTS_IOXIF_EN)) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: Device disabled, transaction out aborted\n",
+                      DEVICE(s)->canonical_path);
+    }
+
+    nout = npcm8xx_sgpio_get_out_port(s);
+    dout = reg - NPCM8XX_SGPIO_XDOUT0;
+    if (dout >= nout) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: Accessing XDOUT%d when NOUT is %d\n",
+                      DEVICE(s)->canonical_path, dout, nout);
+    }
+    s->pin_out_level[dout] = s->regs[NPCM8XX_SGPIO_XDOUT0 + dout];
+    /* unset WR_PEND */
+    s->regs[NPCM8XX_SGPIO_IOXCTS] &= ~0x40;
+}
+
+static uint64_t npcm8xx_sgpio_regs_read(void *opaque, hwaddr addr,
+                                       unsigned int size)
+{
+    NPCM8xxSGPIOState *s = opaque;
+    uint8_t rd_mode = s->regs[NPCM8XX_SGPIO_IOXCTS] &
+                      NPCM8XX_SGPIO_RD_MODE_MASK;
+    hwaddr reg = addr / sizeof(uint8_t);
+    uint8_t nout, nin, din, dout;
+    uint64_t value = 0;
+
+    switch (reg) {
+    case NPCM8XX_SGPIO_XDOUT0 ... NPCM8XX_SGPIO_XDOUT7:
+        nout = npcm8xx_sgpio_get_out_port(s);
+        dout = reg - NPCM8XX_SGPIO_XDOUT0;
+
+        if (dout >= nout) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: Accessing XDOUT%d when NOUT is %d\n",
+                          DEVICE(s)->canonical_path, dout, nout);
+            break;
+        }
+
+        value = npcm8xx_sgpio_regs_read_with_cfg(s, reg);
+        break;
+
+    case NPCM8XX_SGPIO_XDIN0 ... NPCM8XX_SGPIO_XDIN7:
+        nin = npcm8xx_sgpio_get_in_port(s);
+        din = reg - NPCM8XX_SGPIO_XDIN0;
+
+        if (din >= nin) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: Accessing XDIN%d when NIN is %d\n",
+                          DEVICE(s)->canonical_path, din, nin);
+            break;
+        }
+
+        switch (rd_mode) {
+        case NPCM8XX_SGPIO_RD_MODE_PERIODIC:
+            /* XDIN are up-to-date from scanning, return directly. */
+            value = npcm8xx_sgpio_regs_read_with_cfg(s, reg);
+            break;
+        case NPCM8XX_SGPIO_RD_MODE_ON_DEMAND:
+            /*
+             * IOX_SCAN write behavior is unimplemented.
+             * Event generation is also umimplemented.
+             */
+            qemu_log_mask(LOG_UNIMP,
+                        "%s: On Demand with Polling reading mode is not implemented.\n",
+                        __func__);
+            break;
+        default:
+            qemu_log_mask(LOG_GUEST_ERROR, "%s: Unknown read mode\n", __func__);
+        }
+        break;
+
+    case NPCM8XX_SGPIO_XEVCFG0 ... NPCM8XX_SGPIO_XEVCFG7:
+        value = ((uint64_t)s->regs[reg] << 8) | s->regs[reg + 1];
+        break;
+
+    case NPCM8XX_SGPIO_XEVSTS0 ... NPCM8XX_SGPIO_XEVSTS7:
+        value = npcm8xx_sgpio_regs_read_with_cfg(s, reg);
+        break;
+
+    case NPCM8XX_SGPIO_IOXCTS ... NPCM8XX_SGPIO_IOXDATR:
+        value = s->regs[reg];
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: read from invalid offset 0x%" HWADDR_PRIx "\n",
+                      DEVICE(s)->canonical_path, addr);
+        break;
+    }
+
+    trace_npcm8xx_sgpio_read(DEVICE(s)->canonical_path, addr, value);
+
+    return value;
+}
+
+static void npcm8xx_sgpio_regs_write(void *opaque, hwaddr addr, uint64_t v,
+                                    unsigned int size)
+{
+    hwaddr reg = addr / sizeof(uint8_t);
+    uint8_t hi_val = (uint8_t)(v >> 8);
+    NPCM8xxSGPIOState *s = opaque;
+    uint8_t value = (uint8_t) v;
+    uint8_t diff;
+
+    trace_npcm8xx_sgpio_write(DEVICE(s)->canonical_path, addr, v);
+
+    switch (reg) {
+    case NPCM8XX_SGPIO_XDOUT0 ... NPCM8XX_SGPIO_XDOUT7:
+        /* Set WR_PEND bit */
+        s->regs[NPCM8XX_SGPIO_IOXCTS] |= 0x40;
+        if (npcm8xx_sgpio_is_16bit(s)) {
+            if ((reg - NPCM8XX_SGPIO_XDOUT0) % 2) {
+                qemu_log_mask(LOG_GUEST_ERROR,
+                              "%s: write unaligned 16 bit register @ 0x%"
+                              HWADDR_PRIx "\n",
+                              DEVICE(s)->canonical_path, addr);
+                break;
+            }
+            s->regs[reg] = hi_val;
+            s->regs[reg + 1] = value;
+            npcm8xx_sgpio_update_pins_out(s, reg + 1);
+        } else {
+            s->regs[reg] = value;
+        }
+        npcm8xx_sgpio_update_pins_out(s, reg);
+        break;
+
+    /*  2 byte long regs */
+    case NPCM8XX_SGPIO_XEVCFG0 ... NPCM8XX_SGPIO_XEVCFG7:
+        if (~(s->regs[NPCM8XX_SGPIO_IOXCTS] & NPCM8XX_SGPIO_IOXCTS_IOXIF_EN)) {
+            s->regs[reg] = hi_val;
+            s->regs[reg + 1] = value;
+        }
+        break;
+
+    case NPCM8XX_SGPIO_XEVSTS0 ... NPCM8XX_SGPIO_XEVSTS7:
+        if (npcm8xx_sgpio_is_16bit(s)) {
+            if ((reg - NPCM8XX_SGPIO_XEVSTS0) % 2) {
+                qemu_log_mask(LOG_GUEST_ERROR,
+                            "%s: write unaligned 16 bit register @ 0x%"
+                            HWADDR_PRIx "\n",
+                            DEVICE(s)->canonical_path, addr);
+                break;
+            }
+            s->regs[reg] ^= hi_val;
+            s->regs[reg + 1] ^= value;
+            npcm8xx_sgpio_update_event(s, 0);
+        } else {
+            s->regs[reg] ^= value;
+            npcm8xx_sgpio_update_event(s, 0);
+        }
+        break;
+
+    case NPCM8XX_SGPIO_IOXCTS:
+        /* Make sure RO bit WR_PEND is not written to */
+        value &= ~NPCM8XX_SGPIO_IOXCTS_WR_PEND;
+        diff = s->regs[reg] ^ value;
+        s->regs[reg] = value;
+        if ((s->regs[NPCM8XX_SGPIO_IOXCTS] & NPCM8XX_SGPIO_IOXCTS_IOXIF_EN) &&
+            (diff & NPCM8XX_SGPIO_RD_MODE_MASK)) {
+            /* reset RD_MODE if attempting to write with IOXIF_EN enabled */
+            s->regs[reg] ^= (diff & NPCM8XX_SGPIO_RD_MODE_MASK);
+        }
+        break;
+
+    case NPCM8XX_SGPIO_IOXINDR:
+        /*
+         * Only relevant to SIOX1.
+         * HSIOX unimplemented for both, set value and do nothing.
+         */
+        s->regs[reg] = value;
+        break;
+
+    case NPCM8XX_SGPIO_IOXCFG1:
+    case NPCM8XX_SGPIO_IOXCFG2:
+        if (~(s->regs[NPCM8XX_SGPIO_IOXCTS] & NPCM8XX_SGPIO_IOXCTS_IOXIF_EN)) {
+            s->regs[reg] = value;
+        } else {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                    "%s: trying to write to register @ 0x%"
+                    HWADDR_PRIx "while IOXIF_EN is enabled\n",
+                    DEVICE(s)->canonical_path, addr);
+        }
+        break;
+
+    case NPCM8XX_SGPIO_XDIN0 ... NPCM8XX_SGPIO_XDIN7:
+    case NPCM8XX_SGPIO_IOXDATR:
+        /* IOX_SCAN is unimplemented given no on-demand mode */
+        qemu_log_mask(LOG_GUEST_ERROR,
+                    "%s: write to read-only register @ 0x%" HWADDR_PRIx "\n",
+                    DEVICE(s)->canonical_path, addr);
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: write to invalid offset 0x%" HWADDR_PRIx "\n",
+                      DEVICE(s)->canonical_path, addr);
+        break;
+    }
+}
+
+static const MemoryRegionOps npcm8xx_sgpio_regs_ops = {
+    .read = npcm8xx_sgpio_regs_read,
+    .write = npcm8xx_sgpio_regs_write,
+    .endianness = DEVICE_NATIVE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 2,
+        .unaligned = false,
+    },
+};
+
+static void npcm8xx_sgpio_enter_reset(Object *obj, ResetType type)
+{
+    NPCM8xxSGPIOState *s = NPCM8XX_SGPIO(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+}
+
+static void npcm8xx_sgpio_hold_reset(Object *obj, ResetType type)
+{
+    NPCM8xxSGPIOState *s = NPCM8XX_SGPIO(obj);
+
+    npcm8xx_sgpio_update_pins_in(s, -1);
+}
+
+static void npcm8xx_sgpio_init(Object *obj)
+{
+    NPCM8xxSGPIOState *s = NPCM8XX_SGPIO(obj);
+
+    memory_region_init_io(&s->mmio, obj, &npcm8xx_sgpio_regs_ops, s,
+                          "regs", NPCM8XX_SGPIO_REGS_SIZE);
+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
+    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);
+
+    /* TODO: Add input GPIO pins */
+}
+
+static const VMStateDescription vmstate_npcm8xx_sgpio = {
+    .name = "npcm8xx-sgpio",
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT8_ARRAY(pin_in_level, NPCM8xxSGPIOState,
+                            NPCM8XX_SGPIO_NR_PINS / NPCM8XX_SGPIO_MAX_PORTS),
+        VMSTATE_UINT8_ARRAY(pin_out_level, NPCM8xxSGPIOState,
+                            NPCM8XX_SGPIO_NR_PINS / NPCM8XX_SGPIO_MAX_PORTS),
+        VMSTATE_UINT8_ARRAY(regs, NPCM8xxSGPIOState, NPCM8XX_SGPIO_NR_REGS),
+        VMSTATE_END_OF_LIST(),
+    },
+};
+
+static void npcm8xx_sgpio_class_init(ObjectClass *klass, const void *data)
+{
+    ResettableClass *reset = RESETTABLE_CLASS(klass);
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    QEMU_BUILD_BUG_ON(NPCM8XX_SGPIO_REGS_END > NPCM8XX_SGPIO_NR_REGS);
+
+    dc->desc = "NPCM8xx SIOX Controller";
+    dc->vmsd = &vmstate_npcm8xx_sgpio;
+    reset->phases.enter = npcm8xx_sgpio_enter_reset;
+    reset->phases.hold = npcm8xx_sgpio_hold_reset;
+}
+
+static const TypeInfo npcm8xx_sgpio_types[] = {
+    {
+        .name = TYPE_NPCM8XX_SGPIO,
+        .parent = TYPE_SYS_BUS_DEVICE,
+        .instance_size = sizeof(NPCM8xxSGPIOState),
+        .class_init = npcm8xx_sgpio_class_init,
+        .instance_init = npcm8xx_sgpio_init,
+    },
+};
+DEFINE_TYPES(npcm8xx_sgpio_types);
diff --git a/hw/gpio/trace-events b/hw/gpio/trace-events
index cea896b28f..5a3625cabc 100644
--- a/hw/gpio/trace-events
+++ b/hw/gpio/trace-events
@@ -12,6 +12,10 @@ npcm7xx_gpio_set_input(const char *id, int32_t line, int32_t level) "%s line: %"
 npcm7xx_gpio_set_output(const char *id, int32_t line, int32_t level) "%s line: %" PRIi32 " level: %" PRIi32
 npcm7xx_gpio_update_events(const char *id, uint32_t evst, uint32_t even) "%s evst: 0x%08" PRIx32 " even: 0x%08" PRIx32
 
+# npcm8xx_sgpio.c
+npcm8xx_sgpio_read(const char *id, uint64_t offset, uint64_t value) " %s offset: 0x%04" PRIx64 " value 0x%08" PRIx64
+npcm8xx_sgpio_write(const char *id, uint64_t offset, uint64_t value) " %s offset: 0x%04" PRIx64 " value 0x%08" PRIx64
+
 # nrf51_gpio.c
 nrf51_gpio_read(uint64_t offset, uint64_t r) "offset 0x%" PRIx64 " value 0x%" PRIx64
 nrf51_gpio_write(uint64_t offset, uint64_t value) "offset 0x%" PRIx64 " value 0x%" PRIx64
diff --git a/include/hw/arm/npcm8xx.h b/include/hw/arm/npcm8xx.h
index a8377db490..2d177329b8 100644
--- a/include/hw/arm/npcm8xx.h
+++ b/include/hw/arm/npcm8xx.h
@@ -21,6 +21,7 @@
 #include "hw/cpu/cluster.h"
 #include "hw/gpio/npcm7xx_gpio.h"
 #include "hw/i2c/npcm7xx_smbus.h"
+#include "hw/gpio/npcm8xx_sgpio.h"
 #include "hw/intc/arm_gic_common.h"
 #include "hw/mem/npcm7xx_mc.h"
 #include "hw/misc/npcm_clk.h"
@@ -104,6 +105,7 @@ struct NPCM8xxState {
     NPCMPCSState        pcs;
     NPCM7xxSDHCIState   mmc;
     NPCMPSPIState       pspi;
+    NPCM8xxSGPIOState   sgpio[2];
 };
 
 struct NPCM8xxClass {
diff --git a/include/hw/gpio/npcm8xx_sgpio.h b/include/hw/gpio/npcm8xx_sgpio.h
new file mode 100644
index 0000000000..cce844951e
--- /dev/null
+++ b/include/hw/gpio/npcm8xx_sgpio.h
@@ -0,0 +1,45 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * Nuvoton NPCM8xx General Purpose Input / Output (GPIO)
+ *
+ * Copyright 2025 Google LLC
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+#ifndef NPCM8XX_SGPIO_H
+#define NPCM8XX_SGPIO_H
+
+#include "hw/sysbus.h"
+
+/* Number of pins managed by each controller. */
+#define NPCM8XX_SGPIO_NR_PINS (64)
+
+/*
+ * Number of registers in our device state structure. Don't change this without
+ * incrementing the version_id in the vmstate.
+ */
+#define NPCM8XX_SGPIO_NR_REGS (0x2e)
+#define NPCM8XX_SGPIO_MAX_PORTS 8
+
+typedef struct NPCM8xxSGPIOState {
+    SysBusDevice parent;
+
+    MemoryRegion mmio;
+    qemu_irq irq;
+
+    uint8_t pin_in_level[NPCM8XX_SGPIO_MAX_PORTS];
+    uint8_t pin_out_level[NPCM8XX_SGPIO_MAX_PORTS];
+    uint8_t regs[NPCM8XX_SGPIO_NR_REGS];
+} NPCM8xxSGPIOState;
+
+#define TYPE_NPCM8XX_SGPIO "npcm8xx-sgpio"
+OBJECT_DECLARE_SIMPLE_TYPE(NPCM8xxSGPIOState, NPCM8XX_SGPIO)
+
+#endif /* NPCM8XX_SGPIO_H */
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 669d07c06b..12737ad21f 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -212,7 +212,8 @@ qtests_npcm7xx = \
    'npcm7xx_watchdog_timer-test'] + \
    (slirp.found() ? ['npcm7xx_emc-test'] : [])
 qtests_npcm8xx = \
-  ['npcm_gmac-test']
+  ['npcm_gmac-test',
+   'npcm8xx_sgpio-test',]
 qtests_aspeed = \
   ['aspeed_gpio-test',
    'aspeed_hace-test',
diff --git a/tests/qtest/npcm8xx_sgpio-test.c b/tests/qtest/npcm8xx_sgpio-test.c
new file mode 100644
index 0000000000..b0b11b3481
--- /dev/null
+++ b/tests/qtest/npcm8xx_sgpio-test.c
@@ -0,0 +1,100 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * QTest testcase for the Nuvoton NPCM8xx I/O EXPANSION INTERFACE (SOIX)
+ * modules.
+ *
+ * Copyright 2025 Google LLC
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "qemu/osdep.h"
+#include "libqtest-single.h"
+
+#define NR_SGPIO_DEVICES 8
+#define SGPIO(x)         (0xf0101000 + (x) * 0x1000)
+#define SGPIO_IRQ(x)     (19 + (x))
+
+/* SGPIO registers */
+#define GP_N_XDOUT(x)   (0x00 + x)
+#define GP_N_XDIN(x)    (0x08 + x)
+#define GP_N_XEVCFG(x)  (0x10 + (x) * 0x2)
+#define GP_N_XEVSTS(x)  (0x20 + x)
+#define GP_N_IOXCTS     0x28
+#define GP_N_IOXINDR    0x29
+#define GP_N_IOXCFG1    0x2a
+#define GP_N_IOXCFG2    0x2b
+#define GP_N_RD_MODE_PERIODIC  0x4
+#define GP_N_IOXIF_EN  0x80
+
+
+/* Restore the SGPIO controller to a sensible default state. */
+static void sgpio_reset(int n)
+{
+    int i;
+
+    for (i = 0; i < NR_SGPIO_DEVICES; ++i) {
+        writel(SGPIO(n) + GP_N_XDOUT(i), 0x00000000);
+        writel(SGPIO(n) + GP_N_XEVCFG(i), 0x00000000);
+        writel(SGPIO(n) + GP_N_XEVSTS(i), 0x00000000);
+    }
+    writel(SGPIO(n) + GP_N_IOXCTS, 0x00000000);
+    writel(SGPIO(n) + GP_N_IOXINDR, 0x00000000);
+    writel(SGPIO(n) + GP_N_IOXCFG1, 0x00000000);
+    writel(SGPIO(n) + GP_N_IOXCFG2, 0x00000000);
+}
+
+static void test_read_dout_byte(void)
+{
+    int i;
+
+    sgpio_reset(0);
+
+    /* set all 8 output devices */
+    writel(SGPIO(0) + GP_N_IOXCFG2, NR_SGPIO_DEVICES << 4);
+    for (i = 0; i < NR_SGPIO_DEVICES; ++i) {
+        writel(SGPIO(0) + GP_N_XDOUT(i), 0xff);
+        g_assert_cmphex(readb(SGPIO(0) + GP_N_XDOUT(i)), ==, 0xff);
+    }
+}
+
+static void test_read_dout_word(void)
+{
+    int i;
+
+    sgpio_reset(0);
+    /* set all 8 output devices */
+    writel(SGPIO(0) + GP_N_IOXCFG2, NR_SGPIO_DEVICES << 4);
+    /* set 16 bit aligned access */
+    writel(SGPIO(0) + GP_N_IOXCTS, 1 << 3);
+    for (i = 0; i < NR_SGPIO_DEVICES / 2; ++i) {
+        writel(SGPIO(0) + GP_N_XDOUT(i * 2), 0xf0f0);
+        g_assert_cmphex(readw(SGPIO(0) + GP_N_XDOUT(i * 2)), ==, 0xf0f0);
+    }
+}
+
+int main(int argc, char **argv)
+{
+    int ret;
+
+    g_test_init(&argc, &argv, NULL);
+    g_test_set_nonfatal_assertions();
+
+    qtest_add_func("/npcm8xx_sgpio/read_dout_byte", test_read_dout_byte);
+    qtest_add_func("/npcm8xx_sgpio/read_dout_word", test_read_dout_word);
+
+    qtest_start("-machine npcm845-evb");
+    qtest_irq_intercept_in(global_qtest, "/machine/soc/sgpio");
+    ret = g_test_run();
+    qtest_end();
+
+    return ret;
+}
-- 
2.51.0.338.gd7d06c2dae-goog