]>
Commit | Line | Data |
---|---|---|
97fb5e8d | 1 | // SPDX-License-Identifier: GPL-2.0-only |
7ce75bb2 LI |
2 | /* |
3 | * Copyright (c) 2011-2014, The Linux Foundation. All rights reserved. | |
4 | * Copyright (c) 2014,2015, Linaro Ltd. | |
5 | * | |
3b904b04 | 6 | * SAW power controller driver |
7ce75bb2 LI |
7 | */ |
8 | ||
7ce75bb2 LI |
9 | #include <linux/kernel.h> |
10 | #include <linux/init.h> | |
11 | #include <linux/io.h> | |
12 | #include <linux/slab.h> | |
13 | #include <linux/of.h> | |
14 | #include <linux/of_address.h> | |
15 | #include <linux/of_device.h> | |
16 | #include <linux/err.h> | |
17 | #include <linux/platform_device.h> | |
18 | #include <linux/cpuidle.h> | |
19 | #include <linux/cpu_pm.h> | |
20 | #include <linux/qcom_scm.h> | |
21 | ||
7ce75bb2 LI |
22 | #include <asm/proc-fns.h> |
23 | #include <asm/suspend.h> | |
24 | ||
a871be6b SG |
25 | #include "dt_idle_states.h" |
26 | ||
7ce75bb2 LI |
27 | #define MAX_PMIC_DATA 2 |
28 | #define MAX_SEQ_DATA 64 | |
29 | #define SPM_CTL_INDEX 0x7f | |
30 | #define SPM_CTL_INDEX_SHIFT 4 | |
31 | #define SPM_CTL_EN BIT(0) | |
32 | ||
33 | enum pm_sleep_mode { | |
34 | PM_SLEEP_MODE_STBY, | |
35 | PM_SLEEP_MODE_RET, | |
36 | PM_SLEEP_MODE_SPC, | |
37 | PM_SLEEP_MODE_PC, | |
38 | PM_SLEEP_MODE_NR, | |
39 | }; | |
40 | ||
41 | enum spm_reg { | |
42 | SPM_REG_CFG, | |
43 | SPM_REG_SPM_CTL, | |
44 | SPM_REG_DLY, | |
45 | SPM_REG_PMIC_DLY, | |
46 | SPM_REG_PMIC_DATA_0, | |
47 | SPM_REG_PMIC_DATA_1, | |
48 | SPM_REG_VCTL, | |
49 | SPM_REG_SEQ_ENTRY, | |
50 | SPM_REG_SPM_STS, | |
51 | SPM_REG_PMIC_STS, | |
52 | SPM_REG_NR, | |
53 | }; | |
54 | ||
55 | struct spm_reg_data { | |
56 | const u8 *reg_offset; | |
57 | u32 spm_cfg; | |
58 | u32 spm_dly; | |
59 | u32 pmic_dly; | |
60 | u32 pmic_data[MAX_PMIC_DATA]; | |
61 | u8 seq[MAX_SEQ_DATA]; | |
62 | u8 start_index[PM_SLEEP_MODE_NR]; | |
63 | }; | |
64 | ||
65 | struct spm_driver_data { | |
a871be6b | 66 | struct cpuidle_driver cpuidle_driver; |
7ce75bb2 LI |
67 | void __iomem *reg_base; |
68 | const struct spm_reg_data *reg_data; | |
69 | }; | |
70 | ||
71 | static const u8 spm_reg_offset_v2_1[SPM_REG_NR] = { | |
72 | [SPM_REG_CFG] = 0x08, | |
73 | [SPM_REG_SPM_CTL] = 0x30, | |
74 | [SPM_REG_DLY] = 0x34, | |
75 | [SPM_REG_SEQ_ENTRY] = 0x80, | |
76 | }; | |
77 | ||
78 | /* SPM register data for 8974, 8084 */ | |
79 | static const struct spm_reg_data spm_reg_8974_8084_cpu = { | |
80 | .reg_offset = spm_reg_offset_v2_1, | |
81 | .spm_cfg = 0x1, | |
82 | .spm_dly = 0x3C102800, | |
83 | .seq = { 0x03, 0x0B, 0x0F, 0x00, 0x20, 0x80, 0x10, 0xE8, 0x5B, 0x03, | |
84 | 0x3B, 0xE8, 0x5B, 0x82, 0x10, 0x0B, 0x30, 0x06, 0x26, 0x30, | |
85 | 0x0F }, | |
86 | .start_index[PM_SLEEP_MODE_STBY] = 0, | |
87 | .start_index[PM_SLEEP_MODE_SPC] = 3, | |
88 | }; | |
89 | ||
90 | static const u8 spm_reg_offset_v1_1[SPM_REG_NR] = { | |
91 | [SPM_REG_CFG] = 0x08, | |
92 | [SPM_REG_SPM_CTL] = 0x20, | |
93 | [SPM_REG_PMIC_DLY] = 0x24, | |
94 | [SPM_REG_PMIC_DATA_0] = 0x28, | |
95 | [SPM_REG_PMIC_DATA_1] = 0x2C, | |
96 | [SPM_REG_SEQ_ENTRY] = 0x80, | |
97 | }; | |
98 | ||
99 | /* SPM register data for 8064 */ | |
100 | static const struct spm_reg_data spm_reg_8064_cpu = { | |
101 | .reg_offset = spm_reg_offset_v1_1, | |
102 | .spm_cfg = 0x1F, | |
103 | .pmic_dly = 0x02020004, | |
104 | .pmic_data[0] = 0x0084009C, | |
105 | .pmic_data[1] = 0x00A4001C, | |
106 | .seq = { 0x03, 0x0F, 0x00, 0x24, 0x54, 0x10, 0x09, 0x03, 0x01, | |
107 | 0x10, 0x54, 0x30, 0x0C, 0x24, 0x30, 0x0F }, | |
108 | .start_index[PM_SLEEP_MODE_STBY] = 0, | |
109 | .start_index[PM_SLEEP_MODE_SPC] = 2, | |
110 | }; | |
111 | ||
7ce75bb2 LI |
112 | static inline void spm_register_write(struct spm_driver_data *drv, |
113 | enum spm_reg reg, u32 val) | |
114 | { | |
115 | if (drv->reg_data->reg_offset[reg]) | |
116 | writel_relaxed(val, drv->reg_base + | |
117 | drv->reg_data->reg_offset[reg]); | |
118 | } | |
119 | ||
120 | /* Ensure a guaranteed write, before return */ | |
121 | static inline void spm_register_write_sync(struct spm_driver_data *drv, | |
122 | enum spm_reg reg, u32 val) | |
123 | { | |
124 | u32 ret; | |
125 | ||
126 | if (!drv->reg_data->reg_offset[reg]) | |
127 | return; | |
128 | ||
129 | do { | |
130 | writel_relaxed(val, drv->reg_base + | |
131 | drv->reg_data->reg_offset[reg]); | |
132 | ret = readl_relaxed(drv->reg_base + | |
133 | drv->reg_data->reg_offset[reg]); | |
134 | if (ret == val) | |
135 | break; | |
136 | cpu_relax(); | |
137 | } while (1); | |
138 | } | |
139 | ||
140 | static inline u32 spm_register_read(struct spm_driver_data *drv, | |
141 | enum spm_reg reg) | |
142 | { | |
143 | return readl_relaxed(drv->reg_base + drv->reg_data->reg_offset[reg]); | |
144 | } | |
145 | ||
146 | static void spm_set_low_power_mode(struct spm_driver_data *drv, | |
147 | enum pm_sleep_mode mode) | |
148 | { | |
149 | u32 start_index; | |
150 | u32 ctl_val; | |
151 | ||
152 | start_index = drv->reg_data->start_index[mode]; | |
153 | ||
154 | ctl_val = spm_register_read(drv, SPM_REG_SPM_CTL); | |
155 | ctl_val &= ~(SPM_CTL_INDEX << SPM_CTL_INDEX_SHIFT); | |
156 | ctl_val |= start_index << SPM_CTL_INDEX_SHIFT; | |
157 | ctl_val |= SPM_CTL_EN; | |
158 | spm_register_write_sync(drv, SPM_REG_SPM_CTL, ctl_val); | |
159 | } | |
160 | ||
161 | static int qcom_pm_collapse(unsigned long int unused) | |
162 | { | |
163 | qcom_scm_cpu_power_down(QCOM_SCM_CPU_PWR_DOWN_L2_ON); | |
164 | ||
165 | /* | |
166 | * Returns here only if there was a pending interrupt and we did not | |
167 | * power down as a result. | |
168 | */ | |
169 | return -1; | |
170 | } | |
171 | ||
a871be6b | 172 | static int qcom_cpu_spc(struct spm_driver_data *drv) |
7ce75bb2 LI |
173 | { |
174 | int ret; | |
7ce75bb2 LI |
175 | |
176 | spm_set_low_power_mode(drv, PM_SLEEP_MODE_SPC); | |
177 | ret = cpu_suspend(0, qcom_pm_collapse); | |
178 | /* | |
179 | * ARM common code executes WFI without calling into our driver and | |
180 | * if the SPM mode is not reset, then we may accidently power down the | |
181 | * cpu when we intended only to gate the cpu clock. | |
182 | * Ensure the state is set to standby before returning. | |
183 | */ | |
184 | spm_set_low_power_mode(drv, PM_SLEEP_MODE_STBY); | |
185 | ||
186 | return ret; | |
187 | } | |
188 | ||
a871be6b SG |
189 | static int spm_enter_idle_state(struct cpuidle_device *dev, |
190 | struct cpuidle_driver *drv, int idx) | |
7ce75bb2 | 191 | { |
a871be6b SG |
192 | struct spm_driver_data *data = container_of(drv, struct spm_driver_data, |
193 | cpuidle_driver); | |
194 | ||
195 | return CPU_PM_CPU_IDLE_ENTER_PARAM(qcom_cpu_spc, idx, data); | |
7ce75bb2 LI |
196 | } |
197 | ||
a871be6b SG |
198 | static struct cpuidle_driver qcom_spm_idle_driver = { |
199 | .name = "qcom_spm", | |
200 | .owner = THIS_MODULE, | |
201 | .states[0] = { | |
202 | .enter = spm_enter_idle_state, | |
203 | .exit_latency = 1, | |
204 | .target_residency = 1, | |
205 | .power_usage = UINT_MAX, | |
206 | .name = "WFI", | |
207 | .desc = "ARM WFI", | |
208 | } | |
209 | }; | |
210 | ||
211 | static const struct of_device_id qcom_idle_state_match[] = { | |
212 | { .compatible = "qcom,idle-state-spc", .data = spm_enter_idle_state }, | |
7ce75bb2 LI |
213 | { }, |
214 | }; | |
215 | ||
a871be6b | 216 | static int spm_cpuidle_init(struct cpuidle_driver *drv, int cpu) |
7ce75bb2 | 217 | { |
a871be6b | 218 | int ret; |
7ce75bb2 | 219 | |
a871be6b SG |
220 | memcpy(drv, &qcom_spm_idle_driver, sizeof(*drv)); |
221 | drv->cpumask = (struct cpumask *)cpumask_of(cpu); | |
7ce75bb2 | 222 | |
a871be6b SG |
223 | /* Parse idle states from device tree */ |
224 | ret = dt_init_idle_driver(drv, qcom_idle_state_match, 1); | |
225 | if (ret <= 0) | |
226 | return ret ? : -ENODEV; | |
7ce75bb2 | 227 | |
a871be6b SG |
228 | /* We have atleast one power down mode */ |
229 | return qcom_scm_set_warm_boot_addr(cpu_resume_arm, drv->cpumask); | |
7ce75bb2 LI |
230 | } |
231 | ||
7ce75bb2 LI |
232 | static struct spm_driver_data *spm_get_drv(struct platform_device *pdev, |
233 | int *spm_cpu) | |
234 | { | |
235 | struct spm_driver_data *drv = NULL; | |
236 | struct device_node *cpu_node, *saw_node; | |
237 | int cpu; | |
00affcac | 238 | bool found = 0; |
7ce75bb2 LI |
239 | |
240 | for_each_possible_cpu(cpu) { | |
241 | cpu_node = of_cpu_device_node_get(cpu); | |
242 | if (!cpu_node) | |
243 | continue; | |
244 | saw_node = of_parse_phandle(cpu_node, "qcom,saw", 0); | |
245 | found = (saw_node == pdev->dev.of_node); | |
246 | of_node_put(saw_node); | |
247 | of_node_put(cpu_node); | |
248 | if (found) | |
249 | break; | |
250 | } | |
251 | ||
252 | if (found) { | |
253 | drv = devm_kzalloc(&pdev->dev, sizeof(*drv), GFP_KERNEL); | |
254 | if (drv) | |
255 | *spm_cpu = cpu; | |
256 | } | |
257 | ||
258 | return drv; | |
259 | } | |
260 | ||
261 | static const struct of_device_id spm_match_table[] = { | |
262 | { .compatible = "qcom,msm8974-saw2-v2.1-cpu", | |
263 | .data = &spm_reg_8974_8084_cpu }, | |
264 | { .compatible = "qcom,apq8084-saw2-v2.1-cpu", | |
265 | .data = &spm_reg_8974_8084_cpu }, | |
266 | { .compatible = "qcom,apq8064-saw2-v1.1-cpu", | |
267 | .data = &spm_reg_8064_cpu }, | |
268 | { }, | |
269 | }; | |
270 | ||
271 | static int spm_dev_probe(struct platform_device *pdev) | |
272 | { | |
273 | struct spm_driver_data *drv; | |
274 | struct resource *res; | |
275 | const struct of_device_id *match_id; | |
276 | void __iomem *addr; | |
a871be6b SG |
277 | int cpu, ret; |
278 | ||
279 | if (!qcom_scm_is_available()) | |
280 | return -EPROBE_DEFER; | |
7ce75bb2 LI |
281 | |
282 | drv = spm_get_drv(pdev, &cpu); | |
283 | if (!drv) | |
284 | return -EINVAL; | |
a871be6b | 285 | platform_set_drvdata(pdev, drv); |
7ce75bb2 LI |
286 | |
287 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
288 | drv->reg_base = devm_ioremap_resource(&pdev->dev, res); | |
289 | if (IS_ERR(drv->reg_base)) | |
290 | return PTR_ERR(drv->reg_base); | |
291 | ||
292 | match_id = of_match_node(spm_match_table, pdev->dev.of_node); | |
293 | if (!match_id) | |
294 | return -ENODEV; | |
295 | ||
296 | drv->reg_data = match_id->data; | |
297 | ||
a871be6b SG |
298 | ret = spm_cpuidle_init(&drv->cpuidle_driver, cpu); |
299 | if (ret) | |
300 | return ret; | |
301 | ||
7ce75bb2 LI |
302 | /* Write the SPM sequences first.. */ |
303 | addr = drv->reg_base + drv->reg_data->reg_offset[SPM_REG_SEQ_ENTRY]; | |
304 | __iowrite32_copy(addr, drv->reg_data->seq, | |
305 | ARRAY_SIZE(drv->reg_data->seq) / 4); | |
306 | ||
307 | /* | |
308 | * ..and then the control registers. | |
309 | * On some SoC if the control registers are written first and if the | |
310 | * CPU was held in reset, the reset signal could trigger the SPM state | |
311 | * machine, before the sequences are completely written. | |
312 | */ | |
313 | spm_register_write(drv, SPM_REG_CFG, drv->reg_data->spm_cfg); | |
314 | spm_register_write(drv, SPM_REG_DLY, drv->reg_data->spm_dly); | |
315 | spm_register_write(drv, SPM_REG_PMIC_DLY, drv->reg_data->pmic_dly); | |
316 | spm_register_write(drv, SPM_REG_PMIC_DATA_0, | |
317 | drv->reg_data->pmic_data[0]); | |
318 | spm_register_write(drv, SPM_REG_PMIC_DATA_1, | |
319 | drv->reg_data->pmic_data[1]); | |
320 | ||
321 | /* Set up Standby as the default low power mode */ | |
322 | spm_set_low_power_mode(drv, PM_SLEEP_MODE_STBY); | |
323 | ||
a871be6b SG |
324 | return cpuidle_register(&drv->cpuidle_driver, NULL); |
325 | } | |
326 | ||
327 | static int spm_dev_remove(struct platform_device *pdev) | |
328 | { | |
329 | struct spm_driver_data *drv = platform_get_drvdata(pdev); | |
7ce75bb2 | 330 | |
a871be6b | 331 | cpuidle_unregister(&drv->cpuidle_driver); |
7ce75bb2 LI |
332 | return 0; |
333 | } | |
334 | ||
335 | static struct platform_driver spm_driver = { | |
336 | .probe = spm_dev_probe, | |
a871be6b | 337 | .remove = spm_dev_remove, |
7ce75bb2 LI |
338 | .driver = { |
339 | .name = "saw", | |
340 | .of_match_table = spm_match_table, | |
341 | }, | |
342 | }; | |
7ce75bb2 | 343 | |
3b904b04 | 344 | builtin_platform_driver(spm_driver); |