linux/drivers/watchdog/scx200_wdt.c
<<
>>
Prefs
   1// SPDX-License-Identifier: GPL-2.0-or-later
   2/* drivers/char/watchdog/scx200_wdt.c
   3
   4   National Semiconductor SCx200 Watchdog support
   5
   6   Copyright (c) 2001,2002 Christer Weinigel <wingel@nano-system.com>
   7
   8   Some code taken from:
   9   National Semiconductor PC87307/PC97307 (ala SC1200) WDT driver
  10   (c) Copyright 2002 Zwane Mwaikambo <zwane@commfireservices.com>
  11
  12
  13   The author(s) of this software shall not be held liable for damages
  14   of any nature resulting due to the use of this software. This
  15   software is provided AS-IS with no warranties. */
  16
  17#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
  18
  19#include <linux/module.h>
  20#include <linux/moduleparam.h>
  21#include <linux/init.h>
  22#include <linux/miscdevice.h>
  23#include <linux/watchdog.h>
  24#include <linux/notifier.h>
  25#include <linux/reboot.h>
  26#include <linux/fs.h>
  27#include <linux/ioport.h>
  28#include <linux/scx200.h>
  29#include <linux/uaccess.h>
  30#include <linux/io.h>
  31
  32#define DEBUG
  33
  34MODULE_AUTHOR("Christer Weinigel <wingel@nano-system.com>");
  35MODULE_DESCRIPTION("NatSemi SCx200 Watchdog Driver");
  36MODULE_LICENSE("GPL");
  37
  38static int margin = 60;         /* in seconds */
  39module_param(margin, int, 0);
  40MODULE_PARM_DESC(margin, "Watchdog margin in seconds");
  41
  42static bool nowayout = WATCHDOG_NOWAYOUT;
  43module_param(nowayout, bool, 0);
  44MODULE_PARM_DESC(nowayout, "Disable watchdog shutdown on close");
  45
  46static u16 wdto_restart;
  47static char expect_close;
  48static unsigned long open_lock;
  49static DEFINE_SPINLOCK(scx_lock);
  50
  51/* Bits of the WDCNFG register */
  52#define W_ENABLE 0x00fa         /* Enable watchdog */
  53#define W_DISABLE 0x0000        /* Disable watchdog */
  54
  55/* The scaling factor for the timer, this depends on the value of W_ENABLE */
  56#define W_SCALE (32768/1024)
  57
  58static void scx200_wdt_ping(void)
  59{
  60        spin_lock(&scx_lock);
  61        outw(wdto_restart, scx200_cb_base + SCx200_WDT_WDTO);
  62        spin_unlock(&scx_lock);
  63}
  64
  65static void scx200_wdt_update_margin(void)
  66{
  67        pr_info("timer margin %d seconds\n", margin);
  68        wdto_restart = margin * W_SCALE;
  69}
  70
  71static void scx200_wdt_enable(void)
  72{
  73        pr_debug("enabling watchdog timer, wdto_restart = %d\n", wdto_restart);
  74
  75        spin_lock(&scx_lock);
  76        outw(0, scx200_cb_base + SCx200_WDT_WDTO);
  77        outb(SCx200_WDT_WDSTS_WDOVF, scx200_cb_base + SCx200_WDT_WDSTS);
  78        outw(W_ENABLE, scx200_cb_base + SCx200_WDT_WDCNFG);
  79        spin_unlock(&scx_lock);
  80
  81        scx200_wdt_ping();
  82}
  83
  84static void scx200_wdt_disable(void)
  85{
  86        pr_debug("disabling watchdog timer\n");
  87
  88        spin_lock(&scx_lock);
  89        outw(0, scx200_cb_base + SCx200_WDT_WDTO);
  90        outb(SCx200_WDT_WDSTS_WDOVF, scx200_cb_base + SCx200_WDT_WDSTS);
  91        outw(W_DISABLE, scx200_cb_base + SCx200_WDT_WDCNFG);
  92        spin_unlock(&scx_lock);
  93}
  94
  95static int scx200_wdt_open(struct inode *inode, struct file *file)
  96{
  97        /* only allow one at a time */
  98        if (test_and_set_bit(0, &open_lock))
  99                return -EBUSY;
 100        scx200_wdt_enable();
 101
 102        return stream_open(inode, file);
 103}
 104
 105static int scx200_wdt_release(struct inode *inode, struct file *file)
 106{
 107        if (expect_close != 42)
 108                pr_warn("watchdog device closed unexpectedly, will not disable the watchdog timer\n");
 109        else if (!nowayout)
 110                scx200_wdt_disable();
 111        expect_close = 0;
 112        clear_bit(0, &open_lock);
 113
 114        return 0;
 115}
 116
 117static int scx200_wdt_notify_sys(struct notifier_block *this,
 118                                      unsigned long code, void *unused)
 119{
 120        if (code == SYS_HALT || code == SYS_POWER_OFF)
 121                if (!nowayout)
 122                        scx200_wdt_disable();
 123
 124        return NOTIFY_DONE;
 125}
 126
 127static struct notifier_block scx200_wdt_notifier = {
 128        .notifier_call = scx200_wdt_notify_sys,
 129};
 130
 131static ssize_t scx200_wdt_write(struct file *file, const char __user *data,
 132                                     size_t len, loff_t *ppos)
 133{
 134        /* check for a magic close character */
 135        if (len) {
 136                size_t i;
 137
 138                scx200_wdt_ping();
 139
 140                expect_close = 0;
 141                for (i = 0; i < len; ++i) {
 142                        char c;
 143                        if (get_user(c, data + i))
 144                                return -EFAULT;
 145                        if (c == 'V')
 146                                expect_close = 42;
 147                }
 148
 149                return len;
 150        }
 151
 152        return 0;
 153}
 154
 155static long scx200_wdt_ioctl(struct file *file, unsigned int cmd,
 156                                                        unsigned long arg)
 157{
 158        void __user *argp = (void __user *)arg;
 159        int __user *p = argp;
 160        static const struct watchdog_info ident = {
 161                .identity = "NatSemi SCx200 Watchdog",
 162                .firmware_version = 1,
 163                .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING |
 164                                                WDIOF_MAGICCLOSE,
 165        };
 166        int new_margin;
 167
 168        switch (cmd) {
 169        case WDIOC_GETSUPPORT:
 170                if (copy_to_user(argp, &ident, sizeof(ident)))
 171                        return -EFAULT;
 172                return 0;
 173        case WDIOC_GETSTATUS:
 174        case WDIOC_GETBOOTSTATUS:
 175                if (put_user(0, p))
 176                        return -EFAULT;
 177                return 0;
 178        case WDIOC_KEEPALIVE:
 179                scx200_wdt_ping();
 180                return 0;
 181        case WDIOC_SETTIMEOUT:
 182                if (get_user(new_margin, p))
 183                        return -EFAULT;
 184                if (new_margin < 1)
 185                        return -EINVAL;
 186                margin = new_margin;
 187                scx200_wdt_update_margin();
 188                scx200_wdt_ping();
 189                fallthrough;
 190        case WDIOC_GETTIMEOUT:
 191                if (put_user(margin, p))
 192                        return -EFAULT;
 193                return 0;
 194        default:
 195                return -ENOTTY;
 196        }
 197}
 198
 199static const struct file_operations scx200_wdt_fops = {
 200        .owner = THIS_MODULE,
 201        .llseek = no_llseek,
 202        .write = scx200_wdt_write,
 203        .unlocked_ioctl = scx200_wdt_ioctl,
 204        .compat_ioctl   = compat_ptr_ioctl,
 205        .open = scx200_wdt_open,
 206        .release = scx200_wdt_release,
 207};
 208
 209static struct miscdevice scx200_wdt_miscdev = {
 210        .minor = WATCHDOG_MINOR,
 211        .name = "watchdog",
 212        .fops = &scx200_wdt_fops,
 213};
 214
 215static int __init scx200_wdt_init(void)
 216{
 217        int r;
 218
 219        pr_debug("NatSemi SCx200 Watchdog Driver\n");
 220
 221        /* check that we have found the configuration block */
 222        if (!scx200_cb_present())
 223                return -ENODEV;
 224
 225        if (!request_region(scx200_cb_base + SCx200_WDT_OFFSET,
 226                            SCx200_WDT_SIZE,
 227                            "NatSemi SCx200 Watchdog")) {
 228                pr_warn("watchdog I/O region busy\n");
 229                return -EBUSY;
 230        }
 231
 232        scx200_wdt_update_margin();
 233        scx200_wdt_disable();
 234
 235        r = register_reboot_notifier(&scx200_wdt_notifier);
 236        if (r) {
 237                pr_err("unable to register reboot notifier\n");
 238                release_region(scx200_cb_base + SCx200_WDT_OFFSET,
 239                                SCx200_WDT_SIZE);
 240                return r;
 241        }
 242
 243        r = misc_register(&scx200_wdt_miscdev);
 244        if (r) {
 245                unregister_reboot_notifier(&scx200_wdt_notifier);
 246                release_region(scx200_cb_base + SCx200_WDT_OFFSET,
 247                                SCx200_WDT_SIZE);
 248                return r;
 249        }
 250
 251        return 0;
 252}
 253
 254static void __exit scx200_wdt_cleanup(void)
 255{
 256        misc_deregister(&scx200_wdt_miscdev);
 257        unregister_reboot_notifier(&scx200_wdt_notifier);
 258        release_region(scx200_cb_base + SCx200_WDT_OFFSET,
 259                       SCx200_WDT_SIZE);
 260}
 261
 262module_init(scx200_wdt_init);
 263module_exit(scx200_wdt_cleanup);
 264