]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
irqchip/ast2700-intc: Add AST2700-A2 support
authorRyan Chen <ryan_chen@aspeedtech.com>
Tue, 7 Apr 2026 03:08:05 +0000 (11:08 +0800)
committerThomas Gleixner <tglx@kernel.org>
Thu, 30 Apr 2026 10:53:03 +0000 (12:53 +0200)
The AST2700 interrupt fabric is shared by multiple integrated processors
(PSP/SSP/TSP/BootMCU), each with its own interrupt controller and its own
devicetree view of the system. As a result, interrupt routing cannot be
treated as fixed: the valid route for a peripheral interrupt depends on
which processor is consuming it.

The INTC0 driver models this by creating a hierarchical irqdomain under
the upstream interrupt controller selected by the interrupt-parent
property in the devicetree. Information derived from this relationship
is incorporated into the route resolution logic for the controller.

The INTC1 driver implements the banked INTM-fed controller and forwards
interrupts toward INTC0, without embedding assumptions about the final
destination processor.

Signed-off-by: Ryan Chen <ryan_chen@aspeedtech.com>
Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Link: https://patch.msgid.link/20260407-irqchip-v5-2-c0b0a300a057@aspeedtech.com
drivers/irqchip/Kconfig
drivers/irqchip/Makefile
drivers/irqchip/irq-ast2700-intc0.c [new file with mode: 0644]
drivers/irqchip/irq-ast2700-intc1.c [new file with mode: 0644]
drivers/irqchip/irq-ast2700.c [new file with mode: 0644]
drivers/irqchip/irq-ast2700.h [new file with mode: 0644]

index e755a2a05209657f9ea84d5daf7da19378c455e6..cb446e1234579d4fbaedf5a67a7ebc87480ffcd5 100644 (file)
@@ -110,6 +110,18 @@ config AL_FIC
        help
          Support Amazon's Annapurna Labs Fabric Interrupt Controller.
 
+config ASPEED_AST2700_INTC
+       bool "ASPEED AST2700 Interrupt Controller support"
+       depends on OF
+       depends on ARCH_ASPEED || COMPILE_TEST
+       select IRQ_DOMAIN_HIERARCHY
+       help
+         Enable support for the ASPEED AST2700 interrupt controller.
+         This driver handles interrupt, routing and merged interrupt
+         sources to upstream parent interrupt controllers.
+
+         If unsure, say N.
+
 config ATMEL_AIC_IRQ
        bool
        select GENERIC_IRQ_CHIP
index 26aa3b6ec99fd4b45a0bfd8a934752d7295bfe8c..62790663f982323eff3160364cac0d12141ea194 100644 (file)
@@ -89,6 +89,7 @@ obj-$(CONFIG_MVEBU_PIC)                       += irq-mvebu-pic.o
 obj-$(CONFIG_MVEBU_SEI)                        += irq-mvebu-sei.o
 obj-$(CONFIG_LS_EXTIRQ)                        += irq-ls-extirq.o
 obj-$(CONFIG_LS_SCFG_MSI)              += irq-ls-scfg-msi.o
+obj-$(CONFIG_ASPEED_AST2700_INTC)      += irq-ast2700.o irq-ast2700-intc0.o irq-ast2700-intc1.o
 obj-$(CONFIG_ARCH_ASPEED)              += irq-aspeed-vic.o irq-aspeed-i2c-ic.o irq-aspeed-scu-ic.o
 obj-$(CONFIG_ARCH_ASPEED)              += irq-aspeed-intc.o
 obj-$(CONFIG_STM32MP_EXTI)             += irq-stm32mp-exti.o
