* [PATCH] gpio: Add Intel Nova Lake ACPI GPIO events driver
@ 2026-03-18 14:24 Alan Borzeszkowski
2026-03-18 14:36 ` Andy Shevchenko
0 siblings, 1 reply; 3+ messages in thread
From: Alan Borzeszkowski @ 2026-03-18 14:24 UTC (permalink / raw)
To: linux-gpio, linusw, brgl
Cc: mika.westerberg, andriy.shevchenko, alan.borzeszkowski
This driver provides support for new way of handling platform events,
through the use of GPIO-signaled ACPI events. This mechanism is used on
Intel client platforms released in 2026 and later, starting with Intel
Nova Lake.
Signed-off-by: Alan Borzeszkowski <alan.borzeszkowski@linux.intel.com>
---
MAINTAINERS | 7 +
drivers/gpio/Kconfig | 26 +++
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-novalake-events.c | 324 ++++++++++++++++++++++++++++
4 files changed, 358 insertions(+)
create mode 100644 drivers/gpio/gpio-novalake-events.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 0d718b6274df..039be0519e58 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12847,6 +12847,13 @@ F: drivers/gpio/gpio-sodaville.c
F: drivers/gpio/gpio-tangier.c
F: drivers/gpio/gpio-tangier.h
+INTEL GPIO GPE DRIVER
+M: Alan Borzeszkowski <alan.borzeszkowski@linux.intel.com>
+R: Mika Westerberg <westeri@kernel.org>
+L: linux-gpio@vger.kernel.org
+S: Supported
+F: drivers/gpio/gpio-novalake-events.c
+
INTEL GVT-g DRIVERS (Intel GPU Virtualization)
R: Zhenyu Wang <zhenyuw.linux@gmail.com>
R: Zhi Wang <zhi.wang.linux@gmail.com>
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index b45fb799e36c..c7a80e5189d8 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -1057,6 +1057,32 @@ config GPIO_SCH
The Intel Quark X1000 SoC has 2 GPIOs powered by the core
power well and 6 from the suspend power well.
+config GPIO_NOVALAKE
+ tristate "Intel Nova Lake GPIO-signaled ACPI events support"
+ depends on (X86 || COMPILE_TEST) && ACPI
+ select GPIOLIB_IRQCHIP
+ help
+ Select this to enable GPIO-signaled ACPI events support on platforms
+ with the following SoCs:
+
+ - Intel Nova Lake
+
+ This driver adds support for new mode of handling platform events,
+ through the use of GPIO-signaled ACPI events. Main purpose is to
+ handle platform IRQs that originate in PCH components, for example
+ interrupt triggered by Power Management Event (PME).
+
+ This driver, at this time, is not required to handle platform events.
+ Listed platform(s) will stay in legacy mode, handling ACPI events as
+ in previous generations. However, future platforms will eventually
+ switch to new handling mode, requiring this driver to run events
+ properly.
+
+ Driver supports up to 128 GPIO pins per GPE block.
+
+ To compile this driver as a module, choose M here: the module will
+ be called gpio-novalake-events.
+
config GPIO_SCH311X
tristate "SMSC SCH311x SuperI/O GPIO"
help
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index c05f7d795c43..f563917cda83 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -134,6 +134,7 @@ obj-$(CONFIG_GPIO_MXC) += gpio-mxc.o
obj-$(CONFIG_GPIO_MXS) += gpio-mxs.o
obj-$(CONFIG_GPIO_NCT6694) += gpio-nct6694.o
obj-$(CONFIG_GPIO_NOMADIK) += gpio-nomadik.o
+obj-$(CONFIG_GPIO_NOVALAKE) += gpio-novalake-events.o
obj-$(CONFIG_GPIO_NPCM_SGPIO) += gpio-npcm-sgpio.o
obj-$(CONFIG_GPIO_OCTEON) += gpio-octeon.o
obj-$(CONFIG_GPIO_OMAP) += gpio-omap.o
diff --git a/drivers/gpio/gpio-novalake-events.c b/drivers/gpio/gpio-novalake-events.c
new file mode 100644
index 000000000000..00f219402826
--- /dev/null
+++ b/drivers/gpio/gpio-novalake-events.c
@@ -0,0 +1,324 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Intel Nova Lake GPIO-signaled ACPI events driver
+ *
+ * Copyright (c) 2026, Intel Corporation.
+ *
+ * Author: Alan Borzeszkowski <alan.borzeszkowski@linux.intel.com>
+ *
+ * Intel client platforms released in 2026 and later (starting with Intel Nova
+ * Lake) support two modes of handling ACPI General Purpose Events (GPE):
+ * exposed GPIO interrupt mode and legacy mode.
+ *
+ * By default, the platform uses legacy mode, handling GPEs as usual. If this
+ * driver is installed, it signals to the platform (on every boot) that exposed
+ * GPIO interrupt mode is supported. The platform then switches to exposed
+ * mode, which takes effect on next boot. From the user perspective, this
+ * change is transparent.
+ *
+ * However, if driver is uninstalled while in exposed interrupt mode, GPEs will
+ * _not_ be handled until platform falls back to legacy mode. This means that
+ * USB keyboard, mouse might not function properly for the fallback duration.
+ * Fallback requires two reboots to take effect: on first reboot, platform no
+ * longer receives signal from this driver and switches to legacy mode, which
+ * takes effect on second boot.
+ *
+ * Example ACPI event: Power Management Event coming from motherboard PCH,
+ * waking system from sleep following USB mouse hotplug.
+ *
+ * This driver supports up to 128 GPIO pins in each GPE block, per ACPI
+ * specification v6.6 section 5.6.4.
+ */
+
+#include <linux/acpi.h>
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/gfp_types.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/ioport.h>
+#include <linux/irq.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include <linux/uuid.h>
+
+#include <linux/gpio/driver.h>
+
+/*
+ * GPE block has two registers, each register takes half the block size.
+ * Convert size to bits to get total GPIO pin count.
+ */
+#define GPE_BLK_REG_SIZE(block_size) ((block_size) / 2)
+#define GPE_REG_PIN_COUNT(block_size) BYTES_TO_BITS(GPE_BLK_REG_SIZE(block_size))
+#define GPE_STS_REG_OFFSET 0
+#define GPE_EN_REG_OFFSET(block_size) GPE_BLK_REG_SIZE(block_size)
+
+/**
+ * struct nvl_gpio - Intel Nova Lake GPIO driver state
+ * @gc: GPIO controller interface
+ * @reg_base: Base address of the GPE registers
+ * @lock: Guard register access
+ * @blk_size: GPE block length
+ */
+struct nvl_gpio {
+ struct gpio_chip gc;
+ void __iomem *reg_base;
+ raw_spinlock_t lock;
+ size_t blk_size;
+};
+
+static void __iomem *nvl_gpio_get_byte_addr(struct nvl_gpio *priv,
+ unsigned int reg_offset,
+ unsigned long gpio)
+{
+ return priv->reg_base + reg_offset + gpio;
+}
+
+static int nvl_gpio_get(struct gpio_chip *gc, unsigned int gpio)
+{
+ struct nvl_gpio *priv = gpiochip_get_data(gc);
+ unsigned int byte_idx = gpio / BITS_PER_BYTE;
+ unsigned int bit_idx = gpio % BITS_PER_BYTE;
+ void __iomem *addr;
+ u8 reg;
+
+ addr = nvl_gpio_get_byte_addr(priv, GPE_STS_REG_OFFSET, byte_idx);
+
+ guard(raw_spinlock_irqsave)(&priv->lock);
+
+ reg = ioread8(addr);
+
+ return !!(reg & BIT(bit_idx));
+}
+
+static const struct gpio_chip nvl_gpio_chip = {
+ .owner = THIS_MODULE,
+ .get = nvl_gpio_get,
+};
+
+static int nvl_gpio_irq_set_type(struct irq_data *d, unsigned int type)
+{
+ if (type & IRQ_TYPE_EDGE_BOTH)
+ irq_set_handler_locked(d, handle_edge_irq);
+ else if (type & IRQ_TYPE_LEVEL_MASK)
+ irq_set_handler_locked(d, handle_level_irq);
+
+ return 0;
+}
+
+static void nvl_gpio_irq_mask_unmask(struct gpio_chip *gc, unsigned long hwirq,
+ bool mask)
+{
+ struct nvl_gpio *priv = gpiochip_get_data(gc);
+ unsigned int byte_idx = hwirq / BITS_PER_BYTE;
+ unsigned int bit_idx = hwirq % BITS_PER_BYTE;
+ void __iomem *addr;
+ u8 reg;
+
+ addr = nvl_gpio_get_byte_addr(priv, GPE_EN_REG_OFFSET(priv->blk_size),
+ byte_idx);
+
+ guard(raw_spinlock_irqsave)(&priv->lock);
+
+ reg = ioread8(addr);
+ if (mask)
+ reg &= ~BIT(bit_idx);
+ else
+ reg |= BIT(bit_idx);
+ iowrite8(reg, addr);
+}
+
+static void nvl_gpio_irq_unmask(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+
+ gpiochip_enable_irq(gc, hwirq);
+ nvl_gpio_irq_mask_unmask(gc, hwirq, false);
+}
+
+static void nvl_gpio_irq_mask(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+
+ nvl_gpio_irq_mask_unmask(gc, hwirq, true);
+ gpiochip_disable_irq(gc, hwirq);
+}
+
+static void nvl_gpio_irq_ack(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct nvl_gpio *priv = gpiochip_get_data(gc);
+ irq_hw_number_t hwirq = irqd_to_hwirq(d);
+ unsigned int byte_idx = hwirq / BITS_PER_BYTE;
+ unsigned int bit_idx = hwirq % BITS_PER_BYTE;
+ void __iomem *addr;
+ u8 reg;
+
+ addr = nvl_gpio_get_byte_addr(priv, GPE_STS_REG_OFFSET, byte_idx);
+
+ guard(raw_spinlock_irqsave)(&priv->lock);
+
+ reg = ioread8(addr);
+ reg |= BIT(bit_idx);
+ iowrite8(reg, addr);
+}
+
+static const struct irq_chip nvl_gpio_irq_chip = {
+ .name = "gpio-novalake",
+ .irq_ack = nvl_gpio_irq_ack,
+ .irq_mask = nvl_gpio_irq_mask,
+ .irq_unmask = nvl_gpio_irq_unmask,
+ .irq_set_type = nvl_gpio_irq_set_type,
+ .flags = IRQCHIP_IMMUTABLE,
+ GPIOCHIP_IRQ_RESOURCE_HELPERS,
+};
+
+static irqreturn_t nvl_gpio_irq(int irq, void *data)
+{
+ struct nvl_gpio *priv = data;
+ const size_t block_size = priv->blk_size;
+ unsigned int handled = 0;
+
+ for (unsigned int i = 0; i < block_size; i++) {
+ const void __iomem *reg = priv->reg_base + i;
+ unsigned long pending;
+ unsigned long enabled;
+ unsigned int bit_idx;
+
+ scoped_guard(raw_spinlock, &priv->lock) {
+ pending = ioread8(reg + GPE_STS_REG_OFFSET);
+ enabled = ioread8(reg + GPE_EN_REG_OFFSET(block_size));
+ }
+ pending &= enabled;
+
+ for_each_set_bit(bit_idx, &pending, BITS_PER_BYTE) {
+ unsigned int hwirq = i * BITS_PER_BYTE + bit_idx;
+
+ generic_handle_domain_irq(priv->gc.irq.domain, hwirq);
+ }
+
+ handled += pending ? 1 : 0;
+ }
+
+ return IRQ_RETVAL(handled);
+}
+
+/* UUID for GPE device _DSM: 079406e6-bdea-49cf-8563-03e2811901cb */
+static const guid_t nvl_gpe_dsm_guid =
+ GUID_INIT(0x079406e6, 0xbdea, 0x49cf,
+ 0x85, 0x63, 0x03, 0xe2, 0x81, 0x19, 0x01, 0xcb);
+
+#define DSM_GPE_MODE_REV 1
+#define DSM_GPE_MODE_FN_INDEX 1
+#define DSM_ENABLE_GPE_MODE 1
+
+static int nvl_acpi_enable_gpe_mode(struct device *dev)
+{
+ union acpi_object argv4[2];
+ union acpi_object *obj;
+
+ argv4[0].type = ACPI_TYPE_PACKAGE;
+ argv4[0].package.count = 1;
+ argv4[0].package.elements = &argv4[1];
+ argv4[1].integer.type = ACPI_TYPE_INTEGER;
+ argv4[1].integer.value = DSM_ENABLE_GPE_MODE;
+
+ obj = acpi_evaluate_dsm_typed(ACPI_HANDLE(dev), &nvl_gpe_dsm_guid,
+ DSM_GPE_MODE_REV, DSM_GPE_MODE_FN_INDEX,
+ argv4, ACPI_TYPE_BUFFER);
+ if (!obj)
+ return -EIO;
+ ACPI_FREE(obj);
+
+ return 0;
+}
+
+static int nvl_gpio_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ resource_size_t ioresource_size;
+ struct gpio_irq_chip *girq;
+ struct nvl_gpio *priv;
+ struct resource *res;
+ void __iomem *regs;
+ int ret, irq;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+ raw_spin_lock_init(&priv->lock);
+
+ res = platform_get_resource(pdev, IORESOURCE_IO, 0);
+ if (!res)
+ return -ENXIO;
+
+ /*
+ * GPE block length should be non-negative multiple of two and allow up
+ * to 128 pins. ACPI v6.6 section 5.2.9 and 5.6.4.
+ */
+ ioresource_size = resource_size(res);
+ if (!ioresource_size || ioresource_size % 2 || ioresource_size > 0x20)
+ return dev_err_probe(dev, -EINVAL,
+ "invalid GPE block length, resource: %pR\n",
+ res);
+
+ regs = devm_ioport_map(dev, res->start, ioresource_size);
+ if (!regs)
+ return -ENOMEM;
+ priv->reg_base = regs;
+ priv->blk_size = ioresource_size;
+
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0)
+ return irq;
+
+ ret = devm_request_irq(dev, irq, nvl_gpio_irq, IRQF_SHARED,
+ dev_name(dev), priv);
+ if (ret)
+ return ret;
+
+ priv->gc = nvl_gpio_chip;
+ priv->gc.label = dev_name(dev);
+ priv->gc.parent = dev;
+ priv->gc.ngpio = GPE_REG_PIN_COUNT(priv->blk_size);
+ priv->gc.base = -1;
+
+ girq = &priv->gc.irq;
+ gpio_irq_chip_set_chip(girq, &nvl_gpio_irq_chip);
+ girq->parent_handler = NULL;
+ girq->num_parents = 0;
+ girq->parents = NULL;
+ girq->default_type = IRQ_TYPE_NONE;
+ girq->handler = handle_bad_irq;
+
+ ret = devm_gpiochip_add_data(dev, &priv->gc, priv);
+ if (ret)
+ return ret;
+
+ return nvl_acpi_enable_gpe_mode(dev);
+}
+
+static const struct acpi_device_id nvl_gpio_acpi_match[] = {
+ { "INTC1114" },
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, nvl_gpio_acpi_match);
+
+static struct platform_driver nvl_gpio_driver = {
+ .driver = {
+ .name = "gpio-novalake-events",
+ .acpi_match_table = nvl_gpio_acpi_match,
+ },
+ .probe = nvl_gpio_probe,
+};
+module_platform_driver(nvl_gpio_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Alan Borzeszkowski <alan.borzeszkowski@linux.intel.com>");
+MODULE_DESCRIPTION("Intel Nova Lake ACPI GPIO events driver");
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread* Re: [PATCH] gpio: Add Intel Nova Lake ACPI GPIO events driver
2026-03-18 14:24 [PATCH] gpio: Add Intel Nova Lake ACPI GPIO events driver Alan Borzeszkowski
@ 2026-03-18 14:36 ` Andy Shevchenko
2026-03-31 17:53 ` Alan Borzeszkowski
0 siblings, 1 reply; 3+ messages in thread
From: Andy Shevchenko @ 2026-03-18 14:36 UTC (permalink / raw)
To: Alan Borzeszkowski; +Cc: linux-gpio, linusw, brgl, mika.westerberg
On Wed, Mar 18, 2026 at 03:24:18PM +0100, Alan Borzeszkowski wrote:
> This driver provides support for new way of handling platform events,
> through the use of GPIO-signaled ACPI events. This mechanism is used on
> Intel client platforms released in 2026 and later, starting with Intel
> Nova Lake.
...
> +static void nvl_gpio_irq_mask_unmask(struct gpio_chip *gc, unsigned long hwirq,
> + bool mask)
> + addr = nvl_gpio_get_byte_addr(priv, GPE_EN_REG_OFFSET(priv->blk_size),
> + byte_idx);
These are only two cases where we go a bit further than 80, so I would just
make them a single line each.
...
> + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> + if (!priv)
> + return -ENOMEM;
> + raw_spin_lock_init(&priv->lock);
> +
> + res = platform_get_resource(pdev, IORESOURCE_IO, 0);
> + if (!res)
> + return -ENXIO;
> + /*
> + * GPE block length should be non-negative multiple of two and allow up
> + * to 128 pins. ACPI v6.6 section 5.2.9 and 5.6.4.
> + */
> + ioresource_size = resource_size(res);
> + if (!ioresource_size || ioresource_size % 2 || ioresource_size > 0x20)
> + return dev_err_probe(dev, -EINVAL,
> + "invalid GPE block length, resource: %pR\n",
> + res);
There is no point to even allocate memory if the resource is not available or
invalid.
..._get_resource()
...validate...
...map, as we still have local variable for that...
priv = devm_kzalloc(...);
...
_init_lock(...);
> + regs = devm_ioport_map(dev, res->start, ioresource_size);
> + if (!regs)
> + return -ENOMEM;
> + priv->reg_base = regs;
> + priv->blk_size = ioresource_size;
> +
> + irq = platform_get_irq(pdev, 0);
> + if (irq < 0)
> + return irq;
> +
> + ret = devm_request_irq(dev, irq, nvl_gpio_irq, IRQF_SHARED,
> + dev_name(dev), priv);
I would also go with a single line here:
ret = devm_request_irq(dev, irq, nvl_gpio_irq, IRQF_SHARED, dev_name(dev), priv);
One Q though, is it indeed shared? Or can it be shared?
> + if (ret)
> + return ret;
--
With Best Regards,
Andy Shevchenko
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] gpio: Add Intel Nova Lake ACPI GPIO events driver
2026-03-18 14:36 ` Andy Shevchenko
@ 2026-03-31 17:53 ` Alan Borzeszkowski
0 siblings, 0 replies; 3+ messages in thread
From: Alan Borzeszkowski @ 2026-03-31 17:53 UTC (permalink / raw)
To: Andy Shevchenko; +Cc: linux-gpio, linusw, brgl, mika.westerberg
Hi Andy,
On Wed, 2026-03-18 at 16:36 +0200, Andy Shevchenko wrote:
> On Wed, Mar 18, 2026 at 03:24:18PM +0100, Alan Borzeszkowski wrote:
> > This driver provides support for new way of handling platform
> > events,
> > through the use of GPIO-signaled ACPI events. This mechanism is
> > used on
> > Intel client platforms released in 2026 and later, starting with
> > Intel
> > Nova Lake.
>
> ...
>
> > +static void nvl_gpio_irq_mask_unmask(struct gpio_chip *gc,
> > unsigned long hwirq,
> > + bool mask)
>
> > + addr = nvl_gpio_get_byte_addr(priv,
> > GPE_EN_REG_OFFSET(priv->blk_size),
> > + byte_idx);
>
> These are only two cases where we go a bit further than 80, so I
> would just
> make them a single line each.
>
>
> ...
>
> > + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> > + if (!priv)
> > + return -ENOMEM;
> > + raw_spin_lock_init(&priv->lock);
> > +
> > + res = platform_get_resource(pdev, IORESOURCE_IO, 0);
> > + if (!res)
> > + return -ENXIO;
> > + /*
> > + * GPE block length should be non-negative multiple of two
> > and allow up
> > + * to 128 pins. ACPI v6.6 section 5.2.9 and 5.6.4.
> > + */
> > + ioresource_size = resource_size(res);
> > + if (!ioresource_size || ioresource_size % 2 ||
> > ioresource_size > 0x20)
> > + return dev_err_probe(dev, -EINVAL,
> > + "invalid GPE block length,
> > resource: %pR\n",
> > + res);
>
> There is no point to even allocate memory if the resource is not
> available or
> invalid.
>
> ..._get_resource()
> ...validate...
> ...map, as we still have local variable for that...
>
> priv = devm_kzalloc(...);
> ...
> _init_lock(...);
>
>
> > + regs = devm_ioport_map(dev, res->start, ioresource_size);
> > + if (!regs)
> > + return -ENOMEM;
> > + priv->reg_base = regs;
> > + priv->blk_size = ioresource_size;
> > +
> > + irq = platform_get_irq(pdev, 0);
> > + if (irq < 0)
> > + return irq;
> > +
> > + ret = devm_request_irq(dev, irq, nvl_gpio_irq,
> > IRQF_SHARED,
> > + dev_name(dev), priv);
>
> I would also go with a single line here:
>
> ret = devm_request_irq(dev, irq, nvl_gpio_irq, IRQF_SHARED,
> dev_name(dev), priv);
>
> One Q though, is it indeed shared? Or can it be shared?
It can be shared.
>
> > + if (ret)
> > + return ret;
>
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-03-31 17:56 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-18 14:24 [PATCH] gpio: Add Intel Nova Lake ACPI GPIO events driver Alan Borzeszkowski
2026-03-18 14:36 ` Andy Shevchenko
2026-03-31 17:53 ` Alan Borzeszkowski
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox