Contributors: 3
Author Tokens Token Proportion Commits Commit Proportion
Mark Pearson 1730 99.20% 1 33.33%
Henry Martin 11 0.63% 1 33.33%
Stephen Rothwell 3 0.17% 1 33.33%
Total 1744 3


// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * WDT driver for Lenovo SE30 device
 */

#define dev_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/io.h>
#include <linux/dmi.h>
#include <linux/delay.h>
#include <linux/iommu.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/platform_device.h>
#include <linux/watchdog.h>

#define IOREGION_OFFSET	4 /* Use EC port 1 */
#define IOREGION_LENGTH	4

#define WATCHDOG_TIMEOUT	60

#define MIN_TIMEOUT	1
#define MAX_TIMEOUT	255
#define MAX_WAIT	10

static int timeout; /* in seconds */
module_param(timeout, int, 0);
MODULE_PARM_DESC(timeout,
		 "Watchdog timeout in seconds. 1 <= timeout <= 255, 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) ")");

#define LNV_SE30_NAME	"lenovo-se30-wdt"
#define LNV_SE30_ID	0x0110
#define CHIPID_MASK	0xFFF0

#define CHIPID_REG	0x20
#define SIO_REG		0x2e
#define LDN_REG		0x07
#define UNLOCK_KEY	0x87
#define LOCK_KEY	0xAA
#define LD_NUM_SHM	0x0F
#define LD_BASE_ADDR	0xF8

#define WDT_MODULE	0x10
#define WDT_CFG_INDEX	0x15 /* WD configuration register */
#define WDT_CNT_INDEX	0x16 /* WD timer count register */
#define WDT_CFG_RESET	0x2

/* Host Interface WIN2 offset definition */
#define SHM_WIN_SIZE		0xFF
#define SHM_WIN_MOD_OFFSET	0x01
#define SHM_WIN_CMD_OFFSET	0x02
#define SHM_WIN_SEL_OFFSET	0x03
#define SHM_WIN_CTL_OFFSET	0x04
#define VAL_SHM_WIN_CTRL_WR	0x40
#define VAL_SHM_WIN_CTRL_RD	0x80
#define SHM_WIN_ID_OFFSET	0x08
#define SHM_WIN_DAT_OFFSET	0x10

struct nct6692_reg {
	unsigned char mod;
	unsigned char cmd;
	unsigned char sel;
	unsigned int idx;
};

/* Watchdog is based on NCT6692 device */
struct lenovo_se30_wdt {
	unsigned char __iomem *shm_base_addr;
	struct nct6692_reg wdt_cfg;
	struct nct6692_reg wdt_cnt;
	struct watchdog_device wdt;
};

static inline void superio_outb(int ioreg, int reg, int val)
{
	outb(reg, ioreg);
	outb(val, ioreg + 1);
}

static inline int superio_inb(int ioreg, int reg)
{
	outb(reg, ioreg);
	return inb(ioreg + 1);
}

static inline int superio_enter(int key, int addr, const char *name)
{
	if (!request_muxed_region(addr, 2, name)) {
		pr_err("I/O address 0x%04x already in use\n", addr);
		return -EBUSY;
	}
	outb(key, addr); /* Enter extended function mode */
	outb(key, addr); /* Again according to manual */

	return 0;
}

static inline void superio_exit(int key, int addr)
{
	outb(key, addr); /* Leave extended function mode */
	release_region(addr, 2);
}

static int shm_get_ready(unsigned char __iomem *shm_base_addr,
			 const struct nct6692_reg *reg)
{
	unsigned char pre_id, new_id;
	int loop = 0;

	iowrite8(reg->mod, shm_base_addr + SHM_WIN_MOD_OFFSET);
	iowrite8(reg->cmd, shm_base_addr + SHM_WIN_CMD_OFFSET);
	iowrite8(reg->sel, shm_base_addr + SHM_WIN_SEL_OFFSET);

	pre_id = ioread8(shm_base_addr + SHM_WIN_ID_OFFSET);
	iowrite8(VAL_SHM_WIN_CTRL_RD, shm_base_addr + SHM_WIN_CTL_OFFSET);

	/* Loop checking when interface is ready */
	while (loop < MAX_WAIT) {
		new_id = ioread8(shm_base_addr + SHM_WIN_ID_OFFSET);
		if (new_id != pre_id)
			return 0;
		loop++;
		usleep_range(10, 125);
	}
	return -ETIMEDOUT;
}

