root/drivers/irqchip/exynos-combiner.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2010-2011 Samsung Electronics Co., Ltd.
 *              http://www.samsung.com
 *
 * Combiner irqchip for EXYNOS
 */
#include <linux/err.h>
#include <linux/export.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/syscore_ops.h>
#include <linux/irqdomain.h>
#include <linux/irqchip.h>
#include <linux/irqchip/chained_irq.h>
#include <linux/interrupt.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>

#define COMBINER_ENABLE_SET     0x0
#define COMBINER_ENABLE_CLEAR   0x4
#define COMBINER_INT_STATUS     0xC

#define IRQ_IN_COMBINER         8

static DEFINE_SPINLOCK(irq_controller_lock);

struct combiner_chip_data {
        unsigned int hwirq_offset;
        unsigned int irq_mask;
        void __iomem *base;
        unsigned int parent_irq;
#ifdef CONFIG_PM
        u32 pm_save;
#endif
};

static struct combiner_chip_data *combiner_data;
static struct irq_domain *combiner_irq_domain;
static unsigned int max_nr = 20;

static inline void __iomem *combiner_base(struct irq_data *data)
{
        struct combiner_chip_data *combiner_data =
                irq_data_get_irq_chip_data(data);

        return combiner_data->base;
}

static void combiner_mask_irq(struct irq_data *data)
{
        u32 mask = 1 << (data->hwirq % 32);

        writel_relaxed(mask, combiner_base(data) + COMBINER_ENABLE_CLEAR);
}

static void combiner_unmask_irq(struct irq_data *data)
{
        u32 mask = 1 << (data->hwirq % 32);

        writel_relaxed(mask, combiner_base(data) + COMBINER_ENABLE_SET);
}

static void combiner_handle_cascade_irq(struct irq_desc *desc)
{
        struct combiner_chip_data *chip_data = irq_desc_get_handler_data(desc);
        struct irq_chip *chip = irq_desc_get_chip(desc);
        unsigned int combiner_irq;
        unsigned long status;
        int ret;

        chained_irq_enter(chip, desc);

        spin_lock(&irq_controller_lock);
        status = readl_relaxed(chip_data->base + COMBINER_INT_STATUS);
        spin_unlock(&irq_controller_lock);
        status &= chip_data->irq_mask;

        if (status == 0)
                goto out;

        combiner_irq = chip_data->hwirq_offset + __ffs(status);
        ret = generic_handle_domain_irq(combiner_irq_domain, combiner_irq);
        if (unlikely(ret))
                handle_bad_irq(desc);

 out:
        chained_irq_exit(chip, desc);
}

#ifdef CONFIG_SMP
static int combiner_set_affinity(struct irq_data *d,
                                 const struct cpumask *mask_val, bool force)
{
        struct combiner_chip_data *chip_data = irq_data_get_irq_chip_data(d);
        struct irq_chip *chip = irq_get_chip(chip_data->parent_irq);
        struct irq_data *data = irq_get_irq_data(chip_data->parent_irq);

        if (chip && chip->irq_set_affinity)
                return chip->irq_set_affinity(data, mask_val, force);
        else
                return -EINVAL;
}
#endif

static struct irq_chip combiner_chip = {
        .name                   = "COMBINER",
        .irq_mask               = combiner_mask_irq,
        .irq_unmask             = combiner_unmask_irq,
#ifdef CONFIG_SMP
        .irq_set_affinity       = combiner_set_affinity,
#endif
};

static void __init combiner_cascade_irq(struct combiner_chip_data *combiner_data,
                                        unsigned int irq)
{
        irq_set_chained_handler_and_data(irq, combiner_handle_cascade_irq,
                                         combiner_data);
}

static void __init combiner_init_one(struct combiner_chip_data *combiner_data,
                                     unsigned int combiner_nr,
                                     void __iomem *base, unsigned int irq)
{
        combiner_data->base = base;
        combiner_data->hwirq_offset = (combiner_nr & ~3) * IRQ_IN_COMBINER;
        combiner_data->irq_mask = 0xff << ((combiner_nr % 4) << 3);
        combiner_data->parent_irq = irq;

        /* Disable all interrupts */
        writel_relaxed(combiner_data->irq_mask, base + COMBINER_ENABLE_CLEAR);
}

