Linux Documentation
 help / color / mirror / Atom feed
* [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support
@ 2026-11-14  0:41 Davíð Steinn Geirsson
  2026-11-14  0:41 ` [PATCH 1/4] dt-bindings: hwmon: adt7475: Add #thermal-sensor-cells Davíð Steinn Geirsson
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Davíð Steinn Geirsson @ 2026-11-14  0:41 UTC (permalink / raw)
  To: Guenter Roeck, Jean Delvare, linux-hwmon
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
	Uwe Kleine-König, linux-pwm, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-doc, linux-kernel,
	Davíð Steinn Geirsson


This series lets boards use the temperature channels of the ADT7473,
ADT7475, ADT7476 and ADT7490 as thermal zone sensors, and drive their
PWM outputs with a PWM consumer such as pwm-fan. The kernel thermal
framework can then control the fans. It follows the ADT7470 series [1],
merged for v7.3.

The board it is for is the Ubiquiti UniFi USW Pro XG 8 PoE, a switch
with a Realtek RTL9313 (MIPS) SoC. Its ADT7475 drives both fans and
reads the CPU and SFP area temperatures. The fans must also follow
eight PHY temperatures that the ADT7475 cannot read, so the chip's
automatic mode does not fit.

Patch 1 adds "#thermal-sensor-cells" to the binding. Patch 2 converts
the driver to hwmon_device_register_with_info(), through which the
hwmon core registers thermal sensors. Patch 3 registers them. Patch 4
registers a PWM chip when the node has "#pwm-cells".

Differences from ADT7470:
- While a PWM consumer holds a channel, writes to that channel's hwmon
  PWM attributes fail with -EBUSY. ADT7470 hides its PWM attributes
  when "#pwm-cells" is present. The ADT7475 binding has documented
  "#pwm-cells" for fan-N children since v6.12, and device trees that
  follow it must keep their attributes. If preferred, the attributes
  could instead be hidden when "#pwm-cells" is present and there are
  no fan-N children.
- The period of each supported frequency is 1 s divided by the
  frequency, rounded down, so the 44444 ns the binding documents
  selects 22.5 kHz exactly. ADT7470 rounds up.
- No warning about "#pwm-cells" without CONFIG_PWM, because fan-N
  children use the property too.

duty_offset_ns is ignored: the chip has no offset, and 0 is the
round-down result for any requested offset.

Testing on hwmon-next, the base of this series:
- checkpatch.pl --strict and dt_binding_check are clean. W=1 builds of
  adt7475.o after each patch, with CONFIG_PWM and CONFIG_THERMAL_OF
  each on and off, give no warnings. sparse reports nothing for
  adt7475.c, and the hwmon htmldocs build gives no warning for
  adt7475.rst.
- x86_64 QEMU guest with i2c-stub, all four chip types, five register
  images each: the attribute list, every value read and written, and
  the final register contents are identical before and after patch 2,
  apart from the two -ERANGE cases its message lists. The same
  comparison between patches 3 and 4, with and without "#pwm-cells"
  and with no PWM consumer, is identical too.
- The same guest with PWM_DEBUG and PROVE_LOCKING runs the PWM sysfs
  interface and pwm-fan as consumers, including unbinding the chip
  under them. No PWM core or lockdep warning.

Testing on hardware, on the switch above running OpenWrt with this
series backported to 6.18. The backported driver differs from this
series only where 6.18 and hwmon-next differ:
- Ten thermal zones (the ADT7475's two remote channels and the eight
  PHYs) drive both fans through pwm-fan at 22.5 kHz. The fan curve
  was stepped through with emul_temp on three zones, and the first
  trip on the other seven; fan state and duty registers matched the
  expected values.
- Unloading and reloading pwm-fan and adt7475: the fans stay at full
  speed while pwm-fan is unloaded, and thermal control resumes after
  the reload.
- Writes to the hwmon attributes of the held channels return -EBUSY
  and leave the registers unchanged; pwm3 stays writable.
The hwmon-next version itself has not run on hardware.

This patch was written with Claude Code, using the models Claude
Opus 5.5 and Claude Fable 5.2. They wrote the code and the initial
commit message draft, which I cleaned up. I directed the design,
reviewed the result, compared against the datasheet as far as I
could, and ran tests including on actual hardware.

Davíð Steinn Geirsson (4):
  dt-bindings: hwmon: adt7475: Add #thermal-sensor-cells
  hwmon: (adt7475) Convert to hwmon_device_register_with_info()
  hwmon: (adt7475) Add thermal zone sensor support
  hwmon: (adt7475) Expose fan control via PWM framework

 .../devicetree/bindings/hwmon/adt7475.yaml    |   16 +
 Documentation/hwmon/adt7475.rst               |   18 +
 drivers/hwmon/adt7475.c                       | 1266 ++++++++++-------
 3 files changed, 809 insertions(+), 491 deletions(-)


base-commit: f9f5501d60c77c71f60bf5b273375e3777d66080
-- 
2.56.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/4] dt-bindings: hwmon: adt7475: Add #thermal-sensor-cells
  2026-11-14  0:41 [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Davíð Steinn Geirsson
@ 2026-11-14  0:41 ` Davíð Steinn Geirsson
  2026-11-14  0:41 ` [PATCH 2/4] hwmon: (adt7475) Convert to hwmon_device_register_with_info() Davíð Steinn Geirsson
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Davíð Steinn Geirsson @ 2026-11-14  0:41 UTC (permalink / raw)
  To: Guenter Roeck, Jean Delvare, linux-hwmon
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
	Uwe Kleine-König, linux-pwm, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-doc, linux-kernel,
	Davíð Steinn Geirsson

The ADT7473, ADT7475, ADT7476 and ADT7490 measure two remote
temperatures and their own. Add the "#thermal-sensor-cells" property so
that thermal zones can use these as sensors. Its single cell selects the
temperature channel: 0 for Remote 1, 1 for Local and 2 for Remote 2.

Signed-off-by: Davíð Steinn Geirsson <david@dsg.is>
---
This patch was written with Claude Code, using the models Claude
Opus 5.5 and Claude Fable 5.2. They wrote the code and the initial
commit message draft, which I cleaned up. I directed the design,
reviewed the result, compared against the datasheet as far as I
could, and ran tests including on actual hardware.

 .../devicetree/bindings/hwmon/adt7475.yaml       | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/Documentation/devicetree/bindings/hwmon/adt7475.yaml b/Documentation/devicetree/bindings/hwmon/adt7475.yaml
index 43e9fe225870..cbd44bf6759a 100644
--- a/Documentation/devicetree/bindings/hwmon/adt7475.yaml
+++ b/Documentation/devicetree/bindings/hwmon/adt7475.yaml
@@ -73,6 +73,14 @@ properties:
       - 2: PWM flags 0 or PWM_POLARITY_INVERTED
       - 3: The default PWM duty cycle in nanoseconds, defaults to period.
 
+  "#thermal-sensor-cells":
+    const: 1
+    description: |
+      The cell is the temperature channel.
+      - 0: Remote 1
+      - 1: Local
+      - 2: Remote 2
+
 patternProperties:
   "^adi,bypass-attenuator-in[0-4]$":
     description: |
@@ -111,6 +119,13 @@ required:
   - compatible
   - reg
 
+allOf:
+  - if:
+      required:
+        - "#thermal-sensor-cells"
+    then:
+      $ref: /schemas/thermal/thermal-sensor.yaml#
+
 additionalProperties: false
 
 examples:
@@ -128,6 +143,7 @@ examples:
         adi,pin10-function = "smbalert#";
         adi,pin14-function = "tach4";
         #pwm-cells = <3>;
+        #thermal-sensor-cells = <1>;
 
         /* PWMs at 22.5 kHz frequency */
         fan-0 {
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/4] hwmon: (adt7475) Convert to hwmon_device_register_with_info()
  2026-11-14  0:41 [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Davíð Steinn Geirsson
  2026-11-14  0:41 ` [PATCH 1/4] dt-bindings: hwmon: adt7475: Add #thermal-sensor-cells Davíð Steinn Geirsson
@ 2026-11-14  0:41 ` Davíð Steinn Geirsson
  2026-11-14  0:41 ` [PATCH 3/4] hwmon: (adt7475) Add thermal zone sensor support Davíð Steinn Geirsson
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Davíð Steinn Geirsson @ 2026-11-14  0:41 UTC (permalink / raw)
  To: Guenter Roeck, Jean Delvare, linux-hwmon
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
	Uwe Kleine-König, linux-pwm, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-doc, linux-kernel,
	Davíð Steinn Geirsson

Convert the driver from hwmon_device_register_with_groups() to
hwmon_device_register_with_info(). The standard voltage, temperature,
fan and PWM attributes move to hwmon_ops. The chip-specific attributes
stay driver-defined and are passed as extra groups: the automatic fan
control points, temperature smoothing, PWM stall disable,
pwm_use_point2_pwm_at_crit and the VID attributes.

The driver keeps data->lock, although the hwmon core serialises the
hwmon_ops callbacks. The attributes in the extra groups run outside that
serialisation, and they share the register cache and its periodic update
with the hwmon_ops callbacks. They do not take hwmon_lock() instead, to
keep this conversion free of locking changes.

The sysfs attribute names, permissions, values and register writes are
unchanged. This was checked with i2c-stub on all four chip types and
several register images, comparing the attribute list, every value read
and written, and the final register contents before and after the
conversion.

The hwmon core parses the standard attributes with kstrtol() and
returns its error, which gives two differences for input that is out of
range:
- Writing a number that does not fit in a long now fails with -ERANGE
  instead of -EINVAL.
- fanN_min was parsed with kstrtoul(), so values above LONG_MAX were
  accepted (and set the lowest limit). They now fail with -ERANGE. On
  32-bit systems this applies from 2147483648 rpm, far beyond any real
  fan.

Signed-off-by: Davíð Steinn Geirsson <david@dsg.is>
---
This patch was written with Claude Code, using the models Claude
Opus 5.5 and Claude Fable 5.2. They wrote the code and the initial
commit message draft, which I cleaned up. I directed the design,
reviewed the result, compared against the datasheet as far as I
could, and ran tests including on actual hardware.

 drivers/hwmon/adt7475.c | 952 ++++++++++++++++++++--------------------
 1 file changed, 467 insertions(+), 485 deletions(-)

diff --git a/drivers/hwmon/adt7475.c b/drivers/hwmon/adt7475.c
index cd1ce846e2fd..d5bdb8011e6d 100644
--- a/drivers/hwmon/adt7475.c
+++ b/drivers/hwmon/adt7475.c
@@ -222,7 +222,7 @@ struct adt7475_data {
 
 	u8 vid;
 	u8 vrm;
-	const struct attribute_group *groups[10];
+	const struct attribute_group *groups[4];
 };
 
 static struct i2c_driver adt7475_driver;
@@ -332,85 +332,15 @@ static void adt7475_write_word(struct i2c_client *client, int reg, u16 val)
 	i2c_smbus_write_byte_data(client, reg, val & 0xFF);
 }
 
