[PATCH 4/4] hwmon: (witrn) Add monitoring support

Rong Zhang posted 4 patches 6 days, 23 hours ago
[PATCH 4/4] hwmon: (witrn) Add monitoring support
Posted by Rong Zhang 6 days, 23 hours ago
With sensor data collected, they can be exported to userspace via hwmon.

Register hwmon driver for witrn, with appropriate channels and
attributes.

Developed and tested with WITRN K2 USB-C tester. Theoretically this
driver should work properly on other models. They can be added to the
HID match table too if someone tests the driver with them.

Signed-off-by: Rong Zhang <i@rong.moe>
---
 Documentation/hwmon/witrn.rst |  30 +++
 drivers/hwmon/Kconfig         |   1 +
 drivers/hwmon/witrn.c         | 423 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 454 insertions(+)

diff --git a/Documentation/hwmon/witrn.rst b/Documentation/hwmon/witrn.rst
index e64c527928d0..f13323fdd9d9 100644
--- a/Documentation/hwmon/witrn.rst
+++ b/Documentation/hwmon/witrn.rst
@@ -21,3 +21,33 @@ Description
 This driver implements support for the WITRN USB tester family.
 
 The device communicates with the custom protocol over USB HID.
+
+As current can flow in both directions through the tester the sign of the
+channel "curr1_input" (label "IBUS") indicates the direction.
+
+Sysfs entries
+-------------
+
+  ===============  ==========  ==============================================================
+  Name             Label       Description
+  ===============  ==========  ==============================================================
+  in0_input        VBUS        Measured VBUS voltage (mV)
+  in0_average      VBUS        Calculated average VBUS voltage (mV)
+  in1_input        D+          Measured D+ voltage (mV)
+  in2_input        D-          Measured D- voltage (mV)
+  in3_input        CC1         Measured CC1 voltage (mV)
+  in4_input        CC2         Measured CC2 voltage (mV)
+  cur1_input       IBUS        Measured VBUS current (mA)
+  curr1_average    IBUS        Calculated average VBUS current (mA)
+  curr1_rated_min  IBUS        Stop accumulating (recording) below this VBUS current (mA)
+  power1_input     PBUS        Calculated VBUS power (uW)
+  power1_average   PBUS        Calculated average VBUS power (uW)
+  energy1_input    EBUS        Accumulated VBUS energy (uJ)
+  charge1_input    CBUS        Accumulated VBUS charge (mC)
+  temp1_input      Thermistor  Measured thermistor temperature (m°C), -EXDEV if not connected
+  record_group                 ID of the record group for accumulative values
+  record_time                  Accumulated time for recording (s), see also curr1_rated_min
+  uptime                       Accumulated time since the device has been powered on (s)
+  ===============  ==========  ==============================================================
+
+All entries are readonly.
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 746184608f81..c8b5144707a1 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -2632,6 +2632,7 @@ config SENSORS_W83627EHF
 config SENSORS_WITRN
 	tristate "WITRN USB tester"
 	depends on USB_HID
+	select HWMON_FP
 	help
 	  If you say yes here you get support for WITRN USB charging
 	  testers.
diff --git a/drivers/hwmon/witrn.c b/drivers/hwmon/witrn.c
index e8713da6de5a..f43bdbf13435 100644
--- a/drivers/hwmon/witrn.c
+++ b/drivers/hwmon/witrn.c
@@ -11,14 +11,19 @@
 #include <linux/cleanup.h>
 #include <linux/device.h>
 #include <linux/hid.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
 #include <linux/jiffies.h>
 #include <linux/limits.h>
 #include <linux/module.h>
 #include <linux/spinlock.h>
+#include <linux/sysfs.h>
 #include <linux/types.h>
 #include <linux/units.h>
 #include <linux/workqueue.h>
 
+#include "hwmon-fp.h"
+
 #define DRIVER_NAME		"witrn"
 #define WITRN_EP_CMD_OUT	0x01
 #define WITRN_EP_DATA_IN	0x81
