[PATCH] hwmon: add in watchdog for nct6686

From: David Ober
Date: Tue Aug 15 2023 - 07:58:10 EST


This change adds in the watchdog timer support for the nct6686
chip so that it can be used on the Lenovo m90n IOT device

Signed-off-by: David Ober <dober6023@xxxxxxxxx>
---
Documentation/hwmon/nct6683.rst | 5 +-
drivers/hwmon/nct6683.c | 247 ++++++++++++++++++++++++++++++--
2 files changed, 242 insertions(+), 10 deletions(-)

diff --git a/Documentation/hwmon/nct6683.rst b/Documentation/hwmon/nct6683.rst
index 2e1408d174bd..7421bc444365 100644
--- a/Documentation/hwmon/nct6683.rst
+++ b/Documentation/hwmon/nct6683.rst
@@ -3,7 +3,7 @@ Kernel driver nct6683

Supported chips:

- * Nuvoton NCT6683D/NCT6687D
+ * Nuvoton NCT6683D/NCT6686D/NCT6687D

Prefix: 'nct6683'

@@ -49,6 +49,8 @@ The driver has only been tested with the Intel firmware, and by default
only instantiates on Intel boards. To enable it on non-Intel boards,
set the 'force' module parameter to 1.

+Implement the watchdog functionality of the NCT6686D eSIO chip
+
Tested Boards and Firmware Versions
-----------------------------------

@@ -63,4 +65,5 @@ Intel DH87MC NCT6683D EC firmware version 1.0 build 04/03/13
Intel DB85FL NCT6683D EC firmware version 1.0 build 04/03/13
ASRock X570 NCT6683D EC firmware version 1.0 build 06/28/19
MSI B550 NCT6687D EC firmware version 1.0 build 05/07/20
+LENOVO M90n-1 NCT6686D EC firmware version 9.0 build 04/21/21
=============== ===============================================
diff --git a/drivers/hwmon/nct6683.c b/drivers/hwmon/nct6683.c
index f673f7d07941..eb95b91c4d39 100644
--- a/drivers/hwmon/nct6683.c
+++ b/drivers/hwmon/nct6683.c
@@ -24,15 +24,16 @@
#include <linux/acpi.h>
#include <linux/delay.h>
#include <linux/err.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/jiffies.h>
-#include <linux/hwmon.h>
-#include <linux/hwmon-sysfs.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
+#include <linux/watchdog.h>

enum kinds { nct6683, nct6686, nct6687 };

