1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | |
3 | /* |
4 | * Copyright (C) 2021, Linaro Limited. All rights reserved. |
5 | */ |
6 | #include <linux/module.h> |
7 | #include <linux/interrupt.h> |
8 | #include <linux/irqdomain.h> |
9 | #include <linux/err.h> |
10 | #include <linux/platform_device.h> |
11 | #include <linux/of_platform.h> |
12 | #include <linux/slab.h> |
13 | #include <linux/firmware/qcom/qcom_scm.h> |
14 | |
15 | #define LMH_NODE_DCVS 0x44435653 |
16 | #define LMH_CLUSTER0_NODE_ID 0x6370302D |
17 | #define LMH_CLUSTER1_NODE_ID 0x6370312D |
18 | |
19 | #define LMH_SUB_FN_THERMAL 0x54484D4C |
20 | #define LMH_SUB_FN_CRNT 0x43524E54 |
21 | #define LMH_SUB_FN_REL 0x52454C00 |
22 | #define LMH_SUB_FN_BCL 0x42434C00 |
23 | |
24 | #define LMH_ALGO_MODE_ENABLE 0x454E424C |
25 | #define LMH_TH_HI_THRESHOLD 0x48494748 |
26 | #define LMH_TH_LOW_THRESHOLD 0x4C4F5700 |
27 | #define LMH_TH_ARM_THRESHOLD 0x41524D00 |
28 | |
29 | #define LMH_REG_DCVS_INTR_CLR 0x8 |
30 | |
31 | #define LMH_ENABLE_ALGOS 1 |
32 | |
33 | struct lmh_hw_data { |
34 | void __iomem *base; |
35 | struct irq_domain *domain; |
36 | int irq; |
37 | }; |
38 | |
39 | static irqreturn_t lmh_handle_irq(int hw_irq, void *data) |
40 | { |
41 | struct lmh_hw_data *lmh_data = data; |
42 | int irq = irq_find_mapping(domain: lmh_data->domain, hwirq: 0); |
43 | |
44 | /* Call the cpufreq driver to handle the interrupt */ |
45 | if (irq) |
46 | generic_handle_irq(irq); |
47 | |
48 | return IRQ_HANDLED; |
49 | } |
50 | |
51 | static void lmh_enable_interrupt(struct irq_data *d) |
52 | { |
53 | struct lmh_hw_data *lmh_data = irq_data_get_irq_chip_data(d); |
54 | |
55 | /* Clear the existing interrupt */ |
56 | writel(0xff, lmh_data->base + LMH_REG_DCVS_INTR_CLR); |
57 | enable_irq(irq: lmh_data->irq); |
58 | } |
59 | |
60 | static void lmh_disable_interrupt(struct irq_data *d) |
61 | { |
62 | struct lmh_hw_data *lmh_data = irq_data_get_irq_chip_data(d); |
63 | |
64 | disable_irq_nosync(irq: lmh_data->irq); |
65 | } |
66 | |
67 | static struct irq_chip lmh_irq_chip = { |
68 | .name = "lmh" , |
69 | .irq_enable = lmh_enable_interrupt, |
70 | .irq_disable = lmh_disable_interrupt |
71 | }; |
72 | |
73 | static int lmh_irq_map(struct irq_domain *d, unsigned int irq, irq_hw_number_t hw) |
74 | { |
75 | struct lmh_hw_data *lmh_data = d->host_data; |
76 | |
77 | irq_set_chip_and_handler(irq, &lmh_irq_chip, handle_simple_irq); |
78 | irq_set_chip_data(irq, lmh_data); |
79 | |
80 | return 0; |
81 | } |
82 | |
83 | static const struct irq_domain_ops lmh_irq_ops = { |
84 | .map = lmh_irq_map, |
85 | .xlate = irq_domain_xlate_onecell, |
86 | }; |
87 | |
88 | static int lmh_probe(struct platform_device *pdev) |
89 | { |
90 | struct device *dev = &pdev->dev; |
91 | struct device_node *np = dev->of_node; |
92 | struct device_node *cpu_node; |
93 | struct lmh_hw_data *lmh_data; |
94 | int temp_low, temp_high, temp_arm, cpu_id, ret; |
95 | unsigned int enable_alg; |
96 | u32 node_id; |
97 | |
98 | lmh_data = devm_kzalloc(dev, size: sizeof(*lmh_data), GFP_KERNEL); |
99 | if (!lmh_data) |
100 | return -ENOMEM; |
101 | |
102 | lmh_data->base = devm_platform_ioremap_resource(pdev, index: 0); |
103 | if (IS_ERR(ptr: lmh_data->base)) |
104 | return PTR_ERR(ptr: lmh_data->base); |
105 | |
106 | cpu_node = of_parse_phandle(np, phandle_name: "cpus" , index: 0); |
107 | if (!cpu_node) |
108 | return -EINVAL; |
109 | cpu_id = of_cpu_node_to_id(np: cpu_node); |
110 | of_node_put(node: cpu_node); |
111 | |
112 | ret = of_property_read_u32(np, propname: "qcom,lmh-temp-high-millicelsius" , out_value: &temp_high); |
113 | if (ret) { |
114 | dev_err(dev, "missing qcom,lmh-temp-high-millicelsius property\n" ); |
115 | return ret; |
116 | } |
117 | |
118 | ret = of_property_read_u32(np, propname: "qcom,lmh-temp-low-millicelsius" , out_value: &temp_low); |
119 | if (ret) { |
120 | dev_err(dev, "missing qcom,lmh-temp-low-millicelsius property\n" ); |
121 | return ret; |
122 | } |
123 | |
124 | ret = of_property_read_u32(np, propname: "qcom,lmh-temp-arm-millicelsius" , out_value: &temp_arm); |
125 | if (ret) { |
126 | dev_err(dev, "missing qcom,lmh-temp-arm-millicelsius property\n" ); |
127 | return ret; |
128 | } |
129 | |
130 | /* |
131 | * Only sdm845 has lmh hardware currently enabled from hlos. If this is needed |
132 | * for other platforms, revisit this to check if the <cpu-id, node-id> should be part |
133 | * of a dt match table. |
134 | */ |
135 | if (cpu_id == 0) { |
136 | node_id = LMH_CLUSTER0_NODE_ID; |
137 | } else if (cpu_id == 4) { |
138 | node_id = LMH_CLUSTER1_NODE_ID; |
139 | } else { |
140 | dev_err(dev, "Wrong CPU id associated with LMh node\n" ); |
141 | return -EINVAL; |
142 | } |
143 | |
144 | if (!qcom_scm_lmh_dcvsh_available()) |
145 | return -EINVAL; |
146 | |
147 | enable_alg = (uintptr_t)of_device_get_match_data(dev); |
148 | |
149 | if (enable_alg) { |
150 | ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_CRNT, LMH_ALGO_MODE_ENABLE, payload_val: 1, |
151 | LMH_NODE_DCVS, node_id, version: 0); |
152 | if (ret) |
153 | dev_err(dev, "Error %d enabling current subfunction\n" , ret); |
154 | |
155 | ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_REL, LMH_ALGO_MODE_ENABLE, payload_val: 1, |
156 | LMH_NODE_DCVS, node_id, version: 0); |
157 | if (ret) |
158 | dev_err(dev, "Error %d enabling reliability subfunction\n" , ret); |
159 | |
160 | ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_BCL, LMH_ALGO_MODE_ENABLE, payload_val: 1, |
161 | LMH_NODE_DCVS, node_id, version: 0); |
162 | if (ret) |
163 | dev_err(dev, "Error %d enabling BCL subfunction\n" , ret); |
164 | |
165 | ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_THERMAL, LMH_ALGO_MODE_ENABLE, payload_val: 1, |
166 | LMH_NODE_DCVS, node_id, version: 0); |
167 | if (ret) { |
168 | dev_err(dev, "Error %d enabling thermal subfunction\n" , ret); |
169 | return ret; |
170 | } |
171 | |
172 | ret = qcom_scm_lmh_profile_change(profile_id: 0x1); |
173 | if (ret) { |
174 | dev_err(dev, "Error %d changing profile\n" , ret); |
175 | return ret; |
176 | } |
177 | } |
178 | |
179 | /* Set default thermal trips */ |
180 | ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_THERMAL, LMH_TH_ARM_THRESHOLD, payload_val: temp_arm, |
181 | LMH_NODE_DCVS, node_id, version: 0); |
182 | if (ret) { |
183 | dev_err(dev, "Error setting thermal ARM threshold%d\n" , ret); |
184 | return ret; |
185 | } |
186 | |
187 | ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_THERMAL, LMH_TH_HI_THRESHOLD, payload_val: temp_high, |
188 | LMH_NODE_DCVS, node_id, version: 0); |
189 | if (ret) { |
190 | dev_err(dev, "Error setting thermal HI threshold%d\n" , ret); |
191 | return ret; |
192 | } |
193 | |
194 | ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_THERMAL, LMH_TH_LOW_THRESHOLD, payload_val: temp_low, |
195 | LMH_NODE_DCVS, node_id, version: 0); |
196 | if (ret) { |
197 | dev_err(dev, "Error setting thermal ARM threshold%d\n" , ret); |
198 | return ret; |
199 | } |
200 | |
201 | lmh_data->irq = platform_get_irq(pdev, 0); |
202 | lmh_data->domain = irq_domain_add_linear(of_node: np, size: 1, ops: &lmh_irq_ops, host_data: lmh_data); |
203 | if (!lmh_data->domain) { |
204 | dev_err(dev, "Error adding irq_domain\n" ); |
205 | return -EINVAL; |
206 | } |
207 | |
208 | /* Disable the irq and let cpufreq enable it when ready to handle the interrupt */ |
209 | irq_set_status_flags(lmh_data->irq, IRQ_NOAUTOEN); |
210 | ret = devm_request_irq(dev, irq: lmh_data->irq, handler: lmh_handle_irq, |
211 | IRQF_ONESHOT | IRQF_NO_SUSPEND, |
212 | devname: "lmh-irq" , dev_id: lmh_data); |
213 | if (ret) { |
214 | dev_err(dev, "Error %d registering irq %x\n" , ret, lmh_data->irq); |
215 | irq_domain_remove(host: lmh_data->domain); |
216 | return ret; |
217 | } |
218 | |
219 | return 0; |
220 | } |
221 | |
222 | static const struct of_device_id lmh_table[] = { |
223 | { .compatible = "qcom,sc8180x-lmh" , }, |
224 | { .compatible = "qcom,sdm845-lmh" , .data = (void *)LMH_ENABLE_ALGOS}, |
225 | { .compatible = "qcom,sm8150-lmh" , }, |
226 | {} |
227 | }; |
228 | MODULE_DEVICE_TABLE(of, lmh_table); |
229 | |
230 | static struct platform_driver lmh_driver = { |
231 | .probe = lmh_probe, |
232 | .driver = { |
233 | .name = "qcom-lmh" , |
234 | .of_match_table = lmh_table, |
235 | .suppress_bind_attrs = true, |
236 | }, |
237 | }; |
238 | module_platform_driver(lmh_driver); |
239 | |
240 | MODULE_LICENSE("GPL v2" ); |
241 | MODULE_DESCRIPTION("QCOM LMh driver" ); |
242 | |