| /* |
| * drivers/amlogic/irqchip/irq-meson-gpio-double-edge.c |
| * |
| * Copyright (C) 2017 Amlogic, Inc. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
| * more details. |
| * |
| */ |
| |
| #include <linux/of_address.h> |
| #include <linux/of.h> |
| #include <linux/irq.h> |
| #include <linux/irqdomain.h> |
| #include <linux/irqchip/chained_irq.h> |
| #include <linux/irqchip.h> |
| |
| #ifndef NO_IRQ |
| #define NO_IRQ ((unsigned int)(-1)) |
| #endif |
| |
| #define DRIVER_NAME "GPIO-INTC" |
| #define MESON_GPIO_BIND_PARENT_IRQ_NUM_MAX 2 |
| |
| #define REG_EDGE_POL 0x0 |
| #define REG_PIN_03_SEL 0x4 |
| #define REG_PIN_47_SEL 0x8 |
| #define REG_FILTER_SEL 0xc |
| |
| #define REG_EDGE_POL_MASK(x) (BIT(x) | BIT(16 + x)) |
| #define REG_EDGE_SET(x) BIT(x) |
| #define REG_POL_SET(x) BIT(16+x) |
| /** |
| * struct gpio_parent_irq - describe the parent irq for gpio |
| * |
| * @virq: virtual interrupt number of parent irq |
| * @owner: hwirq for gpio |
| */ |
| struct gpio_parent_irq { |
| int virq; |
| unsigned int owner; |
| }; |
| |
| struct meson_gpio_irq_data { |
| unsigned int nr_hwirq; |
| }; |
| |
| struct meson_gpio_intc { |
| unsigned char nr_gicirq; |
| spinlock_t lock; |
| void __iomem *base; |
| struct irq_domain *irqdomain; |
| struct gpio_parent_irq *parent_irqs; |
| const struct meson_gpio_irq_data *data; |
| }; |
| |
| static const struct meson_gpio_irq_data meson8_data = { |
| .nr_hwirq = 134, |
| }; |
| |
| static const struct meson_gpio_irq_data meson8b_data = { |
| .nr_hwirq = 119, |
| }; |
| |
| static const struct meson_gpio_irq_data gxbb_data = { |
| .nr_hwirq = 133, |
| }; |
| |
| static const struct meson_gpio_irq_data gxl_data = { |
| .nr_hwirq = 110, |
| }; |
| |
| static const struct meson_gpio_irq_data axg_data = { |
| .nr_hwirq = 100, |
| }; |
| |
| static const struct meson_gpio_irq_data txlx_data = { |
| .nr_hwirq = 119, |
| }; |
| |
| static const struct meson_gpio_irq_data g12a_data = { |
| .nr_hwirq = 100, |
| }; |
| |
| static const struct meson_gpio_irq_data txl_data = { |
| .nr_hwirq = 93, |
| }; |
| |
| static const struct meson_gpio_irq_data tl1_data = { |
| .nr_hwirq = 102, |
| }; |
| |
| static const struct of_device_id meson_gpio_irq_matches[] = { |
| { .compatible = "amlogic,meson8-gpio-intc", .data = &meson8_data }, |
| { .compatible = "amlogic,meson8b-gpio-intc", .data = &meson8b_data }, |
| { .compatible = "amlogic,meson-gxbb-gpio-intc", .data = &gxbb_data }, |
| { .compatible = "amlogic,meson-gxl-gpio-intc", .data = &gxl_data }, |
| { .compatible = "amlogic,meson-axg-gpio-intc", .data = &axg_data }, |
| { .compatible = "amlogic,meson-txlx-gpio-intc", .data = &txlx_data }, |
| { .compatible = "amlogic,meson-g12a-gpio-intc", .data = &g12a_data }, |
| { .compatible = "amlogic,meson-txl-gpio-intc", .data = &txl_data }, |
| { .compatible = "amlogic,meson-tl1-gpio-intc", .data = &tl1_data }, |
| { } |
| }; |
| |
| static void meson_reg_update_bits(void __iomem *base, unsigned int regoff, |
| unsigned int mask, const unsigned int val) |
| { |
| unsigned int orig; |
| |
| orig = readl_relaxed(base + regoff); |
| orig = orig & (~mask); |
| orig |= val; |
| writel_relaxed(orig, base + regoff); |
| } |
| |
| static int meson_gpio_irq_find_by_parent_virq(struct irq_data *irqd) |
| { |
| struct irq_desc *desc = irq_data_to_desc(irqd); |
| struct meson_gpio_intc *intc = irq_desc_get_handler_data(desc); |
| int idx; |
| |
| for (idx = 0; idx < intc->nr_gicirq; idx++) |
| if (intc->parent_irqs[idx].virq == irqd->irq) |
| return idx; |
| |
| return -EINVAL; |
| } |
| |
| static int meson_gpio_parent_irq_find_by_hwirq(struct irq_data *irqd, |
| int *data, int dlen) |
| { |
| struct meson_gpio_intc *intc = irq_data_get_irq_chip_data(irqd); |
| int nr = 0; |
| int idx; |
| |
| for (idx = 0; idx < intc->nr_gicirq; idx++) { |
| if (nr >= dlen) |
| break; |
| |
| if (intc->parent_irqs[idx].owner == irqd->hwirq) |
| data[nr++] = idx; |
| } |
| |
| return nr; |
| } |
| |
| /* |
| * NOP functions |
| */ |
| static void noop(struct irq_data *irqd) { } |
| |
| static void meson_gpio_parent_irq_unmask(int virq) |
| { |
| struct irq_data *parent_data; |
| |
| parent_data = irq_get_irq_data(virq); |
| |
| /*enable the interrupt line of gpio in GIC controller*/ |
| parent_data->chip->irq_unmask(parent_data); |
| } |
| static void meson_gpio_parent_irq_mask(int virq) |
| { |
| struct irq_data *parent_data; |
| |
| parent_data = irq_get_irq_data(virq); |
| |
| /*disable the interrupt line of gpio in GIC controller*/ |
| parent_data->chip->irq_mask(parent_data); |
| } |
| |
| static int meson_gpio_parent_irq_request(struct irq_data *irqd, |
| unsigned int type) |
| { |
| struct meson_gpio_intc *intc = irq_data_get_irq_chip_data(irqd); |
| struct irq_data *parent_data; |
| unsigned int idx; |
| |
| for (idx = 0; idx < intc->nr_gicirq; idx++) { |
| if (intc->parent_irqs[idx].owner == NO_IRQ) { |
| intc->parent_irqs[idx].owner = irqd->hwirq; |
| break; |
| } |
| } |
| |
| if (idx == intc->nr_gicirq) { |
| pr_warn("%s: no more gpio irqs available\n", DRIVER_NAME); |
| return -EINVAL; |
| } |
| |
| parent_data = irq_get_irq_data(intc->parent_irqs[idx].virq); |
| |
| /*set trigger type of gpio in GIC controller*/ |
| if (type & IRQ_TYPE_EDGE_BOTH) |
| parent_data->chip->irq_set_type(parent_data, |
| IRQ_TYPE_EDGE_RISING); |
| else |
| parent_data->chip->irq_set_type(parent_data, |
| IRQ_TYPE_LEVEL_HIGH); |
| |
| pr_info("%s: gpio virq[%d] connect to GIC hwirq[%ld]\n", DRIVER_NAME, |
| irqd->irq, parent_data->hwirq); |
| |
| return idx; |
| } |
| |
| static int meson_gpio_parent_irq_release(struct irq_data *irqd) |
| { |
| struct meson_gpio_intc *intc = irq_data_get_irq_chip_data(irqd); |
| int idx[MESON_GPIO_BIND_PARENT_IRQ_NUM_MAX]; |
| int nr; |
| |
| nr = meson_gpio_parent_irq_find_by_hwirq(irqd, idx, ARRAY_SIZE(idx)); |
| while (nr--) |
| intc->parent_irqs[idx[nr]].owner = NO_IRQ; |
| |
| return 0; |
| } |
| |
| static void meson_gpio_irq_regs_config(struct irq_data *irqd, |
| int idx, unsigned int type) |
| { |
| struct meson_gpio_intc *intc = irq_data_get_irq_chip_data(irqd); |
| unsigned int val = 0; |
| unsigned long flags; |
| int regoff; |
| int shift; |
| |
| spin_lock_irqsave(&intc->lock, flags); |
| |
| if (type & IRQ_TYPE_EDGE_BOTH) |
| val |= REG_EDGE_SET(idx); |
| |
| if (type & (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_LEVEL_LOW)) |
| val |= REG_POL_SET(idx); |
| |
| meson_reg_update_bits(intc->base, REG_EDGE_POL, |
| REG_EDGE_POL_MASK(idx), val); |
| |
| /*set the filter registers*/ |
| shift = idx << 2; |
| meson_reg_update_bits(intc->base, REG_FILTER_SEL, |
| 0x7 << shift, 0x7 << shift); |
| |
| /*set pin select register*/ |
| shift = (idx << 3) % 32; |
| regoff = (idx < 4) ? REG_PIN_03_SEL : REG_PIN_47_SEL; |
| meson_reg_update_bits(intc->base, regoff, |
| 0xff << shift, irqd->hwirq << shift); |
| |
| spin_unlock_irqrestore(&intc->lock, flags); |
| } |
| |
| static void meson_gpio_irq_enable(struct irq_data *irqd) |
| { |
| struct meson_gpio_intc *intc = irq_data_get_irq_chip_data(irqd); |
| int idx[MESON_GPIO_BIND_PARENT_IRQ_NUM_MAX]; |
| unsigned long flags; |
| int nr; |
| |
| spin_lock_irqsave(&intc->lock, flags); |
| |
| nr = meson_gpio_parent_irq_find_by_hwirq(irqd, |
| idx, ARRAY_SIZE(idx)); |
| |
| while (nr--) |
| meson_gpio_parent_irq_unmask(intc->parent_irqs[idx[nr]].virq); |
| |
| spin_unlock_irqrestore(&intc->lock, flags); |
| } |
| |
| static void meson_gpio_irq_disable(struct irq_data *irqd) |
| { |
| struct meson_gpio_intc *intc = irq_data_get_irq_chip_data(irqd); |
| int idx[MESON_GPIO_BIND_PARENT_IRQ_NUM_MAX]; |
| unsigned long flags; |
| int nr; |
| |
| spin_lock_irqsave(&intc->lock, flags); |
| |
| nr = meson_gpio_parent_irq_find_by_hwirq(irqd, idx, ARRAY_SIZE(idx)); |
| while (nr--) |
| meson_gpio_parent_irq_mask(intc->parent_irqs[idx[nr]].virq); |
| |
| spin_unlock_irqrestore(&intc->lock, flags); |
| } |
| |
| /** |
| *free gpio irq when free_irq() is called, and another pin can use it again. |
| */ |
| static void meson_gpio_irq_shutdown(struct irq_data *irqd) |
| { |
| struct meson_gpio_intc *intc = irq_data_get_irq_chip_data(irqd); |
| unsigned long flags; |
| |
| spin_lock_irqsave(&intc->lock, flags); |
| meson_gpio_parent_irq_release(irqd); |
| spin_unlock_irqrestore(&intc->lock, flags); |
| |
| meson_gpio_irq_disable(irqd); |
| } |
| |
| static int meson_gpio_irq_type(struct irq_data *irqd, unsigned int type) |
| { |
| struct meson_gpio_intc *intc = irq_data_get_irq_chip_data(irqd); |
| unsigned long flags; |
| int nr_parent_irq; |
| int idx; |
| |
| type = type & IRQ_TYPE_SENSE_MASK; |
| nr_parent_irq = (type == IRQ_TYPE_EDGE_BOTH) ? 2 : 1; |
| |
| while (nr_parent_irq--) { |
| if (type == IRQ_TYPE_EDGE_BOTH) |
| type = IRQ_TYPE_EDGE_FALLING; |
| |
| spin_lock_irqsave(&intc->lock, flags); |
| idx = meson_gpio_parent_irq_request(irqd, type); |
| spin_unlock_irqrestore(&intc->lock, flags); |
| if (idx < 0) { |
| meson_gpio_irq_shutdown(irqd); |
| return -EINVAL; |
| } |
| |
| meson_gpio_irq_regs_config(irqd, idx, type); |
| |
| if (nr_parent_irq > 0) |
| type = IRQ_TYPE_EDGE_RISING; |
| } |
| |
| return 0; |
| } |
| |
| static struct irq_chip meson_gpio_intc_chip = { |
| .name = "meson-gpio-irqchip", |
| .irq_enable = meson_gpio_irq_enable, |
| .irq_disable = meson_gpio_irq_disable, |
| .irq_set_type = meson_gpio_irq_type, |
| .irq_mask = noop, |
| .irq_unmask = noop, |
| .irq_shutdown = meson_gpio_irq_shutdown, |
| }; |
| |
| static int meson_gpio_intc_domain_map(struct irq_domain *d, unsigned int virq, |
| irq_hw_number_t hwirq) |
| { |
| struct meson_gpio_intc *intc = d->host_data; |
| |
| irq_set_chip_and_handler(virq, |
| &meson_gpio_intc_chip, handle_simple_irq); |
| irq_set_chip_data(virq, intc); |
| |
| return 0; |
| } |
| |
| static const struct irq_domain_ops meson_gpio_intc_domain_ops = { |
| .map = meson_gpio_intc_domain_map, |
| .xlate = irq_domain_xlate_twocell, |
| }; |
| |
| static void meson_gpio_irq_handler(struct irq_desc *desc) |
| { |
| struct irq_chip *chip = irq_desc_get_chip(desc); |
| struct irq_data *parent_data = irq_desc_get_irq_data(desc); |
| struct meson_gpio_intc *intc = irq_desc_get_handler_data(desc); |
| int idx; |
| |
| chained_irq_enter(chip, desc); |
| |
| idx = meson_gpio_irq_find_by_parent_virq(parent_data); |
| if (idx >= 0 && intc->parent_irqs[idx].owner != NO_IRQ) |
| generic_handle_irq(irq_find_mapping(intc->irqdomain, |
| intc->parent_irqs[idx].owner)); |
| |
| chained_irq_exit(chip, desc); |
| } |
| |
| static int __init meson_gpio_intc_init(struct device_node *node, |
| struct device_node *parent) |
| { |
| struct meson_gpio_intc *intc; |
| const struct of_device_id *match; |
| struct irq_fwspec fwspec; |
| int parent_hwirq; |
| int parent_virq; |
| int ret; |
| int idx; |
| |
| intc = kzalloc(sizeof(struct meson_gpio_intc), GFP_KERNEL); |
| if (!intc) |
| return -ENOMEM; |
| |
| match = of_match_node(meson_gpio_irq_matches, node); |
| if (!match) { |
| ret = -ENODEV; |
| pr_err("%s: fail to match device node\n", DRIVER_NAME); |
| goto alloc_err1; |
| } |
| intc->data = match->data; |
| |
| ret = of_property_count_elems_of_size(node, |
| "amlogic,channel-interrupts", sizeof(u32)); |
| if (ret <= 0) { |
| pr_err("%s: fail to get the number of elements\n", DRIVER_NAME); |
| ret = -EINVAL; |
| goto alloc_err1; |
| } |
| intc->nr_gicirq = ret; |
| |
| intc->parent_irqs = kcalloc(intc->nr_gicirq, |
| sizeof(struct gpio_parent_irq), GFP_KERNEL); |
| if (!intc->parent_irqs) { |
| ret = -ENOMEM; |
| goto alloc_err1; |
| } |
| |
| spin_lock_init(&intc->lock); |
| |
| intc->base = of_iomap(node, 0); |
| if (IS_ERR_OR_NULL(intc->base)) { |
| ret = -ENOMEM; |
| goto alloc_err2; |
| } |
| |
| intc->irqdomain = irq_domain_add_linear(node, intc->data->nr_hwirq, |
| &meson_gpio_intc_domain_ops, intc); |
| if (IS_ERR_OR_NULL(intc->irqdomain)) { |
| ret = -ENOMEM; |
| goto iomap_err; |
| } |
| |
| for (idx = 0; idx < intc->nr_gicirq; idx++) { |
| ret = of_property_read_u32_index(node, |
| "amlogic,channel-interrupts", idx, |
| &parent_hwirq); |
| if (ret < 0) { |
| pr_err("%s: fail to read property value\n", |
| DRIVER_NAME); |
| goto iomap_err; |
| } |
| |
| fwspec.fwnode = of_node_to_fwnode(parent); |
| fwspec.param_count = 3; |
| fwspec.param[0] = 0; |
| fwspec.param[1] = parent_hwirq; |
| fwspec.param[2] = IRQ_TYPE_EDGE_RISING; |
| |
| parent_virq = irq_create_fwspec_mapping(&fwspec); |
| |
| intc->parent_irqs[idx].virq = parent_virq; |
| intc->parent_irqs[idx].owner = NO_IRQ; |
| irq_set_handler_data(parent_virq, intc); |
| irq_set_chained_handler(parent_virq, meson_gpio_irq_handler); |
| |
| /*disable the interrupt line of gpio in GIC controller*/ |
| meson_gpio_parent_irq_mask(parent_virq); |
| } |
| |
| pr_info("%s: support to detect double-edge trigger signal\n", |
| DRIVER_NAME); |
| |
| return 0; |
| |
| iomap_err: |
| iounmap(intc->base); |
| |
| alloc_err2: |
| kfree(intc->parent_irqs); |
| |
| alloc_err1: |
| kfree(intc); |
| |
| return ret; |
| } |
| |
| /* |
| * if you want to use the Meson GPIO IRQ which support the |
| * double-edge detection, please set the compatible property |
| * to "amlogic,meson-gpio-intc-ext" in dts |
| */ |
| IRQCHIP_DECLARE(meson_gpio, "amlogic,meson-gpio-intc-ext", |
| meson_gpio_intc_init); |