static int combiner_irq_domain_xlate(struct irq_domain *d,
                                     struct device_node *controller,
                                     const u32 *intspec, unsigned int intsize,
                                     unsigned long *out_hwirq,
                                     unsigned int *out_type)
{
        if (irq_domain_get_of_node(d) != controller)
                return -EINVAL;

        if (intsize < 2)
                return -EINVAL;

        *out_hwirq = intspec[0] * IRQ_IN_COMBINER + intspec[1];
        *out_type = 0;

        return 0;
}

static int combiner_irq_domain_map(struct irq_domain *d, unsigned int irq,
                                   irq_hw_number_t hw)
{
        struct combiner_chip_data *combiner_data = d->host_data;

        irq_set_chip_and_handler(irq, &combiner_chip, handle_level_irq);
        irq_set_chip_data(irq, &combiner_data[hw >> 3]);
        irq_set_probe(irq);

        return 0;
}

static const struct irq_domain_ops combiner_irq_domain_ops = {
        .xlate  = combiner_irq_domain_xlate,
        .map    = combiner_irq_domain_map,
};

static void __init combiner_init(void __iomem *combiner_base,
                                 struct device_node *np)
{
        int i, irq;
        unsigned int nr_irq;

        nr_irq = max_nr * IRQ_IN_COMBINER;

        combiner_data = kzalloc_objs(*combiner_data, max_nr);
        if (!combiner_data)
                return;

        combiner_irq_domain = irq_domain_create_linear(of_fwnode_handle(np), nr_irq,
                                &combiner_irq_domain_ops, combiner_data);
        if (WARN_ON(!combiner_irq_domain)) {
                pr_warn("%s: irq domain init failed\n", __func__);
                return;
        }

        for (i = 0; i < max_nr; i++) {
                irq = irq_of_parse_and_map(np, i);

                combiner_init_one(&combiner_data[i], i,
                                  combiner_base + (i >> 2) * 0x10, irq);
                combiner_cascade_irq(&combiner_data[i], irq);
        }
}

#ifdef CONFIG_PM

/**
 * combiner_suspend - save interrupt combiner state before suspend
 * @data: syscore context
 *
 * Save the interrupt enable set register for all combiner groups since
 * the state is lost when the system enters into a sleep state.
 *
 */
static int combiner_suspend(void *data)
{
        int i;

        for (i = 0; i < max_nr; i++)
                combiner_data[i].pm_save =
                        readl_relaxed(combiner_data[i].base + COMBINER_ENABLE_SET);

        return 0;
}

/**
 * combiner_resume - restore interrupt combiner state after resume
 * @data: syscore context
 *
 * Restore the interrupt enable set register for all combiner groups since
 * the state is lost when the system enters into a sleep state on suspend.
 *
 */
static void combiner_resume(void *data)
{
        int i;

        for (i = 0; i < max_nr; i++) {
                writel_relaxed(combiner_data[i].irq_mask,
                             combiner_data[i].base + COMBINER_ENABLE_CLEAR);
                writel_relaxed(combiner_data[i].pm_save,
                             combiner_data[i].base + COMBINER_ENABLE_SET);
        }
}

#else
#define combiner_suspend        NULL
#define combiner_resume         NULL
#endif

static const struct syscore_ops combiner_syscore_ops = {
        .suspend        = combiner_suspend,
        .resume         = combiner_resume,
};

static struct syscore combiner_syscore = {
        .ops = &combiner_syscore_ops,
};

static int __init combiner_of_init(struct device_node *np,
                                   struct device_node *parent)
{
        void __iomem *combiner_base;

        combiner_base = of_iomap(np, 0);
        if (!combiner_base) {
                pr_err("%s: failed to map combiner registers\n", __func__);
                return -ENXIO;
        }

        if (of_property_read_u32(np, "samsung,combiner-nr", &max_nr)) {
                pr_info("%s: number of combiners not specified, "
                        "setting default as %d.\n",
                        __func__, max_nr);
        }

        combiner_init(combiner_base, np);

        register_syscore(&combiner_syscore);

        return 0;
}
IRQCHIP_DECLARE(exynos4210_combiner, "samsung,exynos4210-combiner",
                combiner_of_init);