root/drivers/irqchip/irq-gic-v5-iwb.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/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 be the wire
                 * 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);

        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);