root/drivers/irqchip/irq-ast2700-intc0.c
// 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 (!IS_ENABLED(CONFIG_ASPEED_AST2700_INTC_TEST) &&
            !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)