root/drivers/irqchip/irq-mvebu-sei.c
// SPDX-License-Identifier: GPL-2.0

#define pr_fmt(fmt) "mvebu-sei: " fmt

#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/irqchip.h>
#include <linux/irqchip/chained_irq.h>
#include <linux/irqdomain.h>
#include <linux/kernel.h>
#include <linux/msi.h>
#include <linux/platform_device.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_platform.h>

#include <linux/irqchip/irq-msi-lib.h>

/* Cause register */
#define GICP_SECR(idx)          (0x0  + ((idx) * 0x4))
/* Mask register */
#define GICP_SEMR(idx)          (0x20 + ((idx) * 0x4))
#define GICP_SET_SEI_OFFSET     0x30

#define SEI_IRQ_COUNT_PER_REG   32
#define SEI_IRQ_REG_COUNT       2
#define SEI_IRQ_COUNT           (SEI_IRQ_COUNT_PER_REG * SEI_IRQ_REG_COUNT)
#define SEI_IRQ_REG_IDX(irq_id) ((irq_id) / SEI_IRQ_COUNT_PER_REG)
#define SEI_IRQ_REG_BIT(irq_id) ((irq_id) % SEI_IRQ_COUNT_PER_REG)

struct mvebu_sei_interrupt_range {
        u32 first;
        u32 size;
};

struct mvebu_sei_caps {
        struct mvebu_sei_interrupt_range ap_range;
        struct mvebu_sei_interrupt_range cp_range;
};

struct mvebu_sei {
        struct device *dev;
        void __iomem *base;
        struct resource *res;
        struct irq_domain *sei_domain;
        struct irq_domain *ap_domain;
        struct irq_domain *cp_domain;
        const struct mvebu_sei_caps *caps;

        /* Lock on MSI allocations/releases */
        struct mutex cp_msi_lock;
        DECLARE_BITMAP(cp_msi_bitmap, SEI_IRQ_COUNT);

        /* Lock on IRQ masking register */
        raw_spinlock_t mask_lock;
};

static void mvebu_sei_ack_irq(struct irq_data *d)
{
        struct mvebu_sei *sei = irq_data_get_irq_chip_data(d);
        u32 reg_idx = SEI_IRQ_REG_IDX(d->hwirq);

        writel_relaxed(BIT(SEI_IRQ_REG_BIT(d->hwirq)),
                       sei->base + GICP_SECR(reg_idx));
}

static void mvebu_sei_mask_irq(struct irq_data *d)
{
        struct mvebu_sei *sei = irq_data_get_irq_chip_data(d);
        u32 reg, reg_idx = SEI_IRQ_REG_IDX(d->hwirq);
        unsigned long flags;

        /* 1 disables the interrupt */
        raw_spin_lock_irqsave(&sei->mask_lock, flags);
        reg = readl_relaxed(sei->base + GICP_SEMR(reg_idx));
        reg |= BIT(SEI_IRQ_REG_BIT(d->hwirq));
        writel_relaxed(reg, sei->base + GICP_SEMR(reg_idx));
        raw_spin_unlock_irqrestore(&sei->mask_lock, flags);
}

static void mvebu_sei_unmask_irq(struct irq_data *d)
{
        struct mvebu_sei *sei = irq_data_get_irq_chip_data(d);
        u32 reg, reg_idx = SEI_IRQ_REG_IDX(d->hwirq);
        unsigned long flags;

        /* 0 enables the interrupt */
        raw_spin_lock_irqsave(&sei->mask_lock, flags);
        reg = readl_relaxed(sei->base + GICP_SEMR(reg_idx));
        reg &= ~BIT(SEI_IRQ_REG_BIT(d->hwirq));
        writel_relaxed(reg, sei->base + GICP_SEMR(reg_idx));
        raw_spin_unlock_irqrestore(&sei->mask_lock, flags);
}

static int mvebu_sei_set_affinity(struct irq_data *d,
                                  const struct cpumask *mask_val,
                                  bool force)
{
        return -EINVAL;
}

static int mvebu_sei_set_irqchip_state(struct irq_data *d,
                                       enum irqchip_irq_state which,
                                       bool state)
{
        /* We can only clear the pending state by acking the interrupt */
        if (which != IRQCHIP_STATE_PENDING || state)
                return -EINVAL;

        mvebu_sei_ack_irq(d);
        return 0;
}

static struct irq_chip mvebu_sei_irq_chip = {
        .name                   = "SEI",
        .irq_ack                = mvebu_sei_ack_irq,
        .irq_mask               = mvebu_sei_mask_irq,
        .irq_unmask             = mvebu_sei_unmask_irq,
        .irq_set_affinity       = mvebu_sei_set_affinity,
        .irq_set_irqchip_state  = mvebu_sei_set_irqchip_state,
};

