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
© 2016 - 2025 Red Hat, Inc.