| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Copyright (C) 2024-2025 ARM Limited, All Rights Reserved. | |
| */ | |
| #define pr_fmt(fmt) "GICv5 IWB: " fmt | |
| #include <linux/acpi.h> | |
| #include <linux/init.h> | |
| #include <linux/kernel.h> | |
| #include <linux/msi.h> | |
| #include <linux/of.h> | |
| #include <linux/of_address.h> | |
| #include <linux/of_platform.h> | |
| #include <linux/irqchip.h> | |
| #include <linux/irqchip/arm-gic-v5.h> | |
| struct gicv5_iwb_chip_data { | |
| void __iomem *iwb_base; | |
| u16 nr_regs; | |
| }; | |
| static u32 iwb_readl_relaxed(struct gicv5_iwb_chip_data *iwb_node, const u32 reg_offset) | |
| { | |
| return readl_relaxed(iwb_node->iwb_base + reg_offset); | |
| } | |
| static void iwb_writel_relaxed(struct gicv5_iwb_chip_data *iwb_node, const u32 val, | |
| const u32 reg_offset) | |
| { | |
| writel_relaxed(val, iwb_node->iwb_base + reg_offset); | |
| } | |
| static int gicv5_iwb_wait_for_wenabler(struct gicv5_iwb_chip_data *iwb_node) | |
| { | |
| return gicv5_wait_for_op_atomic(iwb_node->iwb_base, GICV5_IWB_WENABLE_STATUSR, | |
| GICV5_IWB_WENABLE_STATUSR_IDLE, NULL); | |
| } | |
| static int __gicv5_iwb_set_wire_enable(struct gicv5_iwb_chip_data *iwb_node, | |
| u32 iwb_wire, bool enable) | |
| { | |
| u32 n = iwb_wire / 32; | |
| u8 i = iwb_wire % 32; | |
| u32 val; | |
| if (n >= iwb_node->nr_regs) { | |
| pr_err("IWB_WENABLER<n> is invalid for n=%u\n", n); | |
| return -EINVAL; | |
| } | |
| /* | |
| * Enable IWB wire/pin at this point | |
| * Note: This is not the same as enabling the interrupt | |
| */ | |
| val = iwb_readl_relaxed(iwb_node, GICV5_IWB_WENABLER + (4 * n)); | |
| if (enable) | |
| val |= BIT(i); | |
| else | |
| val &= ~BIT(i); | |
| iwb_writel_relaxed(iwb_node, val, GICV5_IWB_WENABLER + (4 * n)); | |
| return gicv5_iwb_wait_for_wenabler(iwb_node); | |
| } | |
| static int gicv5_iwb_enable_wire(struct gicv5_iwb_chip_data *iwb_node, | |
| u32 iwb_wire) | |
| { | |
| return __gicv5_iwb_set_wire_enable(iwb_node, iwb_wire, true); | |
| } | |
| static int gicv5_iwb_disable_wire(struct gicv5_iwb_chip_data *iwb_node, | |
| u32 iwb_wire) | |
| { | |
| return __gicv5_iwb_set_wire_enable(iwb_node, iwb_wire, false); | |
| } | |
| static void gicv5_iwb_irq_disable(struct irq_data *d) | |
| { | |
| struct gicv5_iwb_chip_data *iwb_node = irq_data_get_irq_chip_data(d); | |
| gicv5_iwb_disable_wire(iwb_node, d->hwirq); | |
| irq_chip_disable_parent(d); | |
| } | |
| static void gicv5_iwb_irq_enable(struct irq_data *d) | |
| { | |
| struct gicv5_iwb_chip_data *iwb_node = irq_data_get_irq_chip_data(d); | |
| gicv5_iwb_enable_wire(iwb_node, d->hwirq); | |
| irq_chip_enable_parent(d); | |
| } | |
| static int gicv5_iwb_set_type(struct irq_data *d, unsigned int type) | |
| { | |
| struct gicv5_iwb_chip_data *iwb_node = irq_data_get_irq_chip_data(d); | |
| u32 iwb_wire, n, wtmr; | |
| u8 i; | |
| iwb_wire = d->hwirq; | |
| i = iwb_wire % 32; | |
| n = iwb_wire / 32; | |
| if (n >= iwb_node->nr_regs) { | |
| pr_err_once("reg %u out of range\n", n); | |
| return -EINVAL; | |
| } | |
| wtmr = iwb_readl_relaxed(iwb_node, GICV5_IWB_WTMR + (4 * n)); | |
| switch (type) { | |
| case IRQ_TYPE_LEVEL_HIGH: | |
| case IRQ_TYPE_LEVEL_LOW: | |
| wtmr |= BIT(i); | |
| break; | |
| case IRQ_TYPE_EDGE_RISING: | |
| case IRQ_TYPE_EDGE_FALLING: | |
| wtmr &= ~BIT(i); | |
| break; | |
| default: | |
| pr_debug("unexpected wire trigger mode"); | |
| return -EINVAL; | |
| } | |
| iwb_writel_relaxed(iwb_node, wtmr, GICV5_IWB_WTMR + (4 * n)); | |
| return 0; | |
| } | |
| static void gicv5_iwb_domain_set_desc(msi_alloc_info_t *alloc_info, struct msi_desc *desc) | |
| { | |
| alloc_info->desc = desc; | |
| alloc_info->hwirq = (u32)desc->data.icookie.value; | |
| } | |
| static int gicv5_iwb_irq_domain_translate(struct irq_domain *d, struct irq_fwspec *fwspec, | |
| irq_hw_number_t *hwirq, | |
| unsigned int *type) | |
| { | |
| if (is_of_node(fwspec->fwnode)) { | |
| if (fwspec->param_count < 2) | |
| return -EINVAL; | |
| /* | |
| * param[0] is the wire interrupt number | |
| * param[1] is the interrupt type | |
| */ | |
| *hwirq = fwspec->param[0]; | |
| *type = fwspec->param[1] & IRQ_TYPE_SENSE_MASK; | |
| } | |
| if (is_acpi_device_node(fwspec->fwnode)) { | |
| if (fwspec->param_count < 2) | |
| return -EINVAL; | |
| /* | |
| * Extract the wire from param[0] | |
| * param[1] is the interrupt type | |
| */ | |
| *hwirq = FIELD_GET(GICV5_GSI_IWB_WIRE, fwspec->param[0]); | |
| *type = fwspec->param[1] & IRQ_TYPE_SENSE_MASK; | |
| } | |
| return 0; | |
| } | |
| static void gicv5_iwb_write_msi_msg(struct irq_data *d, struct msi_msg *msg) {} | |
| static const struct msi_domain_template iwb_msi_template = { | |
| .chip = { | |
| .name = "GICv5-IWB", | |
| .irq_mask = irq_chip_mask_parent, | |
| .irq_unmask = irq_chip_unmask_parent, | |
| .irq_enable = gicv5_iwb_irq_enable, | |
| .irq_disable = gicv5_iwb_irq_disable, | |
| .irq_eoi = irq_chip_eoi_parent, | |
| .irq_set_type = gicv5_iwb_set_type, | |
| .irq_write_msi_msg = gicv5_iwb_write_msi_msg, | |
| .irq_set_affinity = irq_chip_set_affinity_parent, | |
| .irq_get_irqchip_state = irq_chip_get_parent_state, | |
| .irq_set_irqchip_state = irq_chip_set_parent_state, | |
| .flags = IRQCHIP_SET_TYPE_MASKED | | |
| IRQCHIP_SKIP_SET_WAKE | | |
| IRQCHIP_MASK_ON_SUSPEND, | |
| }, | |
| .ops = { | |
| .set_desc = gicv5_iwb_domain_set_desc, | |
| .msi_translate = gicv5_iwb_irq_domain_translate, | |
| }, | |
| .info = { | |
| .bus_token = DOMAIN_BUS_WIRED_TO_MSI, | |
| .flags = MSI_FLAG_USE_DEV_FWNODE, | |
| }, | |
| .alloc_info = { | |
| .flags = MSI_ALLOC_FLAGS_FIXED_MSG_DATA, | |
| }, | |
| }; | |
| static bool gicv5_iwb_create_device_domain(struct device *dev, unsigned int size, | |
| struct gicv5_iwb_chip_data *iwb_node) | |
| { | |
| if (WARN_ON_ONCE(!dev->msi.domain)) | |
| return false; | |
| return msi_create_device_irq_domain(dev, MSI_DEFAULT_DOMAIN, | |
| &iwb_msi_template, size, | |
| NULL, iwb_node); | |
| } | |
| static struct gicv5_iwb_chip_data * | |
| gicv5_iwb_init_bases(void __iomem *iwb_base, struct platform_device *pdev) | |
| { | |
| u32 nr_wires, idr0, cr0; | |
| unsigned int n; | |
| int ret; | |
| struct gicv5_iwb_chip_data *iwb_node __free(kfree) = kzalloc_obj(*iwb_node); | |
| if (!iwb_node) | |
| return ERR_PTR(-ENOMEM); | |
| iwb_node->iwb_base = iwb_base; | |
| idr0 = iwb_readl_relaxed(iwb_node, GICV5_IWB_IDR0); | |
| nr_wires = (FIELD_GET(GICV5_IWB_IDR0_IW_RANGE, idr0) + 1) * 32; | |
| cr0 = iwb_readl_relaxed(iwb_node, GICV5_IWB_CR0); | |
| if (!FIELD_GET(GICV5_IWB_CR0_IWBEN, cr0)) { | |
| dev_err(&pdev->dev, "IWB must be enabled in firmware\n"); | |
| return ERR_PTR(-EINVAL); | |
| } | |
| iwb_node->nr_regs = FIELD_GET(GICV5_IWB_IDR0_IW_RANGE, idr0) + 1; | |
| for (n = 0; n < iwb_node->nr_regs; n++) | |
| iwb_writel_relaxed(iwb_node, 0, GICV5_IWB_WENABLER + (sizeof(u32) * n)); | |
| ret = gicv5_iwb_wait_for_wenabler(iwb_node); | |
| if (ret) | |
| return ERR_PTR(ret); | |
| if (!gicv5_iwb_create_device_domain(&pdev->dev, nr_wires, iwb_node)) | |
| return ERR_PTR(-ENOMEM); | |
| return_ptr(iwb_node); | |
| } | |
| static int gicv5_iwb_device_probe(struct platform_device *pdev) | |
| { | |
| struct gicv5_iwb_chip_data *iwb_node; | |
| void __iomem *iwb_base; | |
| struct resource *res; | |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
| if (!res) | |
| return -EINVAL; | |
| iwb_base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); | |
| if (!iwb_base) { | |
| dev_err(&pdev->dev, "failed to ioremap %pR\n", res); | |
| return -ENOMEM; | |
| } | |
| iwb_node = gicv5_iwb_init_bases(iwb_base, pdev); | |
| if (IS_ERR(iwb_node)) | |
| return PTR_ERR(iwb_node); | |
| acpi_device_clear_deps(&pdev->dev); | |
| return 0; | |
| } | |
| static const struct of_device_id gicv5_iwb_of_match[] = { | |
| { .compatible = "arm,gic-v5-iwb" }, | |
| { /* END */ } | |
| }; | |
| MODULE_DEVICE_TABLE(of, gicv5_iwb_of_match); | |
| #ifdef CONFIG_ACPI | |
| static const struct acpi_device_id iwb_acpi_match[] = { | |
| { "ARMH0003", 0 }, | |
| {} | |
| }; | |
| #endif | |
| static struct platform_driver gicv5_iwb_platform_driver = { | |
| .driver = { | |
| .name = "GICv5 IWB", | |
| .of_match_table = gicv5_iwb_of_match, | |
| .acpi_match_table = ACPI_PTR(iwb_acpi_match), | |
| .suppress_bind_attrs = true, | |
| }, | |
| .probe = gicv5_iwb_device_probe, | |
| }; | |
| module_platform_driver(gicv5_iwb_platform_driver); |