|  | // SPDX-License-Identifier: GPL-2.0-only | 
|  | /* | 
|  | * Driver code for Tegra's Legacy Interrupt Controller | 
|  | * | 
|  | * Author: Marc Zyngier <marc.zyngier@arm.com> | 
|  | * | 
|  | * Heavily based on the original arch/arm/mach-tegra/irq.c code: | 
|  | * Copyright (C) 2011 Google, Inc. | 
|  | * | 
|  | * Author: | 
|  | *	Colin Cross <ccross@android.com> | 
|  | * | 
|  | * Copyright (C) 2010,2013, NVIDIA Corporation | 
|  | */ | 
|  |  | 
|  | #include <linux/io.h> | 
|  | #include <linux/irq.h> | 
|  | #include <linux/irqchip.h> | 
|  | #include <linux/irqdomain.h> | 
|  | #include <linux/of_address.h> | 
|  | #include <linux/slab.h> | 
|  | #include <linux/syscore_ops.h> | 
|  |  | 
|  | #include <dt-bindings/interrupt-controller/arm-gic.h> | 
|  |  | 
|  | #define ICTLR_CPU_IEP_VFIQ	0x08 | 
|  | #define ICTLR_CPU_IEP_FIR	0x14 | 
|  | #define ICTLR_CPU_IEP_FIR_SET	0x18 | 
|  | #define ICTLR_CPU_IEP_FIR_CLR	0x1c | 
|  |  | 
|  | #define ICTLR_CPU_IER		0x20 | 
|  | #define ICTLR_CPU_IER_SET	0x24 | 
|  | #define ICTLR_CPU_IER_CLR	0x28 | 
|  | #define ICTLR_CPU_IEP_CLASS	0x2C | 
|  |  | 
|  | #define ICTLR_COP_IER		0x30 | 
|  | #define ICTLR_COP_IER_SET	0x34 | 
|  | #define ICTLR_COP_IER_CLR	0x38 | 
|  | #define ICTLR_COP_IEP_CLASS	0x3c | 
|  |  | 
|  | #define TEGRA_MAX_NUM_ICTLRS	6 | 
|  |  | 
|  | static unsigned int num_ictlrs; | 
|  |  | 
|  | struct tegra_ictlr_soc { | 
|  | unsigned int num_ictlrs; | 
|  | }; | 
|  |  | 
|  | static const struct tegra_ictlr_soc tegra20_ictlr_soc = { | 
|  | .num_ictlrs = 4, | 
|  | }; | 
|  |  | 
|  | static const struct tegra_ictlr_soc tegra30_ictlr_soc = { | 
|  | .num_ictlrs = 5, | 
|  | }; | 
|  |  | 
|  | static const struct tegra_ictlr_soc tegra210_ictlr_soc = { | 
|  | .num_ictlrs = 6, | 
|  | }; | 
|  |  | 
|  | static const struct of_device_id ictlr_matches[] = { | 
|  | { .compatible = "nvidia,tegra210-ictlr", .data = &tegra210_ictlr_soc }, | 
|  | { .compatible = "nvidia,tegra30-ictlr", .data = &tegra30_ictlr_soc }, | 
|  | { .compatible = "nvidia,tegra20-ictlr", .data = &tegra20_ictlr_soc }, | 
|  | { } | 
|  | }; | 
|  |  | 
|  | struct tegra_ictlr_info { | 
|  | void __iomem *base[TEGRA_MAX_NUM_ICTLRS]; | 
|  | #ifdef CONFIG_PM_SLEEP | 
|  | u32 cop_ier[TEGRA_MAX_NUM_ICTLRS]; | 
|  | u32 cop_iep[TEGRA_MAX_NUM_ICTLRS]; | 
|  | u32 cpu_ier[TEGRA_MAX_NUM_ICTLRS]; | 
|  | u32 cpu_iep[TEGRA_MAX_NUM_ICTLRS]; | 
|  |  | 
|  | u32 ictlr_wake_mask[TEGRA_MAX_NUM_ICTLRS]; | 
|  | #endif | 
|  | }; | 
|  |  | 
|  | static struct tegra_ictlr_info *lic; | 
|  |  | 
|  | static inline void tegra_ictlr_write_mask(struct irq_data *d, unsigned long reg) | 
|  | { | 
|  | void __iomem *base = (void __iomem __force *)d->chip_data; | 
|  | u32 mask; | 
|  |  | 
|  | mask = BIT(d->hwirq % 32); | 
|  | writel_relaxed(mask, base + reg); | 
|  | } | 
|  |  | 
|  | static void tegra_mask(struct irq_data *d) | 
|  | { | 
|  | tegra_ictlr_write_mask(d, ICTLR_CPU_IER_CLR); | 
|  | irq_chip_mask_parent(d); | 
|  | } | 
|  |  | 
|  | static void tegra_unmask(struct irq_data *d) | 
|  | { | 
|  | tegra_ictlr_write_mask(d, ICTLR_CPU_IER_SET); | 
|  | irq_chip_unmask_parent(d); | 
|  | } | 
|  |  | 
|  | static void tegra_eoi(struct irq_data *d) | 
|  | { | 
|  | tegra_ictlr_write_mask(d, ICTLR_CPU_IEP_FIR_CLR); | 
|  | irq_chip_eoi_parent(d); | 
|  | } | 
|  |  | 
|  | static int tegra_retrigger(struct irq_data *d) | 
|  | { | 
|  | tegra_ictlr_write_mask(d, ICTLR_CPU_IEP_FIR_SET); | 
|  | return irq_chip_retrigger_hierarchy(d); | 
|  | } | 
|  |  | 
|  | #ifdef CONFIG_PM_SLEEP | 
|  | static int tegra_set_wake(struct irq_data *d, unsigned int enable) | 
|  | { | 
|  | u32 irq = d->hwirq; | 
|  | u32 index, mask; | 
|  |  | 
|  | index = (irq / 32); | 
|  | mask = BIT(irq % 32); | 
|  | if (enable) | 
|  | lic->ictlr_wake_mask[index] |= mask; | 
|  | else | 
|  | lic->ictlr_wake_mask[index] &= ~mask; | 
|  |  | 
|  | /* | 
|  | * Do *not* call into the parent, as the GIC doesn't have any | 
|  | * wake-up facility... | 
|  | */ | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int tegra_ictlr_suspend(void) | 
|  | { | 
|  | unsigned long flags; | 
|  | unsigned int i; | 
|  |  | 
|  | local_irq_save(flags); | 
|  | for (i = 0; i < num_ictlrs; i++) { | 
|  | void __iomem *ictlr = lic->base[i]; | 
|  |  | 
|  | /* Save interrupt state */ | 
|  | lic->cpu_ier[i] = readl_relaxed(ictlr + ICTLR_CPU_IER); | 
|  | lic->cpu_iep[i] = readl_relaxed(ictlr + ICTLR_CPU_IEP_CLASS); | 
|  | lic->cop_ier[i] = readl_relaxed(ictlr + ICTLR_COP_IER); | 
|  | lic->cop_iep[i] = readl_relaxed(ictlr + ICTLR_COP_IEP_CLASS); | 
|  |  | 
|  | /* Disable COP interrupts */ | 
|  | writel_relaxed(GENMASK(31, 0), ictlr + ICTLR_COP_IER_CLR); | 
|  |  | 
|  | /* Disable CPU interrupts */ | 
|  | writel_relaxed(GENMASK(31, 0), ictlr + ICTLR_CPU_IER_CLR); | 
|  |  | 
|  | /* Enable the wakeup sources of ictlr */ | 
|  | writel_relaxed(lic->ictlr_wake_mask[i], ictlr + ICTLR_CPU_IER_SET); | 
|  | } | 
|  | local_irq_restore(flags); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void tegra_ictlr_resume(void) | 
|  | { | 
|  | unsigned long flags; | 
|  | unsigned int i; | 
|  |  | 
|  | local_irq_save(flags); | 
|  | for (i = 0; i < num_ictlrs; i++) { | 
|  | void __iomem *ictlr = lic->base[i]; | 
|  |  | 
|  | writel_relaxed(lic->cpu_iep[i], | 
|  | ictlr + ICTLR_CPU_IEP_CLASS); | 
|  | writel_relaxed(GENMASK(31, 0), ictlr + ICTLR_CPU_IER_CLR); | 
|  | writel_relaxed(lic->cpu_ier[i], | 
|  | ictlr + ICTLR_CPU_IER_SET); | 
|  | writel_relaxed(lic->cop_iep[i], | 
|  | ictlr + ICTLR_COP_IEP_CLASS); | 
|  | writel_relaxed(GENMASK(31, 0), ictlr + ICTLR_COP_IER_CLR); | 
|  | writel_relaxed(lic->cop_ier[i], | 
|  | ictlr + ICTLR_COP_IER_SET); | 
|  | } | 
|  | local_irq_restore(flags); | 
|  | } | 
|  |  | 
|  | static struct syscore_ops tegra_ictlr_syscore_ops = { | 
|  | .suspend	= tegra_ictlr_suspend, | 
|  | .resume		= tegra_ictlr_resume, | 
|  | }; | 
|  |  | 
|  | static void tegra_ictlr_syscore_init(void) | 
|  | { | 
|  | register_syscore_ops(&tegra_ictlr_syscore_ops); | 
|  | } | 
|  | #else | 
|  | #define tegra_set_wake	NULL | 
|  | static inline void tegra_ictlr_syscore_init(void) {} | 
|  | #endif | 
|  |  | 
|  | static struct irq_chip tegra_ictlr_chip = { | 
|  | .name			= "LIC", | 
|  | .irq_eoi		= tegra_eoi, | 
|  | .irq_mask		= tegra_mask, | 
|  | .irq_unmask		= tegra_unmask, | 
|  | .irq_retrigger		= tegra_retrigger, | 
|  | .irq_set_wake		= tegra_set_wake, | 
|  | .irq_set_type		= irq_chip_set_type_parent, | 
|  | .flags			= IRQCHIP_MASK_ON_SUSPEND, | 
|  | #ifdef CONFIG_SMP | 
|  | .irq_set_affinity	= irq_chip_set_affinity_parent, | 
|  | #endif | 
|  | }; | 
|  |  | 
|  | static int tegra_ictlr_domain_translate(struct irq_domain *d, | 
|  | struct irq_fwspec *fwspec, | 
|  | unsigned long *hwirq, | 
|  | unsigned int *type) | 
|  | { | 
|  | if (is_of_node(fwspec->fwnode)) { | 
|  | if (fwspec->param_count != 3) | 
|  | return -EINVAL; | 
|  |  | 
|  | /* No PPI should point to this domain */ | 
|  | if (fwspec->param[0] != 0) | 
|  | return -EINVAL; | 
|  |  | 
|  | *hwirq = fwspec->param[1]; | 
|  | *type = fwspec->param[2] & IRQ_TYPE_SENSE_MASK; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | static int tegra_ictlr_domain_alloc(struct irq_domain *domain, | 
|  | unsigned int virq, | 
|  | unsigned int nr_irqs, void *data) | 
|  | { | 
|  | struct irq_fwspec *fwspec = data; | 
|  | struct irq_fwspec parent_fwspec; | 
|  | struct tegra_ictlr_info *info = domain->host_data; | 
|  | irq_hw_number_t hwirq; | 
|  | unsigned int i; | 
|  |  | 
|  | if (fwspec->param_count != 3) | 
|  | return -EINVAL;	/* Not GIC compliant */ | 
|  | if (fwspec->param[0] != GIC_SPI) | 
|  | return -EINVAL;	/* No PPI should point to this domain */ | 
|  |  | 
|  | hwirq = fwspec->param[1]; | 
|  | if (hwirq >= (num_ictlrs * 32)) | 
|  | return -EINVAL; | 
|  |  | 
|  | for (i = 0; i < nr_irqs; i++) { | 
|  | int ictlr = (hwirq + i) / 32; | 
|  |  | 
|  | irq_domain_set_hwirq_and_chip(domain, virq + i, hwirq + i, | 
|  | &tegra_ictlr_chip, | 
|  | (void __force *)info->base[ictlr]); | 
|  | } | 
|  |  | 
|  | parent_fwspec = *fwspec; | 
|  | parent_fwspec.fwnode = domain->parent->fwnode; | 
|  | return irq_domain_alloc_irqs_parent(domain, virq, nr_irqs, | 
|  | &parent_fwspec); | 
|  | } | 
|  |  | 
|  | static const struct irq_domain_ops tegra_ictlr_domain_ops = { | 
|  | .translate	= tegra_ictlr_domain_translate, | 
|  | .alloc		= tegra_ictlr_domain_alloc, | 
|  | .free		= irq_domain_free_irqs_common, | 
|  | }; | 
|  |  | 
|  | static int __init tegra_ictlr_init(struct device_node *node, | 
|  | struct device_node *parent) | 
|  | { | 
|  | struct irq_domain *parent_domain, *domain; | 
|  | const struct of_device_id *match; | 
|  | const struct tegra_ictlr_soc *soc; | 
|  | unsigned int i; | 
|  | int err; | 
|  |  | 
|  | if (!parent) { | 
|  | pr_err("%pOF: no parent, giving up\n", node); | 
|  | return -ENODEV; | 
|  | } | 
|  |  | 
|  | parent_domain = irq_find_host(parent); | 
|  | if (!parent_domain) { | 
|  | pr_err("%pOF: unable to obtain parent domain\n", node); | 
|  | return -ENXIO; | 
|  | } | 
|  |  | 
|  | match = of_match_node(ictlr_matches, node); | 
|  | if (!match)		/* Should never happen... */ | 
|  | return -ENODEV; | 
|  |  | 
|  | soc = match->data; | 
|  |  | 
|  | lic = kzalloc(sizeof(*lic), GFP_KERNEL); | 
|  | if (!lic) | 
|  | return -ENOMEM; | 
|  |  | 
|  | for (i = 0; i < TEGRA_MAX_NUM_ICTLRS; i++) { | 
|  | void __iomem *base; | 
|  |  | 
|  | base = of_iomap(node, i); | 
|  | if (!base) | 
|  | break; | 
|  |  | 
|  | lic->base[i] = base; | 
|  |  | 
|  | /* Disable all interrupts */ | 
|  | writel_relaxed(GENMASK(31, 0), base + ICTLR_CPU_IER_CLR); | 
|  | /* All interrupts target IRQ */ | 
|  | writel_relaxed(0, base + ICTLR_CPU_IEP_CLASS); | 
|  |  | 
|  | num_ictlrs++; | 
|  | } | 
|  |  | 
|  | if (!num_ictlrs) { | 
|  | pr_err("%pOF: no valid regions, giving up\n", node); | 
|  | err = -ENOMEM; | 
|  | goto out_free; | 
|  | } | 
|  |  | 
|  | WARN(num_ictlrs != soc->num_ictlrs, | 
|  | "%pOF: Found %u interrupt controllers in DT; expected %u.\n", | 
|  | node, num_ictlrs, soc->num_ictlrs); | 
|  |  | 
|  |  | 
|  | domain = irq_domain_add_hierarchy(parent_domain, 0, num_ictlrs * 32, | 
|  | node, &tegra_ictlr_domain_ops, | 
|  | lic); | 
|  | if (!domain) { | 
|  | pr_err("%pOF: failed to allocated domain\n", node); | 
|  | err = -ENOMEM; | 
|  | goto out_unmap; | 
|  | } | 
|  |  | 
|  | tegra_ictlr_syscore_init(); | 
|  |  | 
|  | pr_info("%pOF: %d interrupts forwarded to %pOF\n", | 
|  | node, num_ictlrs * 32, parent); | 
|  |  | 
|  | return 0; | 
|  |  | 
|  | out_unmap: | 
|  | for (i = 0; i < num_ictlrs; i++) | 
|  | iounmap(lic->base[i]); | 
|  | out_free: | 
|  | kfree(lic); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | IRQCHIP_DECLARE(tegra20_ictlr, "nvidia,tegra20-ictlr", tegra_ictlr_init); | 
|  | IRQCHIP_DECLARE(tegra30_ictlr, "nvidia,tegra30-ictlr", tegra_ictlr_init); | 
|  | IRQCHIP_DECLARE(tegra210_ictlr, "nvidia,tegra210-ictlr", tegra_ictlr_init); |