-static ssize_t voltage_show(struct device *dev, struct device_attribute *attr,
-			    char *buf)
-{
-	struct adt7475_data *data = adt7475_update_device(dev);
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
-	unsigned short val;
-
-	if (IS_ERR(data))
-		return PTR_ERR(data);
-
-	switch (sattr->nr) {
-	case ALARM:
-		return sprintf(buf, "%d\n",
-			       (data->alarms >> sattr->index) & 1);
-	default:
-		val = data->voltage[sattr->nr][sattr->index];
-		return sprintf(buf, "%d\n",
-			       reg2volt(sattr->index, val, data->bypass_attn));
-	}
-}
-
-static ssize_t voltage_store(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 adt7475_data *data = dev_get_drvdata(dev);
-	struct i2c_client *client = data->client;
-	unsigned char reg;
-	long val;
-
-	if (kstrtol(buf, 10, &val))
-		return -EINVAL;
-
-	mutex_lock(&data->lock);
-
-	data->voltage[sattr->nr][sattr->index] =
-				volt2reg(sattr->index, val, data->bypass_attn);
-
-	if (sattr->index < ADT7475_VOLTAGE_COUNT) {
-		if (sattr->nr == MIN)
-			reg = VOLTAGE_MIN_REG(sattr->index);
-		else
-			reg = VOLTAGE_MAX_REG(sattr->index);
-	} else if (sattr->index == 5) {
-		if (sattr->nr == MIN)
-			reg = REG_VTT_MIN;
-		else
-			reg = REG_VTT_MAX;
-	} else {
-		if (sattr->nr == MIN)
-			reg = REG_IMON_MIN;
-		else
-			reg = REG_IMON_MAX;
-	}
-
-	i2c_smbus_write_byte_data(client, reg,
-				  data->voltage[sattr->nr][sattr->index] >> 2);
-	mutex_unlock(&data->lock);
-
-	return count;
-}
-
-static ssize_t temp_show(struct device *dev, struct device_attribute *attr,
-			 char *buf)
+static int adt7475_temp_value(struct adt7475_data *data, int nr, int index)
 {
-	struct adt7475_data *data = adt7475_update_device(dev);
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
 	int out;
 
-	if (IS_ERR(data))
-		return PTR_ERR(data);
-
-	switch (sattr->nr) {
+	switch (nr) {
 	case HYSTERSIS:
 		mutex_lock(&data->lock);
-		out = data->temp[sattr->nr][sattr->index];
-		if (sattr->index != 1)
+		out = data->temp[nr][index];
+		if (index != 1)
 			out = (out >> 4) & 0xF;
 		else
 			out = (out & 0xF);
@@ -418,7 +348,7 @@ static ssize_t temp_show(struct device *dev, struct device_attribute *attr,
 		 * Show the value as an absolute number tied to
 		 * THERM
 		 */
-		out = reg2temp(data, data->temp[THERM][sattr->index]) -
+		out = reg2temp(data, data->temp[THERM][index]) -
 			out * 1000;
 		mutex_unlock(&data->lock);
 		break;
@@ -429,7 +359,7 @@ static ssize_t temp_show(struct device *dev, struct device_attribute *attr,
 		 * setting in CONFIG5
 		 */
 		mutex_lock(&data->lock);
-		out = (s8)data->temp[sattr->nr][sattr->index];
+		out = (s8)data->temp[nr][index];
 		if (data->config5 & CONFIG5_TEMPOFFSET)
 			out *= 1000;
 		else
@@ -438,50 +368,58 @@ static ssize_t temp_show(struct device *dev, struct device_attribute *attr,
 		break;
 
 	case ALARM:
-		out = (data->alarms >> (sattr->index + 4)) & 1;
+		out = (data->alarms >> (index + 4)) & 1;
 		break;
 
 	case FAULT:
 		/* Note - only for remote1 and remote2 */
-		out = !!(data->alarms & (sattr->index ? 0x8000 : 0x4000));
+		out = !!(data->alarms & (index ? 0x8000 : 0x4000));
 		break;
 
 	default:
 		/* All other temp values are in the configured format */
-		out = reg2temp(data, data->temp[sattr->nr][sattr->index]);
+		out = reg2temp(data, data->temp[nr][index]);
 	}
 
-	return sprintf(buf, "%d\n", out);
+	return out;
 }
 
-static ssize_t temp_store(struct device *dev, struct device_attribute *attr,
-			  const char *buf, size_t count)
+static ssize_t temp_show(struct device *dev, struct device_attribute *attr,
+			 char *buf)
 {
+	struct adt7475_data *data = adt7475_update_device(dev);
 	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
-	struct adt7475_data *data = dev_get_drvdata(dev);
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	return sprintf(buf, "%d\n",
+		       adt7475_temp_value(data, sattr->nr, sattr->index));
+}
+
+static void adt7475_temp_set(struct adt7475_data *data, int nr, int index,
+			     long val)
+{
 	struct i2c_client *client = data->client;
 	unsigned char reg = 0;
 	u8 out;
 	int temp;
-	long val;
-
-	if (kstrtol(buf, 10, &val))
-		return -EINVAL;
 
 	mutex_lock(&data->lock);
 
 	/* We need the config register in all cases for temp <-> reg conv. */
 	data->config5 = adt7475_read(REG_CONFIG5);
 
-	switch (sattr->nr) {
+	switch (nr) {
 	case OFFSET:
 		if (data->config5 & CONFIG5_TEMPOFFSET) {
 			val = clamp_val(val, -63000, 127000);
-			out = data->temp[OFFSET][sattr->index] = val / 1000;
+			data->temp[OFFSET][index] = val / 1000;
 		} else {
 			val = clamp_val(val, -63000, 64000);
-			out = data->temp[OFFSET][sattr->index] = val / 500;
+			data->temp[OFFSET][index] = val / 500;
 		}
+		out = data->temp[OFFSET][index];
 		break;
 
 	case HYSTERSIS:
@@ -491,53 +429,53 @@ static ssize_t temp_store(struct device *dev, struct device_attribute *attr,
 		 */
 
 		/* Read fresh THERM and HYSTERSIS values from the chip */
-		data->temp[THERM][sattr->index] =
-			adt7475_read(TEMP_THERM_REG(sattr->index)) << 2;
+		data->temp[THERM][index] =
+			adt7475_read(TEMP_THERM_REG(index)) << 2;
 		adt7475_read_hystersis(client);
 
-		temp = reg2temp(data, data->temp[THERM][sattr->index]);
+		temp = reg2temp(data, data->temp[THERM][index]);
 		val = clamp_val(val, temp - 15000, temp);
 		val = (temp - val) / 1000;
 
-		if (sattr->index != 1) {
-			data->temp[HYSTERSIS][sattr->index] &= 0x0F;
-			data->temp[HYSTERSIS][sattr->index] |= (val & 0xF) << 4;
+		if (index != 1) {
+			data->temp[HYSTERSIS][index] &= 0x0F;
+			data->temp[HYSTERSIS][index] |= (val & 0xF) << 4;
 		} else {
-			data->temp[HYSTERSIS][sattr->index] &= 0xF0;
-			data->temp[HYSTERSIS][sattr->index] |= (val & 0xF);
+			data->temp[HYSTERSIS][index] &= 0xF0;
+			data->temp[HYSTERSIS][index] |= (val & 0xF);
 		}
 
-		out = data->temp[HYSTERSIS][sattr->index];
+		out = data->temp[HYSTERSIS][index];
 		break;
 
 	default:
-		data->temp[sattr->nr][sattr->index] = temp2reg(data, val);
+		data->temp[nr][index] = temp2reg(data, val);
 
 		/*
 		 * We maintain an extra 2 digits of precision for simplicity
 		 * - shift those back off before writing the value
 		 */
-		out = (u8) (data->temp[sattr->nr][sattr->index] >> 2);
+		out = (u8)(data->temp[nr][index] >> 2);
 	}
 
-	switch (sattr->nr) {
+	switch (nr) {
 	case MIN:
-		reg = TEMP_MIN_REG(sattr->index);
+		reg = TEMP_MIN_REG(index);
 		break;
 	case MAX:
-		reg = TEMP_MAX_REG(sattr->index);
+		reg = TEMP_MAX_REG(index);
 		break;
 	case OFFSET:
-		reg = TEMP_OFFSET_REG(sattr->index);
+		reg = TEMP_OFFSET_REG(index);
 		break;
 	case AUTOMIN:
-		reg = TEMP_TMIN_REG(sattr->index);
+		reg = TEMP_TMIN_REG(index);
 		break;
 	case THERM:
-		reg = TEMP_THERM_REG(sattr->index);
+		reg = TEMP_THERM_REG(index);
 		break;
 	case HYSTERSIS:
-		if (sattr->index != 2)
+		if (index != 2)
 			reg = REG_REMOTE1_HYSTERSIS;
 		else
 			reg = REG_REMOTE2_HYSTERSIS;
@@ -548,6 +486,20 @@ static ssize_t temp_store(struct device *dev, struct device_attribute *attr,
 	i2c_smbus_write_byte_data(client, reg, out);
 
 	mutex_unlock(&data->lock);
+}
+
+static ssize_t temp_store(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 adt7475_data *data = dev_get_drvdata(dev);
+	long val;
+
+	if (kstrtol(buf, 10, &val))
+		return -EINVAL;
+
+	adt7475_temp_set(data, sattr->nr, sattr->index, val);
+
 	return count;
 }
 
@@ -704,47 +656,6 @@ static ssize_t point2_store(struct device *dev, struct device_attribute *attr,
 	return count;
 }
 
-static ssize_t tach_show(struct device *dev, struct device_attribute *attr,
-			 char *buf)
-{
-	struct adt7475_data *data = adt7475_update_device(dev);
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
-	int out;
-
-	if (IS_ERR(data))
-		return PTR_ERR(data);
-
-	if (sattr->nr == ALARM)
-		out = (data->alarms >> (sattr->index + 10)) & 1;
-	else
-		out = tach2rpm(data->tach[sattr->nr][sattr->index]);
-
-	return sprintf(buf, "%d\n", out);
-}
-
-static ssize_t tach_store(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 adt7475_data *data = dev_get_drvdata(dev);
-	struct i2c_client *client = data->client;
-	unsigned long val;
-
-	if (kstrtoul(buf, 10, &val))
-		return -EINVAL;
-
-	mutex_lock(&data->lock);
-
-	data->tach[MIN][sattr->index] = rpm2tach(val);
-
-	adt7475_write_word(client, TACH_MIN_REG(sattr->index),
-			   data->tach[MIN][sattr->index]);
-
-	mutex_unlock(&data->lock);
-	return count;
-}
-
 static ssize_t pwm_show(struct device *dev, struct device_attribute *attr,
 			char *buf)
 {
@@ -757,76 +668,57 @@ static ssize_t pwm_show(struct device *dev, struct device_attribute *attr,
 	return sprintf(buf, "%d\n", data->pwm[sattr->nr][sattr->index]);
 }
 
-static ssize_t pwmchan_show(struct device *dev, struct device_attribute *attr,
-			    char *buf)
-{
-	struct adt7475_data *data = adt7475_update_device(dev);
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
-
-	if (IS_ERR(data))
-		return PTR_ERR(data);
-
-	return sprintf(buf, "%d\n", data->pwmchan[sattr->index]);
-}
-
-static ssize_t pwmctrl_show(struct device *dev, struct device_attribute *attr,
-			    char *buf)
-{
-	struct adt7475_data *data = adt7475_update_device(dev);
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
-
-	if (IS_ERR(data))
-		return PTR_ERR(data);
-
-	return sprintf(buf, "%d\n", data->pwmctl[sattr->index]);
-}
-
-static ssize_t pwm_store(struct device *dev, struct device_attribute *attr,
-			 const char *buf, size_t count)
+static void adt7475_pwm_set(struct adt7475_data *data, int nr, int index,
+			    long val)
 {
-
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
-	struct adt7475_data *data = dev_get_drvdata(dev);
 	struct i2c_client *client = data->client;
 	unsigned char reg = 0;
-	long val;
-
-	if (kstrtol(buf, 10, &val))
-		return -EINVAL;
 
 	mutex_lock(&data->lock);
 
-	switch (sattr->nr) {
+	switch (nr) {
 	case INPUT:
 		/* Get a fresh value for CONTROL */
-		data->pwm[CONTROL][sattr->index] =
-			adt7475_read(PWM_CONFIG_REG(sattr->index));
+		data->pwm[CONTROL][index] =
+			adt7475_read(PWM_CONFIG_REG(index));
 
 		/*
 		 * If we are not in manual mode, then we shouldn't allow
 		 * the user to set the pwm speed
 		 */
-		if (((data->pwm[CONTROL][sattr->index] >> 5) & 7) != 7) {
+		if (((data->pwm[CONTROL][index] >> 5) & 7) != 7) {
 			mutex_unlock(&data->lock);
-			return count;
+			return;
 		}
 
-		reg = PWM_REG(sattr->index);
+		reg = PWM_REG(index);
 		break;
 
 	case MIN:
-		reg = PWM_MIN_REG(sattr->index);
+		reg = PWM_MIN_REG(index);
 		break;
 
 	case MAX:
-		reg = PWM_MAX_REG(sattr->index);
+		reg = PWM_MAX_REG(index);
 		break;
 	}
 
-	data->pwm[sattr->nr][sattr->index] = clamp_val(val, 0, 0xFF);
-	i2c_smbus_write_byte_data(client, reg,
-				  data->pwm[sattr->nr][sattr->index]);
+	data->pwm[nr][index] = clamp_val(val, 0, 0xFF);
+	i2c_smbus_write_byte_data(client, reg, data->pwm[nr][index]);
 	mutex_unlock(&data->lock);
+}
+
+static ssize_t pwm_store(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 adt7475_data *data = dev_get_drvdata(dev);
+	long val;
+
+	if (kstrtol(buf, 10, &val))
+		return -EINVAL;
+
+	adt7475_pwm_set(data, sattr->nr, sattr->index, val);
 
 	return count;
 }
@@ -926,263 +818,475 @@ static int hw_set_pwm(struct i2c_client *client, int index,
 	return 0;
 }
 
-static ssize_t pwmchan_store(struct device *dev,
-			     struct device_attribute *attr, const char *buf,
-			     size_t count)
+/* List of frequencies for the PWM */
+static const int pwmfreq_table[] = {
+	11, 14, 22, 29, 35, 44, 58, 88, 22500
+};
+
+static ssize_t pwm_use_point2_pwm_at_crit_show(struct device *dev,
+					       struct device_attribute *devattr,
+					       char *buf)
+{
+	struct adt7475_data *data = adt7475_update_device(dev);
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	return sprintf(buf, "%d\n", !!(data->config4 & CONFIG4_MAXDUTY));
+}
+
+static ssize_t pwm_use_point2_pwm_at_crit_store(struct device *dev,
+						struct device_attribute *devattr,
+						const char *buf, size_t count)
 {
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
 	struct adt7475_data *data = dev_get_drvdata(dev);
 	struct i2c_client *client = data->client;
-	int r;
 	long val;
 
 	if (kstrtol(buf, 10, &val))
 		return -EINVAL;
+	if (val != 0 && val != 1)
+		return -EINVAL;
 
 	mutex_lock(&data->lock);
-	/* Read Modify Write PWM values */
-	adt7475_read_pwm(client, sattr->index);
-	r = hw_set_pwm(client, sattr->index, data->pwmctl[sattr->index], val);
-	if (r)
-		count = r;
+	data->config4 = i2c_smbus_read_byte_data(client, REG_CONFIG4);
+	if (val)
+		data->config4 |= CONFIG4_MAXDUTY;
+	else
+		data->config4 &= ~CONFIG4_MAXDUTY;
+	i2c_smbus_write_byte_data(client, REG_CONFIG4, data->config4);
 	mutex_unlock(&data->lock);
 
 	return count;
 }
 
-static ssize_t pwmctrl_store(struct device *dev,
-			     struct device_attribute *attr, const char *buf,
-			     size_t count)
+static ssize_t vrm_show(struct device *dev, struct device_attribute *devattr,
+			char *buf)
+{
+	struct adt7475_data *data = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%d\n", (int)data->vrm);
+}
+
+static ssize_t vrm_store(struct device *dev, struct device_attribute *devattr,
+			 const char *buf, size_t count)
 {
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
 	struct adt7475_data *data = dev_get_drvdata(dev);
-	struct i2c_client *client = data->client;
-	int r;
 	long val;
 
 	if (kstrtol(buf, 10, &val))
 		return -EINVAL;
-
-	mutex_lock(&data->lock);
-	/* Read Modify Write PWM values */
-	adt7475_read_pwm(client, sattr->index);
-	r = hw_set_pwm(client, sattr->index, val, data->pwmchan[sattr->index]);
-	if (r)
-		count = r;
-	mutex_unlock(&data->lock);
+	if (val < 0 || val > 255)
+		return -EINVAL;
+	data->vrm = val;
 
 	return count;
 }
 
-/* List of frequencies for the PWM */
-static const int pwmfreq_table[] = {
-	11, 14, 22, 29, 35, 44, 58, 88, 22500
-};
+static ssize_t cpu0_vid_show(struct device *dev,
+			     struct device_attribute *devattr, char *buf)
+{
+	struct adt7475_data *data = adt7475_update_device(dev);
 
-static ssize_t pwmfreq_show(struct device *dev, struct device_attribute *attr,
-			    char *buf)
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	return sprintf(buf, "%d\n", vid_from_reg(data->vid, data->vrm));
+}
+
+/* STATUS bit of each voltage input's alarm, in alarms */
+static const u8 adt7475_in_alarm_bit[] = { 0, 1, 2, 3, 8, 31, 30 };
+
+static int adt7475_in_read(struct device *dev, u32 attr, int channel,
+			   long *val)
 {
 	struct adt7475_data *data = adt7475_update_device(dev);
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
-	int idx;
 
 	if (IS_ERR(data))
 		return PTR_ERR(data);
-	idx = clamp_val(data->range[sattr->index] & 0xf, 0,
-			ARRAY_SIZE(pwmfreq_table) - 1);
 
-	return sprintf(buf, "%d\n", pwmfreq_table[idx]);
+	switch (attr) {
+	case hwmon_in_input:
+		*val = reg2volt(channel, data->voltage[INPUT][channel],
+				data->bypass_attn);
+		return 0;
+	case hwmon_in_min:
+		*val = reg2volt(channel, data->voltage[MIN][channel],
+				data->bypass_attn);
+		return 0;
+	case hwmon_in_max:
+		*val = reg2volt(channel, data->voltage[MAX][channel],
+				data->bypass_attn);
+		return 0;
+	case hwmon_in_alarm:
+		*val = (data->alarms >> adt7475_in_alarm_bit[channel]) & 1;
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
 }
 
-static ssize_t pwmfreq_store(struct device *dev,
-			     struct device_attribute *attr, const char *buf,
-			     size_t count)
+static int adt7475_in_write(struct device *dev, u32 attr, int channel,
+			    long val)
 {
-	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
 	struct adt7475_data *data = dev_get_drvdata(dev);
 	struct i2c_client *client = data->client;
-	int out;
-	long val;
-
-	if (kstrtol(buf, 10, &val))
-		return -EINVAL;
+	unsigned char reg;
+	int nr;
 
-	out = find_closest(val, pwmfreq_table, ARRAY_SIZE(pwmfreq_table));
+	switch (attr) {
+	case hwmon_in_min:
+		nr = MIN;
+		break;
+	case hwmon_in_max:
+		nr = MAX;
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
 
 	mutex_lock(&data->lock);
 
-	data->range[sattr->index] =
-		adt7475_read(TEMP_TRANGE_REG(sattr->index));
-	data->range[sattr->index] &= ~0xf;
-	data->range[sattr->index] |= out;
+	data->voltage[nr][channel] = volt2reg(channel, val, data->bypass_attn);
 
-	i2c_smbus_write_byte_data(client, TEMP_TRANGE_REG(sattr->index),
-				  data->range[sattr->index]);
+	if (channel < ADT7475_VOLTAGE_COUNT)
+		reg = nr == MIN ? VOLTAGE_MIN_REG(channel) :
+				  VOLTAGE_MAX_REG(channel);
+	else if (channel == 5)
+		reg = nr == MIN ? REG_VTT_MIN : REG_VTT_MAX;
+	else
+		reg = nr == MIN ? REG_IMON_MIN : REG_IMON_MAX;
 
+	i2c_smbus_write_byte_data(client, reg,
+				  data->voltage[nr][channel] >> 2);
 	mutex_unlock(&data->lock);
-	return count;
+
+	return 0;
 }
 
-static ssize_t pwm_use_point2_pwm_at_crit_show(struct device *dev,
-					struct device_attribute *devattr,
-					char *buf)
+static int adt7475_temp_nr(u32 attr)
+{
+	switch (attr) {
+	case hwmon_temp_input:
+		return INPUT;
+	case hwmon_temp_min:
+		return MIN;
+	case hwmon_temp_max:
+		return MAX;
+	case hwmon_temp_offset:
+		return OFFSET;
+	case hwmon_temp_crit:
+		return THERM;
+	case hwmon_temp_crit_hyst:
+		return HYSTERSIS;
+	case hwmon_temp_alarm:
+		return ALARM;
+	case hwmon_temp_fault:
+		return FAULT;
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int adt7475_temp_read(struct device *dev, u32 attr, int channel,
+			     long *val)
+{
+	struct adt7475_data *data;
+	int nr = adt7475_temp_nr(attr);
+
+	if (nr < 0)
+		return nr;
+
+	data = adt7475_update_device(dev);
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	*val = adt7475_temp_value(data, nr, channel);
+
+	return 0;
+}
+
+static int adt7475_temp_write(struct device *dev, u32 attr, int channel,
+			      long val)
+{
+	struct adt7475_data *data = dev_get_drvdata(dev);
+	int nr = adt7475_temp_nr(attr);
+
+	switch (nr) {
+	case MIN:
+	case MAX:
+	case OFFSET:
+	case THERM:
+	case HYSTERSIS:
+		adt7475_temp_set(data, nr, channel, val);
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static int adt7475_fan_read(struct device *dev, u32 attr, int channel,
+			    long *val)
 {
 	struct adt7475_data *data = adt7475_update_device(dev);
 
 	if (IS_ERR(data))
 		return PTR_ERR(data);
 
-	return sprintf(buf, "%d\n", !!(data->config4 & CONFIG4_MAXDUTY));
+	switch (attr) {
+	case hwmon_fan_input:
+		*val = tach2rpm(data->tach[INPUT][channel]);
+		return 0;
+	case hwmon_fan_min:
+		*val = tach2rpm(data->tach[MIN][channel]);
+		return 0;
+	case hwmon_fan_alarm:
+		*val = (data->alarms >> (channel + 10)) & 1;
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
 }
 
-static ssize_t pwm_use_point2_pwm_at_crit_store(struct device *dev,
-					struct device_attribute *devattr,
-					const char *buf, size_t count)
+static int adt7475_fan_write(struct device *dev, u32 attr, int channel,
+			     long val)
 {
 	struct adt7475_data *data = dev_get_drvdata(dev);
-	struct i2c_client *client = data->client;
-	long val;
 
-	if (kstrtol(buf, 10, &val))
-		return -EINVAL;
-	if (val != 0 && val != 1)
+	if (attr != hwmon_fan_min)
+		return -EOPNOTSUPP;
+
+	/* fanN_min takes an unsigned value */
+	if (val < 0)
 		return -EINVAL;
 
 	mutex_lock(&data->lock);
-	data->config4 = i2c_smbus_read_byte_data(client, REG_CONFIG4);
-	if (val)
-		data->config4 |= CONFIG4_MAXDUTY;
-	else
-		data->config4 &= ~CONFIG4_MAXDUTY;
-	i2c_smbus_write_byte_data(client, REG_CONFIG4, data->config4);
+
+	data->tach[MIN][channel] = rpm2tach(val);
+
+	adt7475_write_word(data->client, TACH_MIN_REG(channel),
+			   data->tach[MIN][channel]);
+
 	mutex_unlock(&data->lock);
 
-	return count;
+	return 0;
 }
 
-static ssize_t vrm_show(struct device *dev, struct device_attribute *devattr,
-			char *buf)
+static int adt7475_pwm_read(struct device *dev, u32 attr, int channel,
+			    long *val)
 {
-	struct adt7475_data *data = dev_get_drvdata(dev);
-	return sprintf(buf, "%d\n", (int)data->vrm);
+	struct adt7475_data *data = adt7475_update_device(dev);
+	int idx;
+
+	if (IS_ERR(data))
+		return PTR_ERR(data);
+
+	switch (attr) {
+	case hwmon_pwm_input:
+		*val = data->pwm[INPUT][channel];
+		return 0;
+	case hwmon_pwm_enable:
+		*val = data->pwmctl[channel];
+		return 0;
+	case hwmon_pwm_auto_channels_temp:
+		*val = data->pwmchan[channel];
+		return 0;
+	case hwmon_pwm_freq:
+		idx = clamp_val(data->range[channel] & 0xf, 0,
+				ARRAY_SIZE(pwmfreq_table) - 1);
+		*val = pwmfreq_table[idx];
+		return 0;
+	default:
+		return -EOPNOTSUPP;
+	}
 }
 
-static ssize_t vrm_store(struct device *dev, struct device_attribute *devattr,
-			 const char *buf, size_t count)
+static int adt7475_pwm_write(struct device *dev, u32 attr, int channel,
+			     long val)
 {
 	struct adt7475_data *data = dev_get_drvdata(dev);
-	long val;
+	struct i2c_client *client = data->client;
+	int ret = 0;
+	int out;
 
-	if (kstrtol(buf, 10, &val))
-		return -EINVAL;
-	if (val < 0 || val > 255)
-		return -EINVAL;
-	data->vrm = val;
+	switch (attr) {
+	case hwmon_pwm_input:
+		adt7475_pwm_set(data, INPUT, channel, val);
+		break;
 
-	return count;
+	case hwmon_pwm_enable:
+		mutex_lock(&data->lock);
+		/* Read Modify Write PWM values */
+		adt7475_read_pwm(client, channel);
+		ret = hw_set_pwm(client, channel, val, data->pwmchan[channel]);
+		mutex_unlock(&data->lock);
+		break;
+
+	case hwmon_pwm_auto_channels_temp:
+		mutex_lock(&data->lock);
+		/* Read Modify Write PWM values */
+		adt7475_read_pwm(client, channel);
+		ret = hw_set_pwm(client, channel, data->pwmctl[channel], val);
+		mutex_unlock(&data->lock);
+		break;
+
+	case hwmon_pwm_freq:
+		out = find_closest(val, pwmfreq_table,
+				   ARRAY_SIZE(pwmfreq_table));
+
+		mutex_lock(&data->lock);
+
+		data->range[channel] =
+			adt7475_read(TEMP_TRANGE_REG(channel));
+		data->range[channel] &= ~0xf;
+		data->range[channel] |= out;
+
+		i2c_smbus_write_byte_data(client, TEMP_TRANGE_REG(channel),
+					  data->range[channel]);
+
+		mutex_unlock(&data->lock);
+		break;
+
+	default:
+		ret = -EOPNOTSUPP;
+	}
+
+	return ret;
 }
 
-static ssize_t cpu0_vid_show(struct device *dev,
-			     struct device_attribute *devattr, char *buf)
+static int adt7475_hwmon_read(struct device *dev,
+			      enum hwmon_sensor_types type, u32 attr,
+			      int channel, long *val)
 {
-	struct adt7475_data *data = adt7475_update_device(dev);
+	switch (type) {
+	case hwmon_in:
+		return adt7475_in_read(dev, attr, channel, val);
+	case hwmon_temp:
+		return adt7475_temp_read(dev, attr, channel, val);
+	case hwmon_fan:
+		return adt7475_fan_read(dev, attr, channel, val);
+	case hwmon_pwm:
+		return adt7475_pwm_read(dev, attr, channel, val);
+	default:
+		return -EOPNOTSUPP;
+	}
+}
 
-	if (IS_ERR(data))
-		return PTR_ERR(data);
+static int adt7475_hwmon_write(struct device *dev,
+			       enum hwmon_sensor_types type, u32 attr,
+			       int channel, long val)
+{
+	switch (type) {
+	case hwmon_in:
+		return adt7475_in_write(dev, attr, channel, val);
+	case hwmon_temp:
+		return adt7475_temp_write(dev, attr, channel, val);
+	case hwmon_fan:
+		return adt7475_fan_write(dev, attr, channel, val);
+	case hwmon_pwm:
+		return adt7475_pwm_write(dev, attr, channel, val);
+	default:
+		return -EOPNOTSUPP;
+	}
+}
 
-	return sprintf(buf, "%d\n", vid_from_reg(data->vid, data->vrm));
+static umode_t adt7475_is_visible(const void *_data,
+				  enum hwmon_sensor_types type, u32 attr,
+				  int channel)
+{
+	const struct adt7475_data *data = _data;
+
+	switch (type) {
+	case hwmon_in:
+		if (!(data->has_voltage & BIT(channel)))
+			return 0;
+		if (attr == hwmon_in_min || attr == hwmon_in_max)
+			return 0644;
+		return 0444;
+	case hwmon_temp:
+		switch (attr) {
+		case hwmon_temp_input:
+		case hwmon_temp_alarm:
+		case hwmon_temp_fault:
+			return 0444;
+		default:
+			return 0644;
+		}
+	case hwmon_fan:
+		if (channel == 3 && !data->has_fan4)
+			return 0;
+		if (attr == hwmon_fan_min)
+			return 0644;
+		return 0444;
+	case hwmon_pwm:
+		if (channel == 1 && !data->has_pwm2)
+			return 0;
+		return 0644;
+	default:
+		return 0;
+	}
 }
 
-static SENSOR_DEVICE_ATTR_2_RO(in0_input, voltage, INPUT, 0);
-static SENSOR_DEVICE_ATTR_2_RW(in0_max, voltage, MAX, 0);
-static SENSOR_DEVICE_ATTR_2_RW(in0_min, voltage, MIN, 0);
-static SENSOR_DEVICE_ATTR_2_RO(in0_alarm, voltage, ALARM, 0);
-static SENSOR_DEVICE_ATTR_2_RO(in1_input, voltage, INPUT, 1);
-static SENSOR_DEVICE_ATTR_2_RW(in1_max, voltage, MAX, 1);
-static SENSOR_DEVICE_ATTR_2_RW(in1_min, voltage, MIN, 1);
-static SENSOR_DEVICE_ATTR_2_RO(in1_alarm, voltage, ALARM, 1);
-static SENSOR_DEVICE_ATTR_2_RO(in2_input, voltage, INPUT, 2);
-static SENSOR_DEVICE_ATTR_2_RW(in2_max, voltage, MAX, 2);
-static SENSOR_DEVICE_ATTR_2_RW(in2_min, voltage, MIN, 2);
-static SENSOR_DEVICE_ATTR_2_RO(in2_alarm, voltage, ALARM, 2);
-static SENSOR_DEVICE_ATTR_2_RO(in3_input, voltage, INPUT, 3);
-static SENSOR_DEVICE_ATTR_2_RW(in3_max, voltage, MAX, 3);
-static SENSOR_DEVICE_ATTR_2_RW(in3_min, voltage, MIN, 3);
-static SENSOR_DEVICE_ATTR_2_RO(in3_alarm, voltage, ALARM, 3);
-static SENSOR_DEVICE_ATTR_2_RO(in4_input, voltage, INPUT, 4);
-static SENSOR_DEVICE_ATTR_2_RW(in4_max, voltage, MAX, 4);
-static SENSOR_DEVICE_ATTR_2_RW(in4_min, voltage, MIN, 4);
-static SENSOR_DEVICE_ATTR_2_RO(in4_alarm, voltage, ALARM, 8);
-static SENSOR_DEVICE_ATTR_2_RO(in5_input, voltage, INPUT, 5);
-static SENSOR_DEVICE_ATTR_2_RW(in5_max, voltage, MAX, 5);
-static SENSOR_DEVICE_ATTR_2_RW(in5_min, voltage, MIN, 5);
-static SENSOR_DEVICE_ATTR_2_RO(in5_alarm, voltage, ALARM, 31);
-static SENSOR_DEVICE_ATTR_2_RO(in6_input, voltage, INPUT, 6);
-static SENSOR_DEVICE_ATTR_2_RW(in6_max, voltage, MAX, 6);
-static SENSOR_DEVICE_ATTR_2_RW(in6_min, voltage, MIN, 6);
-static SENSOR_DEVICE_ATTR_2_RO(in6_alarm, voltage, ALARM, 30);
-static SENSOR_DEVICE_ATTR_2_RO(temp1_input, temp, INPUT, 0);
-static SENSOR_DEVICE_ATTR_2_RO(temp1_alarm, temp, ALARM, 0);
-static SENSOR_DEVICE_ATTR_2_RO(temp1_fault, temp, FAULT, 0);
-static SENSOR_DEVICE_ATTR_2_RW(temp1_max, temp, MAX, 0);
-static SENSOR_DEVICE_ATTR_2_RW(temp1_min, temp, MIN, 0);
-static SENSOR_DEVICE_ATTR_2_RW(temp1_offset, temp, OFFSET, 0);
+static const struct hwmon_ops adt7475_hwmon_ops = {
+	.is_visible = adt7475_is_visible,
+	.read = adt7475_hwmon_read,
+	.write = adt7475_hwmon_write,
+};
+
+#define ADT7475_IN_CONFIG	(HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX | \
+				 HWMON_I_ALARM)
+#define ADT7475_TEMP_CONFIG	(HWMON_T_INPUT | HWMON_T_MIN | HWMON_T_MAX | \
+				 HWMON_T_OFFSET | HWMON_T_CRIT | \
+				 HWMON_T_CRIT_HYST | HWMON_T_ALARM)
+#define ADT7475_FAN_CONFIG	(HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_ALARM)
+#define ADT7475_PWM_CONFIG	(HWMON_PWM_INPUT | HWMON_PWM_ENABLE | \
+				 HWMON_PWM_FREQ | HWMON_PWM_AUTO_CHANNELS_TEMP)
+
+static const struct hwmon_channel_info * const adt7475_info[] = {
+	HWMON_CHANNEL_INFO(in,
+			   ADT7475_IN_CONFIG,	/* in0: +2.5V */
+			   ADT7475_IN_CONFIG,	/* in1: Vccp */
+			   ADT7475_IN_CONFIG,	/* in2: Vcc */
+			   ADT7475_IN_CONFIG,	/* in3: +5V */
+			   ADT7475_IN_CONFIG,	/* in4: +12V */
+			   ADT7475_IN_CONFIG,	/* in5: Vtt */
+			   ADT7475_IN_CONFIG),	/* in6: Imon */
+	HWMON_CHANNEL_INFO(temp,
+			   ADT7475_TEMP_CONFIG | HWMON_T_FAULT,	/* remote 1 */
+			   ADT7475_TEMP_CONFIG,			/* local */
+			   ADT7475_TEMP_CONFIG | HWMON_T_FAULT),	/* remote 2 */
+	HWMON_CHANNEL_INFO(fan,
+			   ADT7475_FAN_CONFIG,
+			   ADT7475_FAN_CONFIG,
+			   ADT7475_FAN_CONFIG,
+			   ADT7475_FAN_CONFIG),
+	HWMON_CHANNEL_INFO(pwm,
+			   ADT7475_PWM_CONFIG,
+			   ADT7475_PWM_CONFIG,
+			   ADT7475_PWM_CONFIG),
+	NULL
+};
+
+static const struct hwmon_chip_info adt7475_chip_info = {
+	.ops = &adt7475_hwmon_ops,
+	.info = adt7475_info,
+};
+
 static SENSOR_DEVICE_ATTR_2_RW(temp1_auto_point1_temp, temp, AUTOMIN, 0);
 static SENSOR_DEVICE_ATTR_2_RW(temp1_auto_point2_temp, point2, 0, 0);
-static SENSOR_DEVICE_ATTR_2_RW(temp1_crit, temp, THERM, 0);
-static SENSOR_DEVICE_ATTR_2_RW(temp1_crit_hyst, temp, HYSTERSIS, 0);
 static SENSOR_DEVICE_ATTR_2_RW(temp1_smoothing, temp_st, 0, 0);
-static SENSOR_DEVICE_ATTR_2_RO(temp2_input, temp, INPUT, 1);
-static SENSOR_DEVICE_ATTR_2_RO(temp2_alarm, temp, ALARM, 1);
-static SENSOR_DEVICE_ATTR_2_RW(temp2_max, temp, MAX, 1);
-static SENSOR_DEVICE_ATTR_2_RW(temp2_min, temp, MIN, 1);
-static SENSOR_DEVICE_ATTR_2_RW(temp2_offset, temp, OFFSET, 1);
 static SENSOR_DEVICE_ATTR_2_RW(temp2_auto_point1_temp, temp, AUTOMIN, 1);
 static SENSOR_DEVICE_ATTR_2_RW(temp2_auto_point2_temp, point2, 0, 1);
-static SENSOR_DEVICE_ATTR_2_RW(temp2_crit, temp, THERM, 1);
-static SENSOR_DEVICE_ATTR_2_RW(temp2_crit_hyst, temp, HYSTERSIS, 1);
 static SENSOR_DEVICE_ATTR_2_RW(temp2_smoothing, temp_st, 0, 1);
-static SENSOR_DEVICE_ATTR_2_RO(temp3_input, temp, INPUT, 2);
-static SENSOR_DEVICE_ATTR_2_RO(temp3_alarm, temp, ALARM, 2);
-static SENSOR_DEVICE_ATTR_2_RO(temp3_fault, temp, FAULT, 2);
-static SENSOR_DEVICE_ATTR_2_RW(temp3_max, temp, MAX, 2);
-static SENSOR_DEVICE_ATTR_2_RW(temp3_min, temp, MIN, 2);
-static SENSOR_DEVICE_ATTR_2_RW(temp3_offset, temp, OFFSET, 2);
 static SENSOR_DEVICE_ATTR_2_RW(temp3_auto_point1_temp, temp, AUTOMIN, 2);
 static SENSOR_DEVICE_ATTR_2_RW(temp3_auto_point2_temp, point2, 0, 2);
-static SENSOR_DEVICE_ATTR_2_RW(temp3_crit, temp, THERM, 2);
-static SENSOR_DEVICE_ATTR_2_RW(temp3_crit_hyst, temp, HYSTERSIS, 2);
 static SENSOR_DEVICE_ATTR_2_RW(temp3_smoothing, temp_st, 0, 2);
-static SENSOR_DEVICE_ATTR_2_RO(fan1_input, tach, INPUT, 0);
-static SENSOR_DEVICE_ATTR_2_RW(fan1_min, tach, MIN, 0);
-static SENSOR_DEVICE_ATTR_2_RO(fan1_alarm, tach, ALARM, 0);
-static SENSOR_DEVICE_ATTR_2_RO(fan2_input, tach, INPUT, 1);
-static SENSOR_DEVICE_ATTR_2_RW(fan2_min, tach, MIN, 1);
-static SENSOR_DEVICE_ATTR_2_RO(fan2_alarm, tach, ALARM, 1);
-static SENSOR_DEVICE_ATTR_2_RO(fan3_input, tach, INPUT, 2);
-static SENSOR_DEVICE_ATTR_2_RW(fan3_min, tach, MIN, 2);
-static SENSOR_DEVICE_ATTR_2_RO(fan3_alarm, tach, ALARM, 2);
-static SENSOR_DEVICE_ATTR_2_RO(fan4_input, tach, INPUT, 3);
-static SENSOR_DEVICE_ATTR_2_RW(fan4_min, tach, MIN, 3);
-static SENSOR_DEVICE_ATTR_2_RO(fan4_alarm, tach, ALARM, 3);
-static SENSOR_DEVICE_ATTR_2_RW(pwm1, pwm, INPUT, 0);
-static SENSOR_DEVICE_ATTR_2_RW(pwm1_freq, pwmfreq, INPUT, 0);
-static SENSOR_DEVICE_ATTR_2_RW(pwm1_enable, pwmctrl, INPUT, 0);
-static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_channels_temp, pwmchan, INPUT, 0);
 static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_pwm, pwm, MIN, 0);
 static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_pwm, pwm, MAX, 0);
 static SENSOR_DEVICE_ATTR_2_RW(pwm1_stall_disable, stall_disable, 0, 0);
-static SENSOR_DEVICE_ATTR_2_RW(pwm2, pwm, INPUT, 1);
-static SENSOR_DEVICE_ATTR_2_RW(pwm2_freq, pwmfreq, INPUT, 1);
-static SENSOR_DEVICE_ATTR_2_RW(pwm2_enable, pwmctrl, INPUT, 1);
-static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_channels_temp, pwmchan, INPUT, 1);
 static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_pwm, pwm, MIN, 1);
 static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_pwm, pwm, MAX, 1);
 static SENSOR_DEVICE_ATTR_2_RW(pwm2_stall_disable, stall_disable, 0, 1);
-static SENSOR_DEVICE_ATTR_2_RW(pwm3, pwm, INPUT, 2);
-static SENSOR_DEVICE_ATTR_2_RW(pwm3_freq, pwmfreq, INPUT, 2);
-static SENSOR_DEVICE_ATTR_2_RW(pwm3_enable, pwmctrl, INPUT, 2);
-static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_channels_temp, pwmchan, INPUT, 2);
 static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point1_pwm, pwm, MIN, 2);
 static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point2_pwm, pwm, MAX, 2);
 static SENSOR_DEVICE_ATTR_2_RW(pwm3_stall_disable, stall_disable, 0, 2);
@@ -1194,66 +1298,18 @@ static DEVICE_ATTR_RW(vrm);
 static DEVICE_ATTR_RO(cpu0_vid);
 
 static struct attribute *adt7475_attrs[] = {
-	&sensor_dev_attr_in1_input.dev_attr.attr,
-	&sensor_dev_attr_in1_max.dev_attr.attr,
-	&sensor_dev_attr_in1_min.dev_attr.attr,
-	&sensor_dev_attr_in1_alarm.dev_attr.attr,
-	&sensor_dev_attr_in2_input.dev_attr.attr,
-	&sensor_dev_attr_in2_max.dev_attr.attr,
-	&sensor_dev_attr_in2_min.dev_attr.attr,
-	&sensor_dev_attr_in2_alarm.dev_attr.attr,
-	&sensor_dev_attr_temp1_input.dev_attr.attr,
-	&sensor_dev_attr_temp1_alarm.dev_attr.attr,
-	&sensor_dev_attr_temp1_fault.dev_attr.attr,
-	&sensor_dev_attr_temp1_max.dev_attr.attr,
-	&sensor_dev_attr_temp1_min.dev_attr.attr,
-	&sensor_dev_attr_temp1_offset.dev_attr.attr,
 	&sensor_dev_attr_temp1_auto_point1_temp.dev_attr.attr,
 	&sensor_dev_attr_temp1_auto_point2_temp.dev_attr.attr,
-	&sensor_dev_attr_temp1_crit.dev_attr.attr,
-	&sensor_dev_attr_temp1_crit_hyst.dev_attr.attr,
 	&sensor_dev_attr_temp1_smoothing.dev_attr.attr,
-	&sensor_dev_attr_temp2_input.dev_attr.attr,
-	&sensor_dev_attr_temp2_alarm.dev_attr.attr,
-	&sensor_dev_attr_temp2_max.dev_attr.attr,
-	&sensor_dev_attr_temp2_min.dev_attr.attr,
-	&sensor_dev_attr_temp2_offset.dev_attr.attr,
 	&sensor_dev_attr_temp2_auto_point1_temp.dev_attr.attr,
 	&sensor_dev_attr_temp2_auto_point2_temp.dev_attr.attr,
-	&sensor_dev_attr_temp2_crit.dev_attr.attr,
-	&sensor_dev_attr_temp2_crit_hyst.dev_attr.attr,
 	&sensor_dev_attr_temp2_smoothing.dev_attr.attr,
-	&sensor_dev_attr_temp3_input.dev_attr.attr,
-	&sensor_dev_attr_temp3_fault.dev_attr.attr,
-	&sensor_dev_attr_temp3_alarm.dev_attr.attr,
-	&sensor_dev_attr_temp3_max.dev_attr.attr,
-	&sensor_dev_attr_temp3_min.dev_attr.attr,
-	&sensor_dev_attr_temp3_offset.dev_attr.attr,
 	&sensor_dev_attr_temp3_auto_point1_temp.dev_attr.attr,
 	&sensor_dev_attr_temp3_auto_point2_temp.dev_attr.attr,
-	&sensor_dev_attr_temp3_crit.dev_attr.attr,
-	&sensor_dev_attr_temp3_crit_hyst.dev_attr.attr,
 	&sensor_dev_attr_temp3_smoothing.dev_attr.attr,
-	&sensor_dev_attr_fan1_input.dev_attr.attr,
-	&sensor_dev_attr_fan1_min.dev_attr.attr,
-	&sensor_dev_attr_fan1_alarm.dev_attr.attr,
-	&sensor_dev_attr_fan2_input.dev_attr.attr,
-	&sensor_dev_attr_fan2_min.dev_attr.attr,
-	&sensor_dev_attr_fan2_alarm.dev_attr.attr,
-	&sensor_dev_attr_fan3_input.dev_attr.attr,
-	&sensor_dev_attr_fan3_min.dev_attr.attr,
-	&sensor_dev_attr_fan3_alarm.dev_attr.attr,
-	&sensor_dev_attr_pwm1.dev_attr.attr,
-	&sensor_dev_attr_pwm1_freq.dev_attr.attr,
-	&sensor_dev_attr_pwm1_enable.dev_attr.attr,
-	&sensor_dev_attr_pwm1_auto_channels_temp.dev_attr.attr,
 	&sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr,
 	&sensor_dev_attr_pwm1_auto_point2_pwm.dev_attr.attr,
 	&sensor_dev_attr_pwm1_stall_disable.dev_attr.attr,
-	&sensor_dev_attr_pwm3.dev_attr.attr,
-	&sensor_dev_attr_pwm3_freq.dev_attr.attr,
-	&sensor_dev_attr_pwm3_enable.dev_attr.attr,
-	&sensor_dev_attr_pwm3_auto_channels_temp.dev_attr.attr,
 	&sensor_dev_attr_pwm3_auto_point1_pwm.dev_attr.attr,
 	&sensor_dev_attr_pwm3_auto_point2_pwm.dev_attr.attr,
 	&sensor_dev_attr_pwm3_stall_disable.dev_attr.attr,
@@ -1261,64 +1317,13 @@ static struct attribute *adt7475_attrs[] = {
 	NULL,
 };
 
-static struct attribute *fan4_attrs[] = {
-	&sensor_dev_attr_fan4_input.dev_attr.attr,
-	&sensor_dev_attr_fan4_min.dev_attr.attr,
-	&sensor_dev_attr_fan4_alarm.dev_attr.attr,
-	NULL
-};
-
 static struct attribute *pwm2_attrs[] = {
-	&sensor_dev_attr_pwm2.dev_attr.attr,
-	&sensor_dev_attr_pwm2_freq.dev_attr.attr,
-	&sensor_dev_attr_pwm2_enable.dev_attr.attr,
-	&sensor_dev_attr_pwm2_auto_channels_temp.dev_attr.attr,
 	&sensor_dev_attr_pwm2_auto_point1_pwm.dev_attr.attr,
 	&sensor_dev_attr_pwm2_auto_point2_pwm.dev_attr.attr,
 	&sensor_dev_attr_pwm2_stall_disable.dev_attr.attr,
 	NULL
 };
 
-static struct attribute *in0_attrs[] = {
-	&sensor_dev_attr_in0_input.dev_attr.attr,
-	&sensor_dev_attr_in0_max.dev_attr.attr,
-	&sensor_dev_attr_in0_min.dev_attr.attr,
-	&sensor_dev_attr_in0_alarm.dev_attr.attr,
-	NULL
-};
-
-static struct attribute *in3_attrs[] = {
-	&sensor_dev_attr_in3_input.dev_attr.attr,
-	&sensor_dev_attr_in3_max.dev_attr.attr,
-	&sensor_dev_attr_in3_min.dev_attr.attr,
-	&sensor_dev_attr_in3_alarm.dev_attr.attr,
-	NULL
-};
-
-static struct attribute *in4_attrs[] = {
-	&sensor_dev_attr_in4_input.dev_attr.attr,
-	&sensor_dev_attr_in4_max.dev_attr.attr,
-	&sensor_dev_attr_in4_min.dev_attr.attr,
-	&sensor_dev_attr_in4_alarm.dev_attr.attr,
-	NULL
-};
-
-static struct attribute *in5_attrs[] = {
-	&sensor_dev_attr_in5_input.dev_attr.attr,
-	&sensor_dev_attr_in5_max.dev_attr.attr,
-	&sensor_dev_attr_in5_min.dev_attr.attr,
-	&sensor_dev_attr_in5_alarm.dev_attr.attr,
-	NULL
-};
-
-static struct attribute *in6_attrs[] = {
-	&sensor_dev_attr_in6_input.dev_attr.attr,
-	&sensor_dev_attr_in6_max.dev_attr.attr,
-	&sensor_dev_attr_in6_min.dev_attr.attr,
-	&sensor_dev_attr_in6_alarm.dev_attr.attr,
-	NULL
-};
-
 static struct attribute *vid_attrs[] = {
 	&dev_attr_cpu0_vid.attr,
 	&dev_attr_vrm.attr,
@@ -1326,13 +1331,7 @@ static struct attribute *vid_attrs[] = {
 };
 
 static const struct attribute_group adt7475_attr_group = { .attrs = adt7475_attrs };
-static const struct attribute_group fan4_attr_group = { .attrs = fan4_attrs };
 static const struct attribute_group pwm2_attr_group = { .attrs = pwm2_attrs };
-static const struct attribute_group in0_attr_group = { .attrs = in0_attrs };
-static const struct attribute_group in3_attr_group = { .attrs = in3_attrs };
-static const struct attribute_group in4_attr_group = { .attrs = in4_attrs };
-static const struct attribute_group in5_attr_group = { .attrs = in5_attrs };
-static const struct attribute_group in6_attr_group = { .attrs = in6_attrs };
 static const struct attribute_group vid_attr_group = { .attrs = vid_attrs };
 
 static int adt7475_detect(struct i2c_client *client,
@@ -1932,36 +1931,19 @@ static int adt7475_probe(struct i2c_client *client)
 	data->groups[group_num++] = &adt7475_attr_group;
 
 	/* Features that can be disabled individually */
-	if (data->has_fan4) {
-		data->groups[group_num++] = &fan4_attr_group;
-	}
 	if (data->has_pwm2) {
 		data->groups[group_num++] = &pwm2_attr_group;
 	}
-	if (data->has_voltage & (1 << 0)) {
-		data->groups[group_num++] = &in0_attr_group;
-	}
-	if (data->has_voltage & (1 << 3)) {
-		data->groups[group_num++] = &in3_attr_group;
-	}
-	if (data->has_voltage & (1 << 4)) {
-		data->groups[group_num++] = &in4_attr_group;
-	}
-	if (data->has_voltage & (1 << 5)) {
-		data->groups[group_num++] = &in5_attr_group;
-	}
-	if (data->has_voltage & (1 << 6)) {
-		data->groups[group_num++] = &in6_attr_group;
-	}
 	if (data->has_vid) {
 		data->vrm = vid_which_vrm();
 		data->groups[group_num] = &vid_attr_group;
 	}
 
 	/* register device with all the acquired attributes */
-	hwmon_dev = devm_hwmon_device_register_with_groups(&client->dev,
-							   client->name, data,
-							   data->groups);
+	hwmon_dev = devm_hwmon_device_register_with_info(&client->dev,
+							 client->name, data,
+							 &adt7475_chip_info,
+							 data->groups);
 
 	if (IS_ERR(hwmon_dev)) {
 		ret = PTR_ERR(hwmon_dev);
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/4] hwmon: (adt7475) Add thermal zone sensor support
  2026-11-14  0:41 [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Davíð Steinn Geirsson
  2026-11-14  0:41 ` [PATCH 1/4] dt-bindings: hwmon: adt7475: Add #thermal-sensor-cells Davíð Steinn Geirsson
  2026-11-14  0:41 ` [PATCH 2/4] hwmon: (adt7475) Convert to hwmon_device_register_with_info() Davíð Steinn Geirsson
@ 2026-11-14  0:41 ` Davíð Steinn Geirsson
  2026-11-14  0:41 ` [PATCH 4/4] hwmon: (adt7475) Expose fan control via PWM framework Davíð Steinn Geirsson
  2026-11-14  1:03 ` [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Guenter Roeck
  4 siblings, 0 replies; 6+ messages in thread
From: Davíð Steinn Geirsson @ 2026-11-14  0:41 UTC (permalink / raw)
  To: Guenter Roeck, Jean Delvare, linux-hwmon
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
	Uwe Kleine-König, linux-pwm, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-doc, linux-kernel,
	Davíð Steinn Geirsson

Register the temperature channels as thermal zone sensors. Sensor id N
is temperature channel N, i.e. temp(N+1). Document this in adt7475.rst.

The hwmon core reads a sensor's temperature while registering the
thermal zone, from within hwmon_device_register_with_info(). Read the
chip limits before registering, so that the temperature encoding
(CONFIG5 two's complement bit) is known for that first read.

With CONFIG_THERMAL_OF, devices that have a devicetree node, or sit
below one (the hwmon core uses the nearest ancestor's node), see two
changes:
- Each temperature channel that no thermal zone uses logs
  "tempN_input not attached to any thermal zone" once at probe.
- When a thermal zone uses a channel, the hwmon core sets that
  channel's min and max limit registers to the zone's current trip
  window, as it does for other drivers registering thermal sensors.

Signed-off-by: Davíð Steinn Geirsson <david@dsg.is>
---
This patch was written with Claude Code, using the models Claude
Opus 5.5 and Claude Fable 5.2. They wrote the code and the initial
commit message draft, which I cleaned up. I directed the design,
reviewed the result, compared against the datasheet as far as I
could, and ran tests including on actual hardware.

 Documentation/hwmon/adt7475.rst |  3 +++
 drivers/hwmon/adt7475.c         | 17 ++++++++++++-----
 2 files changed, 15 insertions(+), 5 deletions(-)

diff --git a/Documentation/hwmon/adt7475.rst b/Documentation/hwmon/adt7475.rst
index f90f769d82d6..b5976e3ff3d4 100644
--- a/Documentation/hwmon/adt7475.rst
+++ b/Documentation/hwmon/adt7475.rst
@@ -118,6 +118,9 @@ with a resolution of 0.25 degree Celsius. Temperature readings can be
 configured either for two's complement format or "Offset 64" format,
 wherein 64 is subtracted from the raw value to get the temperature value.
 
+With ``#thermal-sensor-cells = <1>`` in the device node, thermal sensor id N
+is temperature channel N, reported as temp(N+1).
+
 The datasheet is very detailed and describes a procedure for determining
 an optimal configuration for the automatic PWM control.
 
diff --git a/drivers/hwmon/adt7475.c b/drivers/hwmon/adt7475.c
index d5bdb8011e6d..73c19c10bb34 100644
--- a/drivers/hwmon/adt7475.c
+++ b/drivers/hwmon/adt7475.c
@@ -1243,6 +1243,9 @@ static const struct hwmon_ops adt7475_hwmon_ops = {
 				 HWMON_PWM_FREQ | HWMON_PWM_AUTO_CHANNELS_TEMP)
 
 static const struct hwmon_channel_info * const adt7475_info[] = {
+	/* The hwmon core only looks for HWMON_C_REGISTER_TZ in info[0] */
+	HWMON_CHANNEL_INFO(chip,
+			   HWMON_C_REGISTER_TZ),
 	HWMON_CHANNEL_INFO(in,
 			   ADT7475_IN_CONFIG,	/* in0: +2.5V */
 			   ADT7475_IN_CONFIG,	/* in1: Vccp */
@@ -1928,6 +1931,15 @@ static int adt7475_probe(struct i2c_client *client)
 		break;
 	}
 
+	/*
+	 * Read the limits and settings once, before registering: the hwmon
+	 * core reads the thermal sensors while registering, and that needs
+	 * the temperature encoding from CONFIG5.
+	 */
+	ret = adt7475_update_limits(client);
+	if (ret)
+		return ret;
+
 	data->groups[group_num++] = &adt7475_attr_group;
 
 	/* Features that can be disabled individually */
@@ -1966,11 +1978,6 @@ static int adt7475_probe(struct i2c_client *client)
 			 (data->bypass_attn & (1 << 3)) ? " in3" : "",
 			 (data->bypass_attn & (1 << 4)) ? " in4" : "");
 
-	/* Limits and settings, should never change update more than once */
-	ret = adt7475_update_limits(client);
-	if (ret)
-		return ret;
-
 	return 0;
 }
 
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 4/4] hwmon: (adt7475) Expose fan control via PWM framework
  2026-11-14  0:41 [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Davíð Steinn Geirsson
                   ` (2 preceding siblings ...)
  2026-11-14  0:41 ` [PATCH 3/4] hwmon: (adt7475) Add thermal zone sensor support Davíð Steinn Geirsson
@ 2026-11-14  0:41 ` Davíð Steinn Geirsson
  2026-11-14  1:03 ` [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Guenter Roeck
  4 siblings, 0 replies; 6+ messages in thread
From: Davíð Steinn Geirsson @ 2026-11-14  0:41 UTC (permalink / raw)
  To: Guenter Roeck, Jean Delvare, linux-hwmon
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
	Uwe Kleine-König, linux-pwm, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-doc, linux-kernel,
	Davíð Steinn Geirsson

Register the PWM outputs with the PWM framework when the node has
"#pwm-cells", so that generic consumers such as pwm-fan can drive them,
for example as thermal cooling devices. This follows the ADT7470
driver. A failure to register the PWM chip fails the probe. Without
CONFIG_PWM, no PWM chip is registered. Unlike ADT7470, the driver does
not warn about "#pwm-cells" without CONFIG_PWM: fan-N children use the
property too, so its presence does not imply a PWM consumer.
CONFIG_PWM is bool, so no Kconfig dependency is needed.

The binding also documents "#pwm-cells" without a PWM consumer: fan-N
child nodes reference the chip itself to configure a channel at probe.
Device trees that do this keep working unchanged. Registering the PWM
chip does not touch the hardware, and requesting a channel only marks
it as held. Nothing changes for a channel until a consumer applies a
waveform to it.

The driver implements the waveform callbacks:
* The period selects the longest supported period that does not exceed
  the request. The periods are 1 s divided by the table frequency,
  rounded down, so 44444 ns selects 22.5 kHz exactly. A shorter period
  is rounded up to 44444 ns. pwm_set_waveform_might_sleep() applies
  that unless an exact match is requested; pwm_apply_might_sleep()
  rejects it with -EINVAL.
* The duty cycle is the high time in 1/255 steps of that period, rounded
  down. The duty offset is always 0.
* Writing an enabled waveform puts the channel in manual mode, clears
  the output inversion bit and writes the duty cycle and the per-channel
  frequency. Once a consumer has enabled a channel, the consumer's
  polarity flags therefore replace "adi,pwm-active-state" and the fan-N
  flags for that channel.
* A disabled waveform gives duty 0 (constant low) on a channel in manual
  mode, and leaves a channel in any other mode alone.
* An I2C error is returned to the PWM core. The hwmon register cache
  follows each successful register write, so pwmN_enable reports manual
  mode (1) once a consumer has enabled the channel.
* Reading returns the hardware state. A channel that is not in manual
  mode is not driven by the PWM framework and reads as disabled, so a
  consumer's first enabled request always differs from the read-back
  state and is never skipped by the core. An inverted output reads as
  its high time.

While a consumer holds a channel, writes to that channel's pwmN,
pwmN_enable, pwmN_freq, pwmN_auto_channels_temp,
pwmN_auto_point{1,2}_pwm and pwmN_stall_disable fail with -EBUSY, so
userspace cannot fight the kernel. The attributes stay readable. The
other channels and the temperature-based auto points stay writable: the
auto points only matter in automatic mode, and other channels may use
them.

The ADT7470 driver hides its PWM attributes when "#pwm-cells" is
present. This driver returns -EBUSY instead:
* The ADT7475 binding has documented "#pwm-cells" for fan-N children
  since v6.12. Device trees that follow it must keep their attributes.
  No in-tree device tree uses "#pwm-cells" on these chips today.
* Attribute visibility is fixed when the hwmon device registers, but a
  channel is only held once a consumer requests it, after the attributes
  exist.

Document the PWM provider in adt7475.rst.

Signed-off-by: Davíð Steinn Geirsson <david@dsg.is>
---
This patch was written with Claude Code, using the models Claude
Opus 5.5 and Claude Fable 5.2. They wrote the code and the initial
commit message draft, which I cleaned up. I directed the design,
reviewed the result, compared against the datasheet as far as I
could, and ran tests including on actual hardware.

 Documentation/hwmon/adt7475.rst |  15 ++
 drivers/hwmon/adt7475.c         | 319 ++++++++++++++++++++++++++++++--
 2 files changed, 322 insertions(+), 12 deletions(-)

diff --git a/Documentation/hwmon/adt7475.rst b/Documentation/hwmon/adt7475.rst
index b5976e3ff3d4..0a864adda5e0 100644
--- a/Documentation/hwmon/adt7475.rst
+++ b/Documentation/hwmon/adt7475.rst
@@ -152,6 +152,21 @@ The responsiveness of the ADT747x to temperature changes can be configured.
 This allows smoothing of the fan speed transition. To set the transition time
 set the value in ms in the `temp[1-*]_smoothing` sysfs attribute.
 
+PWM Provider
+------------
+
+With CONFIG_PWM enabled and a ``#pwm-cells`` property in the device node, the
+driver registers the PWM outputs as a PWM chip, so that a consumer such as
+``pwm-fan`` can drive them.
+
+While a consumer holds a channel, writes to that channel's ``pwmN``,
+``pwmN_enable``, ``pwmN_freq``, ``pwmN_auto_channels_temp``,
+``pwmN_auto_point[12]_pwm`` and ``pwmN_stall_disable`` attributes fail with
+-EBUSY. Reads still work. Once a consumer has enabled a channel, the channel
+is in manual mode and ``pwmN_enable`` reads 1. When the consumer releases the
+channel, the attributes become writable again and the channel keeps its last
+mode and duty cycle.
+
 Notes
 -----
 
diff --git a/drivers/hwmon/adt7475.c b/drivers/hwmon/adt7475.c
index 73c19c10bb34..6577a73f71a7 100644
--- a/drivers/hwmon/adt7475.c
+++ b/drivers/hwmon/adt7475.c
@@ -18,8 +18,10 @@
 #include <linux/hwmon-vid.h>
 #include <linux/err.h>
 #include <linux/jiffies.h>
+#include <linux/math64.h>
 #include <linux/of.h>
 #include <linux/property.h>
+#include <linux/pwm.h>
 #include <linux/util_macros.h>
 
 #include <dt-bindings/pwm/pwm.h>
@@ -219,6 +221,7 @@ struct adt7475_data {
 	u8 pwmctl[3];
 	u8 pwmchan[3];
 	u8 enh_acoustics[2];
+	u8 pwm_held;		/* Channels requested through the PWM framework */
 
 	u8 vid;
 	u8 vrm;
@@ -668,14 +671,32 @@ static ssize_t pwm_show(struct device *dev, struct device_attribute *attr,
 	return sprintf(buf, "%d\n", data->pwm[sattr->nr][sattr->index]);
 }
 
-static void adt7475_pwm_set(struct adt7475_data *data, int nr, int index,
-			    long val)
+/*
+ * Writes to the hwmon attributes of a PWM channel fail while a PWM consumer
+ * holds it. Call with data->lock held.
+ */
+static int adt7475_pwm_check_held(struct adt7475_data *data, int index)
+{
+	lockdep_assert_held(&data->lock);
+
+	return data->pwm_held & BIT(index) ? -EBUSY : 0;
+}
+
+static int adt7475_pwm_set(struct adt7475_data *data, int nr, int index,
+			   long val)
 {
 	struct i2c_client *client = data->client;
 	unsigned char reg = 0;
+	int ret;
 
 	mutex_lock(&data->lock);
 
+	ret = adt7475_pwm_check_held(data, index);
+	if (ret) {
+		mutex_unlock(&data->lock);
+		return ret;
+	}
+
 	switch (nr) {
 	case INPUT:
 		/* Get a fresh value for CONTROL */
@@ -688,7 +709,7 @@ static void adt7475_pwm_set(struct adt7475_data *data, int nr, int index,
 		 */
 		if (((data->pwm[CONTROL][index] >> 5) & 7) != 7) {
 			mutex_unlock(&data->lock);
-			return;
+			return 0;
 		}
 
 		reg = PWM_REG(index);
@@ -706,6 +727,8 @@ static void adt7475_pwm_set(struct adt7475_data *data, int nr, int index,
 	data->pwm[nr][index] = clamp_val(val, 0, 0xFF);
 	i2c_smbus_write_byte_data(client, reg, data->pwm[nr][index]);
 	mutex_unlock(&data->lock);
+
+	return 0;
 }
 
 static ssize_t pwm_store(struct device *dev, struct device_attribute *attr,
@@ -714,13 +737,14 @@ static ssize_t pwm_store(struct device *dev, struct device_attribute *attr,
 	struct sensor_device_attribute_2 *sattr = to_sensor_dev_attr_2(attr);
 	struct adt7475_data *data = dev_get_drvdata(dev);
 	long val;
+	int ret;
 
 	if (kstrtol(buf, 10, &val))
 		return -EINVAL;
 
-	adt7475_pwm_set(data, sattr->nr, sattr->index, val);
+	ret = adt7475_pwm_set(data, sattr->nr, sattr->index, val);
 
-	return count;
+	return ret ? ret : count;
 }
 
 static ssize_t stall_disable_show(struct device *dev,
@@ -749,6 +773,11 @@ static ssize_t stall_disable_store(struct device *dev,
 
 	mutex_lock(&data->lock);
 
+	if (adt7475_pwm_check_held(data, sattr->index)) {
+		mutex_unlock(&data->lock);
+		return -EBUSY;
+	}
+
 	data->enh_acoustics[0] &= ~mask;
 	if (val)
 		data->enh_acoustics[0] |= mask;
@@ -1111,22 +1140,30 @@ static int adt7475_pwm_write(struct device *dev, u32 attr, int channel,
 
 	switch (attr) {
 	case hwmon_pwm_input:
-		adt7475_pwm_set(data, INPUT, channel, val);
+		ret = adt7475_pwm_set(data, INPUT, channel, val);
 		break;
 
 	case hwmon_pwm_enable:
 		mutex_lock(&data->lock);
-		/* Read Modify Write PWM values */
-		adt7475_read_pwm(client, channel);
-		ret = hw_set_pwm(client, channel, val, data->pwmchan[channel]);
+		ret = adt7475_pwm_check_held(data, channel);
+		if (!ret) {
+			/* Read Modify Write PWM values */
+			adt7475_read_pwm(client, channel);
+			ret = hw_set_pwm(client, channel, val,
+					 data->pwmchan[channel]);
+		}
 		mutex_unlock(&data->lock);
 		break;
 
 	case hwmon_pwm_auto_channels_temp:
 		mutex_lock(&data->lock);
-		/* Read Modify Write PWM values */
-		adt7475_read_pwm(client, channel);
-		ret = hw_set_pwm(client, channel, data->pwmctl[channel], val);
+		ret = adt7475_pwm_check_held(data, channel);
+		if (!ret) {
+			/* Read Modify Write PWM values */
+			adt7475_read_pwm(client, channel);
+			ret = hw_set_pwm(client, channel,
+					 data->pwmctl[channel], val);
+		}
 		mutex_unlock(&data->lock);
 		break;
 
@@ -1136,6 +1173,12 @@ static int adt7475_pwm_write(struct device *dev, u32 attr, int channel,
 
 		mutex_lock(&data->lock);
 
+		ret = adt7475_pwm_check_held(data, channel);
+		if (ret) {
+			mutex_unlock(&data->lock);
+			break;
+		}
+
 		data->range[channel] =
 			adt7475_read(TEMP_TRANGE_REG(channel));
 		data->range[channel] &= ~0xf;
@@ -1804,6 +1847,250 @@ static int adt7475_fan_pwm_config(struct i2c_client *client)
 	return 0;
 }
 
+/*
+ * PWM framework provider. Requesting a channel only marks it held: the
+ * hardware is left alone until the consumer applies a state.
+ *
+ * Limitations:
+ * - Only the fixed periods of pwmfreq_table (11 Hz to 22.5 kHz) are
+ *   supported. The duty cycle is set in 1/255 steps of the period.
+ * - The duty offset is not supported and is always 0.
+ * - Disabling a channel in manual mode sets its duty cycle to 0, so the
+ *   output is constant low. A channel in any other mode is left alone.
+ * - Mode, duty cycle and frequency are separate register writes, in that
+ *   order. An update is not atomic, and the output can briefly run with
+ *   a mix of old and new settings.
+ */
+static int adt7475_pwm_request(struct pwm_chip *chip, struct pwm_device *pwm)
+{
+	struct adt7475_data *data = pwmchip_get_drvdata(chip);
+
+	/* The PWM2 pin is the SMBALERT# output */
+	if (pwm->hwpwm == 1 && !data->has_pwm2)
+		return -ENODEV;
+
+	mutex_lock(&data->lock);
+	data->pwm_held |= BIT(pwm->hwpwm);
+	mutex_unlock(&data->lock);
+
+	return 0;
+}
+
+static void adt7475_pwm_free(struct pwm_chip *chip, struct pwm_device *pwm)
+{
+	struct adt7475_data *data = pwmchip_get_drvdata(chip);
+
+	mutex_lock(&data->lock);
+	data->pwm_held &= ~BIT(pwm->hwpwm);
+	mutex_unlock(&data->lock);
+}
+
+/*
+ * The waveform API has no polarity: duty is the high time in 1/255 steps of
+ * the period.
+ */
+struct adt7475_pwm_wfhw {
+	bool enabled;
+	u8 duty;
+	u8 freq_idx;	/* Index into pwmfreq_table */
+};
+
+static u64 adt7475_pwm_period(int freq_idx)
+{
+	return NSEC_PER_SEC / pwmfreq_table[freq_idx];
+}
+
+static int adt7475_pwm_round_waveform_tohw(struct pwm_chip *chip,
+					   struct pwm_device *pwm,
+					   const struct pwm_waveform *wf,
+					   void *_wfhw)
+{
+	struct adt7475_pwm_wfhw *wfhw = _wfhw;
+	u64 period;
+	int i;
+
+	if (!wf->period_length_ns) {
+		*wfhw = (struct adt7475_pwm_wfhw){ .enabled = false };
+		return 0;
+	}
+
+	/* The longest supported period that does not exceed the request */
+	for (i = 0; i < ARRAY_SIZE(pwmfreq_table) - 1; i++)
+		if (adt7475_pwm_period(i) <= wf->period_length_ns)
+			break;
+	period = adt7475_pwm_period(i);
+
+	wfhw->enabled = true;
+	wfhw->freq_idx = i;
+	if (wf->duty_length_ns >= period)
+		wfhw->duty = 0xFF;
+	else
+		wfhw->duty = mul_u64_u64_div_u64(wf->duty_length_ns, 0xFF,
+						 period);
+
+	/* Requested period shorter than the shortest supported one */
+	return period > wf->period_length_ns ? 1 : 0;
+}
+
+static int adt7475_pwm_round_waveform_fromhw(struct pwm_chip *chip,
+					     struct pwm_device *pwm,
+					     const void *_wfhw,
+					     struct pwm_waveform *wf)
+{
+	const struct adt7475_pwm_wfhw *wfhw = _wfhw;
+	u64 period;
+
+	if (!wfhw->enabled) {
+		*wf = (struct pwm_waveform){ };
+		return 0;
+	}
+
+	period = adt7475_pwm_period(wfhw->freq_idx);
+	wf->period_length_ns = period;
+	wf->duty_length_ns = DIV_ROUND_UP_ULL((u64)wfhw->duty * period, 0xFF);
+	wf->duty_offset_ns = 0;
+
+	return 0;
+}
+
+/*
+ * A channel that is not in manual mode is not driven by the PWM framework
+ * and reads as disabled. A consumer's first enabled request then always
+ * differs from the read-back state, so the core does not skip it.
+ */
+static int adt7475_pwm_read_waveform(struct pwm_chip *chip,
+				     struct pwm_device *pwm, void *_wfhw)
+{
+	struct adt7475_data *data = pwmchip_get_drvdata(chip);
+	struct i2c_client *client = data->client;
+	struct adt7475_pwm_wfhw *wfhw = _wfhw;
+	int config, duty, range;
+
+	mutex_lock(&data->lock);
+	config = adt7475_read(PWM_CONFIG_REG(pwm->hwpwm));
+	duty = adt7475_read(PWM_REG(pwm->hwpwm));
+	range = adt7475_read(TEMP_TRANGE_REG(pwm->hwpwm));
+	mutex_unlock(&data->lock);
+
+	if (config < 0)
+		return config;
+	if (duty < 0)
+		return duty;
+	if (range < 0)
+		return range;
+
+	if (((config >> 5) & 7) != 7) {
+		*wfhw = (struct adt7475_pwm_wfhw){ .enabled = false };
+		return 0;
+	}
+
+	wfhw->enabled = true;
+	wfhw->duty = config & BIT(4) ? 0xFF - duty : duty;
+	wfhw->freq_idx = clamp_val(range & 0xf, 0,
+				   ARRAY_SIZE(pwmfreq_table) - 1);
+
+	return 0;
+}
+
+/*
+ * Switch the channel to manual mode with normal polarity and write the duty
+ * cycle. A negative freq_idx keeps the current frequency. A negative duty
+ * means disabled: a channel in manual mode gets duty 0, a channel in another
+ * mode is left alone. Each cache is updated as soon as its register write
+ * succeeds. Call with data->lock held.
+ */
+static int adt7475_pwm_write_hw(struct adt7475_data *data, int index,
+				int duty, int freq_idx)
+{
+	struct i2c_client *client = data->client;
+	int ret;
+	u8 val;
+
+	ret = adt7475_read(PWM_CONFIG_REG(index));
+	if (ret < 0)
+		return ret;
+	if (duty < 0) {
+		if (((ret >> 5) & 7) != 7)
+			return 0;
+		duty = 0;
+	}
+	val = (ret & ~(0xE0 | BIT(4))) | (7 << 5);
+	if (val != ret) {
+		ret = i2c_smbus_write_byte_data(client, PWM_CONFIG_REG(index),
+						val);
+		if (ret)
+			return ret;
+	}
+	data->pwm[CONTROL][index] = val;
+	data->pwmctl[index] = 1;
+
+	ret = i2c_smbus_write_byte_data(client, PWM_REG(index), duty);
+	if (ret)
+		return ret;
+	data->pwm[INPUT][index] = duty;
+
+	if (freq_idx < 0)
+		return 0;
+
+	ret = adt7475_read(TEMP_TRANGE_REG(index));
+	if (ret < 0)
+		return ret;
+	val = (ret & ~0xf) | freq_idx;
+	if (val != ret) {
+		ret = i2c_smbus_write_byte_data(client, TEMP_TRANGE_REG(index),
+						val);
+		if (ret)
+			return ret;
+	}
+	data->range[index] = val;
+
+	return 0;
+}
+
+static int adt7475_pwm_write_waveform(struct pwm_chip *chip,
+				      struct pwm_device *pwm,
+				      const void *_wfhw)
+{
+	struct adt7475_data *data = pwmchip_get_drvdata(chip);
+	const struct adt7475_pwm_wfhw *wfhw = _wfhw;
+	int ret;
+
+	mutex_lock(&data->lock);
+	if (wfhw->enabled)
+		ret = adt7475_pwm_write_hw(data, pwm->hwpwm, wfhw->duty,
+					   wfhw->freq_idx);
+	else
+		ret = adt7475_pwm_write_hw(data, pwm->hwpwm, -1, -1);
+	mutex_unlock(&data->lock);
+
+	return ret;
+}
+
+static const struct pwm_ops adt7475_pwm_ops = {
+	.request = adt7475_pwm_request,
+	.free = adt7475_pwm_free,
+	.sizeof_wfhw = sizeof(struct adt7475_pwm_wfhw),
+	.round_waveform_tohw = adt7475_pwm_round_waveform_tohw,
+	.round_waveform_fromhw = adt7475_pwm_round_waveform_fromhw,
+	.read_waveform = adt7475_pwm_read_waveform,
+	.write_waveform = adt7475_pwm_write_waveform,
+};
+
+static int adt7475_pwm_register(struct i2c_client *client)
+{
+	struct adt7475_data *data = i2c_get_clientdata(client);
+	struct pwm_chip *chip;
+
+	chip = devm_pwmchip_alloc(&client->dev, ADT7475_PWM_COUNT, 0);
+	if (IS_ERR(chip))
+		return PTR_ERR(chip);
+
+	chip->ops = &adt7475_pwm_ops;
+	pwmchip_set_drvdata(chip, data);
+
+	return devm_pwmchip_add(&client->dev, chip);
+}
+
 static int adt7475_probe(struct i2c_client *client)
 {
 	enum chips chip;
@@ -1962,6 +2249,14 @@ static int adt7475_probe(struct i2c_client *client)
 		return ret;
 	}
 
+	if (IS_ENABLED(CONFIG_PWM) &&
+	    device_property_present(&client->dev, "#pwm-cells")) {
+		ret = adt7475_pwm_register(client);
+		if (ret)
+			return dev_err_probe(&client->dev, ret,
+					     "Failed to register PWM chip\n");
+	}
+
 	dev_info(&client->dev, "%s device, revision %d\n",
 		 names[chip], revision);
 	if ((data->has_voltage & 0x11) || data->has_fan4 || data->has_pwm2)
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support
  2026-11-14  0:41 [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Davíð Steinn Geirsson
                   ` (3 preceding siblings ...)
  2026-11-14  0:41 ` [PATCH 4/4] hwmon: (adt7475) Expose fan control via PWM framework Davíð Steinn Geirsson
@ 2026-11-14  1:03 ` Guenter Roeck
  4 siblings, 0 replies; 6+ messages in thread
From: Guenter Roeck @ 2026-11-14  1:03 UTC (permalink / raw)
  To: Davíð Steinn Geirsson, Jean Delvare, linux-hwmon
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
	Uwe Kleine-König, linux-pwm, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, linux-doc, linux-kernel

On 11/13/26 16:41, Davíð Steinn Geirsson wrote:
> 
> This series lets boards use the temperature channels of the ADT7473,
> ADT7475, ADT7476 and ADT7490 as thermal zone sensors, and drive their
> PWM outputs with a PWM consumer such as pwm-fan. The kernel thermal
> framework can then control the fans. It follows the ADT7470 series [1],
> merged for v7.3.
> 

I am getting flooded with patches, and I have no time to review them all.

Rule change: Functional changes for any hardware monitoring driver which
does not already implement it will have to include conversion to the
with_info hardware monitoring API.

Thanks,
Guenter


^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-11-14  1:03 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-14  0:41 [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Davíð Steinn Geirsson
2026-11-14  0:41 ` [PATCH 1/4] dt-bindings: hwmon: adt7475: Add #thermal-sensor-cells Davíð Steinn Geirsson
2026-11-14  0:41 ` [PATCH 2/4] hwmon: (adt7475) Convert to hwmon_device_register_with_info() Davíð Steinn Geirsson
2026-11-14  0:41 ` [PATCH 3/4] hwmon: (adt7475) Add thermal zone sensor support Davíð Steinn Geirsson
2026-11-14  0:41 ` [PATCH 4/4] hwmon: (adt7475) Expose fan control via PWM framework Davíð Steinn Geirsson
2026-11-14  1:03 ` [PATCH 0/4] hwmon: (adt7475) Add thermal sensor and PWM provider support Guenter Roeck

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox