Model the Synopsys DW_apb_gpio controller on the Kendryte K230: direction, 
output, per-pin edge/level interrupts with EOI and output loopback in 
EXT_PORTA. Each pin drives its own PLIC line. Register layout from the K230 TRM 
v0.3.1. Covered by tests/qtest/k230-gpio-test.

Signed-off-by: Leo Cheng <[email protected]>
---
 hw/gpio/Kconfig              |   3 +
 hw/gpio/k230_gpio.c          | 270 +++++++++++++++++++++++++++++++++++
 hw/gpio/meson.build          |   1 +
 hw/gpio/trace-events         |   4 +
 include/hw/gpio/k230_gpio.h  |  68 +++++++++
 tests/qtest/k230-gpio-test.c | 143 +++++++++++++++++++
 6 files changed, 489 insertions(+)
 create mode 100644 hw/gpio/k230_gpio.c
 create mode 100644 include/hw/gpio/k230_gpio.h
 create mode 100644 tests/qtest/k230-gpio-test.c

diff --git a/hw/gpio/Kconfig b/hw/gpio/Kconfig
index a209294..07ef5a2 100644
--- a/hw/gpio/Kconfig
+++ b/hw/gpio/Kconfig
@@ -30,3 +30,6 @@ config PCF8574
 
 config ZAURUS_SCOOP
     bool
