This patch implements Allwinner A10 GPIO peripheral emulation.

The emulation focuses on input/output and interrupt functionality,
whereas the drive and pull up/down functionality is not emulated.

Signed-off-by: Strahinja Jankovic <[email protected]>
---
 MAINTAINERS                           |   2 +
 hw/gpio/Kconfig                       |   3 +
 hw/gpio/allwinner-gpio.c              | 465 ++++++++++++++++++++++++++++++++++
 hw/gpio/meson.build                   |   1 +
 hw/gpio/trace-events                  |   6 +
 include/hw/gpio/allwinner-gpio-regs.h | 228 +++++++++++++++++
 include/hw/gpio/allwinner-gpio.h      |  41 +++
 7 files changed, 746 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index b51f5c3e60..6cefdde70b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -743,6 +743,8 @@ F: include/hw/*/allwinner*
 F: hw/arm/cubieboard.c
 F: docs/system/arm/cubieboard.rst
 F: hw/misc/axp2xx.c
+F: hw/gpio/allwinner-gpio.c
+F: include/hw/gpio/allwinner-gpio*
 F: tests/functional/arm/test_cubieboard.py
 
 Allwinner-h3
diff --git a/hw/gpio/Kconfig b/hw/gpio/Kconfig
index fcc7c70bd5..ea0fb72b40 100644
--- a/hw/gpio/Kconfig
+++ b/hw/gpio/Kconfig
@@ -33,3 +33,6 @@ config ZAURUS_SCOOP
 
 config CADENCE_GPIO
     bool
+
+config ALLWINNER_GPIO
+    bool
diff --git a/hw/gpio/allwinner-gpio.c b/hw/gpio/allwinner-gpio.c
new file mode 100644
index 0000000000..fb0f3f7ca1
--- /dev/null
+++ b/hw/gpio/allwinner-gpio.c
@@ -0,0 +1,465 @@
+/*
+ *  Allwinner GPIO Emulation
+ *
+ *  Copyright (C) 2026 Strahinja Jankovic <[email protected]>
+ *
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/gpio/allwinner-gpio.h"
+#include "hw/gpio/allwinner-gpio-regs.h"
+#include "hw/core/irq.h"
+#include "hw/core/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "trace.h"
+
+#define REG_INDEX(offset)         (offset / sizeof(uint32_t))
+
+#define AW_GPIO_SET(port) \
+    static inline void allwinner_gpio_set_##port(void *opaque, \
+                                                 int line, \
+                                                 int level) { \
+        allwinner_gpio_set(opaque, port, line, level); \
+    }
+
+static void allwinner_gpio_set(void *opaque, int port, int line, int level);
+
+AW_GPIO_SET(GPIO_PA);
+AW_GPIO_SET(GPIO_PB);
+AW_GPIO_SET(GPIO_PC);
+AW_GPIO_SET(GPIO_PD);
+AW_GPIO_SET(GPIO_PE);
+AW_GPIO_SET(GPIO_PF);
+AW_GPIO_SET(GPIO_PG);
+AW_GPIO_SET(GPIO_PH);
+AW_GPIO_SET(GPIO_PI);
+
+static void (*AW_GPIO_IN_HANDLER[AW_GPIO_PORTS_NUM]) (void *opaque,
+                                                      int line,
+                                                      int level) = {
+    allwinner_gpio_set_GPIO_PA,
+    allwinner_gpio_set_GPIO_PB,
+    allwinner_gpio_set_GPIO_PC,
+    allwinner_gpio_set_GPIO_PD,
+    allwinner_gpio_set_GPIO_PE,
+    allwinner_gpio_set_GPIO_PF,
+    allwinner_gpio_set_GPIO_PG,
+    allwinner_gpio_set_GPIO_PH,
+    allwinner_gpio_set_GPIO_PI,
+};
+
+static char *allwinner_gpio_get_regname(unsigned offset)
+{
+    switch (offset) {
+    case 0 ... GPIO_Pn_PUL1(GPIO_PI):
+    {
+        const char *regname = "?";
+        switch (offset % PORT_STRIDE) {
+        case CFG0:
+            regname = "CFG0";
+            break;
+        case CFG1:
+            regname = "CFG1";
+            break;
+        case CFG2:
+            regname = "CFG2";
+            break;
+        case CFG3:
+            regname = "CFG3";
+            break;
+        case DAT:
+            regname = "DAT";
+            break;
+        case DRV0:
+            regname = "DRV0";
+            break;
+        case DRV1:
+            regname = "DRV1";
+            break;
+        case PUL0:
+            regname = "PUL0";
+            break;
+        case PUL1:
+            regname = "PUL1";
+            break;
+        }
+        return g_strdup_printf("%s:%s",
+                               portname(offset / PORT_STRIDE),
+                               regname);
+    }
+    case GPIO_INT_CFG0:
+        return g_strdup("INT_CFG0");
+    case GPIO_INT_CFG1:
+        return g_strdup("INT_CFG1");
+    case GPIO_INT_CFG2:
+        return g_strdup("INT_CFG2");
+    case GPIO_INT_CFG3:
+        return g_strdup("INT_CFG3");
+    case GPIO_INT_CTL:
+        return g_strdup("INT_CTL");
+    case GPIO_INT_STA:
+        return g_strdup("INT_STA");
+    case GPIO_INT_DEB:
+        return g_strdup("INT_DEB");
+    default:
+        return g_strdup("[?]");
+    }
+}
+
+static bool gpio_is_input(AWPortMap *port, uint32_t pin);
+static int int_ctl_cfg(AWGPIOState *s, int irq_line);
+
+static inline void allwinner_gpio_update_int(AWGPIOState *s)
+{
+    qemu_set_irq(
+        s->irq,
+        !!(s->regs[REG_INDEX(GPIO_INT_CTL)] & s->regs[REG_INDEX(GPIO_INT_STA)])
+    );
+}
+
+static void allwinner_gpio_set_int_line(AWGPIOState *s, int port,
+                                        int line, AWGPIOLevel level)
+{
+    AWPortsOverlay *o = (AWPortsOverlay *)s->regs;
+    int irq_line = irq_nr(port, line);
+    bool raise_irq = false;
+    unsigned input_val;
+    int int_cfg;
+
+    /* Check if pin can actually trigger an interrupt */
+    if (irq_line == -1) {
+        return;
+    }
+
+    /* if this signal isn't configured as an input signal, nothing to do */
+    if (!gpio_is_input(&o->ports[port], line)) {
+        return;
+    }
+
+    /* Get current input value */
+    input_val = extract32(o->ports[port].dat, line, 1);
+    int_cfg = int_ctl_cfg(s, irq_line);
+
+    switch (int_cfg) {
+    case AW_GPIO_IRQ_CFG_RISING_EDGE:
+        if (!input_val && level) {
+            raise_irq = true;
+        }
+        break;
+    case AW_GPIO_IRQ_CFG_FALLING_EDGE:
+        if (input_val && !level) {
+            raise_irq = true;
+        }
+        break;
+    case AW_GPIO_IRQ_CFG_HIGH_LEVEL:
+        if (level) {
+            raise_irq = true;
+        }
+        break;
+    case AW_GPIO_IRQ_CFG_LOW_LEVEL:
+        if (!level) {
+            raise_irq = true;
+        }
+        break;
+    case AW_GPIO_IRQ_CFG_BOTH_EDGE:
+        if (input_val != level) {
+            raise_irq = true;
+        }
+        break;
+    default:
+        /* Unexpected */
+        break;
+    }
+
+    if (raise_irq) {
+        s->regs[REG_INDEX(GPIO_INT_STA)] |= 1u << irq_line;
+    }
+}
+
+static void port_set_all_int_lines(AWGPIOState *s, int port)
+{
+    AWPortsOverlay *o = (AWPortsOverlay *)s->regs;
+    int i;
+
+    for (i = 0; i < AW_PINS_PER_PORT[port]; i++) {
+        AWGPIOLevel aw_level = extract32(o->ports[port].dat, i, 1);
+        allwinner_gpio_set_int_line(s, port, i, aw_level);
+    }
+
+    allwinner_gpio_update_int(s);
+}
+
+static void allwinner_set_all_int_lines(AWGPIOState *s)
+{
+    int i;
+
+    for (i = 0; i < AW_GPIO_PORTS_NUM; i++) {
+        port_set_all_int_lines(s, i);
+    }
+}
+
+static void allwinner_gpio_set(void *opaque, int port, int line, int level)
+{
+    AWGPIOState *s = AW_GPIO(opaque);
+    AWPortsOverlay *o = (AWPortsOverlay *)s->regs;
+    AWGPIOLevel aw_level = level ? AW_GPIO_LEVEL_HIGH : AW_GPIO_LEVEL_LOW;
+
+    if (gpio_is_input(&o->ports[port], line)) {
+
+        trace_allwinner_gpio_set(portname(port), line, aw_level);
+
+        allwinner_gpio_set_int_line(s, port, line, aw_level);
+
+        o->ports[port].dat = (o->ports[port].dat & ~(1u << line)) |
+                             (aw_level << line);
+    }
+    allwinner_gpio_update_int(s);
+}
+
+
+static inline bool gpio_is_output(AWPortMap *port, uint32_t pin)
+{
+    uint32_t cfg_n = pin / CFG_PINS_PER_REG;
+    uint32_t pin_shift = (pin % CFG_PINS_PER_REG) * CFG_PIN_STRIDE;
+    return (extract32(port->cfg[cfg_n], pin_shift, CFG_PIN_STRIDE - 1) ==
+        CFG_OUTPUT_MASK);
+}
+
+static inline bool gpio_is_input(AWPortMap *port, uint32_t pin)
+{
+    uint32_t cfg_n = pin / CFG_PINS_PER_REG;
+    uint32_t pin_shift = (pin % CFG_PINS_PER_REG) * CFG_PIN_STRIDE;
+    return (extract32(port->cfg[cfg_n], pin_shift, CFG_PIN_STRIDE - 1) ==
+        CFG_INPUT_MASK);
+}
+
+static inline int int_ctl_cfg(AWGPIOState *s, int irq_line)
+{
+    unsigned int_cfg_n = s->regs[REG_INDEX(GPIO_INT_CFG0) + irq_line / 8];
+    return extract32(int_cfg_n,
+                     (irq_line % INT_CFG_IRQ_PER_REG) * INT_CFG_IRQ_STRIDE,
+                     INT_CFG_IRQ_STRIDE - 1);
+}
+
+static inline void port_update_output_lines(AWGPIOState *s, uint32_t port)
+{
+    AWPortsOverlay *o = (AWPortsOverlay *)s->regs;
+    int pin;
+
+    for (pin = 0; pin < AW_PINS_PER_PORT[port]; pin++) {
+        if (gpio_is_output(&o->ports[port], pin)) {
+            qemu_set_irq(s->output[port][pin],
+                         !!(o->ports[port].dat & BIT_MASK(pin)));
+            trace_allwinner_gpio_out_pin(
+                portname(port), pin,
+                !!(o->ports[port].dat & BIT_MASK(pin)));
+         }
+        else if (gpio_is_input(&o->ports[port], pin)) {
+            qemu_irq_lower(s->output[port][pin]);
+        }
+    }
+}
+
+static inline void allwinner_gpio_update_all_output_lines(AWGPIOState *s)
+{
+    for (int port = 0; port < AW_GPIO_PORTS_NUM; port++) {
+        port_update_output_lines(s, port);
+    }
+}
+
+
+static uint64_t allwinner_gpio_read(void *opaque, hwaddr offset, unsigned size)
+{
+    AWGPIOState *s = AW_GPIO(opaque);
+    uint32_t reg_value = 0;
+    g_autofree char *regname = allwinner_gpio_get_regname(offset);
+
+    switch (offset) {
+    case 0 ... GPIO_Pn_PUL1(GPIO_PI):
+    case GPIO_INT_CFG0:
+    case GPIO_INT_CFG1:
+    case GPIO_INT_CFG2:
+    case GPIO_INT_CFG3:
+    case GPIO_INT_CTL:
+    case GPIO_INT_STA:
+    case GPIO_INT_DEB:
+        reg_value = s->regs[REG_INDEX(offset)];
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR, "[%s]%s: Bad register at offset 0x%"
+                      HWADDR_PRIx "\n", TYPE_AW_GPIO, __func__, offset);
+        break;
+    }
+
+    trace_allwinner_gpio_read(regname, reg_value);
+
+    return reg_value;
+}
+
+/* update output values */
+/* update interrupts */
+static void allwinner_port_write(AWGPIOState *s, hwaddr offset, uint64_t value)
+{
+    AWPortsOverlay *o = (AWPortsOverlay *)s->regs;
+    uint32_t port = offset / PORT_STRIDE;
+    uint32_t reg = offset % PORT_STRIDE;
+
+    switch (reg) {
+    case CFG0:
+        o->ports[port].cfg[0] =
+            value & GPIO_CFG0_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    case CFG1:
+        o->ports[port].cfg[1] =
+            value & GPIO_CFG1_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    case CFG2:
+        o->ports[port].cfg[2] =
+            value & GPIO_CFG2_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    case CFG3:
+        o->ports[port].cfg[3] =
+            value & GPIO_CFG3_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    case DAT:
+        o->ports[port].dat =
+            value & GPIO_DAT_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    case DRV0:
+        o->ports[port].drv[0] =
+            value & GPIO_DRV0_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    case DRV1:
+        o->ports[port].drv[1] =
+            value & GPIO_DRV1_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    case PUL0:
+        o->ports[port].pul[0] =
+            value & GPIO_PUL0_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    case PUL1:
+        o->ports[port].pul[1] =
+            value & GPIO_PUL1_PINS_MASK(AW_PINS_PER_PORT[port]);
+        break;
+    }
+    port_update_output_lines(s, port);
+    port_set_all_int_lines(s, port);
+}
+
+static void allwinner_gpio_write(void *opaque, hwaddr offset, uint64_t value,
+                           unsigned size)
+{
+    AWGPIOState *s = AW_GPIO(opaque);
+    g_autofree char *regname = allwinner_gpio_get_regname(offset);
+
+    trace_allwinner_gpio_write(regname, value);
+
+    switch (offset) {
+    case 0 ... GPIO_Pn_PUL1(GPIO_PI):
+        allwinner_port_write(s, offset, value);
+        break;
+    case GPIO_INT_CFG0:
+    case GPIO_INT_CFG1:
+    case GPIO_INT_CFG2:
+    case GPIO_INT_CFG3:
+    case GPIO_INT_CTL:
+    case GPIO_INT_DEB:
+        s->regs[REG_INDEX(offset)] = value;
+        /* update interrupts */
+        allwinner_set_all_int_lines(s);
+        break;
+    case GPIO_INT_STA:
+        /* W1C */
+        s->regs[REG_INDEX(offset)] &= ~value;
+        allwinner_set_all_int_lines(s);
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR, "[%s]%s: Bad register at offset 0x%"
+                      HWADDR_PRIx "\n", TYPE_AW_GPIO, __func__, offset);
+        break;
+    }
+}
+
+static const MemoryRegionOps allwinner_gpio_ops = {
+    .read = allwinner_gpio_read,
+    .write = allwinner_gpio_write,
+    .valid.min_access_size = 4,
+    .valid.max_access_size = 4,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+};
+
+static const VMStateDescription vmstate_allwinner_gpio = {
+    .name = TYPE_AW_GPIO,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, AWGPIOState, AW_GPIO_REGS_NUM),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void allwinner_gpio_reset(DeviceState *dev)
+{
+    AWGPIOState *s = AW_GPIO(dev);
+    AWPortsOverlay *o = (AWPortsOverlay *)s->regs;
+    int port = 0;
+
+    memset(s->regs, 0, AW_GPIO_IOSIZE);
+
+    for (port = 0; port < AW_GPIO_PORTS_NUM; port++) {
+        o->ports[port].drv[0] = aw_gpio_port_reset[port].drv[0];
+        o->ports[port].drv[1] = aw_gpio_port_reset[port].drv[1];
+        o->ports[port].pul[0] = aw_gpio_port_reset[port].pul[0];
+        o->ports[port].pul[1] = aw_gpio_port_reset[port].pul[1];
+    }
+
+    allwinner_gpio_update_all_output_lines(s);
+    allwinner_gpio_update_int(s);
+}
+
+static void allwinner_gpio_realize(DeviceState *dev, Error **errp)
+{
+    AWGPIOState *s = AW_GPIO(dev);
+    int port;
+
+    memory_region_init_io(&s->iomem, OBJECT(s), &allwinner_gpio_ops, s,
+                          TYPE_AW_GPIO, AW_GPIO_IOSIZE);
+
+    for (port = 0; port < AW_GPIO_PORTS_NUM; port++) {
+        g_autofree char *in_name = portname_in(port);
+        g_autofree char *out_name = portname_out(port);
+        qdev_init_gpio_in_named(dev, AW_GPIO_IN_HANDLER[port],
+                                in_name, AW_PINS_PER_PORT[port]);
+        qdev_init_gpio_out_named(dev, s->output[port],
+                                 out_name, AW_PINS_PER_PORT[port]);
+    }
+    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);
+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
+}
+
+static void allwinner_gpio_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = allwinner_gpio_realize;
+    device_class_set_legacy_reset(dc, allwinner_gpio_reset);
+    dc->vmsd = &vmstate_allwinner_gpio;
+    dc->desc = "Allwinner GPIO controller";
+}
+
+static const TypeInfo allwinner_gpio_info = {
+    .name = TYPE_AW_GPIO,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(AWGPIOState),
+    .class_init = allwinner_gpio_class_init,
+};
+
+static void allwinner_gpio_register_types(void)
+{
+    type_register_static(&allwinner_gpio_info);
+}
+
+type_init(allwinner_gpio_register_types)
diff --git a/hw/gpio/meson.build b/hw/gpio/meson.build
index 0555f44b6a..8ede5e88ae 100644
--- a/hw/gpio/meson.build
+++ b/hw/gpio/meson.build
@@ -20,3 +20,4 @@ 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_CADENCE_GPIO', if_true: files('cadence_gpio.c'))
+system_ss.add(when: 'CONFIG_ALLWINNER_GPIO', if_true: 
files('allwinner-gpio.c'))
diff --git a/hw/gpio/trace-events b/hw/gpio/trace-events
index 80ca783a03..5ef3e0b8fd 100644
--- a/hw/gpio/trace-events
+++ b/hw/gpio/trace-events
@@ -51,3 +51,9 @@ stm32l4x5_gpio_pins(char *gpio, uint16_t disconnected, 
uint16_t high) "GPIO%s di
 cdns_gpio_read(const char *path, uint64_t offset, uint32_t value) 
"%s:reg[0x%04" PRIx64 "] -> 0x%" PRIx32
 cdns_gpio_write(const char *path, uint64_t offset, uint64_t value) 
"%s:reg[0x%04" PRIx64 "] <- 0x%04" PRIx64
 cdns_gpio_set(const char *path, int line, int level) "%s:[%d] <- %d"
+
+# allwinner_gpio.c
+allwinner_gpio_set(const char *port, int line, int level) "[%s:%d] <- %d"
+allwinner_gpio_read(const char *reg, uint32_t value) "[%s] -> 0x%" PRIx32
+allwinner_gpio_write(const char *reg, uint32_t value) "[%s] <- 0x%" PRIx32
+allwinner_gpio_out_pin(const char *reg, uint32_t pin, uint32_t value) "[%s.%d] 
<- 0x%" PRIx32
diff --git a/include/hw/gpio/allwinner-gpio-regs.h 
b/include/hw/gpio/allwinner-gpio-regs.h
new file mode 100644
index 0000000000..52d72c0f25
--- /dev/null
+++ b/include/hw/gpio/allwinner-gpio-regs.h
@@ -0,0 +1,228 @@
+/*
+ *  Allwinner GPIO registers definition
+ *
+ *  Copyright (C) 2026 Strahinja Jankovic. <[email protected]>
+ *
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef ALLWINNER_GPIO_REGS_H
+#define ALLWINNER_GPIO_REGS_H
+
+/* GPIO ports index (n) */
+#define GPIO_PA     0
+#define GPIO_PB     1
+#define GPIO_PC     2
+#define GPIO_PD     3
+#define GPIO_PE     4
+#define GPIO_PF     5
+#define GPIO_PG     6
+#define GPIO_PH     7
+#define GPIO_PI     8
+
+#ifndef AW_GPIO_PORTS_NUM
+#define AW_GPIO_PORTS_NUM 9
+#endif
+
+#ifndef AW_GPIO_PIN_COUNT
+#define AW_GPIO_PIN_COUNT 32
+#endif
+
+typedef struct AWPortMap {
+    uint32_t cfg[4];
+    uint32_t dat;
+    uint32_t drv[2];
+    uint32_t pul[2];
+} AWPortMap;
+
+typedef struct AWPortsOverlay {
+    AWPortMap ports[AW_GPIO_PORTS_NUM];
+} AWPortsOverlay;
+
+static const uint32_t AW_PINS_PER_PORT[AW_GPIO_PORTS_NUM] = {
+    18,
+    24,
+    25,
+    28,
+    12,
+    6,
+    12,
+    28,
+    22
+};
+
+typedef struct {
+    uint32_t drv[2];
+    uint32_t pul[2];
+} AWPortResetVals;
+
+static const AWPortResetVals aw_gpio_port_reset[] = {
+    [GPIO_PA] = { .drv = { 0x55555555, 0x00000005 } },
+    [GPIO_PB] = { .drv = { 0x55555555, 0x00005555 } },
+    [GPIO_PC] = { .drv = { 0x55555555, 0x00015555 },
+                  .pul = { 0x00005140, 0x00004016 } },
+    [GPIO_PD] = { .drv = { 0x55555555, 0x00555555 } },
+    [GPIO_PE] = { .drv = { 0x00555555, 0x0 } },
+    [GPIO_PF] = { .drv = { 0x00000555, 0x0 } },
+    [GPIO_PG] = { .drv = { 0x05555555, 0x0 } },
+    [GPIO_PH] = { .drv = { 0x55555555, 0x00555555 } },
+    [GPIO_PI] = { .drv = { 0x55555555, 0x00000555 } },
+};
+
+#define DEFAULT_CFG_MASK    0x77777777
+#define DEFAULT_DRV_MASK    0xffffffff
+#define DEFAULT_PUL_MASK    0xffffffff
+
+#define CFG_INPUT_MASK      0x0
+#define CFG_OUTPUT_MASK     0x1
+#define CFG_IO_MASK         0x1
+#define CFG_PIN_STRIDE      4
+#define CFG_PINS_PER_REG    (AW_GPIO_PIN_COUNT / CFG_PIN_STRIDE)
+
+#define AW_INT_GPIO_COUNT   32
+#define INT_CFG_IRQ_STRIDE  4
+#define INT_CFG_IRQ_PER_REG (AW_INT_GPIO_COUNT / INT_CFG_IRQ_STRIDE)
+
+/* GPIO masks per number of pins */
+#define GPIO_CFG0_PINS_MASK(pins) \
+    ((pins >= CFG_PINS_PER_REG) ? DEFAULT_CFG_MASK : \
+    ((1 << (pins * CFG_PIN_STRIDE)) - 1) & DEFAULT_CFG_MASK)
+
+#define GPIO_CFG1_PINS_MASK(pins) \
+    ((pins >= 16) ? DEFAULT_CFG_MASK : \
+    ((pins > 8) ? ((1 << ((pins - 8) * 4)) - 1) & DEFAULT_CFG_MASK : 0))
+
+#define GPIO_CFG2_PINS_MASK(pins) \
+    ((pins >= 24) ? DEFAULT_CFG_MASK : \
+    ((pins > 16) ? ((1 << ((pins - 16) * 4)) - 1) & DEFAULT_CFG_MASK : 0))
+
+#define GPIO_CFG3_PINS_MASK(pins) \
+    ((pins > 24) ? ((1 << ((pins - 24) * 4)) - 1) & DEFAULT_CFG_MASK : 0)
+
+#define GPIO_DAT_PINS_MASK(pins)    ((1 << pins) - 1)
+
+#define GPIO_DRV0_PINS_MASK(pins) \
+    ((pins > 16) ? DEFAULT_DRV_MASK : ((1 << (pins * 2)) - 1))
+
+#define GPIO_DRV1_PINS_MASK(pins) \
+    ((pins > 16) ? ((1 << ((pins - 16) * 2)) - 1) : 0)
+
+#define GPIO_PUL0_PINS_MASK(pins) \
+    ((pins > 16) ? DEFAULT_PUL_MASK : ((1 << (pins * 2)) - 1))
+
+#define GPIO_PUL1_PINS_MASK(pins) \
+    ((pins > 16) ? ((1 << ((pins - 16) * 2)) - 1) : 0)
+
+
+#define PORT_STRIDE     0x24
+
+/* Allwinner GPIO memory map */
+#define CFG0    0x00 /* Configure register 0 */
+#define CFG1    0x04 /* Configure register 1 */
+#define CFG2    0x08 /* Configure register 2 */
+#define CFG3    0x0c /* Configure register 3 */
+#define DAT     0x10 /* Data register */
+#define DRV0    0x14 /* Multi-driving register 0 */
+#define DRV1    0x18 /* Multi-driving register 1 */
+#define PUL0    0x1c /* Pull register 0 */
+#define PUL1    0x20 /* Pull register 1 */
+
+/* Port n configure register 0 */
+#define GPIO_Pn_CFG0(n)   (n*PORT_STRIDE + CFG0)
+/* Port n configure register 1 */
+#define GPIO_Pn_CFG1(n)   (n*PORT_STRIDE + CFG1)
+/* Port n configure register 2 */
+#define GPIO_Pn_CFG2(n)   (n*PORT_STRIDE + CFG2)
+/* Port n configure register 3 */
+#define GPIO_Pn_CFG3(n)   (n*PORT_STRIDE + CFG3)
+/* Port n data register */
+#define GPIO_Pn_DAT(n)    (n*PORT_STRIDE + DAT)
+/* Port n Multi-driving register 0 */
+#define GPIO_Pn_DRV0(n)   (n*PORT_STRIDE + DRV0)
+/* Port n Multi-driving register 1 */
+#define GPIO_Pn_DRV1(n)   (n*PORT_STRIDE + DRV1)
+/* Port n Pull register 0 */
+#define GPIO_Pn_PUL0(n)   (n*PORT_STRIDE + PUL0)
+/* Port n Pull register 1 */
+#define GPIO_Pn_PUL1(n)   (n*PORT_STRIDE + PUL1)
+/* PIO interrupt configure register 0 */
+#define GPIO_INT_CFG0       0x200
+/* PIO interrupt configure register 1 */
+#define GPIO_INT_CFG1       0x204
+/* PIO interrupt configure register 2 */
+#define GPIO_INT_CFG2       0x208
+/* PIO interrupt configure register 3 */
+#define GPIO_INT_CFG3       0x20c
+/* PIO interrupt control register */
+#define GPIO_INT_CTL        0x210
+/* PIO interrupt status register */
+#define GPIO_INT_STA        0x214
+/* PIO interrupt debounce register */
+#define GPIO_INT_DEB        0x218
+/* SDRAM Pad Multi-driving register */
+#define SDR_PAD_DRV         0x220
+/* SDRAM Pad Pull register */
+#define SDR_PAD_PUL         0x224
+
+typedef enum AWGPIOLevel {
+    AW_GPIO_LEVEL_LOW = 0,
+    AW_GPIO_LEVEL_HIGH = 1,
+} AWGPIOLevel;
+
+typedef enum AWGPIOCfg {
+    AW_GPIO_CFG_IN = 0,
+    AW_GPIO_CFG_OUT = 1,
+    AW_GPIO_CFG_FUN = 2,
+    /* Use only fun for testing */
+} AWGPIOCfg;
+
+typedef enum AWGPIOIrqCfg {
+    AW_GPIO_IRQ_CFG_RISING_EDGE = 0,
+    AW_GPIO_IRQ_CFG_FALLING_EDGE = 1,
+    AW_GPIO_IRQ_CFG_HIGH_LEVEL = 2,
+    AW_GPIO_IRQ_CFG_LOW_LEVEL = 3,
+    AW_GPIO_IRQ_CFG_BOTH_EDGE = 4
+} AWGPIOIrqCfg;
+
+static const char *const port_name_table[] = {
+    "PA", "PB", "PC", "PD", "PE", "PF", "PG", "PH", "PI"
+};
+
+static const char *portname(unsigned index)
+{
+    return index < AW_GPIO_PORTS_NUM ? port_name_table[index] : "?";
+}
+
+static inline char *portname_in(int port)
+{
+    return g_strconcat(portname(port), "_IN", NULL);
+}
+
+static inline char *portname_out(int port)
+{
+    return g_strconcat(portname(port), "_OUT", NULL);
+}
+
+static int irq_nr(int port, int line)
+{
+    int irq = -1;
+
+    switch (port) {
+    case GPIO_PH:
+        if (line <= 21) {
+            irq = line;
+        }
+        break;
+    case GPIO_PI:
+        if ((line >= 10) && (line <= 19)) {
+            irq = line + 12;
+        }
+        break;
+    default:
+        break;
+    }
+    return irq;
+}
+
+
+#endif /* ALLWINNER_GPIO_REGS_H */
diff --git a/include/hw/gpio/allwinner-gpio.h b/include/hw/gpio/allwinner-gpio.h
new file mode 100644
index 0000000000..f6778d1893
--- /dev/null
+++ b/include/hw/gpio/allwinner-gpio.h
@@ -0,0 +1,41 @@
+/*
+ *  Allwinner GPIO registers definition
+ *
+ *  Copyright (C) 2026 Strahinja Jankovic. <[email protected]>
+ *
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef ALLWINNER_GPIO_H
+#define ALLWINNER_GPIO_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+
+/** Size of register I/O address space used by GPIO device */
+#define AW_GPIO_IOSIZE (0x400)
+
+/** Total number of known registers */
+#define AW_GPIO_REGS_NUM    (AW_GPIO_IOSIZE / sizeof(uint32_t))
+
+/** Max number of ports */
+#define AW_GPIO_PORTS_NUM   9
+/** Max number of pins per ports */
+#define AW_GPIO_PIN_COUNT 32
+
+#define TYPE_AW_GPIO        "allwinner.gpio"
+OBJECT_DECLARE_SIMPLE_TYPE(AWGPIOState, AW_GPIO)
+
+struct AWGPIOState {
+    /*< private >*/
+    SysBusDevice parent_obj;
+
+    /*< public >*/
+    MemoryRegion iomem;
+    qemu_irq irq;
+    qemu_irq output[AW_GPIO_PORTS_NUM][AW_GPIO_PIN_COUNT];
+
+    uint32_t regs[AW_GPIO_REGS_NUM];
+};
+
+#endif /* ALLWINNER_GPIO_H */

-- 
2.55.0


Reply via email to