]>
Commit | Line | Data |
---|---|---|
aae03dc9 OR |
1 | /* |
2 | * Watchdog driver for Alphascale ASM9260. | |
3 | * | |
4 | * Copyright (c) 2014 Oleksij Rempel <linux@rempel-privat.de> | |
5 | * | |
6 | * Licensed under GPLv2 or later. | |
7 | */ | |
8 | ||
9 | #include <linux/bitops.h> | |
10 | #include <linux/clk.h> | |
11 | #include <linux/delay.h> | |
12 | #include <linux/interrupt.h> | |
13 | #include <linux/io.h> | |
14 | #include <linux/module.h> | |
15 | #include <linux/of.h> | |
16 | #include <linux/platform_device.h> | |
aae03dc9 OR |
17 | #include <linux/reset.h> |
18 | #include <linux/watchdog.h> | |
19 | ||
20 | #define CLOCK_FREQ 1000000 | |
21 | ||
22 | /* Watchdog Mode register */ | |
23 | #define HW_WDMOD 0x00 | |
24 | /* Wake interrupt. Set by HW, can't be cleared. */ | |
25 | #define BM_MOD_WDINT BIT(3) | |
26 | /* This bit set if timeout reached. Cleared by SW. */ | |
27 | #define BM_MOD_WDTOF BIT(2) | |
28 | /* HW Reset on timeout */ | |
29 | #define BM_MOD_WDRESET BIT(1) | |
30 | /* WD enable */ | |
31 | #define BM_MOD_WDEN BIT(0) | |
32 | ||
33 | /* | |
34 | * Watchdog Timer Constant register | |
35 | * Minimal value is 0xff, the meaning of this value | |
36 | * depends on used clock: T = WDCLK * (0xff + 1) * 4 | |
37 | */ | |
38 | #define HW_WDTC 0x04 | |
39 | #define BM_WDTC_MAX(freq) (0x7fffffff / (freq)) | |
40 | ||
41 | /* Watchdog Feed register */ | |
42 | #define HW_WDFEED 0x08 | |
43 | ||
44 | /* Watchdog Timer Value register */ | |
45 | #define HW_WDTV 0x0c | |
46 | ||
47 | #define ASM9260_WDT_DEFAULT_TIMEOUT 30 | |
48 | ||
49 | enum asm9260_wdt_mode { | |
50 | HW_RESET, | |
51 | SW_RESET, | |
52 | DEBUG, | |
53 | }; | |
54 | ||
55 | struct asm9260_wdt_priv { | |
56 | struct device *dev; | |
57 | struct watchdog_device wdd; | |
58 | struct clk *clk; | |
59 | struct clk *clk_ahb; | |
60 | struct reset_control *rst; | |
aae03dc9 OR |
61 | |
62 | void __iomem *iobase; | |
63 | int irq; | |
64 | unsigned long wdt_freq; | |
65 | enum asm9260_wdt_mode mode; | |
66 | }; | |
67 | ||
68 | static int asm9260_wdt_feed(struct watchdog_device *wdd) | |
69 | { | |
70 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
71 | ||
72 | iowrite32(0xaa, priv->iobase + HW_WDFEED); | |
73 | iowrite32(0x55, priv->iobase + HW_WDFEED); | |
74 | ||
75 | return 0; | |
76 | } | |
77 | ||
78 | static unsigned int asm9260_wdt_gettimeleft(struct watchdog_device *wdd) | |
79 | { | |
80 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
81 | u32 counter; | |
82 | ||
83 | counter = ioread32(priv->iobase + HW_WDTV); | |
84 | ||
85 | return DIV_ROUND_CLOSEST(counter, priv->wdt_freq); | |
86 | } | |
87 | ||
88 | static int asm9260_wdt_updatetimeout(struct watchdog_device *wdd) | |
89 | { | |
90 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
91 | u32 counter; | |
92 | ||
93 | counter = wdd->timeout * priv->wdt_freq; | |
94 | ||
95 | iowrite32(counter, priv->iobase + HW_WDTC); | |
96 | ||
97 | return 0; | |
98 | } | |
99 | ||
100 | static int asm9260_wdt_enable(struct watchdog_device *wdd) | |
101 | { | |
102 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
103 | u32 mode = 0; | |
104 | ||
105 | if (priv->mode == HW_RESET) | |
106 | mode = BM_MOD_WDRESET; | |
107 | ||
108 | iowrite32(BM_MOD_WDEN | mode, priv->iobase + HW_WDMOD); | |
109 | ||
110 | asm9260_wdt_updatetimeout(wdd); | |
111 | ||
112 | asm9260_wdt_feed(wdd); | |
113 | ||
114 | return 0; | |
115 | } | |
116 | ||
117 | static int asm9260_wdt_disable(struct watchdog_device *wdd) | |
118 | { | |
119 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
120 | ||
121 | /* The only way to disable WD is to reset it. */ | |
122 | reset_control_assert(priv->rst); | |
123 | reset_control_deassert(priv->rst); | |
124 | ||
125 | return 0; | |
126 | } | |
127 | ||
128 | static int asm9260_wdt_settimeout(struct watchdog_device *wdd, unsigned int to) | |
129 | { | |
130 | wdd->timeout = to; | |
131 | asm9260_wdt_updatetimeout(wdd); | |
132 | ||
133 | return 0; | |
134 | } | |
135 | ||
136 | static void asm9260_wdt_sys_reset(struct asm9260_wdt_priv *priv) | |
137 | { | |
138 | /* init WD if it was not started */ | |
139 | ||
140 | iowrite32(BM_MOD_WDEN | BM_MOD_WDRESET, priv->iobase + HW_WDMOD); | |
141 | ||
142 | iowrite32(0xff, priv->iobase + HW_WDTC); | |
143 | /* first pass correct sequence */ | |
144 | asm9260_wdt_feed(&priv->wdd); | |
145 | /* | |
146 | * Then write wrong pattern to the feed to trigger reset | |
147 | * ASAP. | |
148 | */ | |
149 | iowrite32(0xff, priv->iobase + HW_WDFEED); | |
150 | ||
151 | mdelay(1000); | |
152 | } | |
153 | ||
154 | static irqreturn_t asm9260_wdt_irq(int irq, void *devid) | |
155 | { | |
156 | struct asm9260_wdt_priv *priv = devid; | |
157 | u32 stat; | |
158 | ||
159 | stat = ioread32(priv->iobase + HW_WDMOD); | |
160 | if (!(stat & BM_MOD_WDINT)) | |
161 | return IRQ_NONE; | |
162 | ||
163 | if (priv->mode == DEBUG) { | |
164 | dev_info(priv->dev, "Watchdog Timeout. Do nothing.\n"); | |
165 | } else { | |
166 | dev_info(priv->dev, "Watchdog Timeout. Doing SW Reset.\n"); | |
167 | asm9260_wdt_sys_reset(priv); | |
168 | } | |
169 | ||
170 | return IRQ_HANDLED; | |
171 | } | |
172 | ||
55dbe8f3 GR |
173 | static int asm9260_restart(struct watchdog_device *wdd, unsigned long action, |
174 | void *data) | |
aae03dc9 | 175 | { |
55dbe8f3 | 176 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); |
aae03dc9 OR |
177 | |
178 | asm9260_wdt_sys_reset(priv); | |
179 | ||
55dbe8f3 | 180 | return 0; |
aae03dc9 OR |
181 | } |
182 | ||
183 | static const struct watchdog_info asm9260_wdt_ident = { | |
184 | .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | |
185 | | WDIOF_MAGICCLOSE, | |
186 | .identity = "Alphascale asm9260 Watchdog", | |
187 | }; | |
188 | ||
b893e344 | 189 | static const struct watchdog_ops asm9260_wdt_ops = { |
aae03dc9 OR |
190 | .owner = THIS_MODULE, |
191 | .start = asm9260_wdt_enable, | |
192 | .stop = asm9260_wdt_disable, | |
193 | .get_timeleft = asm9260_wdt_gettimeleft, | |
194 | .ping = asm9260_wdt_feed, | |
195 | .set_timeout = asm9260_wdt_settimeout, | |
55dbe8f3 | 196 | .restart = asm9260_restart, |
aae03dc9 OR |
197 | }; |
198 | ||
ac36856f | 199 | static int asm9260_wdt_get_dt_clks(struct asm9260_wdt_priv *priv) |
aae03dc9 OR |
200 | { |
201 | int err; | |
202 | unsigned long clk; | |
203 | ||
204 | priv->clk = devm_clk_get(priv->dev, "mod"); | |
205 | if (IS_ERR(priv->clk)) { | |
206 | dev_err(priv->dev, "Failed to get \"mod\" clk\n"); | |
207 | return PTR_ERR(priv->clk); | |
208 | } | |
209 | ||
210 | /* configure AHB clock */ | |
211 | priv->clk_ahb = devm_clk_get(priv->dev, "ahb"); | |
212 | if (IS_ERR(priv->clk_ahb)) { | |
213 | dev_err(priv->dev, "Failed to get \"ahb\" clk\n"); | |
214 | return PTR_ERR(priv->clk_ahb); | |
215 | } | |
216 | ||
217 | err = clk_prepare_enable(priv->clk_ahb); | |
218 | if (err) { | |
219 | dev_err(priv->dev, "Failed to enable ahb_clk!\n"); | |
220 | return err; | |
221 | } | |
222 | ||
223 | err = clk_set_rate(priv->clk, CLOCK_FREQ); | |
224 | if (err) { | |
225 | clk_disable_unprepare(priv->clk_ahb); | |
226 | dev_err(priv->dev, "Failed to set rate!\n"); | |
227 | return err; | |
228 | } | |
229 | ||
230 | err = clk_prepare_enable(priv->clk); | |
231 | if (err) { | |
232 | clk_disable_unprepare(priv->clk_ahb); | |
233 | dev_err(priv->dev, "Failed to enable clk!\n"); | |
234 | return err; | |
235 | } | |
236 | ||
237 | /* wdt has internal divider */ | |
238 | clk = clk_get_rate(priv->clk); | |
239 | if (!clk) { | |
240 | clk_disable_unprepare(priv->clk); | |
241 | clk_disable_unprepare(priv->clk_ahb); | |
242 | dev_err(priv->dev, "Failed, clk is 0!\n"); | |
243 | return -EINVAL; | |
244 | } | |
245 | ||
246 | priv->wdt_freq = clk / 2; | |
247 | ||
248 | return 0; | |
249 | } | |
250 | ||
ac36856f | 251 | static void asm9260_wdt_get_dt_mode(struct asm9260_wdt_priv *priv) |
aae03dc9 OR |
252 | { |
253 | const char *tmp; | |
254 | int ret; | |
255 | ||
256 | /* default mode */ | |
257 | priv->mode = HW_RESET; | |
258 | ||
259 | ret = of_property_read_string(priv->dev->of_node, | |
260 | "alphascale,mode", &tmp); | |
261 | if (ret < 0) | |
262 | return; | |
263 | ||
264 | if (!strcmp(tmp, "hw")) | |
265 | priv->mode = HW_RESET; | |
266 | else if (!strcmp(tmp, "sw")) | |
267 | priv->mode = SW_RESET; | |
268 | else if (!strcmp(tmp, "debug")) | |
269 | priv->mode = DEBUG; | |
270 | else | |
271 | dev_warn(priv->dev, "unknown reset-type: %s. Using default \"hw\" mode.", | |
272 | tmp); | |
273 | } | |
274 | ||
ac36856f | 275 | static int asm9260_wdt_probe(struct platform_device *pdev) |
aae03dc9 OR |
276 | { |
277 | struct asm9260_wdt_priv *priv; | |
278 | struct watchdog_device *wdd; | |
279 | struct resource *res; | |
280 | int ret; | |
281 | const char * const mode_name[] = { "hw", "sw", "debug", }; | |
282 | ||
283 | priv = devm_kzalloc(&pdev->dev, sizeof(struct asm9260_wdt_priv), | |
284 | GFP_KERNEL); | |
285 | if (!priv) | |
286 | return -ENOMEM; | |
287 | ||
288 | priv->dev = &pdev->dev; | |
289 | ||
290 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
291 | priv->iobase = devm_ioremap_resource(&pdev->dev, res); | |
292 | if (IS_ERR(priv->iobase)) | |
293 | return PTR_ERR(priv->iobase); | |
294 | ||
295 | ret = asm9260_wdt_get_dt_clks(priv); | |
296 | if (ret) | |
297 | return ret; | |
298 | ||
299 | priv->rst = devm_reset_control_get(&pdev->dev, "wdt_rst"); | |
300 | if (IS_ERR(priv->rst)) | |
301 | return PTR_ERR(priv->rst); | |
302 | ||
303 | wdd = &priv->wdd; | |
304 | wdd->info = &asm9260_wdt_ident; | |
305 | wdd->ops = &asm9260_wdt_ops; | |
306 | wdd->min_timeout = 1; | |
307 | wdd->max_timeout = BM_WDTC_MAX(priv->wdt_freq); | |
308 | wdd->parent = &pdev->dev; | |
309 | ||
310 | watchdog_set_drvdata(wdd, priv); | |
311 | ||
312 | /* | |
313 | * If 'timeout-sec' unspecified in devicetree, assume a 30 second | |
314 | * default, unless the max timeout is less than 30 seconds, then use | |
315 | * the max instead. | |
316 | */ | |
317 | wdd->timeout = ASM9260_WDT_DEFAULT_TIMEOUT; | |
318 | watchdog_init_timeout(wdd, 0, &pdev->dev); | |
319 | ||
320 | asm9260_wdt_get_dt_mode(priv); | |
321 | ||
322 | if (priv->mode != HW_RESET) | |
323 | priv->irq = platform_get_irq(pdev, 0); | |
324 | ||
325 | if (priv->irq > 0) { | |
326 | /* | |
327 | * Not all supported platforms specify an interrupt for the | |
328 | * watchdog, so let's make it optional. | |
329 | */ | |
330 | ret = devm_request_irq(&pdev->dev, priv->irq, | |
331 | asm9260_wdt_irq, 0, pdev->name, priv); | |
332 | if (ret < 0) | |
333 | dev_warn(&pdev->dev, "failed to request IRQ\n"); | |
334 | } | |
335 | ||
55dbe8f3 GR |
336 | watchdog_set_restart_priority(wdd, 128); |
337 | ||
aae03dc9 OR |
338 | ret = watchdog_register_device(wdd); |
339 | if (ret) | |
340 | goto clk_off; | |
341 | ||
342 | platform_set_drvdata(pdev, priv); | |
343 | ||
aae03dc9 OR |
344 | dev_info(&pdev->dev, "Watchdog enabled (timeout: %d sec, mode: %s)\n", |
345 | wdd->timeout, mode_name[priv->mode]); | |
346 | return 0; | |
347 | ||
348 | clk_off: | |
349 | clk_disable_unprepare(priv->clk); | |
350 | clk_disable_unprepare(priv->clk_ahb); | |
351 | return ret; | |
352 | } | |
353 | ||
354 | static void asm9260_wdt_shutdown(struct platform_device *pdev) | |
355 | { | |
356 | struct asm9260_wdt_priv *priv = platform_get_drvdata(pdev); | |
357 | ||
358 | asm9260_wdt_disable(&priv->wdd); | |
359 | } | |
360 | ||
ac36856f | 361 | static int asm9260_wdt_remove(struct platform_device *pdev) |
aae03dc9 OR |
362 | { |
363 | struct asm9260_wdt_priv *priv = platform_get_drvdata(pdev); | |
364 | ||
365 | asm9260_wdt_disable(&priv->wdd); | |
366 | ||
aae03dc9 OR |
367 | watchdog_unregister_device(&priv->wdd); |
368 | ||
369 | clk_disable_unprepare(priv->clk); | |
370 | clk_disable_unprepare(priv->clk_ahb); | |
371 | ||
372 | return 0; | |
373 | } | |
374 | ||
375 | static const struct of_device_id asm9260_wdt_of_match[] = { | |
376 | { .compatible = "alphascale,asm9260-wdt"}, | |
377 | {}, | |
378 | }; | |
379 | MODULE_DEVICE_TABLE(of, asm9260_wdt_of_match); | |
380 | ||
381 | static struct platform_driver asm9260_wdt_driver = { | |
382 | .driver = { | |
383 | .name = "asm9260-wdt", | |
aae03dc9 OR |
384 | .of_match_table = asm9260_wdt_of_match, |
385 | }, | |
386 | .probe = asm9260_wdt_probe, | |
387 | .remove = asm9260_wdt_remove, | |
388 | .shutdown = asm9260_wdt_shutdown, | |
389 | }; | |
390 | module_platform_driver(asm9260_wdt_driver); | |
391 | ||
392 | MODULE_DESCRIPTION("asm9260 WatchDog Timer Driver"); | |
393 | MODULE_AUTHOR("Oleksij Rempel <linux@rempel-privat.de>"); | |
394 | MODULE_LICENSE("GPL"); |