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 | |
95aa21a3 | 40 | #define AT91_SHDW_WKUPDBC_MASK GENMASK(26, 24) |
f80cb488 NF |
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 | ||
b7e15bd0 CB |
81 | struct ddrc_reg_config { |
82 | u32 type_offset; | |
83 | u32 type_mask; | |
84 | }; | |
85 | ||
a4f06df1 CB |
86 | struct reg_config { |
87 | struct shdwc_reg_config shdwc; | |
d39284f2 | 88 | struct pmc_reg_config pmc; |
b7e15bd0 | 89 | struct ddrc_reg_config ddrc; |
a4f06df1 CB |
90 | }; |
91 | ||
f80cb488 | 92 | struct shdwc { |
a4f06df1 | 93 | const struct reg_config *rcfg; |
6764aca1 | 94 | struct clk *sclk; |
d12f8490 | 95 | void __iomem *shdwc_base; |
9be74f0d | 96 | void __iomem *mpddrc_base; |
9f7195da | 97 | void __iomem *pmc_base; |
f80cb488 NF |
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; | |
f80cb488 NF |
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); | |
a4f06df1 | 113 | const struct reg_config *rcfg = shdw->rcfg; |
f80cb488 NF |
114 | u32 reg; |
115 | char *reason = "unknown"; | |
116 | ||
d12f8490 | 117 | reg = readl(shdw->shdwc_base + AT91_SHDW_SR); |
f80cb488 NF |
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 | ||
a4f06df1 | 125 | if (SHDW_WK_PIN(reg, &rcfg->shdwc)) |
f80cb488 | 126 | reason = "WKUP pin"; |
a4f06df1 | 127 | else if (SHDW_RTCWK(reg, &rcfg->shdwc)) |
f80cb488 | 128 | reason = "RTC"; |
a4f06df1 | 129 | else if (SHDW_RTTWK(reg, &rcfg->shdwc)) |
5c6c513d | 130 | reason = "RTT"; |
f80cb488 NF |
131 | |
132 | pr_info("AT91: Wake-Up source: %s\n", reason); | |
133 | } | |
134 | ||
135 | static void at91_poweroff(void) | |
0b040874 AB |
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 */ | |
4e018c1e CB |
145 | " tst %0, #0\n\t" |
146 | " beq 1f\n\t" | |
0b040874 | 147 | " str %1, [%0, #" __stringify(AT91_DDRSDRC_LPR) "]\n\t" |
9f7195da CB |
148 | |
149 | /* Switch the master clock source to slow clock. */ | |
d39284f2 | 150 | "1: ldr r6, [%4, %5]\n\t" |
9f7195da | 151 | " bic r6, r6, #" __stringify(AT91_PMC_CSS) "\n\t" |
d39284f2 | 152 | " str r6, [%4, %5]\n\t" |
9f7195da | 153 | /* Wait for clock switch. */ |
4e018c1e | 154 | "2: ldr r6, [%4, #" __stringify(AT91_PMC_SR) "]\n\t" |
9f7195da | 155 | " tst r6, #" __stringify(AT91_PMC_MCKRDY) "\n\t" |
4e018c1e | 156 | " beq 2b\n\t" |
9f7195da | 157 | |
0b040874 AB |
158 | /* Shutdown CPU */ |
159 | " str %3, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" | |
160 | ||
161 | " b .\n\t" | |
162 | : | |
9be74f0d | 163 | : "r" (at91_shdwc->mpddrc_base), |
0b040874 | 164 | "r" cpu_to_le32(AT91_DDRSDRC_LPDDR2_PWOFF), |
d12f8490 | 165 | "r" (at91_shdwc->shdwc_base), |
9f7195da | 166 | "r" cpu_to_le32(AT91_SHDW_KEY | AT91_SHDW_SHDW), |
d39284f2 CB |
167 | "r" (at91_shdwc->pmc_base), |
168 | "r" (at91_shdwc->rcfg->pmc.mckr) | |
be04a0d7 | 169 | : "r6"); |
0b040874 AB |
170 | } |
171 | ||
f80cb488 NF |
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(cnp, "reg", &wk_input)) { | |
2f04cd2a RH |
208 | dev_warn(&pdev->dev, "reg property is missing for %pOF\n", |
209 | cnp); | |
f80cb488 NF |
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(cnp, "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); | |
a4f06df1 | 235 | const struct reg_config *rcfg = shdw->rcfg; |
f80cb488 NF |
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, "debounce-delay-us", &tmp)) | |
245 | mode |= AT91_SHDW_WKUPDBC(at91_shdwc_debouncer_value(pdev, tmp)); | |
246 | ||
247 | if (of_property_read_bool(np, "atmel,wakeup-rtc-timer")) | |
a4f06df1 | 248 | mode |= SHDW_RTCWKEN(&rcfg->shdwc); |
f80cb488 | 249 | |
5c6c513d | 250 | if (of_property_read_bool(np, "atmel,wakeup-rtt-timer")) |
a4f06df1 | 251 | mode |= SHDW_RTTWKEN(&rcfg->shdwc); |
5c6c513d | 252 | |
f80cb488 | 253 | dev_dbg(&pdev->dev, "%s: mode = %#x\n", __func__, mode); |
d12f8490 | 254 | writel(mode, shdw->shdwc_base + AT91_SHDW_MR); |
f80cb488 NF |
255 | |
256 | input = at91_shdwc_get_wakeup_input(pdev, np); | |
d12f8490 | 257 | writel(input, shdw->shdwc_base + AT91_SHDW_WUIR); |
f80cb488 NF |
258 | } |
259 | ||
a4f06df1 CB |
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 | }, | |
d39284f2 CB |
268 | .pmc = { |
269 | .mckr = 0x30, | |
270 | }, | |
b7e15bd0 CB |
271 | .ddrc = { |
272 | .type_offset = AT91_DDRSDRC_MDR, | |
273 | .type_mask = AT91_DDRSDRC_MD | |
274 | }, | |
5c6c513d | 275 | }; |
17d2e876 | 276 | |
a4f06df1 CB |
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 | }, | |
d39284f2 CB |
285 | .pmc = { |
286 | .mckr = 0x28, | |
287 | }, | |
b7e15bd0 CB |
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 | }, | |
f80cb488 NF |
305 | }; |
306 | ||
307 | static const struct of_device_id at91_shdwc_of_match[] = { | |
308 | { | |
309 | .compatible = "atmel,sama5d2-shdwc", | |
a4f06df1 | 310 | .data = &sama5d2_reg_config, |
17d2e876 CB |
311 | }, |
312 | { | |
313 | .compatible = "microchip,sam9x60-shdwc", | |
a4f06df1 | 314 | .data = &sam9x60_reg_config, |
b7e15bd0 CB |
315 | }, |
316 | { | |
317 | .compatible = "microchip,sama7g5-shdwc", | |
318 | .data = &sama7g5_reg_config, | |
f80cb488 NF |
319 | }, { |
320 | /*sentinel*/ | |
321 | } | |
322 | }; | |
323 | MODULE_DEVICE_TABLE(of, at91_shdwc_of_match); | |
324 | ||
b0ac8596 CB |
325 | static const struct of_device_id at91_pmc_ids[] = { |
326 | { .compatible = "atmel,sama5d2-pmc" }, | |
327 | { .compatible = "microchip,sam9x60-pmc" }, | |
b7e15bd0 | 328 | { .compatible = "microchip,sama7g5-pmc" }, |
b0ac8596 CB |
329 | { /* Sentinel. */ } |
330 | }; | |
331 | ||
f80cb488 NF |
332 | static int __init at91_shdwc_probe(struct platform_device *pdev) |
333 | { | |
334 | struct resource *res; | |
335 | const struct of_device_id *match; | |
0b040874 AB |
336 | struct device_node *np; |
337 | u32 ddr_type; | |
f80cb488 NF |
338 | int ret; |
339 | ||
340 | if (!pdev->dev.of_node) | |
341 | return -ENODEV; | |
342 | ||
9f1e4477 CB |
343 | if (at91_shdwc) |
344 | return -EBUSY; | |
345 | ||
f80cb488 NF |
346 | at91_shdwc = devm_kzalloc(&pdev->dev, sizeof(*at91_shdwc), GFP_KERNEL); |
347 | if (!at91_shdwc) | |
348 | return -ENOMEM; | |
349 | ||
350 | platform_set_drvdata(pdev, at91_shdwc); | |
351 | ||
352 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
d12f8490 CB |
353 | at91_shdwc->shdwc_base = devm_ioremap_resource(&pdev->dev, res); |
354 | if (IS_ERR(at91_shdwc->shdwc_base)) { | |
f80cb488 | 355 | dev_err(&pdev->dev, "Could not map reset controller address\n"); |
d12f8490 | 356 | return PTR_ERR(at91_shdwc->shdwc_base); |
f80cb488 NF |
357 | } |
358 | ||
359 | match = of_match_node(at91_shdwc_of_match, pdev->dev.of_node); | |
a4f06df1 | 360 | at91_shdwc->rcfg = match->data; |
f80cb488 | 361 | |
6764aca1 CB |
362 | at91_shdwc->sclk = devm_clk_get(&pdev->dev, NULL); |
363 | if (IS_ERR(at91_shdwc->sclk)) | |
364 | return PTR_ERR(at91_shdwc->sclk); | |
f80cb488 | 365 | |
6764aca1 | 366 | ret = clk_prepare_enable(at91_shdwc->sclk); |
f80cb488 NF |
367 | if (ret) { |
368 | dev_err(&pdev->dev, "Could not enable slow clock\n"); | |
369 | return ret; | |
370 | } | |
371 | ||
372 | at91_wakeup_status(pdev); | |
373 | ||
374 | at91_shdwc_dt_configure(pdev); | |
375 | ||
b0ac8596 | 376 | np = of_find_matching_node(NULL, at91_pmc_ids); |
9f7195da CB |
377 | if (!np) { |
378 | ret = -ENODEV; | |
379 | goto clk_disable; | |
380 | } | |
381 | ||
382 | at91_shdwc->pmc_base = of_iomap(np, 0); | |
383 | of_node_put(np); | |
384 | ||
385 | if (!at91_shdwc->pmc_base) { | |
386 | ret = -ENOMEM; | |
387 | goto clk_disable; | |
388 | } | |
f80cb488 | 389 | |
b7e15bd0 CB |
390 | if (at91_shdwc->rcfg->ddrc.type_mask) { |
391 | np = of_find_compatible_node(NULL, NULL, | |
392 | "atmel,sama5d3-ddramc"); | |
393 | if (!np) { | |
394 | ret = -ENODEV; | |
395 | goto unmap; | |
396 | } | |
0b040874 | 397 | |
b7e15bd0 CB |
398 | at91_shdwc->mpddrc_base = of_iomap(np, 0); |
399 | of_node_put(np); | |
0b040874 | 400 | |
b7e15bd0 CB |
401 | if (!at91_shdwc->mpddrc_base) { |
402 | ret = -ENOMEM; | |
403 | goto unmap; | |
404 | } | |
405 | ||
406 | ddr_type = readl(at91_shdwc->mpddrc_base + | |
407 | at91_shdwc->rcfg->ddrc.type_offset) & | |
408 | at91_shdwc->rcfg->ddrc.type_mask; | |
409 | if (ddr_type != AT91_DDRSDRC_MD_LPDDR2 && | |
410 | ddr_type != AT91_DDRSDRC_MD_LPDDR3) { | |
411 | iounmap(at91_shdwc->mpddrc_base); | |
412 | at91_shdwc->mpddrc_base = NULL; | |
413 | } | |
9f7195da CB |
414 | } |
415 | ||
416 | pm_power_off = at91_poweroff; | |
0b040874 | 417 | |
f80cb488 | 418 | return 0; |
9f7195da CB |
419 | |
420 | unmap: | |
421 | iounmap(at91_shdwc->pmc_base); | |
422 | clk_disable: | |
6764aca1 | 423 | clk_disable_unprepare(at91_shdwc->sclk); |
9f7195da CB |
424 | |
425 | return ret; | |
f80cb488 NF |
426 | } |
427 | ||
428 | static int __exit at91_shdwc_remove(struct platform_device *pdev) | |
429 | { | |
430 | struct shdwc *shdw = platform_get_drvdata(pdev); | |
431 | ||
4e018c1e | 432 | if (pm_power_off == at91_poweroff) |
f80cb488 NF |
433 | pm_power_off = NULL; |
434 | ||
435 | /* Reset values to disable wake-up features */ | |
d12f8490 CB |
436 | writel(0, shdw->shdwc_base + AT91_SHDW_MR); |
437 | writel(0, shdw->shdwc_base + AT91_SHDW_WUIR); | |
f80cb488 | 438 | |
9be74f0d CB |
439 | if (shdw->mpddrc_base) |
440 | iounmap(shdw->mpddrc_base); | |
9f7195da CB |
441 | iounmap(shdw->pmc_base); |
442 | ||
6764aca1 | 443 | clk_disable_unprepare(shdw->sclk); |
f80cb488 NF |
444 | |
445 | return 0; | |
446 | } | |
447 | ||
448 | static struct platform_driver at91_shdwc_driver = { | |
449 | .remove = __exit_p(at91_shdwc_remove), | |
450 | .driver = { | |
451 | .name = "at91-shdwc", | |
452 | .of_match_table = at91_shdwc_of_match, | |
453 | }, | |
454 | }; | |
455 | module_platform_driver_probe(at91_shdwc_driver, at91_shdwc_probe); | |
456 | ||
457 | MODULE_AUTHOR("Nicolas Ferre <nicolas.ferre@atmel.com>"); | |
458 | MODULE_DESCRIPTION("Atmel shutdown controller driver"); | |
459 | MODULE_LICENSE("GPL v2"); |