static int mvebu_sei_ap_set_type(struct irq_data *data, unsigned int type)
{
        if ((type & IRQ_TYPE_SENSE_MASK) != IRQ_TYPE_LEVEL_HIGH)
                return -EINVAL;

        return 0;
}

static struct irq_chip mvebu_sei_ap_irq_chip = {
        .name                   = "AP SEI",
        .irq_ack                = irq_chip_ack_parent,
        .irq_mask               = irq_chip_mask_parent,
        .irq_unmask             = irq_chip_unmask_parent,
        .irq_set_affinity       = irq_chip_set_affinity_parent,
        .irq_set_type           = mvebu_sei_ap_set_type,
};

static void mvebu_sei_cp_compose_msi_msg(struct irq_data *data,
                                         struct msi_msg *msg)
{
        struct mvebu_sei *sei = data->chip_data;
        phys_addr_t set = sei->res->start + GICP_SET_SEI_OFFSET;

        msg->data = data->hwirq + sei->caps->cp_range.first;
        msg->address_lo = lower_32_bits(set);
        msg->address_hi = upper_32_bits(set);
}

static int mvebu_sei_cp_set_type(struct irq_data *data, unsigned int type)
{
        if ((type & IRQ_TYPE_SENSE_MASK) != IRQ_TYPE_EDGE_RISING)
                return -EINVAL;

        return 0;
}

static struct irq_chip mvebu_sei_cp_irq_chip = {
        .name                   = "CP SEI",
        .irq_ack                = irq_chip_ack_parent,
        .irq_mask               = irq_chip_mask_parent,
        .irq_unmask             = irq_chip_unmask_parent,
        .irq_set_affinity       = irq_chip_set_affinity_parent,
        .irq_set_type           = mvebu_sei_cp_set_type,
        .irq_compose_msi_msg    = mvebu_sei_cp_compose_msi_msg,
};

static int mvebu_sei_domain_alloc(struct irq_domain *domain, unsigned int virq,
                                  unsigned int nr_irqs, void *arg)
{
        struct mvebu_sei *sei = domain->host_data;
        struct irq_fwspec *fwspec = arg;

        /* Not much to do, just setup the irqdata */
        irq_domain_set_hwirq_and_chip(domain, virq, fwspec->param[0],
                                      &mvebu_sei_irq_chip, sei);

        return 0;
}

static void mvebu_sei_domain_free(struct irq_domain *domain, unsigned int virq,
                                  unsigned int nr_irqs)
{
        int i;

        for (i = 0; i < nr_irqs; i++) {
                struct irq_data *d = irq_domain_get_irq_data(domain, virq + i);
                irq_set_handler(virq + i, NULL);
                irq_domain_reset_irq_data(d);
        }
}

static const struct irq_domain_ops mvebu_sei_domain_ops = {
        .alloc  = mvebu_sei_domain_alloc,
        .free   = mvebu_sei_domain_free,
};

static int mvebu_sei_ap_translate(struct irq_domain *domain,
                                  struct irq_fwspec *fwspec,
                                  unsigned long *hwirq,
                                  unsigned int *type)
{
        *hwirq = fwspec->param[0];
        *type  = IRQ_TYPE_LEVEL_HIGH;

        return 0;
}

static int mvebu_sei_ap_alloc(struct irq_domain *domain, unsigned int virq,
                              unsigned int nr_irqs, void *arg)
{
        struct mvebu_sei *sei = domain->host_data;
        struct irq_fwspec fwspec;
        unsigned long hwirq;
        unsigned int type;
        int err;

        mvebu_sei_ap_translate(domain, arg, &hwirq, &type);

        fwspec.fwnode = domain->parent->fwnode;
        fwspec.param_count = 1;
        fwspec.param[0] = hwirq + sei->caps->ap_range.first;

        err = irq_domain_alloc_irqs_parent(domain, virq, 1, &fwspec);
        if (err)
                return err;

        irq_domain_set_info(domain, virq, hwirq,
                            &mvebu_sei_ap_irq_chip, sei,
                            handle_level_irq, NULL, NULL);
        irq_set_probe(virq);

        return 0;
}

static const struct irq_domain_ops mvebu_sei_ap_domain_ops = {
        .translate      = mvebu_sei_ap_translate,
        .alloc          = mvebu_sei_ap_alloc,
        .free           = irq_domain_free_irqs_parent,
};

static void mvebu_sei_cp_release_irq(struct mvebu_sei *sei, unsigned long hwirq)
{
        mutex_lock(&sei->cp_msi_lock);
        clear_bit(hwirq, sei->cp_msi_bitmap);
        mutex_unlock(&sei->cp_msi_lock);
}

static int mvebu_sei_cp_domain_alloc(struct irq_domain *domain,
                                     unsigned int virq, unsigned int nr_irqs,
                                     void *args)
{
        struct mvebu_sei *sei = domain->host_data;
        struct irq_fwspec fwspec;
        unsigned long hwirq;
        int ret;

        /* The software only supports single allocations for now */
        if (nr_irqs != 1)
                return -ENOTSUPP;

        mutex_lock(&sei->cp_msi_lock);
        hwirq = find_first_zero_bit(sei->cp_msi_bitmap,
                                    sei->caps->cp_range.size);
        if (hwirq < sei->caps->cp_range.size)
                set_bit(hwirq, sei->cp_msi_bitmap);
        mutex_unlock(&sei->cp_msi_lock);

        if (hwirq == sei->caps->cp_range.size)
                return -ENOSPC;

        fwspec.fwnode = domain->parent->fwnode;
        fwspec.param_count = 1;
        fwspec.param[0] = hwirq + sei->caps->cp_range.first;

        ret = irq_domain_alloc_irqs_parent(domain, virq, 1, &fwspec);
        if (ret)
                goto free_irq;

        irq_domain_set_info(domain, virq, hwirq,
                            &mvebu_sei_cp_irq_chip, sei,
                            handle_edge_irq, NULL, NULL);

        return 0;

free_irq:
        mvebu_sei_cp_release_irq(sei, hwirq);
        return ret;
}

static void mvebu_sei_cp_domain_free(struct irq_domain *domain,
                                     unsigned int virq, unsigned int nr_irqs)
{
        struct mvebu_sei *sei = domain->host_data;
        struct irq_data *d = irq_domain_get_irq_data(domain, virq);

        if (nr_irqs != 1 || d->hwirq >= sei->caps->cp_range.size) {
                dev_err(sei->dev, "Invalid hwirq %lu\n", d->hwirq);
                return;
        }

        mvebu_sei_cp_release_irq(sei, d->hwirq);
        irq_domain_free_irqs_parent(domain, virq, 1);
}

static const struct irq_domain_ops mvebu_sei_cp_domain_ops = {
        .select = msi_lib_irq_domain_select,
        .alloc  = mvebu_sei_cp_domain_alloc,
        .free   = mvebu_sei_cp_domain_free,
};

static void mvebu_sei_handle_cascade_irq(struct irq_desc *desc)
{
        struct mvebu_sei *sei = irq_desc_get_handler_data(desc);
        struct irq_chip *chip = irq_desc_get_chip(desc);
        u32 idx;

        chained_irq_enter(chip, desc);

        for (idx = 0; idx < SEI_IRQ_REG_COUNT; idx++) {
                unsigned long irqmap;
                int bit;

                irqmap = readl_relaxed(sei->base + GICP_SECR(idx));
                for_each_set_bit(bit, &irqmap, SEI_IRQ_COUNT_PER_REG) {
                        unsigned long hwirq;
                        int err;

                        hwirq = idx * SEI_IRQ_COUNT_PER_REG + bit;
                        err = generic_handle_domain_irq(sei->sei_domain, hwirq);
                        if (unlikely(err))
                                dev_warn(sei->dev, "Spurious IRQ detected (hwirq %lu)\n", hwirq);
                }
        }

        chained_irq_exit(chip, desc);
}

static void mvebu_sei_reset(struct mvebu_sei *sei)
{
        u32 reg_idx;

        /* Clear IRQ cause registers, mask all interrupts */
        for (reg_idx = 0; reg_idx < SEI_IRQ_REG_COUNT; reg_idx++) {
                writel_relaxed(0xFFFFFFFF, sei->base + GICP_SECR(reg_idx));
                writel_relaxed(0xFFFFFFFF, sei->base + GICP_SEMR(reg_idx));
        }
}

#define SEI_MSI_FLAGS_REQUIRED  (MSI_FLAG_USE_DEF_DOM_OPS |     \
                                 MSI_FLAG_USE_DEF_CHIP_OPS)

#define SEI_MSI_FLAGS_SUPPORTED (MSI_GENERIC_FLAGS_MASK)

