[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-Id: <20250321134633.2155141-4-cjd@cjdns.fr>
Date: Fri, 21 Mar 2025 13:46:28 +0000
From: Caleb James DeLisle <cjd@...ns.fr>
To: linux-mips@...r.kernel.org
Cc: Thomas Gleixner <tglx@...utronix.de>,
Rob Herring <robh@...nel.org>,
Krzysztof Kozlowski <krzk+dt@...nel.org>,
Conor Dooley <conor+dt@...nel.org>,
Thomas Bogendoerfer <tsbogend@...ha.franken.de>,
Daniel Lezcano <daniel.lezcano@...aro.org>,
devicetree@...r.kernel.org,
linux-kernel@...r.kernel.org,
benjamin.larsson@...exis.eu,
Caleb James DeLisle <cjd@...ns.fr>
Subject: [PATCH v1 3/8] irqchip: Add EcoNet EN751221 INTC
Add a driver for the interrupt controller in the EcoNet EN751221 MIPS SoC.
Signed-off-by: Caleb James DeLisle <cjd@...ns.fr>
---
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@...ns.fr>
+ */
+
+#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
Powered by blists - more mailing lists