static int read_shm_win(unsigned char __iomem *shm_base_addr,
			const struct nct6692_reg *reg,
			unsigned char idx_offset,
			unsigned char *data)
{
	int err = shm_get_ready(shm_base_addr, reg);

	if (err)
		return err;
	*data = ioread8(shm_base_addr + SHM_WIN_DAT_OFFSET + reg->idx + idx_offset);
	return 0;
}

static int write_shm_win(unsigned char __iomem *shm_base_addr,
			 const struct nct6692_reg *reg,
			 unsigned char idx_offset,
			 unsigned char val)
{
	int err = shm_get_ready(shm_base_addr, reg);

	if (err)
		return err;
	iowrite8(val, shm_base_addr + SHM_WIN_DAT_OFFSET + reg->idx + idx_offset);
	iowrite8(VAL_SHM_WIN_CTRL_WR, shm_base_addr + SHM_WIN_CTL_OFFSET);
	err = shm_get_ready(shm_base_addr, reg);
	return err;
}

static int lenovo_se30_wdt_enable(struct lenovo_se30_wdt *data, unsigned int timeout)
{
	if (timeout) {
		int err = write_shm_win(data->shm_base_addr, &data->wdt_cfg, 0, WDT_CFG_RESET);

		if (err)
			return err;
	}
	return write_shm_win(data->shm_base_addr, &data->wdt_cnt, 0, timeout);
}

static int lenovo_se30_wdt_start(struct watchdog_device *wdog)
{
	struct lenovo_se30_wdt *data = watchdog_get_drvdata(wdog);

	return lenovo_se30_wdt_enable(data, wdog->timeout);
}

static int lenovo_se30_wdt_stop(struct watchdog_device *wdog)
{
	struct lenovo_se30_wdt *data = watchdog_get_drvdata(wdog);

	return lenovo_se30_wdt_enable(data, 0);
}

static unsigned int lenovo_se30_wdt_get_timeleft(struct watchdog_device *wdog)
{
	struct lenovo_se30_wdt *data = watchdog_get_drvdata(wdog);
	unsigned char timeleft;
	int err;

	err = read_shm_win(data->shm_base_addr, &data->wdt_cnt, 0, &timeleft);
	if (err)
		return 0;
	return timeleft;
}

static int lenovo_se30_wdt_ping(struct watchdog_device *wdt)
{
	struct lenovo_se30_wdt *data = watchdog_get_drvdata(wdt);
	int err = 0;

	/*
	 * Device does not support refreshing WDT_TIMER_REG register when
	 * the watchdog is active.  Need to disable, feed and enable again
	 */
	err = lenovo_se30_wdt_enable(data, 0);
	if (err)
		return err;

	err = write_shm_win(data->shm_base_addr, &data->wdt_cnt, 0, wdt->timeout);
	if (!err)
		err = lenovo_se30_wdt_enable(data, wdt->timeout);

	return err;
}

static const struct watchdog_info lenovo_se30_wdt_info = {
	.options	= WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING |
			  WDIOF_MAGICCLOSE,
	.identity	= "Lenovo SE30 watchdog",
};

static const struct watchdog_ops lenovo_se30_wdt_ops = {
	.owner		= THIS_MODULE,
	.start		= lenovo_se30_wdt_start,
	.stop		= lenovo_se30_wdt_stop,
	.ping		= lenovo_se30_wdt_ping,
	.get_timeleft	= lenovo_se30_wdt_get_timeleft,
};

