Commit | Line | Data |
---|---|---|
1da177e4 | 1 | /* |
b1fa888e | 2 | * drivers/watchdog/shwdt.c |
1da177e4 LT |
3 | * |
4 | * Watchdog driver for integrated watchdog in the SuperH processors. | |
5 | * | |
b1fa888e | 6 | * Copyright (C) 2001 - 2010 Paul Mundt <lethal@linux-sh.org> |
1da177e4 LT |
7 | * |
8 | * This program is free software; you can redistribute it and/or modify it | |
9 | * under the terms of the GNU General Public License as published by the | |
10 | * Free Software Foundation; either version 2 of the License, or (at your | |
11 | * option) any later version. | |
12 | * | |
13 | * 14-Dec-2001 Matt Domsch <Matt_Domsch@dell.com> | |
14 | * Added nowayout module option to override CONFIG_WATCHDOG_NOWAYOUT | |
15 | * | |
16 | * 19-Apr-2002 Rob Radez <rob@osinvestor.com> | |
17 | * Added expect close support, made emulated timeout runtime changeable | |
18 | * general cleanups, add some ioctls | |
19 | */ | |
1da177e4 LT |
20 | #include <linux/module.h> |
21 | #include <linux/moduleparam.h> | |
8f5585ec | 22 | #include <linux/platform_device.h> |
1da177e4 LT |
23 | #include <linux/init.h> |
24 | #include <linux/types.h> | |
25 | #include <linux/miscdevice.h> | |
26 | #include <linux/watchdog.h> | |
27 | #include <linux/reboot.h> | |
28 | #include <linux/notifier.h> | |
29 | #include <linux/ioport.h> | |
30 | #include <linux/fs.h> | |
f118420b | 31 | #include <linux/mm.h> |
8f5585ec | 32 | #include <linux/slab.h> |
70b814ec AC |
33 | #include <linux/io.h> |
34 | #include <linux/uaccess.h> | |
58cf4198 | 35 | #include <asm/watchdog.h> |
1da177e4 | 36 | |
8f5585ec | 37 | #define DRV_NAME "sh-wdt" |
1da177e4 LT |
38 | |
39 | /* | |
40 | * Default clock division ratio is 5.25 msecs. For an additional table of | |
41 | * values, consult the asm-sh/watchdog.h. Overload this at module load | |
42 | * time. | |
43 | * | |
44 | * In order for this to work reliably we need to have HZ set to 1000 or | |
45 | * something quite higher than 100 (or we need a proper high-res timer | |
46 | * implementation that will deal with this properly), otherwise the 10ms | |
47 | * resolution of a jiffy is enough to trigger the overflow. For things like | |
48 | * the SH-4 and SH-5, this isn't necessarily that big of a problem, though | |
49 | * for the SH-2 and SH-3, this isn't recommended unless the WDT is absolutely | |
50 | * necssary. | |
51 | * | |
52 | * As a result of this timing problem, the only modes that are particularly | |
25985edc | 53 | * feasible are the 4096 and the 2048 divisors, which yield 5.25 and 2.62ms |
1da177e4 LT |
54 | * overflow periods respectively. |
55 | * | |
56 | * Also, since we can't really expect userspace to be responsive enough | |
ee0fc097 | 57 | * before the overflow happens, we maintain two separate timers .. One in |
1da177e4 LT |
58 | * the kernel for clearing out WOVF every 2ms or so (again, this depends on |
59 | * HZ == 1000), and another for monitoring userspace writes to the WDT device. | |
60 | * | |
61 | * As such, we currently use a configurable heartbeat interval which defaults | |
62 | * to 30s. In this case, the userspace daemon is only responsible for periodic | |
63 | * writes to the device before the next heartbeat is scheduled. If the daemon | |
64 | * misses its deadline, the kernel timer will allow the WDT to overflow. | |
65 | */ | |
66 | static int clock_division_ratio = WTCSR_CKS_4096; | |
bea19066 | 67 | #define next_ping_period(cks) (jiffies + msecs_to_jiffies(cks - 4)) |
1da177e4 | 68 | |
58cf4198 | 69 | static const struct watchdog_info sh_wdt_info; |
8f5585ec | 70 | static struct platform_device *sh_wdt_dev; |
70b814ec | 71 | static DEFINE_SPINLOCK(shwdt_lock); |
1da177e4 LT |
72 | |
73 | #define WATCHDOG_HEARTBEAT 30 /* 30 sec default heartbeat */ | |
74 | static int heartbeat = WATCHDOG_HEARTBEAT; /* in seconds */ | |
4bfdf378 | 75 | static int nowayout = WATCHDOG_NOWAYOUT; |
8f5585ec PM |
76 | static unsigned long next_heartbeat; |
77 | ||
78 | struct sh_wdt { | |
79 | void __iomem *base; | |
80 | struct device *dev; | |
1da177e4 | 81 | |
8f5585ec PM |
82 | struct timer_list timer; |
83 | ||
84 | unsigned long enabled; | |
85 | char expect_close; | |
86 | }; | |
87 | ||
88 | static void sh_wdt_start(struct sh_wdt *wdt) | |
1da177e4 | 89 | { |
70b814ec | 90 | unsigned long flags; |
8f5585ec | 91 | u8 csr; |
70b814ec | 92 | |
58cf4198 | 93 | spin_lock_irqsave(&shwdt_lock, flags); |
1da177e4 LT |
94 | |
95 | next_heartbeat = jiffies + (heartbeat * HZ); | |
8f5585ec | 96 | mod_timer(&wdt->timer, next_ping_period(clock_division_ratio)); |
1da177e4 LT |
97 | |
98 | csr = sh_wdt_read_csr(); | |
99 | csr |= WTCSR_WT | clock_division_ratio; | |
100 | sh_wdt_write_csr(csr); | |
101 | ||
102 | sh_wdt_write_cnt(0); | |
103 | ||
104 | /* | |
105 | * These processors have a bit of an inconsistent initialization | |
106 | * process.. starting with SH-3, RSTS was moved to WTCSR, and the | |
107 | * RSTCSR register was removed. | |
108 | * | |
109 | * On the SH-2 however, in addition with bits being in different | |
110 | * locations, we must deal with RSTCSR outright.. | |
111 | */ | |
112 | csr = sh_wdt_read_csr(); | |
113 | csr |= WTCSR_TME; | |
114 | csr &= ~WTCSR_RSTS; | |
115 | sh_wdt_write_csr(csr); | |
116 | ||
117 | #ifdef CONFIG_CPU_SH2 | |
1da177e4 LT |
118 | csr = sh_wdt_read_rstcsr(); |
119 | csr &= ~RSTCSR_RSTS; | |
120 | sh_wdt_write_rstcsr(csr); | |
121 | #endif | |
58cf4198 | 122 | spin_unlock_irqrestore(&shwdt_lock, flags); |
1da177e4 LT |
123 | } |
124 | ||
8f5585ec | 125 | static void sh_wdt_stop(struct sh_wdt *wdt) |
1da177e4 | 126 | { |
70b814ec | 127 | unsigned long flags; |
8f5585ec | 128 | u8 csr; |
70b814ec | 129 | |
58cf4198 | 130 | spin_lock_irqsave(&shwdt_lock, flags); |
1da177e4 | 131 | |
8f5585ec | 132 | del_timer(&wdt->timer); |
1da177e4 LT |
133 | |
134 | csr = sh_wdt_read_csr(); | |
135 | csr &= ~WTCSR_TME; | |
136 | sh_wdt_write_csr(csr); | |
8f5585ec | 137 | |
58cf4198 | 138 | spin_unlock_irqrestore(&shwdt_lock, flags); |
1da177e4 LT |
139 | } |
140 | ||
8f5585ec | 141 | static inline void sh_wdt_keepalive(struct sh_wdt *wdt) |
1da177e4 | 142 | { |
70b814ec AC |
143 | unsigned long flags; |
144 | ||
58cf4198 | 145 | spin_lock_irqsave(&shwdt_lock, flags); |
1da177e4 | 146 | next_heartbeat = jiffies + (heartbeat * HZ); |
58cf4198 | 147 | spin_unlock_irqrestore(&shwdt_lock, flags); |
1da177e4 LT |
148 | } |
149 | ||
1da177e4 LT |
150 | static int sh_wdt_set_heartbeat(int t) |
151 | { | |
70b814ec AC |
152 | unsigned long flags; |
153 | ||
154 | if (unlikely(t < 1 || t > 3600)) /* arbitrary upper limit */ | |
1da177e4 LT |
155 | return -EINVAL; |
156 | ||
58cf4198 | 157 | spin_lock_irqsave(&shwdt_lock, flags); |
1da177e4 | 158 | heartbeat = t; |
58cf4198 | 159 | spin_unlock_irqrestore(&shwdt_lock, flags); |
1da177e4 LT |
160 | return 0; |
161 | } | |
162 | ||
1da177e4 LT |
163 | static void sh_wdt_ping(unsigned long data) |
164 | { | |
8f5585ec | 165 | struct sh_wdt *wdt = (struct sh_wdt *)data; |
70b814ec AC |
166 | unsigned long flags; |
167 | ||
58cf4198 | 168 | spin_lock_irqsave(&shwdt_lock, flags); |
1da177e4 | 169 | if (time_before(jiffies, next_heartbeat)) { |
8f5585ec | 170 | u8 csr; |
1da177e4 LT |
171 | |
172 | csr = sh_wdt_read_csr(); | |
173 | csr &= ~WTCSR_IOVF; | |
174 | sh_wdt_write_csr(csr); | |
175 | ||
176 | sh_wdt_write_cnt(0); | |
177 | ||
8f5585ec | 178 | mod_timer(&wdt->timer, next_ping_period(clock_division_ratio)); |
e4c2cfee | 179 | } else |
8f5585ec PM |
180 | dev_warn(wdt->dev, "Heartbeat lost! Will not ping " |
181 | "the watchdog\n"); | |
58cf4198 | 182 | spin_unlock_irqrestore(&shwdt_lock, flags); |
1da177e4 LT |
183 | } |
184 | ||
1da177e4 LT |
185 | static int sh_wdt_open(struct inode *inode, struct file *file) |
186 | { | |
8f5585ec PM |
187 | struct sh_wdt *wdt = platform_get_drvdata(sh_wdt_dev); |
188 | ||
189 | if (test_and_set_bit(0, &wdt->enabled)) | |
1da177e4 LT |
190 | return -EBUSY; |
191 | if (nowayout) | |
192 | __module_get(THIS_MODULE); | |
193 | ||
8f5585ec PM |
194 | file->private_data = wdt; |
195 | ||
196 | sh_wdt_start(wdt); | |
1da177e4 LT |
197 | |
198 | return nonseekable_open(inode, file); | |
199 | } | |
200 | ||
1da177e4 LT |
201 | static int sh_wdt_close(struct inode *inode, struct file *file) |
202 | { | |
8f5585ec PM |
203 | struct sh_wdt *wdt = file->private_data; |
204 | ||
205 | if (wdt->expect_close == 42) { | |
206 | sh_wdt_stop(wdt); | |
1da177e4 | 207 | } else { |
8f5585ec PM |
208 | dev_crit(wdt->dev, "Unexpected close, not " |
209 | "stopping watchdog!\n"); | |
210 | sh_wdt_keepalive(wdt); | |
1da177e4 LT |
211 | } |
212 | ||
8f5585ec PM |
213 | clear_bit(0, &wdt->enabled); |
214 | wdt->expect_close = 0; | |
1da177e4 LT |
215 | |
216 | return 0; | |
217 | } | |
218 | ||
1da177e4 LT |
219 | static ssize_t sh_wdt_write(struct file *file, const char *buf, |
220 | size_t count, loff_t *ppos) | |
221 | { | |
8f5585ec PM |
222 | struct sh_wdt *wdt = file->private_data; |
223 | ||
1da177e4 LT |
224 | if (count) { |
225 | if (!nowayout) { | |
226 | size_t i; | |
227 | ||
8f5585ec | 228 | wdt->expect_close = 0; |
1da177e4 LT |
229 | |
230 | for (i = 0; i != count; i++) { | |
231 | char c; | |
232 | if (get_user(c, buf + i)) | |
233 | return -EFAULT; | |
234 | if (c == 'V') | |
8f5585ec | 235 | wdt->expect_close = 42; |
1da177e4 LT |
236 | } |
237 | } | |
8f5585ec | 238 | sh_wdt_keepalive(wdt); |
1da177e4 LT |
239 | } |
240 | ||
241 | return count; | |
242 | } | |
243 | ||
70b814ec AC |
244 | static long sh_wdt_ioctl(struct file *file, unsigned int cmd, |
245 | unsigned long arg) | |
1da177e4 | 246 | { |
8f5585ec | 247 | struct sh_wdt *wdt = file->private_data; |
1da177e4 LT |
248 | int new_heartbeat; |
249 | int options, retval = -EINVAL; | |
250 | ||
251 | switch (cmd) { | |
70b814ec AC |
252 | case WDIOC_GETSUPPORT: |
253 | return copy_to_user((struct watchdog_info *)arg, | |
254 | &sh_wdt_info, sizeof(sh_wdt_info)) ? -EFAULT : 0; | |
255 | case WDIOC_GETSTATUS: | |
256 | case WDIOC_GETBOOTSTATUS: | |
257 | return put_user(0, (int *)arg); | |
70b814ec AC |
258 | case WDIOC_SETOPTIONS: |
259 | if (get_user(options, (int *)arg)) | |
260 | return -EFAULT; | |
261 | ||
262 | if (options & WDIOS_DISABLECARD) { | |
8f5585ec | 263 | sh_wdt_stop(wdt); |
70b814ec AC |
264 | retval = 0; |
265 | } | |
266 | ||
267 | if (options & WDIOS_ENABLECARD) { | |
8f5585ec | 268 | sh_wdt_start(wdt); |
70b814ec AC |
269 | retval = 0; |
270 | } | |
1da177e4 | 271 | |
70b814ec | 272 | return retval; |
0c06090c | 273 | case WDIOC_KEEPALIVE: |
8f5585ec | 274 | sh_wdt_keepalive(wdt); |
0c06090c WVS |
275 | return 0; |
276 | case WDIOC_SETTIMEOUT: | |
277 | if (get_user(new_heartbeat, (int *)arg)) | |
278 | return -EFAULT; | |
279 | ||
280 | if (sh_wdt_set_heartbeat(new_heartbeat)) | |
281 | return -EINVAL; | |
282 | ||
8f5585ec | 283 | sh_wdt_keepalive(wdt); |
0c06090c WVS |
284 | /* Fall */ |
285 | case WDIOC_GETTIMEOUT: | |
286 | return put_user(heartbeat, (int *)arg); | |
70b814ec AC |
287 | default: |
288 | return -ENOTTY; | |
289 | } | |
1da177e4 LT |
290 | return 0; |
291 | } | |
292 | ||
1da177e4 LT |
293 | static int sh_wdt_notify_sys(struct notifier_block *this, |
294 | unsigned long code, void *unused) | |
295 | { | |
8f5585ec PM |
296 | struct sh_wdt *wdt = platform_get_drvdata(sh_wdt_dev); |
297 | ||
e4c2cfee | 298 | if (code == SYS_DOWN || code == SYS_HALT) |
8f5585ec | 299 | sh_wdt_stop(wdt); |
1da177e4 LT |
300 | |
301 | return NOTIFY_DONE; | |
302 | } | |
303 | ||
62322d25 | 304 | static const struct file_operations sh_wdt_fops = { |
1da177e4 LT |
305 | .owner = THIS_MODULE, |
306 | .llseek = no_llseek, | |
307 | .write = sh_wdt_write, | |
70b814ec | 308 | .unlocked_ioctl = sh_wdt_ioctl, |
1da177e4 LT |
309 | .open = sh_wdt_open, |
310 | .release = sh_wdt_close, | |
311 | }; | |
312 | ||
70b814ec | 313 | static const struct watchdog_info sh_wdt_info = { |
e4c2cfee PM |
314 | .options = WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT | |
315 | WDIOF_MAGICCLOSE, | |
1da177e4 LT |
316 | .firmware_version = 1, |
317 | .identity = "SH WDT", | |
318 | }; | |
319 | ||
320 | static struct notifier_block sh_wdt_notifier = { | |
321 | .notifier_call = sh_wdt_notify_sys, | |
322 | }; | |
323 | ||
324 | static struct miscdevice sh_wdt_miscdev = { | |
325 | .minor = WATCHDOG_MINOR, | |
326 | .name = "watchdog", | |
327 | .fops = &sh_wdt_fops, | |
328 | }; | |
329 | ||
8f5585ec | 330 | static int __devinit sh_wdt_probe(struct platform_device *pdev) |
1da177e4 | 331 | { |
8f5585ec PM |
332 | struct sh_wdt *wdt; |
333 | struct resource *res; | |
1da177e4 LT |
334 | int rc; |
335 | ||
8f5585ec PM |
336 | /* |
337 | * As this driver only covers the global watchdog case, reject | |
338 | * any attempts to register per-CPU watchdogs. | |
339 | */ | |
340 | if (pdev->id != -1) | |
341 | return -EINVAL; | |
342 | ||
343 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
344 | if (unlikely(!res)) | |
345 | return -EINVAL; | |
346 | ||
347 | if (!devm_request_mem_region(&pdev->dev, res->start, | |
348 | resource_size(res), DRV_NAME)) | |
349 | return -EBUSY; | |
350 | ||
351 | wdt = devm_kzalloc(&pdev->dev, sizeof(struct sh_wdt), GFP_KERNEL); | |
352 | if (unlikely(!wdt)) { | |
353 | rc = -ENOMEM; | |
354 | goto out_release; | |
1da177e4 LT |
355 | } |
356 | ||
8f5585ec PM |
357 | wdt->dev = &pdev->dev; |
358 | ||
359 | wdt->base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); | |
360 | if (unlikely(!wdt->base)) { | |
361 | rc = -ENXIO; | |
362 | goto out_err; | |
1da177e4 LT |
363 | } |
364 | ||
1da177e4 | 365 | rc = register_reboot_notifier(&sh_wdt_notifier); |
e4c2cfee | 366 | if (unlikely(rc)) { |
8f5585ec | 367 | dev_err(&pdev->dev, |
70b814ec | 368 | "Can't register reboot notifier (err=%d)\n", rc); |
8f5585ec | 369 | goto out_unmap; |
1da177e4 LT |
370 | } |
371 | ||
8f5585ec PM |
372 | sh_wdt_miscdev.parent = wdt->dev; |
373 | ||
1da177e4 | 374 | rc = misc_register(&sh_wdt_miscdev); |
e4c2cfee | 375 | if (unlikely(rc)) { |
8f5585ec | 376 | dev_err(&pdev->dev, |
70b814ec AC |
377 | "Can't register miscdev on minor=%d (err=%d)\n", |
378 | sh_wdt_miscdev.minor, rc); | |
8f5585ec | 379 | goto out_unreg; |
1da177e4 LT |
380 | } |
381 | ||
8f5585ec PM |
382 | init_timer(&wdt->timer); |
383 | wdt->timer.function = sh_wdt_ping; | |
384 | wdt->timer.data = (unsigned long)wdt; | |
385 | wdt->timer.expires = next_ping_period(clock_division_ratio); | |
386 | ||
387 | platform_set_drvdata(pdev, wdt); | |
388 | sh_wdt_dev = pdev; | |
389 | ||
390 | dev_info(&pdev->dev, "initialized.\n"); | |
1da177e4 LT |
391 | |
392 | return 0; | |
8f5585ec PM |
393 | |
394 | out_unreg: | |
395 | unregister_reboot_notifier(&sh_wdt_notifier); | |
396 | out_unmap: | |
397 | devm_iounmap(&pdev->dev, wdt->base); | |
398 | out_err: | |
399 | devm_kfree(&pdev->dev, wdt); | |
400 | out_release: | |
401 | devm_release_mem_region(&pdev->dev, res->start, resource_size(res)); | |
402 | ||
403 | return rc; | |
1da177e4 LT |
404 | } |
405 | ||
8f5585ec | 406 | static int __devexit sh_wdt_remove(struct platform_device *pdev) |
1da177e4 | 407 | { |
8f5585ec PM |
408 | struct sh_wdt *wdt = platform_get_drvdata(pdev); |
409 | struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
410 | ||
411 | platform_set_drvdata(pdev, NULL); | |
412 | ||
1da177e4 | 413 | misc_deregister(&sh_wdt_miscdev); |
8f5585ec PM |
414 | |
415 | sh_wdt_dev = NULL; | |
416 | ||
1da177e4 | 417 | unregister_reboot_notifier(&sh_wdt_notifier); |
8f5585ec PM |
418 | devm_release_mem_region(&pdev->dev, res->start, resource_size(res)); |
419 | devm_iounmap(&pdev->dev, wdt->base); | |
420 | devm_kfree(&pdev->dev, wdt); | |
421 | ||
422 | return 0; | |
423 | } | |
424 | ||
425 | static struct platform_driver sh_wdt_driver = { | |
426 | .driver = { | |
427 | .name = DRV_NAME, | |
428 | .owner = THIS_MODULE, | |
429 | }, | |
430 | ||
431 | .probe = sh_wdt_probe, | |
432 | .remove = __devexit_p(sh_wdt_remove), | |
433 | }; | |
434 | ||
435 | static int __init sh_wdt_init(void) | |
436 | { | |
437 | int rc; | |
438 | ||
439 | if (unlikely(clock_division_ratio < 0x5 || | |
440 | clock_division_ratio > 0x7)) { | |
441 | clock_division_ratio = WTCSR_CKS_4096; | |
442 | ||
443 | pr_info("%s: divisor must be 0x5<=x<=0x7, using %d\n", | |
444 | DRV_NAME, clock_division_ratio); | |
445 | } | |
446 | ||
447 | rc = sh_wdt_set_heartbeat(heartbeat); | |
448 | if (unlikely(rc)) { | |
449 | heartbeat = WATCHDOG_HEARTBEAT; | |
450 | ||
451 | pr_info("%s: heartbeat value must be 1<=x<=3600, using %d\n", | |
452 | DRV_NAME, heartbeat); | |
453 | } | |
454 | ||
455 | pr_info("%s: configured with heartbeat=%d sec (nowayout=%d)\n", | |
456 | DRV_NAME, heartbeat, nowayout); | |
457 | ||
458 | return platform_driver_register(&sh_wdt_driver); | |
1da177e4 LT |
459 | } |
460 | ||
8f5585ec PM |
461 | static void __exit sh_wdt_exit(void) |
462 | { | |
463 | platform_driver_unregister(&sh_wdt_driver); | |
464 | } | |
465 | module_init(sh_wdt_init); | |
466 | module_exit(sh_wdt_exit); | |
467 | ||
1da177e4 LT |
468 | MODULE_AUTHOR("Paul Mundt <lethal@linux-sh.org>"); |
469 | MODULE_DESCRIPTION("SuperH watchdog driver"); | |
470 | MODULE_LICENSE("GPL"); | |
8f5585ec | 471 | MODULE_ALIAS("platform:" DRV_NAME); |
1da177e4 LT |
472 | MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR); |
473 | ||
474 | module_param(clock_division_ratio, int, 0); | |
a77dba7e WVS |
475 | MODULE_PARM_DESC(clock_division_ratio, |
476 | "Clock division ratio. Valid ranges are from 0x5 (1.31ms) " | |
76550d32 | 477 | "to 0x7 (5.25ms). (default=" __MODULE_STRING(WTCSR_CKS_4096) ")"); |
1da177e4 LT |
478 | |
479 | module_param(heartbeat, int, 0); | |
70b814ec AC |
480 | MODULE_PARM_DESC(heartbeat, |
481 | "Watchdog heartbeat in seconds. (1 <= heartbeat <= 3600, default=" | |
482 | __MODULE_STRING(WATCHDOG_HEARTBEAT) ")"); | |
1da177e4 LT |
483 | |
484 | module_param(nowayout, int, 0); | |
70b814ec AC |
485 | MODULE_PARM_DESC(nowayout, |
486 | "Watchdog cannot be stopped once started (default=" | |
487 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); |