[PATCH v4 3/3] hwmon: Driver for Nuvoton NCT7363Y

Zev Weiss zev at bewilderbeest.net
Sat Mar 2 19:18:57 AEDT 2024


On Mon, Feb 26, 2024 at 04:56:06PM PST, baneric926 at gmail.com wrote:
>From: Ban Feng <kcfeng0 at nuvoton.com>
>
>NCT7363Y is an I2C based hardware monitoring chip from Nuvoton.
>
>Signed-off-by: Ban Feng <kcfeng0 at nuvoton.com>
>---
> Documentation/hwmon/index.rst   |   1 +
> Documentation/hwmon/nct7363.rst |  33 +++
> MAINTAINERS                     |   2 +
> drivers/hwmon/Kconfig           |  11 +
> drivers/hwmon/Makefile          |   1 +
> drivers/hwmon/nct7363.c         | 412 ++++++++++++++++++++++++++++++++
> 6 files changed, 460 insertions(+)
> create mode 100644 Documentation/hwmon/nct7363.rst
> create mode 100644 drivers/hwmon/nct7363.c
>
>diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
>index c7ed1f73ac06..302f954b45be 100644
>--- a/Documentation/hwmon/index.rst
>+++ b/Documentation/hwmon/index.rst
>@@ -165,6 +165,7 @@ Hardware Monitoring Kernel Drivers
>    mp5990
>    nct6683
>    nct6775
>+   nct7363
>    nct7802
>    nct7904
>    npcm750-pwm-fan
>diff --git a/Documentation/hwmon/nct7363.rst b/Documentation/hwmon/nct7363.rst
>new file mode 100644
>index 000000000000..89699c95aa4b
>--- /dev/null
>+++ b/Documentation/hwmon/nct7363.rst
>@@ -0,0 +1,33 @@
>+.. SPDX-License-Identifier: GPL-2.0
>+
>+Kernel driver nct7363
>+=====================
>+
>+Supported chip:
>+
>+  * Nuvoton NCT7363Y
>+
>+    Prefix: nct7363
>+
>+    Addresses: I2C 0x20, 0x21, 0x22, 0x23
>+
>+Author: Ban Feng <kcfeng0 at nuvoton.com>
>+
>+
>+Description
>+-----------
>+
>+The NCT7363Y is a Fan controller which provides up to 16 independent
>+FAN input monitors, and up to 16 independent PWM output with SMBus interface.
>+
>+
>+Sysfs entries
>+-------------
>+
>+Currently, the driver supports the following features:
>+
>+======================= =======================================================
>+fanX_input		provide current fan rotation value in RPM
>+
>+pwmX			get or set PWM fan control value.
>+======================= =======================================================
>diff --git a/MAINTAINERS b/MAINTAINERS
>index 7b1efefed7c4..7ca66b713e30 100644
>--- a/MAINTAINERS
>+++ b/MAINTAINERS
>@@ -15089,6 +15089,8 @@ M:	Ban Feng <kcfeng0 at nuvoton.com>
> L:	linux-hwmon at vger.kernel.org
> S:	Maintained
> F:	Documentation/devicetree/bindings/hwmon/nuvoton,nct7363.yaml
>+F:	Documentation/hwmon/nct7363.rst
>+F:	drivers/hwmon/nct7363.c
>
> NETDEVSIM
> M:	Jakub Kicinski <kuba at kernel.org>
>diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
>index a608264da87d..a297b5409b04 100644
>--- a/drivers/hwmon/Kconfig
>+++ b/drivers/hwmon/Kconfig
>@@ -1616,6 +1616,17 @@ config SENSORS_NCT6775_I2C
> 	  This driver can also be built as a module. If so, the module
> 	  will be called nct6775-i2c.
>
>+config SENSORS_NCT7363
>+	tristate "Nuvoton NCT7363Y"
>+	depends on I2C
>+	select REGMAP_I2C
>+	help
>+	  If you say yes here you get support for the Nuvoton NCT7363Y,
>+	  hardware monitoring chip.
>+
>+	  This driver can also be built as a module. If so, the module
>+	  will be called nct7363.
>+
> config SENSORS_NCT7802
> 	tristate "Nuvoton NCT7802Y"
> 	depends on I2C
>diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
>index 47be39af5c03..d5e7531204df 100644
>--- a/drivers/hwmon/Makefile
>+++ b/drivers/hwmon/Makefile
>@@ -167,6 +167,7 @@ obj-$(CONFIG_SENSORS_NCT6775_CORE) += nct6775-core.o
> nct6775-objs			:= nct6775-platform.o
> obj-$(CONFIG_SENSORS_NCT6775)	+= nct6775.o
> obj-$(CONFIG_SENSORS_NCT6775_I2C) += nct6775-i2c.o
>+obj-$(CONFIG_SENSORS_NCT7363)	+= nct7363.o
> obj-$(CONFIG_SENSORS_NCT7802)	+= nct7802.o
> obj-$(CONFIG_SENSORS_NCT7904)	+= nct7904.o
> obj-$(CONFIG_SENSORS_NPCM7XX)	+= npcm750-pwm-fan.o
>diff --git a/drivers/hwmon/nct7363.c b/drivers/hwmon/nct7363.c
>new file mode 100644
>index 000000000000..c79d3ca4f111
>--- /dev/null
>+++ b/drivers/hwmon/nct7363.c
>@@ -0,0 +1,412 @@
>+// SPDX-License-Identifier: GPL-2.0-or-later
>+/*
>+ * Copyright (c) 2023 Nuvoton Technology corporation.
>+ */
>+
>+#include <linux/bitfield.h>
>+#include <linux/bits.h>
>+#include <linux/err.h>
>+#include <linux/hwmon.h>
>+#include <linux/hwmon-sysfs.h>
>+#include <linux/i2c.h>
>+#include <linux/module.h>
>+#include <linux/mutex.h>
>+#include <linux/regmap.h>
>+#include <linux/slab.h>
>+
>+#define NCT7363_REG_GPIO_0_3		0x20
>+#define NCT7363_REG_GPIO_4_7		0x21
>+#define NCT7363_REG_GPIO_10_13		0x22
>+#define NCT7363_REG_GPIO_14_17		0x23
>+#define NCT7363_REG_PWMEN_0_7		0x38
>+#define NCT7363_REG_PWMEN_8_15		0x39
>+#define NCT7363_REG_FANINEN_0_7		0x41
>+#define NCT7363_REG_FANINEN_8_15	0x42
>+#define NCT7363_REG_FANINX_HVAL(x)	(0x48 + ((x) * 2))
>+#define NCT7363_REG_FANINX_LVAL(x)	(0x49 + ((x) * 2))
>+#define NCT7363_REG_FSCPXDUTY(x)	(0x90 + ((x) * 2))
>+
>+#define PWM_SEL(x)			(BIT(0) << (((x) % 4) * 2))
>+#define FANIN_SEL(x)			(BIT(1) << (((x) % 4) * 2))
>+
>+#define NCT7363_FANINX_LVAL_MASK	GENMASK(4, 0)
>+#define NCT7363_FANIN_MASK		GENMASK(12, 0)
>+
>+#define NCT7363_PWM_COUNT		16
>+
>+static inline unsigned long FAN_FROM_REG(u16 val)
>+{
>+	/* In case fan is stopped or divide by 0 */
>+	if (val == NCT7363_FANIN_MASK || val == 0)
>+		return	0;
>+
>+	return (1350000UL / val);
>+}
>+
>+static const struct of_device_id nct7363_of_match[] = {
>+	{ .compatible = "nuvoton,nct7363" },

As far as I can see from the code in this driver, it looks like this 
driver should also be compatible with the nct7362; shall we add the ID 
table entry for it now?  (Though I only have a datasheet for the 
nct7362, not the nct7363, so I don't know exactly how they differ.)

>+	{ },
>+};
>+MODULE_DEVICE_TABLE(of, nct7363_of_match);
>+
>+struct nct7363_data {
>+	struct regmap		*regmap;
>+	struct mutex		lock; /* protect register access */
>+
>+	u16			fanin_mask;
>+	u16			pwm_mask;
>+};
>+
>+static int nct7363_read_fan(struct device *dev, u32 attr, int channel,
>+			    long *val)
>+{
>+	struct nct7363_data *data = dev_get_drvdata(dev);
>+	unsigned int hi, lo;
>+	u16 cnt, rpm;
>+	int ret = 0;
>+
>+	switch (attr) {
>+	case hwmon_fan_input:
>+		/*
>+		 * High-byte register should be read first to latch
>+		 * synchronous low-byte value
>+		 */
>+		ret = regmap_read(data->regmap,
>+				  NCT7363_REG_FANINX_HVAL(channel), &hi);
>+		if (ret)
>+			return ret;
>+
>+		ret = regmap_read(data->regmap,
>+			NCT7363_REG_FANINX_LVAL(channel), &lo);
>+		if (ret)
>+			return ret;

I think this pair of reads should be done under data->lock to ensure 
that the LVAL read gets the right latched value in a scenario where 
multiple threads executing this function end up with their register 
reads interleaved.

>+
>+		cnt = (hi << 5) | (lo & NCT7363_FANINX_LVAL_MASK);
>+		rpm = FAN_FROM_REG(cnt);
>+		*val = (long)rpm;

Given that rpm is assigned from an unsigned long (FAN_FROM_REG()) and 
then to a long (*val), is there any reason we want it as a u16 in 
between the two?  Or for that matter, why not just:

   *val = FAN_FROM_REG(cnt);

?

>+		return 0;
>+	default:
>+		return -EOPNOTSUPP;
>+	}
>+}
>+
>+static umode_t nct7363_fan_is_visible(const void *_data, u32 attr, int channel)
>+{
>+	const struct nct7363_data *data = _data;
>+
>+	switch (attr) {
>+	case hwmon_fan_input:
>+		if (data->fanin_mask & BIT(channel))
>+			return 0444;
>+		break;
>+	default:
>+		break;
>+	}
>+
>+	return 0;
>+}
>+
>+static int nct7363_read_pwm(struct device *dev, u32 attr, int channel,
>+			    long *val)
>+{
>+	struct nct7363_data *data = dev_get_drvdata(dev);
>+	unsigned int regval;
>+	u16 ret;
>+
>+	switch (attr) {
>+	case hwmon_pwm_input:
>+		ret = regmap_read(data->regmap,
>+				  NCT7363_REG_FSCPXDUTY(channel), &regval);
>+		if (ret)
>+			return ret;
>+
>+		*val = (long)regval;
>+		return 0;
>+	default:
>+		return -EOPNOTSUPP;
>+	}
>+}
>+
>+static int nct7363_write_pwm(struct device *dev, u32 attr, int channel,
>+			     long val)
>+{
>+	struct nct7363_data *data = dev_get_drvdata(dev);
>+	int ret;
>+
>+	switch (attr) {
>+	case hwmon_pwm_input:
>+		if (val < 0 || val > 255)
>+			return -EINVAL;
>+
>+		mutex_lock(&data->lock);
>+		ret = regmap_write(data->regmap,
>+				   NCT7363_REG_FSCPXDUTY(channel), val);
>+		mutex_unlock(&data->lock);

...though here, I'm not sure the locking is needed -- is there something 
that necessitates it for a single register write?

>+
>+		return ret;
>+
>+	default:
>+		return -EOPNOTSUPP;
>+	}
>+}
>+
>+static umode_t nct7363_pwm_is_visible(const void *_data, u32 attr, int channel)
>+{
>+	const struct nct7363_data *data = _data;
>+
>+	switch (attr) {
>+	case hwmon_pwm_input:
>+		if (data->pwm_mask & BIT(channel))
>+			return 0644;
>+		break;
>+	default:
>+		break;
>+	}
>+
>+	return 0;
>+}
>+
>+static int nct7363_read(struct device *dev, enum hwmon_sensor_types type,
>+			u32 attr, int channel, long *val)
>+{
>+	switch (type) {
>+	case hwmon_fan:
>+		return nct7363_read_fan(dev, attr, channel, val);
>+	case hwmon_pwm:
>+		return nct7363_read_pwm(dev, attr, channel, val);
>+	default:
>+		return -EOPNOTSUPP;
>+	}
>+}
>+
>+static int nct7363_write(struct device *dev, enum hwmon_sensor_types type,
>+			 u32 attr, int channel, long val)
>+{
>+	switch (type) {
>+	case hwmon_pwm:
>+		return nct7363_write_pwm(dev, attr, channel, val);
>+	default:
>+		return -EOPNOTSUPP;
>+	}
>+}
>+
>+static umode_t nct7363_is_visible(const void *data,
>+				  enum hwmon_sensor_types type,
>+				  u32 attr, int channel)
>+{
>+	switch (type) {
>+	case hwmon_fan:
>+		return nct7363_fan_is_visible(data, attr, channel);
>+	case hwmon_pwm:
>+		return nct7363_pwm_is_visible(data, attr, channel);
>+	default:
>+		return 0;
>+	}
>+}
>+
>+static const struct hwmon_channel_info *nct7363_info[] = {
>+	HWMON_CHANNEL_INFO(fan,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT,
>+			   HWMON_F_INPUT),
>+	HWMON_CHANNEL_INFO(pwm,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT,
>+			   HWMON_PWM_INPUT),
>+	NULL
>+};
>+
>+static const struct hwmon_ops nct7363_hwmon_ops = {
>+	.is_visible = nct7363_is_visible,
>+	.read = nct7363_read,
>+	.write = nct7363_write,
>+};
>+
>+static const struct hwmon_chip_info nct7363_chip_info = {
>+	.ops = &nct7363_hwmon_ops,
>+	.info = nct7363_info,
>+};
>+
>+static int nct7363_init_chip(struct nct7363_data *data)
>+{
>+	u8 i, gpio0_3 = 0, gpio4_7 = 0, gpio10_13 = 0, gpio14_17 = 0;
>+	int ret;
>+
>+	for (i = 0; i < NCT7363_PWM_COUNT; i++) {
>+		if (i < 4) {
>+			if (data->pwm_mask & BIT(i))
>+				gpio0_3 |= PWM_SEL(i);
>+			if (data->fanin_mask & BIT(i))
>+				gpio10_13 |= FANIN_SEL(i);
>+		} else if (i < 8) {
>+			if (data->pwm_mask & BIT(i))
>+				gpio4_7 |= PWM_SEL(i);
>+			if (data->fanin_mask & BIT(i))
>+				gpio14_17 |= FANIN_SEL(i);
>+		} else if (i < 12) {
>+			if (data->pwm_mask & BIT(i))
>+				gpio10_13 |= PWM_SEL(i);
>+			if (data->fanin_mask & BIT(i))
>+				gpio0_3 |= FANIN_SEL(i);
>+		} else {
>+			if (data->pwm_mask & BIT(i))
>+				gpio14_17 |= PWM_SEL(i);
>+			if (data->fanin_mask & BIT(i))
>+				gpio4_7 |= FANIN_SEL(i);
>+		}
>+	}

With the caveat that I haven't actually sketched it out myself to be 
sure, might it be a bit cleaner to do this with a 4-element array 
indexed by 'i / 4' instead of a big if/else chain and a bunch of 
near-duplicated blocks?

>+
>+	/* Pin Function Configuration */
>+	ret = regmap_write(data->regmap, NCT7363_REG_GPIO_0_3, gpio0_3);
>+	if (ret < 0)
>+		return ret;
>+	ret = regmap_write(data->regmap, NCT7363_REG_GPIO_4_7, gpio4_7);
>+	if (ret < 0)
>+		return ret;
>+	ret = regmap_write(data->regmap, NCT7363_REG_GPIO_10_13, gpio10_13);
>+	if (ret < 0)
>+		return ret;
>+	ret = regmap_write(data->regmap, NCT7363_REG_GPIO_14_17, gpio14_17);
>+	if (ret < 0)
>+		return ret;
>+
>+	/* PWM and FANIN Monitoring Enable */
>+	ret = regmap_write(data->regmap, NCT7363_REG_PWMEN_0_7,
>+			   data->pwm_mask & 0xff);
>+	if (ret < 0)
>+		return ret;
>+	ret = regmap_write(data->regmap, NCT7363_REG_PWMEN_8_15,
>+			   (data->pwm_mask >> 8) & 0xff);
>+	if (ret < 0)
>+		return ret;
>+	ret = regmap_write(data->regmap, NCT7363_REG_FANINEN_0_7,
>+			   data->fanin_mask & 0xff);
>+	if (ret < 0)
>+		return ret;
>+	ret = regmap_write(data->regmap, NCT7363_REG_FANINEN_8_15,
>+			   (data->fanin_mask >> 8) & 0xff);
>+	if (ret < 0)
>+		return ret;
>+
>+	return 0;
>+}
>+
>+static int nct7363_present_pwm_fanin(struct device *dev,
>+				     struct device_node *child,
>+				     struct nct7363_data *data)
>+{
>+	struct of_phandle_args args;
>+	int ret, fanin_cnt;
>+	u8 *fanin_ch;
>+	u8 ch, index;
>+
>+	ret = of_parse_phandle_with_args(child, "pwms", "#pwm-cells",
>+					 0, &args);
>+	if (ret)
>+		return ret;
>+
>+	data->pwm_mask |= BIT(args.args[0]);

Perhaps we should have

   if (args.args[0] >= NCT7363_PWM_COUNT)
     return -EINVAL;

here?

>+
>+	fanin_cnt = of_property_count_u8_elems(child, "tach-ch");
>+	if (fanin_cnt < 1)

fanin_cnt < 1 || fanin_cnt >= NCT7363_PWM_COUNT

>+		return -EINVAL;
>+
>+	fanin_ch = devm_kcalloc(dev, fanin_cnt, sizeof(*fanin_ch), GFP_KERNEL);

Keeping this allocation around persistently for the whole lifetime of 
the device seems unnecessary -- it's not used beyond this function, 
right?  In fact, dynamically allocating it at all seems like overkill, 
considering that in addition to being temporary, it's also got a pretty 
small upper bound on its size.  I'd think a simple

   u8 fanin_ch[NCT7363_PWM_COUNT];

would suffice?  (Along with the check above to ensure fanin_cnt is 
within range of course.)

>+	if (!fanin_ch)
>+		return -ENOMEM;
>+
>+	ret = of_property_read_u8_array(child, "tach-ch", fanin_ch, fanin_cnt);
>+	if (ret)
>+		return ret;
>+
>+	for (ch = 0; ch < fanin_cnt; ch++) {
>+		index = fanin_ch[ch];

...and likewise a range check here too?

>+		data->fanin_mask |= BIT(index);
>+	}
>+

Should we also grab the pulses-per-revolution property here and factor 
that in in FAN_FROM_REG()?

>+	return 0;
>+}
>+
>+static const struct regmap_config nct7363_regmap_config = {
>+	.reg_bits = 8,
>+	.val_bits = 8,
>+};
>+
>+static int nct7363_probe(struct i2c_client *client)
>+{
>+	struct device *dev = &client->dev;
>+	struct device_node *child;
>+	struct nct7363_data *data;
>+	struct device *hwmon_dev;
>+	int ret;
>+
>+	data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
>+	if (!data)
>+		return -ENOMEM;
>+
>+	data->regmap = devm_regmap_init_i2c(client, &nct7363_regmap_config);
>+	if (IS_ERR(data->regmap))
>+		return PTR_ERR(data->regmap);
>+
>+	mutex_init(&data->lock);
>+
>+	for_each_child_of_node(dev->of_node, child) {
>+		ret = nct7363_present_pwm_fanin(dev, child, data);
>+		if (ret) {
>+			of_node_put(child);
>+			return ret;
>+		}
>+	}
>+
>+	/* Initialize the chip */
>+	ret = nct7363_init_chip(data);
>+	if (ret)
>+		return ret;
>+
>+	hwmon_dev =
>+		devm_hwmon_device_register_with_info(dev, client->name, data,
>+						     &nct7363_chip_info, NULL);
>+	return PTR_ERR_OR_ZERO(hwmon_dev);
>+}
>+
>+static struct i2c_driver nct7363_driver = {
>+	.class = I2C_CLASS_HWMON,

Maybe add an i2c_device_id table to accompany the of_match table?

>+	.driver = {
>+		.name = "nct7363",
>+		.of_match_table = nct7363_of_match,
>+	},
>+	.probe = nct7363_probe,
>+};
>+
>+module_i2c_driver(nct7363_driver);
>+
>+MODULE_AUTHOR("CW Ho <cwho at nuvoton.com>");
>+MODULE_AUTHOR("Ban Feng <kcfeng0 at nuvoton.com>");
>+MODULE_DESCRIPTION("NCT7363 Hardware Monitoring Driver");
>+MODULE_LICENSE("GPL");
>-- 
>2.34.1
>
>


More information about the openbmc mailing list