diff options
Diffstat (limited to 'drivers/watchdog')
-rw-r--r-- | drivers/watchdog/shwdt.c | 227 |
1 files changed, 168 insertions, 59 deletions
diff --git a/drivers/watchdog/shwdt.c b/drivers/watchdog/shwdt.c index bee1f5865825..b7d2f8a0422b 100644 --- a/drivers/watchdog/shwdt.c +++ b/drivers/watchdog/shwdt.c | |||
@@ -19,6 +19,7 @@ | |||
19 | */ | 19 | */ |
20 | #include <linux/module.h> | 20 | #include <linux/module.h> |
21 | #include <linux/moduleparam.h> | 21 | #include <linux/moduleparam.h> |
22 | #include <linux/platform_device.h> | ||
22 | #include <linux/init.h> | 23 | #include <linux/init.h> |
23 | #include <linux/types.h> | 24 | #include <linux/types.h> |
24 | #include <linux/miscdevice.h> | 25 | #include <linux/miscdevice.h> |
@@ -28,11 +29,12 @@ | |||
28 | #include <linux/ioport.h> | 29 | #include <linux/ioport.h> |
29 | #include <linux/fs.h> | 30 | #include <linux/fs.h> |
30 | #include <linux/mm.h> | 31 | #include <linux/mm.h> |
32 | #include <linux/slab.h> | ||
31 | #include <linux/io.h> | 33 | #include <linux/io.h> |
32 | #include <linux/uaccess.h> | 34 | #include <linux/uaccess.h> |
33 | #include <asm/watchdog.h> | 35 | #include <asm/watchdog.h> |
34 | 36 | ||
35 | #define PFX "shwdt: " | 37 | #define DRV_NAME "sh-wdt" |
36 | 38 | ||
37 | /* | 39 | /* |
38 | * Default clock division ratio is 5.25 msecs. For an additional table of | 40 | * Default clock division ratio is 5.25 msecs. For an additional table of |
@@ -62,31 +64,36 @@ | |||
62 | * misses its deadline, the kernel timer will allow the WDT to overflow. | 64 | * misses its deadline, the kernel timer will allow the WDT to overflow. |
63 | */ | 65 | */ |
64 | static int clock_division_ratio = WTCSR_CKS_4096; | 66 | static int clock_division_ratio = WTCSR_CKS_4096; |
65 | |||
66 | #define next_ping_period(cks) msecs_to_jiffies(cks - 4) | 67 | #define next_ping_period(cks) msecs_to_jiffies(cks - 4) |
67 | 68 | ||
68 | static void sh_wdt_ping(unsigned long data); | ||
69 | |||
70 | static unsigned long shwdt_is_open; | ||
71 | static const struct watchdog_info sh_wdt_info; | 69 | static const struct watchdog_info sh_wdt_info; |
72 | static char shwdt_expect_close; | 70 | static struct platform_device *sh_wdt_dev; |
73 | static DEFINE_TIMER(timer, sh_wdt_ping, 0, 0); | ||
74 | static unsigned long next_heartbeat; | ||
75 | static DEFINE_SPINLOCK(shwdt_lock); | 71 | static DEFINE_SPINLOCK(shwdt_lock); |
76 | 72 | ||
77 | #define WATCHDOG_HEARTBEAT 30 /* 30 sec default heartbeat */ | 73 | #define WATCHDOG_HEARTBEAT 30 /* 30 sec default heartbeat */ |
78 | static int heartbeat = WATCHDOG_HEARTBEAT; /* in seconds */ | 74 | static int heartbeat = WATCHDOG_HEARTBEAT; /* in seconds */ |
79 | static int nowayout = WATCHDOG_NOWAYOUT; | 75 | static int nowayout = WATCHDOG_NOWAYOUT; |
76 | static unsigned long next_heartbeat; | ||
77 | |||
78 | struct sh_wdt { | ||
79 | void __iomem *base; | ||
80 | struct device *dev; | ||
80 | 81 | ||
81 | static void sh_wdt_start(void) | 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) | ||
82 | { | 89 | { |
83 | __u8 csr; | ||
84 | unsigned long flags; | 90 | unsigned long flags; |
91 | u8 csr; | ||
85 | 92 | ||
86 | spin_lock_irqsave(&shwdt_lock, flags); | 93 | spin_lock_irqsave(&shwdt_lock, flags); |
87 | 94 | ||
88 | next_heartbeat = jiffies + (heartbeat * HZ); | 95 | next_heartbeat = jiffies + (heartbeat * HZ); |
89 | mod_timer(&timer, next_ping_period(clock_division_ratio)); | 96 | mod_timer(&wdt->timer, next_ping_period(clock_division_ratio)); |
90 | 97 | ||
91 | csr = sh_wdt_read_csr(); | 98 | csr = sh_wdt_read_csr(); |
92 | csr |= WTCSR_WT | clock_division_ratio; | 99 | csr |= WTCSR_WT | clock_division_ratio; |
@@ -115,22 +122,23 @@ static void sh_wdt_start(void) | |||
115 | spin_unlock_irqrestore(&shwdt_lock, flags); | 122 | spin_unlock_irqrestore(&shwdt_lock, flags); |
116 | } | 123 | } |
117 | 124 | ||
118 | static void sh_wdt_stop(void) | 125 | static void sh_wdt_stop(struct sh_wdt *wdt) |
119 | { | 126 | { |
120 | __u8 csr; | ||
121 | unsigned long flags; | 127 | unsigned long flags; |
128 | u8 csr; | ||
122 | 129 | ||
123 | spin_lock_irqsave(&shwdt_lock, flags); | 130 | spin_lock_irqsave(&shwdt_lock, flags); |
124 | 131 | ||
125 | del_timer(&timer); | 132 | del_timer(&wdt->timer); |
126 | 133 | ||
127 | csr = sh_wdt_read_csr(); | 134 | csr = sh_wdt_read_csr(); |
128 | csr &= ~WTCSR_TME; | 135 | csr &= ~WTCSR_TME; |
129 | sh_wdt_write_csr(csr); | 136 | sh_wdt_write_csr(csr); |
137 | |||
130 | spin_unlock_irqrestore(&shwdt_lock, flags); | 138 | spin_unlock_irqrestore(&shwdt_lock, flags); |
131 | } | 139 | } |
132 | 140 | ||
133 | static inline void sh_wdt_keepalive(void) | 141 | static inline void sh_wdt_keepalive(struct sh_wdt *wdt) |
134 | { | 142 | { |
135 | unsigned long flags; | 143 | unsigned long flags; |
136 | 144 | ||
@@ -154,11 +162,12 @@ static int sh_wdt_set_heartbeat(int t) | |||
154 | 162 | ||
155 | static void sh_wdt_ping(unsigned long data) | 163 | static void sh_wdt_ping(unsigned long data) |
156 | { | 164 | { |
165 | struct sh_wdt *wdt = (struct sh_wdt *)data; | ||
157 | unsigned long flags; | 166 | unsigned long flags; |
158 | 167 | ||
159 | spin_lock_irqsave(&shwdt_lock, flags); | 168 | spin_lock_irqsave(&shwdt_lock, flags); |
160 | if (time_before(jiffies, next_heartbeat)) { | 169 | if (time_before(jiffies, next_heartbeat)) { |
161 | __u8 csr; | 170 | u8 csr; |
162 | 171 | ||
163 | csr = sh_wdt_read_csr(); | 172 | csr = sh_wdt_read_csr(); |
164 | csr &= ~WTCSR_IOVF; | 173 | csr &= ~WTCSR_IOVF; |
@@ -166,37 +175,43 @@ static void sh_wdt_ping(unsigned long data) | |||
166 | 175 | ||
167 | sh_wdt_write_cnt(0); | 176 | sh_wdt_write_cnt(0); |
168 | 177 | ||
169 | mod_timer(&timer, next_ping_period(clock_division_ratio)); | 178 | mod_timer(&wdt->timer, next_ping_period(clock_division_ratio)); |
170 | } else | 179 | } else |
171 | printk(KERN_WARNING PFX "Heartbeat lost! Will not ping " | 180 | dev_warn(wdt->dev, "Heartbeat lost! Will not ping " |
172 | "the watchdog\n"); | 181 | "the watchdog\n"); |
173 | spin_unlock_irqrestore(&shwdt_lock, flags); | 182 | spin_unlock_irqrestore(&shwdt_lock, flags); |
174 | } | 183 | } |
175 | 184 | ||
176 | static int sh_wdt_open(struct inode *inode, struct file *file) | 185 | static int sh_wdt_open(struct inode *inode, struct file *file) |
177 | { | 186 | { |
178 | if (test_and_set_bit(0, &shwdt_is_open)) | 187 | struct sh_wdt *wdt = platform_get_drvdata(sh_wdt_dev); |
188 | |||
189 | if (test_and_set_bit(0, &wdt->enabled)) | ||
179 | return -EBUSY; | 190 | return -EBUSY; |
180 | if (nowayout) | 191 | if (nowayout) |
181 | __module_get(THIS_MODULE); | 192 | __module_get(THIS_MODULE); |
182 | 193 | ||
183 | sh_wdt_start(); | 194 | file->private_data = wdt; |
195 | |||
196 | sh_wdt_start(wdt); | ||
184 | 197 | ||
185 | return nonseekable_open(inode, file); | 198 | return nonseekable_open(inode, file); |
186 | } | 199 | } |
187 | 200 | ||
188 | static int sh_wdt_close(struct inode *inode, struct file *file) | 201 | static int sh_wdt_close(struct inode *inode, struct file *file) |
189 | { | 202 | { |
190 | if (shwdt_expect_close == 42) { | 203 | struct sh_wdt *wdt = file->private_data; |
191 | sh_wdt_stop(); | 204 | |
205 | if (wdt->expect_close == 42) { | ||
206 | sh_wdt_stop(wdt); | ||
192 | } else { | 207 | } else { |
193 | printk(KERN_CRIT PFX "Unexpected close, not " | 208 | dev_crit(wdt->dev, "Unexpected close, not " |
194 | "stopping watchdog!\n"); | 209 | "stopping watchdog!\n"); |
195 | sh_wdt_keepalive(); | 210 | sh_wdt_keepalive(wdt); |
196 | } | 211 | } |
197 | 212 | ||
198 | clear_bit(0, &shwdt_is_open); | 213 | clear_bit(0, &wdt->enabled); |
199 | shwdt_expect_close = 0; | 214 | wdt->expect_close = 0; |
200 | 215 | ||
201 | return 0; | 216 | return 0; |
202 | } | 217 | } |
@@ -204,21 +219,23 @@ static int sh_wdt_close(struct inode *inode, struct file *file) | |||
204 | static ssize_t sh_wdt_write(struct file *file, const char *buf, | 219 | static ssize_t sh_wdt_write(struct file *file, const char *buf, |
205 | size_t count, loff_t *ppos) | 220 | size_t count, loff_t *ppos) |
206 | { | 221 | { |
222 | struct sh_wdt *wdt = file->private_data; | ||
223 | |||
207 | if (count) { | 224 | if (count) { |
208 | if (!nowayout) { | 225 | if (!nowayout) { |
209 | size_t i; | 226 | size_t i; |
210 | 227 | ||
211 | shwdt_expect_close = 0; | 228 | wdt->expect_close = 0; |
212 | 229 | ||
213 | for (i = 0; i != count; i++) { | 230 | for (i = 0; i != count; i++) { |
214 | char c; | 231 | char c; |
215 | if (get_user(c, buf + i)) | 232 | if (get_user(c, buf + i)) |
216 | return -EFAULT; | 233 | return -EFAULT; |
217 | if (c == 'V') | 234 | if (c == 'V') |
218 | shwdt_expect_close = 42; | 235 | wdt->expect_close = 42; |
219 | } | 236 | } |
220 | } | 237 | } |
221 | sh_wdt_keepalive(); | 238 | sh_wdt_keepalive(wdt); |
222 | } | 239 | } |
223 | 240 | ||
224 | return count; | 241 | return count; |
@@ -227,6 +244,7 @@ static ssize_t sh_wdt_write(struct file *file, const char *buf, | |||
227 | static long sh_wdt_ioctl(struct file *file, unsigned int cmd, | 244 | static long sh_wdt_ioctl(struct file *file, unsigned int cmd, |
228 | unsigned long arg) | 245 | unsigned long arg) |
229 | { | 246 | { |
247 | struct sh_wdt *wdt = file->private_data; | ||
230 | int new_heartbeat; | 248 | int new_heartbeat; |
231 | int options, retval = -EINVAL; | 249 | int options, retval = -EINVAL; |
232 | 250 | ||
@@ -242,18 +260,18 @@ static long sh_wdt_ioctl(struct file *file, unsigned int cmd, | |||
242 | return -EFAULT; | 260 | return -EFAULT; |
243 | 261 | ||
244 | if (options & WDIOS_DISABLECARD) { | 262 | if (options & WDIOS_DISABLECARD) { |
245 | sh_wdt_stop(); | 263 | sh_wdt_stop(wdt); |
246 | retval = 0; | 264 | retval = 0; |
247 | } | 265 | } |
248 | 266 | ||
249 | if (options & WDIOS_ENABLECARD) { | 267 | if (options & WDIOS_ENABLECARD) { |
250 | sh_wdt_start(); | 268 | sh_wdt_start(wdt); |
251 | retval = 0; | 269 | retval = 0; |
252 | } | 270 | } |
253 | 271 | ||
254 | return retval; | 272 | return retval; |
255 | case WDIOC_KEEPALIVE: | 273 | case WDIOC_KEEPALIVE: |
256 | sh_wdt_keepalive(); | 274 | sh_wdt_keepalive(wdt); |
257 | return 0; | 275 | return 0; |
258 | case WDIOC_SETTIMEOUT: | 276 | case WDIOC_SETTIMEOUT: |
259 | if (get_user(new_heartbeat, (int *)arg)) | 277 | if (get_user(new_heartbeat, (int *)arg)) |
@@ -262,7 +280,7 @@ static long sh_wdt_ioctl(struct file *file, unsigned int cmd, | |||
262 | if (sh_wdt_set_heartbeat(new_heartbeat)) | 280 | if (sh_wdt_set_heartbeat(new_heartbeat)) |
263 | return -EINVAL; | 281 | return -EINVAL; |
264 | 282 | ||
265 | sh_wdt_keepalive(); | 283 | sh_wdt_keepalive(wdt); |
266 | /* Fall */ | 284 | /* Fall */ |
267 | case WDIOC_GETTIMEOUT: | 285 | case WDIOC_GETTIMEOUT: |
268 | return put_user(heartbeat, (int *)arg); | 286 | return put_user(heartbeat, (int *)arg); |
@@ -275,8 +293,10 @@ static long sh_wdt_ioctl(struct file *file, unsigned int cmd, | |||
275 | static int sh_wdt_notify_sys(struct notifier_block *this, | 293 | static int sh_wdt_notify_sys(struct notifier_block *this, |
276 | unsigned long code, void *unused) | 294 | unsigned long code, void *unused) |
277 | { | 295 | { |
296 | struct sh_wdt *wdt = platform_get_drvdata(sh_wdt_dev); | ||
297 | |||
278 | if (code == SYS_DOWN || code == SYS_HALT) | 298 | if (code == SYS_DOWN || code == SYS_HALT) |
279 | sh_wdt_stop(); | 299 | sh_wdt_stop(wdt); |
280 | 300 | ||
281 | return NOTIFY_DONE; | 301 | return NOTIFY_DONE; |
282 | } | 302 | } |
@@ -307,56 +327,148 @@ static struct miscdevice sh_wdt_miscdev = { | |||
307 | .fops = &sh_wdt_fops, | 327 | .fops = &sh_wdt_fops, |
308 | }; | 328 | }; |
309 | 329 | ||
310 | static int __init sh_wdt_init(void) | 330 | static int __devinit sh_wdt_probe(struct platform_device *pdev) |
311 | { | 331 | { |
332 | struct sh_wdt *wdt; | ||
333 | struct resource *res; | ||
312 | int rc; | 334 | int rc; |
313 | 335 | ||
314 | if (clock_division_ratio < 0x5 || clock_division_ratio > 0x7) { | 336 | /* |
315 | clock_division_ratio = WTCSR_CKS_4096; | 337 | * As this driver only covers the global watchdog case, reject |
316 | printk(KERN_INFO PFX | 338 | * any attempts to register per-CPU watchdogs. |
317 | "clock_division_ratio value must be 0x5<=x<=0x7, using %d\n", | 339 | */ |
318 | clock_division_ratio); | 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; | ||
319 | } | 355 | } |
320 | 356 | ||
321 | rc = sh_wdt_set_heartbeat(heartbeat); | 357 | wdt->dev = &pdev->dev; |
322 | if (unlikely(rc)) { | 358 | |
323 | heartbeat = WATCHDOG_HEARTBEAT; | 359 | wdt->base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); |
324 | printk(KERN_INFO PFX | 360 | if (unlikely(!wdt->base)) { |
325 | "heartbeat value must be 1<=x<=3600, using %d\n", | 361 | rc = -ENXIO; |
326 | heartbeat); | 362 | goto out_err; |
327 | } | 363 | } |
328 | 364 | ||
329 | rc = register_reboot_notifier(&sh_wdt_notifier); | 365 | rc = register_reboot_notifier(&sh_wdt_notifier); |
330 | if (unlikely(rc)) { | 366 | if (unlikely(rc)) { |
331 | printk(KERN_ERR PFX | 367 | dev_err(&pdev->dev, |
332 | "Can't register reboot notifier (err=%d)\n", rc); | 368 | "Can't register reboot notifier (err=%d)\n", rc); |
333 | return rc; | 369 | goto out_unmap; |
334 | } | 370 | } |
335 | 371 | ||
372 | sh_wdt_miscdev.parent = wdt->dev; | ||
373 | |||
336 | rc = misc_register(&sh_wdt_miscdev); | 374 | rc = misc_register(&sh_wdt_miscdev); |
337 | if (unlikely(rc)) { | 375 | if (unlikely(rc)) { |
338 | printk(KERN_ERR PFX | 376 | dev_err(&pdev->dev, |
339 | "Can't register miscdev on minor=%d (err=%d)\n", | 377 | "Can't register miscdev on minor=%d (err=%d)\n", |
340 | sh_wdt_miscdev.minor, rc); | 378 | sh_wdt_miscdev.minor, rc); |
341 | unregister_reboot_notifier(&sh_wdt_notifier); | 379 | goto out_unreg; |
342 | return rc; | ||
343 | } | 380 | } |
344 | 381 | ||
345 | printk(KERN_INFO PFX "initialized. heartbeat=%d sec (nowayout=%d)\n", | 382 | init_timer(&wdt->timer); |
346 | heartbeat, nowayout); | 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"); | ||
347 | 391 | ||
348 | return 0; | 392 | return 0; |
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; | ||
349 | } | 404 | } |
350 | 405 | ||
351 | static void __exit sh_wdt_exit(void) | 406 | static int __devexit sh_wdt_remove(struct platform_device *pdev) |
352 | { | 407 | { |
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 | |||
353 | misc_deregister(&sh_wdt_miscdev); | 413 | misc_deregister(&sh_wdt_miscdev); |
414 | |||
415 | sh_wdt_dev = NULL; | ||
416 | |||
354 | unregister_reboot_notifier(&sh_wdt_notifier); | 417 | unregister_reboot_notifier(&sh_wdt_notifier); |
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); | ||
355 | } | 459 | } |
356 | 460 | ||
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 | |||
357 | MODULE_AUTHOR("Paul Mundt <lethal@linux-sh.org>"); | 468 | MODULE_AUTHOR("Paul Mundt <lethal@linux-sh.org>"); |
358 | MODULE_DESCRIPTION("SuperH watchdog driver"); | 469 | MODULE_DESCRIPTION("SuperH watchdog driver"); |
359 | MODULE_LICENSE("GPL"); | 470 | MODULE_LICENSE("GPL"); |
471 | MODULE_ALIAS("platform:" DRV_NAME); | ||
360 | MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR); | 472 | MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR); |
361 | 473 | ||
362 | module_param(clock_division_ratio, int, 0); | 474 | module_param(clock_division_ratio, int, 0); |
@@ -373,6 +485,3 @@ module_param(nowayout, int, 0); | |||
373 | MODULE_PARM_DESC(nowayout, | 485 | MODULE_PARM_DESC(nowayout, |
374 | "Watchdog cannot be stopped once started (default=" | 486 | "Watchdog cannot be stopped once started (default=" |
375 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); | 487 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); |
376 | |||
377 | module_init(sh_wdt_init); | ||
378 | module_exit(sh_wdt_exit); | ||