[PATCH 1/3] hw/gpio: Allwinner A10 GPIO emulation

Strahinja Jankovic <[email protected]>
Newsgroups gmane.comp.emulators.qemu
Message-ID <[email protected]>
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
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.