@@ -74,6 +79,7 @@ struct witrn_report {
 static_assert(sizeof(struct witrn_report) == WITRN_REPORT_SZ);
 
 struct witrn_priv {
+	struct device *hwmon_dev;
 	struct hid_device *hdev;
 
 	struct work_struct pause_work;
@@ -178,6 +184,413 @@ static int witrn_raw_event(struct hid_device *hdev, struct hid_report *report,
 	return 0;
 }
 
+/* ======== HWMON ======== */
+
+static int witrn_collect_sensor(struct witrn_priv *priv, struct witrn_sensor *sensor)
+{
+	int ret;
+
+	scoped_guard(spinlock, &priv->lock) {
+		priv->last_access = jiffies;
+
+		if (!sensor_is_outdated(priv)) {
+			memcpy(sensor, &priv->sensor, sizeof(priv->sensor));
+			return 0;
+		}
+
+		reinit_completion(&priv->completion);
+	}
+
+	ret = witrn_open_hid(priv);
+	if (ret)
+		return ret;
+
+	ret = wait_for_completion_interruptible_timeout(&priv->completion,
+							UP_TO_DATE_TIMEOUT);
+	if (ret == 0)
+		return -ETIMEDOUT;
+	else if (ret < 0)
+		return ret;
+
+	scoped_guard(spinlock, &priv->lock)
+		memcpy(sensor, &priv->sensor, sizeof(priv->sensor));
+
+	return 0;
+}
+
+#define SECS_PER_HOUR		3600ULL
+#define WITRN_SCALE_IN_VCC	(HWMON_FP_SCALE_IN / DECI)		/* dV to mV */
+#define WITRN_SCALE_CHARGE	(HWMON_FP_SCALE_CURR * SECS_PER_HOUR)	/* Ah to mC(mAs) */
+#define WITRN_SCALE_ENERGY	(HWMON_FP_SCALE_ENERGY * SECS_PER_HOUR)	/* Wh to uJ(uWs) */
+
+static int witrn_read_in(const struct witrn_sensor *sensor, u32 attr, int channel, long *val)
+{
+	switch (attr) {
+	case hwmon_in_input:
+		switch (channel) {
+		case 0:
+			return hwmon_fp_float_to_long(le32_to_cpu(sensor->vbus),
+						      HWMON_FP_SCALE_IN, val);
+		case 1:
+			return hwmon_fp_float_to_long(le32_to_cpu(sensor->vdp),
+						      HWMON_FP_SCALE_IN, val);
+		case 2:
+			return hwmon_fp_float_to_long(le32_to_cpu(sensor->vdm),
+						      HWMON_FP_SCALE_IN, val);
+		case 3:
+			*val = sensor->vcc1 * WITRN_SCALE_IN_VCC;
+			return 0;
+		case 4:
+			*val = sensor->vcc2 * WITRN_SCALE_IN_VCC;
+			return 0;
+		default:
+			return -EOPNOTSUPP;
+		}
+	case hwmon_in_average:
+		switch (channel) {
+		case 0:
+			return hwmon_fp_div_to_long(le32_to_cpu(sensor->record_energy),
+						    le32_to_cpu(sensor->record_charge),
+						    HWMON_FP_SCALE_IN, true, val);
+		default:
+			return -EOPNOTSUPP;
+		}
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int witrn_read_curr(const struct witrn_sensor *sensor, u32 attr, int channel, long *val)
+{
+	int ret;
+
+	switch (attr) {
+	case hwmon_curr_input:
+		switch (channel) {
+		case 0:
+			return hwmon_fp_float_to_long(le32_to_cpu(sensor->ibus),
+						      HWMON_FP_SCALE_CURR, val);
+		default:
+			return -EOPNOTSUPP;
+		}
+	case hwmon_curr_average:
+		switch (channel) {
+		case 0: {
+			s64 record_time = le32_to_cpu(sensor->record_time);
+			s64 capacity; /* mC(mAs) */
+
+			if (record_time == 0) {
+				*val = 0;
+				return 0;
+			}
+
+			ret = hwmon_fp_float_to_s64(le32_to_cpu(sensor->record_charge),
+						    WITRN_SCALE_CHARGE, &capacity);
+			if (ret)
+				return ret;
+
+			/* mC(mAs) / s = mA */
+			*val = hwmon_fp_s64_to_long(capacity / record_time);
+			return 0;
+		}
+		default:
+			return -EOPNOTSUPP;
+		}
+	case hwmon_curr_rated_min:
+		switch (channel) {
+		case 0:
+			*val = le16_to_cpu(sensor->record_threshold); /* already in mA */
+			return 0;
+		default:
+			return -EOPNOTSUPP;
+		}
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int witrn_read_power(const struct witrn_sensor *sensor, u32 attr, int channel, long *val)
+{
+	int ret;
+
+	switch (attr) {
+	case hwmon_power_input:
+		switch (channel) {
+		case 0:
+			/*
+			 * The device provides 1e-5 precision.
+			 *
+			 * Though userspace programs can calculate (VBUS * IBUS)
+			 * themselves, this channel is provided for convenience
+			 * and accuracy.
+			 *
+			 * E.g., when VBUS = 5.00049V and IBUS = 0.50049A,
+			 * userspace calculates 5.000V * 0.500A = 2.500000W,
+			 * while this channel reports 2.502695W.
+			 */
+			return hwmon_fp_mul_to_long(le32_to_cpu(sensor->vbus),
+						    le32_to_cpu(sensor->ibus),
+						    HWMON_FP_SCALE_POWER, val);
+		default:
+			return -EOPNOTSUPP;
+		}
+	case hwmon_power_average:
+		switch (channel) {
+		case 0: {
+			s64 record_time = le32_to_cpu(sensor->record_time);
+			s64 energy; /* uJ(uWs) */
+
+			if (record_time == 0) {
+				*val = 0;
+				return 0;
+			}
+
+			ret = hwmon_fp_float_to_s64(le32_to_cpu(sensor->record_energy),
+						    WITRN_SCALE_ENERGY, &energy);
+			if (ret)
+				return ret;
+
+			/* uJ(uWs) / s = uW */
+			*val = hwmon_fp_s64_to_long(energy / record_time);
+			return 0;
+		}
+		default:
+			return -EOPNOTSUPP;
+		}
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int witrn_read_temp(const struct witrn_sensor *sensor, u32 attr, int channel, long *val)
+{
+	int ret;
+
+	switch (attr) {
+	case hwmon_temp_input:
+		switch (channel) {
+		case 0:
+			ret = hwmon_fp_float_to_long(le32_to_cpu(sensor->temp_ntc),
+						     HWMON_FP_SCALE_TEMP, val);
+
+			/*
+			 * The thermistor (NTC, B=3435, T0=25°C, R0=10kohm) is an optional
+			 * addon. When it's missing, an extremely cold temperature
+			 * (-50°C - -80°C) is reported as the device deduced a very large
+			 * resistance value (~500Kohm - ~5Mohm).
+			 *
+			 * We choose -40°C (~250kohm) as the threshold to determine whether
+			 * the thermistor is connected.
+			 *
+			 * The addon can be connected to the device after the device being
+			 * connected to the PC, so we can't use is_visible to hide it.
+			 */
+			if (!ret && *val < -40L * (long)HWMON_FP_SCALE_TEMP)
+				return -EXDEV;
+
+			return ret;
+		default:
+			return -EOPNOTSUPP;
+		}
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int witrn_read_energy(const struct witrn_sensor *sensor, u32 attr, int channel, s64 *val)
+{
+	switch (attr) {
+	case hwmon_energy_input:
+		switch (channel) {
+		case 0:
+			return hwmon_fp_float_to_s64(le32_to_cpu(sensor->record_energy),
+						     WITRN_SCALE_ENERGY, val);
+		default:
+			return -EOPNOTSUPP;
+		}
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int witrn_read(struct device *dev, enum hwmon_sensor_types type,
+		      u32 attr, int channel, long *val)
+{
+	struct witrn_priv *priv = dev_get_drvdata(dev);
+	struct witrn_sensor sensor;
+	int ret;
+
+	ret = witrn_collect_sensor(priv, &sensor);
+	if (ret)
+		return ret;
+
+	switch (type) {
+	case hwmon_in:
+		return witrn_read_in(&sensor, attr, channel, val);
+	case hwmon_curr:
+		return witrn_read_curr(&sensor, attr, channel, val);
+	case hwmon_power:
+		return witrn_read_power(&sensor, attr, channel, val);
+	case hwmon_temp:
+		return witrn_read_temp(&sensor, attr, channel, val);
+	case hwmon_energy64:
+		return witrn_read_energy(&sensor, attr, channel, (s64 *)val);
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int witrn_read_string(struct device *dev, enum hwmon_sensor_types type,
+			     u32 attr, int channel, const char **str)
+{
+	static const char * const in_labels[] = {
+		"VBUS",
+		"D+",
+		"D-",
+		"CC1",
+		"CC2",
+	};
+	static const char * const curr_labels[] = {
+		"IBUS", /* VBUS current */
+	};
+	static const char * const power_labels[] = {
+		"PBUS", /* VBUS power */
+	};
+	static const char * const energy_labels[] = {
+		"EBUS", /* VBUS energy */
+	};
+	static const char * const temp_labels[] = {
+		"Thermistor",
+	};
+
+	if (type == hwmon_in && attr == hwmon_in_label &&
+	    channel < ARRAY_SIZE(in_labels)) {
+		*str = in_labels[channel];
+	} else if (type == hwmon_curr && attr == hwmon_curr_label &&
+		   channel < ARRAY_SIZE(curr_labels)) {
+		*str = curr_labels[channel];
+	} else if (type == hwmon_power && attr == hwmon_power_label &&
+		   channel < ARRAY_SIZE(power_labels)) {
+		*str = power_labels[channel];
+	} else if (type == hwmon_energy64 && attr == hwmon_energy_label &&
+		   channel < ARRAY_SIZE(energy_labels)) {
+		*str = energy_labels[channel];
+	} else if (type == hwmon_temp && attr == hwmon_temp_label &&
+		   channel < ARRAY_SIZE(temp_labels)) {
+		*str = temp_labels[channel];
+	} else {
+		return -EOPNOTSUPP;
+	}
+
+	return 0;
+}
+
+static const struct hwmon_channel_info *const witrn_info[] = {
+	HWMON_CHANNEL_INFO(in,
+			   HWMON_I_INPUT | HWMON_I_LABEL | HWMON_I_AVERAGE,
+			   HWMON_I_INPUT | HWMON_I_LABEL,
+			   HWMON_I_INPUT | HWMON_I_LABEL,
+			   HWMON_I_INPUT | HWMON_I_LABEL,
+			   HWMON_I_INPUT | HWMON_I_LABEL),
+	HWMON_CHANNEL_INFO(curr,
+			   HWMON_C_INPUT | HWMON_C_LABEL | HWMON_C_AVERAGE | HWMON_C_RATED_MIN),
+	HWMON_CHANNEL_INFO(power,
+			   HWMON_P_INPUT | HWMON_P_LABEL | HWMON_P_AVERAGE),
+	HWMON_CHANNEL_INFO(energy64,
+			   HWMON_E_INPUT | HWMON_E_LABEL),
+	HWMON_CHANNEL_INFO(temp,
+			   HWMON_T_INPUT | HWMON_T_LABEL),
+	NULL
+};
+
+static const struct hwmon_ops witrn_hwmon_ops = {
+	.visible = 0444, /* Nothing is tunable from PC :-( */
+	.read = witrn_read,
+	.read_string = witrn_read_string,
+};
+
+static const struct hwmon_chip_info witrn_chip_info = {
+	.ops = &witrn_hwmon_ops,
+	.info = witrn_info,
+};
+
+enum witrn_attr_channel {
+	ATTR_CHARGE,
+	ATTR_RECORD_GROUP,
+	ATTR_RECORD_TIME,
+	ATTR_UPTIME,
+};
+
+static ssize_t witrn_attr_show(struct device *dev, struct device_attribute *attr,
+			       char *buf)
+{
+	enum witrn_attr_channel channel = to_sensor_dev_attr(attr)->index;
+	struct witrn_priv *priv = dev_get_drvdata(dev);
+	struct witrn_sensor sensor;
+	int ret;
+	s64 val;
+
+	ret = witrn_collect_sensor(priv, &sensor);
+	if (ret)
+		return ret;
+
+	switch (channel) {
+	case ATTR_CHARGE:
+		ret = hwmon_fp_float_to_s64(le32_to_cpu(sensor.record_charge),
+					    WITRN_SCALE_CHARGE, &val);
+		if (ret)
+			return ret;
+		break;
+	case ATTR_RECORD_GROUP:
+		/* +1 to match the index displayed on the meter. */
+		val = sensor.record_group + 1;
+		break;
+	case ATTR_RECORD_TIME:
+		val = le32_to_cpu(sensor.record_time);
+		break;
+	case ATTR_UPTIME:
+		val = le32_to_cpu(sensor.uptime);
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	return sysfs_emit(buf, "%lld\n", val);
+}
+
+static ssize_t witrn_attr_label_show(struct device *dev, struct device_attribute *attr,
+				     char *buf)
+{
+	enum witrn_attr_channel channel = to_sensor_dev_attr(attr)->index;
+	const char *str;
+
+	switch (channel) {
+	case ATTR_CHARGE:
+		str = "CBUS"; /* VBUS charge */
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	return sysfs_emit(buf, "%s\n", str);
+}
+
+static SENSOR_DEVICE_ATTR_RO(charge1_input, witrn_attr, ATTR_CHARGE);
+static SENSOR_DEVICE_ATTR_RO(charge1_label, witrn_attr_label, ATTR_CHARGE);
+static SENSOR_DEVICE_ATTR_RO(record_group, witrn_attr, ATTR_RECORD_GROUP);
+static SENSOR_DEVICE_ATTR_RO(record_time, witrn_attr, ATTR_RECORD_TIME);
+static SENSOR_DEVICE_ATTR_RO(uptime, witrn_attr, ATTR_UPTIME);
+
+static struct attribute *witrn_attrs[] = {
+	&sensor_dev_attr_charge1_input.dev_attr.attr,
+	&sensor_dev_attr_charge1_label.dev_attr.attr,
+	&sensor_dev_attr_record_group.dev_attr.attr,
+	&sensor_dev_attr_record_time.dev_attr.attr,
+	&sensor_dev_attr_uptime.dev_attr.attr,
+	NULL
+};
+ATTRIBUTE_GROUPS(witrn);
+
 static int witrn_probe(struct hid_device *hdev, const struct hid_device_id *id)
 {
 	struct device *parent = &hdev->dev;
@@ -219,6 +632,14 @@ static int witrn_probe(struct hid_device *hdev, const struct hid_device_id *id)
 		return ret;
 	}
 
+	priv->hwmon_dev = hwmon_device_register_with_info(parent, DRIVER_NAME, priv,
+							  &witrn_chip_info, witrn_groups);
+	if (IS_ERR(priv->hwmon_dev)) {
+		witrn_close_hid(priv);
+		hid_hw_stop(hdev);
+		return PTR_ERR(priv->hwmon_dev);
+	}
+
 	return 0;
 }
 
@@ -226,6 +647,8 @@ static void witrn_remove(struct hid_device *hdev)
 {
 	struct witrn_priv *priv = hid_get_drvdata(hdev);
 
+	hwmon_device_unregister(priv->hwmon_dev);
+
 	witrn_close_hid(priv);
 
 	/* Cancel it after closing HID so that it won't be rescheduled. */

-- 
2.53.0

Re: [PATCH 4/4] hwmon: (witrn) Add monitoring support
Posted by kernel test robot 4 days, 6 hours ago
Hi Rong,

kernel test robot noticed the following build errors:

[auto build test ERROR on 0138af2472dfdef0d56fc4697416eaa0ff2589bd]

url:    https://github.com/intel-lab-lkp/linux/commits/Rong-Zhang/hwmon-Add-label-support-for-64-bit-energy-attributes/20260329-030139
base:   0138af2472dfdef0d56fc4697416eaa0ff2589bd
patch link:    https://lore.kernel.org/r/20260327-b4-hwmon-witrn-v1-4-8d2f1896c045%40rong.moe
patch subject: [PATCH 4/4] hwmon: (witrn) Add monitoring support
config: csky-allmodconfig (https://download.01.org/0day-ci/archive/20260329/202603292058.Vszdl5uf-lkp@intel.com/config)
compiler: csky-linux-gcc (GCC) 15.2.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260329/202603292058.Vszdl5uf-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202603292058.Vszdl5uf-lkp@intel.com/

All errors (new ones prefixed by >>, old ones prefixed by <<):

>> ERROR: modpost: "__udivdi3" [drivers/hwmon/hwmon-fp.ko] undefined!
>> ERROR: modpost: "__divdi3" [drivers/hwmon/witrn.ko] undefined!

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
Re: [PATCH 4/4] hwmon: (witrn) Add monitoring support
Posted by kernel test robot 4 days, 7 hours ago
Hi Rong,

kernel test robot noticed the following build errors:

[auto build test ERROR on 0138af2472dfdef0d56fc4697416eaa0ff2589bd]

url:    https://github.com/intel-lab-lkp/linux/commits/Rong-Zhang/hwmon-Add-label-support-for-64-bit-energy-attributes/20260329-030139
base:   0138af2472dfdef0d56fc4697416eaa0ff2589bd
patch link:    https://lore.kernel.org/r/20260327-b4-hwmon-witrn-v1-4-8d2f1896c045%40rong.moe
patch subject: [PATCH 4/4] hwmon: (witrn) Add monitoring support
config: nios2-allmodconfig (https://download.01.org/0day-ci/archive/20260329/202603291947.CoxzmTCo-lkp@intel.com/config)
compiler: nios2-linux-gcc (GCC) 11.5.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260329/202603291947.CoxzmTCo-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202603291947.CoxzmTCo-lkp@intel.com/

All errors (new ones prefixed by >>, old ones prefixed by <<):

ERROR: modpost: "__udivdi3" [drivers/hwmon/hwmon-fp.ko] undefined!
>> ERROR: modpost: "__divdi3" [drivers/hwmon/hwmon-fp.ko] undefined!
ERROR: modpost: "__divdi3" [drivers/hwmon/witrn.ko] undefined!

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki