]>
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> | |
22 | #include <linux/io.h> | |
23 | #include <linux/module.h> | |
24 | #include <linux/of.h> | |
522e8ee5 | 25 | #include <linux/of_address.h> |
f80cb488 NF |
26 | #include <linux/platform_device.h> |
27 | #include <linux/printk.h> | |
28 | ||
522e8ee5 AB |
29 | #include <soc/at91/at91sam9_ddrsdr.h> |
30 | ||
f80cb488 NF |
31 | #define SLOW_CLOCK_FREQ 32768 |
32 | ||
33 | #define AT91_SHDW_CR 0x00 /* Shut Down Control Register */ | |
34 | #define AT91_SHDW_SHDW BIT(0) /* Shut Down command */ | |
35 | #define AT91_SHDW_KEY (0xa5UL << 24) /* KEY Password */ | |
36 | ||
37 | #define AT91_SHDW_MR 0x04 /* Shut Down Mode Register */ | |
38 | #define AT91_SHDW_WKUPDBC_SHIFT 24 | |
39 | #define AT91_SHDW_WKUPDBC_MASK GENMASK(31, 16) | |
40 | #define AT91_SHDW_WKUPDBC(x) (((x) << AT91_SHDW_WKUPDBC_SHIFT) \ | |
41 | & AT91_SHDW_WKUPDBC_MASK) | |
42 | ||
43 | #define AT91_SHDW_SR 0x08 /* Shut Down Status Register */ | |
44 | #define AT91_SHDW_WKUPIS_SHIFT 16 | |
45 | #define AT91_SHDW_WKUPIS_MASK GENMASK(31, 16) | |
46 | #define AT91_SHDW_WKUPIS(x) ((1 << (x)) << AT91_SHDW_WKUPIS_SHIFT \ | |
47 | & AT91_SHDW_WKUPIS_MASK) | |
48 | ||
49 | #define AT91_SHDW_WUIR 0x0c /* Shutdown Wake-up Inputs Register */ | |
50 | #define AT91_SHDW_WKUPEN_MASK GENMASK(15, 0) | |
51 | #define AT91_SHDW_WKUPEN(x) ((1 << (x)) & AT91_SHDW_WKUPEN_MASK) | |
52 | #define AT91_SHDW_WKUPT_SHIFT 16 | |
53 | #define AT91_SHDW_WKUPT_MASK GENMASK(31, 16) | |
54 | #define AT91_SHDW_WKUPT(x) ((1 << (x)) << AT91_SHDW_WKUPT_SHIFT \ | |
55 | & AT91_SHDW_WKUPT_MASK) | |
56 | ||
57 | #define SHDW_WK_PIN(reg, cfg) ((reg) & AT91_SHDW_WKUPIS((cfg)->wkup_pin_input)) | |
58 | #define SHDW_RTCWK(reg, cfg) (((reg) >> ((cfg)->sr_rtcwk_shift)) & 0x1) | |
59 | #define SHDW_RTCWKEN(cfg) (1 << ((cfg)->mr_rtcwk_shift)) | |
60 | ||
61 | #define DBC_PERIOD_US(x) DIV_ROUND_UP_ULL((1000000 * (x)), \ | |
62 | SLOW_CLOCK_FREQ) | |
63 | ||
64 | struct shdwc_config { | |
65 | u8 wkup_pin_input; | |
66 | u8 mr_rtcwk_shift; | |
67 | u8 sr_rtcwk_shift; | |
68 | }; | |
69 | ||
70 | struct shdwc { | |
71 | struct shdwc_config *cfg; | |
72 | void __iomem *at91_shdwc_base; | |
73 | }; | |
74 | ||
75 | /* | |
76 | * Hold configuration here, cannot be more than one instance of the driver | |
77 | * since pm_power_off itself is global. | |
78 | */ | |
79 | static struct shdwc *at91_shdwc; | |
80 | static struct clk *sclk; | |
522e8ee5 | 81 | static void __iomem *mpddrc_base; |
f80cb488 NF |
82 | |
83 | static const unsigned long long sdwc_dbc_period[] = { | |
84 | 0, 3, 32, 512, 4096, 32768, | |
85 | }; | |
86 | ||
87 | static void __init at91_wakeup_status(struct platform_device *pdev) | |
88 | { | |
89 | struct shdwc *shdw = platform_get_drvdata(pdev); | |
90 | u32 reg; | |
91 | char *reason = "unknown"; | |
92 | ||
93 | reg = readl(shdw->at91_shdwc_base + AT91_SHDW_SR); | |
94 | ||
95 | dev_dbg(&pdev->dev, "%s: status = %#x\n", __func__, reg); | |
96 | ||
97 | /* Simple power-on, just bail out */ | |
98 | if (!reg) | |
99 | return; | |
100 | ||
101 | if (SHDW_WK_PIN(reg, shdw->cfg)) | |
102 | reason = "WKUP pin"; | |
103 | else if (SHDW_RTCWK(reg, shdw->cfg)) | |
104 | reason = "RTC"; | |
105 | ||
106 | pr_info("AT91: Wake-Up source: %s\n", reason); | |
107 | } | |
108 | ||
109 | static void at91_poweroff(void) | |
110 | { | |
111 | writel(AT91_SHDW_KEY | AT91_SHDW_SHDW, | |
112 | at91_shdwc->at91_shdwc_base + AT91_SHDW_CR); | |
113 | } | |
114 | ||
522e8ee5 AB |
115 | static void at91_lpddr_poweroff(void) |
116 | { | |
117 | asm volatile( | |
118 | /* Align to cache lines */ | |
119 | ".balign 32\n\t" | |
120 | ||
121 | /* Ensure AT91_SHDW_CR is in the TLB by reading it */ | |
122 | " ldr r6, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" | |
123 | ||
124 | /* Power down SDRAM0 */ | |
125 | " str %1, [%0, #" __stringify(AT91_DDRSDRC_LPR) "]\n\t" | |
126 | /* Shutdown CPU */ | |
127 | " str %3, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" | |
128 | ||
129 | " b .\n\t" | |
130 | : | |
131 | : "r" (mpddrc_base), | |
132 | "r" cpu_to_le32(AT91_DDRSDRC_LPDDR2_PWOFF), | |
133 | "r" (at91_shdwc->at91_shdwc_base), | |
134 | "r" cpu_to_le32(AT91_SHDW_KEY | AT91_SHDW_SHDW) | |
135 | : "r0"); | |
136 | } | |
137 | ||
f80cb488 NF |
138 | static u32 at91_shdwc_debouncer_value(struct platform_device *pdev, |
139 | u32 in_period_us) | |
140 | { | |
141 | int i; | |
142 | int max_idx = ARRAY_SIZE(sdwc_dbc_period) - 1; | |
143 | unsigned long long period_us; | |
144 | unsigned long long max_period_us = DBC_PERIOD_US(sdwc_dbc_period[max_idx]); | |
145 | ||
146 | if (in_period_us > max_period_us) { | |
147 | dev_warn(&pdev->dev, | |
148 | "debouncer period %u too big, reduced to %llu us\n", | |
149 | in_period_us, max_period_us); | |
150 | return max_idx; | |
151 | } | |
152 | ||
153 | for (i = max_idx - 1; i > 0; i--) { | |
154 | period_us = DBC_PERIOD_US(sdwc_dbc_period[i]); | |
155 | dev_dbg(&pdev->dev, "%s: ref[%d] = %llu\n", | |
156 | __func__, i, period_us); | |
157 | if (in_period_us > period_us) | |
158 | break; | |
159 | } | |
160 | ||
161 | return i + 1; | |
162 | } | |
163 | ||
164 | static u32 at91_shdwc_get_wakeup_input(struct platform_device *pdev, | |
165 | struct device_node *np) | |
166 | { | |
167 | struct device_node *cnp; | |
168 | u32 wk_input_mask; | |
169 | u32 wuir = 0; | |
170 | u32 wk_input; | |
171 | ||
172 | for_each_child_of_node(np, cnp) { | |
173 | if (of_property_read_u32(cnp, "reg", &wk_input)) { | |
174 | dev_warn(&pdev->dev, "reg property is missing for %s\n", | |
175 | cnp->full_name); | |
176 | continue; | |
177 | } | |
178 | ||
179 | wk_input_mask = 1 << wk_input; | |
180 | if (!(wk_input_mask & AT91_SHDW_WKUPEN_MASK)) { | |
181 | dev_warn(&pdev->dev, | |
182 | "wake-up input %d out of bounds ignore\n", | |
183 | wk_input); | |
184 | continue; | |
185 | } | |
186 | wuir |= wk_input_mask; | |
187 | ||
188 | if (of_property_read_bool(cnp, "atmel,wakeup-active-high")) | |
189 | wuir |= AT91_SHDW_WKUPT(wk_input); | |
190 | ||
191 | dev_dbg(&pdev->dev, "%s: (child %d) wuir = %#x\n", | |
192 | __func__, wk_input, wuir); | |
193 | } | |
194 | ||
195 | return wuir; | |
196 | } | |
197 | ||
198 | static void at91_shdwc_dt_configure(struct platform_device *pdev) | |
199 | { | |
200 | struct shdwc *shdw = platform_get_drvdata(pdev); | |
201 | struct device_node *np = pdev->dev.of_node; | |
202 | u32 mode = 0, tmp, input; | |
203 | ||
204 | if (!np) { | |
205 | dev_err(&pdev->dev, "device node not found\n"); | |
206 | return; | |
207 | } | |
208 | ||
209 | if (!of_property_read_u32(np, "debounce-delay-us", &tmp)) | |
210 | mode |= AT91_SHDW_WKUPDBC(at91_shdwc_debouncer_value(pdev, tmp)); | |
211 | ||
212 | if (of_property_read_bool(np, "atmel,wakeup-rtc-timer")) | |
213 | mode |= SHDW_RTCWKEN(shdw->cfg); | |
214 | ||
215 | dev_dbg(&pdev->dev, "%s: mode = %#x\n", __func__, mode); | |
216 | writel(mode, shdw->at91_shdwc_base + AT91_SHDW_MR); | |
217 | ||
218 | input = at91_shdwc_get_wakeup_input(pdev, np); | |
219 | writel(input, shdw->at91_shdwc_base + AT91_SHDW_WUIR); | |
220 | } | |
221 | ||
222 | static const struct shdwc_config sama5d2_shdwc_config = { | |
223 | .wkup_pin_input = 0, | |
224 | .mr_rtcwk_shift = 17, | |
225 | .sr_rtcwk_shift = 5, | |
226 | }; | |
227 | ||
228 | static const struct of_device_id at91_shdwc_of_match[] = { | |
229 | { | |
230 | .compatible = "atmel,sama5d2-shdwc", | |
231 | .data = &sama5d2_shdwc_config, | |
232 | }, { | |
233 | /*sentinel*/ | |
234 | } | |
235 | }; | |
236 | MODULE_DEVICE_TABLE(of, at91_shdwc_of_match); | |
237 | ||
238 | static int __init at91_shdwc_probe(struct platform_device *pdev) | |
239 | { | |
240 | struct resource *res; | |
241 | const struct of_device_id *match; | |
522e8ee5 AB |
242 | struct device_node *np; |
243 | u32 ddr_type; | |
f80cb488 NF |
244 | int ret; |
245 | ||
246 | if (!pdev->dev.of_node) | |
247 | return -ENODEV; | |
248 | ||
249 | at91_shdwc = devm_kzalloc(&pdev->dev, sizeof(*at91_shdwc), GFP_KERNEL); | |
250 | if (!at91_shdwc) | |
251 | return -ENOMEM; | |
252 | ||
253 | platform_set_drvdata(pdev, at91_shdwc); | |
254 | ||
255 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
256 | at91_shdwc->at91_shdwc_base = devm_ioremap_resource(&pdev->dev, res); | |
257 | if (IS_ERR(at91_shdwc->at91_shdwc_base)) { | |
258 | dev_err(&pdev->dev, "Could not map reset controller address\n"); | |
259 | return PTR_ERR(at91_shdwc->at91_shdwc_base); | |
260 | } | |
261 | ||
262 | match = of_match_node(at91_shdwc_of_match, pdev->dev.of_node); | |
263 | at91_shdwc->cfg = (struct shdwc_config *)(match->data); | |
264 | ||
265 | sclk = devm_clk_get(&pdev->dev, NULL); | |
266 | if (IS_ERR(sclk)) | |
267 | return PTR_ERR(sclk); | |
268 | ||
269 | ret = clk_prepare_enable(sclk); | |
270 | if (ret) { | |
271 | dev_err(&pdev->dev, "Could not enable slow clock\n"); | |
272 | return ret; | |
273 | } | |
274 | ||
275 | at91_wakeup_status(pdev); | |
276 | ||
277 | at91_shdwc_dt_configure(pdev); | |
278 | ||
279 | pm_power_off = at91_poweroff; | |
280 | ||
522e8ee5 AB |
281 | np = of_find_compatible_node(NULL, NULL, "atmel,sama5d3-ddramc"); |
282 | if (!np) | |
283 | return 0; | |
284 | ||
285 | mpddrc_base = of_iomap(np, 0); | |
286 | of_node_put(np); | |
287 | ||
288 | if (!mpddrc_base) | |
289 | return 0; | |
290 | ||
291 | ddr_type = readl(mpddrc_base + AT91_DDRSDRC_MDR) & AT91_DDRSDRC_MD; | |
292 | if ((ddr_type == AT91_DDRSDRC_MD_LPDDR2) || | |
293 | (ddr_type == AT91_DDRSDRC_MD_LPDDR3)) | |
294 | pm_power_off = at91_lpddr_poweroff; | |
295 | else | |
296 | iounmap(mpddrc_base); | |
297 | ||
f80cb488 NF |
298 | return 0; |
299 | } | |
300 | ||
301 | static int __exit at91_shdwc_remove(struct platform_device *pdev) | |
302 | { | |
303 | struct shdwc *shdw = platform_get_drvdata(pdev); | |
304 | ||
522e8ee5 AB |
305 | if (pm_power_off == at91_poweroff || |
306 | pm_power_off == at91_lpddr_poweroff) | |
f80cb488 NF |
307 | pm_power_off = NULL; |
308 | ||
309 | /* Reset values to disable wake-up features */ | |
310 | writel(0, shdw->at91_shdwc_base + AT91_SHDW_MR); | |
311 | writel(0, shdw->at91_shdwc_base + AT91_SHDW_WUIR); | |
312 | ||
313 | clk_disable_unprepare(sclk); | |
314 | ||
315 | return 0; | |
316 | } | |
317 | ||
318 | static struct platform_driver at91_shdwc_driver = { | |
319 | .remove = __exit_p(at91_shdwc_remove), | |
320 | .driver = { | |
321 | .name = "at91-shdwc", | |
322 | .of_match_table = at91_shdwc_of_match, | |
323 | }, | |
324 | }; | |
325 | module_platform_driver_probe(at91_shdwc_driver, at91_shdwc_probe); | |
326 | ||
327 | MODULE_AUTHOR("Nicolas Ferre <nicolas.ferre@atmel.com>"); | |
328 | MODULE_DESCRIPTION("Atmel shutdown controller driver"); | |
329 | MODULE_LICENSE("GPL v2"); |