mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Thomas R <seatommyboy@gmail.com>
To: jdelvare@suse.com, linux@roeck-us.net,
	linux-hwmon@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Driver for Nuvoton NCT6687D eSIO chip
Date: Thu, 20 Jul 2023 06:09:22 -0500	[thread overview]
Message-ID: <b1a0bd3b-a32d-aff0-db4e-d9ba770e3a49@gmail.com> (raw)

[-- Attachment #1: Type: text/plain, Size: 328 bytes --]

To whom it may concern,

I am trying to submit a patch for driver Driver for Nuvoton NCT6687D 
eSIO chip under drivers/hwmon. This driver is confirmed to be working 
for motherboards series B550 and B650.

Credits for creating this driver should go to Frederic BOLTZ 
https://github.com/Fred78290/nct6687d

Attached is my patch

[-- Attachment #2: driver_nct6687.patch --]
[-- Type: text/x-patch, Size: 36570 bytes --]

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 5b3b76477b0e..47a1bcc365f3 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1518,6 +1518,16 @@ config SENSORS_NCT6683
 	  This driver can also be built as a module. If so, the module
 	  will be called nct6683.
 
+config SENSORS_NCT6687
+	tristate "Nuvoton NCT6687D"
+	depends on !PPC
+	help
+	  If you say yes here you get support for the hardware monitoring
+	  functionality of the Nuvoton NCT6687D eSIO chip.
+
+	  This driver can also be built as a module. If so, the module
+	  will be called nct6687.
+
 config SENSORS_NCT6775_CORE
 	tristate
 	select REGMAP
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 88712b5031c8..fb08e00dc931 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -158,6 +158,7 @@ obj-$(CONFIG_SENSORS_MLXREG_FAN) += mlxreg-fan.o
 obj-$(CONFIG_SENSORS_MENF21BMC_HWMON) += menf21bmc_hwmon.o
 obj-$(CONFIG_SENSORS_MR75203)	+= mr75203.o
 obj-$(CONFIG_SENSORS_NCT6683)	+= nct6683.o
+obj-$(CONFIG_SENSORS_NCT6687)	+= nct6687.o
 obj-$(CONFIG_SENSORS_NCT6775_CORE) += nct6775-core.o
 nct6775-objs			:= nct6775-platform.o
 obj-$(CONFIG_SENSORS_NCT6775)	+= nct6775.o
diff --git a/drivers/hwmon/nct6687.c b/drivers/hwmon/nct6687.c
new file mode 100644
index 000000000000..fed2c868fec7
--- /dev/null
+++ b/drivers/hwmon/nct6687.c
@@ -0,0 +1,1272 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * nct6687 - Driver for the hardware monitoring functionality of
+ *	       Nuvoton NCT6687 Super-I/O chips
+ *
+ * Copyright (C) 2020  Frederic Boltz <frederic.boltz@gmail.com>
+ *
+ * Derived from nct6683 driver
+ * Copyright (C) 2013  Guenter Roeck <linux@roeck-us.net>
+ * 
+ * Inspired of LibreHardwareMonitor
+ * https://github.com/LibreHardwareMonitor/LibreHardwareMonitor
+ * 
+ * Supports the following chips:
+ *
+ * Chip       #voltage   #fan    #pwm    #temp  chip ID
+ * nct6683    14(1)      8       8       7      0xc732  (partial support)
+ * nct6686d   21(1)      16      8       32(1)  0xd440
+ * nct6687    14(1)      8       8       7      0xd592
+ *
+ * Notes:
+ *	(1) Total number of voltage and 9 displayed.
+ */
+// #define DEBUG 1
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/acpi.h>
+#include <linux/delay.h>
+#include <linux/err.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>
+
+#define MIN(a,b) (((a)<(b))?(a):(b))
+#define MAX(a,b) (((a)>(b))?(a):(b))
+
+enum kinds
+{
+	nct6683,
+	nct6686,
+	nct6687
+};
+
+static bool force;
+static bool manual;
+
+module_param(force, bool, 0);
+MODULE_PARM_DESC(force, "Set to one to enable support for unknown vendors");
+
+module_param(manual, bool, 0);
+MODULE_PARM_DESC(manual, "Set voltage input and voltage label configured with external sensors file");
+
+static const char *const nct6687_device_names[] = {
+	"nct6683",
+	"nct6686",
+	"nct6687",
+};
+
+static const char *const nct6687_chip_names[] = {
+	"NCT6683D",
+	"NCT6686D",
+	"NCT6687D",
+};
+
+#define DRVNAME "nct6687"
+
+/*
+ * Super-I/O constants and functions
+ */
+
+#define NCT6687_LD_ACPI 0x0a
+#define NCT6687_LD_HWM 0x0b
+#define NCT6687_LD_VID 0x0d
+
+#define SIO_REG_LDSEL 0x07		 /* Logical device select */
+#define SIO_REG_DEVID 0x20		 /* Device ID (2 bytes) */
+#define SIO_REG_DEVREVISION 0x21 /* Device ID (2 bytes) */
+#define SIO_REG_ENABLE 0x30		 /* Logical device enable */
+#define SIO_REG_ADDR 0x60		 /* Logical device address (2 bytes) */
+
+#define SIO_NCT6683D_ID 0xc732
+#define SIO_NCT6686_ID 0xd440
+#define SIO_NCT6686D_ID 0xd441
+#define SIO_NCT6687_ID 0xd451 // 0xd592
+#define SIO_NCT6687D_ID 0xd592
+
+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 void superio_select(int ioreg, int ld)
+{
+	outb(SIO_REG_LDSEL, ioreg);
+	outb(ld, ioreg + 1);
+}
+
+static inline int superio_enter(int ioreg)
+{
+	/*
+	 * Try to reserve <ioreg> and <ioreg + 1> for exclusive access.
+	 */
+	if (!request_muxed_region(ioreg, 2, DRVNAME))
+		return -EBUSY;
+
+	outb(0x87, ioreg);
+	outb(0x87, ioreg);
+
+	return 0;
+}
+
+static inline void superio_exit(int ioreg)
+{
+	outb(0xaa, ioreg);
+	outb(0x02, ioreg);
+	outb(0x02, ioreg + 1);
+
+	release_region(ioreg, 2);
+}
+
+/*
+ * ISA constants
+ */
+
+#define IOREGION_OFFSET 0 /* Use EC port 1 */
+#define IOREGION_LENGTH 4
+
+/* Common and NCT6687 specific data */
+
+#define NCT6687_NUM_REG_VOLTAGE (sizeof(nct6687_voltage_definition) / sizeof(struct voltage_reg))
+#define NCT6687_NUM_REG_TEMP 7
+#define NCT6687_NUM_REG_FAN 8
+#define NCT6687_NUM_REG_PWM 8
+
+#define NCT6687_REG_TEMP(x) (0x100 + (x)*2)
+#define NCT6687_REG_VOLTAGE(x) (0x120 + (x)*2)
+#define NCT6687_REG_FAN_RPM(x) (0x140 + (x)*2)
+#define NCT6687_REG_PWM(x) (0x160 + (x))
+#define NCT6687_REG_PWM_WRITE(x) (0xa28 + (x))
+
+#define NCT6687_HWM_CFG 0x180
+
+#define NCT6687_REG_MON_CFG(x) (0x1a0 + (x))
+#define NCT6687_REG_FANIN_CFG(x) (0xA00 + (x))
+#define NCT6687_REG_FANOUT_CFG(x) (0x1d0 + (x))
+
+#define NCT6687_REG_TEMP_HYST(x) (0x330 + (x))	/* 8 bit */
+#define NCT6687_REG_TEMP_MAX(x) (0x350 + (x))	/* 8 bit */
+#define NCT6687_REG_MON_HIGH(x) (0x370 + (x)*2) /* 8 bit */
+#define NCT6687_REG_MON_LOW(x) (0x371 + (x)*2)	/* 8 bit */
+
+#define NCT6687_REG_FAN_MIN(x) (0x3b8 + (x)*2) /* 16 bit */
+
+#define NCT6687_REG_FAN_CTRL_MODE(x) 0xA00
+#define NCT6687_REG_FAN_PWM_COMMAND(x) 0xA01
+#define NCT6687_FAN_CFG_REQ 0x80
+#define NCT6687_FAN_CFG_DONE 0x40
+
+#define NCT6687_REG_BUILD_YEAR 0x604
+#define NCT6687_REG_BUILD_MONTH 0x605
+#define NCT6687_REG_BUILD_DAY 0x606
+#define NCT6687_REG_SERIAL 0x607
+#define NCT6687_REG_VERSION_HI 0x608
+#define NCT6687_REG_VERSION_LO 0x609
+
+#define NCT6687_REG_CR_CASEOPEN 0xe8
+#define NCT6687_CR_CASEOPEN_MASK (1 << 7)
+
+#define NCT6687_REG_CR_BEEP 0xe0
+#define NCT6687_CR_BEEP_MASK (1 << 6)
+
+#define EC_SPACE_PAGE_REGISTER_OFFSET 0x04
+#define EC_SPACE_INDEX_REGISTER_OFFSET 0x05
+#define EC_SPACE_DATA_REGISTER_OFFSET 0x06
+#define EC_SPACE_PAGE_SELECT 0xFF
+
+struct voltage_reg
+{
+	u16 reg;
+	u16 multiplier;
+	const char *label;
+};
+
+static struct voltage_reg nct6687_voltage_definition[] = {
+	// +12V
+	{
+		.reg = 0,
+		.multiplier = 12,
+		.label = "+12V",
+	},
+	// + 5V
+	{
+		.reg = 1,
+		.multiplier = 5,
+		.label = "+5V",
+	},
+	// +3.3V
+	{
+		.reg = 11,
+		.multiplier = 1,
+		.label = "+3.3V",
+	},
+	// CPU SOC
+	{
+		.reg = 2,
+		.multiplier = 1,
+		.label = "CPU Soc",
+	},
+	// CPU Vcore
+	{
+		.reg = 4,
+		.multiplier = 1,
+		.label = "CPU Vcore",
+	},
+	// CPU 1P8
+	{
+		.reg = 9,
+		.multiplier = 1,
+		.label = "CPU 1P8",
+	},
+	// CPU VDDP
+	{
+		.reg = 10,
+		.multiplier = 1,
+		.label = "CPU VDDP",
+	},
+	// DRAM
+	{
+		.reg = 3,
+		.multiplier = 2,
+		.label = "DRAM",
+	},
+	// Chipset
+	{
+		.reg = 5,
+		.multiplier = 1,
+		.label = "Chipset",
+	},
+
+	// CPU SA
+	{
+		.reg = 6,
+		.multiplier = 1,
+		.label = "CPU SA",
+	},
+	// Voltage #2
+	{
+		.reg = 7,
+		.multiplier = 1,
+		.label = "Voltage #2",
+	},
+	// AVCC3
+	{
+		.reg = 8,
+		.multiplier = 1,
+		.label = "AVCC3",
+	},
+	// AVSB
+	{
+		.reg = 12,
+		.multiplier = 1,
+		.label = "AVSB",
+	},
+	// VBAT
+	{
+		.reg = 13,
+		.multiplier = 1,
+		.label = "VBat",
+	},
+
+};
+
+static const char *const nct6687_temp_label[] = {
+	"CPU",
+	"System",
+	"VRM MOS",
+	"PCH",
+	"CPU Socket",
+	"PCIe x1",
+	"M2_1",
+	NULL,
+};
+
+static const char *const nct6687_fan_label[] = {
+	"CPU Fan",
+	"Pump Fan",
+	"System Fan #1",
+	"System Fan #2",
+	"System Fan #3",
+	"System Fan #4",
+	"System Fan #5",
+	"System Fan #6",
+	NULL,
+};
+
+/* ------------------------------------------------------- */
+struct nct6687_data
+{
+	int addr;	/* IO base of EC space */
+	int sioreg; /* SIO register */
+	enum kinds kind;
+
+	struct device *hwmon_dev;
+	const struct attribute_group *groups[6];
+
+	struct mutex update_lock;	/* used to protect sensor updates */
+	bool valid;					/* true if following fields are valid */
+	unsigned long last_updated; /* In jiffies */
+
+	/* Voltage values */
+	s16 voltage[3][NCT6687_NUM_REG_VOLTAGE]; // 0 = current 1 = min 2 = max
+
+	/* Temperature values */
+	s32 temperature[3][NCT6687_NUM_REG_TEMP]; // 0 = current 1 = min 2 = max
+
+	/* Fan attribute values */
+	u16 rpm[3][NCT6687_NUM_REG_FAN]; // 0 = current 1 = min 2 = max
+	u8 _initialFanControlMode[NCT6687_NUM_REG_FAN];
+	u8 _initialFanPwmCommand[NCT6687_NUM_REG_FAN];
+	bool _restoreDefaultFanControlRequired[NCT6687_NUM_REG_FAN];
+
+	u8 pwm[NCT6687_NUM_REG_PWM];
+
+	/* Remember extra register values over suspend/resume */
+	u8 hwm_cfg;
+};
+
+struct nct6687_sio_data
+{
+	int sioreg;
+	enum kinds kind;
+};
+
+struct sensor_device_template
+{
+	struct device_attribute dev_attr;
+	union
+	{
+		struct
+		{
+			u8 nr;
+			u8 index;
+		} s;
+		int index;
+	} u;
+	bool s2; /* true if both index and nr are used */
+};
+
+struct sensor_device_attr_u
+{
+	union
+	{
+		struct sensor_device_attribute a1;
+		struct sensor_device_attribute_2 a2;
+	} u;
+	char name[32];
+};
+
+#define __TEMPLATE_ATTR(_template, _mode, _show, _store) \
+	{                                                    \
+		.attr = {.name = _template, .mode = _mode},      \
+		.show = _show,                                   \
+		.store = _store,                                 \
+	}
+
+#define SENSOR_DEVICE_TEMPLATE(_template, _mode, _show, _store, _index) \
+	{                                                                   \
+		.dev_attr = __TEMPLATE_ATTR(_template, _mode, _show, _store),   \
+		.u.index = _index,                                              \
+		.s2 = false                                                     \
+	}
+
+#define SENSOR_DEVICE_TEMPLATE_2(_template, _mode, _show, _store,     \
+								 _nr, _index)                         \
+	{                                                                 \
+		.dev_attr = __TEMPLATE_ATTR(_template, _mode, _show, _store), \
+		.u.s.index = _index,                                          \
+		.u.s.nr = _nr,                                                \
+		.s2 = true                                                    \
+	}
+
+#define SENSOR_TEMPLATE(_name, _template, _mode, _show, _store, _index)                                                        \
+	static struct sensor_device_template sensor_dev_template_##_name = SENSOR_DEVICE_TEMPLATE(_template, _mode, _show, _store, \
+																							  _index)
+
+#define SENSOR_TEMPLATE_2(_name, _template, _mode, _show, _store,                                                                \
+						  _nr, _index)                                                                                           \
+	static struct sensor_device_template sensor_dev_template_##_name = SENSOR_DEVICE_TEMPLATE_2(_template, _mode, _show, _store, \
+																								_nr, _index)
+
+struct sensor_template_group
+{
+	struct sensor_device_template **templates;
+	umode_t (*is_visible)(struct kobject *, struct attribute *, int);
+	int base;
+};
+
+static void nct6687_save_fan_control(struct nct6687_data *data, int index);
+
+static const char* nct6687_voltage_label(char* buf, int index)
+{
+	if (manual)
+		sprintf(buf, "in%d", index);
+	else
+		strcpy(buf, nct6687_voltage_definition[index].label);
+
+	return buf;
+}
+
+static struct attribute_group *nct6687_create_attr_group(struct device *dev, const struct sensor_template_group *tg, int repeat)
+{
+	struct sensor_device_attribute_2 *a2;
+	struct sensor_device_attribute *a;
+	struct sensor_device_template **t;
+	struct sensor_device_attr_u *su;
+	struct attribute_group *group;
+	struct attribute **attrs;
+	int i, j, count;
+
+	if (repeat <= 0)
+		return ERR_PTR(-EINVAL);
+
+	t = tg->templates;
+	for (count = 0; *t; t++, count++)
+		;
+
+	if (count == 0)
+		return ERR_PTR(-EINVAL);
+
+	group = devm_kzalloc(dev, sizeof(*group), GFP_KERNEL);
+	if (group == NULL)
+		return ERR_PTR(-ENOMEM);
+
+	attrs = devm_kcalloc(dev, repeat * count + 1, sizeof(*attrs), GFP_KERNEL);
+	if (attrs == NULL)
+		return ERR_PTR(-ENOMEM);
+
+	su = devm_kzalloc(dev, array3_size(repeat, count, sizeof(*su)), GFP_KERNEL);
+	if (su == NULL)
+		return ERR_PTR(-ENOMEM);
+
+	group->attrs = attrs;
+	group->is_visible = tg->is_visible;
+
+	for (i = 0; i < repeat; i++)
+	{
+		t = tg->templates;
+
+		for (j = 0; *t != NULL; j++)
+		{
+			snprintf(su->name, sizeof(su->name), (*t)->dev_attr.attr.name, tg->base + i);
+
+			if ((*t)->s2)
+			{
+				a2 = &su->u.a2;
+				sysfs_attr_init(&a2->dev_attr.attr);
+				a2->dev_attr.attr.name = su->name;
+				a2->nr = (*t)->u.s.nr + i;
+				a2->index = (*t)->u.s.index;
+				a2->dev_attr.attr.mode = (*t)->dev_attr.attr.mode;
+				a2->dev_attr.show = (*t)->dev_attr.show;
+				a2->dev_attr.store = (*t)->dev_attr.store;
+				*attrs = &a2->dev_attr.attr;
+			}
+			else
+			{
+				a = &su->u.a1;
+				sysfs_attr_init(&a->dev_attr.attr);
+				a->dev_attr.attr.name = su->name;
+				a->index = (*t)->u.index + i;
+				a->dev_attr.attr.mode = (*t)->dev_attr.attr.mode;
+				a->dev_attr.show = (*t)->dev_attr.show;
+				a->dev_attr.store = (*t)->dev_attr.store;
+				*attrs = &a->dev_attr.attr;
+			}
+			attrs++;
+			su++;
+			t++;
+		}
+	}
+
+	return group;
+}
+
+static u16 nct6687_read(struct nct6687_data *data, u16 address)
+{
+	u8 page = (u8)(address >> 8);
+	u8 index = (u8)(address & 0xFF);
+	int res;
+
+	outb_p(EC_SPACE_PAGE_SELECT, data->addr + EC_SPACE_PAGE_REGISTER_OFFSET);
+	outb_p(page, data->addr + EC_SPACE_PAGE_REGISTER_OFFSET);
+	outb_p(index, data->addr + EC_SPACE_INDEX_REGISTER_OFFSET);
+
+	res = inb_p(data->addr + EC_SPACE_DATA_REGISTER_OFFSET);
+
+	return res;
+}
+
+static u16 nct6687_read16(struct nct6687_data *data, u16 reg)
+{
+	return (nct6687_read(data, reg) << 8) | nct6687_read(data, reg + 1);
+}
+
+static void nct6687_write(struct nct6687_data *data, u16 address, u16 value)
+{
+	u8 page = (u8)(address >> 8);
+	u8 index = (u8)(address & 0xFF);
+
+	outb_p(EC_SPACE_PAGE_SELECT, data->addr + EC_SPACE_PAGE_REGISTER_OFFSET);
+	outb_p(page, data->addr + EC_SPACE_PAGE_REGISTER_OFFSET);
+	outb_p(index, data->addr + EC_SPACE_INDEX_REGISTER_OFFSET);
+	outb_p(value, data->addr + EC_SPACE_DATA_REGISTER_OFFSET);
+}
+
+static void nct6687_update_temperatures(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_TEMP; i++)
+	{
+		s32 value = (char)nct6687_read(data, NCT6687_REG_TEMP(i));
+		s32 half = (nct6687_read(data, NCT6687_REG_TEMP(i) + 1) >> 7) & 0x1;
+		s32 temperature = (value * 1000) + (500 * half);
+
+		data->temperature[0][i] = temperature;
+		data->temperature[1][i] = MIN(temperature, data->temperature[1][i]);
+		data->temperature[2][i] = MAX(temperature, data->temperature[2][i]);
+
+		pr_debug("nct6687_update_temperatures[%d]], addr=%04X, value=%d, half=%d, temperature=%d\n", i, NCT6687_REG_TEMP(i), value, half, temperature);
+	}
+}
+
+static void nct6687_update_voltage(struct nct6687_data *data)
+{
+	int index;
+	char buf[128];
+
+	/* Measured voltages and limits */
+	for (index = 0; index < NCT6687_NUM_REG_VOLTAGE; index++)
+	{
+		s16 reg = manual ? index : nct6687_voltage_definition[index].reg;
+		s16 high = nct6687_read(data, NCT6687_REG_VOLTAGE(reg)) * 16;
+		s16 low = ((u16)nct6687_read(data, NCT6687_REG_VOLTAGE(reg) + 1)) >> 4;
+		s16 value = low + high;
+		s16 voltage = manual ? value : value * nct6687_voltage_definition[index].multiplier;
+
+		data->voltage[0][index] = voltage;
+		data->voltage[1][index] = MIN(voltage, data->voltage[1][index]);
+		data->voltage[2][index] = MAX(voltage, data->voltage[2][index]);
+
+		pr_debug("nct6687_update_voltage[%d], %s, reg=%d, addr=0x%04x, value=%d, voltage=%d\n", index, nct6687_voltage_label(buf, index), reg, NCT6687_REG_VOLTAGE(index), value, voltage);
+	}
+
+	pr_debug("nct6687_update_voltage\n");
+}
+
+static void nct6687_update_fans(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_FAN; i++)
+	{
+		s16 rmp = nct6687_read16(data, NCT6687_REG_FAN_RPM(i));
+
+		data->rpm[0][i] = rmp;
+		data->rpm[1][i] = MIN(rmp, data->rpm[1][i]);
+		data->rpm[2][i] = MAX(rmp, data->rpm[2][i]);
+
+		pr_debug("nct6687_update_fans[%d], rpm=%d min=%d, max=%d", i, rmp, data->rpm[1][i], data->rpm[2][i]);
+	}
+
+	for (i = 0; i < NCT6687_NUM_REG_PWM; i++)
+	{
+		data->pwm[i] = nct6687_read(data, NCT6687_REG_PWM(i));
+
+		pr_debug("nct6687_update_fans[%d], pwm=%d", i, data->pwm[i]);
+	}
+}
+
+static struct nct6687_data *nct6687_update_device(struct device *dev)
+{
+	struct nct6687_data *data = dev_get_drvdata(dev);
+
+	mutex_lock(&data->update_lock);
+
+	if (time_after(jiffies, data->last_updated + HZ) || !data->valid)
+	{
+		/* Measured voltages and limits */
+		nct6687_update_voltage(data);
+
+		/* Measured temperatures and limits */
+		nct6687_update_temperatures(data);
+
+		/* Measured fan speeds and limits */
+		nct6687_update_fans(data);
+
+		data->last_updated = jiffies;
+		data->valid = true;
+	}
+
+	mutex_unlock(&data->update_lock);
+
+	return data;
+}
+
+/*
+ * Sysfs callback functions
+ */
+static ssize_t show_voltage_label(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr);
+
+	if (manual)
+		return sprintf(buf, "in%d\n", sattr->index);
+	else
+		return sprintf(buf, "%s\n", nct6687_voltage_definition[sattr->index].label);
+}
+
+static ssize_t show_voltage_value(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	struct nct6687_data *data = nct6687_update_device(dev);
+
+	return sprintf(buf, "%d\n", data->voltage[sattr->index][sattr->nr]);
+}
+
+static umode_t nct6687_voltage_is_visible(struct kobject *kobj, struct attribute *attr, int index)
+{
+	pr_debug("nct6687_voltage_is_visible[%d], attr=0x%04X\n", index, attr->mode);
+	return attr->mode;
+}
+
+SENSOR_TEMPLATE(voltage_label, "in%d_label", S_IRUGO, show_voltage_label, NULL, 0);
+SENSOR_TEMPLATE_2(voltage_input, "in%d_input", S_IRUGO, show_voltage_value, NULL, 0, 0);
+SENSOR_TEMPLATE_2(voltage_min, "in%d_min", S_IRUGO, show_voltage_value, NULL, 0, 1);
+SENSOR_TEMPLATE_2(voltage_max, "in%d_max", S_IRUGO, show_voltage_value, NULL, 0, 2);
+
+static struct sensor_device_template *nct6687_attributes_voltage_template[] = {
+	&sensor_dev_template_voltage_label,
+	&sensor_dev_template_voltage_input,
+	&sensor_dev_template_voltage_min,
+	&sensor_dev_template_voltage_max,
+	NULL,
+};
+
+static const struct sensor_template_group nct6687_voltage_template_group = {
+	.templates = nct6687_attributes_voltage_template,
+	.is_visible = nct6687_voltage_is_visible,
+};
+
+static ssize_t show_fan_label(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr);
+
+	return sprintf(buf, "%s\n", nct6687_fan_label[sattr->index]);
+}
+
+static ssize_t show_fan_value(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	struct nct6687_data *data = nct6687_update_device(dev);
+
+	return sprintf(buf, "%d\n", data->rpm[sattr->index][sattr->nr]);
+}
+
+static umode_t nct6687_fan_is_visible(struct kobject *kobj, struct attribute *attr, int index)
+{
+	return attr->mode;
+}
+
+SENSOR_TEMPLATE(fan_label, "fan%d_label", S_IRUGO, show_fan_label, NULL, 0);
+SENSOR_TEMPLATE_2(fan_input, "fan%d_input", S_IRUGO, show_fan_value, NULL, 0, 0);
+SENSOR_TEMPLATE_2(fan_min, "fan%d_min", S_IRUGO, show_fan_value, NULL, 0, 1);
+SENSOR_TEMPLATE_2(fan_max, "fan%d_max", S_IRUGO, show_fan_value, NULL, 0, 2);
+
+/*
+ * nct6687_fan_is_visible uses the index into the following array
+ * to determine if attributes should be created or not.
+ * Any change in order or content must be matched.
+ */
+static struct sensor_device_template *nct6687_attributes_fan_template[] = {
+	&sensor_dev_template_fan_label,
+	&sensor_dev_template_fan_input,
+	&sensor_dev_template_fan_min,
+	&sensor_dev_template_fan_max,
+	NULL,
+};
+
+static const struct sensor_template_group nct6687_fan_template_group = {
+	.templates = nct6687_attributes_fan_template,
+	.is_visible = nct6687_fan_is_visible,
+	.base = 1,
+};
+
+static ssize_t show_temperature_label(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr);
+
+	return sprintf(buf, "%s\n", nct6687_temp_label[sattr->index]);
+}
+
+static ssize_t show_temperature_value(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	struct nct6687_data *data = nct6687_update_device(dev);
+
+	return sprintf(buf, "%d\n", data->temperature[sattr->index][sattr->nr]);
+}
+
+static umode_t nct6687_temp_is_visible(struct kobject *kobj, struct attribute *attr, int index)
+{
+	return attr->mode;
+}
+
+SENSOR_TEMPLATE(temp_label, "temp%d_label", S_IRUGO, show_temperature_label, NULL, 0);
+SENSOR_TEMPLATE_2(temp_input, "temp%d_input", S_IRUGO, show_temperature_value, NULL, 0, 0);
+SENSOR_TEMPLATE_2(temp_min, "temp%d_min", S_IRUGO, show_temperature_value, NULL, 0, 1);
+SENSOR_TEMPLATE_2(temp_max, "temp%d_max", S_IRUGO, show_temperature_value, NULL, 0, 2);
+
+/*
+ * nct6687_temp_is_visible uses the index into the following array
+ * to determine if attributes should be created or not.
+ * Any change in order or content must be matched.
+ */
+static struct sensor_device_template *nct6687_attributes_temp_template[] = {
+	&sensor_dev_template_temp_input,
+	&sensor_dev_template_temp_label,
+	&sensor_dev_template_temp_min,
+	&sensor_dev_template_temp_max,
+	NULL,
+};
+
+static const struct sensor_template_group nct6687_temp_template_group = {
+	.templates = nct6687_attributes_temp_template,
+	.is_visible = nct6687_temp_is_visible,
+	.base = 1,
+};
+
+static ssize_t show_pwm(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct nct6687_data *data = nct6687_update_device(dev);
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	int index = sattr->index;
+
+	return sprintf(buf, "%d\n", data->pwm[index]);
+}
+
+static ssize_t store_pwm(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
+{
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	struct nct6687_data *data = dev_get_drvdata(dev);
+	int index = sattr->index;
+	unsigned long val;
+	u16 mode;
+	u8 bitMask;
+
+	if (kstrtoul(buf, 10, &val) || val > 255 || index >= NCT6687_NUM_REG_FAN)
+		return -EINVAL;
+
+	mutex_lock(&data->update_lock);
+
+	nct6687_save_fan_control(data, index);
+
+	mode = nct6687_read(data, NCT6687_REG_FAN_CTRL_MODE(index));
+	bitMask = (u8)(0x01 << index);
+
+	mode = (u8)(mode | bitMask);
+	nct6687_write(data, NCT6687_REG_FAN_CTRL_MODE(index), mode);
+
+	nct6687_write(data, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_FAN_CFG_REQ);
+	msleep(50);
+	nct6687_write(data, NCT6687_REG_PWM_WRITE(index), val);
+	nct6687_write(data, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_FAN_CFG_DONE);
+	msleep(50);
+
+	mutex_unlock(&data->update_lock);
+
+	return count;
+}
+
+SENSOR_TEMPLATE(pwm, "pwm%d", S_IRUGO, show_pwm, store_pwm, 0);
+
+static void nct6687_save_fan_control(struct nct6687_data *data, int index)
+{
+	if (data->_restoreDefaultFanControlRequired[index] == false)
+	{
+		u16 reg = nct6687_read(data, NCT6687_REG_FAN_CTRL_MODE(index));
+		u16 bitMask = 0x01 << index;
+		u8 pwm = nct6687_read(data, NCT6687_REG_FAN_PWM_COMMAND(index));
+
+		data->_initialFanControlMode[index] = (u8)(reg & bitMask);
+		data->_initialFanPwmCommand[index] = pwm;
+
+		data->_restoreDefaultFanControlRequired[index] = true;
+	}
+}
+
+static void nct6687_restore_fan_control(struct nct6687_data *data, int index)
+{
+	if (data->_restoreDefaultFanControlRequired[index])
+	{
+		u8 mode = nct6687_read(data, NCT6687_REG_FAN_CTRL_MODE(index));
+		mode = (u8)(mode & ~data->_initialFanControlMode[index]);
+
+		nct6687_write(data, NCT6687_REG_FAN_CTRL_MODE(index), mode);
+
+		nct6687_write(data, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_FAN_CFG_REQ);
+		msleep(50);
+		nct6687_write(data, NCT6687_REG_PWM_WRITE(index), data->_initialFanPwmCommand[index]);
+		nct6687_write(data, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_FAN_CFG_DONE);
+		msleep(50);
+
+		data->_restoreDefaultFanControlRequired[index] = false;
+
+		pr_debug("nct6687_restore_fan_control[%d], addr=%04X, ctrl=%04X, _initialFanPwmCommand=%d\n", index, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_REG_PWM_WRITE(index), data->_initialFanPwmCommand[index]);
+	}
+}
+
+static umode_t nct6687_pwm_is_visible(struct kobject *kobj, struct attribute *attr, int index)
+{
+	return attr->mode | S_IWUSR;
+}
+
+static struct sensor_device_template *nct6687_attributes_pwm_template[] = {
+	&sensor_dev_template_pwm,
+	NULL,
+};
+
+static const struct sensor_template_group nct6687_pwm_template_group = {
+	.templates = nct6687_attributes_pwm_template,
+	.is_visible = nct6687_pwm_is_visible,
+	.base = 1,
+};
+
+/* Get the monitoring functions started */
+static inline void nct6687_init_device(struct nct6687_data *data)
+{
+	u8 tmp;
+
+	pr_debug("nct6687_init_device\n");
+
+	/* Start hardware monitoring if needed */
+	tmp = nct6687_read(data, NCT6687_HWM_CFG);
+	if (!(tmp & 0x80))
+	{
+		pr_debug("nct6687_init_device: 0x%04x\n", tmp);
+		nct6687_write(data, NCT6687_HWM_CFG, tmp | 0x80);
+	}
+
+	// enable SIO voltage
+	nct6687_write(data, 0x1BB, 0x61);
+	nct6687_write(data, 0x1BC, 0x62);
+	nct6687_write(data, 0x1BD, 0x63);
+	nct6687_write(data, 0x1BE, 0x64);
+	nct6687_write(data, 0x1BF, 0x65);
+}
+
+/*
+ * There are a total of 8 fan inputs.
+ */
+static void nct6687_setup_fans(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_FAN; i++)
+	{
+		u16 reg = nct6687_read(data, NCT6687_REG_FAN_CTRL_MODE(i));
+		u16 bitMask = 0x01 << i;
+		u16 rpm = nct6687_read16(data, NCT6687_REG_FAN_RPM(i));
+
+		data->rpm[0][i] = rpm;
+		data->rpm[1][i] = rpm;
+		data->rpm[2][i] = rpm;
+		data->_initialFanControlMode[i] = (u8)(reg & bitMask);
+		data->_restoreDefaultFanControlRequired[i] = false;
+
+		pr_debug("nct6687_setup_fans[%d], %s - addr=%04X, ctrl=%04X, rpm=%d, _initialFanControlMode=%d\n", i, nct6687_fan_label[i], NCT6687_REG_FAN_CTRL_MODE(i), reg, rpm, data->_initialFanControlMode[i]);
+	}
+}
+
+static void nct6687_setup_voltages(struct nct6687_data *data)
+{
+	int index;
+	char buf[64];
+
+	/* Measured voltages and limits */
+	for (index = 0; index < NCT6687_NUM_REG_VOLTAGE; index++)
+	{
+		s16 reg = manual ? index : nct6687_voltage_definition[index].reg;
+		s16 high = nct6687_read(data, NCT6687_REG_VOLTAGE(reg)) * 16;
+		s16 low = ((u16)nct6687_read(data, NCT6687_REG_VOLTAGE(reg) + 1)) >> 4;
+		s16 value = low + high;
+		s16 voltage = manual ? value : value * nct6687_voltage_definition[index].multiplier;
+
+		data->voltage[0][index] = voltage;
+		data->voltage[1][index] = voltage;
+		data->voltage[2][index] = voltage;
+
+		pr_debug("nct6687_setup_voltages[%d], %s, addr=0x%04x, value=%d, voltage=%d\n", index, nct6687_voltage_label(buf, index), NCT6687_REG_VOLTAGE(index), value, voltage);
+	}
+}
+
+static void nct6687_setup_temperatures(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_TEMP; i++)
+	{
+		s32 value = (char)nct6687_read(data, NCT6687_REG_TEMP(i));
+		s32 half = (nct6687_read(data, NCT6687_REG_TEMP(i) + 1) >> 7) & 0x1;
+		s32 temperature = (value * 1000) + (5 * half);
+
+		data->temperature[0][i] = temperature;
+		data->temperature[1][i] = temperature;
+		data->temperature[2][i] = temperature;
+
+		pr_debug("nct6687_setup_temperatures[%d]], addr=%04X, value=%d, half=%d, temperature=%d\n", i, NCT6687_REG_TEMP(i), value, half, temperature);
+	}
+}
+
+static void nct6687_setup_pwm(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_PWM; i++)
+	{
+		data->_initialFanPwmCommand[i] = nct6687_read(data, NCT6687_REG_FAN_PWM_COMMAND(i));
+		data->pwm[i] = nct6687_read(data, NCT6687_REG_PWM(i));
+
+		pr_debug("nct6687_setup_pwm[%d], addr=%04X, pwm=%d, _initialFanPwmCommand=%d\n", i, NCT6687_REG_FAN_PWM_COMMAND(i), data->pwm[i], data->_initialFanPwmCommand[i]);
+	}
+}
+
+static int nct6687_remove(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct nct6687_data *data = dev_get_drvdata(dev);
+	int i;
+
+	mutex_lock(&data->update_lock);
+
+	for (i = 0; i < NCT6687_NUM_REG_FAN; i++)
+	{
+		nct6687_restore_fan_control(data, i);
+	}
+
+	mutex_unlock(&data->update_lock);
+
+	return 0;
+}
+
+static int nct6687_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct nct6687_sio_data *sio_data = dev->platform_data;
+	struct attribute_group *group;
+	struct nct6687_data *data;
+	struct device *hwmon_dev;
+	struct resource *res;
+	int groups = 0;
+	char build[16];
+
+	res = platform_get_resource(pdev, IORESOURCE_IO, 0);
+	if (!devm_request_region(dev, res->start, IOREGION_LENGTH, DRVNAME))
+		return -EBUSY;
+
+	data = devm_kzalloc(dev, sizeof(struct nct6687_data), GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	data->kind = sio_data->kind;
+	data->sioreg = sio_data->sioreg;
+	data->addr = res->start;
+
+	pr_debug("nct6687_probe addr=0x%04X, sioreg=0x%04X\n", data->addr, data->sioreg);
+
+	mutex_init(&data->update_lock);
+
+	platform_set_drvdata(pdev, data);
+
+	nct6687_init_device(data);
+	nct6687_setup_fans(data);
+	nct6687_setup_pwm(data);
+	nct6687_setup_temperatures(data);
+	nct6687_setup_voltages(data);
+
+	/* Register sysfs hooks */
+
+	group = nct6687_create_attr_group(dev, &nct6687_pwm_template_group, NCT6687_NUM_REG_FAN);
+
+	if (IS_ERR(group))
+		return PTR_ERR(group);
+
+	data->groups[groups++] = group;
+
+	group = nct6687_create_attr_group(dev, &nct6687_voltage_template_group, NCT6687_NUM_REG_VOLTAGE);
+
+	if (IS_ERR(group))
+		return PTR_ERR(group);
+
+	data->groups[groups++] = group;
+
+	group = nct6687_create_attr_group(dev, &nct6687_fan_template_group, NCT6687_NUM_REG_FAN);
+
+	if (IS_ERR(group))
+		return PTR_ERR(group);
+
+	data->groups[groups++] = group;
+
+	group = nct6687_create_attr_group(dev, &nct6687_temp_template_group, NCT6687_NUM_REG_TEMP);
+
+	if (IS_ERR(group))
+		return PTR_ERR(group);
+
+	data->groups[groups++] = group;
+
+	scnprintf(build, sizeof(build), "%02d/%02d/%02d", nct6687_read(data, NCT6687_REG_BUILD_MONTH), nct6687_read(data, NCT6687_REG_BUILD_DAY), nct6687_read(data, NCT6687_REG_BUILD_YEAR));
+
+	dev_info(dev, "%s EC firmware version %d.%d build %s\n", nct6687_chip_names[data->kind], nct6687_read(data, NCT6687_REG_VERSION_HI), nct6687_read(data, NCT6687_REG_VERSION_LO), build);
+
+	hwmon_dev = devm_hwmon_device_register_with_groups(dev, nct6687_device_names[data->kind], data, data->groups);
+
+	return PTR_ERR_OR_ZERO(hwmon_dev);
+}
+
+static int nct6687_suspend(struct platform_device *pdev, pm_message_t state)
+{
+	struct nct6687_data *data = nct6687_update_device(&pdev->dev);
+
+	mutex_lock(&data->update_lock);
+	data->hwm_cfg = nct6687_read(data, NCT6687_HWM_CFG);
+	mutex_unlock(&data->update_lock);
+
+	return 0;
+}
+
+static int nct6687_resume(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct nct6687_data *data = dev_get_drvdata(dev);
+
+	mutex_lock(&data->update_lock);
+
+	nct6687_write(data, NCT6687_HWM_CFG, data->hwm_cfg);
+
+	/* Force re-reading all values */
+	data->valid = false;
+	mutex_unlock(&data->update_lock);
+
+	return 0;
+}
+
+// static const struct dev_pm_ops nct6687_dev_pm_ops = {
+// 	.suspend = nct6687_suspend,
+// 	.resume = nct6687_resume,
+// 	.freeze = nct6687_suspend,
+// 	.restore = nct6687_resume,
+// };
+
+#define NCT6687_DEV_PM_OPS NULL
+
+static struct platform_driver nct6687_driver = {
+	.driver = {
+		.name = DRVNAME,
+		.pm = NCT6687_DEV_PM_OPS,
+	},
+	.probe = nct6687_probe,
+	.remove = nct6687_remove,
+	.suspend = nct6687_suspend,
+	.resume = nct6687_resume,
+};
+
+static int __init nct6687_find(int sioaddr, struct nct6687_sio_data *sio_data)
+{
+	u16 address;
+	u16 verify;
+	u16 val;
+	int err;
+
+	err = superio_enter(sioaddr);
+	if (err)
+		return err;
+
+	val = (superio_inb(sioaddr, SIO_REG_DEVID) << 8) | superio_inb(sioaddr, SIO_REG_DEVREVISION);
+
+	pr_debug("found chip ID: 0x%04x\n", val);
+
+	if (val == SIO_NCT6683D_ID) {
+		sio_data->kind = nct6683;
+	} else if (val == SIO_NCT6686_ID || val == SIO_NCT6686D_ID) {
+		sio_data->kind = nct6686;
+	} else if (val == SIO_NCT6687_ID || val == SIO_NCT6687D_ID || force)
+	{
+		sio_data->kind = nct6687;
+	}
+	else
+	{
+		if (val != 0xffff)
+			pr_debug("unsupported chip ID: 0x%04x\n", val);
+		goto fail;
+	}
+
+	/* We have a known chip, find the HWM I/O address */
+	superio_select(sioaddr, NCT6687_LD_HWM);
+	address = (superio_inb(sioaddr, SIO_REG_ADDR) << 8) | superio_inb(sioaddr, SIO_REG_ADDR + 1);
+	ssleep(1);
+	verify = (superio_inb(sioaddr, SIO_REG_ADDR) << 8) | superio_inb(sioaddr, SIO_REG_ADDR + 1);
+
+	if (address == 0 || address != verify)
+	{
+		pr_err("EC base I/O port unconfigured\n");
+		goto fail;
+	}
+
+	if ((address & 0x07) == 0x05)
+		address &= 0xFFF8;
+
+	if (address < 0x100 || (address & 0xF007) != 0)
+	{
+		pr_err("EC Invalid address: 0x%04X\n", address);
+		goto fail;
+	}
+
+	/* Activate logical device if needed */
+	val = superio_inb(sioaddr, SIO_REG_ENABLE);
+	if (!(val & 0x01))
+	{
+		pr_warn("Forcibly enabling EC access. Data may be unusable.\n");
+		superio_outb(sioaddr, SIO_REG_ENABLE, val | 0x01);
+	}
+
+	superio_exit(sioaddr);
+	pr_info("Found %s or compatible chip at 0x%04x:0x%04x\n", nct6687_chip_names[sio_data->kind], sioaddr, address);
+	sio_data->sioreg = sioaddr;
+
+	return address;
+
+fail:
+	superio_exit(sioaddr);
+	return -ENODEV;
+}
+
+/*
+ * when Super-I/O functions move to a separate file, the Super-I/O
+ * bus will manage the lifetime of the device and this module will only keep
+ * track of the nct6687 driver. But since we use platform_device_alloc(), we
+ * must keep track of the device
+ */
+static struct platform_device *pdev[2];
+
+static int __init sensors_nct6687_init(void)
+{
+	struct nct6687_sio_data sio_data;
+	int sioaddr[2] = {0x2e, 0x4e};
+	struct resource res;
+	bool found = false;
+	int address;
+	int i, err;
+
+	err = platform_driver_register(&nct6687_driver);
+	if (err)
+		return err;
+
+	/*
+	 * initialize sio_data->kind and sio_data->sioreg.
+	 *
+	 * when Super-I/O functions move to a separate file, the Super-I/O
+	 * driver will probe 0x2e and 0x4e and auto-detect the presence of a
+	 * nct6687 hardware monitor, and call probe()
+	 */
+	for (i = 0; i < ARRAY_SIZE(pdev); i++)
+	{
+		address = nct6687_find(sioaddr[i], &sio_data);
+		if (address <= 0)
+			continue;
+
+		found = true;
+
+		pdev[i] = platform_device_alloc(DRVNAME, address);
+		if (!pdev[i])
+		{
+			err = -ENOMEM;
+			goto exit_device_unregister;
+		}
+
+		err = platform_device_add_data(pdev[i], &sio_data, sizeof(struct nct6687_sio_data));
+		if (err)
+			goto exit_device_put;
+
+		memset(&res, 0, sizeof(res));
+
+		res.name = DRVNAME;
+		res.start = address + IOREGION_OFFSET;
+		res.end = address + IOREGION_OFFSET + IOREGION_LENGTH - 1;
+		res.flags = IORESOURCE_IO;
+
+		err = acpi_check_resource_conflict(&res);
+		if (err)
+		{
+			platform_device_put(pdev[i]);
+			pdev[i] = NULL;
+			continue;
+		}
+
+		err = platform_device_add_resources(pdev[i], &res, 1);
+		if (err)
+			goto exit_device_put;
+
+		/* platform_device_add calls probe() */
+		err = platform_device_add(pdev[i]);
+		if (err)
+			goto exit_device_put;
+	}
+
+	if (!found)
+	{
+		err = -ENODEV;
+		goto exit_unregister;
+	}
+
+	return 0;
+
+exit_device_put:
+	platform_device_put(pdev[i]);
+
+exit_device_unregister:
+	while (--i >= 0)
+	{
+		if (pdev[i])
+			platform_device_unregister(pdev[i]);
+	}
+
+exit_unregister:
+	platform_driver_unregister(&nct6687_driver);
+
+	return err;
+}
+
+static void __exit sensors_nct6687_exit(void)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(pdev); i++)
+	{
+		if (pdev[i])
+			platform_device_unregister(pdev[i]);
+	}
+
+	platform_driver_unregister(&nct6687_driver);
+}
+
+MODULE_AUTHOR("Frederic Boltz <frederic.boltz@gmail.com>");
+MODULE_DESCRIPTION("Driver for NCT6687D");
+MODULE_LICENSE("GPL");
+
+module_init(sensors_nct6687_init);
+module_exit(sensors_nct6687_exit);

