// SPDX-License-Identifier: GPL-2.0+ /* * exar_wdt.c - Driver for the watchdog present in some * Exar/MaxLinear UART chips like the XR28V38x. * * (c) Copyright 2022 D. Müller . * */ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include #include #include #include #include #include #define DRV_NAME "exar_wdt" static const unsigned short sio_config_ports[] = { 0x2e, 0x4e }; static const unsigned char sio_enter_keys[] = { 0x67, 0x77, 0x87, 0xA0 }; #define EXAR_EXIT_KEY 0xAA #define EXAR_LDN 0x07 #define EXAR_DID 0x20 #define EXAR_VID 0x23 #define EXAR_WDT 0x26 #define EXAR_ACT 0x30 #define EXAR_RTBASE 0x60 #define EXAR_WDT_LDEV 0x08 #define EXAR_VEN_ID 0x13A8 #define EXAR_DEV_382 0x0382 #define EXAR_DEV_384 0x0384 /* WDT runtime registers */ #define WDT_CTRL 0x00 #define WDT_VAL 0x01 #define WDT_UNITS_10MS 0x0 /* the 10 millisec unit of the HW is not used */ #define WDT_UNITS_SEC 0x2 #define WDT_UNITS_MIN 0x4 /* default WDT control for WDTOUT signal activ / rearm by read */ #define EXAR_WDT_DEF_CONF 0 struct wdt_pdev_node { struct list_head list; struct platform_device *pdev; const char name[16]; }; struct wdt_priv { /* the lock for WDT io operations */ spinlock_t io_lock; struct resource wdt_res; struct watchdog_device wdt_dev; unsigned short did; unsigned short config_port; unsigned char enter_key; unsigned char unit; unsigned char timeout; }; #define WATCHDOG_TIMEOUT 60 static int timeout = WATCHDOG_TIMEOUT; module_param(timeout, int, 0); MODULE_PARM_DESC(timeout, "Watchdog timeout in seconds. 1<=timeout<=15300, default=" __MODULE_STRING(WATCHDOG_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) ")"); static int exar_sio_enter(const unsigned short config_port, const unsigned char key) { if (!request_muxed_region(config_port, 2, DRV_NAME)) return -EBUSY; /* write the ENTER-KEY twice */ outb(key, config_port); outb(key, config_port); return 0; } static void exar_sio_exit(const unsigned short config_port) { outb(EXAR_EXIT_KEY, config_port); release_region(config_port, 2); } static unsigned char exar_sio_read(const unsigned short config_port, const unsigned char reg) { outb(reg, config_port); return inb(config_port + 1); } static void exar_sio_write(const unsigned short config_port, const unsigned char reg, const unsigned char val) { outb(reg, config_port); outb(val, config_port + 1); } static unsigned short exar_sio_read16(const unsigned short config_port, const unsigned char reg) { unsigned char msb, lsb; msb = exar_sio_read(config_port, reg); lsb = exar_sio_read(config_port, reg + 1); return (msb << 8) | lsb; } static void exar_sio_select_wdt(const unsigned short config_port) { exar_sio_write(config_port, EXAR_LDN, EXAR_WDT_LDEV); } static void exar_wdt_arm(const struct wdt_priv *priv) { unsigned short rt_base = priv->wdt_res.start; /* write timeout value twice to arm watchdog */ outb(priv->timeout, rt_base + WDT_VAL); outb(priv->timeout, rt_base + WDT_VAL); } static void exar_wdt_disarm(const struct wdt_priv *priv) { unsigned short rt_base = priv->wdt_res.start; /* * use two accesses with different values to make sure * that a combination of a previous single access and * the ones below with the same value are not falsely * interpreted as "arm watchdog" */ outb(0xFF, rt_base + WDT_VAL); outb(0, rt_base + WDT_VAL); } static int exar_wdt_start(struct watchdog_device *wdog) { struct wdt_priv *priv = watchdog_get_drvdata(wdog); unsigned short rt_base = priv->wdt_res.start; spin_lock(&priv->io_lock); exar_wdt_disarm(priv); outb(priv->unit, rt_base + WDT_CTRL); exar_wdt_arm(priv); spin_unlock(&priv->io_lock); return 0; } static int exar_wdt_stop(struct watchdog_device *wdog) { struct wdt_priv *priv = watchdog_get_drvdata(wdog); spin_lock(&priv->io_lock); exar_wdt_disarm(priv); spin_unlock(&priv->io_lock); return 0; } static int exar_wdt_keepalive(struct watchdog_device *wdog) { struct wdt_priv *priv = watchdog_get_drvdata(wdog); unsigned short rt_base = priv->wdt_res.start; spin_lock(&priv->io_lock); /* reading the WDT_VAL reg will feed the watchdog */ inb(rt_base + WDT_VAL); spin_unlock(&priv->io_lock); return 0; } static int exar_wdt_set_timeout(struct watchdog_device *wdog, unsigned int t) { struct wdt_priv *priv = watchdog_get_drvdata(wdog); bool unit_min = false; /* * if new timeout is bigger then 255 seconds, change the * unit to minutes and round the timeout up to the next whole minute */ if (t > 255) { unit_min = true; t = DIV_ROUND_UP(t, 60); } /* save for later use in exar_wdt_start() */ priv->unit = unit_min ? WDT_UNITS_MIN : WDT_UNITS_SEC; priv->timeout = t; wdog->timeout = unit_min ? t * 60 : t; if (watchdog_hw_running(wdog)) exar_wdt_start(wdog); return 0; } static const struct watchdog_info exar_wdt_info = { .options = WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT | WDIOF_MAGICCLOSE, .identity = "Exar/MaxLinear XR28V38x Watchdog", }; static const struct watchdog_ops exar_wdt_ops = { .owner = THIS_MODULE, .start = exar_wdt_start, .stop = exar_wdt_stop, .ping = exar_wdt_keepalive, .set_timeout = exar_wdt_set_timeout, }; static int exar_wdt_config(struct watchdog_device *wdog, const unsigned char conf) { struct wdt_priv *priv = watchdog_get_drvdata(wdog); int ret; ret = exar_sio_enter(priv->config_port, priv->enter_key); if (ret) return ret; exar_sio_select_wdt(priv->config_port); exar_sio_write(priv->config_port, EXAR_WDT, conf); exar_sio_exit(priv->config_port); return 0; } static int __init exar_wdt_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct wdt_priv *priv = dev->platform_data; struct watchdog_device *wdt_dev = &priv->wdt_dev; struct resource *res; int ret; res = platform_get_resource(pdev, IORESOURCE_IO, 0); if (!res) return -ENXIO; spin_lock_init(&priv->io_lock); wdt_dev->info = &exar_wdt_info; wdt_dev->ops = &exar_wdt_ops; wdt_dev->min_timeout = 1; wdt_dev->max_timeout = 255 * 60; watchdog_init_timeout(wdt_dev, timeout, NULL); watchdog_set_nowayout(wdt_dev, nowayout); watchdog_stop_on_reboot(wdt_dev); watchdog_stop_on_unregister(wdt_dev); watchdog_set_drvdata(wdt_dev, priv); ret = exar_wdt_config(wdt_dev, EXAR_WDT_DEF_CONF); if (ret) return ret; exar_wdt_set_timeout(wdt_dev, timeout); /* Make sure that the watchdog is not running */ exar_wdt_stop(wdt_dev); ret = devm_watchdog_register_device(dev, wdt_dev); if (ret) return ret; dev_info(dev, "XR28V%X WDT initialized. timeout=%d sec (nowayout=%d)\n", priv->did, timeout, nowayout); return 0; } static unsigned short __init exar_detect(const unsigned short config_port, const unsigned char key, unsigned short *rt_base) { int ret; unsigned short base = 0; unsigned short vid, did; ret = exar_sio_enter(config_port, key); if (ret) return 0; vid = exar_sio_read16(config_port, EXAR_VID); did = exar_sio_read16(config_port, EXAR_DID); /* check for the vendor and device IDs we currently know about */ if (vid == EXAR_VEN_ID && (did == EXAR_DEV_382 || did == EXAR_DEV_384)) { exar_sio_select_wdt(config_port); /* is device active? */ if (exar_sio_read(config_port, EXAR_ACT) == 0x01) base = exar_sio_read16(config_port, EXAR_RTBASE); } exar_sio_exit(config_port); if (base) { pr_debug("Found a XR28V%X WDT (conf: 0x%x / rt: 0x%04x)\n", did, config_port, base); *rt_base = base; return did; } return 0; } static struct platform_driver exar_wdt_driver = { .driver = { .name = DRV_NAME, }, }; static LIST_HEAD(pdev_list); static int __init exar_wdt_register(struct wdt_priv *priv, const int idx) { struct wdt_pdev_node *n; n = kzalloc(sizeof(*n), GFP_KERNEL); if (!n) return -ENOMEM; INIT_LIST_HEAD(&n->list); scnprintf((char *)n->name, sizeof(n->name), DRV_NAME ".%d", idx); priv->wdt_res.name = n->name; n->pdev = platform_device_register_resndata(NULL, DRV_NAME, idx, &priv->wdt_res, 1, priv, sizeof(*priv)); if (IS_ERR(n->pdev)) { int err = PTR_ERR(n->pdev); kfree(n); return err; } list_add_tail(&n->list, &pdev_list); return 0; } static void exar_wdt_unregister(void) { struct wdt_pdev_node *n, *t; list_for_each_entry_safe(n, t, &pdev_list, list) { platform_device_unregister(n->pdev); list_del(&n->list); kfree(n); } } static int __init exar_wdt_init(void) { int ret, i, j, idx = 0; /* search for active Exar watchdogs on all possible locations */ for (i = 0; i < ARRAY_SIZE(sio_config_ports); i++) { for (j = 0; j < ARRAY_SIZE(sio_enter_keys); j++) { unsigned short did, rt_base = 0; did = exar_detect(sio_config_ports[i], sio_enter_keys[j], &rt_base); if (did) { struct wdt_priv priv = { .wdt_res = DEFINE_RES_IO(rt_base, 2), .did = did, .config_port = sio_config_ports[i], .enter_key = sio_enter_keys[j], }; ret = exar_wdt_register(&priv, idx); if (!ret) idx++; } } } if (!idx) return -ENODEV; ret = platform_driver_probe(&exar_wdt_driver, exar_wdt_probe); if (ret) exar_wdt_unregister(); return ret; } static void __exit exar_wdt_exit(void) { exar_wdt_unregister(); platform_driver_unregister(&exar_wdt_driver); } module_init(exar_wdt_init); module_exit(exar_wdt_exit); MODULE_AUTHOR("David Müller "); MODULE_DESCRIPTION("Exar/MaxLinear Watchdog Driver"); MODULE_LICENSE("GPL");