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