@@ -73,6 +74,34 @@ static const char * const nct6683_chip_names[] = {
#define SIO_NCT6687_ID 0xd590
#define SIO_ID_MASK 0xFFF0

+#define WDT_CFG 0x828 /* W/O Lock Watchdog Register */
+#define WDT_CNT 0x829 /* R/W Watchdog Timer Register */
+#define WDT_STS 0x82A /* R/O Watchdog Status Register */
+#define WDT_STS_EVT_POS (0)
+#define WDT_STS_EVT_MSK (0x3 << WDT_STS_EVT_POS)
+#define WDT_SOFT_EN 0x87 /* Enable soft watchdog timer */
+#define WDT_SOFT_DIS 0xAA /* Disable soft watchdog timer */
+
+#define WATCHDOG_TIMEOUT 60 /* 1 minute default timeout */
+
+/* The timeout range is 1-255 seconds */
+#define MIN_TIMEOUT 1
+#define MAX_TIMEOUT 255
+
+static int timeout;
+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) ")");
+
+static int early_disable;
+module_param(early_disable, int, 0);
+MODULE_PARM_DESC(early_disable, "Disable watchdog at boot time (default=0)");
+
static inline void
superio_outb(int ioreg, int reg, int val)
{
@@ -171,10 +200,10 @@ superio_exit(int ioreg)

#define NCT6683_REG_CUSTOMER_ID 0x602
#define NCT6683_CUSTOMER_ID_INTEL 0x805
+#define NCT6683_CUSTOMER_ID_LENOVO 0x1101
#define NCT6683_CUSTOMER_ID_MITAC 0xa0e
#define NCT6683_CUSTOMER_ID_MSI 0x201
-#define NCT6683_CUSTOMER_ID_MSI2 0x200
-#define NCT6683_CUSTOMER_ID_ASROCK 0xe2c
+#define NCT6683_CUSTOMER_ID_ASROCK 0xe2c
#define NCT6683_CUSTOMER_ID_ASROCK2 0xe1b

#define NCT6683_REG_BUILD_YEAR 0x604
@@ -183,6 +212,9 @@ superio_exit(int ioreg)
#define NCT6683_REG_SERIAL 0x607
#define NCT6683_REG_VERSION_HI 0x608
#define NCT6683_REG_VERSION_LO 0x609
+#define NCT6686_PAGE_REG_OFFSET 0
+#define NCT6686_ADDR_REG_OFFSET 1
+#define NCT6686_DATA_REG_OFFSET 2

#define NCT6683_REG_CR_CASEOPEN 0xe8
#define NCT6683_CR_CASEOPEN_MASK (1 << 7)
@@ -304,6 +336,7 @@ struct nct6683_data {

struct device *hwmon_dev;
const struct attribute_group *groups[6];
+ struct watchdog_device wdt;

int temp_num; /* number of temperature attributes */
u8 temp_index[NCT6683_NUM_REG_MON];
@@ -518,6 +551,39 @@ static void nct6683_write(struct nct6683_data *data, u16 reg, u16 value)
outb_p(value & 0xff, data->addr + EC_DATA_REG);
}

+static inline void nct6686_wdt_set_bank(int base_addr, u16 reg)
+{
+ outb_p(0xFF, base_addr + NCT6686_PAGE_REG_OFFSET);
+ outb_p(reg >> 8, base_addr + NCT6686_PAGE_REG_OFFSET);
+}
+
+/* Not strictly necessary, but play it safe for now */
+static inline void nct6686_wdt_reset_bank(int base_addr, u16 reg)
+{
+ if (reg & 0xff00)
+ outb_p(0xFF, base_addr + NCT6686_PAGE_REG_OFFSET);
+}
+
+static u8 nct6686_read(struct nct6683_data *data, u16 reg)
+{
+ u8 res;
+
+ nct6686_wdt_set_bank(data->addr, reg);
+ outb_p(reg & 0xff, data->addr + NCT6686_ADDR_REG_OFFSET);
+ res = inb_p(data->addr + NCT6686_DATA_REG_OFFSET);
+
+ nct6686_wdt_reset_bank(data->addr, reg);
+ return res;
+}
+
+static void nct6686_write(struct nct6683_data *data, u16 reg, u8 value)
+{
+ nct6686_wdt_set_bank(data->addr, reg);
+ outb_p(reg & 0xff, data->addr + NCT6686_ADDR_REG_OFFSET);
+ outb_p(value & 0xff, data->addr + NCT6686_DATA_REG_OFFSET);
+ nct6686_wdt_reset_bank(data->addr, reg);
+}
+
static int get_in_reg(struct nct6683_data *data, int nr, int index)
{
int ch = data->in_index[index];
@@ -680,11 +746,12 @@ static umode_t nct6683_in_is_visible(struct kobject *kobj,
int nr = index % 4; /* attribute */

/*
- * Voltage limits exist for Intel boards,
+ * Voltage limits exist for Intel and Lenovo boards,
* but register location and encoding is unknown
*/
if ((nr == 2 || nr == 3) &&
- data->customer_id == NCT6683_CUSTOMER_ID_INTEL)
+ (data->customer_id == NCT6683_CUSTOMER_ID_INTEL ||
+ data->customer_id == NCT6683_CUSTOMER_ID_LENOVO))
return 0;

return attr->mode;
@@ -1186,6 +1253,139 @@ static void nct6683_setup_sensors(struct nct6683_data *data)
}
}