[-- Attachment #3: 0001-Adding-driver-for-NCT6687D-Kernel-module.patch --]
[-- Type: text/x-patch, Size: 37095 bytes --]

From 8c49ae386d76ebdf6ca99102eaa50fc9eaba84a1 Mon Sep 17 00:00:00 2001
From: Thomas Raines <seatommyboy@gmail.com>
Date: Sun, 16 Jul 2023 07:53:30 -0500
Subject: [PATCH] Adding driver for NCT6687D Kernel module. Original author is
 Frederic BOLTZ https://github.com/Fred78290/nct6687d.

---
 drivers/hwmon/Kconfig   |   10 +
 drivers/hwmon/Makefile  |    1 +
 drivers/hwmon/nct6687.c | 1272 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 1283 insertions(+)
 create mode 100644 drivers/hwmon/nct6687.c

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index fc640201a2de..2e5a0b30b5ec 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1518,6 +1518,16 @@ config SENSORS_NCT6683
 	  This driver can also be built as a module. If so, the module
 	  will be called nct6683.
 
+config SENSORS_NCT6687
+	tristate "Nuvoton NCT6687D"
+	depends on !PPC
+	help
+	  If you say yes here you get support for the hardware monitoring
+	  functionality of the Nuvoton NCT6687D eSIO chip.
+
+	  This driver can also be built as a module. If so, the module
+	  will be called nct6687.
+
 config SENSORS_NCT6775_CORE
 	tristate
 	select REGMAP
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index cd8c568c80a9..dfc3bbe962a2 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -158,6 +158,7 @@ obj-$(CONFIG_SENSORS_MLXREG_FAN) += mlxreg-fan.o
 obj-$(CONFIG_SENSORS_MENF21BMC_HWMON) += menf21bmc_hwmon.o
 obj-$(CONFIG_SENSORS_MR75203)	+= mr75203.o
 obj-$(CONFIG_SENSORS_NCT6683)	+= nct6683.o
+obj-$(CONFIG_SENSORS_NCT6687)	+= nct6687.o
 obj-$(CONFIG_SENSORS_NCT6775_CORE) += nct6775-core.o
 nct6775-objs			:= nct6775-platform.o
 obj-$(CONFIG_SENSORS_NCT6775)	+= nct6775.o
diff --git a/drivers/hwmon/nct6687.c b/drivers/hwmon/nct6687.c
new file mode 100644
index 000000000000..fed2c868fec7
--- /dev/null
+++ b/drivers/hwmon/nct6687.c
@@ -0,0 +1,1272 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * nct6687 - Driver for the hardware monitoring functionality of
+ *	       Nuvoton NCT6687 Super-I/O chips
+ *
+ * Copyright (C) 2020  Frederic Boltz <frederic.boltz@gmail.com>
+ *
+ * Derived from nct6683 driver
+ * Copyright (C) 2013  Guenter Roeck <linux@roeck-us.net>
+ * 
+ * Inspired of LibreHardwareMonitor
+ * https://github.com/LibreHardwareMonitor/LibreHardwareMonitor
+ * 
+ * Supports the following chips:
+ *
+ * Chip       #voltage   #fan    #pwm    #temp  chip ID
+ * nct6683    14(1)      8       8       7      0xc732  (partial support)
+ * nct6686d   21(1)      16      8       32(1)  0xd440
+ * nct6687    14(1)      8       8       7      0xd592
+ *
+ * Notes:
+ *	(1) Total number of voltage and 9 displayed.
+ */
+// #define DEBUG 1
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/acpi.h>
+#include <linux/delay.h>
+#include <linux/err.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>
+
+#define MIN(a,b) (((a)<(b))?(a):(b))
+#define MAX(a,b) (((a)>(b))?(a):(b))
+
+enum kinds
+{
+	nct6683,
+	nct6686,
+	nct6687
+};
+
+static bool force;
+static bool manual;
+
+module_param(force, bool, 0);
+MODULE_PARM_DESC(force, "Set to one to enable support for unknown vendors");
+
+module_param(manual, bool, 0);
+MODULE_PARM_DESC(manual, "Set voltage input and voltage label configured with external sensors file");
+
+static const char *const nct6687_device_names[] = {
+	"nct6683",
+	"nct6686",
+	"nct6687",
+};
+
+static const char *const nct6687_chip_names[] = {
+	"NCT6683D",
+	"NCT6686D",
+	"NCT6687D",
+};
+
+#define DRVNAME "nct6687"
+
+/*
+ * Super-I/O constants and functions
+ */
+
+#define NCT6687_LD_ACPI 0x0a
+#define NCT6687_LD_HWM 0x0b
+#define NCT6687_LD_VID 0x0d
+
+#define SIO_REG_LDSEL 0x07		 /* Logical device select */
+#define SIO_REG_DEVID 0x20		 /* Device ID (2 bytes) */
+#define SIO_REG_DEVREVISION 0x21 /* Device ID (2 bytes) */
+#define SIO_REG_ENABLE 0x30		 /* Logical device enable */
+#define SIO_REG_ADDR 0x60		 /* Logical device address (2 bytes) */
+
+#define SIO_NCT6683D_ID 0xc732
+#define SIO_NCT6686_ID 0xd440
+#define SIO_NCT6686D_ID 0xd441
+#define SIO_NCT6687_ID 0xd451 // 0xd592
+#define SIO_NCT6687D_ID 0xd592
+
+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 void superio_select(int ioreg, int ld)
+{
+	outb(SIO_REG_LDSEL, ioreg);
+	outb(ld, ioreg + 1);
+}
+
+static inline int superio_enter(int ioreg)
+{
+	/*
+	 * Try to reserve <ioreg> and <ioreg + 1> for exclusive access.
+	 */
+	if (!request_muxed_region(ioreg, 2, DRVNAME))
+		return -EBUSY;
+
+	outb(0x87, ioreg);
+	outb(0x87, ioreg);
+
+	return 0;
+}
+
+static inline void superio_exit(int ioreg)
+{
+	outb(0xaa, ioreg);
+	outb(0x02, ioreg);
+	outb(0x02, ioreg + 1);
+
+	release_region(ioreg, 2);
+}
+
+/*
+ * ISA constants
+ */
+
+#define IOREGION_OFFSET 0 /* Use EC port 1 */
+#define IOREGION_LENGTH 4
+
+/* Common and NCT6687 specific data */
+
+#define NCT6687_NUM_REG_VOLTAGE (sizeof(nct6687_voltage_definition) / sizeof(struct voltage_reg))
+#define NCT6687_NUM_REG_TEMP 7
+#define NCT6687_NUM_REG_FAN 8
+#define NCT6687_NUM_REG_PWM 8
+
+#define NCT6687_REG_TEMP(x) (0x100 + (x)*2)
+#define NCT6687_REG_VOLTAGE(x) (0x120 + (x)*2)
+#define NCT6687_REG_FAN_RPM(x) (0x140 + (x)*2)
+#define NCT6687_REG_PWM(x) (0x160 + (x))
+#define NCT6687_REG_PWM_WRITE(x) (0xa28 + (x))
+
+#define NCT6687_HWM_CFG 0x180
+
+#define NCT6687_REG_MON_CFG(x) (0x1a0 + (x))
+#define NCT6687_REG_FANIN_CFG(x) (0xA00 + (x))
+#define NCT6687_REG_FANOUT_CFG(x) (0x1d0 + (x))
+
+#define NCT6687_REG_TEMP_HYST(x) (0x330 + (x))	/* 8 bit */
+#define NCT6687_REG_TEMP_MAX(x) (0x350 + (x))	/* 8 bit */
+#define NCT6687_REG_MON_HIGH(x) (0x370 + (x)*2) /* 8 bit */
+#define NCT6687_REG_MON_LOW(x) (0x371 + (x)*2)	/* 8 bit */
+
+#define NCT6687_REG_FAN_MIN(x) (0x3b8 + (x)*2) /* 16 bit */
+
+#define NCT6687_REG_FAN_CTRL_MODE(x) 0xA00
+#define NCT6687_REG_FAN_PWM_COMMAND(x) 0xA01
+#define NCT6687_FAN_CFG_REQ 0x80
+#define NCT6687_FAN_CFG_DONE 0x40
+
+#define NCT6687_REG_BUILD_YEAR 0x604
+#define NCT6687_REG_BUILD_MONTH 0x605
+#define NCT6687_REG_BUILD_DAY 0x606
+#define NCT6687_REG_SERIAL 0x607
+#define NCT6687_REG_VERSION_HI 0x608
+#define NCT6687_REG_VERSION_LO 0x609
+
+#define NCT6687_REG_CR_CASEOPEN 0xe8
+#define NCT6687_CR_CASEOPEN_MASK (1 << 7)
+
+#define NCT6687_REG_CR_BEEP 0xe0
+#define NCT6687_CR_BEEP_MASK (1 << 6)
+
+#define EC_SPACE_PAGE_REGISTER_OFFSET 0x04
+#define EC_SPACE_INDEX_REGISTER_OFFSET 0x05
+#define EC_SPACE_DATA_REGISTER_OFFSET 0x06
+#define EC_SPACE_PAGE_SELECT 0xFF
+
+struct voltage_reg
+{
+	u16 reg;
+	u16 multiplier;
+	const char *label;
+};
+
+static struct voltage_reg nct6687_voltage_definition[] = {
+	// +12V
+	{
+		.reg = 0,
+		.multiplier = 12,
+		.label = "+12V",
+	},
+	// + 5V
+	{
+		.reg = 1,
+		.multiplier = 5,
+		.label = "+5V",
+	},
+	// +3.3V
+	{
+		.reg = 11,
+		.multiplier = 1,
+		.label = "+3.3V",
+	},
+	// CPU SOC
+	{
+		.reg = 2,
+		.multiplier = 1,
+		.label = "CPU Soc",
+	},
+	// CPU Vcore
+	{
+		.reg = 4,
+		.multiplier = 1,
+		.label = "CPU Vcore",
+	},
+	// CPU 1P8
+	{
+		.reg = 9,
+		.multiplier = 1,
+		.label = "CPU 1P8",
+	},
+	// CPU VDDP
+	{
+		.reg = 10,
+		.multiplier = 1,
+		.label = "CPU VDDP",
+	},
+	// DRAM
+	{
+		.reg = 3,
+		.multiplier = 2,
+		.label = "DRAM",
+	},
+	// Chipset
+	{
+		.reg = 5,
+		.multiplier = 1,
+		.label = "Chipset",
+	},
+
+	// CPU SA
+	{
+		.reg = 6,
+		.multiplier = 1,
+		.label = "CPU SA",
+	},
+	// Voltage #2
+	{
+		.reg = 7,
+		.multiplier = 1,
+		.label = "Voltage #2",
+	},
+	// AVCC3
+	{
+		.reg = 8,
+		.multiplier = 1,
+		.label = "AVCC3",
+	},
+	// AVSB
+	{
+		.reg = 12,
+		.multiplier = 1,
+		.label = "AVSB",
+	},
+	// VBAT
+	{
+		.reg = 13,
+		.multiplier = 1,
+		.label = "VBat",
+	},
+
+};
+
+static const char *const nct6687_temp_label[] = {
+	"CPU",
+	"System",
+	"VRM MOS",
+	"PCH",
+	"CPU Socket",
+	"PCIe x1",
+	"M2_1",
+	NULL,
+};
+
+static const char *const nct6687_fan_label[] = {
+	"CPU Fan",
+	"Pump Fan",
+	"System Fan #1",
+	"System Fan #2",
+	"System Fan #3",
+	"System Fan #4",
+	"System Fan #5",
+	"System Fan #6",
+	NULL,
+};
+
+/* ------------------------------------------------------- */
+struct nct6687_data
+{
+	int addr;	/* IO base of EC space */
+	int sioreg; /* SIO register */
+	enum kinds kind;
+
+	struct device *hwmon_dev;
+	const struct attribute_group *groups[6];
+
+	struct mutex update_lock;	/* used to protect sensor updates */
+	bool valid;					/* true if following fields are valid */
+	unsigned long last_updated; /* In jiffies */
+
+	/* Voltage values */
+	s16 voltage[3][NCT6687_NUM_REG_VOLTAGE]; // 0 = current 1 = min 2 = max
+
+	/* Temperature values */
+	s32 temperature[3][NCT6687_NUM_REG_TEMP]; // 0 = current 1 = min 2 = max
+
+	/* Fan attribute values */
+	u16 rpm[3][NCT6687_NUM_REG_FAN]; // 0 = current 1 = min 2 = max
+	u8 _initialFanControlMode[NCT6687_NUM_REG_FAN];
+	u8 _initialFanPwmCommand[NCT6687_NUM_REG_FAN];
+	bool _restoreDefaultFanControlRequired[NCT6687_NUM_REG_FAN];
+
+	u8 pwm[NCT6687_NUM_REG_PWM];
+
+	/* Remember extra register values over suspend/resume */
+	u8 hwm_cfg;
+};
+
+struct nct6687_sio_data
+{
+	int sioreg;
+	enum kinds kind;
+};
+
+struct sensor_device_template
+{
+	struct device_attribute dev_attr;
+	union
+	{
+		struct
+		{
+			u8 nr;
+			u8 index;
+		} s;
+		int index;
+	} u;
+	bool s2; /* true if both index and nr are used */
+};
+
+struct sensor_device_attr_u
+{
+	union
+	{
+		struct sensor_device_attribute a1;
+		struct sensor_device_attribute_2 a2;
+	} u;
+	char name[32];
+};
+
+#define __TEMPLATE_ATTR(_template, _mode, _show, _store) \
+	{                                                    \
+		.attr = {.name = _template, .mode = _mode},      \
+		.show = _show,                                   \
+		.store = _store,                                 \
+	}
+
+#define SENSOR_DEVICE_TEMPLATE(_template, _mode, _show, _store, _index) \
+	{                                                                   \
+		.dev_attr = __TEMPLATE_ATTR(_template, _mode, _show, _store),   \
+		.u.index = _index,                                              \
+		.s2 = false                                                     \
+	}
+
+#define SENSOR_DEVICE_TEMPLATE_2(_template, _mode, _show, _store,     \
+								 _nr, _index)                         \
+	{                                                                 \
+		.dev_attr = __TEMPLATE_ATTR(_template, _mode, _show, _store), \
+		.u.s.index = _index,                                          \
+		.u.s.nr = _nr,                                                \
+		.s2 = true                                                    \
+	}
+
+#define SENSOR_TEMPLATE(_name, _template, _mode, _show, _store, _index)                                                        \
+	static struct sensor_device_template sensor_dev_template_##_name = SENSOR_DEVICE_TEMPLATE(_template, _mode, _show, _store, \
+																							  _index)
+
+#define SENSOR_TEMPLATE_2(_name, _template, _mode, _show, _store,                                                                \
+						  _nr, _index)                                                                                           \
+	static struct sensor_device_template sensor_dev_template_##_name = SENSOR_DEVICE_TEMPLATE_2(_template, _mode, _show, _store, \
+																								_nr, _index)
+
+struct sensor_template_group
+{
+	struct sensor_device_template **templates;
+	umode_t (*is_visible)(struct kobject *, struct attribute *, int);
+	int base;
+};
+
+static void nct6687_save_fan_control(struct nct6687_data *data, int index);
+
+static const char* nct6687_voltage_label(char* buf, int index)
+{
+	if (manual)
+		sprintf(buf, "in%d", index);
+	else
+		strcpy(buf, nct6687_voltage_definition[index].label);
+
+	return buf;
+}
+
+static struct attribute_group *nct6687_create_attr_group(struct device *dev, const struct sensor_template_group *tg, int repeat)
+{
+	struct sensor_device_attribute_2 *a2;
+	struct sensor_device_attribute *a;
+	struct sensor_device_template **t;
+	struct sensor_device_attr_u *su;
+	struct attribute_group *group;
+	struct attribute **attrs;
+	int i, j, count;
+
+	if (repeat <= 0)
+		return ERR_PTR(-EINVAL);
+
+	t = tg->templates;
+	for (count = 0; *t; t++, count++)
+		;
+
+	if (count == 0)
+		return ERR_PTR(-EINVAL);
+
+	group = devm_kzalloc(dev, sizeof(*group), GFP_KERNEL);
+	if (group == NULL)
+		return ERR_PTR(-ENOMEM);
+
+	attrs = devm_kcalloc(dev, repeat * count + 1, sizeof(*attrs), GFP_KERNEL);
+	if (attrs == NULL)
+		return ERR_PTR(-ENOMEM);
+
+	su = devm_kzalloc(dev, array3_size(repeat, count, sizeof(*su)), GFP_KERNEL);
+	if (su == NULL)
+		return ERR_PTR(-ENOMEM);
+
+	group->attrs = attrs;
+	group->is_visible = tg->is_visible;
+
+	for (i = 0; i < repeat; i++)
+	{
+		t = tg->templates;
+
+		for (j = 0; *t != NULL; j++)
+		{
+			snprintf(su->name, sizeof(su->name), (*t)->dev_attr.attr.name, tg->base + i);
+
+			if ((*t)->s2)
+			{
+				a2 = &su->u.a2;
+				sysfs_attr_init(&a2->dev_attr.attr);
+				a2->dev_attr.attr.name = su->name;
+				a2->nr = (*t)->u.s.nr + i;
+				a2->index = (*t)->u.s.index;
+				a2->dev_attr.attr.mode = (*t)->dev_attr.attr.mode;
+				a2->dev_attr.show = (*t)->dev_attr.show;
+				a2->dev_attr.store = (*t)->dev_attr.store;
+				*attrs = &a2->dev_attr.attr;
+			}
+			else
+			{
+				a = &su->u.a1;
+				sysfs_attr_init(&a->dev_attr.attr);
+				a->dev_attr.attr.name = su->name;
+				a->index = (*t)->u.index + i;
+				a->dev_attr.attr.mode = (*t)->dev_attr.attr.mode;
+				a->dev_attr.show = (*t)->dev_attr.show;
+				a->dev_attr.store = (*t)->dev_attr.store;
+				*attrs = &a->dev_attr.attr;
+			}
+			attrs++;
+			su++;
+			t++;
+		}
+	}
+
+	return group;
+}
+
+static u16 nct6687_read(struct nct6687_data *data, u16 address)
+{
+	u8 page = (u8)(address >> 8);
+	u8 index = (u8)(address & 0xFF);
+	int res;
+
+	outb_p(EC_SPACE_PAGE_SELECT, data->addr + EC_SPACE_PAGE_REGISTER_OFFSET);
+	outb_p(page, data->addr + EC_SPACE_PAGE_REGISTER_OFFSET);
+	outb_p(index, data->addr + EC_SPACE_INDEX_REGISTER_OFFSET);
+
+	res = inb_p(data->addr + EC_SPACE_DATA_REGISTER_OFFSET);
+
+	return res;
+}
+
+static u16 nct6687_read16(struct nct6687_data *data, u16 reg)
+{
+	return (nct6687_read(data, reg) << 8) | nct6687_read(data, reg + 1);
+}
+
+static void nct6687_write(struct nct6687_data *data, u16 address, u16 value)
+{
+	u8 page = (u8)(address >> 8);
+	u8 index = (u8)(address & 0xFF);
+
+	outb_p(EC_SPACE_PAGE_SELECT, data->addr + EC_SPACE_PAGE_REGISTER_OFFSET);
+	outb_p(page, data->addr + EC_SPACE_PAGE_REGISTER_OFFSET);
+	outb_p(index, data->addr + EC_SPACE_INDEX_REGISTER_OFFSET);
+	outb_p(value, data->addr + EC_SPACE_DATA_REGISTER_OFFSET);
+}
+
+static void nct6687_update_temperatures(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_TEMP; i++)
+	{
+		s32 value = (char)nct6687_read(data, NCT6687_REG_TEMP(i));
+		s32 half = (nct6687_read(data, NCT6687_REG_TEMP(i) + 1) >> 7) & 0x1;
+		s32 temperature = (value * 1000) + (500 * half);
+
+		data->temperature[0][i] = temperature;
+		data->temperature[1][i] = MIN(temperature, data->temperature[1][i]);
+		data->temperature[2][i] = MAX(temperature, data->temperature[2][i]);
+
+		pr_debug("nct6687_update_temperatures[%d]], addr=%04X, value=%d, half=%d, temperature=%d\n", i, NCT6687_REG_TEMP(i), value, half, temperature);
+	}
+}
+
+static void nct6687_update_voltage(struct nct6687_data *data)
+{
+	int index;
+	char buf[128];
+
+	/* Measured voltages and limits */
+	for (index = 0; index < NCT6687_NUM_REG_VOLTAGE; index++)
+	{
+		s16 reg = manual ? index : nct6687_voltage_definition[index].reg;
+		s16 high = nct6687_read(data, NCT6687_REG_VOLTAGE(reg)) * 16;
+		s16 low = ((u16)nct6687_read(data, NCT6687_REG_VOLTAGE(reg) + 1)) >> 4;
+		s16 value = low + high;
+		s16 voltage = manual ? value : value * nct6687_voltage_definition[index].multiplier;
+
+		data->voltage[0][index] = voltage;
+		data->voltage[1][index] = MIN(voltage, data->voltage[1][index]);
+		data->voltage[2][index] = MAX(voltage, data->voltage[2][index]);
+
+		pr_debug("nct6687_update_voltage[%d], %s, reg=%d, addr=0x%04x, value=%d, voltage=%d\n", index, nct6687_voltage_label(buf, index), reg, NCT6687_REG_VOLTAGE(index), value, voltage);
+	}
+
+	pr_debug("nct6687_update_voltage\n");
+}
+
+static void nct6687_update_fans(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_FAN; i++)
+	{
+		s16 rmp = nct6687_read16(data, NCT6687_REG_FAN_RPM(i));
+
+		data->rpm[0][i] = rmp;
+		data->rpm[1][i] = MIN(rmp, data->rpm[1][i]);
+		data->rpm[2][i] = MAX(rmp, data->rpm[2][i]);
+
+		pr_debug("nct6687_update_fans[%d], rpm=%d min=%d, max=%d", i, rmp, data->rpm[1][i], data->rpm[2][i]);
+	}
+
+	for (i = 0; i < NCT6687_NUM_REG_PWM; i++)
+	{
+		data->pwm[i] = nct6687_read(data, NCT6687_REG_PWM(i));
+
+		pr_debug("nct6687_update_fans[%d], pwm=%d", i, data->pwm[i]);
+	}
+}
+
+static struct nct6687_data *nct6687_update_device(struct device *dev)
+{
+	struct nct6687_data *data = dev_get_drvdata(dev);
+
+	mutex_lock(&data->update_lock);
+
+	if (time_after(jiffies, data->last_updated + HZ) || !data->valid)
+	{
+		/* Measured voltages and limits */
+		nct6687_update_voltage(data);
+
+		/* Measured temperatures and limits */
+		nct6687_update_temperatures(data);
+
+		/* Measured fan speeds and limits */
+		nct6687_update_fans(data);
+
+		data->last_updated = jiffies;
+		data->valid = true;
+	}
+
+	mutex_unlock(&data->update_lock);
+
+	return data;
+}
+
+/*
+ * Sysfs callback functions
+ */
+static ssize_t show_voltage_label(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr);
+
+	if (manual)
+		return sprintf(buf, "in%d\n", sattr->index);
+	else
+		return sprintf(buf, "%s\n", nct6687_voltage_definition[sattr->index].label);
+}
+
+static ssize_t show_voltage_value(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	struct nct6687_data *data = nct6687_update_device(dev);
+
+	return sprintf(buf, "%d\n", data->voltage[sattr->index][sattr->nr]);
+}
+
+static umode_t nct6687_voltage_is_visible(struct kobject *kobj, struct attribute *attr, int index)
+{
+	pr_debug("nct6687_voltage_is_visible[%d], attr=0x%04X\n", index, attr->mode);
+	return attr->mode;
+}
+
+SENSOR_TEMPLATE(voltage_label, "in%d_label", S_IRUGO, show_voltage_label, NULL, 0);
+SENSOR_TEMPLATE_2(voltage_input, "in%d_input", S_IRUGO, show_voltage_value, NULL, 0, 0);
+SENSOR_TEMPLATE_2(voltage_min, "in%d_min", S_IRUGO, show_voltage_value, NULL, 0, 1);
+SENSOR_TEMPLATE_2(voltage_max, "in%d_max", S_IRUGO, show_voltage_value, NULL, 0, 2);
+
+static struct sensor_device_template *nct6687_attributes_voltage_template[] = {
+	&sensor_dev_template_voltage_label,
+	&sensor_dev_template_voltage_input,
+	&sensor_dev_template_voltage_min,
+	&sensor_dev_template_voltage_max,
+	NULL,
+};
+
+static const struct sensor_template_group nct6687_voltage_template_group = {
+	.templates = nct6687_attributes_voltage_template,
+	.is_visible = nct6687_voltage_is_visible,
+};
+
+static ssize_t show_fan_label(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr);
+
+	return sprintf(buf, "%s\n", nct6687_fan_label[sattr->index]);
+}
+
+static ssize_t show_fan_value(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	struct nct6687_data *data = nct6687_update_device(dev);
+
+	return sprintf(buf, "%d\n", data->rpm[sattr->index][sattr->nr]);
+}
+
+static umode_t nct6687_fan_is_visible(struct kobject *kobj, struct attribute *attr, int index)
+{
+	return attr->mode;
+}
+
+SENSOR_TEMPLATE(fan_label, "fan%d_label", S_IRUGO, show_fan_label, NULL, 0);
+SENSOR_TEMPLATE_2(fan_input, "fan%d_input", S_IRUGO, show_fan_value, NULL, 0, 0);
+SENSOR_TEMPLATE_2(fan_min, "fan%d_min", S_IRUGO, show_fan_value, NULL, 0, 1);
+SENSOR_TEMPLATE_2(fan_max, "fan%d_max", S_IRUGO, show_fan_value, NULL, 0, 2);
+
+/*
+ * nct6687_fan_is_visible uses the index into the following array
+ * to determine if attributes should be created or not.
+ * Any change in order or content must be matched.
+ */
+static struct sensor_device_template *nct6687_attributes_fan_template[] = {
+	&sensor_dev_template_fan_label,
+	&sensor_dev_template_fan_input,
+	&sensor_dev_template_fan_min,
+	&sensor_dev_template_fan_max,
+	NULL,
+};
+
+static const struct sensor_template_group nct6687_fan_template_group = {
+	.templates = nct6687_attributes_fan_template,
+	.is_visible = nct6687_fan_is_visible,
+	.base = 1,
+};
+
+static ssize_t show_temperature_label(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr);
+
+	return sprintf(buf, "%s\n", nct6687_temp_label[sattr->index]);
+}
+
+static ssize_t show_temperature_value(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	struct nct6687_data *data = nct6687_update_device(dev);
+
+	return sprintf(buf, "%d\n", data->temperature[sattr->index][sattr->nr]);
+}
+
+static umode_t nct6687_temp_is_visible(struct kobject *kobj, struct attribute *attr, int index)
+{
+	return attr->mode;
+}
+
+SENSOR_TEMPLATE(temp_label, "temp%d_label", S_IRUGO, show_temperature_label, NULL, 0);
+SENSOR_TEMPLATE_2(temp_input, "temp%d_input", S_IRUGO, show_temperature_value, NULL, 0, 0);
+SENSOR_TEMPLATE_2(temp_min, "temp%d_min", S_IRUGO, show_temperature_value, NULL, 0, 1);
+SENSOR_TEMPLATE_2(temp_max, "temp%d_max", S_IRUGO, show_temperature_value, NULL, 0, 2);
+
+/*
+ * nct6687_temp_is_visible uses the index into the following array
+ * to determine if attributes should be created or not.
+ * Any change in order or content must be matched.
+ */
+static struct sensor_device_template *nct6687_attributes_temp_template[] = {
+	&sensor_dev_template_temp_input,
+	&sensor_dev_template_temp_label,
+	&sensor_dev_template_temp_min,
+	&sensor_dev_template_temp_max,
+	NULL,
+};
+
+static const struct sensor_template_group nct6687_temp_template_group = {
+	.templates = nct6687_attributes_temp_template,
+	.is_visible = nct6687_temp_is_visible,
+	.base = 1,
+};
+
+static ssize_t show_pwm(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct nct6687_data *data = nct6687_update_device(dev);
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	int index = sattr->index;
+
+	return sprintf(buf, "%d\n", data->pwm[index]);
+}
+
+static ssize_t store_pwm(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
+{
+	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
+	struct nct6687_data *data = dev_get_drvdata(dev);
+	int index = sattr->index;
+	unsigned long val;
+	u16 mode;
+	u8 bitMask;
+
+	if (kstrtoul(buf, 10, &val) || val > 255 || index >= NCT6687_NUM_REG_FAN)
+		return -EINVAL;
+
+	mutex_lock(&data->update_lock);
+
+	nct6687_save_fan_control(data, index);
+
+	mode = nct6687_read(data, NCT6687_REG_FAN_CTRL_MODE(index));
+	bitMask = (u8)(0x01 << index);
+
+	mode = (u8)(mode | bitMask);
+	nct6687_write(data, NCT6687_REG_FAN_CTRL_MODE(index), mode);
+
+	nct6687_write(data, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_FAN_CFG_REQ);
+	msleep(50);
+	nct6687_write(data, NCT6687_REG_PWM_WRITE(index), val);
+	nct6687_write(data, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_FAN_CFG_DONE);
+	msleep(50);
+
+	mutex_unlock(&data->update_lock);
+
+	return count;
+}
+
+SENSOR_TEMPLATE(pwm, "pwm%d", S_IRUGO, show_pwm, store_pwm, 0);
+
+static void nct6687_save_fan_control(struct nct6687_data *data, int index)
+{
+	if (data->_restoreDefaultFanControlRequired[index] == false)
+	{
+		u16 reg = nct6687_read(data, NCT6687_REG_FAN_CTRL_MODE(index));
+		u16 bitMask = 0x01 << index;
+		u8 pwm = nct6687_read(data, NCT6687_REG_FAN_PWM_COMMAND(index));
+
+		data->_initialFanControlMode[index] = (u8)(reg & bitMask);
+		data->_initialFanPwmCommand[index] = pwm;
+
+		data->_restoreDefaultFanControlRequired[index] = true;
+	}
+}
+
+static void nct6687_restore_fan_control(struct nct6687_data *data, int index)
+{
+	if (data->_restoreDefaultFanControlRequired[index])
+	{
+		u8 mode = nct6687_read(data, NCT6687_REG_FAN_CTRL_MODE(index));
+		mode = (u8)(mode & ~data->_initialFanControlMode[index]);
+
+		nct6687_write(data, NCT6687_REG_FAN_CTRL_MODE(index), mode);
+
+		nct6687_write(data, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_FAN_CFG_REQ);
+		msleep(50);
+		nct6687_write(data, NCT6687_REG_PWM_WRITE(index), data->_initialFanPwmCommand[index]);
+		nct6687_write(data, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_FAN_CFG_DONE);
+		msleep(50);
+
+		data->_restoreDefaultFanControlRequired[index] = false;
+
+		pr_debug("nct6687_restore_fan_control[%d], addr=%04X, ctrl=%04X, _initialFanPwmCommand=%d\n", index, NCT6687_REG_FAN_PWM_COMMAND(index), NCT6687_REG_PWM_WRITE(index), data->_initialFanPwmCommand[index]);
+	}
+}
+
+static umode_t nct6687_pwm_is_visible(struct kobject *kobj, struct attribute *attr, int index)
+{
+	return attr->mode | S_IWUSR;
+}
+
+static struct sensor_device_template *nct6687_attributes_pwm_template[] = {
+	&sensor_dev_template_pwm,
+	NULL,
+};
+
+static const struct sensor_template_group nct6687_pwm_template_group = {
+	.templates = nct6687_attributes_pwm_template,
+	.is_visible = nct6687_pwm_is_visible,
+	.base = 1,
+};
+
+/* Get the monitoring functions started */
+static inline void nct6687_init_device(struct nct6687_data *data)
+{
+	u8 tmp;
+
+	pr_debug("nct6687_init_device\n");
+
+	/* Start hardware monitoring if needed */
+	tmp = nct6687_read(data, NCT6687_HWM_CFG);
+	if (!(tmp & 0x80))
+	{
+		pr_debug("nct6687_init_device: 0x%04x\n", tmp);
+		nct6687_write(data, NCT6687_HWM_CFG, tmp | 0x80);
+	}
+
+	// enable SIO voltage
+	nct6687_write(data, 0x1BB, 0x61);
+	nct6687_write(data, 0x1BC, 0x62);
+	nct6687_write(data, 0x1BD, 0x63);
+	nct6687_write(data, 0x1BE, 0x64);
+	nct6687_write(data, 0x1BF, 0x65);
+}
+
+/*
+ * There are a total of 8 fan inputs.
+ */
+static void nct6687_setup_fans(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_FAN; i++)
+	{
+		u16 reg = nct6687_read(data, NCT6687_REG_FAN_CTRL_MODE(i));
+		u16 bitMask = 0x01 << i;
+		u16 rpm = nct6687_read16(data, NCT6687_REG_FAN_RPM(i));
+
+		data->rpm[0][i] = rpm;
+		data->rpm[1][i] = rpm;
+		data->rpm[2][i] = rpm;
+		data->_initialFanControlMode[i] = (u8)(reg & bitMask);
+		data->_restoreDefaultFanControlRequired[i] = false;
+
+		pr_debug("nct6687_setup_fans[%d], %s - addr=%04X, ctrl=%04X, rpm=%d, _initialFanControlMode=%d\n", i, nct6687_fan_label[i], NCT6687_REG_FAN_CTRL_MODE(i), reg, rpm, data->_initialFanControlMode[i]);
+	}
+}
+
+static void nct6687_setup_voltages(struct nct6687_data *data)
+{
+	int index;
+	char buf[64];
+
+	/* Measured voltages and limits */
+	for (index = 0; index < NCT6687_NUM_REG_VOLTAGE; index++)
+	{
+		s16 reg = manual ? index : nct6687_voltage_definition[index].reg;
+		s16 high = nct6687_read(data, NCT6687_REG_VOLTAGE(reg)) * 16;
+		s16 low = ((u16)nct6687_read(data, NCT6687_REG_VOLTAGE(reg) + 1)) >> 4;
+		s16 value = low + high;
+		s16 voltage = manual ? value : value * nct6687_voltage_definition[index].multiplier;
+
+		data->voltage[0][index] = voltage;
+		data->voltage[1][index] = voltage;
+		data->voltage[2][index] = voltage;
+
+		pr_debug("nct6687_setup_voltages[%d], %s, addr=0x%04x, value=%d, voltage=%d\n", index, nct6687_voltage_label(buf, index), NCT6687_REG_VOLTAGE(index), value, voltage);
+	}
+}
+
+static void nct6687_setup_temperatures(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_TEMP; i++)
+	{
+		s32 value = (char)nct6687_read(data, NCT6687_REG_TEMP(i));
+		s32 half = (nct6687_read(data, NCT6687_REG_TEMP(i) + 1) >> 7) & 0x1;
+		s32 temperature = (value * 1000) + (5 * half);
+
+		data->temperature[0][i] = temperature;
+		data->temperature[1][i] = temperature;
+		data->temperature[2][i] = temperature;
+
+		pr_debug("nct6687_setup_temperatures[%d]], addr=%04X, value=%d, half=%d, temperature=%d\n", i, NCT6687_REG_TEMP(i), value, half, temperature);
+	}
+}
+
+static void nct6687_setup_pwm(struct nct6687_data *data)
+{
+	int i;
+
+	for (i = 0; i < NCT6687_NUM_REG_PWM; i++)
+	{
+		data->_initialFanPwmCommand[i] = nct6687_read(data, NCT6687_REG_FAN_PWM_COMMAND(i));
+		data->pwm[i] = nct6687_read(data, NCT6687_REG_PWM(i));
+
+		pr_debug("nct6687_setup_pwm[%d], addr=%04X, pwm=%d, _initialFanPwmCommand=%d\n", i, NCT6687_REG_FAN_PWM_COMMAND(i), data->pwm[i], data->_initialFanPwmCommand[i]);
+	}
+}
+
+static int nct6687_remove(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct nct6687_data *data = dev_get_drvdata(dev);
+	int i;
+
+	mutex_lock(&data->update_lock);
+
+	for (i = 0; i < NCT6687_NUM_REG_FAN; i++)
+	{
+		nct6687_restore_fan_control(data, i);
+	}
+
+	mutex_unlock(&data->update_lock);
+
+	return 0;
+}
+
+static int nct6687_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct nct6687_sio_data *sio_data = dev->platform_data;
+	struct attribute_group *group;
+	struct nct6687_data *data;
+	struct device *hwmon_dev;
+	struct resource *res;
+	int groups = 0;
+	char build[16];
+
+	res = platform_get_resource(pdev, IORESOURCE_IO, 0);
+	if (!devm_request_region(dev, res->start, IOREGION_LENGTH, DRVNAME))
+		return -EBUSY;
+
+	data = devm_kzalloc(dev, sizeof(struct nct6687_data), GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	data->kind = sio_data->kind;
+	data->sioreg = sio_data->sioreg;
+	data->addr = res->start;
+
+	pr_debug("nct6687_probe addr=0x%04X, sioreg=0x%04X\n", data->addr, data->sioreg);
+
+	mutex_init(&data->update_lock);
+
+	platform_set_drvdata(pdev, data);
+
+	nct6687_init_device(data);
+	nct6687_setup_fans(data);
+	nct6687_setup_pwm(data);
+	nct6687_setup_temperatures(data);
+	nct6687_setup_voltages(data);
+
+	/* Register sysfs hooks */
+
+	group = nct6687_create_attr_group(dev, &nct6687_pwm_template_group, NCT6687_NUM_REG_FAN);
+
+	if (IS_ERR(group))
+		return PTR_ERR(group);
+
+	data->groups[groups++] = group;
+
+	group = nct6687_create_attr_group(dev, &nct6687_voltage_template_group, NCT6687_NUM_REG_VOLTAGE);
+
+	if (IS_ERR(group))
+		return PTR_ERR(group);
+
+	data->groups[groups++] = group;
+
+	group = nct6687_create_attr_group(dev, &nct6687_fan_template_group, NCT6687_NUM_REG_FAN);
+
+	if (IS_ERR(group))
+		return PTR_ERR(group);
+
+	data->groups[groups++] = group;
+
+	group = nct6687_create_attr_group(dev, &nct6687_temp_template_group, NCT6687_NUM_REG_TEMP);
+
+	if (IS_ERR(group))
+		return PTR_ERR(group);
+
+	data->groups[groups++] = group;
+
+	scnprintf(build, sizeof(build), "%02d/%02d/%02d", nct6687_read(data, NCT6687_REG_BUILD_MONTH), nct6687_read(data, NCT6687_REG_BUILD_DAY), nct6687_read(data, NCT6687_REG_BUILD_YEAR));
+
+	dev_info(dev, "%s EC firmware version %d.%d build %s\n", nct6687_chip_names[data->kind], nct6687_read(data, NCT6687_REG_VERSION_HI), nct6687_read(data, NCT6687_REG_VERSION_LO), build);
+
+	hwmon_dev = devm_hwmon_device_register_with_groups(dev, nct6687_device_names[data->kind], data, data->groups);
+
+	return PTR_ERR_OR_ZERO(hwmon_dev);
+}
+
+static int nct6687_suspend(struct platform_device *pdev, pm_message_t state)
+{
+	struct nct6687_data *data = nct6687_update_device(&pdev->dev);
+
+	mutex_lock(&data->update_lock);
+	data->hwm_cfg = nct6687_read(data, NCT6687_HWM_CFG);
+	mutex_unlock(&data->update_lock);
+
+	return 0;
+}
+
+static int nct6687_resume(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct nct6687_data *data = dev_get_drvdata(dev);
+
+	mutex_lock(&data->update_lock);
+
+	nct6687_write(data, NCT6687_HWM_CFG, data->hwm_cfg);
+
+	/* Force re-reading all values */
+	data->valid = false;
+	mutex_unlock(&data->update_lock);
+
+	return 0;
+}
+
+// static const struct dev_pm_ops nct6687_dev_pm_ops = {
+// 	.suspend = nct6687_suspend,
+// 	.resume = nct6687_resume,
+// 	.freeze = nct6687_suspend,
+// 	.restore = nct6687_resume,
+// };
+
+#define NCT6687_DEV_PM_OPS NULL
+
+static struct platform_driver nct6687_driver = {
+	.driver = {
+		.name = DRVNAME,
+		.pm = NCT6687_DEV_PM_OPS,
+	},
+	.probe = nct6687_probe,
+	.remove = nct6687_remove,
+	.suspend = nct6687_suspend,
+	.resume = nct6687_resume,
+};
+
+static int __init nct6687_find(int sioaddr, struct nct6687_sio_data *sio_data)
+{
+	u16 address;
+	u16 verify;
+	u16 val;
+	int err;
+
+	err = superio_enter(sioaddr);
+	if (err)
+		return err;
+
+	val = (superio_inb(sioaddr, SIO_REG_DEVID) << 8) | superio_inb(sioaddr, SIO_REG_DEVREVISION);
+
+	pr_debug("found chip ID: 0x%04x\n", val);
+
+	if (val == SIO_NCT6683D_ID) {
+		sio_data->kind = nct6683;
+	} else if (val == SIO_NCT6686_ID || val == SIO_NCT6686D_ID) {
+		sio_data->kind = nct6686;
+	} else if (val == SIO_NCT6687_ID || val == SIO_NCT6687D_ID || force)
+	{
+		sio_data->kind = nct6687;
+	}
+	else
+	{
+		if (val != 0xffff)
+			pr_debug("unsupported chip ID: 0x%04x\n", val);
+		goto fail;
+	}
+
+	/* We have a known chip, find the HWM I/O address */
+	superio_select(sioaddr, NCT6687_LD_HWM);
+	address = (superio_inb(sioaddr, SIO_REG_ADDR) << 8) | superio_inb(sioaddr, SIO_REG_ADDR + 1);
+	ssleep(1);
+	verify = (superio_inb(sioaddr, SIO_REG_ADDR) << 8) | superio_inb(sioaddr, SIO_REG_ADDR + 1);
+
+	if (address == 0 || address != verify)
+	{
+		pr_err("EC base I/O port unconfigured\n");
+		goto fail;
+	}
+
+	if ((address & 0x07) == 0x05)
+		address &= 0xFFF8;
+
+	if (address < 0x100 || (address & 0xF007) != 0)
+	{
+		pr_err("EC Invalid address: 0x%04X\n", address);
+		goto fail;
+	}
+
+	/* Activate logical device if needed */
+	val = superio_inb(sioaddr, SIO_REG_ENABLE);
+	if (!(val & 0x01))
+	{
+		pr_warn("Forcibly enabling EC access. Data may be unusable.\n");
+		superio_outb(sioaddr, SIO_REG_ENABLE, val | 0x01);
+	}
+
+	superio_exit(sioaddr);
+	pr_info("Found %s or compatible chip at 0x%04x:0x%04x\n", nct6687_chip_names[sio_data->kind], sioaddr, address);
+	sio_data->sioreg = sioaddr;
+
+	return address;
+
+fail:
+	superio_exit(sioaddr);
+	return -ENODEV;
+}
+
+/*
+ * when Super-I/O functions move to a separate file, the Super-I/O
+ * bus will manage the lifetime of the device and this module will only keep
+ * track of the nct6687 driver. But since we use platform_device_alloc(), we
+ * must keep track of the device
+ */
+static struct platform_device *pdev[2];
+
+static int __init sensors_nct6687_init(void)
+{
+	struct nct6687_sio_data sio_data;
+	int sioaddr[2] = {0x2e, 0x4e};
+	struct resource res;
+	bool found = false;
+	int address;
+	int i, err;
+
+	err = platform_driver_register(&nct6687_driver);
+	if (err)
+		return err;
+
+	/*
+	 * initialize sio_data->kind and sio_data->sioreg.
+	 *
+	 * when Super-I/O functions move to a separate file, the Super-I/O
+	 * driver will probe 0x2e and 0x4e and auto-detect the presence of a
+	 * nct6687 hardware monitor, and call probe()
+	 */
+	for (i = 0; i < ARRAY_SIZE(pdev); i++)
+	{
+		address = nct6687_find(sioaddr[i], &sio_data);
+		if (address <= 0)
+			continue;
+
+		found = true;
+
+		pdev[i] = platform_device_alloc(DRVNAME, address);
+		if (!pdev[i])
+		{
+			err = -ENOMEM;
+			goto exit_device_unregister;
+		}
+
+		err = platform_device_add_data(pdev[i], &sio_data, sizeof(struct nct6687_sio_data));
+		if (err)
+			goto exit_device_put;
+
+		memset(&res, 0, sizeof(res));
+
+		res.name = DRVNAME;
+		res.start = address + IOREGION_OFFSET;
+		res.end = address + IOREGION_OFFSET + IOREGION_LENGTH - 1;
+		res.flags = IORESOURCE_IO;
+
+		err = acpi_check_resource_conflict(&res);
+		if (err)
+		{
+			platform_device_put(pdev[i]);
+			pdev[i] = NULL;
+			continue;
+		}
+
+		err = platform_device_add_resources(pdev[i], &res, 1);
+		if (err)
+			goto exit_device_put;
+
+		/* platform_device_add calls probe() */
+		err = platform_device_add(pdev[i]);
+		if (err)
+			goto exit_device_put;
+	}
+
+	if (!found)
+	{
+		err = -ENODEV;
+		goto exit_unregister;
+	}
+
+	return 0;
+
+exit_device_put:
+	platform_device_put(pdev[i]);
+
+exit_device_unregister:
+	while (--i >= 0)
+	{
+		if (pdev[i])
+			platform_device_unregister(pdev[i]);
+	}
+
+exit_unregister:
+	platform_driver_unregister(&nct6687_driver);
+
+	return err;
+}
+
+static void __exit sensors_nct6687_exit(void)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(pdev); i++)
+	{
+		if (pdev[i])
+			platform_device_unregister(pdev[i]);
+	}
+
+	platform_driver_unregister(&nct6687_driver);
+}
+
+MODULE_AUTHOR("Frederic Boltz <frederic.boltz@gmail.com>");
+MODULE_DESCRIPTION("Driver for NCT6687D");
+MODULE_LICENSE("GPL");
+
+module_init(sensors_nct6687_init);
+module_exit(sensors_nct6687_exit);
-- 
2.34.1


             reply	other threads:[~2023-07-20 11:09 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-07-20 11:09 Thomas R [this message]
2023-07-20 13:25 ` Guenter Roeck

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=b1a0bd3b-a32d-aff0-db4e-d9ba770e3a49@gmail.com \
    --to=seatommyboy@gmail.com \
    --cc=jdelvare@suse.com \
    --cc=linux-hwmon@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux@roeck-us.net \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

Powered by JetHome