mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2025-08-05 16:54:27 +00:00

The GICv5 architecture implements the Interrupt Wire Bridge (IWB) in order to support wired interrupts that cannot be connected directly to an IRS and instead uses the ITS to translate a wire event into an IRQ signal. Add the wired-to-MSI IWB driver to manage IWB wired interrupts. An IWB is connected to an ITS and it has its own deviceID for all interrupt wires that it manages; the IWB input wire number must be exposed to the ITS as an eventID with a 1:1 mapping. This eventID is not programmable and therefore requires a new msi_alloc_info_t flag to make sure the ITS driver does not allocate an eventid for the wire but rather it uses the msi_alloc_info_t.hwirq number to gather the ITS eventID. Co-developed-by: Sascha Bischoff <sascha.bischoff@arm.com> Signed-off-by: Sascha Bischoff <sascha.bischoff@arm.com> Co-developed-by: Timothy Hayes <timothy.hayes@arm.com> Signed-off-by: Timothy Hayes <timothy.hayes@arm.com> Signed-off-by: Lorenzo Pieralisi <lpieralisi@kernel.org> Reviewed-by: Marc Zyngier <maz@kernel.org> Cc: Thomas Gleixner <tglx@linutronix.de> Cc: Marc Zyngier <maz@kernel.org> Link: https://lore.kernel.org/r/20250703-gicv5-host-v7-29-12e71f1b3528@kernel.org Signed-off-by: Marc Zyngier <maz@kernel.org>
284 lines
6.9 KiB
C
284 lines
6.9 KiB
C
// 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/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))
|
|
return -EINVAL;
|
|
|
|
if (fwspec->param_count < 2)
|
|
return -EINVAL;
|
|
|
|
/*
|
|
* param[0] is be the wire
|
|
* param[1] is the interrupt type
|
|
*/
|
|
*hwirq = 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(sizeof(*iwb_node),
|
|
GFP_KERNEL);
|
|
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;
|
|
int ret;
|
|
|
|
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)) {
|
|
ret = PTR_ERR(iwb_node);
|
|
goto out_unmap;
|
|
}
|
|
|
|
return 0;
|
|
|
|
out_unmap:
|
|
iounmap(iwb_base);
|
|
return ret;
|
|
}
|
|
|
|
static const struct of_device_id gicv5_iwb_of_match[] = {
|
|
{ .compatible = "arm,gic-v5-iwb" },
|
|
{ /* END */ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, gicv5_iwb_of_match);
|
|
|
|
static struct platform_driver gicv5_iwb_platform_driver = {
|
|
.driver = {
|
|
.name = "GICv5 IWB",
|
|
.of_match_table = gicv5_iwb_of_match,
|
|
.suppress_bind_attrs = true,
|
|
},
|
|
.probe = gicv5_iwb_device_probe,
|
|
};
|
|
|
|
module_platform_driver(gicv5_iwb_platform_driver);
|