[PATCH 1/5] hw/gpio: add K230 GPIO controller.

Leo Cheng <[email protected]>
Newsgroups org.nongnu.qemu-devel,org.nongnu.qemu-riscv
Message-ID <[email protected]>
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.