diff --git a/drivers/irqchip/irq-ast2700-intc0.c b/drivers/irqchip/irq-ast2700-intc0.c
new file mode 100644 (file)
index 0000000..65e17b2
--- /dev/null
@@ -0,0 +1,581 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ *  Aspeed AST2700 Interrupt Controller.
+ *
+ *  Copyright (C) 2026 ASPEED Technology Inc.
+ */
+
+#include <linux/bitops.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/fwnode.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/irqchip.h>
+#include <linux/irqchip/chained_irq.h>
+#include <linux/irqdomain.h>
+#include <linux/kconfig.h>
+#include <linux/of.h>
+#include <linux/of_irq.h>
+#include <linux/overflow.h>
+#include <linux/property.h>
+#include <linux/spinlock.h>
+
+#include "irq-ast2700.h"
+
+#define INT_NUM                480
+#define INTM_NUM       50
+#define SWINT_NUM      16
+
+#define INTM_BASE      (INT_NUM)
+#define SWINT_BASE     (INT_NUM + INTM_NUM)
+#define INT0_NUM       (INT_NUM + INTM_NUM + SWINT_NUM)
+
+#define INTC0_IN_NUM           480
+#define INTC0_ROUTE_NUM                5
+#define INTC0_INTM_NUM         50
+#define INTC0_ROUTE_BITS       3
+
+#define GIC_P2P_SPI_END                128
+#define INTC0_SWINT_OUT_BASE   144
+
+#define INTC0_SWINT_IER                0x10
+#define INTC0_SWINT_ISR                0x14
+#define INTC0_INTBANKX_IER     0x1000
+#define INTC0_INTBANK_SIZE     0x100
+#define INTC0_INTBANK_GROUPS   11
+#define INTC0_INTBANKS_PER_GRP 3
+#define INTC0_INTMX_IER                0x1b00
+#define INTC0_INTMX_ISR                0x1b04
+#define INTC0_INTMX_BANK_SIZE  0x10
+#define INTC0_INTM_BANK_NUM    3
+#define INTC0_IRQS_PER_BANK    32
+#define INTM_IRQS_PER_BANK     10
+#define INTC0_SEL_BASE                 0x200
+#define INTC0_SEL_BANK_SIZE            0x4
+#define INTC0_SEL_ROUTE_SIZE   0x100
+
+static void aspeed_swint_irq_mask(struct irq_data *data)
+{
+       struct aspeed_intc0 *intc0 = irq_data_get_irq_chip_data(data);
+       int bit = data->hwirq - SWINT_BASE;
+       u32 ier;
+
+       guard(raw_spinlock)(&intc0->intc_lock);
+       ier = readl(intc0->base + INTC0_SWINT_IER) & ~BIT(bit);
+       writel(ier, intc0->base + INTC0_SWINT_IER);
+       irq_chip_mask_parent(data);
+}
+
+static void aspeed_swint_irq_unmask(struct irq_data *data)
+{
+       struct aspeed_intc0 *intc0 = irq_data_get_irq_chip_data(data);
+       int bit = data->hwirq - SWINT_BASE;
+       u32 ier;
+
+       guard(raw_spinlock)(&intc0->intc_lock);
+       ier = readl(intc0->base + INTC0_SWINT_IER) | BIT(bit);
+       writel(ier, intc0->base + INTC0_SWINT_IER);
+       irq_chip_unmask_parent(data);
+}
+
+static void aspeed_swint_irq_eoi(struct irq_data *data)
+{
+       struct aspeed_intc0 *intc0 = irq_data_get_irq_chip_data(data);
+       int bit = data->hwirq - SWINT_BASE;
+
+       writel(BIT(bit), intc0->base + INTC0_SWINT_ISR);
+       irq_chip_eoi_parent(data);
+}
+
+static struct irq_chip aspeed_swint_chip = {
+       .name                   = "ast2700-swint",
+       .irq_eoi                = aspeed_swint_irq_eoi,
+       .irq_mask               = aspeed_swint_irq_mask,
+       .irq_unmask             = aspeed_swint_irq_unmask,
+       .irq_set_affinity       = irq_chip_set_affinity_parent,
+       .flags                  = IRQCHIP_SET_TYPE_MASKED,
+};
+
+static void aspeed_intc0_irq_mask(struct irq_data *data)
+{
+       struct aspeed_intc0 *intc0 = irq_data_get_irq_chip_data(data);
+       int bank = (data->hwirq - INTM_BASE) / INTM_IRQS_PER_BANK;
+       int bit = (data->hwirq - INTM_BASE) % INTM_IRQS_PER_BANK;
+       u32 ier;
+
+       guard(raw_spinlock)(&intc0->intc_lock);
+       ier = readl(intc0->base + INTC0_INTMX_IER + bank * INTC0_INTMX_BANK_SIZE) & ~BIT(bit);
+       writel(ier, intc0->base + INTC0_INTMX_IER + bank * INTC0_INTMX_BANK_SIZE);
+       irq_chip_mask_parent(data);
+}
+
+static void aspeed_intc0_irq_unmask(struct irq_data *data)
+{
+       struct aspeed_intc0 *intc0 = irq_data_get_irq_chip_data(data);
+       int bank = (data->hwirq - INTM_BASE) / INTM_IRQS_PER_BANK;
+       int bit = (data->hwirq - INTM_BASE) % INTM_IRQS_PER_BANK;
+       u32 ier;
+
+       guard(raw_spinlock)(&intc0->intc_lock);
+       ier = readl(intc0->base + INTC0_INTMX_IER + bank * INTC0_INTMX_BANK_SIZE) | BIT(bit);
+       writel(ier, intc0->base + INTC0_INTMX_IER + bank * INTC0_INTMX_BANK_SIZE);
+       irq_chip_unmask_parent(data);
+}
+
+static void aspeed_intc0_irq_eoi(struct irq_data *data)
+{
+       struct aspeed_intc0 *intc0 = irq_data_get_irq_chip_data(data);
+       int bank = (data->hwirq - INTM_BASE) / INTM_IRQS_PER_BANK;
+       int bit = (data->hwirq - INTM_BASE) % INTM_IRQS_PER_BANK;
+
+       writel(BIT(bit), intc0->base + INTC0_INTMX_ISR + bank * INTC0_INTMX_BANK_SIZE);
+       irq_chip_eoi_parent(data);
+}
+
+static struct irq_chip aspeed_intm_chip = {
+       .name                   = "ast2700-intmerge",
+       .irq_eoi                = aspeed_intc0_irq_eoi,
+       .irq_mask               = aspeed_intc0_irq_mask,
+       .irq_unmask             = aspeed_intc0_irq_unmask,
+       .irq_set_affinity       = irq_chip_set_affinity_parent,
+       .flags                  = IRQCHIP_SET_TYPE_MASKED,
+};
+
+static struct irq_chip linear_intr_irq_chip = {
+       .name                   = "ast2700-int",
+       .irq_eoi                = irq_chip_eoi_parent,
+       .irq_mask               = irq_chip_mask_parent,
+       .irq_unmask             = irq_chip_unmask_parent,
+       .irq_set_affinity       = irq_chip_set_affinity_parent,
+       .flags                  = IRQCHIP_SET_TYPE_MASKED,
+};
+
+static const u32 aspeed_intc0_routes[INTC0_IN_NUM / INTC0_IRQS_PER_BANK][INTC0_ROUTE_NUM] = {
+       { 0, 256, 426, AST2700_INTC_INVALID_ROUTE, AST2700_INTC_INVALID_ROUTE },
+       { 32, 288, 458, AST2700_INTC_INVALID_ROUTE, AST2700_INTC_INVALID_ROUTE },
+       { 64, 320, 490, AST2700_INTC_INVALID_ROUTE, AST2700_INTC_INVALID_ROUTE },
+       { 96, 352, 522, AST2700_INTC_INVALID_ROUTE, AST2700_INTC_INVALID_ROUTE },
+       { 128, 384, 554, 160, 176 },
+       { 129, 385, 555, 161, 177 },
+       { 130, 386, 556, 162, 178 },
+       { 131, 387, 557, 163, 179 },
+       { 132, 388, 558, 164, 180 },
+       { 133, 544, 714, 165, 181 },
+       { 134, 545, 715, 166, 182 },
+       { 135, 546, 706, 167, 183 },
+       { 136, 547, 707, 168, 184 },
+       { 137, 548, 708, 169, 185 },
+       { 138, 549, 709, 170, 186 },
+};
+
+static const u32 aspeed_intc0_intm_routes[INTC0_INTM_NUM / INTM_IRQS_PER_BANK] = {
+       192, 416, 586, 208, 224
+};
+
+static int resolve_input_from_child_ranges(const struct aspeed_intc0 *intc0,
+                                          const struct aspeed_intc_interrupt_range *range,
+                                          u32 outpin, u32 *input)
+{
+       u32 offset, base;
+
+       if (!in_range32(outpin, range->start, range->count))
+               return -ENOENT;
+
+       if (range->upstream.param_count == 0)
+               return -EINVAL;
+
+       base = range->upstream.param[ASPEED_INTC_RANGES_BASE];
+       offset = outpin - range->start;
+       if (check_add_overflow(base, offset, input)) {
+               dev_warn(intc0->dev, "%s: Arithmetic overflow for input derivation: %u + %u\n",
+                        __func__, base, offset);
+               return -EINVAL;
+       }
+       return 0;
+}
+
+static int resolve_parent_range_for_output(const struct aspeed_intc0 *intc0,
+                                          const struct fwnode_handle *parent, u32 output,
+                                          struct aspeed_intc_interrupt_range *resolved)
+{
+       for (size_t i = 0; i < intc0->ranges.nranges; i++) {
+               struct aspeed_intc_interrupt_range range = intc0->ranges.ranges[i];
+
+               if (!in_range32(output, range.start, range.count))
+                       continue;
+
+               if (range.upstream.fwnode != parent)
+                       continue;
+
+               if (resolved) {
+                       resolved->start = output;
+                       resolved->count = 1;
+                       resolved->upstream = range.upstream;
+                       resolved->upstream.param[ASPEED_INTC_RANGES_COUNT] +=
+                               output - range.start;
+               }
+
+               return 0;
+       }
+
+       return -ENOENT;
+}
+
+static int resolve_parent_route_for_input(const struct aspeed_intc0 *intc0,
+                                         const struct fwnode_handle *parent, u32 input,
+                                         struct aspeed_intc_interrupt_range *resolved)
+{
+       int rc = -ENOENT;
+       u32 c0o;
+
+       if (input < INT_NUM) {
+               static_assert(INTC0_ROUTE_NUM < INT_MAX, "Broken cast");
+               for (size_t i = 0; rc == -ENOENT && i < INTC0_ROUTE_NUM; i++) {
+                       c0o = aspeed_intc0_routes[input / INTC0_IRQS_PER_BANK][i];
+                       if (c0o == AST2700_INTC_INVALID_ROUTE)
+                               continue;
+
+                       if (input < GIC_P2P_SPI_END)
+                               c0o += input % INTC0_IRQS_PER_BANK;
+
+                       rc = resolve_parent_range_for_output(intc0, parent, c0o, resolved);
+                       if (!rc)
+                               return (int)i;
+               }
+       } else if (input < (INT_NUM + INTM_NUM)) {
+               c0o = aspeed_intc0_intm_routes[(input - INT_NUM) / INTM_IRQS_PER_BANK];
+               c0o += ((input - INT_NUM) % INTM_IRQS_PER_BANK);
+               return resolve_parent_range_for_output(intc0, parent, c0o, resolved);
+       } else if (input < (INT_NUM + INTM_NUM + SWINT_NUM)) {
+               c0o = input - SWINT_BASE + INTC0_SWINT_OUT_BASE;
+               return resolve_parent_range_for_output(intc0, parent, c0o, resolved);
+       } else {
+               return -ENOENT;
+       }
+
+       return rc;
+}
+
+/**
+ * aspeed_intc0_resolve_route - Determine the necessary interrupt output at intc1
+ * @c0domain: The pointer to intc0's irq_domain
+ * @nc1outs: The number of valid intc1 outputs available for the input
+ * @c1outs: The array of available intc1 output indices for the input
+ * @nc1ranges: The number of interrupt range entries for intc1
+ * @c1ranges: The array of configured intc1 interrupt ranges
+ * @resolved: The fully resolved range entry after applying the resolution
+ *            algorithm
+ *
+ * Returns: The intc1 route index associated with the intc1 output identified in
+ * @resolved on success. Otherwise, a negative errno value.
+ *
+ * The AST2700 interrupt architecture allows any peripheral interrupt source
+ * to be routed to one of up to four processors running in the SoC. A processor
+ * binding a driver for a peripheral that requests an interrupt is (without
+ * further design and effort) the destination for the requested interrupt.
+ *
+ * Routing a peripheral interrupt to its destination processor requires
+ * coordination between INTC0 on the CPU die and one or more INTC1 instances.
+ * At least one INTC1 instance exists in the SoC on the IO-die, however up
+ * to two more instances may be integrated via LTPI (LVDS Tunneling Protocol
+ * & Interface).
+ *
+ * Between the multiple destinations, various route constraints, and the
+ * devicetree binding design, some information that's needed at INTC1 instances
+ * to route inbound interrupts correctly to the destination processor is only
+ * available at INTC0.
+ *
+ * aspeed_intc0_resolve_route() is to be invoked by INTC1 driver instances to
+ * perform the route resolution. The implementation in INTC0 allows INTC0 to
+ * encapsulate the information used to perform route selection, and provides it
+ * with an opportunity to apply policy as part of the selection process. Such
+ * policy may, for instance, choose to de-prioritise some interrupts destined
+ * for the PSP (Primary Service Processor) GIC.
+ */
+int aspeed_intc0_resolve_route(const struct irq_domain *c0domain, size_t nc1outs,
+                              const u32 *c1outs, size_t nc1ranges,
+                              const struct aspeed_intc_interrupt_range *c1ranges,
+                              struct aspeed_intc_interrupt_range *resolved)
+{
+       struct fwnode_handle *parent_fwnode;
+       struct aspeed_intc0 *intc0;
+       int ret;
+
+       if (!c0domain || !resolved)
+               return -EINVAL;
+
+       if (nc1outs > INT_MAX)
+               return -EINVAL;
+
+       if (nc1outs == 0 || nc1ranges == 0)
+               return -ENOENT;
+
+       if (!fwnode_device_is_compatible(c0domain->fwnode, "aspeed,ast2700-intc0"))
+               return -ENODEV;
+
+       intc0 = c0domain->host_data;
+       if (!intc0)
+               return -EINVAL;
+
+       parent_fwnode = of_fwnode_handle(intc0->parent);
+
+       for (size_t i = 0; i < nc1outs; i++) {
+               u32 c1o = c1outs[i];
+
+               if (c1o == AST2700_INTC_INVALID_ROUTE)
+                       continue;
+
+               for (size_t j = 0; j < nc1ranges; j++) {
+                       struct aspeed_intc_interrupt_range c1r = c1ranges[j];
+                       u32 input;
+
+                       /*
+                        * Range match for intc1 output pin
+                        *
+                        * Assume a failed match is still a match for the purpose of testing,
+                        * saves a bunch of mess in the test fixtures
+                        */
+                       if (!(c0domain == c1r.domain ||
+                             IS_ENABLED(CONFIG_ASPEED_AST2700_INTC_TEST)))
+                               continue;
+
+                       ret = resolve_input_from_child_ranges(intc0, &c1r, c1o, &input);
+                       if (ret)
+                               continue;
+
+                       /*
+                        * INTC1 should never request routes for peripheral interrupt sources
+                        * directly attached to INTC0.
+                        */
+                       if (input < GIC_P2P_SPI_END)
+                               continue;
+
+                       ret = resolve_parent_route_for_input(intc0, parent_fwnode, input, NULL);
+                       if (ret < 0)
+                               continue;
+
+                       /* Route resolution succeeded */
+                       resolved->start = c1o;
+                       resolved->count = 1;
+                       resolved->upstream = c1r.upstream;
+                       resolved->upstream.param[ASPEED_INTC_RANGES_BASE] = input;
+                       /* Cast protected by prior test against nc1outs */
+                       return (int)i;
+               }
+       }
+
+       return -ENOENT;
+}
+
+static int aspeed_intc0_irq_domain_map(struct irq_domain *domain,
+                                      unsigned int irq, irq_hw_number_t hwirq)
+{
+       if (hwirq < GIC_P2P_SPI_END)
+               irq_set_chip_and_handler(irq, &linear_intr_irq_chip, handle_level_irq);
+       else if (hwirq < INTM_BASE)
+               return -EINVAL;
+       else if (hwirq < SWINT_BASE)
+               irq_set_chip_and_handler(irq, &aspeed_intm_chip, handle_level_irq);
+       else if (hwirq < INT0_NUM)
+               irq_set_chip_and_handler(irq, &aspeed_swint_chip, handle_level_irq);
+       else
+               return -EINVAL;
+
+       irq_set_chip_data(irq, domain->host_data);
+       return 0;
+}
+
+static int aspeed_intc0_irq_domain_translate(struct irq_domain *domain,
+                                            struct irq_fwspec *fwspec,
+                                            unsigned long *hwirq,
+                                            unsigned int *type)
+{
+       if (fwspec->param_count != 1)
+               return -EINVAL;
+
+       *hwirq = fwspec->param[0];
+       *type = IRQ_TYPE_NONE;
+       return 0;
+}
+
+static int aspeed_intc0_irq_domain_alloc(struct irq_domain *domain,
+                                        unsigned int virq,
+                                        unsigned int nr_irqs, void *data)
+{
+       struct aspeed_intc0 *intc0 = domain->host_data;
+       struct aspeed_intc_interrupt_range resolved;
+       struct irq_fwspec *fwspec = data;
+       struct irq_fwspec parent_fwspec;
+       struct irq_chip *chip;
+       unsigned long hwirq;
+       unsigned int type;
+       int ret;
+
+       ret = aspeed_intc0_irq_domain_translate(domain, fwspec, &hwirq, &type);
+       if (ret)
+               return ret;
+
+       if (hwirq >= GIC_P2P_SPI_END && hwirq < INT_NUM)
+               return -EINVAL;
+
+       if (hwirq < INTM_BASE)
+               chip = &linear_intr_irq_chip;
+       else if (hwirq < SWINT_BASE)
+               chip = &aspeed_intm_chip;
+       else
+               chip = &aspeed_swint_chip;
+
+       ret = resolve_parent_route_for_input(intc0, domain->parent->fwnode,
+                                            (u32)hwirq, &resolved);
+       if (ret)
+               return ret;
+
+       parent_fwspec = resolved.upstream;
+       ret = irq_domain_alloc_irqs_parent(domain, virq, nr_irqs,
+                                          &parent_fwspec);
+       if (ret)
+               return ret;
+
+       for (int i = 0; i < nr_irqs; ++i, ++hwirq, ++virq) {
+               ret = irq_domain_set_hwirq_and_chip(domain, virq, hwirq, chip,
+                                                   domain->host_data);
+               if (ret)
+                       return ret;
+       }
+
+       return 0;
+}
+
+static int aspeed_intc0_irq_domain_activate(struct irq_domain *domain,
+                                           struct irq_data *data, bool reserve)
+{
+       struct aspeed_intc0 *intc0 = irq_data_get_irq_chip_data(data);
+       unsigned long hwirq = data->hwirq;
+       int route, bank, bit;
+       u32 mask;
+
+       if (hwirq >= INT0_NUM)
+               return -EINVAL;
+
+       if (in_range32(hwirq, INTM_BASE, INTM_NUM + SWINT_NUM))
+               return 0;
+
+       bank = hwirq / INTC0_IRQS_PER_BANK;
+       bit = hwirq % INTC0_IRQS_PER_BANK;
+       mask = BIT(bit);
+
+       route = resolve_parent_route_for_input(intc0, intc0->local->parent->fwnode,
+                                              hwirq, NULL);
+       if (route < 0)
+               return route;
+
+       guard(raw_spinlock)(&intc0->intc_lock);
+       for (int i = 0; i < INTC0_ROUTE_BITS; i++) {
+               void __iomem *sel = intc0->base + INTC0_SEL_BASE +
+                                   (bank * INTC0_SEL_BANK_SIZE) +
+                                   (INTC0_SEL_ROUTE_SIZE * i);
+               u32 reg = readl(sel);
+
+               if (route & BIT(i))
+                       reg |= mask;
+               else
+                       reg &= ~mask;
+
+               writel(reg, sel);
+               if (readl(sel) != reg)
+                       return -EACCES;
+       }
+
+       return 0;
+}
+
+static const struct irq_domain_ops aspeed_intc0_irq_domain_ops = {
+       .translate      = aspeed_intc0_irq_domain_translate,
+       .activate       = aspeed_intc0_irq_domain_activate,
+       .alloc          = aspeed_intc0_irq_domain_alloc,
+       .free           = irq_domain_free_irqs_common,
+       .map            = aspeed_intc0_irq_domain_map,
+};
+
+static void aspeed_intc0_disable_swint(struct aspeed_intc0 *intc0)
+{
+       writel(0, intc0->base + INTC0_SWINT_IER);
+}
+
+static void aspeed_intc0_disable_intbank(struct aspeed_intc0 *intc0)
+{
+       for (int i = 0; i < INTC0_INTBANK_GROUPS; i++) {
+               for (int j = 0; j < INTC0_INTBANKS_PER_GRP; j++) {
+                       u32 base = INTC0_INTBANKX_IER +
+                                  (INTC0_INTBANK_SIZE * i) +
+                                  (INTC0_INTMX_BANK_SIZE * j);
+
+                       writel(0, intc0->base + base);
+               }
+       }
+}
+
+static void aspeed_intc0_disable_intm(struct aspeed_intc0 *intc0)
+{
+       for (int i = 0; i < INTC0_INTM_BANK_NUM; i++)
+               writel(0, intc0->base + INTC0_INTMX_IER + (INTC0_INTMX_BANK_SIZE * i));
+}
+
+static int aspeed_intc0_probe(struct platform_device *pdev,
+                             struct device_node *parent)
+{
+       struct device_node *node = pdev->dev.of_node;
+       struct irq_domain *parent_domain;
+       struct aspeed_intc0 *intc0;
+       int ret;
+
+       if (!parent) {
+               pr_err("missing parent interrupt node\n");
+               return -ENODEV;
+       }
+
+       intc0 = devm_kzalloc(&pdev->dev, sizeof(*intc0), GFP_KERNEL);
+       if (!intc0)
+               return -ENOMEM;
+
+       intc0->dev = &pdev->dev;
+       intc0->parent = parent;
+       intc0->base = devm_platform_ioremap_resource(pdev, 0);
+       if (IS_ERR(intc0->base))
+               return PTR_ERR(intc0->base);
+
+       aspeed_intc0_disable_swint(intc0);
+       aspeed_intc0_disable_intbank(intc0);
+       aspeed_intc0_disable_intm(intc0);
+
+       raw_spin_lock_init(&intc0->intc_lock);
+
+       parent_domain = irq_find_host(parent);
+       if (!parent_domain) {
+               pr_err("unable to obtain parent domain\n");
+               return -ENODEV;
+       }
+
+       if (!of_device_is_compatible(parent, "arm,gic-v3"))
+               return -ENODEV;
+
+       intc0->local = irq_domain_create_hierarchy(parent_domain, 0, INT0_NUM,
+                                                  of_fwnode_handle(node),
+                                                  &aspeed_intc0_irq_domain_ops,
+                                                  intc0);
+       if (!intc0->local)
+               return -ENOMEM;
+
+       ret = aspeed_intc_populate_ranges(&pdev->dev, &intc0->ranges);
+       if (ret < 0) {
+               irq_domain_remove(intc0->local);
+               return ret;
+       }
+
+       return 0;
+}
+
+IRQCHIP_PLATFORM_DRIVER_BEGIN(ast2700_intc0)
+IRQCHIP_MATCH("aspeed,ast2700-intc0", aspeed_intc0_probe)
+IRQCHIP_PLATFORM_DRIVER_END(ast2700_intc0)
diff --git a/drivers/irqchip/irq-ast2700-intc1.c b/drivers/irqchip/irq-ast2700-intc1.c
new file mode 100644 (file)
index 0000000..59e8f0d
--- /dev/null
@@ -0,0 +1,280 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ *  Aspeed AST2700 Interrupt Controller.
+ *
+ *  Copyright (C) 2026 ASPEED Technology Inc.
+ */
+
+#include <linux/bitops.h>
+#include <linux/device.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/irqchip.h>
+#include <linux/irqchip/chained_irq.h>
+#include <linux/irqdomain.h>
+#include <linux/of.h>
+#include <linux/of_irq.h>
+#include <linux/spinlock.h>
+
+#include "irq-ast2700.h"
+
+#define INTC1_IER                      0x100
+#define INTC1_ISR                      0x104
+#define INTC1_BANK_SIZE                0x10
+#define INTC1_SEL_BASE                 0x80
+#define INTC1_SEL_BANK_SIZE            0x4
+#define INTC1_SEL_ROUTE_SIZE   0x20
+#define INTC1_IRQS_PER_BANK            32
+#define INTC1_BANK_NUM                 6
+#define INTC1_ROUTE_NUM                        7
+#define INTC1_IN_NUM                   192
+#define INTC1_BOOTMCU_ROUTE            6
+#define INTC1_ROUTE_SELECTOR_BITS      3
+#define INTC1_ROUTE_IRQS_PER_GROUP     32
+#define INTC1_ROUTE_SHIFT              5
+
+struct aspeed_intc1 {
+       struct device                           *dev;
+       void __iomem                            *base;
+       raw_spinlock_t                          intc_lock;
+       struct irq_domain                       *local;
+       struct irq_domain                       *upstream;
+       struct aspeed_intc_interrupt_ranges     ranges;
+};
+
+static void aspeed_intc1_disable_int(struct aspeed_intc1 *intc1)
+{
+       for (int i = 0; i < INTC1_BANK_NUM; i++)
+               writel(0, intc1->base + INTC1_IER + (INTC1_BANK_SIZE * i));
+}
+
+static void aspeed_intc1_irq_handler(struct irq_desc *desc)
+{
+       struct aspeed_intc1 *intc1 = irq_desc_get_handler_data(desc);
+       struct irq_chip *chip = irq_desc_get_chip(desc);
+       unsigned long bit, status;
+
+       chained_irq_enter(chip, desc);
+
+       for (int bank = 0; bank < INTC1_BANK_NUM; bank++) {
+               status = readl(intc1->base + INTC1_ISR + (INTC1_BANK_SIZE * bank));
+               if (!status)
+                       continue;
+
+               for_each_set_bit(bit, &status, INTC1_IRQS_PER_BANK) {
+                       generic_handle_domain_irq(intc1->local, (bank * INTC1_IRQS_PER_BANK) + bit);
+                       writel(BIT(bit), intc1->base + INTC1_ISR + (INTC1_BANK_SIZE * bank));
+               }
+       }
+
+       chained_irq_exit(chip, desc);
+}
+
+static void aspeed_intc1_irq_mask(struct irq_data *data)
+{
+       struct aspeed_intc1 *intc1 = irq_data_get_irq_chip_data(data);
+       int bank = data->hwirq / INTC1_IRQS_PER_BANK;
+       int bit = data->hwirq % INTC1_IRQS_PER_BANK;
+       u32 ier;
+
+       guard(raw_spinlock)(&intc1->intc_lock);
+       ier = readl(intc1->base + INTC1_IER + (INTC1_BANK_SIZE * bank)) & ~BIT(bit);
+       writel(ier, intc1->base + INTC1_IER + (INTC1_BANK_SIZE * bank));
+}
+
+static void aspeed_intc1_irq_unmask(struct irq_data *data)
+{
+       struct aspeed_intc1 *intc1 = irq_data_get_irq_chip_data(data);
+       int bank = data->hwirq / INTC1_IRQS_PER_BANK;
+       int bit = data->hwirq % INTC1_IRQS_PER_BANK;
+       u32 ier;
+
+       guard(raw_spinlock)(&intc1->intc_lock);
+       ier = readl(intc1->base + INTC1_IER + (INTC1_BANK_SIZE * bank)) | BIT(bit);
+       writel(ier, intc1->base + INTC1_IER + (INTC1_BANK_SIZE * bank));
+}
+
+static struct irq_chip aspeed_intc_chip = {
+       .name           = "ASPEED INTC1",
+       .irq_mask       = aspeed_intc1_irq_mask,
+       .irq_unmask     = aspeed_intc1_irq_unmask,
+};
+
+static int aspeed_intc1_irq_domain_translate(struct irq_domain *domain,
+                                            struct irq_fwspec *fwspec,
+                                            unsigned long *hwirq,
+                                            unsigned int *type)
+{
+       if (fwspec->param_count != 1)
+               return -EINVAL;
+
+       *hwirq = fwspec->param[0];
+       *type = IRQ_TYPE_LEVEL_HIGH;
+       return 0;
+}
+
+static int aspeed_intc1_map_irq_domain(struct irq_domain *domain,
+                                      unsigned int irq,
+                                      irq_hw_number_t hwirq)
+{
+       irq_domain_set_info(domain, irq, hwirq, &aspeed_intc_chip,
+                           domain->host_data, handle_level_irq, NULL, NULL);
+       return 0;
+}
+
+/*
+ * In-bound interrupts are progressively merged into one out-bound interrupt in
+ * groups of 32. Apply this fact to compress the route table in corresponding
+ * groups of 32.
+ */
+static const u32
+aspeed_intc1_routes[INTC1_IN_NUM / INTC1_ROUTE_IRQS_PER_GROUP][INTC1_ROUTE_NUM] = {
+       { 0, AST2700_INTC_INVALID_ROUTE, 10, 20, 30, 40, 50 },
+       { 1, AST2700_INTC_INVALID_ROUTE, 11, 21, 31, 41, 50 },
+       { 2, AST2700_INTC_INVALID_ROUTE, 12, 22, 32, 42, 50 },
+       { 3, AST2700_INTC_INVALID_ROUTE, 13, 23, 33, 43, 50 },
+       { 4, AST2700_INTC_INVALID_ROUTE, 14, 24, 34, 44, 50 },
+       { 5, AST2700_INTC_INVALID_ROUTE, 15, 25, 35, 45, 50 },
+};
+
+static int aspeed_intc1_irq_domain_activate(struct irq_domain *domain,
+                                           struct irq_data *data, bool reserve)
+{
+       struct aspeed_intc1 *intc1 = irq_data_get_irq_chip_data(data);
+       struct aspeed_intc_interrupt_range resolved;
+       int rc, bank, bit;
+       u32 mask;
+
+       if (WARN_ON_ONCE((data->hwirq >> INTC1_ROUTE_SHIFT) >= ARRAY_SIZE(aspeed_intc1_routes)))
+               return -EINVAL;
+
+       /*
+        * outpin may be an error if the upstream is the BootMCU APLIC node, or
+        * anything except a valid intc0 driver instance
+        */
+       rc = aspeed_intc0_resolve_route(intc1->upstream, INTC1_ROUTE_NUM,
+                                       aspeed_intc1_routes[data->hwirq >> INTC1_ROUTE_SHIFT],
+                                       intc1->ranges.nranges,
+                                       intc1->ranges.ranges, &resolved);
+       if (rc < 0) {
+               if (!fwnode_device_is_compatible(intc1->upstream->fwnode, "riscv,aplic")) {
+                       dev_warn(intc1->dev,
+                                "Failed to resolve interrupt route for hwirq %lu in domain %s\n",
+                                data->hwirq, domain->name);
+                       return rc;
+               }
+               rc = INTC1_BOOTMCU_ROUTE;
+       }
+
+       bank = data->hwirq / INTC1_IRQS_PER_BANK;
+       bit = data->hwirq % INTC1_IRQS_PER_BANK;
+       mask = BIT(bit);
+
+       guard(raw_spinlock)(&intc1->intc_lock);
+       for (int i = 0; i < INTC1_ROUTE_SELECTOR_BITS; i++) {
+               void __iomem *sel = intc1->base + INTC1_SEL_BASE +
+                                   (bank * INTC1_SEL_BANK_SIZE) +
+                                   (INTC1_SEL_ROUTE_SIZE * i);
+               u32 reg = readl(sel);
+
+               if (rc & BIT(i))
+                       reg |= mask;
+               else
+                       reg &= ~mask;
+
+               writel(reg, sel);
+               if (readl(sel) != reg)
+                       return -EACCES;
+       }
+
+       return 0;
+}
+
+static const struct irq_domain_ops aspeed_intc1_irq_domain_ops = {
+       .map            = aspeed_intc1_map_irq_domain,
+       .translate      = aspeed_intc1_irq_domain_translate,
+       .activate       = aspeed_intc1_irq_domain_activate,
+};
+
+static void aspeed_intc1_request_interrupts(struct aspeed_intc1 *intc1)
+{
+       for (unsigned int i = 0; i < intc1->ranges.nranges; i++) {
+               struct aspeed_intc_interrupt_range *r =
+                       &intc1->ranges.ranges[i];
+
+               if (intc1->upstream != r->domain)
+                       continue;
+
+               for (u32 k = 0; k < r->count; k++) {
+                       struct of_phandle_args parent_irq;
+                       int irq;
+
+                       parent_irq.np = to_of_node(r->upstream.fwnode);
+                       parent_irq.args_count = 1;
+                       parent_irq.args[0] =
+                               intc1->ranges.ranges[i].upstream.param[ASPEED_INTC_RANGES_BASE] + k;
+
+                       irq = irq_create_of_mapping(&parent_irq);
+                       if (!irq)
+                               continue;
+
+                       irq_set_chained_handler_and_data(irq,
+                                                        aspeed_intc1_irq_handler, intc1);
+               }
+       }
+}
+
+static int aspeed_intc1_probe(struct platform_device *pdev,
+                             struct device_node *parent)
+{
+       struct device_node *node = pdev->dev.of_node;
+       struct aspeed_intc1 *intc1;
+       struct irq_domain *host;
+       int ret;
+
+       if (!parent) {
+               dev_err(&pdev->dev, "missing parent interrupt node\n");
+               return -ENODEV;
+       }
+
+       if (!of_device_is_compatible(parent, "aspeed,ast2700-intc0"))
+               return -ENODEV;
+
+       host = irq_find_host(parent);
+       if (!host)
+               return -ENODEV;
+
+       intc1 = devm_kzalloc(&pdev->dev, sizeof(*intc1), GFP_KERNEL);
+       if (!intc1)
+               return -ENOMEM;
+
+       intc1->dev = &pdev->dev;
+       intc1->upstream = host;
+       intc1->base = devm_platform_ioremap_resource(pdev, 0);
+       if (IS_ERR(intc1->base))
+               return PTR_ERR(intc1->base);
+
+       aspeed_intc1_disable_int(intc1);
+
+       raw_spin_lock_init(&intc1->intc_lock);
+
+       intc1->local = irq_domain_create_linear(of_fwnode_handle(node),
+                                               INTC1_BANK_NUM * INTC1_IRQS_PER_BANK,
+                                               &aspeed_intc1_irq_domain_ops, intc1);
+       if (!intc1->local)
+               return -ENOMEM;
+
+       ret = aspeed_intc_populate_ranges(&pdev->dev, &intc1->ranges);
+       if (ret < 0) {
+               irq_domain_remove(intc1->local);
+               return ret;
+       }
+
+       aspeed_intc1_request_interrupts(intc1);
+
+       return 0;
+}
+
+IRQCHIP_PLATFORM_DRIVER_BEGIN(ast2700_intc1)
+IRQCHIP_MATCH("aspeed,ast2700-intc1", aspeed_intc1_probe)
+IRQCHIP_PLATFORM_DRIVER_END(ast2700_intc1)
diff --git a/drivers/irqchip/irq-ast2700.c b/drivers/irqchip/irq-ast2700.c
new file mode 100644 (file)
index 0000000..1e4c4a6
--- /dev/null
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ *  Aspeed AST2700 Interrupt Controller.
+ *
+ *  Copyright (C) 2026 ASPEED Technology Inc.
+ */
+#include "irq-ast2700.h"
+
+#define ASPEED_INTC_RANGE_FIXED_CELLS  3U
+#define ASPEED_INTC_RANGE_OFF_START    0U
+#define ASPEED_INTC_RANGE_OFF_COUNT    1U
+#define ASPEED_INTC_RANGE_OFF_PHANDLE  2U
+
+/**
+ * aspeed_intc_populate_ranges
+ * @dev: Device owning the interrupt controller node.
+ * @ranges: Destination for parsed range descriptors.
+ *
+ * Return: 0 on success, negative errno on error.
+ */
+int aspeed_intc_populate_ranges(struct device *dev,
+                               struct aspeed_intc_interrupt_ranges *ranges)
+{
+       struct aspeed_intc_interrupt_range *arr;
+       const __be32 *pvs, *pve;
+       struct device_node *dn;
+       int len;
+
+       if (!dev || !ranges)
+               return -EINVAL;
+
+       dn = dev->of_node;
+
+       pvs = of_get_property(dn, "aspeed,interrupt-ranges", &len);
+       if (!pvs)
+               return -EINVAL;
+
+       if (len % sizeof(__be32))
+               return -EINVAL;
+
+       /* Over-estimate the range entry count for now */
+       ranges->ranges = devm_kmalloc_array(dev,
+                                           len / (ASPEED_INTC_RANGE_FIXED_CELLS * sizeof(__be32)),
+                                           sizeof(*ranges->ranges),
+                                           GFP_KERNEL);
+       if (!ranges->ranges)
+               return -ENOMEM;
+
+       pve = pvs + (len / sizeof(__be32));
+       for (unsigned int i = 0; pve - pvs >= ASPEED_INTC_RANGE_FIXED_CELLS; i++) {
+               struct aspeed_intc_interrupt_range *r;
+               struct device_node *target;
+               u32 target_cells;
+
+               target = of_find_node_by_phandle(be32_to_cpu(pvs[ASPEED_INTC_RANGE_OFF_PHANDLE]));
+               if (!target)
+                       return -EINVAL;
+
+               if (of_property_read_u32(target, "#interrupt-cells",
+                                        &target_cells)) {
+                       of_node_put(target);
+                       return -EINVAL;
+               }
+
+               if (!target_cells || target_cells > IRQ_DOMAIN_IRQ_SPEC_PARAMS) {
+                       of_node_put(target);
+                       return -EINVAL;
+               }
+
+               if (pve - pvs < ASPEED_INTC_RANGE_FIXED_CELLS + target_cells) {
+                       of_node_put(target);
+                       return -EINVAL;
+               }
+
+               r = &ranges->ranges[i];
+               r->start = be32_to_cpu(pvs[ASPEED_INTC_RANGE_OFF_START]);
+               r->count = be32_to_cpu(pvs[ASPEED_INTC_RANGE_OFF_COUNT]);
+
+               {
+                       struct of_phandle_args args = {
+                               .np = target,
+                               .args_count = target_cells,
+                       };
+
+                       for (u32 j = 0; j < target_cells; j++)
+                               args.args[j] = be32_to_cpu(pvs[ASPEED_INTC_RANGE_FIXED_CELLS + j]);
+
+                       of_phandle_args_to_fwspec(target, args.args,
+                                                 args.args_count,
+                                                 &r->upstream);
+               }
+
+               of_node_put(target);
+               r->domain = irq_find_matching_fwspec(&r->upstream, DOMAIN_BUS_ANY);
+               pvs += ASPEED_INTC_RANGE_FIXED_CELLS + target_cells;
+               ranges->nranges++;
+       }
+
+       /* Re-fit the range array now we know the entry count */
+       arr = devm_krealloc_array(dev, ranges->ranges, ranges->nranges,
+                                 sizeof(*ranges->ranges), GFP_KERNEL);
+       if (!arr)
+               return -ENOMEM;
+       ranges->ranges = arr;
+
+       return 0;
+}
diff --git a/drivers/irqchip/irq-ast2700.h b/drivers/irqchip/irq-ast2700.h
new file mode 100644 (file)
index 0000000..3182966
--- /dev/null
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ *  Aspeed AST2700 Interrupt Controller.
+ *
+ *  Copyright (C) 2026 ASPEED Technology Inc.
+ */
+#ifndef DRIVERS_IRQCHIP_AST2700
+#define DRIVERS_IRQCHIP_AST2700
+
+#include <linux/device.h>
+#include <linux/irqdomain.h>
+
+#define AST2700_INTC_INVALID_ROUTE (~0U)
+#define ASPEED_INTC_RANGES_BASE                0U
+#define ASPEED_INTC_RANGES_COUNT       1U
+
+struct aspeed_intc_interrupt_range {
+       u32               start;
+       u32               count;
+       struct irq_fwspec upstream;
+       struct irq_domain *domain;
+};
+
+struct aspeed_intc_interrupt_ranges {
+       struct aspeed_intc_interrupt_range *ranges;
+       unsigned int                       nranges;
+};
+
+struct aspeed_intc0 {
+       struct device                           *dev;
+       void __iomem                            *base;
+       raw_spinlock_t                          intc_lock;
+       struct irq_domain                       *local;
+       struct device_node                      *parent;
+       struct aspeed_intc_interrupt_ranges     ranges;
+};
+
+int aspeed_intc_populate_ranges(struct device *dev,
+                               struct aspeed_intc_interrupt_ranges *ranges);
+
+int aspeed_intc0_resolve_route(const struct irq_domain *c0domain,
+                              size_t nc1outs,
+                              const u32 *c1outs,
+                              size_t nc1ranges,
+                              const struct aspeed_intc_interrupt_range *c1ranges,
+                              struct aspeed_intc_interrupt_range *resolved);
+
+#endif