blob: df6acbafce2d01336e818da104743e6bbca0ff52 [file] [edit]
// 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 (!of_device_is_compatible(to_of_node(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)