| // SPDX-License-Identifier: GPL-2.0 | 
 | /* | 
 |  * Driver for the Microchip LAN966x outbound interrupt controller | 
 |  * | 
 |  * Copyright (c) 2024 Technology Inc. and its subsidiaries. | 
 |  * | 
 |  * Authors: | 
 |  *	Horatiu Vultur <horatiu.vultur@microchip.com> | 
 |  *	Clément Léger <clement.leger@bootlin.com> | 
 |  *	Herve Codina <herve.codina@bootlin.com> | 
 |  */ | 
 |  | 
 | #include <linux/interrupt.h> | 
 | #include <linux/irqchip/chained_irq.h> | 
 | #include <linux/irqchip.h> | 
 | #include <linux/irq.h> | 
 | #include <linux/mod_devicetable.h> | 
 | #include <linux/module.h> | 
 | #include <linux/platform_device.h> | 
 | #include <linux/slab.h> | 
 |  | 
 | struct lan966x_oic_chip_regs { | 
 | 	int reg_off_ena_set; | 
 | 	int reg_off_ena_clr; | 
 | 	int reg_off_sticky; | 
 | 	int reg_off_ident; | 
 | 	int reg_off_map; | 
 | }; | 
 |  | 
 | struct lan966x_oic_data { | 
 | 	void __iomem	*regs; | 
 | 	int		irq; | 
 | }; | 
 |  | 
 | #define LAN966X_OIC_NR_IRQ 86 | 
 |  | 
 | /* Interrupt sticky status */ | 
 | #define LAN966X_OIC_INTR_STICKY		0x30 | 
 | #define LAN966X_OIC_INTR_STICKY1	0x34 | 
 | #define LAN966X_OIC_INTR_STICKY2	0x38 | 
 |  | 
 | /* Interrupt enable */ | 
 | #define LAN966X_OIC_INTR_ENA		0x48 | 
 | #define LAN966X_OIC_INTR_ENA1		0x4c | 
 | #define LAN966X_OIC_INTR_ENA2		0x50 | 
 |  | 
 | /* Atomic clear of interrupt enable */ | 
 | #define LAN966X_OIC_INTR_ENA_CLR	0x54 | 
 | #define LAN966X_OIC_INTR_ENA_CLR1	0x58 | 
 | #define LAN966X_OIC_INTR_ENA_CLR2	0x5c | 
 |  | 
 | /* Atomic set of interrupt */ | 
 | #define LAN966X_OIC_INTR_ENA_SET	0x60 | 
 | #define LAN966X_OIC_INTR_ENA_SET1	0x64 | 
 | #define LAN966X_OIC_INTR_ENA_SET2	0x68 | 
 |  | 
 | /* Mapping of source to destination interrupts (_n = 0..8) */ | 
 | #define LAN966X_OIC_DST_INTR_MAP(_n)	(0x78 + (_n) * 4) | 
 | #define LAN966X_OIC_DST_INTR_MAP1(_n)	(0x9c + (_n) * 4) | 
 | #define LAN966X_OIC_DST_INTR_MAP2(_n)	(0xc0 + (_n) * 4) | 
 |  | 
 | /* Currently active interrupt sources per destination (_n = 0..8) */ | 
 | #define LAN966X_OIC_DST_INTR_IDENT(_n)	(0xe4 + (_n) * 4) | 
 | #define LAN966X_OIC_DST_INTR_IDENT1(_n)	(0x108 + (_n) * 4) | 
 | #define LAN966X_OIC_DST_INTR_IDENT2(_n)	(0x12c + (_n) * 4) | 
 |  | 
 | static unsigned int lan966x_oic_irq_startup(struct irq_data *data) | 
 | { | 
 | 	struct irq_chip_generic *gc = irq_data_get_irq_chip_data(data); | 
 | 	struct irq_chip_type *ct = irq_data_get_chip_type(data); | 
 | 	struct lan966x_oic_chip_regs *chip_regs = gc->private; | 
 | 	u32 map; | 
 |  | 
 | 	scoped_guard (raw_spinlock, &gc->lock) { | 
 | 		/* Map the source interrupt to the destination */ | 
 | 		map = irq_reg_readl(gc, chip_regs->reg_off_map); | 
 | 		map |= data->mask; | 
 | 		irq_reg_writel(gc, map, chip_regs->reg_off_map); | 
 | 	} | 
 |  | 
 | 	ct->chip.irq_ack(data); | 
 | 	ct->chip.irq_unmask(data); | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void lan966x_oic_irq_shutdown(struct irq_data *data) | 
 | { | 
 | 	struct irq_chip_generic *gc = irq_data_get_irq_chip_data(data); | 
 | 	struct irq_chip_type *ct = irq_data_get_chip_type(data); | 
 | 	struct lan966x_oic_chip_regs *chip_regs = gc->private; | 
 | 	u32 map; | 
 |  | 
 | 	ct->chip.irq_mask(data); | 
 |  | 
 | 	guard(raw_spinlock)(&gc->lock); | 
 |  | 
 | 	/* Unmap the interrupt */ | 
 | 	map = irq_reg_readl(gc, chip_regs->reg_off_map); | 
 | 	map &= ~data->mask; | 
 | 	irq_reg_writel(gc, map, chip_regs->reg_off_map); | 
 | } | 
 |  | 
 | static int lan966x_oic_irq_set_type(struct irq_data *data, | 
 | 				    unsigned int flow_type) | 
 | { | 
 | 	if (flow_type != IRQ_TYPE_LEVEL_HIGH) { | 
 | 		pr_err("lan966x oic doesn't support flow type %d\n", flow_type); | 
 | 		return -EINVAL; | 
 | 	} | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void lan966x_oic_irq_handler_domain(struct irq_domain *d, u32 first_irq) | 
 | { | 
 | 	struct irq_chip_generic *gc = irq_get_domain_generic_chip(d, first_irq); | 
 | 	struct lan966x_oic_chip_regs *chip_regs = gc->private; | 
 | 	unsigned long ident; | 
 | 	unsigned int hwirq; | 
 |  | 
 | 	ident = irq_reg_readl(gc, chip_regs->reg_off_ident); | 
 | 	if (!ident) | 
 | 		return; | 
 |  | 
 | 	for_each_set_bit(hwirq, &ident, 32) | 
 | 		generic_handle_domain_irq(d, hwirq + first_irq); | 
 | } | 
 |  | 
 | static void lan966x_oic_irq_handler(struct irq_desc *desc) | 
 | { | 
 | 	struct irq_domain *d = irq_desc_get_handler_data(desc); | 
 | 	struct irq_chip *chip = irq_desc_get_chip(desc); | 
 |  | 
 | 	chained_irq_enter(chip, desc); | 
 | 	lan966x_oic_irq_handler_domain(d, 0); | 
 | 	lan966x_oic_irq_handler_domain(d, 32); | 
 | 	lan966x_oic_irq_handler_domain(d, 64); | 
 | 	chained_irq_exit(chip, desc); | 
 | } | 
 |  | 
 | static struct lan966x_oic_chip_regs lan966x_oic_chip_regs[3] = { | 
 | 	{ | 
 | 		.reg_off_ena_set = LAN966X_OIC_INTR_ENA_SET, | 
 | 		.reg_off_ena_clr = LAN966X_OIC_INTR_ENA_CLR, | 
 | 		.reg_off_sticky  = LAN966X_OIC_INTR_STICKY, | 
 | 		.reg_off_ident   = LAN966X_OIC_DST_INTR_IDENT(0), | 
 | 		.reg_off_map     = LAN966X_OIC_DST_INTR_MAP(0), | 
 | 	}, { | 
 | 		.reg_off_ena_set = LAN966X_OIC_INTR_ENA_SET1, | 
 | 		.reg_off_ena_clr = LAN966X_OIC_INTR_ENA_CLR1, | 
 | 		.reg_off_sticky  = LAN966X_OIC_INTR_STICKY1, | 
 | 		.reg_off_ident   = LAN966X_OIC_DST_INTR_IDENT1(0), | 
 | 		.reg_off_map     = LAN966X_OIC_DST_INTR_MAP1(0), | 
 | 	}, { | 
 | 		.reg_off_ena_set = LAN966X_OIC_INTR_ENA_SET2, | 
 | 		.reg_off_ena_clr = LAN966X_OIC_INTR_ENA_CLR2, | 
 | 		.reg_off_sticky  = LAN966X_OIC_INTR_STICKY2, | 
 | 		.reg_off_ident   = LAN966X_OIC_DST_INTR_IDENT2(0), | 
 | 		.reg_off_map     = LAN966X_OIC_DST_INTR_MAP2(0), | 
 | 	} | 
 | }; | 
 |  | 
 | static int lan966x_oic_chip_init(struct irq_chip_generic *gc) | 
 | { | 
 | 	struct lan966x_oic_data *lan966x_oic = gc->domain->host_data; | 
 | 	struct lan966x_oic_chip_regs *chip_regs; | 
 |  | 
 | 	chip_regs = &lan966x_oic_chip_regs[gc->irq_base / 32]; | 
 |  | 
 | 	gc->reg_base = lan966x_oic->regs; | 
 | 	gc->chip_types[0].regs.enable = chip_regs->reg_off_ena_set; | 
 | 	gc->chip_types[0].regs.disable = chip_regs->reg_off_ena_clr; | 
 | 	gc->chip_types[0].regs.ack = chip_regs->reg_off_sticky; | 
 | 	gc->chip_types[0].chip.irq_startup = lan966x_oic_irq_startup; | 
 | 	gc->chip_types[0].chip.irq_shutdown = lan966x_oic_irq_shutdown; | 
 | 	gc->chip_types[0].chip.irq_set_type = lan966x_oic_irq_set_type; | 
 | 	gc->chip_types[0].chip.irq_mask = irq_gc_mask_disable_reg; | 
 | 	gc->chip_types[0].chip.irq_unmask = irq_gc_unmask_enable_reg; | 
 | 	gc->chip_types[0].chip.irq_ack = irq_gc_ack_set_bit; | 
 | 	gc->private = chip_regs; | 
 |  | 
 | 	/* Disable all interrupts handled by this chip */ | 
 | 	irq_reg_writel(gc, ~0U, chip_regs->reg_off_ena_clr); | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void lan966x_oic_chip_exit(struct irq_chip_generic *gc) | 
 | { | 
 | 	/* Disable and ack all interrupts handled by this chip */ | 
 | 	irq_reg_writel(gc, ~0U, gc->chip_types[0].regs.disable); | 
 | 	irq_reg_writel(gc, ~0U, gc->chip_types[0].regs.ack); | 
 | } | 
 |  | 
 | static int lan966x_oic_domain_init(struct irq_domain *d) | 
 | { | 
 | 	struct lan966x_oic_data *lan966x_oic = d->host_data; | 
 |  | 
 | 	irq_set_chained_handler_and_data(lan966x_oic->irq, lan966x_oic_irq_handler, d); | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void lan966x_oic_domain_exit(struct irq_domain *d) | 
 | { | 
 | 	struct lan966x_oic_data *lan966x_oic = d->host_data; | 
 |  | 
 | 	irq_set_chained_handler_and_data(lan966x_oic->irq, NULL, NULL); | 
 | } | 
 |  | 
 | static int lan966x_oic_probe(struct platform_device *pdev) | 
 | { | 
 | 	struct irq_domain_chip_generic_info dgc_info = { | 
 | 		.name		= "lan966x-oic", | 
 | 		.handler	= handle_level_irq, | 
 | 		.irqs_per_chip	= 32, | 
 | 		.num_ct		= 1, | 
 | 		.init		= lan966x_oic_chip_init, | 
 | 		.exit		= lan966x_oic_chip_exit, | 
 | 	}; | 
 | 	struct irq_domain_info d_info = { | 
 | 		.fwnode		= of_fwnode_handle(pdev->dev.of_node), | 
 | 		.domain_flags	= IRQ_DOMAIN_FLAG_DESTROY_GC, | 
 | 		.size		= LAN966X_OIC_NR_IRQ, | 
 | 		.hwirq_max	= LAN966X_OIC_NR_IRQ, | 
 | 		.ops		= &irq_generic_chip_ops, | 
 | 		.dgc_info	= &dgc_info, | 
 | 		.init		= lan966x_oic_domain_init, | 
 | 		.exit		= lan966x_oic_domain_exit, | 
 | 	}; | 
 | 	struct lan966x_oic_data *lan966x_oic; | 
 | 	struct device *dev = &pdev->dev; | 
 | 	struct irq_domain *domain; | 
 |  | 
 | 	lan966x_oic = devm_kmalloc(dev, sizeof(*lan966x_oic), GFP_KERNEL); | 
 | 	if (!lan966x_oic) | 
 | 		return -ENOMEM; | 
 |  | 
 | 	lan966x_oic->regs = devm_platform_ioremap_resource(pdev, 0); | 
 | 	if (IS_ERR(lan966x_oic->regs)) | 
 | 		return dev_err_probe(dev, PTR_ERR(lan966x_oic->regs), | 
 | 				     "failed to map resource\n"); | 
 |  | 
 | 	lan966x_oic->irq = platform_get_irq(pdev, 0); | 
 | 	if (lan966x_oic->irq < 0) | 
 | 		return dev_err_probe(dev, lan966x_oic->irq, "failed to get the IRQ\n"); | 
 |  | 
 | 	d_info.host_data = lan966x_oic; | 
 | 	domain = devm_irq_domain_instantiate(dev, &d_info); | 
 | 	if (IS_ERR(domain)) | 
 | 		return dev_err_probe(dev, PTR_ERR(domain), | 
 | 				     "failed to instantiate the IRQ domain\n"); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static const struct of_device_id lan966x_oic_of_match[] = { | 
 | 	{ .compatible = "microchip,lan966x-oic" }, | 
 | 	{} /* sentinel */ | 
 | }; | 
 | MODULE_DEVICE_TABLE(of, lan966x_oic_of_match); | 
 |  | 
 | static struct platform_driver lan966x_oic_driver = { | 
 | 	.probe = lan966x_oic_probe, | 
 | 	.driver = { | 
 | 		.name = "lan966x-oic", | 
 | 		.of_match_table = lan966x_oic_of_match, | 
 | 	}, | 
 | }; | 
 | module_platform_driver(lan966x_oic_driver); | 
 |  | 
 | MODULE_AUTHOR("Herve Codina <herve.codina@bootlin.com>"); | 
 | MODULE_DESCRIPTION("Microchip LAN966x OIC driver"); | 
 | MODULE_LICENSE("GPL"); |