+
+config K230_GPIO
+    bool
diff --git a/hw/gpio/k230_gpio.c b/hw/gpio/k230_gpio.c
new file mode 100644
index 0000000..da4448f
--- /dev/null
+++ b/hw/gpio/k230_gpio.c
@@ -0,0 +1,270 @@
+/*
+ * K230 GPIO controller (Synopsys DW_apb_gpio) compatible with Kendryte K230 
SDK
+ *
+ * Copyright (c) 2026 Leo Cheng <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * 
https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ *
+ * For more information, see <https://www.kendryte.com/en/proDetail/230>
+ */
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/core/irq.h"
+#include "migration/vmstate.h"
+#include "hw/gpio/k230_gpio.h"
+#include "trace.h"
+
+/* Live pin levels: output pins reflect the driven value, inputs the source. */
+static uint32_t k230_gpio_pin_level(K230GpioState *s)
+{
+    return (s->in & ~s->ddr) | (s->dr & s->ddr);
+}
+
+static void k230_gpio_update(K230GpioState *s, uint32_t old_level)
+{
+    uint32_t level = k230_gpio_pin_level(s);
+
+    for (int i = 0; i < K230_GPIO_PINS; i++) {
+        uint32_t bit = 1u << i;
+
+        if (!(s->inten & bit)) {
+            continue;
+        }
+
+        bool active = (s->int_polarity & bit) ? (level & bit) : !(level & bit);
+
+        if (s->inttype_level & bit) {
+            bool was = (s->int_polarity & bit) ? (old_level & bit)
+                                               : !(old_level & bit);
+            if (active && !was) {
+                s->raw_intstatus |= bit;
+            }
+        } else if (active) {
+            s->raw_intstatus |= bit;
+        } else {
+            s->raw_intstatus &= ~bit;
+        }
+    }
+
+    s->intstatus = s->raw_intstatus & ~s->intmask;
+
+    for (int i = 0; i < K230_GPIO_PINS; i++) {
+        qemu_set_irq(s->irq[i], (s->intstatus >> i) & 1);
+    }
+}
+
+static void k230_gpio_set_input(void *opaque, int line, int level)
+{
+    K230GpioState *s = K230_GPIO(opaque);
+    uint32_t old = k230_gpio_pin_level(s);
+
+    if (level) {
+        s->in |= (1u << line);
+    } else {
+        s->in &= ~(1u << line);
+    }
+    k230_gpio_update(s, old);
+}
+
+static uint64_t k230_gpio_read(void *opaque, hwaddr addr, unsigned int size)
+{
+    K230GpioState *s = K230_GPIO(opaque);
+    uint32_t value = 0;
+
+    switch (addr) {
+    case K230_GPIO_SWPORTA_DR:
+        value = s->dr;
+        break;
+    case K230_GPIO_SWPORTA_DDR:
+        value = s->ddr;
+        break;
+    case K230_GPIO_SWPORTA_CTL:
+        value = s->ctl;
+        break;
+    case K230_GPIO_INTEN:
+        value = s->inten;
+        break;
+    case K230_GPIO_INTMASK:
+        value = s->intmask;
+        break;
+    case K230_GPIO_INTTYPE_LEVEL:
+        value = s->inttype_level;
+        break;
+    case K230_GPIO_INT_POLARITY:
+        value = s->int_polarity;
+        break;
+    case K230_GPIO_INTSTATUS:
+        value = s->intstatus;
+        break;
+    case K230_GPIO_RAW_INTSTATUS:
+        value = s->raw_intstatus;
+        break;
+    case K230_GPIO_DEBOUNCE:
+        value = s->debounce;
+        break;
+    case K230_GPIO_EXT_PORTA:
+        value = k230_gpio_pin_level(s);
+        break;
+    case K230_GPIO_LS_SYNC:
+        value = s->ls_sync;
+        break;
+    case K230_GPIO_VER_ID_CODE:
+        value = K230_GPIO_VER_ID_VALUE;
+        break;
+    case K230_GPIO_CONFIG_REG2:
+        /* Port A width - 1 in bits [4:0]; single 32-bit port. */
+        value = K230_GPIO_PINS - 1;
+        break;
+    default:
+        break;
+    }
+
+    trace_k230_gpio_read(addr, value);
+    return value;
+}
+
+static void k230_gpio_write(void *opaque, hwaddr addr,
+                            uint64_t value, unsigned int size)
+{
+    K230GpioState *s = K230_GPIO(opaque);
+    uint32_t old = k230_gpio_pin_level(s);
+
+    trace_k230_gpio_write(addr, value);
+
+    switch (addr) {
+    case K230_GPIO_SWPORTA_DR:
+        s->dr = value;
+        k230_gpio_update(s, old);
+        break;
+    case K230_GPIO_SWPORTA_DDR:
+        s->ddr = value;
+        k230_gpio_update(s, old);
+        break;
+    case K230_GPIO_SWPORTA_CTL:
+        s->ctl = value;
+        break;
+    case K230_GPIO_INTEN:
+        s->inten = value;
+        k230_gpio_update(s, old);
+        break;
+    case K230_GPIO_INTMASK:
+        s->intmask = value;
+        k230_gpio_update(s, old);
+        break;
+    case K230_GPIO_INTTYPE_LEVEL:
+        s->inttype_level = value;
+        k230_gpio_update(s, old);
+        break;
+    case K230_GPIO_INT_POLARITY:
+        s->int_polarity = value;
+        k230_gpio_update(s, old);
+        break;
+    case K230_GPIO_DEBOUNCE:
+        s->debounce = value;
+        break;
+    case K230_GPIO_PORTA_EOI:
+        /* Write-1-to-clear latched edge interrupts only. */
+        s->raw_intstatus &= ~(value & s->inttype_level);
+        k230_gpio_update(s, k230_gpio_pin_level(s));
+        break;
+    case K230_GPIO_LS_SYNC:
+        s->ls_sync = value;
+        break;
+    default:
+        break;
+    }
+}
+
+static const MemoryRegionOps k230_gpio_ops = {
+    .read = k230_gpio_read,
+    .write = k230_gpio_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+static void k230_gpio_reset(DeviceState *dev)
+{
+    K230GpioState *s = K230_GPIO(dev);
+
+    s->dr = 0;
+    s->ddr = 0;
+    s->ctl = 0;
+    s->inten = 0;
+    s->intmask = 0;
+    s->inttype_level = 0;
+    s->int_polarity = 0;
+    s->intstatus = 0;
+    s->raw_intstatus = 0;
+    s->debounce = 0;
+    s->ls_sync = 0;
+    s->in = 0;
+
+    for (int i = 0; i < K230_GPIO_PINS; i++) {
+        qemu_set_irq(s->irq[i], 0);
+    }
+}
+
+static const VMStateDescription vmstate_k230_gpio = {
+    .name = "k230.gpio",
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(dr, K230GpioState),
+        VMSTATE_UINT32(ddr, K230GpioState),
+        VMSTATE_UINT32(ctl, K230GpioState),
+        VMSTATE_UINT32(inten, K230GpioState),
+        VMSTATE_UINT32(intmask, K230GpioState),
+        VMSTATE_UINT32(inttype_level, K230GpioState),
+        VMSTATE_UINT32(int_polarity, K230GpioState),
+        VMSTATE_UINT32(intstatus, K230GpioState),
+        VMSTATE_UINT32(raw_intstatus, K230GpioState),
+        VMSTATE_UINT32(debounce, K230GpioState),
+        VMSTATE_UINT32(ls_sync, K230GpioState),
+        VMSTATE_UINT32(in, K230GpioState),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void k230_gpio_realize(DeviceState *dev, Error **errp)
+{
+    K230GpioState *s = K230_GPIO(dev);
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+
+    memory_region_init_io(&s->mmio, OBJECT(dev), &k230_gpio_ops, s,
+                          TYPE_K230_GPIO, K230_GPIO_MMIO_SIZE);
+    sysbus_init_mmio(sbd, &s->mmio);
+
+    for (int i = 0; i < K230_GPIO_PINS; i++) {
+        sysbus_init_irq(sbd, &s->irq[i]);
+    }
+    qdev_init_gpio_in(dev, k230_gpio_set_input, K230_GPIO_PINS);
+}
+
+static void k230_gpio_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = k230_gpio_realize;
+    device_class_set_legacy_reset(dc, k230_gpio_reset);
+    dc->vmsd = &vmstate_k230_gpio;
+    dc->desc = "K230 GPIO controller";
+}
+
+static const TypeInfo k230_gpio_info = {
+    .name          = TYPE_K230_GPIO,
+    .parent        = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(K230GpioState),
+    .class_init    = k230_gpio_class_init,
+};
+
+static void k230_gpio_register_type(void)
+{
+    type_register_static(&k230_gpio_info);
+}
+type_init(k230_gpio_register_type)
diff --git a/hw/gpio/meson.build b/hw/gpio/meson.build
index 6a67ee9..94b5d9e 100644
--- a/hw/gpio/meson.build
+++ b/hw/gpio/meson.build
@@ -19,3 +19,4 @@ system_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: 
files('aspeed_gpio.c'))
 system_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: files('aspeed_sgpio.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_K230_GPIO', if_true: files('k230_gpio.c'))
