1 | // SPDX-License-Identifier: (GPL-2.0 OR BSD-3-Clause) |
2 | /* |
3 | * Copyright (c) 2020 MediaTek Inc. |
4 | * Author Mark-PK Tsai <mark-pk.tsai@mediatek.com> |
5 | */ |
6 | #include <linux/interrupt.h> |
7 | #include <linux/io.h> |
8 | #include <linux/irq.h> |
9 | #include <linux/irqchip.h> |
10 | #include <linux/irqdomain.h> |
11 | #include <linux/of.h> |
12 | #include <linux/of_address.h> |
13 | #include <linux/of_irq.h> |
14 | #include <linux/slab.h> |
15 | #include <linux/spinlock.h> |
16 | #include <linux/syscore_ops.h> |
17 | |
18 | #define MST_INTC_MAX_IRQS 64 |
19 | |
20 | #define INTC_MASK 0x0 |
21 | #define INTC_REV_POLARITY 0x10 |
22 | #define INTC_EOI 0x20 |
23 | |
24 | #ifdef CONFIG_PM_SLEEP |
25 | static LIST_HEAD(mst_intc_list); |
26 | #endif |
27 | |
28 | struct mst_intc_chip_data { |
29 | raw_spinlock_t lock; |
30 | unsigned int irq_start, nr_irqs; |
31 | void __iomem *base; |
32 | bool no_eoi; |
33 | #ifdef CONFIG_PM_SLEEP |
34 | struct list_head entry; |
35 | u16 saved_polarity_conf[DIV_ROUND_UP(MST_INTC_MAX_IRQS, 16)]; |
36 | #endif |
37 | }; |
38 | |
39 | static void mst_set_irq(struct irq_data *d, u32 offset) |
40 | { |
41 | irq_hw_number_t hwirq = irqd_to_hwirq(d); |
42 | struct mst_intc_chip_data *cd = irq_data_get_irq_chip_data(d); |
43 | u16 val, mask; |
44 | unsigned long flags; |
45 | |
46 | mask = 1 << (hwirq % 16); |
47 | offset += (hwirq / 16) * 4; |
48 | |
49 | raw_spin_lock_irqsave(&cd->lock, flags); |
50 | val = readw_relaxed(cd->base + offset) | mask; |
51 | writew_relaxed(val, cd->base + offset); |
52 | raw_spin_unlock_irqrestore(&cd->lock, flags); |
53 | } |
54 | |
55 | static void mst_clear_irq(struct irq_data *d, u32 offset) |
56 | { |
57 | irq_hw_number_t hwirq = irqd_to_hwirq(d); |
58 | struct mst_intc_chip_data *cd = irq_data_get_irq_chip_data(d); |
59 | u16 val, mask; |
60 | unsigned long flags; |
61 | |
62 | mask = 1 << (hwirq % 16); |
63 | offset += (hwirq / 16) * 4; |
64 | |
65 | raw_spin_lock_irqsave(&cd->lock, flags); |
66 | val = readw_relaxed(cd->base + offset) & ~mask; |
67 | writew_relaxed(val, cd->base + offset); |
68 | raw_spin_unlock_irqrestore(&cd->lock, flags); |
69 | } |
70 | |
71 | static void mst_intc_mask_irq(struct irq_data *d) |
72 | { |
73 | mst_set_irq(d, INTC_MASK); |
74 | irq_chip_mask_parent(data: d); |
75 | } |
76 | |
77 | static void mst_intc_unmask_irq(struct irq_data *d) |
78 | { |
79 | mst_clear_irq(d, INTC_MASK); |
80 | irq_chip_unmask_parent(data: d); |
81 | } |
82 | |
83 | static void mst_intc_eoi_irq(struct irq_data *d) |
84 | { |
85 | struct mst_intc_chip_data *cd = irq_data_get_irq_chip_data(d); |
86 | |
87 | if (!cd->no_eoi) |
88 | mst_set_irq(d, INTC_EOI); |
89 | |
90 | irq_chip_eoi_parent(data: d); |
91 | } |
92 | |
93 | static int mst_irq_chip_set_type(struct irq_data *data, unsigned int type) |
94 | { |
95 | switch (type) { |
96 | case IRQ_TYPE_LEVEL_LOW: |
97 | case IRQ_TYPE_EDGE_FALLING: |
98 | mst_set_irq(d: data, INTC_REV_POLARITY); |
99 | break; |
100 | case IRQ_TYPE_LEVEL_HIGH: |
101 | case IRQ_TYPE_EDGE_RISING: |
102 | mst_clear_irq(d: data, INTC_REV_POLARITY); |
103 | break; |
104 | default: |
105 | return -EINVAL; |
106 | } |
107 | |
108 | return irq_chip_set_type_parent(data, type: IRQ_TYPE_LEVEL_HIGH); |
109 | } |
110 | |
111 | static struct irq_chip mst_intc_chip = { |
112 | .name = "mst-intc" , |
113 | .irq_mask = mst_intc_mask_irq, |
114 | .irq_unmask = mst_intc_unmask_irq, |
115 | .irq_eoi = mst_intc_eoi_irq, |
116 | .irq_get_irqchip_state = irq_chip_get_parent_state, |
117 | .irq_set_irqchip_state = irq_chip_set_parent_state, |
118 | .irq_set_affinity = irq_chip_set_affinity_parent, |
119 | .irq_set_vcpu_affinity = irq_chip_set_vcpu_affinity_parent, |
120 | .irq_set_type = mst_irq_chip_set_type, |
121 | .irq_retrigger = irq_chip_retrigger_hierarchy, |
122 | .flags = IRQCHIP_SET_TYPE_MASKED | |
123 | IRQCHIP_SKIP_SET_WAKE | |
124 | IRQCHIP_MASK_ON_SUSPEND, |
125 | }; |
126 | |
127 | #ifdef CONFIG_PM_SLEEP |
128 | static void mst_intc_polarity_save(struct mst_intc_chip_data *cd) |
129 | { |
130 | int i; |
131 | void __iomem *addr = cd->base + INTC_REV_POLARITY; |
132 | |
133 | for (i = 0; i < DIV_ROUND_UP(cd->nr_irqs, 16); i++) |
134 | cd->saved_polarity_conf[i] = readw_relaxed(addr + i * 4); |
135 | } |
136 | |
137 | static void mst_intc_polarity_restore(struct mst_intc_chip_data *cd) |
138 | { |
139 | int i; |
140 | void __iomem *addr = cd->base + INTC_REV_POLARITY; |
141 | |
142 | for (i = 0; i < DIV_ROUND_UP(cd->nr_irqs, 16); i++) |
143 | writew_relaxed(cd->saved_polarity_conf[i], addr + i * 4); |
144 | } |
145 | |
146 | static void mst_irq_resume(void) |
147 | { |
148 | struct mst_intc_chip_data *cd; |
149 | |
150 | list_for_each_entry(cd, &mst_intc_list, entry) |
151 | mst_intc_polarity_restore(cd); |
152 | } |
153 | |
154 | static int mst_irq_suspend(void) |
155 | { |
156 | struct mst_intc_chip_data *cd; |
157 | |
158 | list_for_each_entry(cd, &mst_intc_list, entry) |
159 | mst_intc_polarity_save(cd); |
160 | return 0; |
161 | } |
162 | |
163 | static struct syscore_ops mst_irq_syscore_ops = { |
164 | .suspend = mst_irq_suspend, |
165 | .resume = mst_irq_resume, |
166 | }; |
167 | |
168 | static int __init mst_irq_pm_init(void) |
169 | { |
170 | register_syscore_ops(ops: &mst_irq_syscore_ops); |
171 | return 0; |
172 | } |
173 | late_initcall(mst_irq_pm_init); |
174 | #endif |
175 | |
176 | static int mst_intc_domain_translate(struct irq_domain *d, |
177 | struct irq_fwspec *fwspec, |
178 | unsigned long *hwirq, |
179 | unsigned int *type) |
180 | { |
181 | struct mst_intc_chip_data *cd = d->host_data; |
182 | |
183 | if (is_of_node(fwnode: fwspec->fwnode)) { |
184 | if (fwspec->param_count != 3) |
185 | return -EINVAL; |
186 | |
187 | /* No PPI should point to this domain */ |
188 | if (fwspec->param[0] != 0) |
189 | return -EINVAL; |
190 | |
191 | if (fwspec->param[1] >= cd->nr_irqs) |
192 | return -EINVAL; |
193 | |
194 | *hwirq = fwspec->param[1]; |
195 | *type = fwspec->param[2] & IRQ_TYPE_SENSE_MASK; |
196 | return 0; |
197 | } |
198 | |
199 | return -EINVAL; |
200 | } |
201 | |
202 | static int mst_intc_domain_alloc(struct irq_domain *domain, unsigned int virq, |
203 | unsigned int nr_irqs, void *data) |
204 | { |
205 | int i; |
206 | irq_hw_number_t hwirq; |
207 | struct irq_fwspec parent_fwspec, *fwspec = data; |
208 | struct mst_intc_chip_data *cd = domain->host_data; |
209 | |
210 | /* Not GIC compliant */ |
211 | if (fwspec->param_count != 3) |
212 | return -EINVAL; |
213 | |
214 | /* No PPI should point to this domain */ |
215 | if (fwspec->param[0]) |
216 | return -EINVAL; |
217 | |
218 | hwirq = fwspec->param[1]; |
219 | for (i = 0; i < nr_irqs; i++) |
220 | irq_domain_set_hwirq_and_chip(domain, virq: virq + i, hwirq: hwirq + i, |
221 | chip: &mst_intc_chip, |
222 | chip_data: domain->host_data); |
223 | |
224 | parent_fwspec = *fwspec; |
225 | parent_fwspec.fwnode = domain->parent->fwnode; |
226 | parent_fwspec.param[1] = cd->irq_start + hwirq; |
227 | |
228 | /* |
229 | * mst-intc latch the interrupt request if it's edge triggered, |
230 | * so the output signal to parent GIC is always level sensitive. |
231 | * And if the irq signal is active low, configure it to active high |
232 | * to meet GIC SPI spec in mst_irq_chip_set_type via REV_POLARITY bit. |
233 | */ |
234 | parent_fwspec.param[2] = IRQ_TYPE_LEVEL_HIGH; |
235 | |
236 | return irq_domain_alloc_irqs_parent(domain, irq_base: virq, nr_irqs, arg: &parent_fwspec); |
237 | } |
238 | |
239 | static const struct irq_domain_ops mst_intc_domain_ops = { |
240 | .translate = mst_intc_domain_translate, |
241 | .alloc = mst_intc_domain_alloc, |
242 | .free = irq_domain_free_irqs_common, |
243 | }; |
244 | |
245 | static int __init mst_intc_of_init(struct device_node *dn, |
246 | struct device_node *parent) |
247 | { |
248 | struct irq_domain *domain, *domain_parent; |
249 | struct mst_intc_chip_data *cd; |
250 | u32 irq_start, irq_end; |
251 | |
252 | domain_parent = irq_find_host(node: parent); |
253 | if (!domain_parent) { |
254 | pr_err("mst-intc: interrupt-parent not found\n" ); |
255 | return -EINVAL; |
256 | } |
257 | |
258 | if (of_property_read_u32_index(np: dn, propname: "mstar,irqs-map-range" , index: 0, out_value: &irq_start) || |
259 | of_property_read_u32_index(np: dn, propname: "mstar,irqs-map-range" , index: 1, out_value: &irq_end)) |
260 | return -EINVAL; |
261 | |
262 | cd = kzalloc(size: sizeof(*cd), GFP_KERNEL); |
263 | if (!cd) |
264 | return -ENOMEM; |
265 | |
266 | cd->base = of_iomap(node: dn, index: 0); |
267 | if (!cd->base) { |
268 | kfree(objp: cd); |
269 | return -ENOMEM; |
270 | } |
271 | |
272 | cd->no_eoi = of_property_read_bool(np: dn, propname: "mstar,intc-no-eoi" ); |
273 | raw_spin_lock_init(&cd->lock); |
274 | cd->irq_start = irq_start; |
275 | cd->nr_irqs = irq_end - irq_start + 1; |
276 | domain = irq_domain_add_hierarchy(parent: domain_parent, flags: 0, size: cd->nr_irqs, node: dn, |
277 | ops: &mst_intc_domain_ops, host_data: cd); |
278 | if (!domain) { |
279 | iounmap(addr: cd->base); |
280 | kfree(objp: cd); |
281 | return -ENOMEM; |
282 | } |
283 | |
284 | #ifdef CONFIG_PM_SLEEP |
285 | INIT_LIST_HEAD(list: &cd->entry); |
286 | list_add_tail(new: &cd->entry, head: &mst_intc_list); |
287 | #endif |
288 | return 0; |
289 | } |
290 | |
291 | IRQCHIP_DECLARE(mst_intc, "mstar,mst-intc" , mst_intc_of_init); |
292 | |