Implement a K230 SoC APB GPIO controller model for QEMU, derived from
the Synopsys DesignWare APB GPIO (the Linux gpio-k230 driver is based
on gpio-dwapb.c and shares the same register layout), with a
Canaan-specific compatible ("canaan,k230-apb-gpio") and hardlock
integration. The model is capable of running the Linux gpio-k230 driver
and driving external input/output lines. Behaviour follows the K230
Technical Reference Manual v0.3.1.
Implemented:
- Port A registers: SWPORTA_DR/DDR/CTL, EXT_PORTA, INTEN, INTMASK,
INTTYPE_LEVEL, INT_POLARITY, INTSTATUS, RAW_INTSTATUS, DEBOUNCE,
LS_SYNC, INT_BOTHEDGE, PORTA_EOI, ID_CODE, VER_ID_CODE,
CONFIG_REG1/2
- Software control mode: DR drives output pad, DDR selects direction,
EXT_PORTA multiplexes external input vs. DR by DDR
- Per-pin IRQ output lines (single interrupt scheme), 32 per group
- Edge detection: rising / falling / both-edge via INT_BOTHEDGE
- Level-sensitive interrupts: active-high / active-low
- PORTA_EOI clears edge interrupts only; level interrupts cleared by
source removal or INTMASK
- INTMASK masking and INTEN gating
- DDR/CTL mode switching: no new interrupts while output/hardware mode,
pending interrupts preserved; on switch back to input/software mode,
level interrupts re-evaluated against current ext level, edge left
untouched, per TRM v0.3.1
- Three-phase Resettable API (enter/hold); VMState migration
Not implemented (no observable effect under QEMU):
- Hardware control mode (aux_porta_out/en/in signals, single-ctl params)
- Debounce logic (dbclk, glitch filtering, both-edge debounce timing)
- Combined interrupt output (gpio_intr_flag OR of all lines)
- Clock-domain synchronization (pclk_intr, ls_sync metastability
registers, gpio_intrclk_en output)
- Config-time parameters GPIO_PA_SYNC_EXT_DATA / GPIO_PA_SYNC_INTERRUPTS
/ GPIO_INT_BOTH_EDGE / GPIO_PORTX_SINGLE_CTL
Two controllers are instantiated in the K230 SoC (gpio0 at 0x9140B000,
gpio1 at 0x9140C000), replacing the previous unimplemented-device
placeholders. Each controller exposes 32 per-pin IRQ lines connected
to the PLIC:
- GPIO0: PLIC sources 32..63
- GPIO1: PLIC sources 64..95
Signed-off-by: guochun wang <guochun.wang@foxmail.com>
---
MAINTAINERS | 2 +
hw/gpio/Kconfig | 3 +
hw/gpio/k230_gpio.c | 362 ++++++++++++++++++++++++++++++++++++
hw/gpio/meson.build | 1 +
hw/riscv/Kconfig | 1 +
hw/riscv/k230.c | 27 ++-
include/hw/gpio/k230_gpio.h | 71 +++++++
include/hw/riscv/k230.h | 5 +
8 files changed, 466 insertions(+), 6 deletions(-)
create mode 100644 hw/gpio/k230_gpio.c
create mode 100644 include/hw/gpio/k230_gpio.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 902db77218..4301d29f7e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1827,8 +1827,10 @@ M: Chao Liu <chao.liu@processmission.com>
L: qemu-riscv@nongnu.org
S: Maintained
F: docs/system/riscv/k230.rst
+F: hw/gpio/k230_gpio.c
F: hw/riscv/k230.c
F: hw/watchdog/k230_wdt.c
+F: include/hw/gpio/k230_gpio.h
F: include/hw/riscv/k230.h
F: include/hw/watchdog/k230_wdt.h
F: tests/functional/riscv64/test_k230.py
diff --git a/hw/gpio/Kconfig b/hw/gpio/Kconfig
index a209294c20..2d37a8bb6f 100644
--- a/hw/gpio/Kconfig
+++ b/hw/gpio/Kconfig
@@ -16,6 +16,9 @@ config SIFIVE_GPIO
config STM32L4X5_GPIO
bool
+config K230_GPIO
+ bool
+
config PCA9552
bool
depends on I2C
diff --git a/hw/gpio/k230_gpio.c b/hw/gpio/k230_gpio.c
new file mode 100644
index 0000000000..9d27f0bbfd
--- /dev/null
+++ b/hw/gpio/k230_gpio.c
@@ -0,0 +1,362 @@
+/*
+ * QEMU K230 GPIO Controller
+ *
+ * Copyright (c) 2025 Wang Guochun <wdasn99@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18), section 12.5 GPIO
+ * 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 "hw/gpio/k230_gpio.h"
+#include "hw/core/irq.h"
+#include "migration/vmstate.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+
+static void k230_gpio_update_int(K230GPIOState *s)
+{
+ uint32_t masked_status = s->raw_intstatus & ~s->intmask & s->inten;
+ for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
+ if (s->irq[i]) {
+ qemu_set_irq(s->irq[i], extract32(masked_status, i, 1));
+ }
+ }
+}
+
+static void k230_gpio_set_int_line(K230GPIOState *s, int line, int level)
+{
+ if (line >= K230_GPIO_PINS_PER_GROUP) {
+ return;
+ }
+
+ uint32_t prev_level = extract32(s->ext_porta, line, 1);
+ uint32_t curr_level = level;
+
+ s->ext_porta = deposit32(s->ext_porta, line, 1, curr_level);
+
+ if (extract32(s->swporta_ddr, line, 1)) {
+ return;
+ }
+
+ if (extract32(s->swporta_ctl, line, 1)) {
+ return;
+ }
+
+ uint32_t pol = extract32(s->int_polarity, line, 1);
+ uint32_t both = extract32(s->int_bothedge, line, 1);
+ uint32_t is_level = !extract32(s->inttype_level, line, 1);
+
+ if (both) {
+ if (prev_level != curr_level) {
+ s->raw_intstatus |= (1U << line);
+ }
+ } else if (is_level) {
+ if (curr_level == pol) {
+ s->raw_intstatus |= (1U << line);
+ } else {
+ s->raw_intstatus &= ~(1U << line);
+ }
+ } else {
+ if (prev_level != curr_level && curr_level == pol) {
+ s->raw_intstatus |= (1U << line);
+ }
+ }
+}
+
+static void k230_gpio_set(void *opaque, int line, int level)
+{
+ K230GPIOState *s = K230_GPIO(opaque);
+
+ k230_gpio_set_int_line(s, line, level);
+ k230_gpio_update_int(s);
+}
+
+static void k230_gpio_set_all_output_lines(K230GPIOState *s)
+{
+ int i;
+
+ for (i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
+ if (extract32(s->swporta_ddr, i, 1) && s->output[i]) {
+ qemu_set_irq(s->output[i], extract32(s->swporta_dr, i, 1));
+ }
+ }
+}
+
+static uint64_t k230_gpio_read(void *opaque, hwaddr offset, unsigned size)
+{
+ K230GPIOState *s = K230_GPIO(opaque);
+
+ switch (offset) {
+ case K230_GPIO_SWPORTA_DR:
+ return s->swporta_dr;
+ case K230_GPIO_SWPORTA_DDR:
+ return s->swporta_ddr;
+ case K230_GPIO_SWPORTA_CTL:
+ return s->swporta_ctl;
+ case K230_GPIO_INTEN:
+ return s->inten;
+ case K230_GPIO_INTMASK:
+ return s->intmask;
+ case K230_GPIO_INTTYPE_LEVEL:
+ return s->inttype_level;
+ case K230_GPIO_INT_POLARITY:
+ return s->int_polarity;
+ case K230_GPIO_INTSTATUS:
+ return s->raw_intstatus & ~s->intmask;
+ case K230_GPIO_RAW_INTSTATUS:
+ return s->raw_intstatus;
+ case K230_GPIO_DEBOUNCE:
+ return s->debounce;
+ case K230_GPIO_EXT_PORTA:
+ return (s->ext_porta & ~s->swporta_ddr) |
+ (s->swporta_dr & s->swporta_ddr);
+ case K230_GPIO_LS_SYNC:
+ return s->ls_sync;
+ case K230_GPIO_ID_CODE:
+ return s->id_code;
+ case K230_GPIO_INT_BOTHEDGE:
+ return s->int_bothedge;
+ case K230_GPIO_VER_ID_CODE:
+ return s->ver_id_code;
+ case K230_GPIO_CONFIG_REG2:
+ return s->config_reg2;
+ case K230_GPIO_CONFIG_REG1:
+ return s->config_reg1;
+ case K230_GPIO_PORTA_EOI:
+ return 0;
+ default:
+ qemu_log_mask(LOG_GUEST_ERROR, "[%s]%s: Bad register at offset 0x%"
+ HWADDR_PRIx "\n", TYPE_K230_GPIO, __func__, offset);
+ return 0;
+ }
+}
+
+static void k230_gpio_write(void *opaque, hwaddr offset, uint64_t value,
+ unsigned size)
+{
+ K230GPIOState *s = K230_GPIO(opaque);
+ uint32_t curr_level, pol;
+
+ switch (offset) {
+ case K230_GPIO_SWPORTA_DR:
+ s->swporta_dr = value;
+ k230_gpio_set_all_output_lines(s);
+ break;
+ case K230_GPIO_SWPORTA_DDR: {
+ uint32_t prev_ddr = s->swporta_ddr;
+ s->swporta_ddr = value;
+ k230_gpio_set_all_output_lines(s);
+ for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
+ /* Skip pins whose direction did not change */
+ if (extract32(prev_ddr, i, 1) == extract32(s->swporta_ddr, i, 1)) {
+ continue;
+ }
+ /*
+ * When switching back to input mode (and still in software
+ * control), re-evaluate level-sensitive interrupts. Edge
+ * interrupts are left untouched, matching the manual TRM v0.3.1.
+ */
+ if (extract32(s->swporta_ddr, i, 1) ||
+ extract32(s->swporta_ctl, i, 1) ||
+ extract32(s->inttype_level, i, 1)) {
+ continue;
+ }
+
+ curr_level = extract32(s->ext_porta, i, 1);
+ pol = extract32(s->int_polarity, i, 1);
+ if (curr_level == pol) {
+ s->raw_intstatus |= (1U << i);
+ } else {
+ s->raw_intstatus &= ~(1U << i);
+ }
+ }
+ k230_gpio_update_int(s);
+ break;
+ }
+ case K230_GPIO_SWPORTA_CTL: {
+ uint32_t prev_ctl = s->swporta_ctl;
+ s->swporta_ctl = value;
+ for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
+ /* Skip pins whose control mode did not change */
+ if (extract32(prev_ctl, i, 1) == extract32(s->swporta_ctl, i, 1)) {
+ continue;
+ }
+ /*
+ * When switching back from hardware to software control (and DDR
+ * is input), re-evaluate level-sensitive interrupts. Edge
+ * interrupts are left untouched, matching the manual TRM v0.3.1.
+ */
+ if (extract32(s->swporta_ddr, i, 1) ||
+ extract32(s->swporta_ctl, i, 1) ||
+ extract32(s->inttype_level, i, 1)) {
+ continue;
+ }
+
+ curr_level = extract32(s->ext_porta, i, 1);
+ pol = extract32(s->int_polarity, i, 1);
+ if (curr_level == pol) {
+ s->raw_intstatus |= (1U << i);
+ } else {
+ s->raw_intstatus &= ~(1U << i);
+ }
+ }
+ k230_gpio_update_int(s);
+ break;
+ }
+ case K230_GPIO_INTEN:
+ s->inten = value;
+ k230_gpio_update_int(s);
+ break;
+ case K230_GPIO_INTMASK:
+ s->intmask = value;
+ k230_gpio_update_int(s);
+ break;
+ case K230_GPIO_INTTYPE_LEVEL:
+ s->inttype_level = value;
+ break;
+ case K230_GPIO_INT_POLARITY:
+ s->int_polarity = value;
+ break;
+ case K230_GPIO_DEBOUNCE:
+ s->debounce = value;
+ break;
+ case K230_GPIO_PORTA_EOI:
+ for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
+ if (extract32(value, i, 1) && extract32(s->inttype_level, i, 1)) {
+ s->raw_intstatus = deposit32(s->raw_intstatus, i, 1, 0);
+ }
+ }
+ k230_gpio_update_int(s);
+ break;
+ case K230_GPIO_LS_SYNC:
+ s->ls_sync = value;
+ break;
+ case K230_GPIO_ID_CODE:
+ break;
+ case K230_GPIO_INT_BOTHEDGE:
+ s->int_bothedge = value;
+ break;
+ case K230_GPIO_VER_ID_CODE:
+ break;
+ case K230_GPIO_CONFIG_REG2:
+ break;
+ case K230_GPIO_CONFIG_REG1:
+ break;
+ case K230_GPIO_INTSTATUS:
+ break;
+ case K230_GPIO_RAW_INTSTATUS:
+ break;
+ case K230_GPIO_EXT_PORTA:
+ break;
+ default:
+ qemu_log_mask(LOG_GUEST_ERROR, "[%s]%s: Bad register at offset 0x%"
+ HWADDR_PRIx "\n", TYPE_K230_GPIO, __func__, offset);
+ break;
+ }
+}
+
+static const MemoryRegionOps k230_gpio_ops = {
+ .read = k230_gpio_read,
+ .write = k230_gpio_write,
+ .valid.min_access_size = 4,
+ .valid.max_access_size = 4,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+};
+
+static const VMStateDescription vmstate_k230_gpio = {
+ .name = TYPE_K230_GPIO,
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (const VMStateField[]) {
+ VMSTATE_UINT32(swporta_dr, K230GPIOState),
+ VMSTATE_UINT32(swporta_ddr, K230GPIOState),
+ VMSTATE_UINT32(swporta_ctl, K230GPIOState),
+ VMSTATE_UINT32(inten, K230GPIOState),
+ VMSTATE_UINT32(intmask, K230GPIOState),
+ VMSTATE_UINT32(inttype_level, K230GPIOState),
+ VMSTATE_UINT32(int_polarity, K230GPIOState),
+ VMSTATE_UINT32(raw_intstatus, K230GPIOState),
+ VMSTATE_UINT32(debounce, K230GPIOState),
+ VMSTATE_UINT32(ext_porta, K230GPIOState),
+ VMSTATE_UINT32(ls_sync, K230GPIOState),
+ VMSTATE_UINT32(int_bothedge, K230GPIOState),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static void k230_gpio_enter_reset(Object *obj, ResetType type)
+{
+ K230GPIOState *s = K230_GPIO(obj);
+
+ s->swporta_dr = 0;
+ s->swporta_ddr = 0;
+ s->swporta_ctl = 0;
+ s->inten = 0;
+ s->intmask = 0;
+ s->inttype_level = 0;
+ s->int_polarity = 0;
+ s->raw_intstatus = 0;
+ s->debounce = 0;
+ s->porta_eoi = 0;
+ s->ext_porta = 0;
+ s->ls_sync = 0;
+ s->id_code = 0;
+ s->int_bothedge = 0;
+ s->ver_id_code = 0;
+ s->config_reg2 = 0;
+ s->config_reg1 = 0;
+}
+
+static void k230_gpio_hold_reset(Object *obj, ResetType type)
+{
+ K230GPIOState *s = K230_GPIO(obj);
+
+ k230_gpio_update_int(s);
+}
+
+static void k230_gpio_realize(DeviceState *dev, Error **errp)
+{
+ K230GPIOState *s = K230_GPIO(dev);
+ SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+ int i;
+
+ memory_region_init_io(&s->iomem, OBJECT(s), &k230_gpio_ops, s,
+ TYPE_K230_GPIO, K230_GPIO_MEM_SIZE);
+ sysbus_init_mmio(sbd, &s->iomem);
+
+ qdev_init_gpio_in(DEVICE(s), k230_gpio_set, K230_GPIO_PINS_PER_GROUP);
+ qdev_init_gpio_out(DEVICE(s), s->output, K230_GPIO_PINS_PER_GROUP);
+
+ for (i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
+ sysbus_init_irq(sbd, &s->irq[i]);
+ }
+}
+
+static void k230_gpio_class_init(ObjectClass *klass, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+ dc->realize = k230_gpio_realize;
+ rc->phases.enter = k230_gpio_enter_reset;
+ rc->phases.hold = k230_gpio_hold_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_types(void)
+{
+ type_register_static(&k230_gpio_info);
+}
+
+type_init(k230_gpio_register_types)
diff --git a/hw/gpio/meson.build b/hw/gpio/meson.build
index 6a67ee958f..94b5d9e2fa 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/riscv/Kconfig b/hw/riscv/Kconfig
index de37c08cae..410877425c 100644
--- a/hw/riscv/Kconfig
+++ b/hw/riscv/Kconfig
@@ -162,3 +162,4 @@ config K230
select SERIAL_MM
select UNIMP
select K230_WDT
+ select K230_GPIO
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index 656f28190c..62967a67d0 100644
--- a/hw/riscv/k230.c
+++ b/hw/riscv/k230.c
@@ -110,6 +110,8 @@ static void k230_soc_init(Object *obj)
object_initialize_child(obj, "c908-cpu", cpu0, TYPE_RISCV_HART_ARRAY);
object_initialize_child(obj, "k230-wdt0", &s->wdt[0], TYPE_K230_WDT);
object_initialize_child(obj, "k230-wdt1", &s->wdt[1], TYPE_K230_WDT);
+ object_initialize_child(obj, "k230-gpio0", &s->gpio[0], TYPE_K230_GPIO);
+ object_initialize_child(obj, "k230-gpio1", &s->gpio[1], TYPE_K230_GPIO);
qdev_prop_set_uint32(DEVICE(cpu0), "hartid-base", 0);
qdev_prop_set_string(DEVICE(cpu0), "cpu-type", TYPE_RISCV_CPU_THEAD_C908);
@@ -206,6 +208,25 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
sysbus_connect_irq(SYS_BUS_DEVICE(&s->wdt[1]), 0,
qdev_get_gpio_in(DEVICE(s->c908_plic), K230_WDT1_IRQ));
+ /* GPIO */
+ sysbus_realize(SYS_BUS_DEVICE(&s->gpio[0]), &error_fatal);
+ sysbus_mmio_map(SYS_BUS_DEVICE(&s->gpio[0]), 0,
+ memmap[K230_DEV_GPIO0].base);
+ for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
+ sysbus_connect_irq(SYS_BUS_DEVICE(&s->gpio[0]), i,
+ qdev_get_gpio_in(DEVICE(s->c908_plic),
+ K230_GPIO0_IRQ_BASE + i));
+ }
+
+ sysbus_realize(SYS_BUS_DEVICE(&s->gpio[1]), &error_fatal);
+ sysbus_mmio_map(SYS_BUS_DEVICE(&s->gpio[1]), 0,
+ memmap[K230_DEV_GPIO1].base);
+ for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
+ sysbus_connect_irq(SYS_BUS_DEVICE(&s->gpio[1]), i,
+ qdev_get_gpio_in(DEVICE(s->c908_plic),
+ K230_GPIO1_IRQ_BASE + i));
+ }
+
/* unimplemented devices */
create_unimplemented_device("kpu.l2-cache",
memmap[K230_DEV_KPU_L2_CACHE].base,
@@ -322,12 +343,6 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
create_unimplemented_device("pwm", memmap[K230_DEV_PWM].base,
memmap[K230_DEV_PWM].size);
- create_unimplemented_device("gpio0", memmap[K230_DEV_GPIO0].base,
- memmap[K230_DEV_GPIO0].size);
-
- create_unimplemented_device("gpio1", memmap[K230_DEV_GPIO1].base,
- memmap[K230_DEV_GPIO1].size);
-
create_unimplemented_device("adc", memmap[K230_DEV_ADC].base,
memmap[K230_DEV_ADC].size);
diff --git a/include/hw/gpio/k230_gpio.h b/include/hw/gpio/k230_gpio.h
new file mode 100644
index 0000000000..32005815d2
--- /dev/null
+++ b/include/hw/gpio/k230_gpio.h
@@ -0,0 +1,71 @@
+/*
+ * QEMU K230 GPIO Controller
+ *
+ * Copyright (c) 2025 Wang Guochun <wdasn99@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18), section 12.5 GPIO
+ * https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ */
+
+#ifndef HW_K230_GPIO_H
+#define HW_K230_GPIO_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+
+#define TYPE_K230_GPIO "k230.gpio"
+OBJECT_DECLARE_SIMPLE_TYPE(K230GPIOState, K230_GPIO)
+
+#define K230_GPIO_MEM_SIZE 0x1000
+
+#define K230_GPIO_SWPORTA_DR 0x00
+#define K230_GPIO_SWPORTA_DDR 0x04
+#define K230_GPIO_SWPORTA_CTL 0x08
+#define K230_GPIO_INTEN 0x30
+#define K230_GPIO_INTMASK 0x34
+#define K230_GPIO_INTTYPE_LEVEL 0x38
+#define K230_GPIO_INT_POLARITY 0x3c
+#define K230_GPIO_INTSTATUS 0x40
+#define K230_GPIO_RAW_INTSTATUS 0x44
+#define K230_GPIO_DEBOUNCE 0x48
+#define K230_GPIO_PORTA_EOI 0x4c
+#define K230_GPIO_EXT_PORTA 0x50
+#define K230_GPIO_LS_SYNC 0x60
+#define K230_GPIO_ID_CODE 0x64
+#define K230_GPIO_INT_BOTHEDGE 0x68
+#define K230_GPIO_VER_ID_CODE 0x6c
+#define K230_GPIO_CONFIG_REG2 0x70
+#define K230_GPIO_CONFIG_REG1 0x74
+
+#define K230_GPIO_PINS_PER_GROUP 32
+
+struct K230GPIOState {
+ SysBusDevice parent_obj;
+
+ MemoryRegion iomem;
+
+ uint32_t swporta_dr;
+ uint32_t swporta_ddr;
+ uint32_t swporta_ctl;
+ uint32_t inten;
+ uint32_t intmask;
+ uint32_t inttype_level;
+ uint32_t int_polarity;
+ uint32_t raw_intstatus;
+ uint32_t debounce;
+ uint32_t porta_eoi;
+ uint32_t ext_porta;
+ uint32_t ls_sync;
+ uint32_t id_code;
+ uint32_t int_bothedge;
+ uint32_t ver_id_code;
+ uint32_t config_reg2;
+ uint32_t config_reg1;
+
+ qemu_irq irq[K230_GPIO_PINS_PER_GROUP];
+ qemu_irq output[K230_GPIO_PINS_PER_GROUP];
+};
+
+#endif
diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
index 592e1c26bf..5fccea7fd5 100644
--- a/include/hw/riscv/k230.h
+++ b/include/hw/riscv/k230.h
@@ -18,6 +18,7 @@
#include "hw/core/boards.h"
#include "hw/riscv/riscv_hart.h"
#include "hw/watchdog/k230_wdt.h"
+#include "hw/gpio/k230_gpio.h"
#define C908_CPU_HARTID (0)
@@ -33,6 +34,7 @@ typedef struct K230SoCState {
RISCVHartArrayState c908_cpu; /* Small core */
K230WdtState wdt[2];
+ K230GPIOState gpio[2];
MemoryRegion sram;
MemoryRegion bootrom;
@@ -127,8 +129,11 @@ enum {
K230_UART2_IRQ = 18,
K230_UART3_IRQ = 19,
K230_UART4_IRQ = 20,
+ K230_GPIO0_IRQ_BASE = 32,
+ K230_GPIO1_IRQ_BASE = 64,
K230_WDT0_IRQ = 107,
K230_WDT1_IRQ = 108,
+
};
#define K230_UART_COUNT 5
--
2.43.0
On Mon, Aug 10, 2026 at 4:02 AM guochun wang <guochun.wang@foxmail.com> wrote:
>
>
> Implement a K230 SoC APB GPIO controller model for QEMU, derived from
> the Synopsys DesignWare APB GPIO (the Linux gpio-k230 driver is based
> on gpio-dwapb.c and shares the same register layout), with a
> Canaan-specific compatible ("canaan,k230-apb-gpio") and hardlock
> integration. The model is capable of running the Linux gpio-k230 driver
> and driving external input/output lines. Behaviour follows the K230
> Technical Reference Manual v0.3.1.
Please split the k230_gpio model into 2 separate models:
- One is generic Synopsys DesignWare GPIO IP model
- Optional Canann K230 specific wrapper glue model to the dw-gpio IP
(if the generic Synopsys DesignWare GPIO IP model, or plus some unimp
region, is good enough to make the software happy, no need to create
this one)
>
> Implemented:
> - Port A registers: SWPORTA_DR/DDR/CTL, EXT_PORTA, INTEN, INTMASK,
> INTTYPE_LEVEL, INT_POLARITY, INTSTATUS, RAW_INTSTATUS, DEBOUNCE,
> LS_SYNC, INT_BOTHEDGE, PORTA_EOI, ID_CODE, VER_ID_CODE,
> CONFIG_REG1/2
> - Software control mode: DR drives output pad, DDR selects direction,
> EXT_PORTA multiplexes external input vs. DR by DDR
> - Per-pin IRQ output lines (single interrupt scheme), 32 per group
> - Edge detection: rising / falling / both-edge via INT_BOTHEDGE
> - Level-sensitive interrupts: active-high / active-low
> - PORTA_EOI clears edge interrupts only; level interrupts cleared by
> source removal or INTMASK
> - INTMASK masking and INTEN gating
> - DDR/CTL mode switching: no new interrupts while output/hardware mode,
> pending interrupts preserved; on switch back to input/software mode,
> level interrupts re-evaluated against current ext level, edge left
> untouched, per TRM v0.3.1
> - Three-phase Resettable API (enter/hold); VMState migration
>
> Not implemented (no observable effect under QEMU):
> - Hardware control mode (aux_porta_out/en/in signals, single-ctl params)
> - Debounce logic (dbclk, glitch filtering, both-edge debounce timing)
> - Combined interrupt output (gpio_intr_flag OR of all lines)
> - Clock-domain synchronization (pclk_intr, ls_sync metastability
> registers, gpio_intrclk_en output)
> - Config-time parameters GPIO_PA_SYNC_EXT_DATA / GPIO_PA_SYNC_INTERRUPTS
> / GPIO_INT_BOTH_EDGE / GPIO_PORTX_SINGLE_CTL
>
> Two controllers are instantiated in the K230 SoC (gpio0 at 0x9140B000,
> gpio1 at 0x9140C000), replacing the previous unimplemented-device
> placeholders. Each controller exposes 32 per-pin IRQ lines connected
> to the PLIC:
> - GPIO0: PLIC sources 32..63
> - GPIO1: PLIC sources 64..95
Please split the integration of the GPIO device to the K230 machine
out of this patch.
>
> Signed-off-by: guochun wang <guochun.wang@foxmail.com>
> ---
> MAINTAINERS | 2 +
> hw/gpio/Kconfig | 3 +
> hw/gpio/k230_gpio.c | 362 ++++++++++++++++++++++++++++++++++++
> hw/gpio/meson.build | 1 +
> hw/riscv/Kconfig | 1 +
> hw/riscv/k230.c | 27 ++-
> include/hw/gpio/k230_gpio.h | 71 +++++++
> include/hw/riscv/k230.h | 5 +
> 8 files changed, 466 insertions(+), 6 deletions(-)
> create mode 100644 hw/gpio/k230_gpio.c
> create mode 100644 include/hw/gpio/k230_gpio.h
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 902db77218..4301d29f7e 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1827,8 +1827,10 @@ M: Chao Liu <chao.liu@processmission.com>
> L: qemu-riscv@nongnu.org
> S: Maintained
> F: docs/system/riscv/k230.rst
> +F: hw/gpio/k230_gpio.c
> F: hw/riscv/k230.c
> F: hw/watchdog/k230_wdt.c
> +F: include/hw/gpio/k230_gpio.h
> F: include/hw/riscv/k230.h
> F: include/hw/watchdog/k230_wdt.h
> F: tests/functional/riscv64/test_k230.py
> diff --git a/hw/gpio/Kconfig b/hw/gpio/Kconfig
> index a209294c20..2d37a8bb6f 100644
> --- a/hw/gpio/Kconfig
> +++ b/hw/gpio/Kconfig
> @@ -16,6 +16,9 @@ config SIFIVE_GPIO
> config STM32L4X5_GPIO
> bool
>
> +config K230_GPIO
> + bool
> +
> config PCA9552
> bool
> depends on I2C
> diff --git a/hw/gpio/k230_gpio.c b/hw/gpio/k230_gpio.c
> new file mode 100644
> index 0000000000..9d27f0bbfd
> --- /dev/null
> +++ b/hw/gpio/k230_gpio.c
> @@ -0,0 +1,362 @@
> +/*
> + * QEMU K230 GPIO Controller
> + *
> + * Copyright (c) 2025 Wang Guochun <wdasn99@gmail.com>
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + *
> + * K230 Technical Reference Manual V0.3.1 (2024-11-18), section 12.5 GPIO
> + * 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 "hw/gpio/k230_gpio.h"
> +#include "hw/core/irq.h"
> +#include "migration/vmstate.h"
> +#include "qemu/log.h"
> +#include "qemu/module.h"
> +
> +static void k230_gpio_update_int(K230GPIOState *s)
> +{
> + uint32_t masked_status = s->raw_intstatus & ~s->intmask & s->inten;
> + for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
> + if (s->irq[i]) {
> + qemu_set_irq(s->irq[i], extract32(masked_status, i, 1));
> + }
> + }
> +}
> +
> +static void k230_gpio_set_int_line(K230GPIOState *s, int line, int level)
> +{
> + if (line >= K230_GPIO_PINS_PER_GROUP) {
> + return;
> + }
> +
> + uint32_t prev_level = extract32(s->ext_porta, line, 1);
> + uint32_t curr_level = level;
> +
> + s->ext_porta = deposit32(s->ext_porta, line, 1, curr_level);
> +
> + if (extract32(s->swporta_ddr, line, 1)) {
> + return;
> + }
> +
> + if (extract32(s->swporta_ctl, line, 1)) {
> + return;
> + }
> +
> + uint32_t pol = extract32(s->int_polarity, line, 1);
> + uint32_t both = extract32(s->int_bothedge, line, 1);
> + uint32_t is_level = !extract32(s->inttype_level, line, 1);
> +
> + if (both) {
> + if (prev_level != curr_level) {
> + s->raw_intstatus |= (1U << line);
> + }
> + } else if (is_level) {
> + if (curr_level == pol) {
> + s->raw_intstatus |= (1U << line);
> + } else {
> + s->raw_intstatus &= ~(1U << line);
> + }
> + } else {
> + if (prev_level != curr_level && curr_level == pol) {
> + s->raw_intstatus |= (1U << line);
> + }
> + }
> +}
> +
> +static void k230_gpio_set(void *opaque, int line, int level)
> +{
> + K230GPIOState *s = K230_GPIO(opaque);
> +
> + k230_gpio_set_int_line(s, line, level);
> + k230_gpio_update_int(s);
> +}
> +
> +static void k230_gpio_set_all_output_lines(K230GPIOState *s)
> +{
> + int i;
> +
> + for (i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
> + if (extract32(s->swporta_ddr, i, 1) && s->output[i]) {
This should check the CTL bit too, like:
if (extract32(s->swporta_ddr, i, 1) && !extract32(s->swporta_ctl, i,
1) && s->output[i])
> + qemu_set_irq(s->output[i], extract32(s->swporta_dr, i, 1));
> + }
> + }
> +}
> +
> +static uint64_t k230_gpio_read(void *opaque, hwaddr offset, unsigned size)
> +{
> + K230GPIOState *s = K230_GPIO(opaque);
> +
> + switch (offset) {
> + case K230_GPIO_SWPORTA_DR:
> + return s->swporta_dr;
> + case K230_GPIO_SWPORTA_DDR:
> + return s->swporta_ddr;
> + case K230_GPIO_SWPORTA_CTL:
> + return s->swporta_ctl;
> + case K230_GPIO_INTEN:
> + return s->inten;
> + case K230_GPIO_INTMASK:
> + return s->intmask;
> + case K230_GPIO_INTTYPE_LEVEL:
> + return s->inttype_level;
> + case K230_GPIO_INT_POLARITY:
> + return s->int_polarity;
> + case K230_GPIO_INTSTATUS:
> + return s->raw_intstatus & ~s->intmask;
> + case K230_GPIO_RAW_INTSTATUS:
> + return s->raw_intstatus;
> + case K230_GPIO_DEBOUNCE:
> + return s->debounce;
> + case K230_GPIO_EXT_PORTA:
> + return (s->ext_porta & ~s->swporta_ddr) |
> + (s->swporta_dr & s->swporta_ddr);
> + case K230_GPIO_LS_SYNC:
> + return s->ls_sync;
Bits 1:31 are reserved and should read as zero.
> + case K230_GPIO_ID_CODE:
> + return s->id_code;
> + case K230_GPIO_INT_BOTHEDGE:
> + return s->int_bothedge;
> + case K230_GPIO_VER_ID_CODE:
> + return s->ver_id_code;
> + case K230_GPIO_CONFIG_REG2:
> + return s->config_reg2;
> + case K230_GPIO_CONFIG_REG1:
> + return s->config_reg1;
> + case K230_GPIO_PORTA_EOI:
> + return 0;
> + default:
> + qemu_log_mask(LOG_GUEST_ERROR, "[%s]%s: Bad register at offset 0x%"
> + HWADDR_PRIx "\n", TYPE_K230_GPIO, __func__, offset);
> + return 0;
> + }
> +}
> +
> +static void k230_gpio_write(void *opaque, hwaddr offset, uint64_t value,
> + unsigned size)
> +{
> + K230GPIOState *s = K230_GPIO(opaque);
> + uint32_t curr_level, pol;
> +
> + switch (offset) {
> + case K230_GPIO_SWPORTA_DR:
> + s->swporta_dr = value;
> + k230_gpio_set_all_output_lines(s);
> + break;
> + case K230_GPIO_SWPORTA_DDR: {
> + uint32_t prev_ddr = s->swporta_ddr;
> + s->swporta_ddr = value;
> + k230_gpio_set_all_output_lines(s);
> + for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
> + /* Skip pins whose direction did not change */
> + if (extract32(prev_ddr, i, 1) == extract32(s->swporta_ddr, i, 1)) {
> + continue;
> + }
> + /*
> + * When switching back to input mode (and still in software
> + * control), re-evaluate level-sensitive interrupts. Edge
> + * interrupts are left untouched, matching the manual TRM v0.3.1.
> + */
> + if (extract32(s->swporta_ddr, i, 1) ||
> + extract32(s->swporta_ctl, i, 1) ||
> + extract32(s->inttype_level, i, 1)) {
Should we test s->int_bothedge here? e.g.:
(extract32(s->inttype_level, i, 1) || extract32(s->int_bothedge, i, 1))
> + continue;
> + }
> +
> + curr_level = extract32(s->ext_porta, i, 1);
> + pol = extract32(s->int_polarity, i, 1);
> + if (curr_level == pol) {
> + s->raw_intstatus |= (1U << i);
> + } else {
> + s->raw_intstatus &= ~(1U << i);
> + }
> + }
> + k230_gpio_update_int(s);
> + break;
> + }
> + case K230_GPIO_SWPORTA_CTL: {
> + uint32_t prev_ctl = s->swporta_ctl;
> + s->swporta_ctl = value;
> + for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
> + /* Skip pins whose control mode did not change */
> + if (extract32(prev_ctl, i, 1) == extract32(s->swporta_ctl, i, 1)) {
> + continue;
> + }
> + /*
> + * When switching back from hardware to software control (and DDR
> + * is input), re-evaluate level-sensitive interrupts. Edge
> + * interrupts are left untouched, matching the manual TRM v0.3.1.
> + */
> + if (extract32(s->swporta_ddr, i, 1) ||
> + extract32(s->swporta_ctl, i, 1) ||
> + extract32(s->inttype_level, i, 1)) {
Should we test s->int_bothedge here? e.g.:
(extract32(s->inttype_level, i, 1) || extract32(s->int_bothedge, i, 1))
> + continue;
> + }
> +
> + curr_level = extract32(s->ext_porta, i, 1);
> + pol = extract32(s->int_polarity, i, 1);
> + if (curr_level == pol) {
> + s->raw_intstatus |= (1U << i);
> + } else {
> + s->raw_intstatus &= ~(1U << i);
> + }
> + }
> + k230_gpio_update_int(s);
> + break;
> + }
> + case K230_GPIO_INTEN:
> + s->inten = value;
> + k230_gpio_update_int(s);
> + break;
> + case K230_GPIO_INTMASK:
> + s->intmask = value;
> + k230_gpio_update_int(s);
> + break;
> + case K230_GPIO_INTTYPE_LEVEL:
> + s->inttype_level = value;
> + break;
> + case K230_GPIO_INT_POLARITY:
> + s->int_polarity = value;
> + break;
> + case K230_GPIO_DEBOUNCE:
> + s->debounce = value;
> + break;
> + case K230_GPIO_PORTA_EOI:
> + for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
> + if (extract32(value, i, 1) && extract32(s->inttype_level, i, 1)) {
Should we test s->int_bothedge here? e.g.:
(extract32(s->inttype_level, i, 1) || extract32(s->int_bothedge, i, 1))
> + s->raw_intstatus = deposit32(s->raw_intstatus, i, 1, 0);
> + }
> + }
> + k230_gpio_update_int(s);
> + break;
> + case K230_GPIO_LS_SYNC:
> + s->ls_sync = value;
Only bit 0 is writable.
> + break;
> + case K230_GPIO_ID_CODE:
> + break;
> + case K230_GPIO_INT_BOTHEDGE:
> + s->int_bothedge = value;
> + break;
> + case K230_GPIO_VER_ID_CODE:
> + break;
> + case K230_GPIO_CONFIG_REG2:
> + break;
> + case K230_GPIO_CONFIG_REG1:
> + break;
> + case K230_GPIO_INTSTATUS:
> + break;
> + case K230_GPIO_RAW_INTSTATUS:
> + break;
> + case K230_GPIO_EXT_PORTA:
> + break;
> + default:
> + qemu_log_mask(LOG_GUEST_ERROR, "[%s]%s: Bad register at offset 0x%"
> + HWADDR_PRIx "\n", TYPE_K230_GPIO, __func__, offset);
> + break;
> + }
> +}
> +
> +static const MemoryRegionOps k230_gpio_ops = {
> + .read = k230_gpio_read,
> + .write = k230_gpio_write,
> + .valid.min_access_size = 4,
> + .valid.max_access_size = 4,
> + .endianness = DEVICE_LITTLE_ENDIAN,
> +};
> +
> +static const VMStateDescription vmstate_k230_gpio = {
> + .name = TYPE_K230_GPIO,
> + .version_id = 1,
> + .minimum_version_id = 1,
> + .fields = (const VMStateField[]) {
> + VMSTATE_UINT32(swporta_dr, K230GPIOState),
> + VMSTATE_UINT32(swporta_ddr, K230GPIOState),
> + VMSTATE_UINT32(swporta_ctl, K230GPIOState),
> + VMSTATE_UINT32(inten, K230GPIOState),
> + VMSTATE_UINT32(intmask, K230GPIOState),
> + VMSTATE_UINT32(inttype_level, K230GPIOState),
> + VMSTATE_UINT32(int_polarity, K230GPIOState),
> + VMSTATE_UINT32(raw_intstatus, K230GPIOState),
> + VMSTATE_UINT32(debounce, K230GPIOState),
> + VMSTATE_UINT32(ext_porta, K230GPIOState),
> + VMSTATE_UINT32(ls_sync, K230GPIOState),
> + VMSTATE_UINT32(int_bothedge, K230GPIOState),
> + VMSTATE_END_OF_LIST()
> + }
> +};
> +
> +static void k230_gpio_enter_reset(Object *obj, ResetType type)
> +{
> + K230GPIOState *s = K230_GPIO(obj);
> +
> + s->swporta_dr = 0;
> + s->swporta_ddr = 0;
> + s->swporta_ctl = 0;
> + s->inten = 0;
> + s->intmask = 0;
> + s->inttype_level = 0;
> + s->int_polarity = 0;
> + s->raw_intstatus = 0;
> + s->debounce = 0;
> + s->porta_eoi = 0;
> + s->ext_porta = 0;
> + s->ls_sync = 0;
> + s->id_code = 0;
> + s->int_bothedge = 0;
> + s->ver_id_code = 0;
> + s->config_reg2 = 0;
> + s->config_reg1 = 0;
> +}
> +
> +static void k230_gpio_hold_reset(Object *obj, ResetType type)
> +{
> + K230GPIOState *s = K230_GPIO(obj);
> +
> + k230_gpio_update_int(s);
> +}
> +
> +static void k230_gpio_realize(DeviceState *dev, Error **errp)
> +{
> + K230GPIOState *s = K230_GPIO(dev);
> + SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
> + int i;
> +
> + memory_region_init_io(&s->iomem, OBJECT(s), &k230_gpio_ops, s,
> + TYPE_K230_GPIO, K230_GPIO_MEM_SIZE);
> + sysbus_init_mmio(sbd, &s->iomem);
> +
> + qdev_init_gpio_in(DEVICE(s), k230_gpio_set, K230_GPIO_PINS_PER_GROUP);
> + qdev_init_gpio_out(DEVICE(s), s->output, K230_GPIO_PINS_PER_GROUP);
> +
> + for (i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
> + sysbus_init_irq(sbd, &s->irq[i]);
> + }
> +}
> +
> +static void k230_gpio_class_init(ObjectClass *klass, const void *data)
> +{
> + DeviceClass *dc = DEVICE_CLASS(klass);
> + ResettableClass *rc = RESETTABLE_CLASS(klass);
> +
> + dc->realize = k230_gpio_realize;
> + rc->phases.enter = k230_gpio_enter_reset;
> + rc->phases.hold = k230_gpio_hold_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_types(void)
> +{
> + type_register_static(&k230_gpio_info);
> +}
> +
> +type_init(k230_gpio_register_types)
> diff --git a/hw/gpio/meson.build b/hw/gpio/meson.build
> index 6a67ee958f..94b5d9e2fa 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/riscv/Kconfig b/hw/riscv/Kconfig
> index de37c08cae..410877425c 100644
> --- a/hw/riscv/Kconfig
> +++ b/hw/riscv/Kconfig
> @@ -162,3 +162,4 @@ config K230
> select SERIAL_MM
> select UNIMP
> select K230_WDT
> + select K230_GPIO
> diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
> index 656f28190c..62967a67d0 100644
> --- a/hw/riscv/k230.c
> +++ b/hw/riscv/k230.c
> @@ -110,6 +110,8 @@ static void k230_soc_init(Object *obj)
> object_initialize_child(obj, "c908-cpu", cpu0, TYPE_RISCV_HART_ARRAY);
> object_initialize_child(obj, "k230-wdt0", &s->wdt[0], TYPE_K230_WDT);
> object_initialize_child(obj, "k230-wdt1", &s->wdt[1], TYPE_K230_WDT);
> + object_initialize_child(obj, "k230-gpio0", &s->gpio[0], TYPE_K230_GPIO);
> + object_initialize_child(obj, "k230-gpio1", &s->gpio[1], TYPE_K230_GPIO);
>
> qdev_prop_set_uint32(DEVICE(cpu0), "hartid-base", 0);
> qdev_prop_set_string(DEVICE(cpu0), "cpu-type", TYPE_RISCV_CPU_THEAD_C908);
> @@ -206,6 +208,25 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
> sysbus_connect_irq(SYS_BUS_DEVICE(&s->wdt[1]), 0,
> qdev_get_gpio_in(DEVICE(s->c908_plic), K230_WDT1_IRQ));
>
> + /* GPIO */
> + sysbus_realize(SYS_BUS_DEVICE(&s->gpio[0]), &error_fatal);
> + sysbus_mmio_map(SYS_BUS_DEVICE(&s->gpio[0]), 0,
> + memmap[K230_DEV_GPIO0].base);
> + for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
> + sysbus_connect_irq(SYS_BUS_DEVICE(&s->gpio[0]), i,
> + qdev_get_gpio_in(DEVICE(s->c908_plic),
> + K230_GPIO0_IRQ_BASE + i));
> + }
> +
> + sysbus_realize(SYS_BUS_DEVICE(&s->gpio[1]), &error_fatal);
> + sysbus_mmio_map(SYS_BUS_DEVICE(&s->gpio[1]), 0,
> + memmap[K230_DEV_GPIO1].base);
> + for (int i = 0; i < K230_GPIO_PINS_PER_GROUP; i++) {
> + sysbus_connect_irq(SYS_BUS_DEVICE(&s->gpio[1]), i,
> + qdev_get_gpio_in(DEVICE(s->c908_plic),
> + K230_GPIO1_IRQ_BASE + i));
> + }
> +
> /* unimplemented devices */
> create_unimplemented_device("kpu.l2-cache",
> memmap[K230_DEV_KPU_L2_CACHE].base,
> @@ -322,12 +343,6 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
> create_unimplemented_device("pwm", memmap[K230_DEV_PWM].base,
> memmap[K230_DEV_PWM].size);
>
> - create_unimplemented_device("gpio0", memmap[K230_DEV_GPIO0].base,
> - memmap[K230_DEV_GPIO0].size);
> -
> - create_unimplemented_device("gpio1", memmap[K230_DEV_GPIO1].base,
> - memmap[K230_DEV_GPIO1].size);
> -
> create_unimplemented_device("adc", memmap[K230_DEV_ADC].base,
> memmap[K230_DEV_ADC].size);
>
> diff --git a/include/hw/gpio/k230_gpio.h b/include/hw/gpio/k230_gpio.h
> new file mode 100644
> index 0000000000..32005815d2
> --- /dev/null
> +++ b/include/hw/gpio/k230_gpio.h
> @@ -0,0 +1,71 @@
> +/*
> + * QEMU K230 GPIO Controller
> + *
> + * Copyright (c) 2025 Wang Guochun <wdasn99@gmail.com>
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + *
> + * K230 Technical Reference Manual V0.3.1 (2024-11-18), section 12.5 GPIO
> + * https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
> + */
> +
> +#ifndef HW_K230_GPIO_H
> +#define HW_K230_GPIO_H
> +
> +#include "hw/core/sysbus.h"
> +#include "qom/object.h"
> +
> +#define TYPE_K230_GPIO "k230.gpio"
> +OBJECT_DECLARE_SIMPLE_TYPE(K230GPIOState, K230_GPIO)
> +
> +#define K230_GPIO_MEM_SIZE 0x1000
> +
> +#define K230_GPIO_SWPORTA_DR 0x00
> +#define K230_GPIO_SWPORTA_DDR 0x04
> +#define K230_GPIO_SWPORTA_CTL 0x08
> +#define K230_GPIO_INTEN 0x30
> +#define K230_GPIO_INTMASK 0x34
> +#define K230_GPIO_INTTYPE_LEVEL 0x38
> +#define K230_GPIO_INT_POLARITY 0x3c
> +#define K230_GPIO_INTSTATUS 0x40
> +#define K230_GPIO_RAW_INTSTATUS 0x44
> +#define K230_GPIO_DEBOUNCE 0x48
> +#define K230_GPIO_PORTA_EOI 0x4c
> +#define K230_GPIO_EXT_PORTA 0x50
> +#define K230_GPIO_LS_SYNC 0x60
> +#define K230_GPIO_ID_CODE 0x64
> +#define K230_GPIO_INT_BOTHEDGE 0x68
> +#define K230_GPIO_VER_ID_CODE 0x6c
> +#define K230_GPIO_CONFIG_REG2 0x70
> +#define K230_GPIO_CONFIG_REG1 0x74
> +
> +#define K230_GPIO_PINS_PER_GROUP 32
> +
> +struct K230GPIOState {
> + SysBusDevice parent_obj;
> +
> + MemoryRegion iomem;
> +
> + uint32_t swporta_dr;
> + uint32_t swporta_ddr;
> + uint32_t swporta_ctl;
> + uint32_t inten;
> + uint32_t intmask;
> + uint32_t inttype_level;
> + uint32_t int_polarity;
> + uint32_t raw_intstatus;
> + uint32_t debounce;
> + uint32_t porta_eoi;
This member seems not to be used anywhere. Drop it.
> + uint32_t ext_porta;
> + uint32_t ls_sync;
> + uint32_t id_code;
> + uint32_t int_bothedge;
> + uint32_t ver_id_code;
> + uint32_t config_reg2;
> + uint32_t config_reg1;
> +
> + qemu_irq irq[K230_GPIO_PINS_PER_GROUP];
> + qemu_irq output[K230_GPIO_PINS_PER_GROUP];
> +};
> +
> +#endif
> diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
> index 592e1c26bf..5fccea7fd5 100644
> --- a/include/hw/riscv/k230.h
> +++ b/include/hw/riscv/k230.h
> @@ -18,6 +18,7 @@
> #include "hw/core/boards.h"
> #include "hw/riscv/riscv_hart.h"
> #include "hw/watchdog/k230_wdt.h"
> +#include "hw/gpio/k230_gpio.h"
>
> #define C908_CPU_HARTID (0)
>
> @@ -33,6 +34,7 @@ typedef struct K230SoCState {
> RISCVHartArrayState c908_cpu; /* Small core */
>
> K230WdtState wdt[2];
> + K230GPIOState gpio[2];
> MemoryRegion sram;
> MemoryRegion bootrom;
>
> @@ -127,8 +129,11 @@ enum {
> K230_UART2_IRQ = 18,
> K230_UART3_IRQ = 19,
> K230_UART4_IRQ = 20,
> + K230_GPIO0_IRQ_BASE = 32,
> + K230_GPIO1_IRQ_BASE = 64,
> K230_WDT0_IRQ = 107,
> K230_WDT1_IRQ = 108,
> +
Remove this blank line.
> };
>
> #define K230_UART_COUNT 5
Regards,
Bin
On 2026-08-14 15:26:41+08:00, Bin Meng wrote:
> On Mon, Aug 10, 2026 at 4:02 AM guochun wang <guochun.wang@foxmail.com> wrote:
>
> > Implement a K230 SoC APB GPIO controller model for QEMU, derived from
> > the Synopsys DesignWare APB GPIO (the Linux gpio-k230 driver is based
> > on gpio-dwapb.c and shares the same register layout), with a
> > Canaan-specific compatible ("canaan,k230-apb-gpio") and hardlock
> > integration. The model is capable of running the Linux gpio-k230 driver
> > and driving external input/output lines. Behaviour follows the K230
> > Technical Reference Manual v0.3.1.
>
> Please split the k230_gpio model into 2 separate models:
>
> - One is generic Synopsys DesignWare GPIO IP model
> - Optional Canann K230 specific wrapper glue model to the dw-gpio IP
> (if the generic Synopsys DesignWare GPIO IP model, or plus some unimp
> region, is good enough to make the software happy, no need to create
> this one)
Hi Bin Meng,
Thanks for the detailed review.
Regarding the suggestion to abstract this into a generic Synopsys DesignWare APB GPIO model, I have a question: do you know of any other QEMU SoCs that use this IP and would benefit from a shared model?
If there is no immediate second user, I wonder whether it might be premature to generalize now, especially while we still need to complete the K230-specific behavior.
Thanks,
Junze Cao
Hi Junze,
On Fri, Aug 14, 2026 at 5:21 PM Junze Cao <caojunze424@gmail.com> wrote:
>
> On 2026-08-14 15:26:41+08:00, Bin Meng wrote:
> > On Mon, Aug 10, 2026 at 4:02 AM guochun wang <guochun.wang@foxmail.com> wrote:
> >
> > > Implement a K230 SoC APB GPIO controller model for QEMU, derived from
> > > the Synopsys DesignWare APB GPIO (the Linux gpio-k230 driver is based
> > > on gpio-dwapb.c and shares the same register layout), with a
> > > Canaan-specific compatible ("canaan,k230-apb-gpio") and hardlock
> > > integration. The model is capable of running the Linux gpio-k230 driver
> > > and driving external input/output lines. Behaviour follows the K230
> > > Technical Reference Manual v0.3.1.
> >
> > Please split the k230_gpio model into 2 separate models:
> >
> > - One is generic Synopsys DesignWare GPIO IP model
> > - Optional Canann K230 specific wrapper glue model to the dw-gpio IP
> > (if the generic Synopsys DesignWare GPIO IP model, or plus some unimp
> > region, is good enough to make the software happy, no need to create
> > this one)
>
> Hi Bin Meng,
>
> Thanks for the detailed review.
> Regarding the suggestion to abstract this into a generic Synopsys DesignWare APB GPIO model, I have a question: do you know of any other QEMU SoCs that use this IP and would benefit from a shared model?
> If there is no immediate second user, I wonder whether it might be premature to generalize now, especially while we still need to complete the K230-specific behavior.
>
There is no other immediate second user as far as I know. But Synopsys
DesignWare IP is not like others, and it's commonly seen on many SoCs.
For RISC-V, I can see Eswin EIC7700, Sophgo CV1800B (Milk-V Duo),
Sophgo SG2042 / SG2044 and T-Head TH1520 (Lichee Pi 4A) all use the
same Synopsys GPIO IP, and they are all good candidate of QEMU models
in the future.
What K230-specific behavior are you concerned about?
Regards,
Bin
On 2026-08-14 17:50:55+08:00, Bin Meng wrote: > Hi Junze, > > On Fri, Aug 14, 2026 at 5:21 PM Junze Cao <caojunze424@gmail.com> wrote: > > > On 2026-08-14 15:26:41+08:00, Bin Meng wrote: > > > > Hi Bin Meng, > > > > Thanks for the detailed review. > > Regarding the suggestion to abstract this into a generic Synopsys DesignWare APB GPIO model, I have a question: do you know of any other QEMU SoCs that use this IP and would benefit from a shared model? > > If there is no immediate second user, I wonder whether it might be premature to generalize now, especially while we still need to complete the K230-specific behavior. > > There is no other immediate second user as far as I know. But Synopsys > DesignWare IP is not like others, and it's commonly seen on many SoCs. > > For RISC-V, I can see Eswin EIC7700, Sophgo CV1800B (Milk-V Duo), > Sophgo SG2042 / SG2044 and T-Head TH1520 (Lichee Pi 4A) all use the > same Synopsys GPIO IP, and they are all good candidate of QEMU models > in the future. > > What K230-specific behavior are you concerned about? > > Regards, > Bin Hi Bin, Thank you for the clarification and for pointing out the other potential users of this IP. After reconsidering the issue, I realize that my earlier concerns about K230-specific behavior were unfounded. I agree that abstracting this into a generic Synopsys DesignWare APB GPIO model would be a worthwhile approach, and I support moving forward in that direction. Thank you again for your guidance. Best regards, Junze
© 2016 - 2026 Red Hat, Inc.