blob: 14b8b88f1179ba5fa8b38ada8deb1e59e827fb9c [file]
// 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)