diff --git a/hw/gpio/trace-events b/hw/gpio/trace-events
index cea896b..ac5d7fd 100644
--- a/hw/gpio/trace-events
+++ b/hw/gpio/trace-events
@@ -46,3 +46,7 @@ stm32l4x5_gpio_read(char *gpio, uint64_t addr) "GPIO%s addr: 
0x%" PRIx64 " "
 stm32l4x5_gpio_write(char *gpio, uint64_t addr, uint64_t data) "GPIO%s addr: 
0x%" PRIx64 " val: 0x%" PRIx64 ""
 stm32l4x5_gpio_update_idr(char *gpio, uint32_t old_idr, uint32_t new_idr) 
"GPIO%s from: 0x%x to: 0x%x"
 stm32l4x5_gpio_pins(char *gpio, uint16_t disconnected, uint16_t high) "GPIO%s 
disconnected pins: 0x%x levels: 0x%x"
+
+# k230_gpio.c
+k230_gpio_read(uint64_t addr, uint32_t data) "K230 GPIO read: [0x%" PRIx64 "] 
-> 0x%" PRIx32
+k230_gpio_write(uint64_t addr, uint64_t data) "K230 GPIO write: [0x%" PRIx64 
"] <- 0x%" PRIx64
diff --git a/include/hw/gpio/k230_gpio.h b/include/hw/gpio/k230_gpio.h
new file mode 100644
index 0000000..10c123d
--- /dev/null
+++ b/include/hw/gpio/k230_gpio.h
@@ -0,0 +1,68 @@
+/*
+ * K230 GPIO controller (Synopsys DW_apb_gpio) compatible with Kendryte K230 
SDK
+ *
+ * Copyright (c) 2026 Leo Cheng <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * 
https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ *
+ * For more information, see <https://www.kendryte.com/en/proDetail/230>
+ */
+#ifndef HW_GPIO_K230_GPIO_H
+#define HW_GPIO_K230_GPIO_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+
+#define TYPE_K230_GPIO "riscv.k230.gpio"
+OBJECT_DECLARE_SIMPLE_TYPE(K230GpioState, K230_GPIO)
+
+#define K230_GPIO_PINS      32
+#define K230_GPIO_MMIO_SIZE 0x1000
+
+enum {
+    K230_GPIO_SWPORTA_DR   = 0x00,
+    K230_GPIO_SWPORTA_DDR  = 0x04,
+    K230_GPIO_SWPORTA_CTL  = 0x08,
+    K230_GPIO_INTEN        = 0x30,
+    K230_GPIO_INTMASK      = 0x34,
+    K230_GPIO_INTTYPE_LEVEL = 0x38,
+    K230_GPIO_INT_POLARITY = 0x3c,
+    K230_GPIO_INTSTATUS    = 0x40,
+    K230_GPIO_RAW_INTSTATUS = 0x44,
+    K230_GPIO_DEBOUNCE     = 0x48,
+    K230_GPIO_PORTA_EOI    = 0x4c,
+    K230_GPIO_EXT_PORTA    = 0x50,
+    K230_GPIO_LS_SYNC      = 0x60,
+    K230_GPIO_ID_CODE      = 0x64,
+    K230_GPIO_VER_ID_CODE  = 0x6c,
+    K230_GPIO_CONFIG_REG2  = 0x70,
+    K230_GPIO_CONFIG_REG1  = 0x74,
+};
+
+#define K230_GPIO_VER_ID_VALUE 0x3230312a
+
+struct K230GpioState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion mmio;
+    qemu_irq irq[K230_GPIO_PINS];
+
+    uint32_t dr;
+    uint32_t ddr;
+    uint32_t ctl;
+    uint32_t inten;
+    uint32_t intmask;
+    uint32_t inttype_level;
+    uint32_t int_polarity;
+    uint32_t intstatus;
+    uint32_t raw_intstatus;
+    uint32_t debounce;
+    uint32_t ls_sync;
+
+    uint32_t in;
+};
+
+#endif
diff --git a/tests/qtest/k230-gpio-test.c b/tests/qtest/k230-gpio-test.c
new file mode 100644
index 0000000..42ef137
--- /dev/null
+++ b/tests/qtest/k230-gpio-test.c
@@ -0,0 +1,143 @@
+/*
+ * QTest testcase for K230 GPIO (DW_apb_gpio)
+ *
+ * Copyright (c) 2026 Leo Cheng <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * 
https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ */
+#include "qemu/osdep.h"
+#include "libqtest.h"
+#include "hw/gpio/k230_gpio.h"
+
+#define GPIO0_BASE 0x9140B000
+#define GPIO1_BASE 0x9140C000
+
+#define PLIC_BASE       0xF00000000ULL
+#define PLIC_PENDING    (PLIC_BASE + 0x1000)
+#define GPIO0_IRQ_BASE  32
+
+static bool plic_pending(QTestState *qts, int irq)
+{
+    uint32_t word = qtest_readl(qts, PLIC_PENDING + (irq / 32) * 4);
+    return (word >> (irq % 32)) & 1;
+}
+
+static void test_reset_values(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR), ==, 
0);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DDR),
+                    ==, 0);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_INTSTATUS), ==, 0);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_EXT_PORTA), ==, 0);
+
+    qtest_quit(qts);
+}
+
+/* Output pins loop back into EXT_PORTA */
+static void test_output_loopback(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DDR, 0x1);
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR, 0x1);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_EXT_PORTA) & 0x1,
+                    ==, 0x1);
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR, 0x0);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_EXT_PORTA) & 0x1,
+                    ==, 0x0);
+
+    /* Independent pins */
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DDR, 0xFFFFFFFF);
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR, 0x80000001);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_EXT_PORTA),
+                    ==, 0x80000001);
+
+    qtest_quit(qts);
+}
+
+/* Rising-edge interrupt via DR toggle on an output pin, cleared by EOI */
+static void test_edge_interrupt(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DDR, 0x1);
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INTTYPE_LEVEL, 0x1); /* edge */
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INT_POLARITY, 0x1);  /* rising */
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INTEN, 0x1);
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR, 0x0);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_INTSTATUS) & 0x1,
+                    ==, 0);
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR, 0x1);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_INTSTATUS) & 0x1,
+                    ==, 0x1);
+    g_assert_true(plic_pending(qts, GPIO0_IRQ_BASE + 0));
+
+    /* Write-1 EOI clears the latched edge */
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_PORTA_EOI, 0x1);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_INTSTATUS) & 0x1,
+                    ==, 0);
+
+    qtest_quit(qts);
+}
+
+/* Level interrupt follows the pin; EOI does not clear it */
+static void test_level_interrupt(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DDR, 0x1);
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INTTYPE_LEVEL, 0x0); /* level */
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INT_POLARITY, 0x1);  /* high */
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INTEN, 0x1);
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR, 0x1);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_INTSTATUS) & 0x1,
+                    ==, 0x1);
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR, 0x0);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_INTSTATUS) & 0x1,
+                    ==, 0);
+
+    qtest_quit(qts);
+}
+
+/* INTMASK hides the interrupt from INTSTATUS/PLIC but not RAW_INTSTATUS */
+static void test_mask(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DDR, 0x1);
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INTTYPE_LEVEL, 0x0);
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INT_POLARITY, 0x1);
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INTMASK, 0x1);
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_INTEN, 0x1);
+
+    qtest_writel(qts, GPIO0_BASE + K230_GPIO_SWPORTA_DR, 0x1);
+    g_assert_cmphex(qtest_readl(qts, GPIO0_BASE + K230_GPIO_INTSTATUS) & 0x1,
+                    ==, 0);
+    g_assert_cmphex(qtest_readl(qts,
+                    GPIO0_BASE + K230_GPIO_RAW_INTSTATUS) & 0x1, ==, 0x1);
+
+    qtest_quit(qts);
+}
+
+int main(int argc, char **argv)
+{
+    g_test_init(&argc, &argv, NULL);
+
+    qtest_add_func("/k230-gpio/reset_values", test_reset_values);
+    qtest_add_func("/k230-gpio/output_loopback", test_output_loopback);
+    qtest_add_func("/k230-gpio/edge_interrupt", test_edge_interrupt);
+    qtest_add_func("/k230-gpio/level_interrupt", test_level_interrupt);
+    qtest_add_func("/k230-gpio/mask", test_mask);
+
+    return g_test_run();
+}
-- 
2.43.0


Reply via email to