+/*
+ * Watchdog Functions
+ */
+static int nct6686_wdt_enable(struct watchdog_device *wdog, bool enable)
+{
+ struct nct6683_data *data = watchdog_get_drvdata(wdog);
+
+ u_char reg;
+
+ mutex_lock(&data->update_lock);
+ reg = nct6686_read(data, WDT_CFG);
+
+ if (enable) {
+ nct6686_write(data, WDT_CFG, reg | 0x3);
+ mutex_unlock(&data->update_lock);
+ return 0;
+ }
+
+ nct6686_write(data, WDT_CFG, reg & ~BIT(0));
+ mutex_unlock(&data->update_lock);
+
+ return 0;
+}
+
+static int nct6686_wdt_set_time(struct watchdog_device *wdog)
+{
+ struct nct6683_data *data = watchdog_get_drvdata(wdog);
+
+ mutex_lock(&data->update_lock);
+ nct6686_write(data, WDT_CNT, wdog->timeout);
+ mutex_unlock(&data->update_lock);
+
+ if (wdog->timeout) {
+ nct6686_wdt_enable(wdog, true);
+ return 0;
+ }
+
+ nct6686_wdt_enable(wdog, false);
+ return 0;
+}
+
+static int nct6686_wdt_start(struct watchdog_device *wdt)
+{
+ struct nct6683_data *data = watchdog_get_drvdata(wdt);
+ u_char reg;
+
+ nct6686_wdt_set_time(wdt);
+
+ /* Enable soft watchdog timer */
+ mutex_lock(&data->update_lock);
+ /* reset trigger status */
+ reg = nct6686_read(data, WDT_STS);
+ nct6686_write(data, WDT_STS, reg & ~WDT_STS_EVT_MSK);
+ mutex_unlock(&data->update_lock);
+ return 0;
+}
+
+static int nct6686_wdt_stop(struct watchdog_device *wdt)
+{
+ struct nct6683_data *data = watchdog_get_drvdata(wdt);
+
+ mutex_lock(&data->update_lock);
+ nct6686_write(data, WDT_CFG, WDT_SOFT_DIS);
+ mutex_unlock(&data->update_lock);
+ return 0;
+}
+
+static int nct6686_wdt_set_timeout(struct watchdog_device *wdt,
+ unsigned int timeout)
+{
+ struct nct6683_data *data = watchdog_get_drvdata(wdt);
+
+ wdt->timeout = timeout;
+ mutex_lock(&data->update_lock);
+ nct6686_write(data, WDT_CNT, timeout);
+ mutex_unlock(&data->update_lock);
+ return 0;
+}
+
+static int nct6686_wdt_ping(struct watchdog_device *wdt)
+{
+ struct nct6683_data *data = watchdog_get_drvdata(wdt);
+ int timeout;
+
+ /*
+ * Note:
+ * NCT6686 does not support refreshing WDT_TIMER_REG register when
+ * the watchdog is active. Please disable watchdog before feeding
+ * the watchdog and enable it again.
+ */
+ /* Disable soft watchdog timer */
+ nct6686_wdt_enable(wdt, false);
+
+ /* feed watchdog */
+ timeout = wdt->timeout;
+ mutex_lock(&data->update_lock);
+ nct6686_write(data, WDT_CNT, timeout);
+ mutex_unlock(&data->update_lock);
+
+ /* Enable soft watchdog timer */
+ nct6686_wdt_enable(wdt, true);
+ return 0;
+}
+
+static unsigned int nct6686_wdt_get_timeleft(struct watchdog_device *wdt)
+{
+ struct nct6683_data *data = watchdog_get_drvdata(wdt);
+ int ret;
+
+ mutex_lock(&data->update_lock);
+ ret = nct6686_read(data, WDT_CNT);
+ mutex_unlock(&data->update_lock);
+ if (ret < 0)
+ return 0;
+
+ return ret;
+}
+
+static const struct watchdog_info nct6686_wdt_info = {
+ .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING |
+ WDIOF_MAGICCLOSE,
+ .identity = "nct6686 watchdog",
+};
+
+static const struct watchdog_ops nct6686_wdt_ops = {
+ .owner = THIS_MODULE,
+ .start = nct6686_wdt_start,
+ .stop = nct6686_wdt_stop,
+ .ping = nct6686_wdt_ping,
+ .set_timeout = nct6686_wdt_set_timeout,
+ .get_timeleft = nct6686_wdt_get_timeleft,
+};
+
static int nct6683_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
@@ -1195,7 +1395,9 @@ static int nct6683_probe(struct platform_device *pdev)
struct device *hwmon_dev;
struct resource *res;
int groups = 0;
+ int ret;
char build[16];
+ u_char reg;

res = platform_get_resource(pdev, IORESOURCE_IO, 0);
if (!devm_request_region(dev, res->start, IOREGION_LENGTH, DRVNAME))
@@ -1215,14 +1417,14 @@ static int nct6683_probe(struct platform_device *pdev)

/* By default only instantiate driver if the customer ID is known */
switch (data->customer_id) {
+ case NCT6683_CUSTOMER_ID_LENOVO:
+ break;
case NCT6683_CUSTOMER_ID_INTEL:
break;
case NCT6683_CUSTOMER_ID_MITAC:
break;
case NCT6683_CUSTOMER_ID_MSI:
break;
- case NCT6683_CUSTOMER_ID_MSI2:
- break;
case NCT6683_CUSTOMER_ID_ASROCK:
break;
case NCT6683_CUSTOMER_ID_ASROCK2:
@@ -1294,7 +1496,34 @@ static int nct6683_probe(struct platform_device *pdev)

hwmon_dev = devm_hwmon_device_register_with_groups(dev,
nct6683_device_names[data->kind], data, data->groups);
- return PTR_ERR_OR_ZERO(hwmon_dev);
+
+ ret = PTR_ERR_OR_ZERO(hwmon_dev);
+ if (ret)
+ return ret;
+
+ if (data->kind == nct6686 && data->customer_id == NCT6683_CUSTOMER_ID_LENOVO) {
+ /* Watchdog initialization */
+ data->wdt.ops = &nct6686_wdt_ops;
+ data->wdt.info = &nct6686_wdt_info;
+
+ data->wdt.timeout = WATCHDOG_TIMEOUT; /* Set default timeout */
+ data->wdt.min_timeout = MIN_TIMEOUT;
+ data->wdt.max_timeout = MAX_TIMEOUT;
+ data->wdt.parent = &pdev->dev;
+
+ watchdog_init_timeout(&data->wdt, timeout, &pdev->dev);
+ watchdog_set_nowayout(&data->wdt, nowayout);
+ watchdog_set_drvdata(&data->wdt, data);
+
+ /* reset trigger status */
+ reg = nct6686_read(data, WDT_STS);
+ nct6686_write(data, WDT_STS, reg & ~WDT_STS_EVT_MSK);
+
+ watchdog_stop_on_unregister(&data->wdt);
+
+ return devm_watchdog_register_device(dev, &data->wdt);
+ }
+ return ret;
}

#ifdef CONFIG_PM
--
2.34.1