Add a driver for the interrupt controller in the EcoNet EN751221 MIPS SoC. Signed-off-by: Caleb James DeLisle <cjd@xxxxxxxx> --- If CPU_MIPSR2_IRQ_EI / CPU_MIPSR2_IRQ_VI are enabled in the build, this device switches to sending all interrupts as vectored - which IRQ_MIPS_CPU is not prepared to handle. If anybody knows how to either disable this behavior, or handle vectored interrupts without ugly code that breaks cascading, please let me know and I will implement that and add MIPS_MT_SMP in a future patchset. --- drivers/irqchip/Kconfig | 5 + drivers/irqchip/Makefile | 1 + drivers/irqchip/irq-econet-en751221.c | 280 ++++++++++++++++++++++++++ 3 files changed, 286 insertions(+) create mode 100644 drivers/irqchip/irq-econet-en751221.c diff --git a/drivers/irqchip/Kconfig b/drivers/irqchip/Kconfig index c11b9965c4ad..a591ad3156dc 100644 --- a/drivers/irqchip/Kconfig +++ b/drivers/irqchip/Kconfig @@ -147,6 +147,11 @@ config DW_APB_ICTL select GENERIC_IRQ_CHIP select IRQ_DOMAIN_HIERARCHY +config ECONET_EN751221_INTC + bool + select GENERIC_IRQ_CHIP + select IRQ_DOMAIN + config FARADAY_FTINTC010 bool select IRQ_DOMAIN diff --git a/drivers/irqchip/Makefile b/drivers/irqchip/Makefile index 25e9ad29b8c4..1ee83823928d 100644 --- a/drivers/irqchip/Makefile +++ b/drivers/irqchip/Makefile @@ -10,6 +10,7 @@ obj-$(CONFIG_ARCH_BCM2835) += irq-bcm2836.o obj-$(CONFIG_ARCH_ACTIONS) += irq-owl-sirq.o obj-$(CONFIG_DAVINCI_CP_INTC) += irq-davinci-cp-intc.o obj-$(CONFIG_EXYNOS_IRQ_COMBINER) += exynos-combiner.o +obj-$(CONFIG_ECONET_EN751221_INTC) += irq-econet-en751221.o obj-$(CONFIG_FARADAY_FTINTC010) += irq-ftintc010.o obj-$(CONFIG_ARCH_HIP04) += irq-hip04.o obj-$(CONFIG_ARCH_LPC32XX) += irq-lpc32xx.o diff --git a/drivers/irqchip/irq-econet-en751221.c b/drivers/irqchip/irq-econet-en751221.c new file mode 100644 index 000000000000..edbb8a3d6d51 --- /dev/null +++ b/drivers/irqchip/irq-econet-en751221.c @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * EN751221 Interrupt Controller Driver. + * + * Copyright (C) 2025 Caleb James DeLisle <cjd@xxxxxxxx> + */ + +#include <linux/io.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_irq.h> +#include <linux/irqdomain.h> +#include <linux/irqchip.h> +#include <linux/irqchip/chained_irq.h> + +#define INTC_IRQ_COUNT 40 + +#define INTC_NO_SHADOW 0xff +#define INTC_IS_SHADOW 0xfe + +#define REG_MASK0 0x04 +#define REG_MASK1 0x50 +#define REG_PENDING0 0x08 +#define REG_PENDING1 0x54 + +static const struct econet_intc { + const struct irq_chip chip; + + const struct irq_domain_ops domain_ops; +} econet_intc; + +static struct { + void __iomem *membase; + u8 shadow_interrupts[INTC_IRQ_COUNT]; +} econet_intc_rai __ro_after_init; + +static DEFINE_RAW_SPINLOCK(irq_lock); + +static void econet_wreg(u32 reg, u32 val, u32 mask) +{ + unsigned long flags; + u32 v; + + raw_spin_lock_irqsave(&irq_lock, flags); + + v = ioread32(econet_intc_rai.membase + reg); + v &= ~mask; + v |= val & mask; + iowrite32(v, econet_intc_rai.membase + reg); + + raw_spin_unlock_irqrestore(&irq_lock, flags); +} + +static void econet_chmask(u32 hwirq, bool unmask) +{ + u32 reg; + u32 mask; + u32 bit; + u8 shadow; + + shadow = econet_intc_rai.shadow_interrupts[hwirq]; + if (WARN_ON_ONCE(shadow == INTC_IS_SHADOW)) + return; + else if (shadow < INTC_NO_SHADOW && smp_processor_id() > 0) + hwirq = shadow; + + if (hwirq >= 32) { + reg = REG_MASK1; + mask = BIT(hwirq - 32); + } else { + reg = REG_MASK0; + mask = BIT(hwirq); + } + bit = (unmask) ? mask : 0; + + econet_wreg(reg, bit, mask); +} + +static void econet_intc_mask(struct irq_data *d) +{ + econet_chmask(d->hwirq, false); +} + +static void econet_intc_unmask(struct irq_data *d) +{ + econet_chmask(d->hwirq, true); +} + +static void econet_mask_all(void) +{ + econet_wreg(REG_MASK0, 0, ~0); + econet_wreg(REG_MASK1, 0, ~0); +} + +static void econet_intc_handle_pending(struct irq_domain *d, u32 pending, u32 offset) +{ + int hwirq; + + while (pending) { + hwirq = fls(pending) - 1; + generic_handle_domain_irq(d, hwirq + offset); + pending &= ~BIT(hwirq); + } +} + +static void econet_intc_from_parent(struct irq_desc *desc) +{ + struct irq_chip *chip = irq_desc_get_chip(desc); + struct irq_domain *domain; + u32 pending0; + u32 pending1; + + chained_irq_enter(chip, desc); + + pending0 = ioread32(econet_intc_rai.membase + REG_PENDING0); + pending1 = ioread32(econet_intc_rai.membase + REG_PENDING1); + + if (unlikely(!(pending0 | pending1))) { + spurious_interrupt(); + goto out; + } + + domain = irq_desc_get_handler_data(desc); + + econet_intc_handle_pending(domain, pending0, 0); + econet_intc_handle_pending(domain, pending1, 32); + +out: + chained_irq_exit(chip, desc); +} + +static int econet_intc_map(struct irq_domain *d, u32 irq, irq_hw_number_t hwirq) +{ + int ret; + + if (hwirq >= INTC_IRQ_COUNT) { + pr_err("%s: hwirq %lu out of range\n", __func__, hwirq); + return -EINVAL; + } else if (econet_intc_rai.shadow_interrupts[hwirq] == INTC_IS_SHADOW) { + pr_err("%s: can't map hwirq %lu, it is a shadow interrupt\n", + __func__, hwirq); + return -EINVAL; + } + if (econet_intc_rai.shadow_interrupts[hwirq] != INTC_NO_SHADOW) { + irq_set_chip_and_handler( + irq, &econet_intc.chip, handle_percpu_devid_irq); + ret = irq_set_percpu_devid(irq); + if (ret) { + pr_warn("%s: Failed irq_set_percpu_devid for %u: %d\n", + d->name, irq, ret); + } + } else { + irq_set_chip_and_handler( + irq, &econet_intc.chip, handle_level_irq); + } + irq_set_chip_data(irq, NULL); + return 0; +} + +static const struct econet_intc econet_intc = { + .chip = { + .name = "en751221-intc", + .irq_unmask = econet_intc_unmask, + .irq_mask = econet_intc_mask, + .irq_mask_ack = econet_intc_mask, + }, + .domain_ops = { + .xlate = irq_domain_xlate_onecell, + .map = econet_intc_map, + }, +}; + +static int __init get_shadow_interrupts(struct device_node *node) +{ + const char *field = "econet,shadow-interrupts"; + int n_shadow_interrupts; + u32 *shadow_interrupts; + + n_shadow_interrupts = of_property_count_u32_elems(node, field); + memset(econet_intc_rai.shadow_interrupts, INTC_NO_SHADOW, + sizeof(econet_intc_rai.shadow_interrupts)); + if (n_shadow_interrupts <= 0) { + return 0; + } else if (n_shadow_interrupts % 2) { + pr_err("%pOF: %s count is odd, ignoring\n", node, field); + return 0; + } + shadow_interrupts = kmalloc_array(n_shadow_interrupts, sizeof(u32), + GFP_KERNEL); + if (!shadow_interrupts) + return -ENOMEM; + if (of_property_read_u32_array(node, field, + shadow_interrupts, n_shadow_interrupts) + ) { + pr_err("%pOF: Failed to read %s\n", node, field); + kfree(shadow_interrupts); + return -EINVAL; + } + for (int i = 0; i < n_shadow_interrupts; i += 2) { + u32 shadow = shadow_interrupts[i + 1]; + u32 target = shadow_interrupts[i]; + + if (shadow > INTC_IRQ_COUNT) { + pr_err("%pOF: %s[%d] shadow(%d) out of range\n", + node, field, i, shadow); + continue; + } + if (target >= INTC_IRQ_COUNT) { + pr_err("%pOF: %s[%d] target(%d) out of range\n", + node, field, i + 1, target); + continue; + } + econet_intc_rai.shadow_interrupts[target] = shadow; + econet_intc_rai.shadow_interrupts[shadow] = INTC_IS_SHADOW; + } + kfree(shadow_interrupts); + return 0; +} + +static int __init econet_intc_of_init(struct device_node *node, struct device_node *parent) +{ + int ret; + int irq; + struct resource res; + struct irq_domain *domain; + + ret = get_shadow_interrupts(node); + if (ret) + return ret; + + irq = irq_of_parse_and_map(node, 0); + if (!irq) { + pr_err("%pOF: DT: Failed to get IRQ from 'interrupts'\n", node); + return -EINVAL; + } + + if (of_address_to_resource(node, 0, &res)) { + pr_err("%pOF: DT: Failed to get 'reg'\n", node); + ret = -EINVAL; + goto err_dispose_mapping; + } + + if (!request_mem_region(res.start, resource_size(&res), res.name)) { + pr_err("%pOF: Failed to request memory\n", node); + ret = -EBUSY; + goto err_dispose_mapping; + } + + econet_intc_rai.membase = ioremap(res.start, resource_size(&res)); + if (!econet_intc_rai.membase) { + pr_err("%pOF: Failed to remap membase\n", node); + ret = -ENOMEM; + goto err_release; + } + + econet_mask_all(); + + domain = irq_domain_add_linear( + node, INTC_IRQ_COUNT, + &econet_intc.domain_ops, NULL); + if (!domain) { + pr_err("%pOF: Failed to add irqdomain\n", node); + ret = -ENOMEM; + goto err_unmap; + } + + irq_set_chained_handler_and_data(irq, econet_intc_from_parent, domain); + + return 0; + +err_unmap: + iounmap(econet_intc_rai.membase); +err_release: + release_mem_region(res.start, resource_size(&res)); +err_dispose_mapping: + irq_dispose_mapping(irq); + return ret; +} + +IRQCHIP_DECLARE(econet_en751221_intc, "econet,en751221-intc", econet_intc_of_init); -- 2.30.2