static int lenovo_se30_wdt_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct lenovo_se30_wdt *priv;
	unsigned long base_phys;
	unsigned short val;
	int err;

	err = superio_enter(UNLOCK_KEY, SIO_REG, LNV_SE30_NAME);
	if (err)
		return err;

	val = superio_inb(SIO_REG, CHIPID_REG) << 8;
	val |= superio_inb(SIO_REG, CHIPID_REG + 1);

	if ((val & CHIPID_MASK) != LNV_SE30_ID) {
		superio_exit(LOCK_KEY, SIO_REG);
		return -ENODEV;
	}

	superio_outb(SIO_REG, LDN_REG, LD_NUM_SHM);
	base_phys = (superio_inb(SIO_REG, LD_BASE_ADDR) |
			 (superio_inb(SIO_REG, LD_BASE_ADDR + 1) << 8) |
			 (superio_inb(SIO_REG, LD_BASE_ADDR + 2) << 16) |
			 (superio_inb(SIO_REG, LD_BASE_ADDR + 3) << 24)) &
			0xFFFFFFFF;

	superio_exit(LOCK_KEY, SIO_REG);
	if (base_phys == 0xFFFFFFFF || base_phys == 0)
		return -ENODEV;

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

	if (!devm_request_mem_region(dev, base_phys, SHM_WIN_SIZE, LNV_SE30_NAME))
		return -EBUSY;

	priv->shm_base_addr = devm_ioremap(dev, base_phys, SHM_WIN_SIZE);
	if (!priv->shm_base_addr)
		return -ENOMEM;

	priv->wdt_cfg.mod = WDT_MODULE;
	priv->wdt_cfg.idx = WDT_CFG_INDEX;
	priv->wdt_cnt.mod = WDT_MODULE;
	priv->wdt_cnt.idx = WDT_CNT_INDEX;

	priv->wdt.ops = &lenovo_se30_wdt_ops;
	priv->wdt.info = &lenovo_se30_wdt_info;
	priv->wdt.timeout = WATCHDOG_TIMEOUT; /* Set default timeout */
	priv->wdt.min_timeout = MIN_TIMEOUT;
	priv->wdt.max_timeout = MAX_TIMEOUT;
	priv->wdt.parent = dev;

	watchdog_init_timeout(&priv->wdt, timeout, dev);
	watchdog_set_drvdata(&priv->wdt, priv);
	watchdog_set_nowayout(&priv->wdt, nowayout);
	watchdog_stop_on_reboot(&priv->wdt);
	watchdog_stop_on_unregister(&priv->wdt);

	return devm_watchdog_register_device(dev, &priv->wdt);
}

static struct platform_device *pdev;

static struct platform_driver lenovo_se30_wdt_driver = {
	.driver = {
		.name = LNV_SE30_NAME,
	},
	.probe  = lenovo_se30_wdt_probe,
};

static int lenovo_se30_create_platform_device(const struct dmi_system_id *id)
{
	int err;

	pdev = platform_device_alloc(LNV_SE30_NAME, -1);
	if (!pdev)
		return -ENOMEM;

	err = platform_device_add(pdev);
	if (err)
		platform_device_put(pdev);

	return err;
}

static const struct dmi_system_id lenovo_se30_wdt_dmi_table[] __initconst = {
	{
		.ident = "LENOVO-SE30",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "11NA"),
		},
		.callback = lenovo_se30_create_platform_device,
	},
	{
		.ident = "LENOVO-SE30",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "11NB"),
		},
		.callback = lenovo_se30_create_platform_device,
	},
	{
		.ident = "LENOVO-SE30",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "11NC"),
		},
		.callback = lenovo_se30_create_platform_device,
	},
	{
		.ident = "LENOVO-SE30",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "11NH"),
		},
		.callback = lenovo_se30_create_platform_device,
	},
	{
		.ident = "LENOVO-SE30",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "11NJ"),
		},
		.callback = lenovo_se30_create_platform_device,
	},
	{
		.ident = "LENOVO-SE30",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "11NK"),
		},
		.callback = lenovo_se30_create_platform_device,
	},
	{}
};
MODULE_DEVICE_TABLE(dmi, lenovo_se30_wdt_dmi_table);

static int __init lenovo_se30_wdt_init(void)
{
	if (!dmi_check_system(lenovo_se30_wdt_dmi_table))
		return -ENODEV;

	return platform_driver_register(&lenovo_se30_wdt_driver);
}

static void __exit lenovo_se30_wdt_exit(void)
{
	if (pdev)
		platform_device_unregister(pdev);
	platform_driver_unregister(&lenovo_se30_wdt_driver);
}

module_init(lenovo_se30_wdt_init);
module_exit(lenovo_se30_wdt_exit);

MODULE_AUTHOR("Mark Pearson <mpearson-lenovo@squebb.ca>");
MODULE_AUTHOR("David Ober <dober@lenovo.com>");
MODULE_DESCRIPTION("Lenovo SE30 watchdog driver");
MODULE_LICENSE("GPL");