1 | /* |
2 | * Atmel SAMA5D2-Compatible Shutdown Controller (SHDWC) driver. |
3 | * Found on some SoCs as the sama5d2 (obviously). |
4 | * |
5 | * Copyright (C) 2015 Atmel Corporation, |
6 | * Nicolas Ferre <nicolas.ferre@atmel.com> |
7 | * |
8 | * Evolved from driver at91-poweroff.c. |
9 | * |
10 | * This file is licensed under the terms of the GNU General Public |
11 | * License version 2. This program is licensed "as is" without any |
12 | * warranty of any kind, whether express or implied. |
13 | * |
14 | * TODO: |
15 | * - addition to status of other wake-up inputs [1 - 15] |
16 | * - Analog Comparator wake-up alarm |
17 | * - Serial RX wake-up alarm |
18 | * - low power debouncer |
19 | */ |
20 | |
21 | #include <linux/clk.h> |
22 | #include <linux/clk/at91_pmc.h> |
23 | #include <linux/io.h> |
24 | #include <linux/module.h> |
25 | #include <linux/of.h> |
26 | #include <linux/of_address.h> |
27 | #include <linux/platform_device.h> |
28 | #include <linux/printk.h> |
29 | |
30 | #include <soc/at91/at91sam9_ddrsdr.h> |
31 | |
32 | #define SLOW_CLOCK_FREQ 32768 |
33 | |
34 | #define AT91_SHDW_CR 0x00 /* Shut Down Control Register */ |
35 | #define AT91_SHDW_SHDW BIT(0) /* Shut Down command */ |
36 | #define AT91_SHDW_KEY (0xa5UL << 24) /* KEY Password */ |
37 | |
38 | #define AT91_SHDW_MR 0x04 /* Shut Down Mode Register */ |
39 | #define AT91_SHDW_WKUPDBC_SHIFT 24 |
40 | #define AT91_SHDW_WKUPDBC_MASK GENMASK(26, 24) |
41 | #define AT91_SHDW_WKUPDBC(x) (((x) << AT91_SHDW_WKUPDBC_SHIFT) \ |
42 | & AT91_SHDW_WKUPDBC_MASK) |
43 | |
44 | #define AT91_SHDW_SR 0x08 /* Shut Down Status Register */ |
45 | #define AT91_SHDW_WKUPIS_SHIFT 16 |
46 | #define AT91_SHDW_WKUPIS_MASK GENMASK(31, 16) |
47 | #define AT91_SHDW_WKUPIS(x) ((1 << (x)) << AT91_SHDW_WKUPIS_SHIFT \ |
48 | & AT91_SHDW_WKUPIS_MASK) |
49 | |
50 | #define AT91_SHDW_WUIR 0x0c /* Shutdown Wake-up Inputs Register */ |
51 | #define AT91_SHDW_WKUPEN_MASK GENMASK(15, 0) |
52 | #define AT91_SHDW_WKUPEN(x) ((1 << (x)) & AT91_SHDW_WKUPEN_MASK) |
53 | #define AT91_SHDW_WKUPT_SHIFT 16 |
54 | #define AT91_SHDW_WKUPT_MASK GENMASK(31, 16) |
55 | #define AT91_SHDW_WKUPT(x) ((1 << (x)) << AT91_SHDW_WKUPT_SHIFT \ |
56 | & AT91_SHDW_WKUPT_MASK) |
57 | |
58 | #define SHDW_WK_PIN(reg, cfg) ((reg) & AT91_SHDW_WKUPIS((cfg)->wkup_pin_input)) |
59 | #define SHDW_RTCWK(reg, cfg) (((reg) >> ((cfg)->sr_rtcwk_shift)) & 0x1) |
60 | #define SHDW_RTTWK(reg, cfg) (((reg) >> ((cfg)->sr_rttwk_shift)) & 0x1) |
61 | #define SHDW_RTCWKEN(cfg) (1 << ((cfg)->mr_rtcwk_shift)) |
62 | #define SHDW_RTTWKEN(cfg) (1 << ((cfg)->mr_rttwk_shift)) |
63 | |
64 | #define DBC_PERIOD_US(x) DIV_ROUND_UP_ULL((1000000 * (x)), \ |
65 | SLOW_CLOCK_FREQ) |
66 | |
67 | #define SHDW_CFG_NOT_USED (32) |
68 | |
69 | struct shdwc_reg_config { |
70 | u8 wkup_pin_input; |
71 | u8 mr_rtcwk_shift; |
72 | u8 mr_rttwk_shift; |
73 | u8 sr_rtcwk_shift; |
74 | u8 sr_rttwk_shift; |
75 | }; |
76 | |
77 | struct pmc_reg_config { |
78 | u8 mckr; |
79 | }; |
80 | |
81 | struct ddrc_reg_config { |
82 | u32 type_offset; |
83 | u32 type_mask; |
84 | }; |
85 | |
86 | struct reg_config { |
87 | struct shdwc_reg_config shdwc; |
88 | struct pmc_reg_config pmc; |
89 | struct ddrc_reg_config ddrc; |
90 | }; |
91 | |
92 | struct shdwc { |
93 | const struct reg_config *rcfg; |
94 | struct clk *sclk; |
95 | void __iomem *shdwc_base; |
96 | void __iomem *mpddrc_base; |
97 | void __iomem *pmc_base; |
98 | }; |
99 | |
100 | /* |
101 | * Hold configuration here, cannot be more than one instance of the driver |
102 | * since pm_power_off itself is global. |
103 | */ |
104 | static struct shdwc *at91_shdwc; |
105 | |
106 | static const unsigned long long sdwc_dbc_period[] = { |
107 | 0, 3, 32, 512, 4096, 32768, |
108 | }; |
109 | |
110 | static void __init at91_wakeup_status(struct platform_device *pdev) |
111 | { |
112 | struct shdwc *shdw = platform_get_drvdata(pdev); |
113 | const struct reg_config *rcfg = shdw->rcfg; |
114 | u32 reg; |
115 | char *reason = "unknown" ; |
116 | |
117 | reg = readl(addr: shdw->shdwc_base + AT91_SHDW_SR); |
118 | |
119 | dev_dbg(&pdev->dev, "%s: status = %#x\n" , __func__, reg); |
120 | |
121 | /* Simple power-on, just bail out */ |
122 | if (!reg) |
123 | return; |
124 | |
125 | if (SHDW_WK_PIN(reg, &rcfg->shdwc)) |
126 | reason = "WKUP pin" ; |
127 | else if (SHDW_RTCWK(reg, &rcfg->shdwc)) |
128 | reason = "RTC" ; |
129 | else if (SHDW_RTTWK(reg, &rcfg->shdwc)) |
130 | reason = "RTT" ; |
131 | |
132 | pr_info("AT91: Wake-Up source: %s\n" , reason); |
133 | } |
134 | |
135 | static void at91_poweroff(void) |
136 | { |
137 | asm volatile( |
138 | /* Align to cache lines */ |
139 | ".balign 32\n\t" |
140 | |
141 | /* Ensure AT91_SHDW_CR is in the TLB by reading it */ |
142 | " ldr r6, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" |
143 | |
144 | /* Power down SDRAM0 */ |
145 | " tst %0, #0\n\t" |
146 | " beq 1f\n\t" |
147 | " str %1, [%0, #" __stringify(AT91_DDRSDRC_LPR) "]\n\t" |
148 | |
149 | /* Switch the master clock source to slow clock. */ |
150 | "1: ldr r6, [%4, %5]\n\t" |
151 | " bic r6, r6, #" __stringify(AT91_PMC_CSS) "\n\t" |
152 | " str r6, [%4, %5]\n\t" |
153 | /* Wait for clock switch. */ |
154 | "2: ldr r6, [%4, #" __stringify(AT91_PMC_SR) "]\n\t" |
155 | " tst r6, #" __stringify(AT91_PMC_MCKRDY) "\n\t" |
156 | " beq 2b\n\t" |
157 | |
158 | /* Shutdown CPU */ |
159 | " str %3, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" |
160 | |
161 | " b .\n\t" |
162 | : |
163 | : "r" (at91_shdwc->mpddrc_base), |
164 | "r" cpu_to_le32(AT91_DDRSDRC_LPDDR2_PWOFF), |
165 | "r" (at91_shdwc->shdwc_base), |
166 | "r" cpu_to_le32(AT91_SHDW_KEY | AT91_SHDW_SHDW), |
167 | "r" (at91_shdwc->pmc_base), |
168 | "r" (at91_shdwc->rcfg->pmc.mckr) |
169 | : "r6" ); |
170 | } |
171 | |
172 | static u32 at91_shdwc_debouncer_value(struct platform_device *pdev, |
173 | u32 in_period_us) |
174 | { |
175 | int i; |
176 | int max_idx = ARRAY_SIZE(sdwc_dbc_period) - 1; |
177 | unsigned long long period_us; |
178 | unsigned long long max_period_us = DBC_PERIOD_US(sdwc_dbc_period[max_idx]); |
179 | |
180 | if (in_period_us > max_period_us) { |
181 | dev_warn(&pdev->dev, |
182 | "debouncer period %u too big, reduced to %llu us\n" , |
183 | in_period_us, max_period_us); |
184 | return max_idx; |
185 | } |
186 | |
187 | for (i = max_idx - 1; i > 0; i--) { |
188 | period_us = DBC_PERIOD_US(sdwc_dbc_period[i]); |
189 | dev_dbg(&pdev->dev, "%s: ref[%d] = %llu\n" , |
190 | __func__, i, period_us); |
191 | if (in_period_us > period_us) |
192 | break; |
193 | } |
194 | |
195 | return i + 1; |
196 | } |
197 | |
198 | static u32 at91_shdwc_get_wakeup_input(struct platform_device *pdev, |
199 | struct device_node *np) |
200 | { |
201 | struct device_node *cnp; |
202 | u32 wk_input_mask; |
203 | u32 wuir = 0; |
204 | u32 wk_input; |
205 | |
206 | for_each_child_of_node(np, cnp) { |
207 | if (of_property_read_u32(np: cnp, propname: "reg" , out_value: &wk_input)) { |
208 | dev_warn(&pdev->dev, "reg property is missing for %pOF\n" , |
209 | cnp); |
210 | continue; |
211 | } |
212 | |
213 | wk_input_mask = 1 << wk_input; |
214 | if (!(wk_input_mask & AT91_SHDW_WKUPEN_MASK)) { |
215 | dev_warn(&pdev->dev, |
216 | "wake-up input %d out of bounds ignore\n" , |
217 | wk_input); |
218 | continue; |
219 | } |
220 | wuir |= wk_input_mask; |
221 | |
222 | if (of_property_read_bool(np: cnp, propname: "atmel,wakeup-active-high" )) |
223 | wuir |= AT91_SHDW_WKUPT(wk_input); |
224 | |
225 | dev_dbg(&pdev->dev, "%s: (child %d) wuir = %#x\n" , |
226 | __func__, wk_input, wuir); |
227 | } |
228 | |
229 | return wuir; |
230 | } |
231 | |
232 | static void at91_shdwc_dt_configure(struct platform_device *pdev) |
233 | { |
234 | struct shdwc *shdw = platform_get_drvdata(pdev); |
235 | const struct reg_config *rcfg = shdw->rcfg; |
236 | struct device_node *np = pdev->dev.of_node; |
237 | u32 mode = 0, tmp, input; |
238 | |
239 | if (!np) { |
240 | dev_err(&pdev->dev, "device node not found\n" ); |
241 | return; |
242 | } |
243 | |
244 | if (!of_property_read_u32(np, propname: "debounce-delay-us" , out_value: &tmp)) |
245 | mode |= AT91_SHDW_WKUPDBC(at91_shdwc_debouncer_value(pdev, tmp)); |
246 | |
247 | if (of_property_read_bool(np, propname: "atmel,wakeup-rtc-timer" )) |
248 | mode |= SHDW_RTCWKEN(&rcfg->shdwc); |
249 | |
250 | if (of_property_read_bool(np, propname: "atmel,wakeup-rtt-timer" )) |
251 | mode |= SHDW_RTTWKEN(&rcfg->shdwc); |
252 | |
253 | dev_dbg(&pdev->dev, "%s: mode = %#x\n" , __func__, mode); |
254 | writel(val: mode, addr: shdw->shdwc_base + AT91_SHDW_MR); |
255 | |
256 | input = at91_shdwc_get_wakeup_input(pdev, np); |
257 | writel(val: input, addr: shdw->shdwc_base + AT91_SHDW_WUIR); |
258 | } |
259 | |
260 | static const struct reg_config sama5d2_reg_config = { |
261 | .shdwc = { |
262 | .wkup_pin_input = 0, |
263 | .mr_rtcwk_shift = 17, |
264 | .mr_rttwk_shift = SHDW_CFG_NOT_USED, |
265 | .sr_rtcwk_shift = 5, |
266 | .sr_rttwk_shift = SHDW_CFG_NOT_USED, |
267 | }, |
268 | .pmc = { |
269 | .mckr = 0x30, |
270 | }, |
271 | .ddrc = { |
272 | .type_offset = AT91_DDRSDRC_MDR, |
273 | .type_mask = AT91_DDRSDRC_MD |
274 | }, |
275 | }; |
276 | |
277 | static const struct reg_config sam9x60_reg_config = { |
278 | .shdwc = { |
279 | .wkup_pin_input = 0, |
280 | .mr_rtcwk_shift = 17, |
281 | .mr_rttwk_shift = 16, |
282 | .sr_rtcwk_shift = 5, |
283 | .sr_rttwk_shift = 4, |
284 | }, |
285 | .pmc = { |
286 | .mckr = 0x28, |
287 | }, |
288 | .ddrc = { |
289 | .type_offset = AT91_DDRSDRC_MDR, |
290 | .type_mask = AT91_DDRSDRC_MD |
291 | }, |
292 | }; |
293 | |
294 | static const struct reg_config sama7g5_reg_config = { |
295 | .shdwc = { |
296 | .wkup_pin_input = 0, |
297 | .mr_rtcwk_shift = 17, |
298 | .mr_rttwk_shift = 16, |
299 | .sr_rtcwk_shift = 5, |
300 | .sr_rttwk_shift = 4, |
301 | }, |
302 | .pmc = { |
303 | .mckr = 0x28, |
304 | }, |
305 | }; |
306 | |
307 | static const struct of_device_id at91_shdwc_of_match[] = { |
308 | { |
309 | .compatible = "atmel,sama5d2-shdwc" , |
310 | .data = &sama5d2_reg_config, |
311 | }, |
312 | { |
313 | .compatible = "microchip,sam9x60-shdwc" , |
314 | .data = &sam9x60_reg_config, |
315 | }, |
316 | { |
317 | .compatible = "microchip,sama7g5-shdwc" , |
318 | .data = &sama7g5_reg_config, |
319 | }, { |
320 | /*sentinel*/ |
321 | } |
322 | }; |
323 | MODULE_DEVICE_TABLE(of, at91_shdwc_of_match); |
324 | |
325 | static const struct of_device_id at91_pmc_ids[] = { |
326 | { .compatible = "atmel,sama5d2-pmc" }, |
327 | { .compatible = "microchip,sam9x60-pmc" }, |
328 | { .compatible = "microchip,sama7g5-pmc" }, |
329 | { /* Sentinel. */ } |
330 | }; |
331 | |
332 | static int __init at91_shdwc_probe(struct platform_device *pdev) |
333 | { |
334 | const struct of_device_id *match; |
335 | struct device_node *np; |
336 | u32 ddr_type; |
337 | int ret; |
338 | |
339 | if (!pdev->dev.of_node) |
340 | return -ENODEV; |
341 | |
342 | if (at91_shdwc) |
343 | return -EBUSY; |
344 | |
345 | at91_shdwc = devm_kzalloc(dev: &pdev->dev, size: sizeof(*at91_shdwc), GFP_KERNEL); |
346 | if (!at91_shdwc) |
347 | return -ENOMEM; |
348 | |
349 | platform_set_drvdata(pdev, data: at91_shdwc); |
350 | |
351 | at91_shdwc->shdwc_base = devm_platform_ioremap_resource(pdev, index: 0); |
352 | if (IS_ERR(ptr: at91_shdwc->shdwc_base)) |
353 | return PTR_ERR(ptr: at91_shdwc->shdwc_base); |
354 | |
355 | match = of_match_node(matches: at91_shdwc_of_match, node: pdev->dev.of_node); |
356 | at91_shdwc->rcfg = match->data; |
357 | |
358 | at91_shdwc->sclk = devm_clk_get(dev: &pdev->dev, NULL); |
359 | if (IS_ERR(ptr: at91_shdwc->sclk)) |
360 | return PTR_ERR(ptr: at91_shdwc->sclk); |
361 | |
362 | ret = clk_prepare_enable(clk: at91_shdwc->sclk); |
363 | if (ret) { |
364 | dev_err(&pdev->dev, "Could not enable slow clock\n" ); |
365 | return ret; |
366 | } |
367 | |
368 | at91_wakeup_status(pdev); |
369 | |
370 | at91_shdwc_dt_configure(pdev); |
371 | |
372 | np = of_find_matching_node(NULL, matches: at91_pmc_ids); |
373 | if (!np) { |
374 | ret = -ENODEV; |
375 | goto clk_disable; |
376 | } |
377 | |
378 | at91_shdwc->pmc_base = of_iomap(node: np, index: 0); |
379 | of_node_put(node: np); |
380 | |
381 | if (!at91_shdwc->pmc_base) { |
382 | ret = -ENOMEM; |
383 | goto clk_disable; |
384 | } |
385 | |
386 | if (at91_shdwc->rcfg->ddrc.type_mask) { |
387 | np = of_find_compatible_node(NULL, NULL, |
388 | compat: "atmel,sama5d3-ddramc" ); |
389 | if (!np) { |
390 | ret = -ENODEV; |
391 | goto unmap; |
392 | } |
393 | |
394 | at91_shdwc->mpddrc_base = of_iomap(node: np, index: 0); |
395 | of_node_put(node: np); |
396 | |
397 | if (!at91_shdwc->mpddrc_base) { |
398 | ret = -ENOMEM; |
399 | goto unmap; |
400 | } |
401 | |
402 | ddr_type = readl(addr: at91_shdwc->mpddrc_base + |
403 | at91_shdwc->rcfg->ddrc.type_offset) & |
404 | at91_shdwc->rcfg->ddrc.type_mask; |
405 | if (ddr_type != AT91_DDRSDRC_MD_LPDDR2 && |
406 | ddr_type != AT91_DDRSDRC_MD_LPDDR3) { |
407 | iounmap(addr: at91_shdwc->mpddrc_base); |
408 | at91_shdwc->mpddrc_base = NULL; |
409 | } |
410 | } |
411 | |
412 | pm_power_off = at91_poweroff; |
413 | |
414 | return 0; |
415 | |
416 | unmap: |
417 | iounmap(addr: at91_shdwc->pmc_base); |
418 | clk_disable: |
419 | clk_disable_unprepare(clk: at91_shdwc->sclk); |
420 | |
421 | return ret; |
422 | } |
423 | |
424 | static int __exit at91_shdwc_remove(struct platform_device *pdev) |
425 | { |
426 | struct shdwc *shdw = platform_get_drvdata(pdev); |
427 | |
428 | if (pm_power_off == at91_poweroff) |
429 | pm_power_off = NULL; |
430 | |
431 | /* Reset values to disable wake-up features */ |
432 | writel(val: 0, addr: shdw->shdwc_base + AT91_SHDW_MR); |
433 | writel(val: 0, addr: shdw->shdwc_base + AT91_SHDW_WUIR); |
434 | |
435 | if (shdw->mpddrc_base) |
436 | iounmap(addr: shdw->mpddrc_base); |
437 | iounmap(addr: shdw->pmc_base); |
438 | |
439 | clk_disable_unprepare(clk: shdw->sclk); |
440 | |
441 | return 0; |
442 | } |
443 | |
444 | static struct platform_driver at91_shdwc_driver = { |
445 | .remove = __exit_p(at91_shdwc_remove), |
446 | .driver = { |
447 | .name = "at91-shdwc" , |
448 | .of_match_table = at91_shdwc_of_match, |
449 | }, |
450 | }; |
451 | module_platform_driver_probe(at91_shdwc_driver, at91_shdwc_probe); |
452 | |
453 | MODULE_AUTHOR("Nicolas Ferre <nicolas.ferre@atmel.com>" ); |
454 | MODULE_DESCRIPTION("Atmel shutdown controller driver" ); |
455 | MODULE_LICENSE("GPL v2" ); |
456 | |