static const struct msi_parent_ops sei_msi_parent_ops = {
        .supported_flags        = SEI_MSI_FLAGS_SUPPORTED,
        .required_flags         = SEI_MSI_FLAGS_REQUIRED,
        .chip_flags             = MSI_CHIP_FLAG_SET_EOI | MSI_CHIP_FLAG_SET_ACK,
        .bus_select_mask        = MATCH_PLATFORM_MSI,
        .bus_select_token       = DOMAIN_BUS_GENERIC_MSI,
        .prefix                 = "SEI-",
        .init_dev_msi_info      = msi_lib_init_dev_msi_info,
};

static int mvebu_sei_probe(struct platform_device *pdev)
{
        struct device_node *node = pdev->dev.of_node;
        struct irq_domain_info info = {
                .fwnode = of_fwnode_handle(node),
                .ops    = &mvebu_sei_cp_domain_ops,
        };
        struct mvebu_sei *sei;
        u32 parent_irq;
        int ret;

        sei = devm_kzalloc(&pdev->dev, sizeof(*sei), GFP_KERNEL);
        if (!sei)
                return -ENOMEM;

        sei->dev = &pdev->dev;

        mutex_init(&sei->cp_msi_lock);
        raw_spin_lock_init(&sei->mask_lock);

        sei->base = devm_platform_get_and_ioremap_resource(pdev, 0, &sei->res);
        if (IS_ERR(sei->base))
                return PTR_ERR(sei->base);

        /* Retrieve the SEI capabilities with the interrupt ranges */
        sei->caps = of_device_get_match_data(&pdev->dev);
        if (!sei->caps) {
                dev_err(sei->dev,
                        "Could not retrieve controller capabilities\n");
                return -EINVAL;
        }

        /*
         * Reserve the single (top-level) parent SPI IRQ from which all the
         * interrupts handled by this driver will be signaled.
         */
        parent_irq = irq_of_parse_and_map(node, 0);
        if (parent_irq <= 0) {
                dev_err(sei->dev, "Failed to retrieve top-level SPI IRQ\n");
                return -ENODEV;
        }

        /* Create the root SEI domain */
        sei->sei_domain = irq_domain_create_linear(of_fwnode_handle(node),
                                                   (sei->caps->ap_range.size +
                                                    sei->caps->cp_range.size),
                                                   &mvebu_sei_domain_ops,
                                                   sei);
        if (!sei->sei_domain) {
                dev_err(sei->dev, "Failed to create SEI IRQ domain\n");
                ret = -ENOMEM;
                goto dispose_irq;
        }

        irq_domain_update_bus_token(sei->sei_domain, DOMAIN_BUS_NEXUS);

        /* Create the 'wired' domain */
        sei->ap_domain = irq_domain_create_hierarchy(sei->sei_domain, 0,
                                                     sei->caps->ap_range.size,
                                                     of_fwnode_handle(node),
                                                     &mvebu_sei_ap_domain_ops,
                                                     sei);
        if (!sei->ap_domain) {
                dev_err(sei->dev, "Failed to create AP IRQ domain\n");
                ret = -ENOMEM;
                goto remove_sei_domain;
        }

        irq_domain_update_bus_token(sei->ap_domain, DOMAIN_BUS_WIRED);

        /* Create the 'MSI' domain */
        info.size = sei->caps->cp_range.size;
        info.host_data = sei;
        info.parent = sei->sei_domain;

        sei->cp_domain = msi_create_parent_irq_domain(&info, &sei_msi_parent_ops);
        if (!sei->cp_domain) {
                pr_err("Failed to create CPs IRQ domain\n");
                ret = -ENOMEM;
                goto remove_ap_domain;
        }

        mvebu_sei_reset(sei);

        irq_set_chained_handler_and_data(parent_irq, mvebu_sei_handle_cascade_irq, sei);
        return 0;

remove_ap_domain:
        irq_domain_remove(sei->ap_domain);
remove_sei_domain:
        irq_domain_remove(sei->sei_domain);
dispose_irq:
        irq_dispose_mapping(parent_irq);
        return ret;
}

static struct mvebu_sei_caps mvebu_sei_ap806_caps = {
        .ap_range = {
                .first = 0,
                .size = 21,
        },
        .cp_range = {
                .first = 21,
                .size = 43,
        },
};

static const struct of_device_id mvebu_sei_of_match[] = {
        {
                .compatible = "marvell,ap806-sei",
                .data = &mvebu_sei_ap806_caps,
        },
        {},
};

static struct platform_driver mvebu_sei_driver = {
        .probe  = mvebu_sei_probe,
        .driver = {
                .name = "mvebu-sei",
                .of_match_table = mvebu_sei_of_match,
        },
};
builtin_platform_driver(mvebu_sei_driver);