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