]>
Commit | Line | Data |
---|---|---|
f80cb488 NF |
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> | |
9f7195da | 22 | #include <linux/clk/at91_pmc.h> |
f80cb488 NF |
23 | #include <linux/io.h> |
24 | #include <linux/module.h> | |
25 | #include <linux/of.h> | |
0b040874 | 26 | #include <linux/of_address.h> |
f80cb488 NF |
27 | #include <linux/platform_device.h> |
28 | #include <linux/printk.h> | |
29 | ||
0b040874 AB |
30 | #include <soc/at91/at91sam9_ddrsdr.h> |
31 | ||
f80cb488 NF |
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(31, 16) | |
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) | |
5c6c513d | 60 | #define SHDW_RTTWK(reg, cfg) (((reg) >> ((cfg)->sr_rttwk_shift)) & 0x1) |
f80cb488 | 61 | #define SHDW_RTCWKEN(cfg) (1 << ((cfg)->mr_rtcwk_shift)) |
5c6c513d | 62 | #define SHDW_RTTWKEN(cfg) (1 << ((cfg)->mr_rttwk_shift)) |
f80cb488 NF |
63 | |
64 | #define DBC_PERIOD_US(x) DIV_ROUND_UP_ULL((1000000 * (x)), \ | |
65 | SLOW_CLOCK_FREQ) | |
66 | ||
5c6c513d CB |
67 | #define SHDW_CFG_NOT_USED (32) |
68 | ||
a4f06df1 | 69 | struct shdwc_reg_config { |
f80cb488 NF |
70 | u8 wkup_pin_input; |
71 | u8 mr_rtcwk_shift; | |
5c6c513d | 72 | u8 mr_rttwk_shift; |
f80cb488 | 73 | u8 sr_rtcwk_shift; |
5c6c513d | 74 | u8 sr_rttwk_shift; |
f80cb488 NF |
75 | }; |
76 | ||
d39284f2 CB |
77 | struct pmc_reg_config { |
78 | u8 mckr; | |
79 | }; | |
80 | ||
a4f06df1 CB |
81 | struct reg_config { |
82 | struct shdwc_reg_config shdwc; | |
d39284f2 | 83 | struct pmc_reg_config pmc; |
a4f06df1 CB |
84 | }; |
85 | ||
f80cb488 | 86 | struct shdwc { |
a4f06df1 | 87 | const struct reg_config *rcfg; |
6764aca1 | 88 | struct clk *sclk; |
d12f8490 | 89 | void __iomem *shdwc_base; |
9be74f0d | 90 | void __iomem *mpddrc_base; |
9f7195da | 91 | void __iomem *pmc_base; |
f80cb488 NF |
92 | }; |
93 | ||
94 | /* | |
95 | * Hold configuration here, cannot be more than one instance of the driver | |
96 | * since pm_power_off itself is global. | |
97 | */ | |
98 | static struct shdwc *at91_shdwc; | |
f80cb488 NF |
99 | |
100 | static const unsigned long long sdwc_dbc_period[] = { | |
101 | 0, 3, 32, 512, 4096, 32768, | |
102 | }; | |
103 | ||
104 | static void __init at91_wakeup_status(struct platform_device *pdev) | |
105 | { | |
106 | struct shdwc *shdw = platform_get_drvdata(pdev); | |
a4f06df1 | 107 | const struct reg_config *rcfg = shdw->rcfg; |
f80cb488 NF |
108 | u32 reg; |
109 | char *reason = "unknown"; | |
110 | ||
d12f8490 | 111 | reg = readl(shdw->shdwc_base + AT91_SHDW_SR); |
f80cb488 NF |
112 | |
113 | dev_dbg(&pdev->dev, "%s: status = %#x\n", __func__, reg); | |
114 | ||
115 | /* Simple power-on, just bail out */ | |
116 | if (!reg) | |
117 | return; | |
118 | ||
a4f06df1 | 119 | if (SHDW_WK_PIN(reg, &rcfg->shdwc)) |
f80cb488 | 120 | reason = "WKUP pin"; |
a4f06df1 | 121 | else if (SHDW_RTCWK(reg, &rcfg->shdwc)) |
f80cb488 | 122 | reason = "RTC"; |
a4f06df1 | 123 | else if (SHDW_RTTWK(reg, &rcfg->shdwc)) |
5c6c513d | 124 | reason = "RTT"; |
f80cb488 NF |
125 | |
126 | pr_info("AT91: Wake-Up source: %s\n", reason); | |
127 | } | |
128 | ||
129 | static void at91_poweroff(void) | |
0b040874 AB |
130 | { |
131 | asm volatile( | |
132 | /* Align to cache lines */ | |
133 | ".balign 32\n\t" | |
134 | ||
135 | /* Ensure AT91_SHDW_CR is in the TLB by reading it */ | |
136 | " ldr r6, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" | |
137 | ||
138 | /* Power down SDRAM0 */ | |
4e018c1e CB |
139 | " tst %0, #0\n\t" |
140 | " beq 1f\n\t" | |
0b040874 | 141 | " str %1, [%0, #" __stringify(AT91_DDRSDRC_LPR) "]\n\t" |
9f7195da CB |
142 | |
143 | /* Switch the master clock source to slow clock. */ | |
d39284f2 | 144 | "1: ldr r6, [%4, %5]\n\t" |
9f7195da | 145 | " bic r6, r6, #" __stringify(AT91_PMC_CSS) "\n\t" |
d39284f2 | 146 | " str r6, [%4, %5]\n\t" |
9f7195da | 147 | /* Wait for clock switch. */ |
4e018c1e | 148 | "2: ldr r6, [%4, #" __stringify(AT91_PMC_SR) "]\n\t" |
9f7195da | 149 | " tst r6, #" __stringify(AT91_PMC_MCKRDY) "\n\t" |
4e018c1e | 150 | " beq 2b\n\t" |
9f7195da | 151 | |
0b040874 AB |
152 | /* Shutdown CPU */ |
153 | " str %3, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" | |
154 | ||
155 | " b .\n\t" | |
156 | : | |
9be74f0d | 157 | : "r" (at91_shdwc->mpddrc_base), |
0b040874 | 158 | "r" cpu_to_le32(AT91_DDRSDRC_LPDDR2_PWOFF), |
d12f8490 | 159 | "r" (at91_shdwc->shdwc_base), |
9f7195da | 160 | "r" cpu_to_le32(AT91_SHDW_KEY | AT91_SHDW_SHDW), |
d39284f2 CB |
161 | "r" (at91_shdwc->pmc_base), |
162 | "r" (at91_shdwc->rcfg->pmc.mckr) | |
be04a0d7 | 163 | : "r6"); |
0b040874 AB |
164 | } |
165 | ||
f80cb488 NF |
166 | static u32 at91_shdwc_debouncer_value(struct platform_device *pdev, |
167 | u32 in_period_us) | |
168 | { | |
169 | int i; | |
170 | int max_idx = ARRAY_SIZE(sdwc_dbc_period) - 1; | |
171 | unsigned long long period_us; | |
172 | unsigned long long max_period_us = DBC_PERIOD_US(sdwc_dbc_period[max_idx]); | |
173 | ||
174 | if (in_period_us > max_period_us) { | |
175 | dev_warn(&pdev->dev, | |
176 | "debouncer period %u too big, reduced to %llu us\n", | |
177 | in_period_us, max_period_us); | |
178 | return max_idx; | |
179 | } | |
180 | ||
181 | for (i = max_idx - 1; i > 0; i--) { | |
182 | period_us = DBC_PERIOD_US(sdwc_dbc_period[i]); | |
183 | dev_dbg(&pdev->dev, "%s: ref[%d] = %llu\n", | |
184 | __func__, i, period_us); | |
185 | if (in_period_us > period_us) | |
186 | break; | |
187 | } | |
188 | ||
189 | return i + 1; | |
190 | } | |
191 | ||
192 | static u32 at91_shdwc_get_wakeup_input(struct platform_device *pdev, | |
193 | struct device_node *np) | |
194 | { | |
195 | struct device_node *cnp; | |
196 | u32 wk_input_mask; | |
197 | u32 wuir = 0; | |
198 | u32 wk_input; | |
199 | ||
200 | for_each_child_of_node(np, cnp) { | |
201 | if (of_property_read_u32(cnp, "reg", &wk_input)) { | |
2f04cd2a RH |
202 | dev_warn(&pdev->dev, "reg property is missing for %pOF\n", |
203 | cnp); | |
f80cb488 NF |
204 | continue; |
205 | } | |
206 | ||
207 | wk_input_mask = 1 << wk_input; | |
208 | if (!(wk_input_mask & AT91_SHDW_WKUPEN_MASK)) { | |
209 | dev_warn(&pdev->dev, | |
210 | "wake-up input %d out of bounds ignore\n", | |
211 | wk_input); | |
212 | continue; | |
213 | } | |
214 | wuir |= wk_input_mask; | |
215 | ||
216 | if (of_property_read_bool(cnp, "atmel,wakeup-active-high")) | |
217 | wuir |= AT91_SHDW_WKUPT(wk_input); | |
218 | ||
219 | dev_dbg(&pdev->dev, "%s: (child %d) wuir = %#x\n", | |
220 | __func__, wk_input, wuir); | |
221 | } | |
222 | ||
223 | return wuir; | |
224 | } | |
225 | ||
226 | static void at91_shdwc_dt_configure(struct platform_device *pdev) | |
227 | { | |
228 | struct shdwc *shdw = platform_get_drvdata(pdev); | |
a4f06df1 | 229 | const struct reg_config *rcfg = shdw->rcfg; |
f80cb488 NF |
230 | struct device_node *np = pdev->dev.of_node; |
231 | u32 mode = 0, tmp, input; | |
232 | ||
233 | if (!np) { | |
234 | dev_err(&pdev->dev, "device node not found\n"); | |
235 | return; | |
236 | } | |
237 | ||
238 | if (!of_property_read_u32(np, "debounce-delay-us", &tmp)) | |
239 | mode |= AT91_SHDW_WKUPDBC(at91_shdwc_debouncer_value(pdev, tmp)); | |
240 | ||
241 | if (of_property_read_bool(np, "atmel,wakeup-rtc-timer")) | |
a4f06df1 | 242 | mode |= SHDW_RTCWKEN(&rcfg->shdwc); |
f80cb488 | 243 | |
5c6c513d | 244 | if (of_property_read_bool(np, "atmel,wakeup-rtt-timer")) |
a4f06df1 | 245 | mode |= SHDW_RTTWKEN(&rcfg->shdwc); |
5c6c513d | 246 | |
f80cb488 | 247 | dev_dbg(&pdev->dev, "%s: mode = %#x\n", __func__, mode); |
d12f8490 | 248 | writel(mode, shdw->shdwc_base + AT91_SHDW_MR); |
f80cb488 NF |
249 | |
250 | input = at91_shdwc_get_wakeup_input(pdev, np); | |
d12f8490 | 251 | writel(input, shdw->shdwc_base + AT91_SHDW_WUIR); |
f80cb488 NF |
252 | } |
253 | ||
a4f06df1 CB |
254 | static const struct reg_config sama5d2_reg_config = { |
255 | .shdwc = { | |
256 | .wkup_pin_input = 0, | |
257 | .mr_rtcwk_shift = 17, | |
258 | .mr_rttwk_shift = SHDW_CFG_NOT_USED, | |
259 | .sr_rtcwk_shift = 5, | |
260 | .sr_rttwk_shift = SHDW_CFG_NOT_USED, | |
261 | }, | |
d39284f2 CB |
262 | .pmc = { |
263 | .mckr = 0x30, | |
264 | }, | |
5c6c513d | 265 | }; |
17d2e876 | 266 | |
a4f06df1 CB |
267 | static const struct reg_config sam9x60_reg_config = { |
268 | .shdwc = { | |
269 | .wkup_pin_input = 0, | |
270 | .mr_rtcwk_shift = 17, | |
271 | .mr_rttwk_shift = 16, | |
272 | .sr_rtcwk_shift = 5, | |
273 | .sr_rttwk_shift = 4, | |
274 | }, | |
d39284f2 CB |
275 | .pmc = { |
276 | .mckr = 0x28, | |
277 | }, | |
f80cb488 NF |
278 | }; |
279 | ||
280 | static const struct of_device_id at91_shdwc_of_match[] = { | |
281 | { | |
282 | .compatible = "atmel,sama5d2-shdwc", | |
a4f06df1 | 283 | .data = &sama5d2_reg_config, |
17d2e876 CB |
284 | }, |
285 | { | |
286 | .compatible = "microchip,sam9x60-shdwc", | |
a4f06df1 | 287 | .data = &sam9x60_reg_config, |
f80cb488 NF |
288 | }, { |
289 | /*sentinel*/ | |
290 | } | |
291 | }; | |
292 | MODULE_DEVICE_TABLE(of, at91_shdwc_of_match); | |
293 | ||
b0ac8596 CB |
294 | static const struct of_device_id at91_pmc_ids[] = { |
295 | { .compatible = "atmel,sama5d2-pmc" }, | |
296 | { .compatible = "microchip,sam9x60-pmc" }, | |
297 | { /* Sentinel. */ } | |
298 | }; | |
299 | ||
f80cb488 NF |
300 | static int __init at91_shdwc_probe(struct platform_device *pdev) |
301 | { | |
302 | struct resource *res; | |
303 | const struct of_device_id *match; | |
0b040874 AB |
304 | struct device_node *np; |
305 | u32 ddr_type; | |
f80cb488 NF |
306 | int ret; |
307 | ||
308 | if (!pdev->dev.of_node) | |
309 | return -ENODEV; | |
310 | ||
9f1e4477 CB |
311 | if (at91_shdwc) |
312 | return -EBUSY; | |
313 | ||
f80cb488 NF |
314 | at91_shdwc = devm_kzalloc(&pdev->dev, sizeof(*at91_shdwc), GFP_KERNEL); |
315 | if (!at91_shdwc) | |
316 | return -ENOMEM; | |
317 | ||
318 | platform_set_drvdata(pdev, at91_shdwc); | |
319 | ||
320 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
d12f8490 CB |
321 | at91_shdwc->shdwc_base = devm_ioremap_resource(&pdev->dev, res); |
322 | if (IS_ERR(at91_shdwc->shdwc_base)) { | |
f80cb488 | 323 | dev_err(&pdev->dev, "Could not map reset controller address\n"); |
d12f8490 | 324 | return PTR_ERR(at91_shdwc->shdwc_base); |
f80cb488 NF |
325 | } |
326 | ||
327 | match = of_match_node(at91_shdwc_of_match, pdev->dev.of_node); | |
a4f06df1 | 328 | at91_shdwc->rcfg = match->data; |
f80cb488 | 329 | |
6764aca1 CB |
330 | at91_shdwc->sclk = devm_clk_get(&pdev->dev, NULL); |
331 | if (IS_ERR(at91_shdwc->sclk)) | |
332 | return PTR_ERR(at91_shdwc->sclk); | |
f80cb488 | 333 | |
6764aca1 | 334 | ret = clk_prepare_enable(at91_shdwc->sclk); |
f80cb488 NF |
335 | if (ret) { |
336 | dev_err(&pdev->dev, "Could not enable slow clock\n"); | |
337 | return ret; | |
338 | } | |
339 | ||
340 | at91_wakeup_status(pdev); | |
341 | ||
342 | at91_shdwc_dt_configure(pdev); | |
343 | ||
b0ac8596 | 344 | np = of_find_matching_node(NULL, at91_pmc_ids); |
9f7195da CB |
345 | if (!np) { |
346 | ret = -ENODEV; | |
347 | goto clk_disable; | |
348 | } | |
349 | ||
350 | at91_shdwc->pmc_base = of_iomap(np, 0); | |
351 | of_node_put(np); | |
352 | ||
353 | if (!at91_shdwc->pmc_base) { | |
354 | ret = -ENOMEM; | |
355 | goto clk_disable; | |
356 | } | |
f80cb488 | 357 | |
0b040874 | 358 | np = of_find_compatible_node(NULL, NULL, "atmel,sama5d3-ddramc"); |
9f7195da CB |
359 | if (!np) { |
360 | ret = -ENODEV; | |
361 | goto unmap; | |
362 | } | |
0b040874 | 363 | |
9be74f0d | 364 | at91_shdwc->mpddrc_base = of_iomap(np, 0); |
0b040874 AB |
365 | of_node_put(np); |
366 | ||
9be74f0d | 367 | if (!at91_shdwc->mpddrc_base) { |
9f7195da CB |
368 | ret = -ENOMEM; |
369 | goto unmap; | |
370 | } | |
371 | ||
372 | pm_power_off = at91_poweroff; | |
0b040874 | 373 | |
9be74f0d CB |
374 | ddr_type = readl(at91_shdwc->mpddrc_base + AT91_DDRSDRC_MDR) & |
375 | AT91_DDRSDRC_MD; | |
4e018c1e CB |
376 | if (ddr_type != AT91_DDRSDRC_MD_LPDDR2 && |
377 | ddr_type != AT91_DDRSDRC_MD_LPDDR3) { | |
9be74f0d CB |
378 | iounmap(at91_shdwc->mpddrc_base); |
379 | at91_shdwc->mpddrc_base = NULL; | |
9f7195da | 380 | } |
0b040874 | 381 | |
f80cb488 | 382 | return 0; |
9f7195da CB |
383 | |
384 | unmap: | |
385 | iounmap(at91_shdwc->pmc_base); | |
386 | clk_disable: | |
6764aca1 | 387 | clk_disable_unprepare(at91_shdwc->sclk); |
9f7195da CB |
388 | |
389 | return ret; | |
f80cb488 NF |
390 | } |
391 | ||
392 | static int __exit at91_shdwc_remove(struct platform_device *pdev) | |
393 | { | |
394 | struct shdwc *shdw = platform_get_drvdata(pdev); | |
395 | ||
4e018c1e | 396 | if (pm_power_off == at91_poweroff) |
f80cb488 NF |
397 | pm_power_off = NULL; |
398 | ||
399 | /* Reset values to disable wake-up features */ | |
d12f8490 CB |
400 | writel(0, shdw->shdwc_base + AT91_SHDW_MR); |
401 | writel(0, shdw->shdwc_base + AT91_SHDW_WUIR); | |
f80cb488 | 402 | |
9be74f0d CB |
403 | if (shdw->mpddrc_base) |
404 | iounmap(shdw->mpddrc_base); | |
9f7195da CB |
405 | iounmap(shdw->pmc_base); |
406 | ||
6764aca1 | 407 | clk_disable_unprepare(shdw->sclk); |
f80cb488 NF |
408 | |
409 | return 0; | |
410 | } | |
411 | ||
412 | static struct platform_driver at91_shdwc_driver = { | |
413 | .remove = __exit_p(at91_shdwc_remove), | |
414 | .driver = { | |
415 | .name = "at91-shdwc", | |
416 | .of_match_table = at91_shdwc_of_match, | |
417 | }, | |
418 | }; | |
419 | module_platform_driver_probe(at91_shdwc_driver, at91_shdwc_probe); | |
420 | ||
421 | MODULE_AUTHOR("Nicolas Ferre <nicolas.ferre@atmel.com>"); | |
422 | MODULE_DESCRIPTION("Atmel shutdown controller driver"); | |
423 | MODULE_LICENSE("GPL v2"); |