root/drivers/watchdog/nic7018_wdt.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2016 National Instruments Corp.
 */

#include <linux/bitops.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/types.h>
#include <linux/watchdog.h>

#define LOCK                    0xA5
#define UNLOCK                  0x5A

#define WDT_CTRL_RESET_EN       BIT(7)
#define WDT_RELOAD_PORT_EN      BIT(7)

#define WDT_CTRL                1
#define WDT_RELOAD_CTRL         2
#define WDT_PRESET_PRESCALE     4
#define WDT_REG_LOCK            5
#define WDT_COUNT               6
#define WDT_RELOAD_PORT         7

#define WDT_MIN_TIMEOUT         1
#define WDT_MAX_TIMEOUT         464
#define WDT_DEFAULT_TIMEOUT     80

#define WDT_MAX_COUNTER         15

static unsigned int timeout;
module_param(timeout, uint, 0);
MODULE_PARM_DESC(timeout,
                 "Watchdog timeout in seconds. (default="
                 __MODULE_STRING(WDT_DEFAULT_TIMEOUT) ")");

static bool nowayout = WATCHDOG_NOWAYOUT;
module_param(nowayout, bool, 0);
MODULE_PARM_DESC(nowayout,
                 "Watchdog cannot be stopped once started. (default="
                 __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");

struct nic7018_wdt {
        u16 io_base;
        u32 period;
        struct watchdog_device wdd;
};

struct nic7018_config {
        u32 period;
        u8 divider;
};

static const struct nic7018_config nic7018_configs[] = {
        {  2, 4 },
        { 32, 5 },
};

static inline u32 nic7018_timeout(u32 period, u8 counter)
{
        return period * counter - period / 2;
}

static const struct nic7018_config *nic7018_get_config(u32 timeout,
                                                       u8 *counter)
{
        const struct nic7018_config *config;
        u8 count;

        if (timeout < 30 && timeout != 16) {
                config = &nic7018_configs[0];
                count = timeout / 2 + 1;
        } else {
                config = &nic7018_configs[1];
                count = DIV_ROUND_UP(timeout + 16, 32);

                if (count > WDT_MAX_COUNTER)
                        count = WDT_MAX_COUNTER;
        }
        *counter = count;
        return config;
}

static int nic7018_set_timeout(struct watchdog_device *wdd,
                               unsigned int timeout)
{
        struct nic7018_wdt *wdt = watchdog_get_drvdata(wdd);
        const struct nic7018_config *config;
        u8 counter;

        config = nic7018_get_config(timeout, &counter);

        outb(counter << 4 | config->divider,
             wdt->io_base + WDT_PRESET_PRESCALE);

        wdd->timeout = nic7018_timeout(config->period, counter);
        wdt->period = config->period;

        return 0;
}

static int nic7018_start(struct watchdog_device *wdd)
{
        struct nic7018_wdt *wdt = watchdog_get_drvdata(wdd);
        u8 control;

        nic7018_set_timeout(wdd, wdd->timeout);

        control = inb(wdt->io_base + WDT_RELOAD_CTRL);
        outb(control | WDT_RELOAD_PORT_EN, wdt->io_base + WDT_RELOAD_CTRL);

        outb(1, wdt->io_base + WDT_RELOAD_PORT);

        control = inb(wdt->io_base + WDT_CTRL);
        outb(control | WDT_CTRL_RESET_EN, wdt->io_base + WDT_CTRL);

        return 0;
}

static int nic7018_stop(struct watchdog_device *wdd)
{
        struct nic7018_wdt *wdt = watchdog_get_drvdata(wdd);

        outb(0, wdt->io_base + WDT_CTRL);
        outb(0, wdt->io_base + WDT_RELOAD_CTRL);
        outb(0xF0, wdt->io_base + WDT_PRESET_PRESCALE);

        return 0;
}

static int nic7018_ping(struct watchdog_device *wdd)
{
        struct nic7018_wdt *wdt = watchdog_get_drvdata(wdd);

        outb(1, wdt->io_base + WDT_RELOAD_PORT);

        return 0;
}

static unsigned int nic7018_get_timeleft(struct watchdog_device *wdd)
{
        struct nic7018_wdt *wdt = watchdog_get_drvdata(wdd);
        u8 count;

        count = inb(wdt->io_base + WDT_COUNT) & 0xF;
        if (!count)
                return 0;

        return nic7018_timeout(wdt->period, count);
}

static const struct watchdog_info nic7018_wdd_info = {
        .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE,
        .identity = "NIC7018 Watchdog",
};

static const struct watchdog_ops nic7018_wdd_ops = {
        .owner = THIS_MODULE,
        .start = nic7018_start,
        .stop = nic7018_stop,
        .ping = nic7018_ping,
        .set_timeout = nic7018_set_timeout,
        .get_timeleft = nic7018_get_timeleft,
};

static int nic7018_probe(struct platform_device *pdev)
{
        struct device *dev = &pdev->dev;
        struct watchdog_device *wdd;
        struct nic7018_wdt *wdt;
        struct resource *io_rc;
        int ret;

        wdt = devm_kzalloc(dev, sizeof(*wdt), GFP_KERNEL);
        if (!wdt)
                return -ENOMEM;

        platform_set_drvdata(pdev, wdt);

        io_rc = platform_get_resource(pdev, IORESOURCE_IO, 0);
        if (!io_rc) {
                dev_err(dev, "missing IO resources\n");
                return -EINVAL;
        }

        if (!devm_request_region(dev, io_rc->start, resource_size(io_rc),
                                 KBUILD_MODNAME)) {
                dev_err(dev, "failed to get IO region\n");
                return -EBUSY;
        }

        wdt->io_base = io_rc->start;
        wdd = &wdt->wdd;
        wdd->info = &nic7018_wdd_info;
        wdd->ops = &nic7018_wdd_ops;
        wdd->min_timeout = WDT_MIN_TIMEOUT;
        wdd->max_timeout = WDT_MAX_TIMEOUT;
        wdd->timeout = WDT_DEFAULT_TIMEOUT;
        wdd->parent = dev;

        watchdog_set_drvdata(wdd, wdt);
        watchdog_set_nowayout(wdd, nowayout);
        watchdog_init_timeout(wdd, timeout, dev);

        /* Unlock WDT register */
        outb(UNLOCK, wdt->io_base + WDT_REG_LOCK);

        ret = watchdog_register_device(wdd);
        if (ret) {
                outb(LOCK, wdt->io_base + WDT_REG_LOCK);
                return ret;
        }

        dev_dbg(dev, "io_base=0x%04X, timeout=%d, nowayout=%d\n",
                wdt->io_base, timeout, nowayout);
        return 0;
}

static void nic7018_remove(struct platform_device *pdev)
{
        struct nic7018_wdt *wdt = platform_get_drvdata(pdev);

        watchdog_unregister_device(&wdt->wdd);

        /* Lock WDT register */
        outb(LOCK, wdt->io_base + WDT_REG_LOCK);
}

static const struct acpi_device_id nic7018_device_ids[] = {
        { "NIC7018" },
        { }
};
MODULE_DEVICE_TABLE(acpi, nic7018_device_ids);

static struct platform_driver watchdog_driver = {
        .probe = nic7018_probe,
        .remove = nic7018_remove,
        .driver = {
                .name = KBUILD_MODNAME,
                .acpi_match_table = nic7018_device_ids,
        },
};

module_platform_driver(watchdog_driver);

MODULE_DESCRIPTION("National Instruments NIC7018 Watchdog driver");
MODULE_AUTHOR("Hui Chun Ong <hui.chun.ong@ni.com>");
MODULE_LICENSE("GPL");