From 08e6615c909b2504a5862764f3a7fca00c7ab066 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 21 Sep 2026 13:56:00 +0800 Subject: [PATCH 01/97] hwmon: (k10temp) Stop matching Hygon devices k10temp matches the Hygon F3 device, but derives the node passed to amd_smn_read() from the F3 PCI slot using the AMD fixed-slot mapping (slot - 0x18). Hygon PCI slots do not encode DF node identity, and their SMN roots are grouped by socket. The derived node number therefore cannot identify a valid Hygon SMN target or provide usable temperature monitoring. Removing the match does not disable a working monitoring path. Remove the Hygon PCI match and Family 0x18 branch. Hygon temperature monitoring will use the Hygon node and SMN interfaces in a separate series. Fixes: d93217d84c6c ("hwmon: (k10temp) Add Hygon Dhyana support") Cc: stable@vger.kernel.org Signed-off-by: Lin Wang Link: https://patch.msgid.link/20260921055603.25112-6-wanglin@open-hieco.net Signed-off-by: Guenter Roeck --- Documentation/hwmon/k10temp.rst | 8 +++----- drivers/hwmon/k10temp.c | 5 ++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Documentation/hwmon/k10temp.rst b/Documentation/hwmon/k10temp.rst index 91b99adc6c48..9680c4f3383c 100644 --- a/Documentation/hwmon/k10temp.rst +++ b/Documentation/hwmon/k10temp.rst @@ -28,8 +28,6 @@ Supported chips: * AMD Family 17h processors: "Zen", "Zen 2" -* AMD Family 18h processors: "Hygon Dhyana" - * AMD Family 19h processors: "Zen 3" Prefix: 'k10temp' @@ -128,7 +126,7 @@ it. Models from 17h family report relative temperature, the driver aims to compensate and report the real temperature. -On Family 17h and Family 18h CPUs, additional temperature sensors may report -Core Complex Die (CCD) temperatures. Up to 8 such temperatures are reported -as temp{3..10}_input, labeled Tccd{1..8}. Actual support depends on the CPU +On Family 17h CPUs, additional temperature sensors may report Core Complex +Die (CCD) temperatures. Up to 8 such temperatures are reported as +temp{3..10}_input, labeled Tccd{1..8}. Actual support depends on the CPU variant. diff --git a/drivers/hwmon/k10temp.c b/drivers/hwmon/k10temp.c index 3e7e63edc6a3..e0a7211a513c 100644 --- a/drivers/hwmon/k10temp.c +++ b/drivers/hwmon/k10temp.c @@ -64,7 +64,7 @@ MODULE_PARM_DESC(force, "force loading on processors with erratum 319"); #define F15H_M60H_HARDWARE_TEMP_CTRL_OFFSET 0xd8200c64 #define F15H_M60H_REPORTED_TEMP_CTRL_OFFSET 0xd8200ca4 -/* Common for Zen CPU families (Family 17h and 18h and 19h and 1Ah) */ +/* Common for Zen CPU families (Family 17h, 19h and 1Ah) */ #define ZEN_REPORTED_TEMP_CTRL_BASE 0x00059800 #define ZEN_CCD_TEMP(offset, x) (ZEN_REPORTED_TEMP_CTRL_BASE + \ @@ -475,7 +475,7 @@ static int k10temp_probe(struct pci_dev *pdev, const struct pci_device_id *id) data->read_tempreg = read_tempreg_pci; } - if (boot_cpu_data.x86 == 0x17 || boot_cpu_data.x86 == 0x18) { + if (boot_cpu_data.x86 == 0x17) { switch (boot_cpu_data.x86_model) { case 0x1: /* Zen */ case 0x8: /* Zen+ */ @@ -583,7 +583,6 @@ static const struct pci_device_id k10temp_id_table[] = { { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_1AH_M60H_DF_F3) }, { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_1AH_M70H_DF_F3) }, { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_1AH_M90H_DF_F3) }, - { PCI_VDEVICE(HYGON, PCI_DEVICE_ID_AMD_17H_DF_F3) }, {} }; MODULE_DEVICE_TABLE(pci, k10temp_id_table); From f58c16056f8585ebafb0f1f5d90c23085443a4b9 Mon Sep 17 00:00:00 2001 From: Ridham Khurana Date: Wed, 23 Sep 2026 17:25:48 +0000 Subject: [PATCH 02/97] hwmon: (lm95245) Fix negative temperature conversion temp_from_reg_signed() uses only the high byte when the sign bit is set and drops the fractional bits in the low byte. Writing -500 to temp2_offset stores 0xff80 (-0.5 degrees C) in the offset registers, but reading temp2_offset back returns -1000. Negative temp1_input and temp2_input values are rounded down to the next whole degree the same way. The chip reports -0.125 degrees C as 0xffe0, which the driver returns as -1000. Treat the register pair as a single 16-bit two's complement value. Fixes: fffd80ccc1e6 ("hwmon: LM95245 driver") Cc: stable@vger.kernel.org Signed-off-by: Ridham Khurana Link: https://patch.msgid.link/20260923172551.1593798-1-khurana.ridham222@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/lm95245.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/hwmon/lm95245.c b/drivers/hwmon/lm95245.c index 11553391f54b..925574b8b0ca 100644 --- a/drivers/hwmon/lm95245.c +++ b/drivers/hwmon/lm95245.c @@ -96,9 +96,9 @@ static int temp_from_reg_unsigned(u8 val_h, u8 val_l) static int temp_from_reg_signed(u8 val_h, u8 val_l) { - if (val_h & 0x80) - return (val_h - 0x100) * 1000; - return temp_from_reg_unsigned(val_h, val_l); + s16 val_hl = (val_h << 8) | val_l; + + return val_hl * 1000 / 256; } static int lm95245_read_conversion_rate(struct lm95245_data *data) From eddb0aaacce4baa9d8176eb2f4bd9e99e867f8fc Mon Sep 17 00:00:00 2001 From: Ridham Khurana Date: Thu, 24 Sep 2026 20:53:23 +0000 Subject: [PATCH 03/97] hwmon: (lm70) Fix rounding of negative temperatures temp1_input_show() drops the low bits of the temperature register by dividing the raw value (raw / 32 on the LM70). These bits are not part of the temperature: the LM70, LM71, LM74 and TMP122/TMP124 always return some of them as 1, and the TMP125 fills them with copies of the temperature LSB. Division rounds toward zero, so a negative temperature with any of these bits set is reported one LSB too high. For example, the TMP122 returns 0xffff for -0.0625 degrees C, which the driver reports as 0. The LM70 returns 0xf39f for -25 degrees C, which the driver reports as -24750. Use an arithmetic right shift instead, which drops the low bits and rounds down. Fixes: e1a8e913f97e ("[PATCH] lm70: New hardware monitoring driver") Fixes: a86e94dc946d ("hwmon: (lm70) Add support for LM71 and LM74") Fixes: cd929672a9ef ("hwmon: (lm70) Add ti,tmp125 support") Cc: stable@vger.kernel.org Signed-off-by: Ridham Khurana Link: https://patch.msgid.link/20260924205326.1739229-1-khurana.ridham222@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/lm70.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/hwmon/lm70.c b/drivers/hwmon/lm70.c index 8b525725fef4..b29bac7a4cc1 100644 --- a/drivers/hwmon/lm70.c +++ b/drivers/hwmon/lm70.c @@ -96,21 +96,21 @@ static ssize_t temp1_input_show(struct device *dev, */ switch (p_lm70->chip) { case LM70_CHIP_LM70: - val = ((int)raw / 32) * 250; + val = ((int)raw >> 5) * 250; break; case LM70_CHIP_TMP121: case LM70_CHIP_TMP122: case LM70_CHIP_LM74: - val = ((int)raw / 8) * 625 / 10; + val = ((int)raw >> 3) * 625 / 10; break; case LM70_CHIP_LM71: - val = ((int)raw / 4) * 3125 / 100; + val = ((int)raw >> 2) * 3125 / 100; break; case LM70_CHIP_TMP125: - val = (sign_extend32(raw, 14) / 32) * 250; + val = (sign_extend32(raw, 14) >> 5) * 250; break; } From e463cd582ca125e359ad1a66cb076434c14638b2 Mon Sep 17 00:00:00 2001 From: Ricardo Neri Date: Thu, 24 Sep 2026 19:33:20 -0700 Subject: [PATCH 04/97] hwmon: (coretemp) Refresh the temperature on the first read show_temp() reads the status MSR only when the cached temperature is older than one second. Commit 5c0e64dde80f ("hwmon: (coretemp) Remove obsolete temp_data->valid") dropped the tdata->valid check that used to force the very first read, leaving that comparison as the only trigger. A never-updated temp_data carries a zero timestamp, which does not look stale on 32-bit kernels: jiffies starts 300 seconds short of wrapping, so the comparison stays false until jiffies wraps and passes HZ. For the first 301 seconds of uptime temp%d_input reports the zero left by the allocator rather than the CPU temperature. 64-bit kernels are unaffected: jiffies starts at a positive value there and does not wrap. Backdate the timestamp when the temperature data is allocated. One jiffy older than the caching interval is stale under either word size, and the first refresh overwrites it. Fixes: 5c0e64dde80f ("hwmon: (coretemp) Remove obsolete temp_data->valid") Cc: stable@vger.kernel.org Signed-off-by: Ricardo Neri Link: https://patch.msgid.link/20260924-coretemp-temp-fault-v1-1-1884f0ff97d5@linux.intel.com Signed-off-by: Guenter Roeck --- drivers/hwmon/coretemp.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/hwmon/coretemp.c b/drivers/hwmon/coretemp.c index 5095eb057680..ace51e08e72d 100644 --- a/drivers/hwmon/coretemp.c +++ b/drivers/hwmon/coretemp.c @@ -511,6 +511,11 @@ init_temp_data(struct platform_data *pdata, unsigned int cpu, int pkg_flag) tdata->cpu = cpu; tdata->cpu_core_id = topology_core_id(cpu); tdata->attr_size = MAX_CORE_ATTRS; + /* + * A zero timestamp does not look stale on 32-bit, where jiffies + * starts just short of wrapping. Backdate it instead. + */ + tdata->last_updated = jiffies - HZ - 1; mutex_init(&tdata->update_lock); return tdata; } From 56c1d32323e773b8b927316f8beeae9238576335 Mon Sep 17 00:00:00 2001 From: Tom Verdonck Date: Fri, 25 Sep 2026 20:23:22 +0200 Subject: [PATCH 05/97] hwmon: (tmp102) Fix jiffies wraparound in conversion-ready check tmp102 records a one-shot deadline in ->ready_time at probe (and resume) and, on every temperature read, refuses the read with -EAGAIN while time_before(jiffies, ready_time) is true, in order to skip the first ~35 ms conversion. ->ready_time is an unsigned long compared with time_before(), whose signed difference is only meaningful while the two values are within LONG_MAX jiffies of each other. Because ->ready_time is set once and never refreshed, jiffies keeps advancing away from it, and after 2^31 jiffies the difference flips sign. On a 32-bit HZ=100 kernel that happens ~248.5 days after boot: time_before() then permanently reports "not ready" and the driver returns -EAGAIN on every read, without ever touching the sensor, until the next reboot. Store the deadline as a 64-bit jiffies value and compare it with get_jiffies_64()/time_before64(), which does not wrap in any practical uptime. Fixes: 3d8f7a89a197 ("hwmon: (tmp102) Improve handling of initial read delay") Cc: stable@vger.kernel.org Signed-off-by: Tom Verdonck Link: https://patch.msgid.link/2952f070f82913aea96eec25d28008d3b9058656.1790358356.git.tom.verdonck@inspiral.be Signed-off-by: Guenter Roeck --- drivers/hwmon/tmp102.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/hwmon/tmp102.c b/drivers/hwmon/tmp102.c index 50a8b050d8da..8c8463ddb6c4 100644 --- a/drivers/hwmon/tmp102.c +++ b/drivers/hwmon/tmp102.c @@ -58,7 +58,7 @@ struct tmp102 { const char *label; struct regmap *regmap; u16 config_orig; - unsigned long ready_time; + u64 ready_time; u16 sample_time; }; @@ -106,7 +106,7 @@ static int tmp102_read_temp(struct device *dev, u32 attr, long *val) switch (attr) { case hwmon_temp_input: /* Is it too early to return a conversion ? */ - if (time_before(jiffies, tmp102->ready_time)) { + if (time_before64(get_jiffies_64(), tmp102->ready_time)) { dev_dbg(dev, "%s: Conversion not ready yet..\n", __func__); return -EAGAIN; } @@ -354,7 +354,7 @@ static int tmp102_probe(struct i2c_client *client) * Mark that we are not ready with data until the first * conversion is complete */ - tmp102->ready_time = jiffies + msecs_to_jiffies(CONVERSION_TIME_MS); + tmp102->ready_time = get_jiffies_64() + msecs_to_jiffies(CONVERSION_TIME_MS); hwmon_dev = devm_hwmon_device_register_with_info(dev, client->name, tmp102, @@ -387,7 +387,7 @@ static int tmp102_resume(struct device *dev) err = regmap_update_bits(tmp102->regmap, TMP102_CONF_REG, TMP102_CONF_SD, 0); - tmp102->ready_time = jiffies + msecs_to_jiffies(CONVERSION_TIME_MS); + tmp102->ready_time = get_jiffies_64() + msecs_to_jiffies(CONVERSION_TIME_MS); return err; } From 6b8b1d1d7bc7e579b53fc7c3293b787d68de702a Mon Sep 17 00:00:00 2001 From: Tom Verdonck Date: Fri, 25 Sep 2026 20:23:23 +0200 Subject: [PATCH 06/97] hwmon: (tmp108) Fix jiffies wraparound in conversion-ready check tmp108 records a one-shot deadline in ->ready_time at probe (and resume) and, on every temperature read, refuses the read with -EAGAIN while time_before(jiffies, ready_time) is true, in order to skip the initial conversion. ->ready_time is an unsigned long compared with time_before(), whose signed difference is only meaningful while the two values are within LONG_MAX jiffies of each other. Because ->ready_time is set once and never refreshed, jiffies keeps advancing away from it, and after 2^31 jiffies the difference flips sign. On a 32-bit HZ=100 kernel that happens ~248.5 days after boot: time_before() then permanently reports "not ready" and the driver returns -EAGAIN on every read, without ever touching the sensor, until the next reboot. The continuous-mode path, which sets ->ready_time to jiffies with no added conversion delay, wraps the same way. Store the deadline as a 64-bit jiffies value and compare it with get_jiffies_64()/time_before64(), which does not wrap in any practical uptime. Fixes: 66e1c9171339 ("hwmon: Add Texas Instruments TMP108 temperature sensor driver.") Cc: stable@vger.kernel.org Signed-off-by: Tom Verdonck Link: https://patch.msgid.link/525fa49d8c50922344fa53e22be5e6133c9bbb62.1790358356.git.tom.verdonck@inspiral.be Signed-off-by: Guenter Roeck --- drivers/hwmon/tmp108.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/hwmon/tmp108.c b/drivers/hwmon/tmp108.c index 9fa31bd66ff6..91d20a1ce112 100644 --- a/drivers/hwmon/tmp108.c +++ b/drivers/hwmon/tmp108.c @@ -78,7 +78,7 @@ struct tmp108 { struct regmap *regmap; u16 orig_config; - unsigned long ready_time; + u64 ready_time; const struct tmp108_params *params; }; @@ -138,7 +138,7 @@ static int tmp108_read(struct device *dev, enum hwmon_sensor_types type, switch (attr) { case hwmon_temp_input: /* Is it too early to return a conversion ? */ - if (time_before(jiffies, tmp108->ready_time)) { + if (time_before64(get_jiffies_64(), tmp108->ready_time)) { dev_dbg(dev, "%s: Conversion not ready yet..\n", __func__); return -EAGAIN; @@ -477,7 +477,7 @@ static int tmp108_common_probe(struct device *dev, struct regmap *regmap, char * return err; } - tmp108->ready_time = jiffies; + tmp108->ready_time = get_jiffies_64(); if ((tmp108->orig_config & TMP108_CONF_MODE_MASK) == TMP108_MODE_SHUTDOWN) tmp108->ready_time += @@ -528,7 +528,7 @@ static int tmp108_resume(struct device *dev) err = regmap_update_bits(tmp108->regmap, TMP108_REG_CONF, TMP108_CONF_MODE_MASK, TMP108_MODE_CONTINUOUS); - tmp108->ready_time = jiffies + + tmp108->ready_time = get_jiffies_64() + msecs_to_jiffies(TMP108_CONVERSION_TIME_MS); return err; } From ce3ff350c4a3b9466bd67bfe2adb2e3bea732b9e Mon Sep 17 00:00:00 2001 From: Tom Verdonck Date: Fri, 25 Sep 2026 20:23:24 +0200 Subject: [PATCH 07/97] hwmon: (sht4x) Fix jiffies wraparound in heater-ready check sht4x stores in ->heating_complete the jiffies deadline at which the current heater pulse finishes. It is set once at probe (to jiffies) and only updated when the heater is explicitly enabled via sysfs, so in the common case where the heater is never used it stays frozen at its probe value. ->heating_complete is an unsigned long compared with time_before(), whose signed difference is only meaningful while the two values are within LONG_MAX jiffies of each other. Because the deadline is frozen, jiffies keeps advancing away from it, and after 2^31 jiffies the difference flips sign. On a 32-bit HZ=100 kernel that happens ~248.5 days after boot. time_before() in sht4x_read_values() then wrongly reports that heating is still in progress and the read path executes msleep(jiffies_to_msecs(data->heating_complete - curr_jiffies)); with a bogus, huge delta, blocking the temperature read for a very long time instead of returning data. The heater_enable sysfs attributes are affected the same way (spurious -EBUSY and "1" readback). Store the deadline as a 64-bit jiffies value and compare it with get_jiffies_64()/time_before64(), which does not wrap in any practical uptime. Fixes: 0eed6fc3d2b9 ("hwmon: (sht4x): add heater support") Cc: stable@vger.kernel.org Signed-off-by: Tom Verdonck Link: https://patch.msgid.link/7b2087ccb963e097cf3f6333efaec0c46549003b.1790358356.git.tom.verdonck@inspiral.be [groeck: Squashed 'Fix torn read' patch into this one] Signed-off-by: Guenter Roeck --- drivers/hwmon/sht4x.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/drivers/hwmon/sht4x.c b/drivers/hwmon/sht4x.c index a97dda9e92dc..27fddff70354 100644 --- a/drivers/hwmon/sht4x.c +++ b/drivers/hwmon/sht4x.c @@ -67,7 +67,7 @@ DECLARE_CRC8_TABLE(sht4x_crc8_table); */ struct sht4x_data { struct i2c_client *client; - unsigned long heating_complete; /* in jiffies */ + u64 heating_complete; /* in jiffies */ bool data_pending; u32 heater_power; /* in milli-watts */ u32 heater_time; /* in milli-seconds */ @@ -92,14 +92,14 @@ static int sht4x_read_values(struct sht4x_data *data) u8 crc; u8 cmd[SHT4X_CMD_LEN] = {SHT4X_CMD_MEASURE_HPM}; u8 raw_data[SHT4X_RESPONSE_LENGTH]; - unsigned long curr_jiffies; + u64 curr_jiffies; - curr_jiffies = jiffies; - if (time_before(curr_jiffies, data->heating_complete)) + curr_jiffies = get_jiffies_64(); + if (time_before64(curr_jiffies, data->heating_complete)) msleep(jiffies_to_msecs(data->heating_complete - curr_jiffies)); if (data->data_pending && - time_before(jiffies, data->heating_complete + data->update_interval)) { + time_before64(get_jiffies_64(), data->heating_complete + data->update_interval)) { data->data_pending = false; } else { next_update = data->last_updated + @@ -237,7 +237,9 @@ static ssize_t heater_enable_show(struct device *dev, { struct sht4x_data *data = dev_get_drvdata(dev); - return sysfs_emit(buf, "%u\n", time_before(jiffies, data->heating_complete)); + guard(hwmon_lock)(dev); + + return sysfs_emit(buf, "%u\n", time_before64(get_jiffies_64(), data->heating_complete)); } static ssize_t heater_enable_store(struct device *dev, @@ -279,14 +281,14 @@ static ssize_t heater_enable_store(struct device *dev, guard(hwmon_lock)(dev); - if (time_before(jiffies, data->heating_complete)) + if (time_before64(get_jiffies_64(), data->heating_complete)) return -EBUSY; ret = i2c_master_send(data->client, &cmd, SHT4X_CMD_LEN); if (ret < 0) return ret; - data->heating_complete = jiffies + msecs_to_jiffies(heating_time_bound); + data->heating_complete = get_jiffies_64() + msecs_to_jiffies(heating_time_bound); data->data_pending = true; return count; } @@ -410,7 +412,7 @@ static int sht4x_probe(struct i2c_client *client) data->client = client; data->heater_power = 200; data->heater_time = 1000; - data->heating_complete = jiffies; + data->heating_complete = get_jiffies_64(); crc8_populate_msb(sht4x_crc8_table, SHT4X_CRC8_POLYNOMIAL); From f63aaa529cbc73146ff0d52235e5479042285cae Mon Sep 17 00:00:00 2001 From: Ricardo Neri Date: Tue, 6 Oct 2026 06:04:56 -0700 Subject: [PATCH 08/97] hwmon: (coretemp) Return -ENODATA for invalid temperature readings IA32_THERM_STATUS and IA32_PACKAGE_THERM_STATUS have a valid bit, bit 31. The Intel SDM documents it only for the former, but the package register implements it too: x86_pkg_temp_thermal has required it since commit f1a18a105660 ("Thermal: CPU Package temperature thermal"). This driver checked the bit for both registers as well, until commit bf6ea084ebb5 ("hwmon: (coretemp) Do not return -EAGAIN for low temperatures") made it ignore the bit because some CPUs clear it while the temperature is too low to be measured. Since then, temp%d_input reports such a reading as a real temperature. Userspace cannot tell it apart from a genuinely low one. Fail the read with -ENODATA while the CPU reports the reading as invalid. On a CPU that clears the bit, reading temp%d_input now fails where it used to return a low value. sensors(1) shows N/A in its place. Cc: stable@vger.kernel.org Suggested-by: Guenter Roeck Link: https://lore.kernel.org/all/46f9f319-de71-412f-a424-6cb801478456@roeck-us.net/ Signed-off-by: Ricardo Neri Link: https://patch.msgid.link/20261006-coretemp-enodata-v2-1-64e788acc7dc@linux.intel.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/coretemp.rst | 3 +++ drivers/hwmon/coretemp.c | 21 +++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Documentation/hwmon/coretemp.rst b/Documentation/hwmon/coretemp.rst index 349301683381..09f885a847e3 100644 --- a/Documentation/hwmon/coretemp.rst +++ b/Documentation/hwmon/coretemp.rst @@ -50,6 +50,9 @@ All sysfs entries are named with their core_id (represented here by 'X'). ================= ======================================================== tempX_input Core temperature (in millidegrees Celsius). + Reading fails with -ENODATA while the CPU reports the + temperature as invalid. Some CPUs do this when the + temperature is too low to be measured. tempX_max Maximum recommended operating temperature (Tcontrol). All cooling devices should be turned on. tempX_crit Maximum junction temperature (in millidegrees Celsius). diff --git a/drivers/hwmon/coretemp.c b/drivers/hwmon/coretemp.c index ace51e08e72d..bee2c9896d4c 100644 --- a/drivers/hwmon/coretemp.c +++ b/drivers/hwmon/coretemp.c @@ -60,6 +60,8 @@ enum coretemp_attr_index { /* * Per-Core Temperature Data + * @temp_valid: Whether the CPU reported @temp as valid. @temp is not + * reported when this is false. * @tjmax: The static tjmax value when tjmax cannot be retrieved from * IA32_TEMPERATURE_TARGET MSR. * @last_updated: The time when the current temperature value was updated @@ -72,6 +74,7 @@ enum coretemp_attr_index { */ struct temp_data { int temp; + bool temp_valid; int tjmax; unsigned long last_updated; unsigned int cpu; @@ -398,7 +401,8 @@ static ssize_t show_temp(struct device *dev, { struct msr val; struct temp_data *tdata = container_of(devattr, struct temp_data, sd_attrs[ATTR_TEMP]); - int tjmax; + int tjmax, temp; + bool valid; mutex_lock(&tdata->update_lock); @@ -407,17 +411,22 @@ static ssize_t show_temp(struct device *dev, if (time_after(jiffies, tdata->last_updated + HZ)) { rdmsrq_on_cpu(tdata->cpu, tdata->status_reg, &val.q); /* - * Ignore the valid bit. In all observed cases the register - * value is either low or zero if the valid bit is 0. - * Return it instead of reporting an error which doesn't - * really help at all. + * Bit 31 indicates whether the reading is valid. Some CPUs + * clear it while the temperature is too low to be measured. + * Such a reading is not a temperature. Do not report it. */ + tdata->temp_valid = (val.l >> 31) & 1; tdata->temp = tjmax - ((val.l >> 16) & 0xff) * 1000; tdata->last_updated = jiffies; } + valid = tdata->temp_valid; + temp = tdata->temp; mutex_unlock(&tdata->update_lock); - return sprintf(buf, "%d\n", tdata->temp); + + if (!valid) + return -ENODATA; + return sprintf(buf, "%d\n", temp); } static int create_core_attrs(struct temp_data *tdata, struct device *dev) From 0e1d292233c83d41530df6ffa4ec5a382f118b12 Mon Sep 17 00:00:00 2001 From: "Baek Kyu, Kim" Date: Sun, 18 Oct 2026 05:52:34 +0900 Subject: [PATCH 09/97] hwmon: (sht4x) Reject incomplete heater command transfers heater_enable_store() treats a zero return from i2c_master_send() as success. It updates the heater state and returns count, so another attempt to enable the heater fails with -EBUSY. i2c-virtio can return zero when the first message fails. Require SHT4X_CMD_LEN before updating the heater state, as sht4x_probe() already does for the reset command. Fixes: 0eed6fc3d2b9 ("hwmon: (sht4x): add heater support") Cc: stable@vger.kernel.org Signed-off-by: Baek Kyu, Kim Link: https://patch.msgid.link/20261017205234.1084949-1-pocj8ur4in@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/sht4x.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/hwmon/sht4x.c b/drivers/hwmon/sht4x.c index 27fddff70354..0bb9227674a8 100644 --- a/drivers/hwmon/sht4x.c +++ b/drivers/hwmon/sht4x.c @@ -287,6 +287,8 @@ static ssize_t heater_enable_store(struct device *dev, ret = i2c_master_send(data->client, &cmd, SHT4X_CMD_LEN); if (ret < 0) return ret; + if (ret != SHT4X_CMD_LEN) + return -EIO; data->heating_complete = get_jiffies_64() + msecs_to_jiffies(heating_time_bound); data->data_pending = true; From e4fdc82f55cf0e3d5df5c88278da5baa76a109d1 Mon Sep 17 00:00:00 2001 From: Sanan Hasanov Date: Thu, 8 Oct 2026 16:52:45 -0400 Subject: [PATCH 10/97] hwmon: (corsair-cpro) Make wait_input_report_lock IRQ safe ccp_raw_event() is the HID ->raw_event() callback and takes wait_input_report_lock with a plain spin_lock(). This callback can run in very different contexts depending on the transport: from process context (e.g. uhid, via a write() to /dev/uhid) with softirqs and interrupts enabled, and from URB completion, which is softirq context for HCDs using HCD_BH and hard interrupt context for HCDs that do not (e.g. OHCI, UHCI). Taking the lock from process context with softirqs enabled while the same lock is also taken from softirq context can deadlock if the softirq interrupts the lock holder on the same CPU. Lockdep reports: WARNING: inconsistent lock state inconsistent {SOFTIRQ-ON-W} -> {IN-SOFTIRQ-W} usage. ... spin_lock include/linux/spinlock.h:347 [inline] ccp_raw_event+0x51/0x110 drivers/hwmon/corsair-cpro.c:161 __hid_input_report+0x41c/0x580 drivers/hid/hid-core.c:2175 hid_irq_in+0x492/0x710 drivers/hid/usbhid/hid-core.c:287 __usb_hcd_giveback_urb+0x378/0x540 drivers/usb/core/hcd.c:1657 dummy_timer+0xa91/0x4cc0 drivers/usb/gadget/udc/dummy_hcd.c:2023 For the same reason, the spin_lock_bh() in send_usb_cmd() is not sufficient when ccp_raw_event() is called from hard interrupt context. Use spin_lock_irqsave() in ccp_raw_event(), since it may be called with interrupts already disabled, and spin_lock_irq() in send_usb_cmd(), which always runs in process context. Fixes: d02abd57e794 ("hwmon: (corsair-cpro) Protect ccp->wait_input_report with a spinlock") Reported-by: syzbot+0f98b0c0bdeaab2dc2fd@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=0f98b0c0bdeaab2dc2fd Cc: stable@vger.kernel.org Signed-off-by: Sanan Hasanov Link: https://patch.msgid.link/20261008205310.30832-1-sanan.hasanou@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/corsair-cpro.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/drivers/hwmon/corsair-cpro.c b/drivers/hwmon/corsair-cpro.c index c09645152613..e6d31d367698 100644 --- a/drivers/hwmon/corsair-cpro.c +++ b/drivers/hwmon/corsair-cpro.c @@ -135,9 +135,9 @@ static int send_usb_cmd(struct ccp_device *ccp, u8 command, u8 byte1, u8 byte2, * the raw event parsing and marked the ccp->wait_input_report * completion as done. */ - spin_lock_bh(&ccp->wait_input_report_lock); + spin_lock_irq(&ccp->wait_input_report_lock); reinit_completion(&ccp->wait_input_report); - spin_unlock_bh(&ccp->wait_input_report_lock); + spin_unlock_irq(&ccp->wait_input_report_lock); ret = hid_hw_output_report(ccp->hdev, ccp->cmd_buffer, OUT_BUFFER_SIZE); if (ret < 0) @@ -156,15 +156,16 @@ static int send_usb_cmd(struct ccp_device *ccp, u8 command, u8 byte1, u8 byte2, static int ccp_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { struct ccp_device *ccp = hid_get_drvdata(hdev); + unsigned long flags; /* only copy buffer when requested */ - spin_lock(&ccp->wait_input_report_lock); + spin_lock_irqsave(&ccp->wait_input_report_lock, flags); if (!completion_done(&ccp->wait_input_report)) { memcpy(ccp->buffer, data, min(IN_BUFFER_SIZE, size)); ccp->buffer_recv_size = size; complete_all(&ccp->wait_input_report); } - spin_unlock(&ccp->wait_input_report_lock); + spin_unlock_irqrestore(&ccp->wait_input_report_lock, flags); return 0; } From 8184f107c1fe860a93ece55d6d5f7cc25af276b9 Mon Sep 17 00:00:00 2001 From: Guenter Roeck Date: Thu, 13 Aug 2026 14:14:33 -0700 Subject: [PATCH 11/97] hwmon: (core) Add support for currX_emergency and inX_[l]emergency attributes Some hardware monitoring chips support three alarm levels for current and voltage high limits, and three alarm levels for voltage low limits. Add support for currX_emergency, inX_emergency, and inX_lemergency attributes together with the appropriate alarm attributes to support such chips. Cc: Manaf Meethalavalappu Pallikunhi Tested-by: Manaf Meethalavalappu Pallikunhi Signed-off-by: Guenter Roeck Link: https://patch.msgid.link/20260813211433.879638-1-linux@roeck-us.net Signed-off-by: Guenter Roeck --- Documentation/ABI/testing/sysfs-class-hwmon | 36 +++++++++++++++++++++ Documentation/hwmon/sysfs-interface.rst | 12 +++++++ drivers/hwmon/hwmon.c | 6 ++++ include/linux/hwmon.h | 12 +++++++ 4 files changed, 66 insertions(+) diff --git a/Documentation/ABI/testing/sysfs-class-hwmon b/Documentation/ABI/testing/sysfs-class-hwmon index b185bdfc7186..34df6d7bd5f8 100644 --- a/Documentation/ABI/testing/sysfs-class-hwmon +++ b/Documentation/ABI/testing/sysfs-class-hwmon @@ -61,6 +61,18 @@ Description: take drastic action such as power down or reset. At the very least, it should report a fault. +What: /sys/class/hwmon/hwmonX/inY_lemergency +Description: + Voltage emergency min value. + + Unit: millivolt + + RW + + If voltage drops to or below this limit, the system is + expected to take drastic action such as immediate power + down or reset. At the very least, it should report a fault. + What: /sys/class/hwmon/hwmonX/inY_max Description: Voltage max value. @@ -81,6 +93,18 @@ Description: take drastic action such as power down or reset. At the very least, it should report a fault. +What: /sys/class/hwmon/hwmonX/inY_emergency +Description: + Voltage emergency max value. + + Unit: millivolt + + RW + + If voltage reaches or exceeds this limit, the system is expected + to take drastic action such as immediate power down or reset. + At the very least, it should report a fault. + What: /sys/class/hwmon/hwmonX/inY_input Description: Voltage input value. @@ -647,6 +671,18 @@ Description: RW +What: /sys/class/hwmon/hwmonX/currY_emergency +Description: + Current emergency high value. + + Unit: milliampere + + RW + + If a current reaches or exceeds this limit, the system is + expected to take drastic action such as immediate power down + or reset. At the very least, it should report a fault. + What: /sys/class/hwmon/hwmonX/currY_input Description: Current input value diff --git a/Documentation/hwmon/sysfs-interface.rst b/Documentation/hwmon/sysfs-interface.rst index 94e1bbce172a..f7ab5d5c4d3a 100644 --- a/Documentation/hwmon/sysfs-interface.rst +++ b/Documentation/hwmon/sysfs-interface.rst @@ -121,12 +121,18 @@ Voltages `in[0-*]_lcrit` Voltage critical min value. +`in[0-*]_lemergency` + Voltage emergency min value. + `in[0-*]_max` Voltage max value. `in[0-*]_crit` Voltage critical max value. +`in[0-*]_emergency` + Voltage emergency max value. + `in[0-*]_input` Voltage input value. @@ -332,6 +338,9 @@ Currents `curr[1-*]_crit` Current critical high value. +`curr[1-*]_emergency` + Current emergency high value. + `curr[1-*]_input` Current input value. @@ -527,12 +536,15 @@ implementation. +-------------------------------+-----------------------+ | **`in[0-*]_min_alarm`, | Limit alarm | | `in[0-*]_max_alarm`, | | +| `in[0-*]_lemergency_alarm`, | | | `in[0-*]_lcrit_alarm`, | - 0: no alarm | | `in[0-*]_crit_alarm`, | - 1: alarm | +| `in[0-*]_emergency_alarm`, | | | `curr[1-*]_min_alarm`, | | | `curr[1-*]_max_alarm`, | RO | | `curr[1-*]_lcrit_alarm`, | | | `curr[1-*]_crit_alarm`, | | +| `curr[1-*]_emergency_alarm`, | | | `power[1-*]_cap_alarm`, | | | `power[1-*]_max_alarm`, | | | `power[1-*]_crit_alarm`, | | diff --git a/drivers/hwmon/hwmon.c b/drivers/hwmon/hwmon.c index 10d2df3efdfa..3f32b0e83b53 100644 --- a/drivers/hwmon/hwmon.c +++ b/drivers/hwmon/hwmon.c @@ -625,6 +625,8 @@ static const char * const hwmon_in_attr_templates[] = { [hwmon_in_max] = "in%d_max", [hwmon_in_lcrit] = "in%d_lcrit", [hwmon_in_crit] = "in%d_crit", + [hwmon_in_lemergency] = "in%d_lemergency", + [hwmon_in_emergency] = "in%d_emergency", [hwmon_in_average] = "in%d_average", [hwmon_in_lowest] = "in%d_lowest", [hwmon_in_highest] = "in%d_highest", @@ -635,6 +637,8 @@ static const char * const hwmon_in_attr_templates[] = { [hwmon_in_max_alarm] = "in%d_max_alarm", [hwmon_in_lcrit_alarm] = "in%d_lcrit_alarm", [hwmon_in_crit_alarm] = "in%d_crit_alarm", + [hwmon_in_lemergency_alarm] = "in%d_lemergency_alarm", + [hwmon_in_emergency_alarm] = "in%d_emergency_alarm", [hwmon_in_rated_min] = "in%d_rated_min", [hwmon_in_rated_max] = "in%d_rated_max", [hwmon_in_beep] = "in%d_beep", @@ -648,6 +652,7 @@ static const char * const hwmon_curr_attr_templates[] = { [hwmon_curr_max] = "curr%d_max", [hwmon_curr_lcrit] = "curr%d_lcrit", [hwmon_curr_crit] = "curr%d_crit", + [hwmon_curr_emergency] = "curr%d_emergency", [hwmon_curr_average] = "curr%d_average", [hwmon_curr_lowest] = "curr%d_lowest", [hwmon_curr_highest] = "curr%d_highest", @@ -658,6 +663,7 @@ static const char * const hwmon_curr_attr_templates[] = { [hwmon_curr_max_alarm] = "curr%d_max_alarm", [hwmon_curr_lcrit_alarm] = "curr%d_lcrit_alarm", [hwmon_curr_crit_alarm] = "curr%d_crit_alarm", + [hwmon_curr_emergency_alarm] = "curr%d_emergency_alarm", [hwmon_curr_rated_min] = "curr%d_rated_min", [hwmon_curr_rated_max] = "curr%d_rated_max", [hwmon_curr_beep] = "curr%d_beep", diff --git a/include/linux/hwmon.h b/include/linux/hwmon.h index dd713e193d0c..a3a7d27f3b5f 100644 --- a/include/linux/hwmon.h +++ b/include/linux/hwmon.h @@ -134,6 +134,8 @@ enum hwmon_in_attributes { hwmon_in_max, hwmon_in_lcrit, hwmon_in_crit, + hwmon_in_lemergency, + hwmon_in_emergency, hwmon_in_average, hwmon_in_lowest, hwmon_in_highest, @@ -144,6 +146,8 @@ enum hwmon_in_attributes { hwmon_in_max_alarm, hwmon_in_lcrit_alarm, hwmon_in_crit_alarm, + hwmon_in_lemergency_alarm, + hwmon_in_emergency_alarm, hwmon_in_rated_min, hwmon_in_rated_max, hwmon_in_beep, @@ -156,6 +160,8 @@ enum hwmon_in_attributes { #define HWMON_I_MAX BIT(hwmon_in_max) #define HWMON_I_LCRIT BIT(hwmon_in_lcrit) #define HWMON_I_CRIT BIT(hwmon_in_crit) +#define HWMON_I_LEMERGENCY BIT(hwmon_in_lemergency) +#define HWMON_I_EMERGENCY BIT(hwmon_in_emergency) #define HWMON_I_AVERAGE BIT(hwmon_in_average) #define HWMON_I_LOWEST BIT(hwmon_in_lowest) #define HWMON_I_HIGHEST BIT(hwmon_in_highest) @@ -166,6 +172,8 @@ enum hwmon_in_attributes { #define HWMON_I_MAX_ALARM BIT(hwmon_in_max_alarm) #define HWMON_I_LCRIT_ALARM BIT(hwmon_in_lcrit_alarm) #define HWMON_I_CRIT_ALARM BIT(hwmon_in_crit_alarm) +#define HWMON_I_LEMERGENCY_ALARM BIT(hwmon_in_lemergency_alarm) +#define HWMON_I_EMERGENCY_ALARM BIT(hwmon_in_emergency_alarm) #define HWMON_I_RATED_MIN BIT(hwmon_in_rated_min) #define HWMON_I_RATED_MAX BIT(hwmon_in_rated_max) #define HWMON_I_BEEP BIT(hwmon_in_beep) @@ -178,6 +186,7 @@ enum hwmon_curr_attributes { hwmon_curr_max, hwmon_curr_lcrit, hwmon_curr_crit, + hwmon_curr_emergency, hwmon_curr_average, hwmon_curr_lowest, hwmon_curr_highest, @@ -188,6 +197,7 @@ enum hwmon_curr_attributes { hwmon_curr_max_alarm, hwmon_curr_lcrit_alarm, hwmon_curr_crit_alarm, + hwmon_curr_emergency_alarm, hwmon_curr_rated_min, hwmon_curr_rated_max, hwmon_curr_beep, @@ -199,6 +209,7 @@ enum hwmon_curr_attributes { #define HWMON_C_MAX BIT(hwmon_curr_max) #define HWMON_C_LCRIT BIT(hwmon_curr_lcrit) #define HWMON_C_CRIT BIT(hwmon_curr_crit) +#define HWMON_C_EMERGENCY BIT(hwmon_curr_emergency) #define HWMON_C_AVERAGE BIT(hwmon_curr_average) #define HWMON_C_LOWEST BIT(hwmon_curr_lowest) #define HWMON_C_HIGHEST BIT(hwmon_curr_highest) @@ -209,6 +220,7 @@ enum hwmon_curr_attributes { #define HWMON_C_MAX_ALARM BIT(hwmon_curr_max_alarm) #define HWMON_C_LCRIT_ALARM BIT(hwmon_curr_lcrit_alarm) #define HWMON_C_CRIT_ALARM BIT(hwmon_curr_crit_alarm) +#define HWMON_C_EMERGENCY_ALARM BIT(hwmon_curr_emergency_alarm) #define HWMON_C_RATED_MIN BIT(hwmon_curr_rated_min) #define HWMON_C_RATED_MAX BIT(hwmon_curr_rated_max) #define HWMON_C_BEEP BIT(hwmon_curr_beep) From ebdf602889477d6df1ae3869c51f25457573701a Mon Sep 17 00:00:00 2001 From: Marek Vasut Date: Wed, 12 Aug 2026 21:09:44 +0200 Subject: [PATCH 12/97] dt-bindings: hwmon: tmp102: Document TMP110 The TMP110 is register compatible with TMP102, document it using a fallback compatible. Signed-off-by: Marek Vasut Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260812191021.65304-1-marex@nabladev.com Signed-off-by: Guenter Roeck --- .../devicetree/bindings/hwmon/ti,tmp102.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml b/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml index 96b2e4969f78..f0dbe9f07b93 100644 --- a/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml +++ b/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml @@ -4,15 +4,19 @@ $id: http://devicetree.org/schemas/hwmon/ti,tmp102.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: TMP102 temperature sensor +title: TMP102 and TMP110 temperature sensor maintainers: - Krzysztof Kozlowski properties: compatible: - enum: - - ti,tmp102 + oneOf: + - items: + - const: ti,tmp110 + - const: ti,tmp102 + - enum: + - ti,tmp102 interrupts: maxItems: 1 @@ -28,7 +32,7 @@ properties: const: 1 vcc-supply: - description: Power supply for tmp102 + description: Power supply for the sensor required: - compatible From c191107b0f77e5003e3210c77bed5b6df4287789 Mon Sep 17 00:00:00 2001 From: rahlquist Date: Thu, 13 Aug 2026 12:47:46 -0400 Subject: [PATCH 13/97] hwmon: (yogafan) Add Lenovo Yoga Pro 9 16IMH9 The Lenovo Yoga Pro 9 16IMH9 (83DN) exposes its fan tachometers at ACPI paths different from the generic Yoga configuration. Add a model-specific two-fan configuration for the PC00.LPCB.EC0 namespace and document the corrected mapping. Tested on a Lenovo Yoga Pro 9 16IMH9 (83DN) with BIOS NKCN35WW: the patched module registers fan1_input and fan2_input, both reporting 1800 RPM at idle. Signed-off-by: Richard Ahlquist Link: https://patch.msgid.link/20260813164746.105154-1-rahlquist@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/yogafan.rst | 3 ++- drivers/hwmon/yogafan.c | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Documentation/hwmon/yogafan.rst b/Documentation/hwmon/yogafan.rst index 6395c94f4a6b..9ff5db5dc08c 100644 --- a/Documentation/hwmon/yogafan.rst +++ b/Documentation/hwmon/yogafan.rst @@ -89,7 +89,8 @@ immediately to ensure the user knows the fan has stopped. ---------------------------------------------------------------------------------------------------- 82N7 | Yoga 14cACN | 0x06 | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 80V2 / 81C3 | Yoga 710/720 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 - 83E2 / 83DN | Yoga Pro 7/9 | 0xFE | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 + 83E2 | Yoga Pro 7 | 0xFE | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 + 83DN | Yoga Pro 9 16IMH9 | 0x06/0xFE | \_SB.PC00.LPCB.EC0.FANS/FA2S | 8-bit | 100 82A2 / 82A3 | Yoga Slim 7 | 0x06 | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 81YM / 82FG | IdeaPad 5 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 82JW / 82JU | Legion 5 (AMD) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS (Fan1) | 16-bit | 1 diff --git a/drivers/hwmon/yogafan.c b/drivers/hwmon/yogafan.c index 278cb089b0fd..413ddc721a11 100644 --- a/drivers/hwmon/yogafan.c +++ b/drivers/hwmon/yogafan.c @@ -90,6 +90,17 @@ static const struct yogafan_config yoga_pro_7_14iah10_cfg = { .paths = { "\\_SB.PC00.LPCB.EC0.FANS", NULL } }; +/* + * Lenovo Yoga Pro 9 16IMH9 (83DN) uses the PC00 namespace and has two + * 8-bit fan tachometer fields. + */ +static const struct yogafan_config yoga_pro_83dn_cfg = { + .multiplier = 100, + .fan_count = 2, + .paths = { "\\_SB.PC00.LPCB.EC0.FANS", + "\\_SB.PC00.LPCB.EC0.FA2S" } +}; + static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm) { ktime_t now = ktime_get_boottime(); @@ -237,6 +248,14 @@ static const struct dmi_system_id yogafan_quirks[] = { }, .driver_data = (void *)&xiaoxin_8bit_dual_cfg, }, + { + .ident = "Lenovo Yoga Pro 9 16IMH9 (83DN)", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_NAME, "83DN"), + }, + .driver_data = (void *)&yoga_pro_83dn_cfg, + }, { .ident = "Lenovo Yoga", .matches = { From 9d5443e2cdaaff3df7c4c383b8656bc7fc9f4144 Mon Sep 17 00:00:00 2001 From: Nikita Dubrovskih Date: Sun, 16 Aug 2026 02:40:41 +0300 Subject: [PATCH 14/97] hwmon: Add fan monitoring support for HONOR FMI-XX The HONOR FMI-XX firmware exposes a serialized \\GFNS ACPI method. It returns a status byte and a 16-bit fan speed in RPM for either of two firmware channels. Add a DMI-restricted, read-only hwmon driver using that firmware interface. The driver deliberately exposes no fan control or direct Embedded Controller access. The interface was validated on firmware 1.09 with fan channel 0 reporting approximately 2500-2800 RPM. Channel 1 is readable and remained at 0 RPM during idle and a short CPU load. Signed-off-by: Nikita Dubrovskih Link: https://patch.msgid.link/20260815234041.2262291-1-testname142@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/honor-fmi.rst | 32 ++++++ Documentation/hwmon/index.rst | 1 + MAINTAINERS | 7 ++ drivers/hwmon/Kconfig | 10 ++ drivers/hwmon/Makefile | 1 + drivers/hwmon/honor-fmi.c | 178 ++++++++++++++++++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 Documentation/hwmon/honor-fmi.rst create mode 100644 drivers/hwmon/honor-fmi.c diff --git a/Documentation/hwmon/honor-fmi.rst b/Documentation/hwmon/honor-fmi.rst new file mode 100644 index 000000000000..a42a1dd532e8 --- /dev/null +++ b/Documentation/hwmon/honor-fmi.rst @@ -0,0 +1,32 @@ +.. SPDX-License-Identifier: GPL-2.0-only + +Kernel driver honor-fmi +======================= + +Supported systems: + + * HONOR FMI-XX + +Author: Nikita Dubrovskih + +Description +----------- + +The driver provides read-only monitoring of the fan speed on the HONOR FMI-XX. +The system firmware implements a ``GFNS`` ACPI method which returns the speed +of one of two firmware fan channels in RPM. Embedded Controller access and +serialization are handled by the firmware method. + +The driver does not expose fan control or direct Embedded Controller access. + +Sysfs entries +------------- + +The following attributes are supported: + +======================= ======= ============================================= +Name Perm Description +======================= ======= ============================================= +``fan1_input`` RO Fan channel 0 speed in RPM +``fan2_input`` RO Fan channel 1 speed in RPM +======================= ======= ============================================= diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst index 9955a525436a..06a7992ce5da 100644 --- a/Documentation/hwmon/index.rst +++ b/Documentation/hwmon/index.rst @@ -90,6 +90,7 @@ Hardware Monitoring Kernel Drivers gxp-fan-ctrl hac300s hih6130 + honor-fmi hp-wmi-sensors hs3001 htu31 diff --git a/MAINTAINERS b/MAINTAINERS index cc3cae2e378b..fdd08ebb204a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11960,6 +11960,13 @@ S: Maintained F: Documentation/devicetree/bindings/iio/pressure/honeywell,mprls0025pa.yaml F: drivers/iio/pressure/mprls0025pa* +HONOR FMI-XX HARDWARE MONITOR DRIVER +M: Nikita Dubrovskih +L: linux-hwmon@vger.kernel.org +S: Maintained +F: Documentation/hwmon/honor-fmi.rst +F: drivers/hwmon/honor-fmi.c + HP BIOSCFG DRIVER M: Jorge Lopez L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index fecff8610ea8..b19aa16986af 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -2827,6 +2827,16 @@ config SENSORS_ASUS_EC This driver can also be built as a module. If so, the module will be called asus_ec_sensors. +config SENSORS_HONOR_FMI + tristate "HONOR FMI-XX fan monitor" + depends on X86 && ACPI + help + If you say yes here, you get support for fan speed monitoring on + the HONOR FMI-XX laptop through its firmware ACPI method. + + This driver can also be built as a module. If so, the module + will be called honor-fmi. + config SENSORS_HP_WMI tristate "HP WMI Sensors" depends on ACPI_WMI diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 1229b6b3996d..646813b7aaa8 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -11,6 +11,7 @@ obj-$(CONFIG_SENSORS_ACPI_POWER) += acpi_power_meter.o obj-$(CONFIG_SENSORS_ATK0110) += asus_atk0110.o obj-$(CONFIG_SENSORS_ASUS_EC) += asus-ec-sensors.o obj-$(CONFIG_SENSORS_ASUS_WMI) += asus_wmi_sensors.o +obj-$(CONFIG_SENSORS_HONOR_FMI) += honor-fmi.o obj-$(CONFIG_SENSORS_HP_WMI) += hp-wmi-sensors.o # Native drivers diff --git a/drivers/hwmon/honor-fmi.c b/drivers/hwmon/honor-fmi.c new file mode 100644 index 000000000000..3421e40a132f --- /dev/null +++ b/drivers/hwmon/honor-fmi.c @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Read-only fan monitoring for the HONOR FMI-XX. + * + * The firmware-provided \GFNS ACPI method accepts a three-byte buffer. + * Byte 2 selects fan 0 or 1. It returns a status byte followed by a + * little-endian 16-bit fan speed in RPM. The method owns all Embedded + * Controller access and serialization; this driver deliberately exposes no + * fan control interface. + */ + +#include +#include +#include +#include +#include +#include + +#define HONOR_FMI_GFNS_RESULT_SIZE 3 + +struct honor_fmi_data { + acpi_handle gfns; +}; + +static const struct dmi_system_id honor_fmi_dmi_table[] = { + { + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "HONOR"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "FMI-XX"), + }, + }, + {} +}; +MODULE_DEVICE_TABLE(dmi, honor_fmi_dmi_table); + +static int honor_fmi_read_rpm(struct honor_fmi_data *data, int channel, + long *rpm) +{ + union acpi_object input = { + .buffer = { + .type = ACPI_TYPE_BUFFER, + .length = 3, + }, + }; + struct acpi_object_list arguments = { + .count = 1, + .pointer = &input, + }; + struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *result; + u8 input_bytes[3] = { 0, 0, channel }; + acpi_status status; + int ret = 0; + + input.buffer.pointer = input_bytes; + + status = acpi_evaluate_object(data->gfns, NULL, &arguments, &output); + if (ACPI_FAILURE(status)) + return -EIO; + + result = output.pointer; + if (!result || result->type != ACPI_TYPE_BUFFER || + result->buffer.length < HONOR_FMI_GFNS_RESULT_SIZE) { + ret = -EPROTO; + goto out_free; + } + + if (result->buffer.pointer[0]) { + ret = -EIO; + goto out_free; + } + + *rpm = result->buffer.pointer[1] | + (result->buffer.pointer[2] << 8); + +out_free: + kfree(output.pointer); + return ret; +} + +static umode_t honor_fmi_is_visible(const void *data, + enum hwmon_sensor_types type, u32 attr, + int channel) +{ + return 0444; +} + +static int honor_fmi_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *value) +{ + struct honor_fmi_data *data = dev_get_drvdata(dev); + + return honor_fmi_read_rpm(data, channel, value); +} + +static const struct hwmon_ops honor_fmi_hwmon_ops = { + .is_visible = honor_fmi_is_visible, + .read = honor_fmi_read, +}; + +static const struct hwmon_channel_info * const honor_fmi_hwmon_info[] = { + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT, HWMON_F_INPUT), + NULL +}; + +static const struct hwmon_chip_info honor_fmi_chip_info = { + .ops = &honor_fmi_hwmon_ops, + .info = honor_fmi_hwmon_info, +}; + +static int honor_fmi_probe(struct platform_device *pdev) +{ + struct honor_fmi_data *data; + struct device *hwmon_dev; + acpi_status status; + + if (!dmi_check_system(honor_fmi_dmi_table)) + return -ENODEV; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + status = acpi_get_handle(NULL, "\\GFNS", &data->gfns); + if (ACPI_FAILURE(status)) + return dev_err_probe(&pdev->dev, -ENODEV, + "firmware does not provide \\GFNS\n"); + + hwmon_dev = devm_hwmon_device_register_with_info(&pdev->dev, "honor_fmi", + data, + &honor_fmi_chip_info, + NULL); + return PTR_ERR_OR_ZERO(hwmon_dev); +} + +static struct platform_driver honor_fmi_driver = { + .probe = honor_fmi_probe, + .driver = { + .name = "honor-fmi-hwmon", + }, +}; + +static struct platform_device *honor_fmi_device; + +static int __init honor_fmi_init(void) +{ + int ret; + + if (!dmi_check_system(honor_fmi_dmi_table)) + return -ENODEV; + + ret = platform_driver_register(&honor_fmi_driver); + if (ret) + return ret; + + honor_fmi_device = platform_device_register_simple("honor-fmi-hwmon", + PLATFORM_DEVID_NONE, + NULL, 0); + if (IS_ERR(honor_fmi_device)) { + platform_driver_unregister(&honor_fmi_driver); + return PTR_ERR(honor_fmi_device); + } + + return 0; +} + +static void __exit honor_fmi_exit(void) +{ + platform_device_unregister(honor_fmi_device); + platform_driver_unregister(&honor_fmi_driver); +} + +module_init(honor_fmi_init); +module_exit(honor_fmi_exit); + +MODULE_AUTHOR("Nikita Dubrovskih "); +MODULE_DESCRIPTION("HONOR FMI-XX fan speed monitor"); +MODULE_LICENSE("GPL"); From 83e3d1372e45e50954dec67be495a8494b61b6e5 Mon Sep 17 00:00:00 2001 From: Stoyan Bogdanov Date: Mon, 17 Aug 2026 13:14:43 +0300 Subject: [PATCH 15/97] hwmon: (pmbus/tps25990) Rework driver for multi-device support Rework existing implementation to allow adding support for new devices to the existing driver. chip_id is used to identify the current device and differentiate logic where needed. Changes include: - Add an enum listing supported chips - Add a structure to hold per-device m, b, R coefficients Signed-off-by: Stoyan Bogdanov Link: https://patch.msgid.link/20260817101455.3526260-2-sbogdanov@baylibre.com Signed-off-by: Guenter Roeck --- drivers/hwmon/pmbus/tps25990.c | 125 +++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/drivers/hwmon/pmbus/tps25990.c b/drivers/hwmon/pmbus/tps25990.c index 9d318e6509ab..7634ac743025 100644 --- a/drivers/hwmon/pmbus/tps25990.c +++ b/drivers/hwmon/pmbus/tps25990.c @@ -47,6 +47,15 @@ PK_MIN_AVG_RST_AVG | \ PK_MIN_AVG_RST_MIN) +enum chips { + tps25990, +}; + +struct tps25990_data { + struct pmbus_driver_info info; + enum chips chip_id; +}; + /* * Arbitrary default Rimon value: 1kOhm * This correspond to an overcurrent limit of 55A, close to the specified limit @@ -337,63 +346,65 @@ static const struct regulator_desc tps25990_reg_desc[] = { }; #endif -static const struct pmbus_driver_info tps25990_base_info = { - .pages = 1, - .format[PSC_VOLTAGE_IN] = direct, - .m[PSC_VOLTAGE_IN] = 5251, - .b[PSC_VOLTAGE_IN] = 0, - .R[PSC_VOLTAGE_IN] = -2, - .format[PSC_VOLTAGE_OUT] = direct, - .m[PSC_VOLTAGE_OUT] = 5251, - .b[PSC_VOLTAGE_OUT] = 0, - .R[PSC_VOLTAGE_OUT] = -2, - .format[PSC_TEMPERATURE] = direct, - .m[PSC_TEMPERATURE] = 140, - .b[PSC_TEMPERATURE] = 32100, - .R[PSC_TEMPERATURE] = -2, - /* - * Current and Power measurement depends on the ohm value - * of Rimon. m is multiplied by 1000 below to have an integer - * and -3 is added to R to compensate. - */ - .format[PSC_CURRENT_IN] = direct, - .m[PSC_CURRENT_IN] = 9538, - .b[PSC_CURRENT_IN] = 0, - .R[PSC_CURRENT_IN] = -6, - .format[PSC_POWER] = direct, - .m[PSC_POWER] = 4901, - .b[PSC_POWER] = 0, - .R[PSC_POWER] = -7, - .func[0] = (PMBUS_HAVE_VIN | - PMBUS_HAVE_VOUT | - PMBUS_HAVE_VMON | - PMBUS_HAVE_IIN | - PMBUS_HAVE_PIN | - PMBUS_HAVE_TEMP | - PMBUS_HAVE_STATUS_VOUT | - PMBUS_HAVE_STATUS_IOUT | - PMBUS_HAVE_STATUS_INPUT | - PMBUS_HAVE_STATUS_TEMP | - PMBUS_HAVE_SAMPLES), - .read_word_data = tps25990_read_word_data, - .write_word_data = tps25990_write_word_data, - .read_byte_data = tps25990_read_byte_data, - .write_byte_data = tps25990_write_byte_data, +static const struct pmbus_driver_info tps25990_base_info[] = { + [tps25990] = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 5251, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -2, + .format[PSC_VOLTAGE_OUT] = direct, + .m[PSC_VOLTAGE_OUT] = 5251, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = -2, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 140, + .b[PSC_TEMPERATURE] = 32100, + .R[PSC_TEMPERATURE] = -2, + /* + * Current and Power measurement depends on the ohm value + * of Rimon. m is multiplied by 1000 below to have an integer + * and -3 is added to R to compensate. + */ + .format[PSC_CURRENT_IN] = direct, + .m[PSC_CURRENT_IN] = 9538, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = -6, + .format[PSC_POWER] = direct, + .m[PSC_POWER] = 4901, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = -7, + .func[0] = (PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | + PMBUS_HAVE_VMON | + PMBUS_HAVE_IIN | + PMBUS_HAVE_PIN | + PMBUS_HAVE_TEMP | + PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_SAMPLES), + .read_word_data = tps25990_read_word_data, + .write_word_data = tps25990_write_word_data, + .read_byte_data = tps25990_read_byte_data, + .write_byte_data = tps25990_write_byte_data, #if IS_ENABLED(CONFIG_SENSORS_TPS25990_REGULATOR) - .reg_desc = tps25990_reg_desc, - .num_regulators = ARRAY_SIZE(tps25990_reg_desc), + .reg_desc = tps25990_reg_desc, + .num_regulators = ARRAY_SIZE(tps25990_reg_desc), #endif + }, }; static const struct i2c_device_id tps25990_i2c_id[] = { - { .name = "tps25990" }, - { } + { .name = "tps25990", .driver_data = tps25990 }, + {} }; MODULE_DEVICE_TABLE(i2c, tps25990_i2c_id); static const struct of_device_id tps25990_of_match[] = { - { .compatible = "ti,tps25990" }, + { .compatible = "ti,tps25990", .data = (void *)tps25990 }, {} }; MODULE_DEVICE_TABLE(of, tps25990_of_match); @@ -401,8 +412,9 @@ MODULE_DEVICE_TABLE(of, tps25990_of_match); static int tps25990_probe(struct i2c_client *client) { struct device *dev = &client->dev; - struct pmbus_driver_info *info; + struct tps25990_data *data; const char *propname; + enum chips chip_id; u32 rimon; int ret; @@ -415,15 +427,20 @@ static int tps25990_probe(struct i2c_client *client) rimon = TPS25990_DEFAULT_RIMON; } - info = devm_kmemdup(dev, &tps25990_base_info, sizeof(*info), GFP_KERNEL); - if (!info) + chip_id = (enum chips)(unsigned long)i2c_get_match_data(client); + + data = devm_kzalloc(dev, sizeof(struct tps25990_data), GFP_KERNEL); + if (!data) return -ENOMEM; - /* Adapt the current and power scale for each instance */ - tps25990_set_m(&info->m[PSC_CURRENT_IN], rimon); - tps25990_set_m(&info->m[PSC_POWER], rimon); + data->info = tps25990_base_info[chip_id]; + data->chip_id = chip_id; - return pmbus_do_probe(client, info); + /* Adapt the current and power scale for each instance */ + tps25990_set_m(&data->info.m[PSC_CURRENT_IN], rimon); + tps25990_set_m(&data->info.m[PSC_POWER], rimon); + + return pmbus_do_probe(client, &data->info); } static struct i2c_driver tps25990_driver = { From 5d41343dc313ef2cebd9f862fed1dc828eaf558c Mon Sep 17 00:00:00 2001 From: Stoyan Bogdanov Date: Mon, 17 Aug 2026 13:14:44 +0300 Subject: [PATCH 16/97] dt-bindings: hwmon: pmbus/tps25990: Add TPS1689 Add device compatible support for TPS1689 Signed-off-by: Stoyan Bogdanov Acked-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260817101455.3526260-3-sbogdanov@baylibre.com Signed-off-by: Guenter Roeck --- .../devicetree/bindings/hwmon/pmbus/ti,tps25990.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/hwmon/pmbus/ti,tps25990.yaml b/Documentation/devicetree/bindings/hwmon/pmbus/ti,tps25990.yaml index f4115870e450..63ccb67576df 100644 --- a/Documentation/devicetree/bindings/hwmon/pmbus/ti,tps25990.yaml +++ b/Documentation/devicetree/bindings/hwmon/pmbus/ti,tps25990.yaml @@ -5,18 +5,20 @@ $id: http://devicetree.org/schemas/hwmon/pmbus/ti,tps25990.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Texas Instruments TPS25990 Stackable eFuse +title: Texas Instruments Stackable eFuses maintainers: - Jerome Brunet description: - The TI TPS25990 is an integrated, high-current circuit + The TI TPS25990 and TPS1689 are integrated, high-current circuit protection and power management device with PMBUS interface properties: compatible: - const: ti,tps25990 + enum: + - ti,tps1689 + - ti,tps25990 reg: maxItems: 1 From d49c6acb348e963a04522bda4acf8e0cca1a782f Mon Sep 17 00:00:00 2001 From: Stoyan Bogdanov Date: Mon, 17 Aug 2026 13:14:45 +0300 Subject: [PATCH 17/97] hwmon: (pmbus/tps25990) Add TPS1689 support Extend the existing TPS25990 driver to support the TPS1689 eFuse, as both devices share the same command interface and functionality. Update the documentation to include TPS1689 support. Signed-off-by: Stoyan Bogdanov Link: https://patch.msgid.link/20260817101455.3526260-4-sbogdanov@baylibre.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/tps25990.rst | 15 ++-- drivers/hwmon/pmbus/tps25990.c | 126 ++++++++++++++++++++++++++++--- 2 files changed, 124 insertions(+), 17 deletions(-) diff --git a/Documentation/hwmon/tps25990.rst b/Documentation/hwmon/tps25990.rst index 04faec780d26..e8bc9a550bda 100644 --- a/Documentation/hwmon/tps25990.rst +++ b/Documentation/hwmon/tps25990.rst @@ -9,26 +9,31 @@ Supported chips: Prefix: 'tps25990' - * Datasheet + Datasheet: Publicly available at Texas Instruments website: https://www.ti.com/lit/gpn/tps25990 - Publicly available at Texas Instruments website: https://www.ti.com/lit/gpn/tps25990 + * TI TPS1689 + + Prefix: 'tps1689' + + Datasheet: Publicly available at Texas Instruments website: https://www.ti.com/lit/gpn/tps1689 Author: Jerome Brunet + Stoyan Bogdanov Description ----------- -This driver implements support for TI TPS25990 eFuse. +This driver implements support for TI TPS25990 and TI TPS1689 eFuse chips. This is an integrated, high-current circuit protection and power management device with PMBUS interface -Device compliant with: +Devices are compliant with: - PMBus rev 1.3 interface. -Device supports direct format for reading input voltages, +Devices supports direct format for reading input voltages, output voltage, input current, input power and temperature. Due to the specificities of the chip, all history reset attributes diff --git a/drivers/hwmon/pmbus/tps25990.c b/drivers/hwmon/pmbus/tps25990.c index 7634ac743025..0d5c99053f23 100644 --- a/drivers/hwmon/pmbus/tps25990.c +++ b/drivers/hwmon/pmbus/tps25990.c @@ -47,7 +47,14 @@ PK_MIN_AVG_RST_AVG | \ PK_MIN_AVG_RST_MIN) +#define TPS1689_VIN_OV_RANGE_SEL_MASK GENMASK(7, 6) +#define TPS1689_VIN_VOV_MASK GENMASK(5, 0) +#define TPS1689_VIN_SCALING 251 +#define TPS1689_VIN_VOV_STEP_MV 250 +#define TPS1689_VIN_RANGE_SPAN_MV 16000 + enum chips { + tps1689, tps25990, }; @@ -105,6 +112,8 @@ static int tps25990_mfr_write_protect_get(struct i2c_client *client) static int tps25990_read_word_data(struct i2c_client *client, int page, int phase, int reg) { + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct tps25990_data *data = container_of(info, struct tps25990_data, info); int ret; switch (reg) { @@ -193,9 +202,18 @@ static int tps25990_read_word_data(struct i2c_client *client, ret = pmbus_read_word_data(client, page, phase, reg); if (ret < 0) break; - ret = DIV_ROUND_CLOSEST(ret * TPS25990_VIN_OVF_NUM, - TPS25990_VIN_OVF_DIV); - ret += TPS25990_VIN_OVF_OFF; + if (data->chip_id == tps25990) { + ret = DIV_ROUND_CLOSEST(ret * TPS25990_VIN_OVF_NUM, + TPS25990_VIN_OVF_DIV); + ret += TPS25990_VIN_OVF_OFF; + } else if (data->chip_id == tps1689) { + int rng = (FIELD_GET(TPS1689_VIN_OV_RANGE_SEL_MASK, ret) + 1) * + TPS1689_VIN_RANGE_SPAN_MV; + int vov = FIELD_GET(TPS1689_VIN_VOV_MASK, ret) * TPS1689_VIN_VOV_STEP_MV; + + ret = DIV_ROUND_CLOSEST(rng + vov - TPS1689_VIN_RANGE_SPAN_MV, + TPS1689_VIN_SCALING); + } break; case PMBUS_IIN_OC_FAULT_LIMIT: @@ -238,6 +256,8 @@ static int tps25990_read_word_data(struct i2c_client *client, static int tps25990_write_word_data(struct i2c_client *client, int page, int reg, u16 value) { + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct tps25990_data *data = container_of(info, struct tps25990_data, info); int ret; switch (reg) { @@ -249,26 +269,52 @@ static int tps25990_write_word_data(struct i2c_client *client, case PMBUS_OT_WARN_LIMIT: case PMBUS_OT_FAULT_LIMIT: case PMBUS_PIN_OP_WARN_LIMIT: - value >>= TPS25990_8B_SHIFT; + value = clamp_val((s16)value, 0, S16_MAX) >> TPS25990_8B_SHIFT; value = clamp_val(value, 0, 0xff); ret = pmbus_write_word_data(client, page, reg, value); break; case PMBUS_VIN_OV_FAULT_LIMIT: - value -= TPS25990_VIN_OVF_OFF; - value = DIV_ROUND_CLOSEST(((unsigned int)value) * TPS25990_VIN_OVF_DIV, - TPS25990_VIN_OVF_NUM); - value = clamp_val(value, 0, 0xf); + if ((s16)value < 0) + return -EINVAL; + + if (data->chip_id == tps25990) { + int tmp = (int)value - TPS25990_VIN_OVF_OFF; + + tmp = clamp_val(tmp, 0, INT_MAX); + value = DIV_ROUND_CLOSEST((unsigned int)tmp * TPS25990_VIN_OVF_DIV, + TPS25990_VIN_OVF_NUM); + value = clamp_val(value, 0, 0xf); + } else if (data->chip_id == tps1689) { + u32 scaled_value = value * TPS1689_VIN_SCALING + TPS1689_VIN_RANGE_SPAN_MV; + u32 rng_idx = scaled_value / TPS1689_VIN_RANGE_SPAN_MV; + u32 ov_set; + + rng_idx = clamp_val(rng_idx, 1, + FIELD_MAX(TPS1689_VIN_OV_RANGE_SEL_MASK) + 1); + ov_set = scaled_value - (TPS1689_VIN_RANGE_SPAN_MV * rng_idx); + ov_set = min_t(u32, ov_set / TPS1689_VIN_VOV_STEP_MV, + FIELD_MAX(TPS1689_VIN_VOV_MASK)); + value = FIELD_PREP(TPS1689_VIN_OV_RANGE_SEL_MASK, rng_idx - 1) | + FIELD_PREP(TPS1689_VIN_VOV_MASK, ov_set); + } ret = pmbus_write_word_data(client, page, reg, value); break; - case PMBUS_IIN_OC_FAULT_LIMIT: - value -= TPS25990_IIN_OCF_OFF; - value = DIV_ROUND_CLOSEST(((unsigned int)value) * TPS25990_IIN_OCF_DIV, + case PMBUS_IIN_OC_FAULT_LIMIT: { + int tmp; + + if ((s16)value < 0) + return -EINVAL; + + tmp = (int)value - TPS25990_IIN_OCF_OFF; + tmp = clamp_val(tmp, 0, INT_MAX); + value = DIV_ROUND_CLOSEST((unsigned int)tmp * TPS25990_IIN_OCF_DIV, TPS25990_IIN_OCF_NUM); value = clamp_val(value, 0, 0x3f); ret = pmbus_write_byte_data(client, page, TPS25990_VIREF, value); break; + } case PMBUS_VIRT_SAMPLES: value = clamp_val(value, 1, 1 << PK_MIN_AVG_AVG_CNT); @@ -347,6 +393,60 @@ static const struct regulator_desc tps25990_reg_desc[] = { #endif static const struct pmbus_driver_info tps25990_base_info[] = { + [tps1689] = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 3984, + .b[PSC_VOLTAGE_IN] = -63750, + .R[PSC_VOLTAGE_IN] = -3, + .format[PSC_VOLTAGE_OUT] = direct, + .m[PSC_VOLTAGE_OUT] = 1166, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = -2, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 140, + .b[PSC_TEMPERATURE] = 32103, + .R[PSC_TEMPERATURE] = -2, + /* + * Current and Power measurement depends on the ohm value + * of Rimon. m is multiplied by 1000 below to have an integer + * and -3 is added to R to compensate. + */ + .format[PSC_CURRENT_IN] = direct, + .m[PSC_CURRENT_IN] = 9548, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = -6, + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_CURRENT_OUT] = 24347, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = -3, + .format[PSC_POWER] = direct, + .m[PSC_POWER] = 2775, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = -4, + .func[0] = (PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | + PMBUS_HAVE_VMON | + PMBUS_HAVE_IIN | + PMBUS_HAVE_IOUT | + PMBUS_HAVE_PIN | + PMBUS_HAVE_TEMP | + PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_SAMPLES), + + .read_word_data = tps25990_read_word_data, + .write_word_data = tps25990_write_word_data, + .read_byte_data = tps25990_read_byte_data, + .write_byte_data = tps25990_write_byte_data, + +#if IS_ENABLED(CONFIG_SENSORS_TPS25990_REGULATOR) + .reg_desc = tps25990_reg_desc, + .num_regulators = ARRAY_SIZE(tps25990_reg_desc), +#endif + }, [tps25990] = { .pages = 1, .format[PSC_VOLTAGE_IN] = direct, @@ -389,7 +489,6 @@ static const struct pmbus_driver_info tps25990_base_info[] = { .write_word_data = tps25990_write_word_data, .read_byte_data = tps25990_read_byte_data, .write_byte_data = tps25990_write_byte_data, - #if IS_ENABLED(CONFIG_SENSORS_TPS25990_REGULATOR) .reg_desc = tps25990_reg_desc, .num_regulators = ARRAY_SIZE(tps25990_reg_desc), @@ -398,12 +497,14 @@ static const struct pmbus_driver_info tps25990_base_info[] = { }; static const struct i2c_device_id tps25990_i2c_id[] = { + { .name = "tps1689", .driver_data = tps1689 }, { .name = "tps25990", .driver_data = tps25990 }, {} }; MODULE_DEVICE_TABLE(i2c, tps25990_i2c_id); static const struct of_device_id tps25990_of_match[] = { + { .compatible = "ti,tps1689", .data = (void *)tps1689 }, { .compatible = "ti,tps25990", .data = (void *)tps25990 }, {} }; @@ -438,6 +539,7 @@ static int tps25990_probe(struct i2c_client *client) /* Adapt the current and power scale for each instance */ tps25990_set_m(&data->info.m[PSC_CURRENT_IN], rimon); + tps25990_set_m(&data->info.m[PSC_CURRENT_OUT], rimon); tps25990_set_m(&data->info.m[PSC_POWER], rimon); return pmbus_do_probe(client, &data->info); From 961fa6fa659cb796a191e14bb49b605466382c31 Mon Sep 17 00:00:00 2001 From: Alessandro Zini Date: Fri, 21 Aug 2026 11:19:23 +0200 Subject: [PATCH 18/97] dt-bindings: trivial-devices: Add Sensirion STS4x series Add "sensirion,sts40" as compatible for the Sensirion STS4x/STS4xA series of digital temperature sensors. Link: https://sensirion.com/resource/datasheet/sts4x Acked-by: Conor Dooley Signed-off-by: Alessandro Zini Link: https://patch.msgid.link/20260821091924.18975-2-alessandro.zini@siemens.com Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/trivial-devices.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/trivial-devices.yaml b/Documentation/devicetree/bindings/trivial-devices.yaml index 2de8eb09cb7d..a82460e2416f 100644 --- a/Documentation/devicetree/bindings/trivial-devices.yaml +++ b/Documentation/devicetree/bindings/trivial-devices.yaml @@ -427,6 +427,8 @@ properties: - sensirion,sht21 - sensirion,sht25 - sensirion,sht4x + # Sensirion temperature sensor with I2C interface + - sensirion,sts40 # Sensortek 3 axis accelerometer - sensortek,stk8312 # Sensortek 3 axis accelerometer From e9e597193e2477f354719e092b1ac9c3e6c61cd0 Mon Sep 17 00:00:00 2001 From: Alessandro Zini Date: Fri, 21 Aug 2026 11:19:24 +0200 Subject: [PATCH 19/97] hwmon: (sht4x) Add support for Sensirion STS4x temperature sensors The Sensirion STS4x series is the temperature-only variant of the SHT4x family. It shares the same I2C command set, conversion formulas, CRC checksum, and timing with the SHT4x, but only returns temperature data (3 bytes: 2 data bytes + 1 CRC byte). Add support for the STS4x series by dynamically adjusting the read response length, suppressing humidity channel attributes when probed as STS4x, and omitting heater sysfs attributes. Link: https://sensirion.com/resource/datasheet/sts4x Signed-off-by: Alessandro Zini Link: https://patch.msgid.link/20260821091924.18975-3-alessandro.zini@siemens.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/sht4x.rst | 17 +++++++-- drivers/hwmon/sht4x.c | 66 +++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/Documentation/hwmon/sht4x.rst b/Documentation/hwmon/sht4x.rst index ba094ad0e281..b9564632a1be 100644 --- a/Documentation/hwmon/sht4x.rst +++ b/Documentation/hwmon/sht4x.rst @@ -15,6 +15,16 @@ Supported Chips: English: https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/2_Humidity_Sensors/Datasheets/Sensirion_Humidity_Sensors_SHT4x_Datasheet.pdf + * Sensirion STS4X + + Prefix: 'sts4x' + + Addresses scanned: None + + Datasheet: + + English: https://sensirion.com/resource/datasheet/sts4x + Author: Navin Sankar Velliangiri @@ -22,9 +32,10 @@ Description ----------- This driver implements support for the Sensirion SHT4x chip, a humidity -and temperature sensor. Temperature is measured in degree celsius, relative -humidity is expressed as a percentage. In sysfs interface, all values are -scaled by 1000, i.e. the value for 31.5 degrees celsius is 31500. +and temperature sensor, and the Sensirion STS4x chip, a temperature sensor. +Temperature is measured in degree celsius, relative humidity is expressed as a +percentage (on SHT4x only). In sysfs interface, all values are scaled by 1000, +i.e. the value for 31.5 degrees celsius is 31500. Usage Notes ----------- diff --git a/drivers/hwmon/sht4x.c b/drivers/hwmon/sht4x.c index 0bb9227674a8..8372b4fc902e 100644 --- a/drivers/hwmon/sht4x.c +++ b/drivers/hwmon/sht4x.c @@ -43,6 +43,7 @@ #define SHT4X_CRC8_LEN 1 #define SHT4X_WORD_LEN 2 #define SHT4X_RESPONSE_LENGTH 6 +#define STS4X_RESPONSE_LENGTH 3 #define SHT4X_CRC8_POLYNOMIAL 0x31 #define SHT4X_CRC8_INIT 0xff #define SHT4X_MIN_TEMPERATURE -45000 @@ -52,9 +53,15 @@ DECLARE_CRC8_TABLE(sht4x_crc8_table); +enum sht4x_chips { + sht4x, + sts4x, +}; + /** * struct sht4x_data - All the data required to operate an SHT4X chip * @client: the i2c client associated with the SHT4X + * @chip_id: the chip type (sht4x or sts4x) * @heating_complete: the time that the last heating finished * @data_pending: true if and only if there are measurements to retrieve after heating * @heater_power: the power at which the heater will be started @@ -67,6 +74,7 @@ DECLARE_CRC8_TABLE(sht4x_crc8_table); */ struct sht4x_data { struct i2c_client *client; + enum sht4x_chips chip_id; u64 heating_complete; /* in jiffies */ bool data_pending; u32 heater_power; /* in milli-watts */ @@ -92,11 +100,15 @@ static int sht4x_read_values(struct sht4x_data *data) u8 crc; u8 cmd[SHT4X_CMD_LEN] = {SHT4X_CMD_MEASURE_HPM}; u8 raw_data[SHT4X_RESPONSE_LENGTH]; - u64 curr_jiffies; + size_t response_length = data->chip_id == sts4x ? + STS4X_RESPONSE_LENGTH : SHT4X_RESPONSE_LENGTH; - curr_jiffies = get_jiffies_64(); - if (time_before64(curr_jiffies, data->heating_complete)) - msleep(jiffies_to_msecs(data->heating_complete - curr_jiffies)); + if (data->chip_id != sts4x) { + u64 curr_jiffies = get_jiffies_64(); + + if (time_before64(curr_jiffies, data->heating_complete)) + msleep(jiffies_to_msecs(data->heating_complete - curr_jiffies)); + } if (data->data_pending && time_before64(get_jiffies_64(), data->heating_complete + data->update_interval)) { @@ -115,15 +127,14 @@ static int sht4x_read_values(struct sht4x_data *data) usleep_range(SHT4X_MEAS_DELAY_HPM, SHT4X_MEAS_DELAY_HPM + SHT4X_DELAY_EXTRA); } - ret = i2c_master_recv(client, raw_data, SHT4X_RESPONSE_LENGTH); - if (ret != SHT4X_RESPONSE_LENGTH) { + ret = i2c_master_recv(client, raw_data, response_length); + if (ret != response_length) { if (ret >= 0) ret = -ENODATA; return ret; } t_ticks = raw_data[0] << 8 | raw_data[1]; - rh_ticks = raw_data[3] << 8 | raw_data[4]; crc = crc8(sht4x_crc8_table, &raw_data[0], SHT4X_WORD_LEN, CRC8_INIT_VALUE); if (crc != raw_data[2]) { @@ -131,14 +142,19 @@ static int sht4x_read_values(struct sht4x_data *data) return -EIO; } - crc = crc8(sht4x_crc8_table, &raw_data[3], SHT4X_WORD_LEN, CRC8_INIT_VALUE); - if (crc != raw_data[5]) { - dev_err(&client->dev, "data integrity check failed\n"); - return -EIO; + data->temperature = ((21875 * (int32_t)t_ticks) >> 13) - 45000; + + if (data->chip_id != sts4x) { + rh_ticks = raw_data[3] << 8 | raw_data[4]; + crc = crc8(sht4x_crc8_table, &raw_data[3], SHT4X_WORD_LEN, CRC8_INIT_VALUE); + if (crc != raw_data[5]) { + dev_err(&client->dev, "data integrity check failed\n"); + return -EIO; + } + + data->humidity = ((15625 * (int32_t)rh_ticks) >> 13) - 6000; } - data->temperature = ((21875 * (int32_t)t_ticks) >> 13) - 45000; - data->humidity = ((15625 * (int32_t)rh_ticks) >> 13) - 6000; data->last_updated = jiffies; data->valid = true; return 0; @@ -190,9 +206,14 @@ static umode_t sht4x_hwmon_visible(const void *data, enum hwmon_sensor_types type, u32 attr, int channel) { + const struct sht4x_data *chip_data = data; + switch (type) { case hwmon_temp: + return 0444; case hwmon_humidity: + if (chip_data->chip_id == sts4x) + return 0; return 0444; case hwmon_chip: return 0644; @@ -392,6 +413,7 @@ static const struct hwmon_chip_info sht4x_chip_info = { static int sht4x_probe(struct i2c_client *client) { + const struct attribute_group **groups = NULL; struct device *device = &client->dev; struct device *hwmon_dev; struct sht4x_data *data; @@ -410,11 +432,15 @@ static int sht4x_probe(struct i2c_client *client) if (!data) return -ENOMEM; + data->chip_id = (uintptr_t)i2c_get_match_data(client); data->update_interval = SHT4X_MIN_POLL_INTERVAL; data->client = client; - data->heater_power = 200; - data->heater_time = 1000; - data->heating_complete = get_jiffies_64(); + if (data->chip_id != sts4x) { + data->heater_power = 200; + data->heater_time = 1000; + data->heating_complete = get_jiffies_64(); + groups = sht4x_groups; + } crc8_populate_msb(sht4x_crc8_table, SHT4X_CRC8_POLYNOMIAL); @@ -428,19 +454,21 @@ static int sht4x_probe(struct i2c_client *client) client->name, data, &sht4x_chip_info, - sht4x_groups); + groups); return PTR_ERR_OR_ZERO(hwmon_dev); } static const struct i2c_device_id sht4x_id[] = { - { .name = "sht4x" }, + { .name = "sht4x", .driver_data = sht4x }, + { .name = "sts4x", .driver_data = sts4x }, { } }; MODULE_DEVICE_TABLE(i2c, sht4x_id); static const struct of_device_id sht4x_of_match[] = { - { .compatible = "sensirion,sht4x" }, + { .compatible = "sensirion,sht4x", .data = (void *)sht4x }, + { .compatible = "sensirion,sts40", .data = (void *)sts4x }, { } }; MODULE_DEVICE_TABLE(of, sht4x_of_match); From 28e319062056586c255bac750eca2b595c19c31f Mon Sep 17 00:00:00 2001 From: "benoit.masson" Date: Sun, 30 Aug 2026 00:01:47 +0200 Subject: [PATCH 20/97] hwmon: (it87) describe per-chip PWM temperature maps Add a per-chip count for PWM temperature mapping sources and use it when reporting and validating mappings. Keep existing chips on their previous three-source defaults. This prepares the driver for chips with a different number of mapping sources. Signed-off-by: benoit.masson Signed-off-by: Jerome Tollet Link: https://patch.msgid.link/619425df92463b3c3f9e90f00c527c4009599ea8.1788040385.git.jerome.tollet@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/it87.c | 46 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/drivers/hwmon/it87.c b/drivers/hwmon/it87.c index 87edb1b6048b..5cb1c0029041 100644 --- a/drivers/hwmon/it87.c +++ b/drivers/hwmon/it87.c @@ -292,6 +292,7 @@ struct it87_devices { const char *name; const char * const model; u32 features; + u8 num_temp_map; u8 peci_mask; u8 old_peci_mask; u8 smbus_bitmap; /* SMBus enable bits in extra config register */ @@ -335,12 +336,14 @@ static const struct it87_devices it87_devices[] = { .model = "IT87F", .features = FEAT_OLD_AUTOPWM | FEAT_FANCTL_ONOFF, /* may need to overwrite */ + .num_temp_map = 3, }, [it8712] = { .name = "it8712", .model = "IT8712F", .features = FEAT_OLD_AUTOPWM | FEAT_VID | FEAT_FANCTL_ONOFF, /* may need to overwrite */ + .num_temp_map = 3, }, [it8716] = { .name = "it8716", @@ -348,6 +351,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_VID | FEAT_FAN16_CONFIG | FEAT_FIVE_FANS | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, }, [it8718] = { .name = "it8718", @@ -355,6 +359,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_VID | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_FIVE_FANS | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .old_peci_mask = 0x4, }, [it8720] = { @@ -363,6 +368,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_VID | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_FIVE_FANS | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .old_peci_mask = 0x4, }, [it8721] = { @@ -372,6 +378,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_TEMP_PECI | FEAT_FAN16_CONFIG | FEAT_FIVE_FANS | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .peci_mask = 0x05, .old_peci_mask = 0x02, /* Actually reports PCH */ }, @@ -382,6 +389,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_FIVE_FANS | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .peci_mask = 0x07, }, [it8732] = { @@ -391,6 +399,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_TEMP_PECI | FEAT_10_9MV_ADC | FEAT_IN7_INTERNAL | FEAT_FOUR_FANS | FEAT_FOUR_PWM | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .peci_mask = 0x07, .old_peci_mask = 0x02, /* Actually reports PCH */ }, @@ -404,6 +413,7 @@ static const struct it87_devices it87_devices[] = { /* 12mV ADC (OHM) */ /* 16 bit fans (OHM) */ /* three fans, always 16 bit (guesswork) */ + .num_temp_map = 3, .peci_mask = 0x07, }, [it8772] = { @@ -416,6 +426,7 @@ static const struct it87_devices it87_devices[] = { /* 12mV ADC (HWSensors4, OHM) */ /* 16 bit fans (HWSensors4, OHM) */ /* three fans, always 16 bit (datasheet) */ + .num_temp_map = 3, .peci_mask = 0x07, }, [it8781] = { @@ -424,6 +435,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .old_peci_mask = 0x4, }, [it8782] = { @@ -432,6 +444,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .old_peci_mask = 0x4, }, [it8783] = { @@ -440,6 +453,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .old_peci_mask = 0x4, }, [it8786] = { @@ -448,6 +462,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_NEWER_AUTOPWM | FEAT_12MV_ADC | FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .peci_mask = 0x07, }, [it8790] = { @@ -456,6 +471,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_NEWER_AUTOPWM | FEAT_12MV_ADC | FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF | FEAT_NOCONF, + .num_temp_map = 3, .peci_mask = 0x07, }, [it8792] = { @@ -465,6 +481,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_TEMP_PECI | FEAT_10_9MV_ADC | FEAT_IN7_INTERNAL | FEAT_FANCTL_ONOFF | FEAT_NOCONF, + .num_temp_map = 3, .peci_mask = 0x07, .old_peci_mask = 0x02, /* Actually reports PCH */ }, @@ -474,6 +491,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_NEWER_AUTOPWM | FEAT_12MV_ADC | FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_IN7_INTERNAL | FEAT_AVCC3 | FEAT_PWM_FREQ2, + .num_temp_map = 3, .peci_mask = 0x07, }, [it8620] = { @@ -483,6 +501,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_SIX_FANS | FEAT_IN7_INTERNAL | FEAT_SIX_PWM | FEAT_PWM_FREQ2 | FEAT_SIX_TEMP | FEAT_VIN3_5V | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .peci_mask = 0x07, }, [it8622] = { @@ -492,6 +511,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_FIVE_FANS | FEAT_FIVE_PWM | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_AVCC3 | FEAT_VIN3_5V | FEAT_FOUR_TEMP, + .num_temp_map = 3, .peci_mask = 0x07, .smbus_bitmap = BIT(1) | BIT(2), }, @@ -502,6 +522,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_SIX_FANS | FEAT_IN7_INTERNAL | FEAT_SIX_PWM | FEAT_PWM_FREQ2 | FEAT_SIX_TEMP | FEAT_VIN3_5V | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .peci_mask = 0x07, }, [it8689] = { @@ -511,6 +532,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_SIX_FANS | FEAT_IN7_INTERNAL | FEAT_SIX_PWM | FEAT_PWM_FREQ2 | FEAT_SIX_TEMP | FEAT_AVCC3 | FEAT_FANCTL_ONOFF, + .num_temp_map = 3, .smbus_bitmap = BIT(1) | BIT(2), }, [it87952] = { @@ -520,6 +542,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_TEMP_PECI | FEAT_10_9MV_ADC | FEAT_IN7_INTERNAL | FEAT_FANCTL_ONOFF | FEAT_NOCONF, + .num_temp_map = 3, .peci_mask = 0x07, .old_peci_mask = 0x02, /* Actually reports PCH */ }, @@ -589,6 +612,7 @@ struct it87_data { int sioaddr; enum chips type; u32 features; + u8 num_temp_map; u8 peci_mask; u8 old_peci_mask; @@ -1693,16 +1717,18 @@ static ssize_t show_pwm_temp_map(struct device *dev, struct sensor_device_attribute *sensor_attr = to_sensor_dev_attr(attr); struct it87_data *data = it87_update_device(dev); int nr = sensor_attr->index; + u8 num_map; int map; if (IS_ERR(data)) return PTR_ERR(data); + num_map = data->num_temp_map; map = data->pwm_temp_map[nr]; - if (map >= 3) + if (map >= num_map) map = 0; /* Should never happen */ - if (nr >= 3) /* pwm channels 3..6 map to temp4..6 */ - map += 3; + if (nr >= num_map) /* pwm channels 3..6 map to temp4..6 */ + map += num_map; return sprintf(buf, "%d\n", (int)BIT(map)); } @@ -1714,6 +1740,7 @@ static ssize_t set_pwm_temp_map(struct device *dev, struct sensor_device_attribute *sensor_attr = to_sensor_dev_attr(attr); struct it87_data *data = dev_get_drvdata(dev); int nr = sensor_attr->index; + u8 num_map = data->num_temp_map; long val; int err; u8 reg; @@ -1721,8 +1748,8 @@ static ssize_t set_pwm_temp_map(struct device *dev, if (kstrtol(buf, 10, &val) < 0) return -EINVAL; - if (nr >= 3) - val -= 3; + if (nr >= num_map) + val -= num_map; switch (val) { case BIT(0): @@ -3461,6 +3488,7 @@ static int it87_probe(struct platform_device *pdev) struct resource *res; struct device *dev = &pdev->dev; struct it87_sio_data *sio_data = dev_get_platdata(dev); + const struct it87_devices *chip; int enable_pwm_interface; struct device *hwmon_dev; int err; @@ -3483,9 +3511,11 @@ static int it87_probe(struct platform_device *pdev) data->type = sio_data->type; data->smbus_bitmap = sio_data->smbus_bitmap; data->ec_special_config = sio_data->ec_special_config; - data->features = it87_devices[sio_data->type].features; - data->peci_mask = it87_devices[sio_data->type].peci_mask; - data->old_peci_mask = it87_devices[sio_data->type].old_peci_mask; + chip = &it87_devices[sio_data->type]; + data->features = chip->features; + data->peci_mask = chip->peci_mask; + data->old_peci_mask = chip->old_peci_mask; + data->num_temp_map = chip->num_temp_map; /* * IT8705F Datasheet 0.4.1, 3h == Version G. * IT8712F Datasheet 0.9.1, section 8.3.5 indicates 8h == Version J. From ab003112b4948774057bde1c488b0bdbd96d687c Mon Sep 17 00:00:00 2001 From: "benoit.masson" Date: Sun, 30 Aug 2026 00:01:48 +0200 Subject: [PATCH 21/97] hwmon: (it87) prepare for extended PWM temp maps Introduce helper logic for PWM-to-temperature mappings so newer register layouts can be supported while retaining the legacy two groups of three temperature sources. Honor the four global temperature sources on IT8603E and IT8622E instead of applying the legacy grouping to those chips. Use per-chip masks and shifts for newer extended mappings. Newer controllers keep the duty cycle in a separate register, so write their temperature mapping in both manual and automatic mode. This keeps the selected mapping across cache refreshes and mode changes. On older controllers, defer mapping writes while in manual mode and apply the cached mapping when switching to automatic mode. Signed-off-by: benoit.masson Signed-off-by: Jerome Tollet Link: https://patch.msgid.link/7b4f2befc3d214b646c0582e6410f17e78748b4e.1788040385.git.jerome.tollet@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/it87.c | 219 ++++++++++++++++++++++++++++++++----------- 1 file changed, 166 insertions(+), 53 deletions(-) diff --git a/drivers/hwmon/it87.c b/drivers/hwmon/it87.c index 5cb1c0029041..232cc8586258 100644 --- a/drivers/hwmon/it87.c +++ b/drivers/hwmon/it87.c @@ -252,6 +252,7 @@ static const u8 IT87_REG_TEMP_OFFSET[] = { 0x56, 0x57, 0x59 }; #define IT87_REG_FAN_MAIN_CTRL 0x13 #define IT87_REG_FAN_CTL 0x14 static const u8 IT87_REG_PWM[] = { 0x15, 0x16, 0x17, 0x7f, 0xa7, 0xaf }; +static const u8 IT87_REG_PWM_8665[] = { 0x15, 0x16, 0x17, 0x1e, 0x1f, 0x92 }; static const u8 IT87_REG_PWM_DUTY[] = { 0x63, 0x6b, 0x73, 0x7b, 0xa3, 0xab }; static const u8 IT87_REG_VIN[] = { 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, @@ -283,6 +284,7 @@ static const u8 IT87_REG_AUTO_BASE[] = { 0x60, 0x68, 0x70, 0x78, 0xa0, 0xa8 }; #define NUM_TEMP 6 #define NUM_TEMP_OFFSET ARRAY_SIZE(IT87_REG_TEMP_OFFSET) #define NUM_TEMP_LIMIT 3 +#define IT87_PWM_OLD_NUM_TEMP 3 #define NUM_FAN ARRAY_SIZE(IT87_REG_FAN) #define NUM_FAN_DIV 3 #define NUM_PWM ARRAY_SIZE(IT87_REG_PWM) @@ -292,6 +294,7 @@ struct it87_devices { const char *name; const char * const model; u32 features; + const u8 *reg_pwm; u8 num_temp_map; u8 peci_mask; u8 old_peci_mask; @@ -329,6 +332,7 @@ struct it87_devices { #define FEAT_FOUR_PWM BIT(21) /* Supports four fan controls */ #define FEAT_FOUR_TEMP BIT(22) #define FEAT_FANCTL_ONOFF BIT(23) /* chip has FAN_CTL ON/OFF */ +#define FEAT_NEW_TEMPMAP BIT(24) /* PWM uses extended temp map */ static const struct it87_devices it87_devices[] = { [it87] = { @@ -336,6 +340,7 @@ static const struct it87_devices it87_devices[] = { .model = "IT87F", .features = FEAT_OLD_AUTOPWM | FEAT_FANCTL_ONOFF, /* may need to overwrite */ + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, }, [it8712] = { @@ -343,6 +348,7 @@ static const struct it87_devices it87_devices[] = { .model = "IT8712F", .features = FEAT_OLD_AUTOPWM | FEAT_VID | FEAT_FANCTL_ONOFF, /* may need to overwrite */ + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, }, [it8716] = { @@ -351,6 +357,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_VID | FEAT_FAN16_CONFIG | FEAT_FIVE_FANS | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, }, [it8718] = { @@ -359,6 +366,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_VID | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_FIVE_FANS | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .old_peci_mask = 0x4, }, @@ -368,6 +376,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_VID | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_FIVE_FANS | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .old_peci_mask = 0x4, }, @@ -378,6 +387,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_TEMP_PECI | FEAT_FAN16_CONFIG | FEAT_FIVE_FANS | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x05, .old_peci_mask = 0x02, /* Actually reports PCH */ @@ -389,6 +399,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_FIVE_FANS | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, }, @@ -399,6 +410,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_TEMP_PECI | FEAT_10_9MV_ADC | FEAT_IN7_INTERNAL | FEAT_FOUR_FANS | FEAT_FOUR_PWM | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, .old_peci_mask = 0x02, /* Actually reports PCH */ @@ -413,6 +425,7 @@ static const struct it87_devices it87_devices[] = { /* 12mV ADC (OHM) */ /* 16 bit fans (OHM) */ /* three fans, always 16 bit (guesswork) */ + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, }, @@ -426,6 +439,7 @@ static const struct it87_devices it87_devices[] = { /* 12mV ADC (HWSensors4, OHM) */ /* 16 bit fans (HWSensors4, OHM) */ /* three fans, always 16 bit (datasheet) */ + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, }, @@ -435,6 +449,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .old_peci_mask = 0x4, }, @@ -444,6 +459,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .old_peci_mask = 0x4, }, @@ -453,6 +469,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_FAN16_CONFIG | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .old_peci_mask = 0x4, }, @@ -462,6 +479,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_NEWER_AUTOPWM | FEAT_12MV_ADC | FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, }, @@ -471,6 +489,7 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_NEWER_AUTOPWM | FEAT_12MV_ADC | FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_FANCTL_ONOFF | FEAT_NOCONF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, }, @@ -481,6 +500,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_TEMP_PECI | FEAT_10_9MV_ADC | FEAT_IN7_INTERNAL | FEAT_FANCTL_ONOFF | FEAT_NOCONF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, .old_peci_mask = 0x02, /* Actually reports PCH */ @@ -491,7 +511,8 @@ static const struct it87_devices it87_devices[] = { .features = FEAT_NEWER_AUTOPWM | FEAT_12MV_ADC | FEAT_16BIT_FANS | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_IN7_INTERNAL | FEAT_AVCC3 | FEAT_PWM_FREQ2, - .num_temp_map = 3, + .reg_pwm = IT87_REG_PWM, + .num_temp_map = 4, .peci_mask = 0x07, }, [it8620] = { @@ -501,6 +522,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_SIX_FANS | FEAT_IN7_INTERNAL | FEAT_SIX_PWM | FEAT_PWM_FREQ2 | FEAT_SIX_TEMP | FEAT_VIN3_5V | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, }, @@ -511,7 +533,8 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_FIVE_FANS | FEAT_FIVE_PWM | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 | FEAT_AVCC3 | FEAT_VIN3_5V | FEAT_FOUR_TEMP, - .num_temp_map = 3, + .reg_pwm = IT87_REG_PWM_8665, + .num_temp_map = 4, .peci_mask = 0x07, .smbus_bitmap = BIT(1) | BIT(2), }, @@ -522,6 +545,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_SIX_FANS | FEAT_IN7_INTERNAL | FEAT_SIX_PWM | FEAT_PWM_FREQ2 | FEAT_SIX_TEMP | FEAT_VIN3_5V | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, }, @@ -532,6 +556,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_SIX_FANS | FEAT_IN7_INTERNAL | FEAT_SIX_PWM | FEAT_PWM_FREQ2 | FEAT_SIX_TEMP | FEAT_AVCC3 | FEAT_FANCTL_ONOFF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .smbus_bitmap = BIT(1) | BIT(2), }, @@ -542,6 +567,7 @@ static const struct it87_devices it87_devices[] = { | FEAT_TEMP_OFFSET | FEAT_TEMP_OLD_PECI | FEAT_TEMP_PECI | FEAT_10_9MV_ADC | FEAT_IN7_INTERNAL | FEAT_FANCTL_ONOFF | FEAT_NOCONF, + .reg_pwm = IT87_REG_PWM, .num_temp_map = 3, .peci_mask = 0x07, .old_peci_mask = 0x02, /* Actually reports PCH */ @@ -583,6 +609,7 @@ static const struct it87_devices it87_devices[] = { #define has_scaling(data) ((data)->features & (FEAT_12MV_ADC | \ FEAT_10_9MV_ADC)) #define has_fanctl_onoff(data) ((data)->features & FEAT_FANCTL_ONOFF) +#define has_new_tempmap(data) ((data)->features & FEAT_NEW_TEMPMAP) struct it87_sio_data { int sioaddr; @@ -612,6 +639,7 @@ struct it87_data { int sioaddr; enum chips type; u32 features; + const u8 *reg_pwm; u8 num_temp_map; u8 peci_mask; u8 old_peci_mask; @@ -659,7 +687,9 @@ struct it87_data { u8 has_pwm; /* Bitfield, pwm control enabled */ u8 pwm_ctrl[NUM_PWM]; /* Register value */ u8 pwm_duty[NUM_PWM]; /* Manual PWM value set by user */ - u8 pwm_temp_map[NUM_PWM];/* PWM to temp. chan. mapping (bits 1-0) */ + u8 pwm_temp_map[NUM_PWM];/* PWM to temp. chan. mapping */ + u8 pwm_temp_map_mask; + u8 pwm_temp_map_shift; /* Automatic fan speed control registers */ u8 auto_pwm[NUM_AUTO_PWM][4]; /* [nr][3] is hard-coded */ @@ -741,6 +771,77 @@ static int pwm_from_reg(const struct it87_data *data, u8 reg) return (reg & 0x7f) << 1; } +static inline u8 pwm_temp_map_get(const struct it87_data *data, u8 ctrl) +{ + return (ctrl >> data->pwm_temp_map_shift) & + data->pwm_temp_map_mask; +} + +static inline u8 pwm_temp_map_set(const struct it87_data *data, u8 ctrl, + u8 map) +{ + ctrl &= ~(data->pwm_temp_map_mask << data->pwm_temp_map_shift); + return ctrl | ((map & data->pwm_temp_map_mask) + << data->pwm_temp_map_shift); +} + +static inline u8 pwm_num_temp_map(const struct it87_data *data) +{ + return data->num_temp_map; +} + +static inline bool uses_global_temp_map(const struct it87_data *data) +{ + return has_new_tempmap(data) || + pwm_num_temp_map(data) != IT87_PWM_OLD_NUM_TEMP; +} + +static unsigned int pwm_temp_channel(const struct it87_data *data, + int nr, u8 map) +{ + if (uses_global_temp_map(data)) { + u8 num = pwm_num_temp_map(data); + + if (map >= num) + map = 0; + return map; + } + + if (map >= IT87_PWM_OLD_NUM_TEMP) + map = 0; + + if (nr >= IT87_PWM_OLD_NUM_TEMP) + map += IT87_PWM_OLD_NUM_TEMP; + + return map; +} + +static int pwm_temp_map_from_channel(const struct it87_data *data, int nr, + unsigned int channel, u8 *map) +{ + if (uses_global_temp_map(data)) { + u8 num = pwm_num_temp_map(data); + + if (channel >= num) + return -EINVAL; + *map = channel; + return 0; + } + + if (nr >= IT87_PWM_OLD_NUM_TEMP) { + if (channel < IT87_PWM_OLD_NUM_TEMP || + channel >= 2 * IT87_PWM_OLD_NUM_TEMP) + return -EINVAL; + channel -= IT87_PWM_OLD_NUM_TEMP; + } else { + if (channel >= IT87_PWM_OLD_NUM_TEMP) + return -EINVAL; + } + + *map = channel; + return 0; +} + static int DIV_TO_REG(int val) { int answer = 0; @@ -752,6 +853,11 @@ static int DIV_TO_REG(int val) #define DIV_FROM_REG(val) BIT(val) +static inline u16 it87_reg_pwm(const struct it87_data *data, int nr) +{ + return data->reg_pwm[nr]; +} + /* * PWM base frequencies. The frequency has to be divided by either 128 or 256, * depending on the chip type, to calculate the actual PWM frequency. @@ -832,16 +938,23 @@ static void it87_write_value(struct it87_data *data, u8 reg, u8 value) static void it87_update_pwm_ctrl(struct it87_data *data, int nr) { - data->pwm_ctrl[nr] = it87_read_value(data, IT87_REG_PWM[nr]); + data->pwm_ctrl[nr] = it87_read_value(data, it87_reg_pwm(data, nr)); if (has_newer_autopwm(data)) { - data->pwm_temp_map[nr] = data->pwm_ctrl[nr] & 0x03; + data->pwm_temp_map[nr] = + pwm_temp_map_get(data, data->pwm_ctrl[nr]); + if (uses_global_temp_map(data) && + data->pwm_temp_map[nr] >= pwm_num_temp_map(data)) + data->pwm_temp_map[nr] = 0; data->pwm_duty[nr] = it87_read_value(data, IT87_REG_PWM_DUTY[nr]); - } else { - if (data->pwm_ctrl[nr] & 0x80) /* Automatic mode */ - data->pwm_temp_map[nr] = data->pwm_ctrl[nr] & 0x03; - else /* Manual mode */ - data->pwm_duty[nr] = data->pwm_ctrl[nr] & 0x7f; + } else if (data->pwm_ctrl[nr] & 0x80) { /* Automatic mode */ + data->pwm_temp_map[nr] = + pwm_temp_map_get(data, data->pwm_ctrl[nr]); + if (uses_global_temp_map(data) && + data->pwm_temp_map[nr] >= pwm_num_temp_map(data)) + data->pwm_temp_map[nr] = 0; + } else { /* Manual mode */ + data->pwm_duty[nr] = data->pwm_ctrl[nr] & 0x7f; } if (has_old_autopwm(data)) { @@ -1591,27 +1704,32 @@ static ssize_t set_pwm_enable(struct device *dev, struct device_attribute *attr, data->pwm_duty[nr]); /* and set manual mode */ if (has_newer_autopwm(data)) { - ctrl = (data->pwm_ctrl[nr] & 0x7c) | - data->pwm_temp_map[nr]; + ctrl = pwm_temp_map_set(data, + data->pwm_ctrl[nr] & + ~0x80, + data->pwm_temp_map[nr]); } else { ctrl = data->pwm_duty[nr]; } data->pwm_ctrl[nr] = ctrl; - it87_write_value(data, IT87_REG_PWM[nr], ctrl); + it87_write_value(data, it87_reg_pwm(data, nr), ctrl); } } else { u8 ctrl; if (has_newer_autopwm(data)) { - ctrl = (data->pwm_ctrl[nr] & 0x7c) | - data->pwm_temp_map[nr]; + ctrl = pwm_temp_map_set(data, + data->pwm_ctrl[nr] & ~0x80, + data->pwm_temp_map[nr]); if (val != 1) ctrl |= 0x80; } else { - ctrl = (val == 1 ? data->pwm_duty[nr] : 0x80); + ctrl = val == 1 ? data->pwm_duty[nr] : + pwm_temp_map_set(data, 0x80, + data->pwm_temp_map[nr]); } data->pwm_ctrl[nr] = ctrl; - it87_write_value(data, IT87_REG_PWM[nr], ctrl); + it87_write_value(data, it87_reg_pwm(data, nr), ctrl); if (has_fanctl_onoff(data) && nr < 3) { /* set SmartGuardian mode */ @@ -1662,7 +1780,7 @@ static ssize_t set_pwm(struct device *dev, struct device_attribute *attr, */ if (!(data->pwm_ctrl[nr] & 0x80)) { data->pwm_ctrl[nr] = data->pwm_duty[nr]; - it87_write_value(data, IT87_REG_PWM[nr], + it87_write_value(data, it87_reg_pwm(data, nr), data->pwm_ctrl[nr]); } } @@ -1717,20 +1835,14 @@ static ssize_t show_pwm_temp_map(struct device *dev, struct sensor_device_attribute *sensor_attr = to_sensor_dev_attr(attr); struct it87_data *data = it87_update_device(dev); int nr = sensor_attr->index; - u8 num_map; - int map; + unsigned int channel; if (IS_ERR(data)) return PTR_ERR(data); - num_map = data->num_temp_map; - map = data->pwm_temp_map[nr]; - if (map >= num_map) - map = 0; /* Should never happen */ - if (nr >= num_map) /* pwm channels 3..6 map to temp4..6 */ - map += num_map; + channel = pwm_temp_channel(data, nr, data->pwm_temp_map[nr]); - return sprintf(buf, "%d\n", (int)BIT(map)); + return sprintf(buf, "%d\n", (int)BIT(channel)); } static ssize_t set_pwm_temp_map(struct device *dev, @@ -1740,45 +1852,35 @@ static ssize_t set_pwm_temp_map(struct device *dev, struct sensor_device_attribute *sensor_attr = to_sensor_dev_attr(attr); struct it87_data *data = dev_get_drvdata(dev); int nr = sensor_attr->index; - u8 num_map = data->num_temp_map; long val; int err; - u8 reg; + unsigned int channel; + u8 map; - if (kstrtol(buf, 10, &val) < 0) + if (kstrtol(buf, 10, &val) < 0 || val <= 0 || !is_power_of_2(val)) return -EINVAL; - if (nr >= num_map) - val -= num_map; - - switch (val) { - case BIT(0): - reg = 0x00; - break; - case BIT(1): - reg = 0x01; - break; - case BIT(2): - reg = 0x02; - break; - default: + channel = __ffs(val); + if (pwm_temp_map_from_channel(data, nr, channel, &map)) return -EINVAL; - } err = it87_lock(data); if (err) return err; it87_update_pwm_ctrl(data, nr); - data->pwm_temp_map[nr] = reg; + data->pwm_temp_map[nr] = map; /* - * If we are in automatic mode, write the temp mapping immediately; - * otherwise, just store it for later use. + * Newer controllers keep the duty cycle in a separate register, so + * their temperature mapping can be updated in any mode. On older + * controllers, defer the update until automatic mode is enabled. */ - if (data->pwm_ctrl[nr] & 0x80) { - data->pwm_ctrl[nr] = (data->pwm_ctrl[nr] & 0xfc) | - data->pwm_temp_map[nr]; - it87_write_value(data, IT87_REG_PWM[nr], data->pwm_ctrl[nr]); + if (has_newer_autopwm(data) || (data->pwm_ctrl[nr] & 0x80)) { + data->pwm_ctrl[nr] = pwm_temp_map_set(data, + data->pwm_ctrl[nr], + data->pwm_temp_map[nr]); + it87_write_value(data, it87_reg_pwm(data, nr), + data->pwm_ctrl[nr]); } it87_unlock(data); return count; @@ -3377,7 +3479,10 @@ static void it87_init_device(struct platform_device *pdev) * manual duty cycle. */ for (i = 0; i < NUM_AUTO_PWM; i++) { - data->pwm_temp_map[i] = i; + if (uses_global_temp_map(data)) + data->pwm_temp_map[i] = 0; + else + data->pwm_temp_map[i] = i % IT87_PWM_OLD_NUM_TEMP; data->pwm_duty[i] = 0x7f; /* Full speed */ data->auto_pwm[i][3] = 0x7f; /* Full speed, hard-coded */ } @@ -3513,9 +3618,17 @@ static int it87_probe(struct platform_device *pdev) data->ec_special_config = sio_data->ec_special_config; chip = &it87_devices[sio_data->type]; data->features = chip->features; + data->reg_pwm = chip->reg_pwm; data->peci_mask = chip->peci_mask; data->old_peci_mask = chip->old_peci_mask; data->num_temp_map = chip->num_temp_map; + if (has_new_tempmap(data)) { + data->pwm_temp_map_mask = 0x07; + data->pwm_temp_map_shift = 3; + } else { + data->pwm_temp_map_mask = 0x03; + data->pwm_temp_map_shift = 0; + } /* * IT8705F Datasheet 0.4.1, 3h == Version G. * IT8712F Datasheet 0.9.1, section 8.3.5 indicates 8h == Version J. From 600a85f556b783b5c619c4b6a576d5b2471be1ca Mon Sep 17 00:00:00 2001 From: "benoit.masson" Date: Sun, 30 Aug 2026 00:01:49 +0200 Subject: [PATCH 22/97] hwmon: (it87) add IT8613E support Teach the Super I/O probe path to recognize IT8613E and add its hardware monitoring configuration. Add feature flags, 11 mV ADC scaling, the IT8665-style PWM register map, six PWM temperature mapping sources, and GPIO pin-mux checks. Only three temperature inputs are currently known, so retain the existing three temperature limit and offset resources. Document the chip in the hwmon guide. Signed-off-by: benoit.masson Signed-off-by: Jerome Tollet Link: https://patch.msgid.link/5afd336442307450f77467b2a749d405970a2099.1788040385.git.jerome.tollet@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/it87.rst | 8 +++++ drivers/hwmon/it87.c | 63 ++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/Documentation/hwmon/it87.rst b/Documentation/hwmon/it87.rst index fc1c90b023ae..c33ba8a0749a 100644 --- a/Documentation/hwmon/it87.rst +++ b/Documentation/hwmon/it87.rst @@ -11,6 +11,14 @@ Supported chips: Datasheet: Not publicly available + * IT8613E + + Prefix: 'it8613' + + Addresses scanned: from Super I/O config space (8 I/O ports) + + Datasheet: Not publicly available + * IT8620E Prefix: 'it8620' diff --git a/drivers/hwmon/it87.c b/drivers/hwmon/it87.c index 232cc8586258..2757d428a23d 100644 --- a/drivers/hwmon/it87.c +++ b/drivers/hwmon/it87.c @@ -36,6 +36,7 @@ * IT8790E Super I/O chip w/LPC interface * IT8792E Super I/O chip w/LPC interface * IT87952E Super I/O chip w/LPC interface + * IT8613E Super I/O chip w/LPC interface * Sis950 A clone of the IT8705F * * Copyright (C) 2001 Chris Gauthron @@ -65,7 +66,7 @@ enum chips { it87, it8712, it8716, it8718, it8720, it8721, it8728, it8732, it8771, it8772, it8781, it8782, it8783, it8786, it8790, - it8792, it8603, it8620, it8622, it8628, it8689, it87952 }; + it8792, it8603, it8613, it8620, it8622, it8628, it8689, it87952 }; static struct platform_device *it87_pdev[2]; @@ -159,6 +160,7 @@ static inline void superio_exit(int ioreg, bool noexit) #define IT8786E_DEVID 0x8786 #define IT8790E_DEVID 0x8790 #define IT8603E_DEVID 0x8603 +#define IT8613E_DEVID 0x8613 #define IT8620E_DEVID 0x8620 #define IT8622E_DEVID 0x8622 #define IT8623E_DEVID 0x8623 @@ -333,6 +335,7 @@ struct it87_devices { #define FEAT_FOUR_TEMP BIT(22) #define FEAT_FANCTL_ONOFF BIT(23) /* chip has FAN_CTL ON/OFF */ #define FEAT_NEW_TEMPMAP BIT(24) /* PWM uses extended temp map */ +#define FEAT_11MV_ADC BIT(25) static const struct it87_devices it87_devices[] = { [it87] = { @@ -515,6 +518,18 @@ static const struct it87_devices it87_devices[] = { .num_temp_map = 4, .peci_mask = 0x07, }, + [it8613] = { + .name = "it8613", + .model = "IT8613E", + /* Only three temperature inputs are currently known. */ + .features = FEAT_NEWER_AUTOPWM | FEAT_11MV_ADC | FEAT_16BIT_FANS + | FEAT_TEMP_OFFSET | FEAT_TEMP_PECI | FEAT_FIVE_FANS + | FEAT_FIVE_PWM | FEAT_IN7_INTERNAL | FEAT_PWM_FREQ2 + | FEAT_AVCC3 | FEAT_NEW_TEMPMAP, + .reg_pwm = IT87_REG_PWM_8665, + .num_temp_map = 6, + .peci_mask = 0x07, + }, [it8620] = { .name = "it8620", .model = "IT8620E", @@ -577,6 +592,7 @@ static const struct it87_devices it87_devices[] = { #define has_16bit_fans(data) ((data)->features & FEAT_16BIT_FANS) #define has_12mv_adc(data) ((data)->features & FEAT_12MV_ADC) #define has_10_9mv_adc(data) ((data)->features & FEAT_10_9MV_ADC) +#define has_11mv_adc(data) ((data)->features & FEAT_11MV_ADC) #define has_newer_autopwm(data) ((data)->features & FEAT_NEWER_AUTOPWM) #define has_old_autopwm(data) ((data)->features & FEAT_OLD_AUTOPWM) #define has_temp_offset(data) ((data)->features & FEAT_TEMP_OFFSET) @@ -607,7 +623,8 @@ static const struct it87_devices it87_devices[] = { #define has_vin3_5v(data) ((data)->features & FEAT_VIN3_5V) #define has_noconf(data) ((data)->features & FEAT_NOCONF) #define has_scaling(data) ((data)->features & (FEAT_12MV_ADC | \ - FEAT_10_9MV_ADC)) + FEAT_10_9MV_ADC | \ + FEAT_11MV_ADC)) #define has_fanctl_onoff(data) ((data)->features & FEAT_FANCTL_ONOFF) #define has_new_tempmap(data) ((data)->features & FEAT_NEW_TEMPMAP) @@ -712,6 +729,8 @@ static int adc_lsb(const struct it87_data *data, int nr) lsb = 120; else if (has_10_9mv_adc(data)) lsb = 109; + else if (has_11mv_adc(data)) + lsb = 110; else lsb = 160; if (data->in_scaled & BIT(nr)) @@ -2919,6 +2938,9 @@ static int __init it87_find(int sioaddr, unsigned short *address, case IT8623E_DEVID: sio_data->type = it8603; break; + case IT8613E_DEVID: + sio_data->type = it8613; + break; case IT8620E_DEVID: sio_data->type = it8620; break; @@ -3096,6 +3118,43 @@ static int __init it87_find(int sioaddr, unsigned short *address, sio_data->skip_in |= BIT(5); /* No VIN5 */ sio_data->skip_in |= BIT(6); /* No VIN6 */ + sio_data->beep_pin = superio_inb(sioaddr, + IT87_SIO_BEEP_PIN_REG) & 0x3f; + } else if (sio_data->type == it8613) { + int reg27, reg29, reg2a; + + superio_select(sioaddr, GPIO); + + /* Check for pwm3, fan3, pwm5, fan5 */ + reg27 = superio_inb(sioaddr, IT87_SIO_GPIO3_REG); + if (!(reg27 & BIT(1))) + sio_data->skip_fan |= BIT(4); + if (reg27 & BIT(3)) + sio_data->skip_pwm |= BIT(4); + if (reg27 & BIT(6)) + sio_data->skip_pwm |= BIT(2); + if (reg27 & BIT(7)) + sio_data->skip_fan |= BIT(2); + + /* Check for pwm2, fan2 */ + reg29 = superio_inb(sioaddr, IT87_SIO_GPIO5_REG); + if (reg29 & BIT(1)) + sio_data->skip_pwm |= BIT(1); + if (reg29 & BIT(2)) + sio_data->skip_fan |= BIT(1); + + /* Check for pwm4, fan4 */ + reg2a = superio_inb(sioaddr, IT87_SIO_PINX1_REG); + if (!(reg2a & BIT(0)) || (reg29 & BIT(7))) { + sio_data->skip_fan |= BIT(3); + sio_data->skip_pwm |= BIT(3); + } + + sio_data->skip_pwm |= BIT(0); /* No pwm1 */ + sio_data->skip_fan |= BIT(0); /* No fan1 */ + sio_data->skip_in |= BIT(3); /* No VIN3 */ + sio_data->skip_in |= BIT(6); /* No VIN6 */ + sio_data->beep_pin = superio_inb(sioaddr, IT87_SIO_BEEP_PIN_REG) & 0x3f; } else if (sio_data->type == it8620 || sio_data->type == it8628) { From 61379d4d883cf92a53a7b7294aaa4ff8fb4ce276 Mon Sep 17 00:00:00 2001 From: Flaviu Nistor Date: Wed, 26 Aug 2026 21:47:48 +0300 Subject: [PATCH 23/97] dt-bindings: hwmon: national,lm90: Fix channel constraints for temperature offset Limit temperature-offset-millicelsius to remote channels only, since channel 0 is local and this property does not apply to it. Channel 2 is only valid on devices with two remote sensors, so reject channel 2 for compatibles that do not support a second remote channel. Signed-off-by: Flaviu Nistor Acked-by: Conor Dooley Link: https://patch.msgid.link/20260826184750.4798-2-flaviu.nistor@gmail.com Signed-off-by: Guenter Roeck --- .../bindings/hwmon/national,lm90.yaml | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/hwmon/national,lm90.yaml b/Documentation/devicetree/bindings/hwmon/national,lm90.yaml index 164068ba069d..a8b2a24501b3 100644 --- a/Documentation/devicetree/bindings/hwmon/national,lm90.yaml +++ b/Documentation/devicetree/bindings/hwmon/national,lm90.yaml @@ -113,6 +113,23 @@ allOf: properties: ti,extended-range-enable: false + - if: + not: + properties: + compatible: + contains: + enum: + - adi,adt7481 + - dallas,max6695 + - dallas,max6696 + then: + patternProperties: + "^channel@[0-1]$": + properties: + reg: + enum: [0, 1] + "channel@2": false + - if: properties: compatible: @@ -134,6 +151,11 @@ allOf: "^channel@([0-2])$": properties: temperature-offset-millicelsius: false + else: + patternProperties: + "channel@0": + properties: + temperature-offset-millicelsius: false - if: properties: @@ -149,7 +171,7 @@ allOf: - onnn,nct1008 then: patternProperties: - "^channel@([0-2])$": + "^channel@([1-2])$": properties: temperature-offset-millicelsius: maximum: 127750 @@ -172,7 +194,7 @@ allOf: - winbond,w83l771 then: patternProperties: - "^channel@([0-2])$": + "^channel@([1-2])$": properties: temperature-offset-millicelsius: maximum: 127875 @@ -186,7 +208,7 @@ allOf: - ti,tmp461 then: patternProperties: - "^channel@([0-2])$": + "^channel@([1-2])$": properties: temperature-offset-millicelsius: maximum: 127937 From e3011805cf3368c00eed972426be0bc94767f20c Mon Sep 17 00:00:00 2001 From: Flaviu Nistor Date: Wed, 26 Aug 2026 21:47:49 +0300 Subject: [PATCH 24/97] hwmon: (lm90) Reject channel 2 on chips with only one remote sensor Validate firmware channel definitions against chip capabilities and return -EINVAL when channel 2 is configured on devices with 1 remote channel. Signed-off-by: Flaviu Nistor Link: https://patch.msgid.link/20260826184750.4798-3-flaviu.nistor@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/lm90.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/hwmon/lm90.c b/drivers/hwmon/lm90.c index 1c603272538a..3faeb2c6ab01 100644 --- a/drivers/hwmon/lm90.c +++ b/drivers/hwmon/lm90.c @@ -2712,6 +2712,11 @@ static int lm90_probe_channel(struct i2c_client *client, return -EINVAL; } + if (id == 2 && !(data->flags & LM90_HAVE_TEMP3)) { + dev_err(dev, "channel %d is not supported for this chip in %pfw\n", id, child); + return -EINVAL; + } + err = fwnode_property_read_string(child, "label", &data->channel_label[id]); if (err == -ENODATA || err == -EILSEQ) { dev_err(dev, "invalid label property in %pfw\n", child); From bc92fc9b12b7155d28848527583b9f1ded98c7c1 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Tue, 1 Sep 2026 00:18:24 +0800 Subject: [PATCH 25/97] hwmon: (asus-ec-sensors) add ROG STRIX X670E-A GAMING WIFI The ROG STRIX X670E-A GAMING WIFI is missing from the driver's DMI table. Consequently, the board lookup fails with -ENODEV, asus_ec_sensors does not load, and no asusec hwmon device or EC temperature readings are available. The board uses the same EC sensors, access mutex, and AMD 600-series register layout as the ROG STRIX X670E-E GAMING WIFI. Add its DMI entry using the existing X670E-E board information and document the board as supported. Before the change, there was no asusec device under /sys/class/hwmon and there were no EC readings. After the change, the driver registered four sensors with representative readings of 57-61 C for CPU, 68-71 C for CPU package, 43-44 C for motherboard, and 49-52 C for VRM. The readings correlated with nct6775 and k10temp. Repeated polling with both hwmon drivers loaded produced no EC access, bank-switch, concurrent access, or locking errors. Tested on an ASUS ROG STRIX X670E-A GAMING WIFI with BIOS 2704. Signed-off-by: Asai Neko Reviewed-by: Eugene Shalygin Link: https://patch.msgid.link/20260901-asus-x670e-a-hwmon-fix-v2-1-759406c2a61a@sne.moe Signed-off-by: Guenter Roeck --- Documentation/hwmon/asus_ec_sensors.rst | 1 + drivers/hwmon/asus-ec-sensors.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Documentation/hwmon/asus_ec_sensors.rst b/Documentation/hwmon/asus_ec_sensors.rst index 2c68a343a2ed..ebed9f8d2adb 100644 --- a/Documentation/hwmon/asus_ec_sensors.rst +++ b/Documentation/hwmon/asus_ec_sensors.rst @@ -45,6 +45,7 @@ Supported boards: * ROG STRIX X570-E GAMING WIFI II * ROG STRIX X570-F GAMING * ROG STRIX X570-I GAMING + * ROG STRIX X670E-A GAMING WIFI * ROG STRIX X670E-E GAMING WIFI * ROG STRIX X670E-I GAMING WIFI * ROG STRIX X870-F GAMING WIFI diff --git a/drivers/hwmon/asus-ec-sensors.c b/drivers/hwmon/asus-ec-sensors.c index e43645e884fd..7293c48c72b1 100644 --- a/drivers/hwmon/asus-ec-sensors.c +++ b/drivers/hwmon/asus-ec-sensors.c @@ -962,6 +962,8 @@ static const struct dmi_system_id dmi_table[] = { &board_info_strix_x570_f_gaming), DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX X570-I GAMING", &board_info_strix_x570_i_gaming), + DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX X670E-A GAMING WIFI", + &board_info_strix_x670e_e_gaming_wifi), DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX X670E-E GAMING WIFI", &board_info_strix_x670e_e_gaming_wifi), DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX X670E-I GAMING WIFI", From 26d139cbc52a0b1d97ef5e3fba88b26683dcbe93 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 26 Aug 2026 17:14:19 -0400 Subject: [PATCH 26/97] dt-bindings: hwmon: tmp102: move ti,tmp103 out of trivial-devices.yaml Move ti,tmp103 binding from trivial-devices.yaml to ti,tmp102.yaml. Both devices are single temperature sensors and update "#thermal-sensor-cells" property to accept values 0 and 1 (passing no argument is equivalent to passing 0 as the first argument) Fix below CHECK_DTBS warnings: arch/arm/boot/dts/nxp/imx/imx6dl-plym2m.dtb: temperature-sensor@70 (ti,tmp103): '#thermal-sensor-cells' does not match any of the regexes: '^pinctrl-[0-9]+$' Squashed with: dt-bindings: hwmon: tmp102: Fix up TMP103 bindings The TMP102 and TMP110 are backward compatible, therefore the valid compatible strings are "ti,tmp110", "ti,tmp102" and "ti,tmp102" . The TMP103 is not backward compatible with TMP102, but it does share a schema now, therefore the only valid compatible string for the TMP103 should be "ti,tmp103" . Make exactly those three compatible strings valid and everything else rejected. Signed-off-by: Frank Li Reviewed-by: Krzysztof Kozlowski Signed-off-by: Marek Vasut Acked-by: Conor Dooley Link: https://patch.msgid.link/20260901174847.97261-1-marex@nabladev.com Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml | 8 ++++---- Documentation/devicetree/bindings/trivial-devices.yaml | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml b/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml index f0dbe9f07b93..477f478b9dd0 100644 --- a/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml +++ b/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml @@ -4,7 +4,7 @@ $id: http://devicetree.org/schemas/hwmon/ti,tmp102.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: TMP102 and TMP110 temperature sensor +title: TMP102/TMP103/TMP110 temperature sensor maintainers: - Krzysztof Kozlowski @@ -15,8 +15,8 @@ properties: - items: - const: ti,tmp110 - const: ti,tmp102 - - enum: - - ti,tmp102 + - const: ti,tmp102 + - const: ti,tmp103 interrupts: maxItems: 1 @@ -29,7 +29,7 @@ properties: A descriptive name for this channel, like "ambient" or "psu". "#thermal-sensor-cells": - const: 1 + enum: [0, 1] vcc-supply: description: Power supply for the sensor diff --git a/Documentation/devicetree/bindings/trivial-devices.yaml b/Documentation/devicetree/bindings/trivial-devices.yaml index a82460e2416f..6229a6516302 100644 --- a/Documentation/devicetree/bindings/trivial-devices.yaml +++ b/Documentation/devicetree/bindings/trivial-devices.yaml @@ -506,8 +506,6 @@ properties: - ti,lm74 # Temperature sensor with integrated fan control - ti,lm96000 - # Low Power Digital Temperature Sensor with SMBUS/Two Wire Serial Interface - - ti,tmp103 # Thermometer with SPI interface - ti,tmp121 - ti,tmp122 From 4494669b80233b6b28c53da27b931109ee6cb102 Mon Sep 17 00:00:00 2001 From: Marek Vasut Date: Tue, 1 Sep 2026 19:47:55 +0200 Subject: [PATCH 27/97] dt-bindings: hwmon: tmp102: Document TMP113 The TMP113 temperature sensor part is register compatible with TMP102, document it using a fallback compatible. Unlike TMP102 and TMP110, the TMP113 does have additional unique ID registers, it is up to the driver to handle those. Signed-off-by: Marek Vasut Acked-by: Conor Dooley Link: https://patch.msgid.link/20260901174847.97261-2-marex@nabladev.com Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml b/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml index 477f478b9dd0..c0b747a9ce0d 100644 --- a/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml +++ b/Documentation/devicetree/bindings/hwmon/ti,tmp102.yaml @@ -4,7 +4,7 @@ $id: http://devicetree.org/schemas/hwmon/ti,tmp102.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: TMP102/TMP103/TMP110 temperature sensor +title: TMP102/TMP103/TMP110/TMP113 temperature sensor maintainers: - Krzysztof Kozlowski @@ -13,7 +13,9 @@ properties: compatible: oneOf: - items: - - const: ti,tmp110 + - enum: + - ti,tmp110 + - ti,tmp113 - const: ti,tmp102 - const: ti,tmp102 - const: ti,tmp103 From b8d4ca3c799ca84c52ee360039e81de56621641d Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Tue, 1 Sep 2026 20:18:48 +0200 Subject: [PATCH 28/97] hwmon: (dell-smm) Add Dell OptiPlex 7090 to fan control whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user reported that the Dell OptiPlex 7090 needs to be whitelisted for the special SMM calls necessary for globally enabling/disabling BIOS fan control. Closes: https://github.com/Wer-Wolf/i8kutils/issues/18 Signed-off-by: Armin Wolf Link: https://patch.msgid.link/20260901181849.241776-1-W_Armin@gmx.de Acked-by: Pali Rohár Signed-off-by: Guenter Roeck --- drivers/hwmon/dell-smm-hwmon.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/hwmon/dell-smm-hwmon.c b/drivers/hwmon/dell-smm-hwmon.c index 20920d4bd81e..a3aa398a9f8f 100644 --- a/drivers/hwmon/dell-smm-hwmon.c +++ b/drivers/hwmon/dell-smm-hwmon.c @@ -1631,6 +1631,14 @@ static const struct dmi_system_id i8k_whitelist_fan_control[] __initconst = { }, .driver_data = (void *)&i8k_fan_control_data[I8K_FAN_34A3_35A3], }, + { + .ident = "Dell Optiplex 7090", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "OptiPlex 7090"), + }, + .driver_data = (void *)&i8k_fan_control_data[I8K_FAN_30A3_31A3], + }, { .ident = "Dell XPS 9315", .matches = { From eeb6b0c7551b439fe0b5a188b7732a2c06609705 Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Tue, 1 Sep 2026 20:18:49 +0200 Subject: [PATCH 29/97] hwmon: (dell-smm) Add Latitude 5420 to fan control whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user reported that the Dell Latitude 5420 needs to be whitelisted for the special SMM calls necessary for globally enabling/disabling BIOS fan control. Reported-by: pp12313124124@gmail.com Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221935 Signed-off-by: Armin Wolf Link: https://patch.msgid.link/20260901181849.241776-2-W_Armin@gmx.de Acked-by: Pali Rohár Signed-off-by: Guenter Roeck --- drivers/hwmon/dell-smm-hwmon.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/hwmon/dell-smm-hwmon.c b/drivers/hwmon/dell-smm-hwmon.c index a3aa398a9f8f..7ed94a3db621 100644 --- a/drivers/hwmon/dell-smm-hwmon.c +++ b/drivers/hwmon/dell-smm-hwmon.c @@ -1543,6 +1543,14 @@ static const struct dmi_system_id i8k_whitelist_fan_control[] __initconst = { }, .driver_data = (void *)&i8k_fan_control_data[I8K_FAN_34A3_35A3], }, + { + .ident = "Dell Latitude 5420", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Latitude 5420"), + }, + .driver_data = (void *)&i8k_fan_control_data[I8K_FAN_30A3_31A3], + }, { .ident = "Dell Latitude 5480", .matches = { From d2e24a5bc9f18cc5f12ded598a92e7900a7c9236 Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Tue, 1 Sep 2026 22:01:41 +0200 Subject: [PATCH 30/97] hwmon: (spd5118) Select page 0 unconditionally during probe Some Intel i2c controllers can be configured by the BIOS to reject writes to the SPD device. This often causes problems when the register page needs to be changed, usually during resume. Avoid probing on affected devices by unconditionally selecting page 0 by writing the SPD5118_REG_I2C_LEGACY_MODE register during probe. This will fail on affected controllers and thus prevent the driver from probing. Signed-off-by: Armin Wolf Link: https://patch.msgid.link/20260901200142.495319-1-W_Armin@gmx.de Signed-off-by: Guenter Roeck --- drivers/hwmon/spd5118.c | 59 ++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/drivers/hwmon/spd5118.c b/drivers/hwmon/spd5118.c index 9724cf70b61d..f79b46085ecc 100644 --- a/drivers/hwmon/spd5118.c +++ b/drivers/hwmon/spd5118.c @@ -637,44 +637,47 @@ static int spd5118_i2c_init(struct i2c_client *client) I2C_FUNC_SMBUS_WORD_DATA)) return -ENODEV; + /* Early check to avoid obviously unsupported I2C devices */ regval = i2c_smbus_read_word_swapped(client, SPD5118_REG_TYPE); - if (regval < 0 || (regval && regval != 0x5118)) + if (regval < 0) + return regval; + + /* + * Some SPD5118 devices report 0x0 when page 0 is not selected, + * so we only fail here if the register value is not 0x0 or 0x5118. + */ + if (regval && regval != 0x5118) return -ENODEV; /* - * If the device type registers return 0, it is possible that the chip - * has a non-zero page selected and takes the specification literally, + * We must select page 0 to ensure that we can reliably read the volatile + * registers on chips that take the specification literally, * i.e. disables access to volatile registers besides the page register * if the page is not 0. The Renesas/ITD SPD5118 Hub Controller is known - * to show this behavior. Try to identify such chips. + * to show this behavior. + * + * We must also perform an unconditional register write to detect if + * the i2c controller blocks write accesses to the SPD device. Some Intel + * controllers might be configured by the BIOS to do this. */ - if (!regval) { - /* Vendor ID registers must also be 0 */ - regval = i2c_smbus_read_word_data(client, SPD5118_REG_VENDOR); - if (regval) - return -ENODEV; + mode = i2c_smbus_read_byte_data(client, SPD5118_REG_I2C_LEGACY_MODE); + if (mode < 0) + return mode; - /* The selected page in MR11 must not be 0 */ - mode = i2c_smbus_read_byte_data(client, SPD5118_REG_I2C_LEGACY_MODE); - if (mode < 0 || (mode & ~SPD5118_LEGACY_MODE_MASK) || - !(mode & SPD5118_LEGACY_PAGE_MASK)) - return -ENODEV; + err = i2c_smbus_write_byte_data(client, SPD5118_REG_I2C_LEGACY_MODE, + mode & ~SPD5118_LEGACY_PAGE_MASK); + if (err < 0) + return err; - err = i2c_smbus_write_byte_data(client, SPD5118_REG_I2C_LEGACY_MODE, - mode & SPD5118_LEGACY_MODE_ADDR); - if (err) - return -ENODEV; + /* We only need to access SPD5118_REG_TYPE again if regval was 0x0 */ + if (regval == 0x5118) + return 0; - /* - * If the device type registers are still bad after selecting - * page 0, this is not a SPD5118 device. Restore original - * legacy mode register value and abort. - */ - regval = i2c_smbus_read_word_swapped(client, SPD5118_REG_TYPE); - if (regval != 0x5118) { - i2c_smbus_write_byte_data(client, SPD5118_REG_I2C_LEGACY_MODE, mode); - return -ENODEV; - } + regval = i2c_smbus_read_word_swapped(client, SPD5118_REG_TYPE); + if (regval != 0x5118) { + /* Restore original register content */ + i2c_smbus_write_byte_data(client, SPD5118_REG_I2C_LEGACY_MODE, mode); + return -ENODEV; } /* We are reasonably sure that this is really a SPD5118 hub controller */ From 720e4bd86c65651fc670dfd4c2cdecf5e414d804 Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Tue, 1 Sep 2026 22:01:42 +0200 Subject: [PATCH 31/97] hwmon: (spd5118) Avoid probing when 16-bit addressing is enabled Support for 16-bit addressing was removed when support for i3c was added to the driver. Switching between 8-bit and 16-bit addressing might confuse the system firmware, so we are forced to bail out if 16-bit addressing was configured during boot. Signed-off-by: Armin Wolf Link: https://patch.msgid.link/20260901200142.495319-2-W_Armin@gmx.de Signed-off-by: Guenter Roeck --- drivers/hwmon/spd5118.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/hwmon/spd5118.c b/drivers/hwmon/spd5118.c index f79b46085ecc..52f354489341 100644 --- a/drivers/hwmon/spd5118.c +++ b/drivers/hwmon/spd5118.c @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -664,6 +665,13 @@ static int spd5118_i2c_init(struct i2c_client *client) if (mode < 0) return mode; + /* 16-bit addressing is not supported */ + if (mode & SPD5118_LEGACY_MODE_ADDR) { + dev_notice(&client->dev, + "Unable to access device due to 16-bit addressing being enabled\n"); + return -ENODEV; + } + err = i2c_smbus_write_byte_data(client, SPD5118_REG_I2C_LEGACY_MODE, mode & ~SPD5118_LEGACY_PAGE_MASK); if (err < 0) From 182534c3fe456ed553e0cecc23f7135f1723906d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Peyrott?= Date: Tue, 1 Sep 2026 21:05:09 -0300 Subject: [PATCH 32/97] hwmon: Add Minisforum UM780 XTX EC monitoring and fan control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a DMI-gated hwmon driver for the embedded controller used by the Minisforum UM780 XTX with board revision 1.1 and BIOS 1.06. Expose the CPU and system fan control temperatures and tachometers. The tachometer protocol returns one byte per OEM command, so serialize transactions and use high-low-high sampling to reject torn values. Allow selecting either complete OEM CPU fan profile through pwm1_enable and changing the two validated system fan transition temperatures through standard automatic-curve attributes. Cache coherent settings and restore them after the firmware reloads defaults following resume. Signed-off-by: Sebastián Peyrott Link: https://patch.msgid.link/20260902000509.191115-1-speyrott@gmail.com [groeck: Folded fixes for gcc-11 build failure and overflow into this patch] Signed-off-by: Guenter Roeck --- Documentation/hwmon/index.rst | 1 + Documentation/hwmon/minisforum-um780xtx.rst | 68 +++ MAINTAINERS | 7 + drivers/hwmon/Kconfig | 16 + drivers/hwmon/Makefile | 1 + drivers/hwmon/minisforum-um780xtx.c | 503 ++++++++++++++++++++ 6 files changed, 596 insertions(+) create mode 100644 Documentation/hwmon/minisforum-um780xtx.rst create mode 100644 drivers/hwmon/minisforum-um780xtx.c diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst index 06a7992ce5da..f07977a20271 100644 --- a/Documentation/hwmon/index.rst +++ b/Documentation/hwmon/index.rst @@ -187,6 +187,7 @@ Hardware Monitoring Kernel Drivers mcp3021 mcp9982 menf21bmc + minisforum-um780xtx mlxreg-fan mp2856 mp2869 diff --git a/Documentation/hwmon/minisforum-um780xtx.rst b/Documentation/hwmon/minisforum-um780xtx.rst new file mode 100644 index 000000000000..8237d525a9c7 --- /dev/null +++ b/Documentation/hwmon/minisforum-um780xtx.rst @@ -0,0 +1,68 @@ +.. SPDX-License-Identifier: GPL-2.0-only + +==================================== +Kernel driver minisforum-um780xtx +==================================== + +Supported systems: + + * Minisforum UM780 XTX + + * DMI product name: ``Venus series`` + * Mainboard: ``F7BSD``, revision ``1.1`` + * BIOS version: ``1.06`` + +Author: Sebastián Peyrott + +Description +----------- + +This driver exposes hardware monitoring and fan-control data cached by the +IT5571E embedded controller. It uses the ACPI EC transport and only binds to +the exact system and firmware identity listed above. + +The two temperature channels are the values used by the EC's fan-control +loops. Their physical sensor placement is not known. The CPU channel is the +filtered AMD SB-TSI temperature, while the system channel is an external +thermistor input. + +The fan tachometers are read through OEM EC commands. Since each byte is +returned by a separate command, the driver uses a high-low-high sequence and +retries if the high byte changes. + +Sysfs entries +------------- + +========================== ============================================== +``temp1_input`` CPU-fan control temperature +``temp2_input`` System-fan control temperature +``fan1_input`` CPU fan speed in RPM +``fan2_input`` System fan speed in RPM +``pwm1_enable`` CPU profile: 2 is OEM B1, 3 is OEM B2 +``pwm2_auto_point1_temp`` Off-to-low system-fan transition temperature +``pwm2_auto_point2_temp`` Low-to-high system-fan transition temperature +========================== ============================================== + +CPU fan profiles +---------------- + +Values 2 and 3 of ``pwm1_enable`` select the two complete automatic profiles +implemented by the firmware. Writing either value reloads the whole CPU fan +curve, including firmware state which is not visible through the ACPI EC +window. Other values are rejected. + +System fan thresholds +--------------------- + +The two writable system-fan temperatures are rounded to whole degrees Celsius +and clamped to preserve strict ordering between both visible thresholds and +the firmware's internal third threshold. The third threshold does not select +a new PWM target and is therefore not exposed as another auto point. + +Resume state preservation +------------------------- + +Fan settings are held in EC RAM. On the supported firmware, initialization +during s2idle resume reloads the factory system-fan curve. The driver retains +the last coherent CPU profile and system-fan thresholds selected through its +interface and restores them synchronously at resume. diff --git a/MAINTAINERS b/MAINTAINERS index fdd08ebb204a..5e4d3fad9959 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -18185,6 +18185,13 @@ F: include/linux/min_heap.h F: lib/min_heap.c F: lib/tests/min_heap_kunit.c +MINISFORUM UM780 XTX EC HARDWARE MONITOR DRIVER +M: Sebastián Peyrott +L: linux-hwmon@vger.kernel.org +S: Maintained +F: Documentation/hwmon/minisforum-um780xtx.rst +F: drivers/hwmon/minisforum-um780xtx.c + MIPI CCS, SMIA AND SMIA++ IMAGE SENSOR DRIVER M: Sakari Ailus L: linux-media@vger.kernel.org diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index b19aa16986af..c1d72af52e8f 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -1490,6 +1490,22 @@ config SENSORS_MENF21BMC_HWMON This driver can also be built as a module. If so the module will be called menf21bmc_hwmon. +config SENSORS_MINISFORUM_UM780XTX + tristate "Minisforum UM780 XTX EC hardware monitoring" + depends on ACPI_EC && DMI && X86 + help + If you say yes here you get support for the hardware monitoring + features of the embedded controller in the Minisforum UM780 XTX. + This includes CPU and system fan speeds, their control temperatures, + two CPU fan profiles, system fan temperature thresholds, and + preservation of the selected settings across suspend and resume. + + This driver only binds to explicitly supported board and firmware + versions. + + This driver can also be built as a module. If so, the module + will be called minisforum-um780xtx. + config SENSORS_MR75203 tristate "Moortec Semiconductor MR75203 PVT Controller" select REGMAP_MMIO diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 646813b7aaa8..0243cb62c058 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -181,6 +181,7 @@ obj-$(CONFIG_SENSORS_TC654) += tc654.o obj-$(CONFIG_SENSORS_TPS23861) += tps23861.o obj-$(CONFIG_SENSORS_MLXREG_FAN) += mlxreg-fan.o obj-$(CONFIG_SENSORS_MENF21BMC_HWMON) += menf21bmc_hwmon.o +obj-$(CONFIG_SENSORS_MINISFORUM_UM780XTX) += minisforum-um780xtx.o obj-$(CONFIG_SENSORS_MR75203) += mr75203.o obj-$(CONFIG_SENSORS_NCT6683) += nct6683.o obj-$(CONFIG_SENSORS_NCT6694) += nct6694-hwmon.o diff --git a/drivers/hwmon/minisforum-um780xtx.c b/drivers/hwmon/minisforum-um780xtx.c new file mode 100644 index 000000000000..bbd77a0ce25d --- /dev/null +++ b/drivers/hwmon/minisforum-um780xtx.c @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Minisforum UM780 XTX (F7BSD) embedded-controller hwmon driver. + * + * Copyright (C) 2026 Sebastián Peyrott + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define UM780XTX_EC_TEMP_SYS 0x05 +#define UM780XTX_EC_TEMP_CPU 0x09 +#define UM780XTX_EC_CPU_PROFILE 0x2f +#define UM780XTX_EC_SYS_POINT1 0x31 +#define UM780XTX_EC_SYS_POINT2 0x34 +#define UM780XTX_EC_SYS_POINT3 0x37 + +#define UM780XTX_EC_PROFILE_B1 0xb1 +#define UM780XTX_EC_PROFILE_B2 0xb2 + +#define UM780XTX_EC_FAN1_LO 0xb6 +#define UM780XTX_EC_FAN1_HI 0xb7 +#define UM780XTX_EC_FAN2_LO 0xb9 +#define UM780XTX_EC_FAN2_HI 0xba + +#define UM780XTX_EC_MAX_RPM 9000 +#define UM780XTX_EC_RPM_RETRIES 3 + +static struct platform_device *um780xtx_pdev; + +struct um780xtx_data { + struct device *hwmon_dev; + u8 saved_profile; + u8 saved_sys_point1; + u8 saved_sys_point2; + u8 sys_point3; +}; + +static const struct dmi_system_id um780xtx_dmi_table[] = { + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, + "Micro Computer (HK) Tech Limited"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Venus series"), + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, + "Shenzhen Meigao Electronic Equipment Co.,Ltd"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "F7BSD"), + }, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, um780xtx_dmi_table); + +static bool um780xtx_firmware_match(void) +{ + const char *board_version = dmi_get_system_info(DMI_BOARD_VERSION); + const char *bios_version = dmi_get_system_info(DMI_BIOS_VERSION); + + return board_version && bios_version && + !strcmp(board_version, "1.1") && !strcmp(bios_version, "1.06"); +} + +static int um780xtx_oem_read(u8 command, u8 *value) +{ + return ec_transaction(command, NULL, 0, value, 1); +} + +static int um780xtx_read_rpm(u8 command_hi, u8 command_lo, long *rpm) +{ + u8 hi_before; + u8 hi_after; + u8 lo; + unsigned int value; + int attempt; + int ret; + + for (attempt = 0; attempt < UM780XTX_EC_RPM_RETRIES; attempt++) { + ret = um780xtx_oem_read(command_hi, &hi_before); + if (ret) + return ret; + ret = um780xtx_oem_read(command_lo, &lo); + if (ret) + return ret; + ret = um780xtx_oem_read(command_hi, &hi_after); + if (ret) + return ret; + if (hi_before != hi_after) + continue; + + value = (hi_after << 8) | lo; + if (value > UM780XTX_EC_MAX_RPM) + continue; + + *rpm = value; + return 0; + } + + return -EIO; +} + +static int um780xtx_read_profile(long *mode) +{ + u8 profile; + int ret; + + ret = ec_read(UM780XTX_EC_CPU_PROFILE, &profile); + if (ret) + return ret; + if (profile == UM780XTX_EC_PROFILE_B1) { + *mode = 2; + return 0; + } + if (profile == UM780XTX_EC_PROFILE_B2) { + *mode = 3; + return 0; + } + + return -ENODATA; +} + +static int um780xtx_write_profile(struct um780xtx_data *data, long mode) +{ + u8 expected; + u8 profile; + int ret; + + if (mode != 2 && mode != 3) + return -EINVAL; + expected = mode == 2 ? UM780XTX_EC_PROFILE_B1 : UM780XTX_EC_PROFILE_B2; + + ret = ec_transaction(expected, NULL, 0, NULL, 0); + if (ret) + return ret; + ret = ec_read(UM780XTX_EC_CPU_PROFILE, &profile); + if (ret) + return ret; + if (profile != expected) + return -EIO; + + data->saved_profile = profile; + return 0; +} + +static const u8 um780xtx_sys_point_offsets[] = { + UM780XTX_EC_SYS_POINT1, + UM780XTX_EC_SYS_POINT2, +}; + +static ssize_t um780xtx_sys_point_temp_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr); + u8 value; + int ret; + + guard(hwmon_lock)(dev); + ret = ec_read(um780xtx_sys_point_offsets[sattr->index], &value); + if (ret) + return ret; + + return sysfs_emit(buf, "%u\n", value * 1000); +} + +static ssize_t um780xtx_sys_point_temp_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct um780xtx_data *data = dev_get_drvdata(dev); + struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr); + u8 points[2]; + u8 readback; + long value, lo, hi; + int ret; + + ret = kstrtol(buf, 10, &value); + if (ret) + return ret; + + guard(hwmon_lock)(dev); + /* Validate and clamp against the live peer threshold. */ + ret = ec_read(UM780XTX_EC_SYS_POINT1, &points[0]); + if (ret) + return ret; + ret = ec_read(UM780XTX_EC_SYS_POINT2, &points[1]); + if (ret) + return ret; + if (points[0] >= points[1] || points[1] >= data->sys_point3) + return -EIO; + + if (!sattr->index) { + lo = 0; + hi = points[1] - 1; + } else { + lo = points[0] + 1; + hi = data->sys_point3 - 1; + } + value = DIV_ROUND_CLOSEST(clamp_val(value, 0, 255000), 1000); + points[sattr->index] = clamp_val(value, lo, hi); + + ret = ec_write(um780xtx_sys_point_offsets[sattr->index], + points[sattr->index]); + if (ret) + return ret; + ret = ec_read(um780xtx_sys_point_offsets[sattr->index], &readback); + if (ret) + return ret; + if (readback != points[sattr->index]) + return -EIO; + + data->saved_sys_point1 = points[0]; + data->saved_sys_point2 = points[1]; + return count; +} + +static SENSOR_DEVICE_ATTR_RW(pwm2_auto_point1_temp, + um780xtx_sys_point_temp, 0); +static SENSOR_DEVICE_ATTR_RW(pwm2_auto_point2_temp, + um780xtx_sys_point_temp, 1); + +static struct attribute *um780xtx_extra_attrs[] = { + &sensor_dev_attr_pwm2_auto_point1_temp.dev_attr.attr, + &sensor_dev_attr_pwm2_auto_point2_temp.dev_attr.attr, + NULL +}; + +static const struct attribute_group um780xtx_extra_group = { + .attrs = um780xtx_extra_attrs, +}; + +static const struct attribute_group *um780xtx_extra_groups[] = { + &um780xtx_extra_group, + NULL +}; + +static umode_t um780xtx_is_visible(const void *data, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + if (type == hwmon_temp || type == hwmon_fan) + return 0444; + if (type == hwmon_pwm && attr == hwmon_pwm_enable) + return 0644; + return 0; +} + +static int um780xtx_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *value) +{ + u8 raw; + int ret; + + if (type == hwmon_temp) { + ret = ec_read(channel ? UM780XTX_EC_TEMP_SYS : + UM780XTX_EC_TEMP_CPU, &raw); + if (ret) + return ret; + *value = raw * 1000L; + return 0; + } + if (type == hwmon_fan) { + if (!channel) + ret = um780xtx_read_rpm(UM780XTX_EC_FAN1_HI, + UM780XTX_EC_FAN1_LO, value); + else + ret = um780xtx_read_rpm(UM780XTX_EC_FAN2_HI, + UM780XTX_EC_FAN2_LO, value); + return ret; + } + if (type == hwmon_pwm) + return um780xtx_read_profile(value); + + return -EOPNOTSUPP; +} + +static int um780xtx_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long value) +{ + struct um780xtx_data *data = dev_get_drvdata(dev); + + if (type == hwmon_pwm && attr == hwmon_pwm_enable) + return um780xtx_write_profile(data, value); + + return -EOPNOTSUPP; +} + +static int um780xtx_read_string(struct device *dev, + enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + static const char * const labels[] = { "CPU", "SYS" }; + + if (type == hwmon_temp && attr == hwmon_temp_label) + *str = labels[channel]; + else if (type == hwmon_fan && attr == hwmon_fan_label) + *str = labels[channel]; + else + return -EOPNOTSUPP; + return 0; +} + +static const struct hwmon_ops um780xtx_hwmon_ops = { + .is_visible = um780xtx_is_visible, + .read = um780xtx_read, + .write = um780xtx_write, + .read_string = um780xtx_read_string, +}; + +static const struct hwmon_channel_info * const um780xtx_info[] = { + HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_LABEL, + HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_ENABLE), + NULL +}; + +static const struct hwmon_chip_info um780xtx_chip_info = { + .ops = &um780xtx_hwmon_ops, + .info = um780xtx_info, +}; + +static int um780xtx_write_sys_point(u8 offset, u8 value) +{ + u8 readback; + int ret; + + ret = ec_write(offset, value); + if (ret) + return ret; + ret = ec_read(offset, &readback); + if (ret) + return ret; + + return readback == value ? 0 : -EIO; +} + +static int um780xtx_restore_sys_points(struct um780xtx_data *data, + u8 current_point2) +{ + u8 point1 = data->saved_sys_point1; + u8 point2 = data->saved_sys_point2; + int ret; + + /* Keep strict ordering valid after each individual EC write. */ + if (point1 >= current_point2) { + ret = um780xtx_write_sys_point(UM780XTX_EC_SYS_POINT2, point2); + if (ret) + return ret; + return um780xtx_write_sys_point(UM780XTX_EC_SYS_POINT1, + point1); + } + + ret = um780xtx_write_sys_point(UM780XTX_EC_SYS_POINT1, point1); + if (ret) + return ret; + return um780xtx_write_sys_point(UM780XTX_EC_SYS_POINT2, point2); +} + +static int um780xtx_read_initial_state(struct um780xtx_data *data) +{ + u8 profile; + u8 point1; + u8 point2; + int ret; + + ret = ec_read(UM780XTX_EC_CPU_PROFILE, &profile); + if (ret) + return ret; + if (profile != UM780XTX_EC_PROFILE_B1 && + profile != UM780XTX_EC_PROFILE_B2) + return -EINVAL; + + ret = ec_read(UM780XTX_EC_SYS_POINT1, &point1); + if (ret) + return ret; + ret = ec_read(UM780XTX_EC_SYS_POINT2, &point2); + if (ret) + return ret; + if (point1 >= point2 || point2 >= data->sys_point3) + return -EINVAL; + + data->saved_profile = profile; + data->saved_sys_point1 = point1; + data->saved_sys_point2 = point2; + return 0; +} + +static int um780xtx_restore_state(struct um780xtx_data *data) +{ + u8 readback; + u8 point2; + int ret; + + ret = ec_transaction(data->saved_profile, NULL, 0, NULL, 0); + if (ret) + return ret; + ret = ec_read(UM780XTX_EC_CPU_PROFILE, &readback); + if (ret) + return ret; + if (readback != data->saved_profile) + return -EIO; + + ret = ec_read(UM780XTX_EC_SYS_POINT2, &point2); + if (ret) + return ret; + + return um780xtx_restore_sys_points(data, point2); +} + +static int um780xtx_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct um780xtx_data *data; + struct device *hwmon; + int ret; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + platform_set_drvdata(pdev, data); + + ret = ec_read(UM780XTX_EC_SYS_POINT3, &data->sys_point3); + if (ret) + return dev_err_probe(dev, ret, + "failed to read system fan limit\n"); + ret = um780xtx_read_initial_state(data); + if (ret) + return dev_err_probe(dev, ret, + "failed to read initial fan settings\n"); + + hwmon = devm_hwmon_device_register_with_info(dev, "um780xtx_ec", data, + &um780xtx_chip_info, + um780xtx_extra_groups); + if (IS_ERR(hwmon)) + return PTR_ERR(hwmon); + data->hwmon_dev = hwmon; + + return 0; +} + +static int um780xtx_resume(struct device *dev) +{ + struct um780xtx_data *data = dev_get_drvdata(dev); + + guard(hwmon_lock)(data->hwmon_dev); + return um780xtx_restore_state(data); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(um780xtx_pm_ops, NULL, um780xtx_resume); + +static struct platform_driver um780xtx_driver = { + .driver = { + .name = "um780xtx-ec-hwmon", + .pm = pm_sleep_ptr(&um780xtx_pm_ops), + }, + .probe = um780xtx_probe, +}; + +static int __init um780xtx_init(void) +{ + int ret; + + if (!dmi_check_system(um780xtx_dmi_table) || + !um780xtx_firmware_match() || !ec_get_handle()) + return -ENODEV; + ret = platform_driver_register(&um780xtx_driver); + if (ret) + return ret; + + um780xtx_pdev = platform_device_register_simple("um780xtx-ec-hwmon", + PLATFORM_DEVID_NONE, + NULL, 0); + if (IS_ERR(um780xtx_pdev)) { + ret = PTR_ERR(um780xtx_pdev); + platform_driver_unregister(&um780xtx_driver); + return ret; + } + return 0; +} + +static void __exit um780xtx_exit(void) +{ + platform_device_unregister(um780xtx_pdev); + platform_driver_unregister(&um780xtx_driver); +} + +module_init(um780xtx_init); +module_exit(um780xtx_exit); + +MODULE_AUTHOR("Sebastián Peyrott "); +MODULE_DESCRIPTION("Minisforum UM780 XTX EC hwmon driver"); +MODULE_LICENSE("GPL"); From b8dccc148af526602502d525a66c9bd764f5d232 Mon Sep 17 00:00:00 2001 From: Sanman Pradhan Date: Tue, 1 Sep 2026 21:11:48 +0000 Subject: [PATCH 33/97] dt-bindings: trivial-devices: Add TI TPS53622 and TPS53659 TPS53622 and TPS53659 are PMBus-compliant D-CAP+ multiphase step-down controllers. Add their compatible strings. Signed-off-by: Sanman Pradhan Acked-by: Conor Dooley Link: https://patch.msgid.link/20260901211129.360792-2-sanman.pradhan@hpe.com Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/trivial-devices.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/devicetree/bindings/trivial-devices.yaml b/Documentation/devicetree/bindings/trivial-devices.yaml index 6229a6516302..94e63d8a58ad 100644 --- a/Documentation/devicetree/bindings/trivial-devices.yaml +++ b/Documentation/devicetree/bindings/trivial-devices.yaml @@ -512,8 +512,12 @@ properties: - ti,tmp125 # TI DC-DC converter on PMBus - ti,tps40400 + # TI Dual channel DCAP+ multiphase controller TPS53622 + - ti,tps53622 # TI DCAP+ multiphase controller - ti,tps53647 + # TI Dual channel DCAP+ multiphase controller TPS53659 + - ti,tps53659 # TI DCAP+ multiphase controller - ti,tps53667 # TI Dual channel DCAP+ multiphase controller TPS53676 with AVSBus From dc62090efa966b96ce5c49baeeee45f401951fbf Mon Sep 17 00:00:00 2001 From: Sanman Pradhan Date: Tue, 1 Sep 2026 21:11:53 +0000 Subject: [PATCH 34/97] hwmon: (pmbus/tps53679) Add support for TPS53622 and TPS53659 TPS53622 and TPS53659 are dual-channel D-CAP+ step-down controllers that use the VID VOUT format and VOUT_MODE identification like the existing TPS53679/TPS53688, so they reuse tps53679_identify(). Shorten the Kconfig prompt to the family name and list the supported chips in the help text instead; this also adds TPS53685, which is already supported by the driver but was missing from the list. Update the driver documentation, including the per-attribute lists, and fix an existing "TPS53588" typo (should be TPS53688) in those lists. Signed-off-by: Sanman Pradhan Link: https://patch.msgid.link/20260901211129.360792-3-sanman.pradhan@hpe.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/tps53679.rst | 24 ++++++++++++++++++++---- drivers/hwmon/pmbus/Kconfig | 5 +++-- drivers/hwmon/pmbus/tps53679.c | 9 ++++++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Documentation/hwmon/tps53679.rst b/Documentation/hwmon/tps53679.rst index dd5e4a37375d..2280e043c4de 100644 --- a/Documentation/hwmon/tps53679.rst +++ b/Documentation/hwmon/tps53679.rst @@ -3,6 +3,14 @@ Kernel driver tps53679 Supported chips: + * Texas Instruments TPS53622 + + Prefix: 'tps53622' + + Addresses scanned: - + + Datasheet: https://www.ti.com/lit/gpn/TPS53622 + * Texas Instruments TPS53647 Prefix: 'tps53647' @@ -11,6 +19,14 @@ Supported chips: Datasheet: https://www.ti.com/lit/gpn/tps53647 + * Texas Instruments TPS53659 + + Prefix: 'tps53659' + + Addresses scanned: - + + Datasheet: https://www.ti.com/lit/gpn/TPS53659 + * Texas Instruments TPS53667 Prefix: 'tps53667' @@ -108,7 +124,7 @@ in1_crit_alarm Input voltage critical high alarm. in[N]_label "vout[1-2]" - TPS53647, TPS53667: N=2 - - TPS53679, TPS53588: N=2,3 + - TPS53622, TPS53659, TPS53679, TPS53688: N=2,3 in[N]_input Measured output voltage. @@ -135,7 +151,7 @@ in[N]_crit_alarm Output voltage critical high alarm. temp[N]_input Measured temperature. - TPS53647, TPS53667: N=1 - - TPS53679, TPS53681, TPS53588: N=1,2 + - TPS53622, TPS53659, TPS53679, TPS53681, TPS53688: N=1,2 temp[N]_max Maximum temperature. @@ -152,7 +168,7 @@ power1_input Measured input power. power[N]_label "pout[1-2]". - TPS53647, TPS53667: N=2 - - TPS53676, TPS53679, TPS53681, TPS53588: N=2,3 + - TPS53622, TPS53659, TPS53676, TPS53679, TPS53681, TPS53688: N=2,3 power[N]_input Measured output power. @@ -175,7 +191,7 @@ curr[N]_label "iout[1-2]" or "iout1.[0-5]". telemetry supported on TPS53676 and TPS53681 only. - TPS53647, TPS53667: N=2 - - TPS53679, TPS53588: N=2,3 + - TPS53622, TPS53659, TPS53679, TPS53688: N=2,3 - TPS53676: N=2-8 - TPS53681: N=2-9 diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig index eb71a0ac044b..bcfdc4ce4c10 100644 --- a/drivers/hwmon/pmbus/Kconfig +++ b/drivers/hwmon/pmbus/Kconfig @@ -790,10 +790,11 @@ config SENSORS_TPS40422 be called tps40422. config SENSORS_TPS53679 - tristate "TI TPS53647, TPS53667, TPS53676, TPS53679, TPS53681, TPS53688" + tristate "TI TPS536xx family" help If you say yes here you get hardware monitoring support for TI - TPS53647, TPS53667, TPS53676, TPS53679, TPS53681, and TPS53688. + TPS53622, TPS53647, TPS53659, TPS53667, TPS53676, TPS53679, TPS53681, + TPS53685, and TPS53688. This driver can also be built as a module. If so, the module will be called tps53679. diff --git a/drivers/hwmon/pmbus/tps53679.c b/drivers/hwmon/pmbus/tps53679.c index 6c25701b36fc..0b94d81ccd13 100644 --- a/drivers/hwmon/pmbus/tps53679.c +++ b/drivers/hwmon/pmbus/tps53679.c @@ -16,7 +16,8 @@ #include "pmbus.h" enum chips { - tps53647, tps53667, tps53676, tps53679, tps53681, tps53685, tps53688 + tps53622, tps53647, tps53659, tps53667, tps53676, tps53679, tps53681, + tps53685, tps53688 }; #define TPS53647_PAGE_NUM 1 @@ -277,6 +278,8 @@ static int tps53679_probe(struct i2c_client *client) case tps53676: info->identify = tps53676_identify; break; + case tps53622: + case tps53659: case tps53679: case tps53688: info->pages = TPS53679_PAGE_NUM; @@ -301,7 +304,9 @@ static int tps53679_probe(struct i2c_client *client) static const struct i2c_device_id tps53679_id[] = { { .name = "bmr474", .driver_data = tps53676 }, + { .name = "tps53622", .driver_data = tps53622 }, { .name = "tps53647", .driver_data = tps53647 }, + { .name = "tps53659", .driver_data = tps53659 }, { .name = "tps53667", .driver_data = tps53667 }, { .name = "tps53676", .driver_data = tps53676 }, { .name = "tps53679", .driver_data = tps53679 }, @@ -314,7 +319,9 @@ static const struct i2c_device_id tps53679_id[] = { MODULE_DEVICE_TABLE(i2c, tps53679_id); static const struct of_device_id __maybe_unused tps53679_of_match[] = { + {.compatible = "ti,tps53622", .data = (void *)tps53622}, {.compatible = "ti,tps53647", .data = (void *)tps53647}, + {.compatible = "ti,tps53659", .data = (void *)tps53659}, {.compatible = "ti,tps53667", .data = (void *)tps53667}, {.compatible = "ti,tps53676", .data = (void *)tps53676}, {.compatible = "ti,tps53679", .data = (void *)tps53679}, From b67f60ae9b0b8eeb1bd29eb73da050bbe761250a Mon Sep 17 00:00:00 2001 From: Mario Nuevo Date: Sat, 5 Sep 2026 10:03:36 +0200 Subject: [PATCH 35/97] hwmon: (asus-ec-sensors) add ROG STRIX Z490-A GAMING The ROG STRIX Z490-A GAMING and Z490-F GAMING are essentially the same board apart from the F's onboard WiFi module, which isn't wired through the EC, so they share the same EC sensor layout. The Z490-A exposes the same EC sensor set as the Z490-F (chipset, CPU, motherboard, T_Sensor and VRM temperatures plus CPU_OPT and VRM HS fans) at the same EC registers, so reuse the Z490-F board_info for it. Signed-off-by: Mario Nuevo Signed-off-by: Eugene Shalygin Link: https://patch.msgid.link/20260905080346.1957380-1-eugene.shalygin@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/asus_ec_sensors.rst | 1 + drivers/hwmon/asus-ec-sensors.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Documentation/hwmon/asus_ec_sensors.rst b/Documentation/hwmon/asus_ec_sensors.rst index ebed9f8d2adb..7032c40731d9 100644 --- a/Documentation/hwmon/asus_ec_sensors.rst +++ b/Documentation/hwmon/asus_ec_sensors.rst @@ -55,6 +55,7 @@ Supported boards: * ROG STRIX X870E-H GAMING WIFI7 * ROG STRIX Z390-E GAMING * ROG STRIX Z390-F GAMING + * ROG STRIX Z490-A GAMING * ROG STRIX Z490-F GAMING * ROG STRIX Z690-A GAMING WIFI D4 * ROG STRIX Z690-E GAMING WIFI diff --git a/drivers/hwmon/asus-ec-sensors.c b/drivers/hwmon/asus-ec-sensors.c index 7293c48c72b1..19f423e45ce5 100644 --- a/drivers/hwmon/asus-ec-sensors.c +++ b/drivers/hwmon/asus-ec-sensors.c @@ -982,6 +982,8 @@ static const struct dmi_system_id dmi_table[] = { &board_info_strix_z390_f_gaming), DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX Z390-F GAMING", &board_info_strix_z390_f_gaming), + DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX Z490-A GAMING", + &board_info_strix_z490_f_gaming), DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX Z490-F GAMING", &board_info_strix_z490_f_gaming), DMI_EXACT_MATCH_ASUS_BOARD_NAME("ROG STRIX Z690-A GAMING WIFI D4", From cae6788d2ecac411b86fc85840f59e9e2a6a0186 Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Wed, 2 Sep 2026 03:54:00 +0200 Subject: [PATCH 36/97] dt-bindings: hwmon: ti,tmp401: add #thermal-sensor-cells Since commit a3b61ba2558e ("hwmon: (tmp401) register with thermal subsystem") the driver sets HWMON_C_REGISTER_TZ, so the hwmon core calls devm_thermal_of_zone_register() for every temperature channel and the device can be referenced as a thermal sensor. Signed-off-by: Vincent Jardin Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260902-for-upstream-dt-tmp401-vcc-tsc-v3-1-49c484d2f08b@free.fr [groeck: Reformatted patch descriptiuon] Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/hwmon/ti,tmp401.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Documentation/devicetree/bindings/hwmon/ti,tmp401.yaml b/Documentation/devicetree/bindings/hwmon/ti,tmp401.yaml index 0e8ddf0ad789..f283b567d5b6 100644 --- a/Documentation/devicetree/bindings/hwmon/ti,tmp401.yaml +++ b/Documentation/devicetree/bindings/hwmon/ti,tmp401.yaml @@ -50,6 +50,9 @@ properties: minimum: 0 maximum: 15 + "#thermal-sensor-cells": + const: 1 + allOf: - if: properties: From b1c986faa7a9276656cb545e0bd5c3ed2bf83048 Mon Sep 17 00:00:00 2001 From: Sergio Melas Date: Tue, 8 Sep 2026 11:49:31 +0200 Subject: [PATCH 37/97] hwmon: (yogafan) Add support for new Lenovo models Add DMI quirk entries and map correct ACPI paths and configurations for additional Lenovo laptop models: Yoga 14cACN 2021, IdeaPad 3 15ALC6 Ub and Yoga 740-15IML, leveraging DMI product family matching. Signed-off-by: Sergio Melas Link: https://patch.msgid.link/20260908094931.2227049-1-sergiomelas@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/yogafan.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/drivers/hwmon/yogafan.c b/drivers/hwmon/yogafan.c index 413ddc721a11..aa3284560715 100644 --- a/drivers/hwmon/yogafan.c +++ b/drivers/hwmon/yogafan.c @@ -53,6 +53,11 @@ struct yoga_fan_data { }; /* Specific configurations mapped via DMI */ +static const struct yogafan_config yoga_740_15iml_cfg = { + .multiplier = 100, + .fan_count = 1, + .paths = { "\\_SB.PCI0.LPCB.EC0.FANS", NULL } +}; static const struct yogafan_config yoga_8bit_fans_cfg = { .multiplier = 100, @@ -200,6 +205,30 @@ static const struct hwmon_chip_info yoga_fan_chip_info = { }; static const struct dmi_system_id yogafan_quirks[] = { + { + .ident = "Lenovo Yoga 740-15IML", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga 740-15IML"), + }, + .driver_data = (void *)&yoga_740_15iml_cfg, + }, + { + .ident = "Lenovo IdeaPad 3 15ALC6 Ub", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_FAMILY, "IdeaPad 3 15ALC6 Ub"), + }, + .driver_data = (void *)&xiaoxin_8bit_dual_cfg, + }, + { + .ident = "Yoga 14cACN 2021", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), + DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga 14cACN 2021"), + }, + .driver_data = (void *)&yoga_8bit_fans_cfg, + }, { .ident = "Lenovo LOQ 15IAX9", .matches = { From 41acf68309a3d290533281bbad967bc01e428190 Mon Sep 17 00:00:00 2001 From: Paul Louvel Date: Wed, 9 Sep 2026 20:25:54 +0200 Subject: [PATCH 38/97] Documentation: hwmon: nct6775: Document NCT6116D/NCT6122D/NCT6126D support The NCT6112D/NCT6114D/NCT6116D family has been supported by the driver since commit 29c7cb485b32 ("hwmon: (nct6775) Integrate new model nct6116"), but was never added to the list of supported chips in the driver documentation. Also add the NCT6122D/NCT6126D. Assisted-by: opencode:z-ai/glm-5.3-flash Signed-off-by: Paul Louvel Link: https://patch.msgid.link/20260909-bootlin-hwmon-nct6126-v1-1-4e85615fef10@bootlin.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/nct6775.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Documentation/hwmon/nct6775.rst b/Documentation/hwmon/nct6775.rst index 41b21bed7e27..5199c083b9e7 100644 --- a/Documentation/hwmon/nct6775.rst +++ b/Documentation/hwmon/nct6775.rst @@ -16,6 +16,22 @@ Supported chips: Datasheet: Available from the Nuvoton web site + * Nuvoton NCT6112D/NCT6114D/NCT6116D + + Prefix: 'nct6116' + + Addresses scanned: ISA address retrieved from Super I/O registers + + Datasheet: Available from the Nuvoton web site + + * Nuvoton NCT6122D/NCT6126D + + Prefix: 'nct6126' + + Addresses scanned: ISA address retrieved from Super I/O registers + + Datasheet: Available from the Nuvoton web site + * Nuvoton NCT5572D/NCT6771F/NCT6772F/NCT6775F/W83677HG-I Prefix: 'nct6775' From a719a0a00e8d3a7b0a383e3d7ee5c336169d4a98 Mon Sep 17 00:00:00 2001 From: Zac Lanzon Date: Wed, 2 Sep 2026 20:40:54 -0400 Subject: [PATCH 39/97] hwmon: (asus_rog_ryujin) Add ROG Ryujin III 360 Add the USB product ID of the base ASUS ROG RYUJIN III 360 (0b05:1aa2, USB product string "ROG RYUJIN III"). It uses the same status report layout and cooler duty channel as the RYUJIN III EXTREME, EVA and WHITE editions, so it can share rog_ryujin_iii_info. Tested on a ROG RYUJIN III 360: coolant temperature, pump speed and internal fan speed report plausible values that track load, and the internal fan follows pwm2 (30% -> 870 RPM, 40% -> 1590 RPM). Assisted-by: Claude:claude-fable-5-1 Signed-off-by: Zac Lanzon Link: https://patch.msgid.link/20260903004054.502171-1-lanzonza@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/asus_rog_ryujin.rst | 1 + drivers/hwmon/asus_rog_ryujin.c | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Documentation/hwmon/asus_rog_ryujin.rst b/Documentation/hwmon/asus_rog_ryujin.rst index b0d7ce8dd921..3ebab230e8ca 100644 --- a/Documentation/hwmon/asus_rog_ryujin.rst +++ b/Documentation/hwmon/asus_rog_ryujin.rst @@ -6,6 +6,7 @@ Kernel driver asus_rog_ryujin Supported devices: * ASUS ROG RYUJIN II 360 +* ASUS ROG RYUJIN III 360 * ASUS ROG RYUJIN III EXTREME * ASUS ROG RYUJIN III EVA EDITION * ASUS ROG RYUJIN III WHITE EDITION diff --git a/drivers/hwmon/asus_rog_ryujin.c b/drivers/hwmon/asus_rog_ryujin.c index e297557ca346..5ced69e94ead 100644 --- a/drivers/hwmon/asus_rog_ryujin.c +++ b/drivers/hwmon/asus_rog_ryujin.c @@ -20,6 +20,7 @@ #define USB_PRODUCT_ID_RYUJIN_III_EXTREME 0x1bcb #define USB_PRODUCT_ID_RYUJIN_III_EVA 0x1ade #define USB_PRODUCT_ID_RYUJIN_III_WHITE 0x1ada +#define USB_PRODUCT_ID_RYUJIN_III 0x1aa2 struct rog_ryujin_device_info { u8 temp_offset; @@ -613,6 +614,8 @@ static const struct hid_device_id rog_ryujin_table[] = { .driver_data = (kernel_ulong_t)&rog_ryujin_iii_info }, { HID_USB_DEVICE(USB_VENDOR_ID_ASUS_ROG, USB_PRODUCT_ID_RYUJIN_III_WHITE), .driver_data = (kernel_ulong_t)&rog_ryujin_iii_info }, + { HID_USB_DEVICE(USB_VENDOR_ID_ASUS_ROG, USB_PRODUCT_ID_RYUJIN_III), + .driver_data = (kernel_ulong_t)&rog_ryujin_iii_info }, { } }; From f911449600bf182c965677c1e986519db86e8534 Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Fri, 4 Sep 2026 17:12:46 +0530 Subject: [PATCH 40/97] hwmon: fix typos in comments Fix typos in comments, reported by scripts/checkpatch.pl using the misspelling list in scripts/spelling.txt. Only touches comments, no code changes. Assisted-by: Cursor:claude-opus-5 Signed-off-by: Hemanth Selam Link: https://patch.msgid.link/20260904114249.26515-1-hemanth.selam@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/abituguru.c | 4 ++-- drivers/hwmon/adm1177.c | 2 +- drivers/hwmon/adt7x10.c | 2 +- drivers/hwmon/emc2103.c | 2 +- drivers/hwmon/lm85.c | 2 +- drivers/hwmon/pmbus/mp2888.c | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/drivers/hwmon/abituguru.c b/drivers/hwmon/abituguru.c index ba8c68ae4595..3bb959d16dbf 100644 --- a/drivers/hwmon/abituguru.c +++ b/drivers/hwmon/abituguru.c @@ -475,7 +475,7 @@ abituguru_detect_bank1_sensor_type(struct abituguru_data *data, u8 val, test_flag, buf[3]; int i, ret = -ENODEV; /* error is the most common used retval :| */ - /* If overriden by the user return the user selected type */ + /* If overridden by the user return the user selected type */ if (bank1_types[sensor_addr] >= ABIT_UGURU_IN_SENSOR && bank1_types[sensor_addr] <= ABIT_UGURU_NC) { ABIT_UGURU_DEBUG(2, "assuming sensor type %d for bank1 sensor " @@ -1262,7 +1262,7 @@ static int abituguru_probe(struct platform_device *pdev) /* * El weirdo probe order, to keep the sysfs order identical to the - * BIOS and window-appliction listing order. + * BIOS and window-application listing order. */ static const u8 probe_order[ABIT_UGURU_MAX_BANK1_SENSORS] = { 0x00, 0x01, 0x03, 0x04, 0x0A, 0x08, 0x0E, 0x02, diff --git a/drivers/hwmon/adm1177.c b/drivers/hwmon/adm1177.c index dc4d8a214d1b..85ad4a3009bd 100644 --- a/drivers/hwmon/adm1177.c +++ b/drivers/hwmon/adm1177.c @@ -105,7 +105,7 @@ static int adm1177_read(struct device *dev, enum hwmon_sensor_types type, return ret; dummy = (data[0] << 4) | (data[2] >> 4); /* - * convert to millivolts based on resistor devision + * convert to millivolts based on resistor division * (V_fullscale / 4096) * raw */ if (st->vrange_high) diff --git a/drivers/hwmon/adt7x10.c b/drivers/hwmon/adt7x10.c index d003ee3ebf06..f99d38e82d0b 100644 --- a/drivers/hwmon/adt7x10.c +++ b/drivers/hwmon/adt7x10.c @@ -338,7 +338,7 @@ int adt7x10_probe(struct device *dev, const char *name, int irq, data->oldconfig = config; /* - * Set to 16 bit resolution, continous conversion and comparator mode. + * Set to 16 bit resolution, continuous conversion and comparator mode. */ data->config = data->oldconfig; data->config &= ~(ADT7X10_MODE_MASK | ADT7X10_CT_POLARITY | diff --git a/drivers/hwmon/emc2103.c b/drivers/hwmon/emc2103.c index 27dc149a3ed9..4adc23ffaf72 100644 --- a/drivers/hwmon/emc2103.c +++ b/drivers/hwmon/emc2103.c @@ -32,7 +32,7 @@ static const u8 REG_TEMP_MAX[4] = { 0x34, 0x30, 0x31, 0x32 }; #define REG_PRODUCT_ID 0xfd #define REG_MFG_ID 0xfe -/* equation 4 from datasheet: rpm = (3932160 * multipler) / count */ +/* equation 4 from datasheet: rpm = (3932160 * multiplier) / count */ #define FAN_RPM_FACTOR 3932160 /* diff --git a/drivers/hwmon/lm85.c b/drivers/hwmon/lm85.c index c56e164d61c1..27ef127ccc2c 100644 --- a/drivers/hwmon/lm85.c +++ b/drivers/hwmon/lm85.c @@ -1445,7 +1445,7 @@ static void lm85_init_client(struct i2c_client *client) static int lm85_is_fake(struct i2c_client *client) { /* - * Differenciate between real LM96000 and Winbond WPCD377I. The latter + * Differentiate between real LM96000 and Winbond WPCD377I. The latter * emulate the former except that it has no hardware monitoring function * so the readings are always 0. */ diff --git a/drivers/hwmon/pmbus/mp2888.c b/drivers/hwmon/pmbus/mp2888.c index c5f35daa3fe1..7a2ecffba960 100644 --- a/drivers/hwmon/pmbus/mp2888.c +++ b/drivers/hwmon/pmbus/mp2888.c @@ -272,14 +272,14 @@ static int mp2888_write_word_data(struct i2c_client *client, int page, int reg, word = clamp_val(word, 0, GENMASK(7, 0)); break; case PMBUS_IOUT_OC_WARN_LIMIT: - /* Fix limit according to total curent resolution. */ + /* Fix limit according to total current resolution. */ word = data->total_curr_resolution ? DIV_ROUND_CLOSEST(word, 8) : DIV_ROUND_CLOSEST(word, 4); /* Drop unused bits 15:10. */ word = clamp_val(word, 0, GENMASK(9, 0)); break; case PMBUS_POUT_OP_WARN_LIMIT: - /* Fix limit according to total curent resolution. */ + /* Fix limit according to total current resolution. */ word = data->total_curr_resolution ? DIV_ROUND_CLOSEST(word, 4) : DIV_ROUND_CLOSEST(word, 2); /* Drop unused bits 15:10. */ From 241956880457326df28bd8a2a4f14fa8aba4f0ff Mon Sep 17 00:00:00 2001 From: Manuel Ebner Date: Thu, 3 Sep 2026 18:12:03 +0200 Subject: [PATCH 41/97] docs: hwmon: sysfs-interface: Fix bracket Add missing ')'. Signed-off-by: Manuel Ebner Acked-by: Randy Dunlap Link: https://patch.msgid.link/20260903161204.735276-2-manuelebnerli@mailbox.org Signed-off-by: Guenter Roeck --- Documentation/hwmon/sysfs-interface.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/hwmon/sysfs-interface.rst b/Documentation/hwmon/sysfs-interface.rst index f7ab5d5c4d3a..c5ac4d3665dc 100644 --- a/Documentation/hwmon/sysfs-interface.rst +++ b/Documentation/hwmon/sysfs-interface.rst @@ -47,7 +47,7 @@ There is only one value per file, unlike the older /proc specification. The common scheme for files naming is: _. Usual types for sensor chips are "in" (voltage), "temp" (temperature) and "fan" (fan). Usual items are "input" (measured value), "max" (high -threshold, "min" (low threshold). Numbering usually starts from 1, +threshold), "min" (low threshold). Numbering usually starts from 1, except for voltages which start from 0 (because most data sheets use this). A number is always used for elements that can be present more than once, even if there is a single element of the given type on the From bc8c5bfc10896374182c1f20201446fc1abecdda Mon Sep 17 00:00:00 2001 From: Carl Lee Date: Mon, 7 Sep 2026 15:58:32 +0800 Subject: [PATCH 42/97] hwmon: (pmbus/ltc4286) Add writable shunt_resistor sysfs attribute The LTC4286/LTC4287 driver uses rsense to scale current and power telemetry. rsense is board-specific rather than a fixed device characteristic, and may differ across qualified BOM variants using the same software image. If rsense does not match the installed board configuration, current and power readings are mis-scaled. Add a writable shunt_resistor sysfs attribute so userspace can update the board calibration value after platform identification. Initialize rsense from shunt-resistor-micro-ohms as before (or the existing default when absent), and validate runtime updates before recalculating the current/power scaling coefficients. Signed-off-by: Carl Lee Link: https://patch.msgid.link/20260907-hwmon-pmbus-ltc4286-add-runtime-shunt-resistor-control-v2-1-5f47f7e5a1bb@amd.com Signed-off-by: Guenter Roeck --- drivers/hwmon/pmbus/ltc4286.c | 96 +++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/drivers/hwmon/pmbus/ltc4286.c b/drivers/hwmon/pmbus/ltc4286.c index 8715d380784a..8190706d1beb 100644 --- a/drivers/hwmon/pmbus/ltc4286.c +++ b/drivers/hwmon/pmbus/ltc4286.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -16,6 +17,85 @@ #define LTC4286_MFR_ID_SIZE 3 +struct ltc4286_data { + struct pmbus_driver_info info; + u32 rsense; + bool vrange_low_enable; +}; + +#define to_ltc4286_data(x) container_of((x), struct ltc4286_data, info) + +static ssize_t ltc4286_rsense_show(struct device *dev, + struct device_attribute *devattr, + char *buf) +{ + struct i2c_client *client = to_i2c_client(dev->parent); + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc4286_data *data = to_ltc4286_data(info); + u32 rsense; + int ret; + + ret = pmbus_lock_interruptible(client); + if (ret) + return ret; + + rsense = data->rsense; + + pmbus_unlock(client); + + return sysfs_emit(buf, "%u\n", rsense); +} + +static ssize_t ltc4286_rsense_store(struct device *dev, + struct device_attribute *devattr, + const char *buf, size_t count) +{ + struct i2c_client *client = to_i2c_client(dev->parent); + const struct pmbus_driver_info *info_ro = pmbus_get_driver_info(client); + struct ltc4286_data *data = to_ltc4286_data(info_ro); + struct pmbus_driver_info *info = &data->info; + u32 rsense; + int ret; + + ret = kstrtou32(buf, 10, &rsense); + if (ret) + return ret; + + if (rsense == 0) + return -EINVAL; + + if (rsense > (INT_MAX / 1024)) + return -EINVAL; + + ret = pmbus_lock_interruptible(client); + if (ret) + return ret; + + data->rsense = rsense; + info->m[PSC_CURRENT_OUT] = 1024 * rsense; + info->m[PSC_POWER] = data->vrange_low_enable ? 4 * rsense : rsense; + + pmbus_unlock(client); + + return count; +} + +static SENSOR_DEVICE_ATTR_RW(shunt_resistor, ltc4286_rsense, 0); + +static struct attribute *ltc4286_attrs[] = { + &sensor_dev_attr_shunt_resistor.dev_attr.attr, + NULL, +}; + +static const struct attribute_group ltc4286_group = { + .attrs = ltc4286_attrs, +}; + +static const struct attribute_group *ltc4286_attribute_groups[] = { + <c4286_group, + NULL, +}; + /* * Initialize the MBR as default settings which is referred to LTC4286 datasheet * (March 22, 2022 version) table 3 page 16 @@ -55,6 +135,7 @@ static struct pmbus_driver_info ltc4286_info = { .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | PMBUS_HAVE_PIN | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_TEMP, + .groups = ltc4286_attribute_groups, }; static const struct i2c_device_id ltc4286_id[] = { @@ -69,6 +150,7 @@ static int ltc4286_probe(struct i2c_client *client) int ret; const struct i2c_device_id *mid; u8 block_buffer[I2C_SMBUS_BLOCK_MAX + 1]; + struct ltc4286_data *data; struct pmbus_driver_info *info; u32 rsense; int vrange_nval, vrange_oval; @@ -114,11 +196,14 @@ static int ltc4286_probe(struct i2c_client *client) if (rsense > (INT_MAX / 1024)) return -EINVAL; - info = devm_kmemdup(&client->dev, <c4286_info, sizeof(*info), - GFP_KERNEL); - if (!info) + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) return -ENOMEM; + data->info = ltc4286_info; + data->rsense = rsense; + info = &data->info; + /* Check MFR1 CONFIG register bit 1 VRANGE_SELECT before driver loading */ vrange_oval = i2c_smbus_read_word_data(client, LTC4286_MFR_CONFIG1); if (vrange_oval < 0) @@ -126,7 +211,10 @@ static int ltc4286_probe(struct i2c_client *client) "Failed to read manufacturer configuration one\n"); vrange_nval = vrange_oval; - if (device_property_read_bool(&client->dev, "adi,vrange-low-enable")) { + data->vrange_low_enable = + device_property_read_bool(&client->dev, "adi,vrange-low-enable"); + + if (data->vrange_low_enable) { vrange_nval &= ~VRANGE_SELECT_BIT; /* VRANGE_SELECT = 0, 25.6 volts */ From a0d99d6bf5eb3455ce14c7cfc607ae94a858ea0c Mon Sep 17 00:00:00 2001 From: Petar Stepanovic Date: Thu, 3 Sep 2026 00:09:47 -0700 Subject: [PATCH 43/97] dt-bindings: hwmon: Add Axiado AX3000 TSADC Add a Devicetree binding for the temperature sensor ADC (TSADC) controller found on the Axiado AX3000 and AX3005 SoCs. The controller monitors the on-die temperature. An SoC may contain multiple TSADC controllers at different locations on the die, each represented by a separate Devicetree node. AX3005 uses the same programming model as AX3000 and is compatible with the AX3000 implementation. Therefore, axiado,ax3000-tsadc is used as its fallback compatible. Also add a MAINTAINERS entry covering the binding and driver. Signed-off-by: Petar Stepanovic Acked-by: Conor Dooley Link: https://patch.msgid.link/20260903-axiado-ax3000-tsadc-v1-1-a8a0f3b251f5@axiado.com Signed-off-by: Guenter Roeck --- .../bindings/hwmon/axiado,ax3000-tsadc.yaml | 45 +++++++++++++++++++ MAINTAINERS | 8 ++++ 2 files changed, 53 insertions(+) create mode 100644 Documentation/devicetree/bindings/hwmon/axiado,ax3000-tsadc.yaml diff --git a/Documentation/devicetree/bindings/hwmon/axiado,ax3000-tsadc.yaml b/Documentation/devicetree/bindings/hwmon/axiado,ax3000-tsadc.yaml new file mode 100644 index 000000000000..4b56e4fd9ef9 --- /dev/null +++ b/Documentation/devicetree/bindings/hwmon/axiado,ax3000-tsadc.yaml @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/hwmon/axiado,ax3000-tsadc.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Axiado AX3000 and AX3005 TSADC + +maintainers: + - Petar Stepanovic + - Akhila Kavi + - Prasad Bolisetty + +description: | + The Axiado AX3000 and AX3005 Temperature Sensor ADC (TSADC) controllers + monitor the on-die temperature of the SoC. + +properties: + compatible: + oneOf: + - const: axiado,ax3000-tsadc + - items: + - const: axiado,ax3005-tsadc + - const: axiado,ax3000-tsadc + + reg: + maxItems: 1 + + clocks: + maxItems: 1 + +required: + - compatible + - reg + - clocks + +additionalProperties: false + +examples: + - | + temperature-sensor@33021000 { + compatible = "axiado,ax3000-tsadc"; + reg = <0x33021000 0x100>; + clocks = <&refclk>; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 5e4d3fad9959..83f50b33e607 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4508,6 +4508,14 @@ F: Documentation/devicetree/bindings/spi/axiado,ax3000-spi.yaml F: drivers/spi/spi-axiado.c F: drivers/spi/spi-axiado.h +AXIADO TSADC DRIVER +M: Petar Stepanovic +M: Akhila Kavi +M: Prasad Bolisetty +L: linux-hwmon@vger.kernel.org +S: Supported +F: Documentation/devicetree/bindings/hwmon/axiado,ax3000-tsadc.yaml + AYANEO PLATFORM EC DRIVER M: Antheas Kapenekakis L: platform-driver-x86@vger.kernel.org From 2f312a9d6dab5fafdf5ba3d86d178a2b5734e0fe Mon Sep 17 00:00:00 2001 From: Petar Stepanovic Date: Thu, 3 Sep 2026 00:09:48 -0700 Subject: [PATCH 44/97] hwmon: Add driver for AX3000/AX3005 TSADC Add a hwmon driver for the temperature sensor ADC (TSADC) controller found on Axiado AX3000 and AX3005 SoCs. The controller measures temperature at locations within the SoC silicon die. Each TSADC controller is registered as a separate hwmon device and exposes one read-only temperature channel. The sensor is configured for continuous conversion during probe. The current TSADC hardware does not support threshold or alarm reporting. All TSADC interrupt sources are masked during initialization, and only the current temperature is exposed through temp1_input. The averaged 12-bit ADC code is converted to millidegrees Celsius using a lookup table with linear interpolation between adjacent entries. Raw values outside the characterized lookup-table range are clamped to the nearest supported temperature. Signed-off-by: Petar Stepanovic Link: https://patch.msgid.link/20260903-axiado-ax3000-tsadc-v1-2-a8a0f3b251f5@axiado.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/axiado-tsadc.rst | 41 ++++ Documentation/hwmon/index.rst | 1 + MAINTAINERS | 2 + drivers/hwmon/Kconfig | 11 ++ drivers/hwmon/Makefile | 1 + drivers/hwmon/axiado-tsadc.c | 275 +++++++++++++++++++++++++++ 6 files changed, 331 insertions(+) create mode 100644 Documentation/hwmon/axiado-tsadc.rst create mode 100644 drivers/hwmon/axiado-tsadc.c diff --git a/Documentation/hwmon/axiado-tsadc.rst b/Documentation/hwmon/axiado-tsadc.rst new file mode 100644 index 000000000000..b8fd0aeacc1f --- /dev/null +++ b/Documentation/hwmon/axiado-tsadc.rst @@ -0,0 +1,41 @@ +.. SPDX-License-Identifier: GPL-2.0 + +Kernel driver axiado-tsadc +========================== + +Supported chips: + + * Axiado AX3000 and AX3005 on-chip temperature sensor (TSADC) + + Prefix: 'axiado_tsadc' + + Addresses scanned: - + + Datasheet: Not publicly available + +Authors: + + - Petar Stepanovic + +Description +----------- + +This driver supports the Temperature Sensor ADC (TSADC) block found on Axiado +AX3000 and AX3005 SoCs. The block monitors the on-die temperature of the SoC. + +An SoC may instantiate several TSADC blocks at different locations on the die. +Each block is described by its own device tree node and is registered as a +separate hwmon device with a single temperature channel. + +The sensor is configured for continuous conversion at probe time, averaging +16 samples per conversion. The averaged 12-bit ADC code is converted to +millidegrees Celsius using a lookup table with linear interpolation between +entries. The table covers -40 to +125 degrees Celsius; readings outside that +range are clamped to the nearest supported temperature. + +sysfs entries +------------- + +======================= ======================================================= +temp1_input Die temperature in millidegrees Celsius (read-only) +======================= ======================================================= diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst index f07977a20271..7ab9cfe71cee 100644 --- a/Documentation/hwmon/index.rst +++ b/Documentation/hwmon/index.rst @@ -51,6 +51,7 @@ Hardware Monitoring Kernel Drivers asus_ec_sensors asus_rog_ryujin asus_wmi_sensors + axiado-tsadc bcm54140 bel-pfe bpa-rs600 diff --git a/MAINTAINERS b/MAINTAINERS index 83f50b33e607..badd2a107cf7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4515,6 +4515,8 @@ M: Prasad Bolisetty L: linux-hwmon@vger.kernel.org S: Supported F: Documentation/devicetree/bindings/hwmon/axiado,ax3000-tsadc.yaml +F: Documentation/hwmon/axiado-tsadc.rst +F: drivers/hwmon/axiado-tsadc.c AYANEO PLATFORM EC DRIVER M: Antheas Kapenekakis diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index c1d72af52e8f..4febde8ff43f 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -311,6 +311,17 @@ config SENSORS_AXI_FAN_CONTROL This driver can also be built as a module. If so, the module will be called axi-fan-control +config SENSORS_AXIADO_TSADC + tristate "Axiado AX3000 TSADC temperature sensor" + depends on ARCH_AXIADO || COMPILE_TEST + help + If you say yes here you get support for the on-chip temperature + sensor (TSADC) found on Axiado AX3000 and AX3005 SoCs. The die + temperature is reported through the hwmon sysfs interface. + + This driver can also be built as a module. If so, the module + will be called axiado-tsadc. + config SENSORS_K8TEMP tristate "AMD Athlon64/FX or Opteron temperature sensor" depends on X86 && PCI diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 0243cb62c058..f84f9df27466 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -60,6 +60,7 @@ obj-$(CONFIG_SENSORS_ASPEED_G6) += aspeed-g6-pwm-tach.o obj-$(CONFIG_SENSORS_ASUS_ROG_RYUJIN) += asus_rog_ryujin.o obj-$(CONFIG_SENSORS_ATXP1) += atxp1.o obj-$(CONFIG_SENSORS_AXI_FAN_CONTROL) += axi-fan-control.o +obj-$(CONFIG_SENSORS_AXIADO_TSADC) += axiado-tsadc.o obj-$(CONFIG_SENSORS_CGBC) += cgbc-hwmon.o obj-$(CONFIG_SENSORS_CHIPCAP2) += chipcap2.o obj-$(CONFIG_SENSORS_CORETEMP) += coretemp.o diff --git a/drivers/hwmon/axiado-tsadc.c b/drivers/hwmon/axiado-tsadc.c new file mode 100644 index 000000000000..c111df6f0044 --- /dev/null +++ b/drivers/hwmon/axiado-tsadc.c @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2021-2026 Axiado Corporation + * + * Driver for the Axiado on-chip temperature sensor (TSADC) + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* TSADC register offsets */ +#define AXIADO_TSADC_MODE 0x0000 +#define AXIADO_TSADC_FUNC_CTRL 0x0004 +#define AXIADO_TSADC_AVG_DOUT 0x000c +#define AXIADO_TSADC_INTR_MASK 0x0010 +#define AXIADO_TSADC_ANA_REG 0x0020 +#define AXIADO_TSADC_SEL 0x0028 +#define AXIADO_TSADC_TSEN_EN 0x002c +#define AXIADO_TSADC_EN 0x0030 + +/* TSADC mode register: continuous conversion */ +#define AXIADO_TSADC_MODE_CONT 0x2 + +/* + * TSADC functional control register: bit 0 starts the conversion and + * bits [31:16] specify the number of samples. Average over 16 samples. + */ +#define AXIADO_TSADC_FUNC_CTRL_START BIT(0) +#define AXIADO_TSADC_FUNC_CTRL_NUM_SAMPLES GENMASK(31, 16) +#define AXIADO_TSADC_FUNC_CTRL_CLR 0x0 +#define AXIADO_TSADC_FUNC_CTRL_SET \ + (FIELD_PREP(AXIADO_TSADC_FUNC_CTRL_NUM_SAMPLES, 16) | \ + AXIADO_TSADC_FUNC_CTRL_START) + +/* TSADC average output register: 12-bit ADC result */ +#define AXIADO_TSADC_AVG_DOUT_MASK GENMASK(11, 0) + +/* Interrupt mask bits */ +#define AXIADO_TSADC_INTR_BIST_DONE BIT(0) +#define AXIADO_TSADC_INTR_LOOP_DONE BIT(1) +#define AXIADO_TSADC_INTR_THRESHOLD BIT(2) +#define AXIADO_TSADC_INTR_MASK_ALL \ + (AXIADO_TSADC_INTR_BIST_DONE | \ + AXIADO_TSADC_INTR_LOOP_DONE | \ + AXIADO_TSADC_INTR_THRESHOLD) + +/* TSADC analog block configuration required for temperature measurement */ +#define AXIADO_TSADC_ANA_REG_INIT 0x6 + +/* TSADC SEL: select temperature measurement */ +#define AXIADO_TSADC_SEL_TEMP 0x00 + +/* TSADC EN / TSEN_EN field values */ +#define AXIADO_TSADC_DIS 0x00 +#define AXIADO_TSADC_ENA 0x01 + +struct axiado_tsadc { + void __iomem *regs; +}; + +/* + * Lookup table: raw ADC output (12-bit) vs temperature in millidegrees + * Celsius. Raw values decrease as temperature increases, from -40°C + * to 125°C in 5°C steps. + */ +struct axiado_tsadc_point { + u32 raw; + int temp_mc; +}; + +static const struct axiado_tsadc_point axiado_tsadc_table[] = { + { .raw = 2776, .temp_mc = -40000 }, + { .raw = 2748, .temp_mc = -35000 }, + { .raw = 2721, .temp_mc = -30000 }, + { .raw = 2694, .temp_mc = -25000 }, + { .raw = 2667, .temp_mc = -20000 }, + { .raw = 2640, .temp_mc = -15000 }, + { .raw = 2612, .temp_mc = -10000 }, + { .raw = 2584, .temp_mc = -5000 }, + { .raw = 2556, .temp_mc = 0 }, + { .raw = 2528, .temp_mc = 5000 }, + { .raw = 2500, .temp_mc = 10000 }, + { .raw = 2472, .temp_mc = 15000 }, + { .raw = 2444, .temp_mc = 20000 }, + { .raw = 2416, .temp_mc = 25000 }, + { .raw = 2389, .temp_mc = 30000 }, + { .raw = 2362, .temp_mc = 35000 }, + { .raw = 2335, .temp_mc = 40000 }, + { .raw = 2307, .temp_mc = 45000 }, + { .raw = 2279, .temp_mc = 50000 }, + { .raw = 2251, .temp_mc = 55000 }, + { .raw = 2223, .temp_mc = 60000 }, + { .raw = 2195, .temp_mc = 65000 }, + { .raw = 2167, .temp_mc = 70000 }, + { .raw = 2138, .temp_mc = 75000 }, + { .raw = 2110, .temp_mc = 80000 }, + { .raw = 2081, .temp_mc = 85000 }, + { .raw = 2052, .temp_mc = 90000 }, + { .raw = 2024, .temp_mc = 95000 }, + { .raw = 1996, .temp_mc = 100000 }, + { .raw = 1967, .temp_mc = 105000 }, + { .raw = 1938, .temp_mc = 110000 }, + { .raw = 1909, .temp_mc = 115000 }, + { .raw = 1880, .temp_mc = 120000 }, + { .raw = 1852, .temp_mc = 125000 }, +}; + +/* Convert raw ADC code to millidegrees Celsius with linear interpolation. */ +static int axiado_tsadc_raw_to_mc(u32 raw, long *val) +{ + int i, n = ARRAY_SIZE(axiado_tsadc_table); + long num, denom; + + if (raw >= axiado_tsadc_table[0].raw) { + *val = axiado_tsadc_table[0].temp_mc; + return 0; + } + + if (raw <= axiado_tsadc_table[n - 1].raw) { + *val = axiado_tsadc_table[n - 1].temp_mc; + return 0; + } + + /* Find i such that axiado_tsadc_table[i].raw >= raw > axiado_tsadc_table[i+1].raw */ + for (i = 0; i < n - 1; i++) { + if (raw >= axiado_tsadc_table[i + 1].raw) + break; + } + + if (raw == axiado_tsadc_table[i].raw) { + *val = axiado_tsadc_table[i].temp_mc; + return 0; + } + + /* Linear interpolation within the interval */ + num = (long)(axiado_tsadc_table[i + 1].temp_mc - axiado_tsadc_table[i].temp_mc) * + (axiado_tsadc_table[i].raw - raw); + denom = axiado_tsadc_table[i].raw - axiado_tsadc_table[i + 1].raw; + *val = axiado_tsadc_table[i].temp_mc + num / denom; + return 0; +} + +static int axiado_tsadc_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct axiado_tsadc *tsadc = dev_get_drvdata(dev); + u32 raw; + + if (type != hwmon_temp) + return -EOPNOTSUPP; + + switch (attr) { + case hwmon_temp_input: + if (ioread32(tsadc->regs + AXIADO_TSADC_SEL) != AXIADO_TSADC_SEL_TEMP || + ioread32(tsadc->regs + AXIADO_TSADC_EN) != AXIADO_TSADC_ENA) + return -ENODATA; + + raw = ioread32(tsadc->regs + AXIADO_TSADC_AVG_DOUT) & + AXIADO_TSADC_AVG_DOUT_MASK; + + return axiado_tsadc_raw_to_mc(raw, val); + default: + return -EOPNOTSUPP; + } +} + +static umode_t axiado_tsadc_is_visible(const void *data, + enum hwmon_sensor_types type, u32 attr, + int channel) +{ + if (type == hwmon_temp) { + switch (attr) { + case hwmon_temp_input: + return 0444; + default: + break; + } + } + + return 0; +} + +static const struct hwmon_channel_info * const axiado_tsadc_info[] = { + HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT), + NULL +}; + +static const struct hwmon_ops axiado_tsadc_hwmon_ops = { + .is_visible = axiado_tsadc_is_visible, + .read = axiado_tsadc_read, +}; + +static const struct hwmon_chip_info axiado_tsadc_chip_info = { + .ops = &axiado_tsadc_hwmon_ops, + .info = axiado_tsadc_info, +}; + +static void axiado_tsadc_disable(void *data) +{ + struct axiado_tsadc *tsadc = data; + + iowrite32(AXIADO_TSADC_DIS, tsadc->regs + AXIADO_TSADC_TSEN_EN); + iowrite32(AXIADO_TSADC_DIS, tsadc->regs + AXIADO_TSADC_EN); +} + +static int axiado_tsadc_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct axiado_tsadc *tsadc; + struct device *hwmon_dev; + struct clk *refclk; + int ret; + + tsadc = devm_kzalloc(dev, sizeof(*tsadc), GFP_KERNEL); + if (!tsadc) + return -ENOMEM; + + tsadc->regs = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(tsadc->regs)) + return dev_err_probe(dev, PTR_ERR(tsadc->regs), + "failed to map registers\n"); + + refclk = devm_clk_get_enabled(dev, NULL); + if (IS_ERR(refclk)) + return dev_err_probe(dev, PTR_ERR(refclk), + "failed to enable reference clock\n"); + + /* Mask all interrupts before setup */ + iowrite32(AXIADO_TSADC_INTR_MASK_ALL, tsadc->regs + AXIADO_TSADC_INTR_MASK); + + iowrite32(AXIADO_TSADC_ANA_REG_INIT, tsadc->regs + AXIADO_TSADC_ANA_REG); + iowrite32(AXIADO_TSADC_ENA, tsadc->regs + AXIADO_TSADC_EN); + iowrite32(AXIADO_TSADC_SEL_TEMP, tsadc->regs + AXIADO_TSADC_SEL); + iowrite32(AXIADO_TSADC_MODE_CONT, tsadc->regs + AXIADO_TSADC_MODE); + iowrite32(AXIADO_TSADC_FUNC_CTRL_CLR, tsadc->regs + AXIADO_TSADC_FUNC_CTRL); + iowrite32(AXIADO_TSADC_FUNC_CTRL_SET, tsadc->regs + AXIADO_TSADC_FUNC_CTRL); + + /* Enable the bandgap circuit once the block is fully configured */ + iowrite32(AXIADO_TSADC_ENA, tsadc->regs + AXIADO_TSADC_TSEN_EN); + + ret = devm_add_action_or_reset(dev, axiado_tsadc_disable, tsadc); + if (ret) + return ret; + + hwmon_dev = devm_hwmon_device_register_with_info(dev, "axiado_tsadc", + tsadc, + &axiado_tsadc_chip_info, + NULL); + return PTR_ERR_OR_ZERO(hwmon_dev); +} + +static const struct of_device_id axiado_tsadc_match[] = { + { .compatible = "axiado,ax3000-tsadc" }, + { } +}; +MODULE_DEVICE_TABLE(of, axiado_tsadc_match); + +static struct platform_driver axiado_tsadc_driver = { + .driver = { + .name = "axiado-tsadc", + .of_match_table = axiado_tsadc_match, + }, + .probe = axiado_tsadc_probe, +}; +module_platform_driver(axiado_tsadc_driver); + +MODULE_DESCRIPTION("Axiado TSADC temperature sensor driver"); +MODULE_AUTHOR("Axiado Corporation"); +MODULE_LICENSE("GPL"); From 0eff7c35479b359bdd6fb4ddb97c4b3b96606262 Mon Sep 17 00:00:00 2001 From: Massinissa Ghoul Date: Mon, 7 Sep 2026 00:35:02 +0200 Subject: [PATCH 45/97] hwmon: (hih6130) Replace sprintf() with sysfs_emit() Use sysfs_emit() instead of sprintf() in the sysfs show functions hih6130_temperature_show() and hih6130_humidity_show() to use the preferred kernel interface for writing to sysfs buffers. No functional change intended. Note: Not runtime tested due to lack of hardware. Signed-off-by: Massinissa Ghoul Link: https://patch.msgid.link/20260907-hwmon-hih6130-sysfs-emit-v1-1-a1adb5245d0f@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/hih6130.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/hih6130.c b/drivers/hwmon/hih6130.c index 7984be1e706d..6a79aab5d128 100644 --- a/drivers/hwmon/hih6130.c +++ b/drivers/hwmon/hih6130.c @@ -168,7 +168,7 @@ static ssize_t hih6130_temperature_show(struct device *dev, ret = hih6130_update_measurements(dev); if (ret < 0) return ret; - return sprintf(buf, "%d\n", hih6130->temperature); + return sysfs_emit(buf, "%d\n", hih6130->temperature); } /** @@ -189,7 +189,7 @@ static ssize_t hih6130_humidity_show(struct device *dev, ret = hih6130_update_measurements(dev); if (ret < 0) return ret; - return sprintf(buf, "%d\n", hih6130->humidity); + return sysfs_emit(buf, "%d\n", hih6130->humidity); } /* sysfs attributes */ From 6c2437800da2f0b19b06990e60f4828cde83f040 Mon Sep 17 00:00:00 2001 From: Sergio Melas Date: Thu, 10 Sep 2026 20:43:22 +0200 Subject: [PATCH 46/97] Documentation: hwmon: (yogafan) Update model reference table and contributors Add the new IdeaPad 3 15ALC6 Ub model and Yoga 740-15IML, update existing model entries, include corresponding contributors, and adjust the reStructuredText table layout and column separators to accommodate the longer strings and prevent Sphinx build warnings. Signed-off-by: Sergio Melas Link: https://patch.msgid.link/20260910184322.1816996-1-sergiomelas@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/yogafan.rst | 47 ++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/Documentation/hwmon/yogafan.rst b/Documentation/hwmon/yogafan.rst index 9ff5db5dc08c..239c7b5761ca 100644 --- a/Documentation/hwmon/yogafan.rst +++ b/Documentation/hwmon/yogafan.rst @@ -85,28 +85,30 @@ immediately to ensure the user knows the fan has stopped. :: - MODEL (DMI PN) | FAMILY / SERIES | EC OFFSET | FULL ACPI OBJECT PATH | WIDTH | MULTiplier - ---------------------------------------------------------------------------------------------------- - 82N7 | Yoga 14cACN | 0x06 | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 - 80V2 / 81C3 | Yoga 710/720 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 - 83E2 | Yoga Pro 7 | 0xFE | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 - 83DN | Yoga Pro 9 16IMH9 | 0x06/0xFE | \_SB.PC00.LPCB.EC0.FANS/FA2S | 8-bit | 100 - 82A2 / 82A3 | Yoga Slim 7 | 0x06 | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 - 81YM / 82FG | IdeaPad 5 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 - 82JW / 82JU | Legion 5 (AMD) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS (Fan1) | 16-bit | 1 - 82JW / 82JU | Legion 5 (AMD) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FA2S (Fan2) | 16-bit | 1 - 82WQ | Legion 7i (Int) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS (Fan1) | 16-bit | 1 - 82WQ | Legion 7i (Int) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FA2S (Fan2) | 16-bit | 1 - 82XV / 83DV | LOQ 15/16 | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS /FA2S | 16-bit | 1 - 83AK | ThinkBook G6 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 - 81X1 | Flex 5 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 - 83KF | XiaoXinPro 13ARE | 0x06/0xFE | \_SB.PCI0.LPC0.EC0.FANS/FA2S | 8-bit | 100 - 82KU | IdeaPad 3 15ALC6 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 - 83RU | Legion Pro 7 16 | 0x03/0x06 | \_SB.PCI0.LPC0.EC0.FANS/FA2S | 8-bit | 100 - 83KF | Yoga Pro 7 14IAH | 0x06 | \_SB.PC00.LPCB.EC0.FANS | 8-bit | 100 - 83BS | Yoga 7 16ARP8 | 0x03/0x06 | \_SB.PCI0.LPC0.EC0.FANS/FA2S | 8-bit | 100 - *Legacy* | Pre-2020 Models | 0x06 | \_SB.PCI0.LPC.EC.FAN0 | 8-bit | 100 - ---------------------------------------------------------------------------------------------------- + MODEL (DMI PN) | FAMILY / SERIES | EC OFFSET | FULL ACPI OBJECT PATH | WIDTH | MULTiplier + ------------------------------------------------------------------------------------------------------- + 82N7 | Yoga 14cACN 2021 | 0x06 | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 + 81TD | Yoga 740-15IML | 0x06 | \_SB.PCI0.LPCB.EC0.FANS | 8-bit | 100 + 82KU | IdeaPad 3 15ALC6 Ub | 0x06 | \_SB.PCI0.LPC0.EC0.FANS/FA2S | 8-bit | 100 + 80V2 / 81C3 | Yoga 710/720 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 + 83E2 | Yoga Pro 7 | 0xFE | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 + 83DN | Yoga Pro 9 16IMH9 | 0x06/0xFE | \_SB.PC00.LPCB.EC0.FANS/FA2S | 8-bit | 100 + 82A2 / 82A3 | Yoga Slim 7 | 0x06 | \_SB.PCI0.LPC0.EC0.FANS | 8-bit | 100 + 81YM / 82FG | IdeaPad 5 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 + 82JW / 82JU | Legion 5 (AMD) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS (Fan1) | 16-bit | 1 + 82JW / 82JU | Legion 5 (AMD) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FA2S (Fan2) | 16-bit | 1 + 82WQ | Legion 7i (Int) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS (Fan1) | 16-bit | 1 + 82WQ | Legion 7i (Int) | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FA2S (Fan2) | 16-bit | 1 + 82XV / 83DV | LOQ 15/16 | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS /FA2S | 16-bit | 1 + 83AK | ThinkBook G6 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 + 81X1 | Flex 5 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 + 83KF | XiaoXinPro 13ARE | 0x06/0xFE | \_SB.PCI0.LPC0.EC0.FANS/FA2S | 8-bit | 100 + 82KU | IdeaPad 3 15ALC6 | 0x06 | \_SB.PCI0.LPC0.EC0.FAN0 | 8-bit | 100 + 83RU | Legion Pro 7 16 | 0x03/0x06 | \_SB.PCI0.LPC0.EC0.FANS/FA2S | 8-bit | 100 + 83KF | Yoga Pro 7 14IAH | 0x06 | \_SB.PC00.LPCB.EC0.FANS | 8-bit | 100 + 83BS | Yoga 7 16ARP8 | 0x03/0x06 | \_SB.PCI0.LPC0.EC0.FANS/FA2S | 8-bit | 100 + *Legacy* | Pre-2020 Models | 0x06 | \_SB.PCI0.LPC.EC.FAN0 | 8-bit | 100 + ------------------------------------------------------------------------------------------------------- METHODOLOGY & IDENTIFICATION: @@ -159,6 +161,7 @@ Contributors & DSDT Providers: - **Sarbajit Sarkar** (Lenovo LOQ 15IAX9) - **HinataKato** (XiaoXin Pro 13ARE 2020 - 83KF) - **PenPenIsGod** (IdeaPad 3 15ALC6 - 82KU & Legion Pro 7 16AFR10H - 83RU) +- **wizard-28** (IdeaPad 3 15ALC6 Ub - 82KU) - **unlockxiaom** (Legion Pro 7 16AFR10H - 83RU, Yoga Pro 7 14IAH10 - 83KF, ThinkCentre M80q) - **Phani Pavan K** (Yoga Pro 7 14IAH10 - 83KF) - **Splarkszter** (Yoga 7 16ARP8 - 83BS) From d17832c1c97b0a7af780ac7d500b5412d9edabd1 Mon Sep 17 00:00:00 2001 From: Aureo Serrano de Souza Date: Thu, 10 Sep 2026 13:03:59 +0800 Subject: [PATCH 47/97] hwmon: (arctic_fan_controller) Default PWM cache to MCU 40% The device has no GET_REPORT and every OUT report carries all 10 channels, so the cache has to start at some value. Starting at 0 means the first sysfs write to a single channel also sends 0% on the other nine. The MCU factory default is 40%. Initialize pwm_duty[] to 102 (40% on the 0-255 sysfs scale) at probe and on reset-resume. Any initial cache can be stale if the module is reloaded without a device reset. 40% matches the hardware after power-on or power-loss resume, and a first single-channel write leaves the fans running at a safe speed. PWM is still not taken from periodic IN reports: the device is manual-only and the host cache stays authoritative after the first successful write. Signed-off-by: Aureo Serrano de Souza Link: https://patch.msgid.link/20260910050401.279868-2-aureo.serrano@arctic.de Signed-off-by: Guenter Roeck --- Documentation/hwmon/arctic_fan_controller.rst | 24 +++++++++---------- drivers/hwmon/arctic_fan_controller.c | 18 ++++++++------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Documentation/hwmon/arctic_fan_controller.rst b/Documentation/hwmon/arctic_fan_controller.rst index b5be88ae464d..200e51932a19 100644 --- a/Documentation/hwmon/arctic_fan_controller.rst +++ b/Documentation/hwmon/arctic_fan_controller.rst @@ -29,18 +29,16 @@ Usage notes Since it is a USB device, hotplug is supported. The device is autodetected. The device does not support GET_REPORT, so the driver cannot read back the -current hardware PWM state at probe time. The cached PWM values (readable -via pwm[1-10]) start at 0 and reflect only values that have been -successfully written. Because each OUT report carries all 10 channel values, -writing a single channel also sends the cached values for all other channels. -Users should set all channels to the desired values before relying on the -cached state. +current hardware PWM state at probe time. Each OUT report carries all 10 +channels, so pwm[1-10] is a host cache. It starts at the MCU factory +default of 40% (sysfs 102). After a successful write, the cache reflects +that value. Users should set all channels to the desired values before +relying on the cached state. -On system suspend, the device may lose power and reset its PWM channels to -hardware defaults. The driver clears its cached duty values on resume so -that reads reflect the unknown hardware state rather than stale pre-suspend -values. Userspace is responsible for re-applying the desired duty cycles -after resume. +On system suspend, the device may lose power and reset PWM to the factory +default. The driver restores the cache to 40% on resume. If the device +kept power across suspend, userspace should re-apply the desired duty +cycles. Sysfs entries ------------- @@ -51,6 +49,6 @@ pwm[1-10] PWM duty cycle (0-255). Write: sends an OUT report setting the duty cycle (scaled from 0-255 to 0-100% for the device); the cached value is updated only after the device ACKs the command with a success status. Read: returns the last - successfully written value; initialized to 0 at driver load - and after resume (hardware state unknown). + successfully written value; initialized to 102 (40%) at + driver load and after resume (MCU factory default). ================ ============================================================== diff --git a/drivers/hwmon/arctic_fan_controller.c b/drivers/hwmon/arctic_fan_controller.c index dbe84cd93c08..9a6092572736 100644 --- a/drivers/hwmon/arctic_fan_controller.c +++ b/drivers/hwmon/arctic_fan_controller.c @@ -35,6 +35,8 @@ * Measured over 500 iterations: max ~563 ms. Keep 1 s as margin. */ #define ARCTIC_ACK_TIMEOUT_MS 1000 +/* MCU factory default; 40% of 0-255 is 102. */ +#define ARCTIC_PWM_DEFAULT 102 struct arctic_fan_data { struct hid_device *hdev; @@ -164,7 +166,7 @@ static int arctic_fan_write(struct device *dev, enum hwmon_sensor_types type, /* * Build the buffer and arm write_pending under in_report_lock so that - * reset_resume() cannot clear pwm_duty[] between the pwm_duty[] read + * reset_resume() cannot replace pwm_duty[] between the pwm_duty[] read * and the buffer write, and raw_event() cannot deliver a stale ACK * from a previous write into this write's completion. * @@ -256,14 +258,14 @@ static int arctic_fan_reset_resume(struct hid_device *hdev) unsigned long flags; /* - * The device resets its PWM channels to hardware defaults on power - * loss during suspend. Clear the cached duty values so they reflect - * the unknown hardware state, consistent with probe-time behaviour - * (the device has no GET_REPORT support). Hold in_report_lock so - * this does not race with a concurrent pwm read or write callback. + * The device resets its PWM channels to the MCU factory default + * (40%) on power loss during suspend. Restore the cache to that + * same default, consistent with probe-time behaviour (the device + * has no GET_REPORT support). Hold in_report_lock so this does + * not race with a concurrent pwm read or write callback. */ spin_lock_irqsave(&priv->in_report_lock, flags); - memset(priv->pwm_duty, 0, sizeof(priv->pwm_duty)); + memset(priv->pwm_duty, ARCTIC_PWM_DEFAULT, sizeof(priv->pwm_duty)); spin_unlock_irqrestore(&priv->in_report_lock, flags); return 0; } @@ -288,6 +290,8 @@ static int arctic_fan_probe(struct hid_device *hdev, priv->hdev = hdev; spin_lock_init(&priv->in_report_lock); init_completion(&priv->in_report_received); + /* Same MCU factory default as reset_resume(); see ARCTIC_PWM_DEFAULT above. */ + memset(priv->pwm_duty, ARCTIC_PWM_DEFAULT, sizeof(priv->pwm_duty)); hid_set_drvdata(hdev, priv); ret = hid_hw_start(hdev, HID_CONNECT_DRIVER); From a6a3622139981f84d600d855d26a55794af29ee5 Mon Sep 17 00:00:00 2001 From: Aureo Serrano de Souza Date: Thu, 10 Sep 2026 13:04:00 +0800 Subject: [PATCH 48/97] hwmon: (arctic_fan_controller) Dual-license GPL-2.0-or-later OR BSD-2-Clause The in-tree driver is solely authored by ARCTIC. Keep the existing GPL-2.0-or-later grant and add BSD-2-Clause so the files may be used under either license. Signed-off-by: Aureo Serrano de Souza Link: https://patch.msgid.link/20260910050401.279868-3-aureo.serrano@arctic.de Signed-off-by: Guenter Roeck --- Documentation/hwmon/arctic_fan_controller.rst | 2 +- drivers/hwmon/arctic_fan_controller.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Documentation/hwmon/arctic_fan_controller.rst b/Documentation/hwmon/arctic_fan_controller.rst index 200e51932a19..60ac09a9cea3 100644 --- a/Documentation/hwmon/arctic_fan_controller.rst +++ b/Documentation/hwmon/arctic_fan_controller.rst @@ -1,4 +1,4 @@ -.. SPDX-License-Identifier: GPL-2.0-or-later +.. SPDX-License-Identifier: GPL-2.0-or-later OR BSD-2-Clause Kernel driver arctic_fan_controller ===================================== diff --git a/drivers/hwmon/arctic_fan_controller.c b/drivers/hwmon/arctic_fan_controller.c index 9a6092572736..9eb83a149cd6 100644 --- a/drivers/hwmon/arctic_fan_controller.c +++ b/drivers/hwmon/arctic_fan_controller.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later OR BSD-2-Clause /* * Linux hwmon driver for ARCTIC Fan Controller * @@ -375,4 +375,4 @@ module_hid_driver(arctic_fan_driver); MODULE_AUTHOR("Aureo Serrano de Souza "); MODULE_DESCRIPTION("HID hwmon driver for ARCTIC Fan Controller"); -MODULE_LICENSE("GPL"); +MODULE_LICENSE("Dual BSD/GPL"); From 071b2c36ea14a43e2fd096f2b2bbe4581e69d36b Mon Sep 17 00:00:00 2001 From: Aureo Serrano de Souza Date: Thu, 10 Sep 2026 13:04:01 +0800 Subject: [PATCH 49/97] hwmon: (arctic_fan_controller) Use shared maintainer address Route driver maintenance mail to the shared ARCTIC (HK) alias instead of a personal mailbox. Signed-off-by: Aureo Serrano de Souza Link: https://patch.msgid.link/20260910050401.279868-4-aureo.serrano@arctic.de Signed-off-by: Guenter Roeck --- MAINTAINERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index badd2a107cf7..5304761819d9 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2177,7 +2177,7 @@ F: drivers/net/arcnet/ F: include/uapi/linux/if_arcnet.h ARCTIC FAN CONTROLLER DRIVER -M: Aureo Serrano de Souza +M: ARCTIC (HK) L: linux-hwmon@vger.kernel.org S: Maintained F: Documentation/hwmon/arctic_fan_controller.rst From 58b824e586309990909b64d0aab0a2e5b29976a9 Mon Sep 17 00:00:00 2001 From: Paul Louvel Date: Thu, 10 Sep 2026 09:02:38 +0200 Subject: [PATCH 50/97] Documentation: hwmon: (nct6775) Document NCT6116D/NCT6122D/NCT6126D support The NCT6112D/NCT6114D/NCT6116D family has been supported by the driver since commit 29c7cb485b32 ("hwmon: (nct6775) Integrate new model nct6116"), but was never added to the list of supported chips in the driver documentation. Also add the NCT6122D/NCT6126D. Assisted-by: opencode:z-ai/glm-5.3-flash Signed-off-by: Paul Louvel Link: https://patch.msgid.link/20260910-bootlin-hwmon-nct6126-v2-1-ee629ab35e49@bootlin.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/nct6775.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Documentation/hwmon/nct6775.rst b/Documentation/hwmon/nct6775.rst index 5199c083b9e7..d2dedade0606 100644 --- a/Documentation/hwmon/nct6775.rst +++ b/Documentation/hwmon/nct6775.rst @@ -32,6 +32,22 @@ Supported chips: Datasheet: Available from the Nuvoton web site + * Nuvoton NCT6112D/NCT6114D/NCT6116D + + Prefix: 'nct6116' + + Addresses scanned: ISA address retrieved from Super I/O registers + + Datasheet: Available from the Nuvoton web site + + * Nuvoton NCT6122D/NCT6126D + + Prefix: 'nct6126' + + Addresses scanned: ISA address retrieved from Super I/O registers + + Datasheet: Available from the Nuvoton web site + * Nuvoton NCT5572D/NCT6771F/NCT6772F/NCT6775F/W83677HG-I Prefix: 'nct6775' From 437b3c2a4bdcd662b0f73b61816e19613bf85908 Mon Sep 17 00:00:00 2001 From: Paul Louvel Date: Thu, 10 Sep 2026 09:02:39 +0200 Subject: [PATCH 51/97] hwmon: (nct6775) Add support for NCT6126D Add support for the hardware monitoring functionality of the NCT6126D Super-I/O chip. The NCT6126D hardware monitor is nearly identical to the NCT6116D: the fan, PWM, temperature, TSI, alarm. The differences are: - Device IDs 0xd283 (A version) and 0xd284 (B version), which both fall into the NCT6116D ID range (0xd280) when masked with SIO_ID_MASK. Instead, compare the full value to distinguish from NCT6116D. - One additional VIN: VHIF scaled at 16 mV/LSB. - The fan count limit registers are documented for all five fans. Also expose fan4_min. Assisted-by: opencode:z-ai/glm-5.3-flash Signed-off-by: Paul Louvel Link: https://patch.msgid.link/20260910-bootlin-hwmon-nct6126-v2-2-ee629ab35e49@bootlin.com Signed-off-by: Guenter Roeck --- drivers/hwmon/nct6775-core.c | 116 +++++++++++++++++++++++++++++++ drivers/hwmon/nct6775-platform.c | 29 +++++++- drivers/hwmon/nct6775.h | 4 +- 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/drivers/hwmon/nct6775-core.c b/drivers/hwmon/nct6775-core.c index 759ea3d1286e..cf27b9c7a89e 100644 --- a/drivers/hwmon/nct6775-core.c +++ b/drivers/hwmon/nct6775-core.c @@ -21,6 +21,8 @@ * Chip #vin #fan #pwm #temp chip IDs man ID * nct6106d 9 3 3 6+3 0xc450 0xc1 0x5ca3 * nct6116d 9 5 5 3+3 0xd280 0xc1 0x5ca3 + * nct6126d 10 5 5 3+3 0xd283 0xc1 0x5ca3 + * (0xd284) * nct6775f 9 4 3 6+3 0xb470 0xc1 0x5ca3 * nct6776f 9 5 3 6+3 0xc330 0xc1 0x5ca3 * nct6779d 15 5 5 2+6 0xc560 0xc1 0x5ca3 @@ -66,6 +68,7 @@ static const char * const nct6775_device_names[] = { [nct6106] = "nct6106", [nct6116] = "nct6116", + [nct6126] = "nct6126", [nct6775] = "nct6775", [nct6776] = "nct6776", [nct6779] = "nct6779", @@ -897,6 +900,31 @@ static const s8 NCT6116_BEEP_BITS[NUM_BEEP_BITS] = { static const u16 NCT6116_REG_TSI_TEMP[] = { 0x59, 0x5b }; +/* NCT6122D/NCT6126D specific data */ + +static const u16 NCT6126_REG_IN[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 }; +static const u16 NCT6126_REG_IN_MAX[] = { + 0x90, 0x92, 0x94, 0x96, 0x98, 0x9a, 0x9c, 0x9e, 0xa0, 0xa2 }; +static const u16 NCT6126_REG_IN_MIN[] = { + 0x91, 0x93, 0x95, 0x97, 0x99, 0x9b, 0x9d, 0x9f, 0xa1, 0xa3 }; + +static const s8 NCT6126_ALARM_BITS[NUM_ALARM_BITS] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, /* in0-in11 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* in12-in23 */ + 32, 33, 34, 35, 36, -1, -1, -1, -1, -1, -1, -1, /* fan1-fan12 */ + 16, 17, 18, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* temp1-temp12 */ + 48, -1, /* intr0-intr1 */ +}; + +static const s8 NCT6126_BEEP_BITS[NUM_BEEP_BITS] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, /* in0-in11 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* in12-in23 */ + 24, 25, 26, 27, 28, -1, -1, -1, -1, -1, -1, -1, /* fan1-fan12 */ + 16, 17, 18, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* temp1-temp12 */ + 34, -1, 32 /* intr0-intr1, beep_en */ +}; + static enum pwm_enable reg_to_pwm_enable(int pwm, int mode) { if (mode == 0 && pwm == 255) @@ -998,6 +1026,14 @@ static const u16 scale_in_6798[NUM_IN] = { 800, 800, 800, 1600, 800 }; +/* + * NCT6126 scaling: + * CPUVCORE, VIN0, AVSB, 3VCC, VIN1, VIN2, VHIF, 3VSB, VBAT, VTT + */ +static const u16 scale_in_6126[10] = { + 800, 800, 1600, 1600, 800, 800, 1600, 1600, 1600, 800 +}; + static inline long in_from_reg(u8 reg, u8 nr, const u16 *scales) { return DIV_ROUND_CLOSEST(reg * scales[nr], 100); @@ -1156,6 +1192,7 @@ bool nct6775_reg_is_word_sized(struct nct6775_data *data, u16 reg) reg == 0xe0 || reg == 0xe2 || reg == 0xe4 || reg == 0x111 || reg == 0x121 || reg == 0x131; case nct6116: + case nct6126: return reg == 0x20 || reg == 0x22 || reg == 0x24 || reg == 0x26 || reg == 0x28 || reg == 0x59 || reg == 0x5b || reg == 0xe0 || reg == 0xe2 || reg == 0xe4 || reg == 0xe6 || @@ -1538,6 +1575,7 @@ static int nct6775_update_pwm_limits(struct device *dev) break; case nct6106: case nct6116: + case nct6126: case nct6779: case nct6791: case nct6792: @@ -3203,6 +3241,7 @@ store_auto_pwm(struct device *dev, struct device_attribute *attr, break; /* always enabled, nothing to do */ case nct6106: case nct6116: + case nct6126: case nct6779: case nct6791: case nct6792: @@ -3677,6 +3716,83 @@ int nct6775_probe(struct device *dev, struct nct6775_data *data, reg_temp_crit_l = NCT6106_REG_TEMP_CRIT_L; reg_temp_crit_h = NCT6106_REG_TEMP_CRIT_H; + break; + case nct6126: + data->in_num = 10; + data->scale_in = scale_in_6126; + data->pwm_num = 5; + data->auto_pwm_num = 4; + data->temp_fixed_num = 3; + data->num_temp_alarms = 3; + data->num_temp_beeps = 3; + + data->fan_from_reg = fan_from_reg13; + data->fan_from_reg_min = fan_from_reg13; + + data->temp_label = nct6776_temp_label; + data->temp_mask = NCT6776_TEMP_MASK; + data->virt_temp_mask = NCT6776_VIRT_TEMP_MASK; + + data->REG_VBAT = NCT6106_REG_VBAT; + data->REG_DIODE = NCT6106_REG_DIODE; + data->DIODE_MASK = NCT6106_DIODE_MASK; + data->REG_VIN = NCT6126_REG_IN; + data->REG_IN_MINMAX[0] = NCT6126_REG_IN_MIN; + data->REG_IN_MINMAX[1] = NCT6126_REG_IN_MAX; + data->REG_TARGET = NCT6116_REG_TARGET; + data->REG_FAN = NCT6116_REG_FAN; + data->REG_FAN_MODE = NCT6116_REG_FAN_MODE; + data->REG_FAN_MIN = NCT6116_REG_FAN_MIN; + data->REG_FAN_PULSES = NCT6116_REG_FAN_PULSES; + data->FAN_PULSE_SHIFT = NCT6116_FAN_PULSE_SHIFT; + data->REG_FAN_TIME[0] = NCT6116_REG_FAN_STOP_TIME; + data->REG_FAN_TIME[1] = NCT6116_REG_FAN_STEP_UP_TIME; + data->REG_FAN_TIME[2] = NCT6116_REG_FAN_STEP_DOWN_TIME; + data->REG_TOLERANCE_H = NCT6116_REG_TOLERANCE_H; + data->REG_PWM[0] = NCT6116_REG_PWM; + data->REG_PWM[1] = NCT6116_REG_FAN_START_OUTPUT; + data->REG_PWM[2] = NCT6116_REG_FAN_STOP_OUTPUT; + data->REG_PWM[5] = NCT6106_REG_WEIGHT_DUTY_STEP; + data->REG_PWM[6] = NCT6106_REG_WEIGHT_DUTY_BASE; + data->REG_PWM_READ = NCT6106_REG_PWM_READ; + data->REG_PWM_MODE = NCT6106_REG_PWM_MODE; + data->PWM_MODE_MASK = NCT6106_PWM_MODE_MASK; + data->REG_AUTO_TEMP = NCT6116_REG_AUTO_TEMP; + data->REG_AUTO_PWM = NCT6116_REG_AUTO_PWM; + data->REG_CRITICAL_TEMP = NCT6116_REG_CRITICAL_TEMP; + data->REG_CRITICAL_TEMP_TOLERANCE = + NCT6116_REG_CRITICAL_TEMP_TOLERANCE; + data->REG_CRITICAL_PWM_ENABLE = NCT6116_REG_CRITICAL_PWM_ENABLE; + data->CRITICAL_PWM_ENABLE_MASK = + NCT6106_CRITICAL_PWM_ENABLE_MASK; + data->REG_CRITICAL_PWM = NCT6116_REG_CRITICAL_PWM; + data->REG_TEMP_OFFSET = NCT6106_REG_TEMP_OFFSET; + data->REG_TEMP_SOURCE = NCT6106_REG_TEMP_SOURCE; + data->REG_TEMP_SEL = NCT6116_REG_TEMP_SEL; + data->REG_WEIGHT_TEMP_SEL = NCT6106_REG_WEIGHT_TEMP_SEL; + data->REG_WEIGHT_TEMP[0] = NCT6106_REG_WEIGHT_TEMP_STEP; + data->REG_WEIGHT_TEMP[1] = NCT6106_REG_WEIGHT_TEMP_STEP_TOL; + data->REG_WEIGHT_TEMP[2] = NCT6106_REG_WEIGHT_TEMP_BASE; + data->REG_ALARM = NCT6106_REG_ALARM; + data->ALARM_BITS = NCT6126_ALARM_BITS; + data->REG_BEEP = NCT6106_REG_BEEP; + data->BEEP_BITS = NCT6126_BEEP_BITS; + data->REG_TSI_TEMP = NCT6116_REG_TSI_TEMP; + + reg_temp = NCT6106_REG_TEMP; + reg_temp_mon = NCT6106_REG_TEMP_MON; + num_reg_temp = 3; + num_reg_temp_mon = ARRAY_SIZE(NCT6106_REG_TEMP_MON); + num_reg_tsi_temp = ARRAY_SIZE(NCT6116_REG_TSI_TEMP); + reg_temp_over = NCT6106_REG_TEMP_OVER; + reg_temp_hyst = NCT6106_REG_TEMP_HYST; + reg_temp_config = NCT6106_REG_TEMP_CONFIG; + num_reg_temp_config = 3; + reg_temp_alternate = NCT6106_REG_TEMP_ALTERNATE; + reg_temp_crit = NCT6106_REG_TEMP_CRIT; + reg_temp_crit_l = NCT6106_REG_TEMP_CRIT_L; + reg_temp_crit_h = NCT6106_REG_TEMP_CRIT_H; + break; case nct6775: data->in_num = 9; diff --git a/drivers/hwmon/nct6775-platform.c b/drivers/hwmon/nct6775-platform.c index d394376e4306..a8ef58d3a883 100644 --- a/drivers/hwmon/nct6775-platform.c +++ b/drivers/hwmon/nct6775-platform.c @@ -25,6 +25,7 @@ enum sensor_access { access_direct, access_asuswmi }; static const char * const nct6775_sio_names[] __initconst = { [nct6106] = "NCT6106D", [nct6116] = "NCT6116D", + [nct6126] = "NCT6126D", [nct6775] = "NCT6775F", [nct6776] = "NCT6776D/F", [nct6779] = "NCT6779D", @@ -76,6 +77,8 @@ MODULE_PARM_DESC(fan_debounce, "Enable debouncing for fan RPM signal"); #define SIO_NCT6106_ID 0xc450 #define SIO_NCT6116_ID 0xd280 +#define SIO_NCT6126_A_ID 0xd283 +#define SIO_NCT6126_B_ID 0xd284 #define SIO_NCT6775_ID 0xb470 #define SIO_NCT6776_ID 0xc330 #define SIO_NCT6779_ID 0xc560 @@ -565,6 +568,25 @@ nct6775_check_fan_inputs(struct nct6775_data *data, struct nct6775_sio_data *sio pwm3pin = fan3pin && (cr24 & 0x08); pwm4pin = fan4pin; pwm5pin = fan5pin; + } else if (data->kind == nct6126) { + int cr1a = sio_data->sio_inb(sio_data, 0x1a); + int cr1b = sio_data->sio_inb(sio_data, 0x1b); + int cr24 = sio_data->sio_inb(sio_data, 0x24); + int cr2a = sio_data->sio_inb(sio_data, 0x2a); + int cr2b = sio_data->sio_inb(sio_data, 0x2b); + int cr2f = sio_data->sio_inb(sio_data, 0x2f); + + fan3pin = !(cr2b & 0x10); + fan4pin = (cr2b & 0x80) || /* pin 1(2) */ + (!(cr2f & 0x10) && (cr1a & 0x04)); /* pin 65(66) */ + fan5pin = (cr2b & 0x80) || /* pin 126(127) */ + (!(cr1b & 0x03) && (cr2a & 0x02)); /* pin 94(96) */ + + pwm3pin = fan3pin && (cr24 & 0x08); + pwm4pin = fan4pin; + pwm5pin = fan5pin; + + fan4min = fan4pin; } else { /* * NCT6779D, NCT6791D, NCT6792D, NCT6793D, NCT6795D, NCT6796D, @@ -861,6 +883,7 @@ static int nct6775_platform_probe_init(struct nct6775_data *data) break; case nct6106: case nct6116: + case nct6126: case nct6779: case nct6791: case nct6792: @@ -892,6 +915,7 @@ static int nct6775_platform_probe_init(struct nct6775_data *data) switch (data->kind) { case nct6106: case nct6116: + case nct6126: tmp |= 0xe0; break; case nct6775: @@ -1006,7 +1030,10 @@ static int __init nct6775_find(int sioaddr, struct nct6775_sio_data *sio_data) sio_data->kind = nct6106; break; case SIO_NCT6116_ID: - sio_data->kind = nct6116; + if (val == SIO_NCT6126_A_ID || val == SIO_NCT6126_B_ID) + sio_data->kind = nct6126; + else + sio_data->kind = nct6116; break; case SIO_NCT6775_ID: sio_data->kind = nct6775; diff --git a/drivers/hwmon/nct6775.h b/drivers/hwmon/nct6775.h index 296eff99d003..d18ac91be497 100644 --- a/drivers/hwmon/nct6775.h +++ b/drivers/hwmon/nct6775.h @@ -4,8 +4,8 @@ #include -enum kinds { nct6106, nct6116, nct6775, nct6776, nct6779, nct6791, nct6792, - nct6793, nct6795, nct6796, nct6797, nct6798, nct6799 }; +enum kinds { nct6106, nct6116, nct6126, nct6775, nct6776, nct6779, nct6791, + nct6792, nct6793, nct6795, nct6796, nct6797, nct6798, nct6799 }; enum pwm_enable { off, manual, thermal_cruise, speed_cruise, sf3, sf4 }; #define NUM_TEMP 12 /* Max number of temp attribute sets w/ limits*/ From 5a6ebf382ae02093fb98131dc588364e5bd59fee Mon Sep 17 00:00:00 2001 From: Paul Louvel Date: Wed, 9 Sep 2026 20:25:56 +0200 Subject: [PATCH 52/97] hwmon: (nct6775) Add support for NCT6122D The NCT6122D hardware monitor is the same as the NCT6126D, the only difference if the number of UARTs: two instead of six. Add it to the driver using the nct6126 chip kind to avoid code duplication. Assisted-by: opencode:z-ai/glm-5.3-flash Signed-off-by: Paul Louvel Link: https://patch.msgid.link/20260909-bootlin-hwmon-nct6126-v1-3-4e85615fef10@bootlin.com Signed-off-by: Guenter Roeck --- drivers/hwmon/nct6775-platform.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/hwmon/nct6775-platform.c b/drivers/hwmon/nct6775-platform.c index a8ef58d3a883..d359c28f3e0f 100644 --- a/drivers/hwmon/nct6775-platform.c +++ b/drivers/hwmon/nct6775-platform.c @@ -25,7 +25,7 @@ enum sensor_access { access_direct, access_asuswmi }; static const char * const nct6775_sio_names[] __initconst = { [nct6106] = "NCT6106D", [nct6116] = "NCT6116D", - [nct6126] = "NCT6126D", + [nct6126] = "NCT6122D/NCT6126D", [nct6775] = "NCT6775F", [nct6776] = "NCT6776D/F", [nct6779] = "NCT6779D", @@ -77,6 +77,7 @@ MODULE_PARM_DESC(fan_debounce, "Enable debouncing for fan RPM signal"); #define SIO_NCT6106_ID 0xc450 #define SIO_NCT6116_ID 0xd280 +#define SIO_NCT6122_ID 0xd2a0 #define SIO_NCT6126_A_ID 0xd283 #define SIO_NCT6126_B_ID 0xd284 #define SIO_NCT6775_ID 0xb470 @@ -1035,6 +1036,9 @@ static int __init nct6775_find(int sioaddr, struct nct6775_sio_data *sio_data) else sio_data->kind = nct6116; break; + case SIO_NCT6122_ID: + sio_data->kind = nct6126; + break; case SIO_NCT6775_ID: sio_data->kind = nct6775; break; From a925e9c47e15bfc731b4e9a7c373c43edbe7f4ca Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Fri, 11 Sep 2026 14:17:13 -0500 Subject: [PATCH 53/97] hwmon: (k10temp) Add support for 1ah/80h 1ah/80h uses same base as other programs. It only supports Tctl. Signed-off-by: Mario Limonciello Link: https://patch.msgid.link/20260911191803.1522358-2-mario.limonciello@amd.com Signed-off-by: Guenter Roeck --- drivers/hwmon/k10temp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/hwmon/k10temp.c b/drivers/hwmon/k10temp.c index e0a7211a513c..76f8829dd63d 100644 --- a/drivers/hwmon/k10temp.c +++ b/drivers/hwmon/k10temp.c @@ -94,6 +94,7 @@ MODULE_PARM_DESC(force, "force loading on processors with erratum 319"); * Defining locally as IDs are not shared. */ #define PCI_DEVICE_ID_AMD_1AH_M50H_DF_F3 0x12cb +#define PCI_DEVICE_ID_AMD_1AH_M80H_DF_F3 0x1243 #define PCI_DEVICE_ID_AMD_1AH_M90H_DF_F3 0x127b struct k10temp_data { @@ -582,6 +583,7 @@ static const struct pci_device_id k10temp_id_table[] = { { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_1AH_M50H_DF_F3) }, { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_1AH_M60H_DF_F3) }, { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_1AH_M70H_DF_F3) }, + { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_1AH_M80H_DF_F3) }, { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_1AH_M90H_DF_F3) }, {} }; From 5e8d596d011641f6abd4377111183d62a6677f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20S=C3=A1?= Date: Fri, 11 Sep 2026 14:53:38 +0100 Subject: [PATCH 54/97] hwmon: (pmbus/core) Add mapping function to pmbus_read_block_data() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is in preparation for adding support to a device which needs to use it's own read_block implementation. The MAX20826 family is one such device. When not in PMBus page mode, each of its two rails sits at a different I2C address, so selecting the rail cannot be done through pmbus_set_page() and has to be handled by the driver. Signed-off-by: Nuno Sá Link: https://patch.msgid.link/20260911-hwmon-max20826-support-v2-2-5e30cbd97d84@analog.com Signed-off-by: Guenter Roeck --- drivers/hwmon/pmbus/pmbus.h | 7 +++++++ drivers/hwmon/pmbus/pmbus_core.c | 24 ++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/pmbus/pmbus.h b/drivers/hwmon/pmbus/pmbus.h index 920c1102ab6d..5525c048a810 100644 --- a/drivers/hwmon/pmbus/pmbus.h +++ b/drivers/hwmon/pmbus/pmbus.h @@ -461,6 +461,13 @@ struct pmbus_driver_info { int (*read_byte_data)(struct i2c_client *client, int page, int reg); int (*read_word_data)(struct i2c_client *client, int page, int phase, int reg); + /* + * data_buf is at least I2C_SMBUS_BLOCK_MAX bytes long. The callback + * must never return more than I2C_SMBUS_BLOCK_MAX bytes of data and + * returns the number of bytes written into data_buf. + */ + int (*read_block_data)(struct i2c_client *client, int page, u8 reg, + char *data_buf); int (*write_byte_data)(struct i2c_client *client, int page, int reg, u8 byte); int (*write_word_data)(struct i2c_client *client, int page, int reg, diff --git a/drivers/hwmon/pmbus/pmbus_core.c b/drivers/hwmon/pmbus/pmbus_core.c index 5f69c1420b4e..5104fd29307b 100644 --- a/drivers/hwmon/pmbus/pmbus_core.c +++ b/drivers/hwmon/pmbus/pmbus_core.c @@ -616,6 +616,26 @@ static int pmbus_read_block_data(struct i2c_client *client, int page, u8 reg, return pmbus_read_smbus_i2c_block_data(client, reg, data_buf); } +/* + * _pmbus_read_block_data() is similar to pmbus_read_block_data(), but checks if + * a device specific mapping function exists and calls it if necessary. + */ +static int _pmbus_read_block_data(struct i2c_client *client, int page, u8 reg, + char *data_buf) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + const struct pmbus_driver_info *info = data->info; + int status; + + if (info->read_block_data) { + status = info->read_block_data(client, page, reg, data_buf); + if (status != -ENODATA) + return status; + } + + return pmbus_read_block_data(client, page, reg, data_buf); +} + static struct pmbus_sensor *pmbus_find_sensor(struct pmbus_data *data, int page, int reg) { @@ -761,7 +781,7 @@ static bool __maybe_unused pmbus_check_block_register(struct i2c_client *client, struct pmbus_data *data = i2c_get_clientdata(client); char data_buf[I2C_SMBUS_BLOCK_MAX + 2]; - rv = pmbus_read_block_data(client, page, reg, data_buf); + rv = _pmbus_read_block_data(client, page, reg, data_buf); if (rv >= 0 && !(data->flags & PMBUS_SKIP_STATUS_CHECK)) rv = pmbus_check_status_cml(client); if (rv < 0 && (data->flags & PMBUS_READ_STATUS_AFTER_FAILED_CHECK)) @@ -3679,7 +3699,7 @@ static ssize_t pmbus_debugfs_block_read(struct file *file, char __user *buf, char data[I2C_SMBUS_BLOCK_MAX + 2] = { 0 }; scoped_guard(pmbus_lock, client) { - rc = pmbus_read_block_data(client, entry->page, entry->reg, data); + rc = _pmbus_read_block_data(client, entry->page, entry->reg, data); if (rc < 0) return rc; } From 39df36b59f01fd8f738a1c18454df54b979a7ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20S=C3=A1?= Date: Fri, 11 Sep 2026 14:53:39 +0100 Subject: [PATCH 55/97] dt-bindings: hwmon/pmbus: Document MAX20826 and similar devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MAX20826 IC and similar provide a high-density, flexible and scalable dual-loop solution for high current cores for AI applications. These are dual loop solutions multiphase voltage regulators. Between Rails A and B, MAX20855B and MAX20908 supports up to 8 phases total configurable from 8+0 to 4+4 phases, MAX20912 supports up to 12 phases from 12+0 to 6+6, and MAX20826 and MAX20916 supports up to 16 phases from 16+0 to 8+8. The output voltage of each rail is controlled either through PMBus or through the high-speed processor voltage-control interface (Intel SVID, AMD SVI3, Nvidia PWMVID or AVSBus, depending on the device). Which one is in use is part of the configuration the device powers up with and is read back from it, hence adi,rail-a-high-speed and adi,rail-b-high-speed only tell whether the high-speed interface or PMBus controls the output voltage of the respective rail. Co-developed-by: Alexis Czezar Torreno Signed-off-by: Alexis Czezar Torreno Signed-off-by: Nuno Sá Acked-by: Conor Dooley Link: https://patch.msgid.link/20260911-hwmon-max20826-support-v2-3-5e30cbd97d84@analog.com Signed-off-by: Guenter Roeck --- .../bindings/hwmon/pmbus/adi,max20826.yaml | 102 ++++++++++++++++++ MAINTAINERS | 7 ++ 2 files changed, 109 insertions(+) create mode 100644 Documentation/devicetree/bindings/hwmon/pmbus/adi,max20826.yaml diff --git a/Documentation/devicetree/bindings/hwmon/pmbus/adi,max20826.yaml b/Documentation/devicetree/bindings/hwmon/pmbus/adi,max20826.yaml new file mode 100644 index 000000000000..e3bb51d6d1d6 --- /dev/null +++ b/Documentation/devicetree/bindings/hwmon/pmbus/adi,max20826.yaml @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/hwmon/pmbus/adi,max20826.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Analog Devices MAX20826 PMBus Voltage Regulator + +maintainers: + - Nuno Sá + +description: + Analog Devices MAX20826 and similar devices are Dual-Output Multiphase + High-Current Controller. + +properties: + compatible: + enum: + - adi,max20826 + - adi,max20855b + - adi,max20908 + - adi,max20912 + - adi,max20916 + + reg: + maxItems: 1 + + vdd3p3-supply: + description: + 3.3V bias supply. It is the input of the internal 1.8V linear regulator + which supplies the controller. + + avren-gpios: + description: GPIO pin to enable/disable the output voltage on Rail A. + maxItems: 1 + + bvren-gpios: + description: GPIO pin to enable/disable the output voltage on Rail B. + maxItems: 1 + + adi,rail-a-high-speed: + type: boolean + description: + The output voltage of Rail A is controlled by the high-speed processor + voltage-control interface the device powered up in (PWMVID, SVID, SVI3 or + AVSBus). If not given, PMBus controls the output voltage of Rail A. + + adi,rail-b-high-speed: + type: boolean + description: + Same as adi,rail-a-high-speed but for Rail B. + + regulators: + type: object + description: + List of regulators provided by this controller. vout0 is Rail A and + vout1 is Rail B. + + patternProperties: + "^vout[0-1]$": + $ref: /schemas/regulator/regulator.yaml# + type: object + unevaluatedProperties: false + + additionalProperties: false + +required: + - compatible + - reg + - vdd3p3-supply + +additionalProperties: false + +examples: + - | + #include + i2c { + #address-cells = <1>; + #size-cells = <0>; + + regulator@64 { + compatible = "adi,max20826"; + reg = <0x64>; + + vdd3p3-supply = <&vdd3p3>; + + avren-gpios = <&gpio 0 GPIO_ACTIVE_HIGH>; + bvren-gpios = <&gpio 1 GPIO_ACTIVE_HIGH>; + + adi,rail-a-high-speed; + + regulators { + vout0 { + regulator-name = "vcore_a"; + }; + + vout1 { + regulator-name = "vcore_b"; + }; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 5304761819d9..c8874088d4e3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -16005,6 +16005,13 @@ F: Documentation/devicetree/bindings/hwmon/pmbus/adi,max17616.yaml F: Documentation/hwmon/max17616.rst F: drivers/hwmon/pmbus/max17616.c +MAX20826 HARDWARE MONITOR CONTROLLER DRIVER +M: Nuno Sá +L: linux-hwmon@vger.kernel.org +S: Supported +W: https://ez.analog.com/linux-software-drivers +F: Documentation/devicetree/bindings/hwmon/pmbus/adi,max20826.yaml + MAX20830 HARDWARE MONITOR DRIVER M: Alexis Czezar Torreno L: linux-hwmon@vger.kernel.org From 6dcb8e4de526fa16887d8cc0aa6fee45a31fca23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20S=C3=A1?= Date: Fri, 11 Sep 2026 14:53:40 +0100 Subject: [PATCH 56/97] hwmon: (pmbus) add support for MAX20826 and similar devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MAX20826 IC and similar provide a high-density, flexible and scalable dual-loop solution for high current cores for AI applications. These are dual loop solutions multiphase voltage regulators. Between Rails A and B, MAX20855B and MAX20908 supports up to 8 phases total configurable from 8+0 to 4+4 phases, MAX20912 supports up to 12 phases from 12+0 to 6+6, and MAX20826 and MAX20916 supports up to 16 phases from 16+0 to 8+8. Co-developed-by: Alexis Czezar Torreno Signed-off-by: Alexis Czezar Torreno Signed-off-by: Nuno Sá Link: https://patch.msgid.link/20260911-hwmon-max20826-support-v2-4-5e30cbd97d84@analog.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/index.rst | 1 + Documentation/hwmon/max20826.rst | 139 ++++ MAINTAINERS | 2 + drivers/hwmon/pmbus/Kconfig | 19 + drivers/hwmon/pmbus/Makefile | 1 + drivers/hwmon/pmbus/max20826.c | 1044 ++++++++++++++++++++++++++++++ 6 files changed, 1206 insertions(+) create mode 100644 Documentation/hwmon/max20826.rst create mode 100644 drivers/hwmon/pmbus/max20826.c diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst index 7ab9cfe71cee..21ad419fcbf1 100644 --- a/Documentation/hwmon/index.rst +++ b/Documentation/hwmon/index.rst @@ -167,6 +167,7 @@ Hardware Monitoring Kernel Drivers max197 max20730 max20751 + max20826 max20830 max20860a max31722 diff --git a/Documentation/hwmon/max20826.rst b/Documentation/hwmon/max20826.rst new file mode 100644 index 000000000000..bcabbc64c039 --- /dev/null +++ b/Documentation/hwmon/max20826.rst @@ -0,0 +1,139 @@ +.. SPDX-License-Identifier: GPL-2.0 + +Kernel driver max20826 +====================== + +Supported chips: + + * Analog Devices MAX20826 + + Prefix: 'max20826' + + Addresses scanned: - + + Datasheet: Datasheet is not publicly available. + + * Analog Devices MAX20855B + + Prefix: 'max20855b' + + Addresses scanned: - + + Datasheet: Datasheet is not publicly available. + + * Analog Devices MAX20908 + + Prefix: 'max20908' + + Addresses scanned: - + + Datasheet: Datasheet is not publicly available. + + * Analog Devices MAX20912 + + Prefix: 'max20912' + + Addresses scanned: - + + Datasheet: Datasheet is not publicly available. + + * Analog Devices MAX20916 + + Prefix: 'max20916' + + Addresses scanned: - + + Datasheet: Datasheet is not publicly available. + +Author: + + - Nuno Sá + + +Description +----------- + +This driver supports hardware monitoring for Analog Devices MAX20826, +MAX20855B, MAX20908, MAX20912, and MAX20916 multiphase voltage regulator +controllers with PMBus interface. + +The devices are dual-loop, multiphase controllers. Depending on the device and +configuration, the high-speed processor voltage-control interface can be +Nvidia PWMVID, Intel SVID, AMD SVI3, or AVSBus. PMBus is used for monitoring, +configuration, status, and fault reporting. + +The driver detects whether the device uses PMBus page mode or direct address +mode. In direct address mode, rail B is accessed at the rail A I2C address plus +one. If rail B is present, the driver exposes a second PMBus page. + +The driver detects the active number of phases and exposes per-phase input and +output current attributes through the PMBus virtual phase support. + +Usage Notes +----------- + +This driver does not auto-detect devices. You will have to instantiate the +devices explicitly. Please see Documentation/i2c/instantiating-devices.rst for +details. + +The optional ``avren`` and ``bvren`` GPIOs may be provided to control the rail A +and rail B hardware enable pins. If regulator support is enabled, the rails are +also registered through the PMBus regulator framework. + +The source of the output voltage of each rail, i.e. either the high-speed +processor voltage-control interface or PMBus, is normally selected once, before +the processor boots. Hence it is described in firmware with the +``adi,rail-a-high-speed`` and ``adi,rail-b-high-speed`` properties. If the +property is not given for a rail, PMBus controls its output voltage. + +Sysfs entries +------------- + +The following attributes are supported. Limits, alarms, and per-phase entries +are exposed depending on device capabilities, rail configuration, and detected +phase count. + +=========================== ================================================ +in1_label "vin" +in1_input Measured input voltage +in1_alarm Input voltage alarm +in[2-3]_label "vout[1-2]" +in[2-3]_input Measured output voltage +in[2-3]_alarm Output voltage alarm +currX_label "iinN", "iinN.P", "ioutN", or "ioutN.P" +currX_input Measured input/output current +currX_alarm Current alarm +powerX_label "pinN" or "poutN" +powerX_input Measured input/output power +temp[1-2]_input Measured temperature +temp[1-2]_alarm Temperature alarm +=========================== ================================================ + +Notes +----- + +``N`` is the rail number, starting at 1. ``P`` is the phase number, starting at +0. The exact ``currX`` indices depend on the number of present rails and on the +detected phase count. + +Debugfs entries +--------------- + +In addition to the entries provided by the PMBus core, the following entries are +available for debug purposes only. They are not part of any ABI and may change +or go away at any time. + +=========================== ================================================ +in[2-3]_high_speed_en Enable high-speed voltage-control interface +in_high_speed_bus Active high-speed voltage-control interface +=========================== ================================================ + +``in[2-3]_high_speed_en`` is a per-rail read/write entry matching the standard +hwmon voltage channels for the output rails. ``in2_high_speed_en`` controls rail +A / ``vout1``. ``in3_high_speed_en`` controls rail B / ``vout2`` and is only +present if rail B is detected. Writing 1 selects the high-speed voltage-control +interface for that rail; writing 0 selects PMBus voltage control. + +``in_high_speed_bus`` is a read-only entry reporting the high-speed interface +used by the device, for example ``Nvidia PWMVID``, ``Intel SVID``, ``AMD SVI3``, +or ``AVSBus``. diff --git a/MAINTAINERS b/MAINTAINERS index c8874088d4e3..9712f007eac5 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -16011,6 +16011,8 @@ L: linux-hwmon@vger.kernel.org S: Supported W: https://ez.analog.com/linux-software-drivers F: Documentation/devicetree/bindings/hwmon/pmbus/adi,max20826.yaml +F: Documentation/hwmon/max20826.rst +F: drivers/hwmon/pmbus/max20826.c MAX20830 HARDWARE MONITOR DRIVER M: Alexis Czezar Torreno diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig index bcfdc4ce4c10..3289ab50a028 100644 --- a/drivers/hwmon/pmbus/Kconfig +++ b/drivers/hwmon/pmbus/Kconfig @@ -393,6 +393,25 @@ config SENSORS_MAX20751 This driver can also be built as a module. If so, the module will be called max20751. +config SENSORS_MAX20826 + tristate "Analog Devices MAX20826 and similar devices" + help + If you say yes here you get hardware monitoring support for Analog + Devices MAX20826 and similar devices. + + This driver can also be built as a module. If so, the module will + be called max20826. + +config SENSORS_MAX20826_REGULATOR + bool "Regulator support for Analog Devices MAX20826 and similar devices" + depends on SENSORS_MAX20826 && REGULATOR + help + If you say yes here you get regulator support for Analog Devices + MAX20826 and similar sensors. + + This enables the MAX20826 to be used as a regulator device, + providing voltage control through the regulator framework. + config SENSORS_MAX20830 tristate "Analog Devices MAX20830 and compatibles" help diff --git a/drivers/hwmon/pmbus/Makefile b/drivers/hwmon/pmbus/Makefile index e288fe72a437..e3fc23f88401 100644 --- a/drivers/hwmon/pmbus/Makefile +++ b/drivers/hwmon/pmbus/Makefile @@ -38,6 +38,7 @@ obj-$(CONFIG_SENSORS_MAX16601) += max16601.o obj-$(CONFIG_SENSORS_MAX17616) += max17616.o obj-$(CONFIG_SENSORS_MAX20730) += max20730.o obj-$(CONFIG_SENSORS_MAX20751) += max20751.o +obj-$(CONFIG_SENSORS_MAX20826) += max20826.o obj-$(CONFIG_SENSORS_MAX20830) += max20830.o obj-$(CONFIG_SENSORS_MAX20860A) += max20860a.o obj-$(CONFIG_SENSORS_MAX31785) += max31785.o diff --git a/drivers/hwmon/pmbus/max20826.c b/drivers/hwmon/pmbus/max20826.c new file mode 100644 index 000000000000..013a97189fde --- /dev/null +++ b/drivers/hwmon/pmbus/max20826.c @@ -0,0 +1,1044 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Hardware monitoring driver for Analog Devices MAX20826 PMBus device + * + * Copyright 2026 Analog Devices Inc. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pmbus.h" + +#define MAX20826_REG_RAIL_PHASE_CFG 0xB1 +#define MAX20826_REG_CTRL_MISC 0xCB +#define MAX20826_OPL_EN_MSK BIT(7) + +#define MAX20826_REG_VOUT_RES 0xDB +#define MAX20826_VOUT_RES_MSK BIT(7) + +#define MAX20855B_REG_VOUT_VRM 0xD1 +#define MAX20855B_REG_VOUT_VRM_MASK BIT(4) + +#define MAX20826_REG_C_MODEAB 0xDC +/* Byte 1 bits 7 and 6*/ +#define MAX20826_C_MODEAB_MASK GENMASK(15, 14) + +#define MAX20826_REG_ADDR_MODE 0xEC +#define MAX20826_PAGE_MODE_MSK BIT(7) +#define MAX20826_DIRECT_ADDR_MSK GENMASK(6, 0) + +#define MAX20826_REG_OVERRIDE 0xED +#define MAX20826_OVERRIDE_MASK BIT(7) + +#define MAX20826_REG_PHASE_DETECT 0xF3 +#define MAX20826_REG_PHASE_READ 0xF4 + +#define MAX20826_REG_STATUS_MON 0xF9 +#define MAX20826_PHASES_NUM_MASK GENMASK(7, 3) + +#define MAX20826_MAX_PAGES 2 +#define MAX20826_MAX_PHASES 16 +#define MAX20826_PHASES_PER_PAGE 8 +#define MAX20826_INTF_PWMVID 1 +#define MAX20826_INTF_AVSBUS 3 + +#define MAX20855B_PHASES_NUM_MASK GENMASK(7, 4) +#define MAX20855B_MAX_PHASES 8 +#define MAX20908_MAX_PHASES 8 +#define MAX20912_MAX_PHASES 12 +#define MAX20916_MAX_PHASES 16 + +struct max20826_chip_info { + const char *vendor_bus_name; + u8 max_phases; + unsigned int phase_num_mask; + u8 start_index_iin; + u8 start_index_iout; + bool is_reg_addr_mode_block; + bool is_vout_direct; + bool select_vrm; + bool has_avsbus; + bool has_opl; + u8 (*count_phases)(const u8 *config, int page); +}; + +struct max20826 { + const struct max20826_chip_info *chip_info; + struct pmbus_driver_info info; + struct i2c_client *client; + /* RAIL-B direct mode */ + struct i2c_client *client_b; + struct i2c_client *curr_client; + struct gpio_desc *avren; + struct gpio_desc *bvren; + bool vendor_bus; + bool high_speed[MAX20826_MAX_PAGES]; + bool on_off_ctrl[MAX20826_MAX_PAGES]; + bool opl_enabled[MAX20826_MAX_PAGES]; +}; + +struct max20826_debugfs_entry { + struct max20826 *st; + u8 page; +}; + +static u8 __max20826_count_phases(const u8 *config, int page) +{ + if (page) + return hweight8(config[4]); + + return hweight8(config[0]) + hweight8(config[1]) - + hweight8(config[4]); +} + +static u8 __max20855b_count_phases(const u8 *config, int page) +{ + if (page) + return hweight8(config[3] & 0x3F); + + return hweight8(config[0]) + hweight8(config[1] & 0x0F) - + hweight8(config[3] & 0x3F); +} + +static u8 __max20908_count_phases(const u8 *config, int page) +{ + if (page) + return hweight8(config[4]); + + return hweight8(config[0]) + hweight8(config[1] & 0xF0) - + hweight8(config[4]); +} + +static u8 __max20912_count_phases(const u8 *config, int page) +{ + if (page) + return hweight8(config[4]); + + return hweight8(config[0]) + hweight8(config[1] & 0xFC) - + hweight8(config[4]); +} + +#define to_max20826(p) container_of(p, struct max20826, info) + +enum { + RAIL_A, + RAIL_B, +}; + +static const struct regulator_desc __maybe_unused max20826_reg_desc[] = { + PMBUS_REGULATOR("vout", 0), + PMBUS_REGULATOR("vout", 1), +}; + +static struct i2c_client *max20826_select_rail(struct max20826 *st, + int page, bool probing) +{ + int ret; + + /* + * If in direct mode and we want RAIL_B (page 1) just return client_b. + * Otherwise, set the proper page (if page mode) and return RAIL_A. + */ + if (st->client_b) { + /* if 0xff just return the last client */ + if (page < 0) + return st->curr_client; + if (page) + st->curr_client = st->client_b; + else + st->curr_client = st->client; + + return st->curr_client; + } + + if (!probing) + ret = pmbus_set_page(st->client, page, 0xff); + else + ret = i2c_smbus_write_byte_data(st->client, PMBUS_PAGE, page); + if (ret < 0) + return ERR_PTR(ret); + + return st->client; +} + +static int max20826_rail_read_block_data(struct i2c_client *rail, u8 reg, + u8 *block, u8 min_len) +{ + int ret; + + ret = pmbus_read_smbus_i2c_block_data(rail, reg, block); + if (ret < 0) + return ret; + if (ret < min_len) + return -EIO; + + return ret; +} + +static int __max20826_read_block_data(struct max20826 *st, int page, u8 reg, + u8 *block, u8 min_len) +{ + struct i2c_client *rail; + + rail = max20826_select_rail(st, page, false); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + return max20826_rail_read_block_data(rail, reg, block, min_len); +} + +static int __max20826_read_byte_data(struct max20826 *st, int page, int reg) +{ + struct i2c_client *rail; + + rail = max20826_select_rail(st, page, false); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + return i2c_smbus_read_byte_data(rail, reg); +} + +static int __max20826_write_byte_data(struct max20826 *st, int page, int reg, + u8 value) +{ + struct i2c_client *rail; + + rail = max20826_select_rail(st, page, false); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + return i2c_smbus_write_byte_data(rail, reg, value); +} + +static int __max20826_read_word_data(struct max20826 *st, int page, int reg) +{ + struct i2c_client *rail; + + rail = max20826_select_rail(st, page, false); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + return i2c_smbus_read_word_data(rail, reg); +} + +/* + * @rail is the already selected rail client and is only given during probe, + * where the PMBus core cannot be used yet. Otherwise, pass NULL and let the + * core dispatch through our own read_byte_data()/write_byte_data() callbacks, + * which take care of selecting the rail. + */ +static int max20826_set_high_speed(struct max20826 *st, struct i2c_client *rail, + u8 page, bool high_speed) +{ + u8 reg, mask, val; + int ret; + + if (st->vendor_bus) { + reg = MAX20826_REG_OVERRIDE; + mask = MAX20826_OVERRIDE_MASK; + val = high_speed ? 0 : FIELD_PREP(MAX20826_OVERRIDE_MASK, 1); + } else { + reg = PMBUS_OPERATION; + mask = PB_OPERATION_CONTROL_V_SRC; + val = high_speed ? FIELD_PREP(PB_OPERATION_CONTROL_V_SRC, MAX20826_INTF_AVSBUS) : 0; + } + + if (!rail) { + ret = pmbus_update_byte_data(st->client, page, reg, mask, val); + } else { + ret = i2c_smbus_read_byte_data(rail, reg); + if (ret < 0) + return ret; + + ret = i2c_smbus_write_byte_data(rail, reg, (ret & ~mask) | val); + } + + if (ret) + return ret; + + st->high_speed[page] = high_speed; + + return 0; +} + +static int max20826_read_curr(struct max20826 *st, int page, int phase, int reg) +{ + u8 val_out[I2C_SMBUS_BLOCK_MAX], start_byte; + struct i2c_client *rail; + int ret; + + if (phase == 0xff) + return __max20826_read_word_data(st, page, reg); + + /* + * On Rail_A phases are ascending (from 1) while on RAIL_B they + * are descending (from 16). + */ + if (!page) + phase += 1; + else + phase = st->chip_info->max_phases - phase; + + /* phase detect and read are only available on RAIL_A */ + rail = max20826_select_rail(st, RAIL_A, false); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + ret = i2c_smbus_write_byte_data(rail, MAX20826_REG_PHASE_DETECT, phase); + if (ret < 0) + return ret; + + /* + * For MAX20826: Byte 2:3 is for phase IOUT 4:5 is for phase IIN. + * For other chips: Byte 0:1 is for phase IOUT 2:3 is for phase IIN. + */ + if (reg == PMBUS_READ_IIN) + start_byte = st->chip_info->start_index_iin; + else + start_byte = st->chip_info->start_index_iout; + + ret = max20826_rail_read_block_data(rail, MAX20826_REG_PHASE_READ, + val_out, start_byte + 2); + if (ret < 0) + return ret; + + return get_unaligned_le16(&val_out[start_byte]); +} + +static int max20826_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max20826 *st = to_max20826(info); + + switch (reg) { + case PMBUS_READ_IIN: + case PMBUS_READ_IOUT: + return max20826_read_curr(st, page, phase, reg); + case PMBUS_IOUT_OC_FAULT_LIMIT: + if (!st->chip_info->has_opl || !st->opl_enabled[page]) + return __max20826_read_word_data(st, page, reg); + return -EIO; + case PMBUS_POUT_OP_FAULT_LIMIT: + /* + * If Over Power Limit is enabled, PMBUS_IOUT_OC_FAULT_LIMIT + * shows the power limit and hence we need to report it + * properly in Watts. + */ + if (!st->chip_info->has_opl) + return __max20826_read_word_data(st, page, reg); + + if (st->opl_enabled[page]) + return __max20826_read_word_data(st, page, + PMBUS_IOUT_OC_FAULT_LIMIT); + return -EIO; + default: + if (reg >= PMBUS_VIRT_BASE) + return -EOPNOTSUPP; + return __max20826_read_word_data(st, page, reg); + } +} + +static int max20826_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max20826 *st = to_max20826(info); + struct i2c_client *rail; + + rail = max20826_select_rail(st, page, false); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + return i2c_smbus_write_word_data(rail, reg, word); +} + +static int max20826_regulator_do_enable(struct max20826 *st, + struct i2c_client *rail, u8 page, + u8 byte) +{ + /* + * If AVSBus is enabled (bits 5 and 4 set) the device refuses to set bit + * 7 of the OPERATION register. Hence, to workaround this, we first + * clear the bits and then set them all together. + */ + if (st->chip_info->has_avsbus && !st->vendor_bus && st->high_speed[page] && + PB_OPERATION_CONTROL_ON & byte) { + u8 __byte = byte & ~(PB_OPERATION_CONTROL_V_SRC | PB_OPERATION_CONTROL_ON); + int ret; + + ret = i2c_smbus_write_byte_data(rail, PMBUS_OPERATION, __byte); + if (ret < 0) + return ret; + } + + return i2c_smbus_write_byte_data(rail, PMBUS_OPERATION, byte); +} + +static int max20826_regulator_enable(struct max20826 *st, int page, u8 byte) +{ + struct i2c_client *rail; + + rail = max20826_select_rail(st, page, false); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + if (page) + gpiod_set_value_cansleep(st->bvren, !!(PB_OPERATION_CONTROL_ON & byte)); + else + gpiod_set_value_cansleep(st->avren, !!(PB_OPERATION_CONTROL_ON & byte)); + + return max20826_regulator_do_enable(st, rail, page, byte); +} + +static int max20826_write_byte_data(struct i2c_client *client, int page, + int reg, u8 byte) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max20826 *st = to_max20826(info); + + switch (reg) { + case PMBUS_OPERATION: + return max20826_regulator_enable(st, page, byte); + default: + return __max20826_write_byte_data(st, page, reg, byte); + } +} + +static int max20826_write_byte(struct i2c_client *client, int page, u8 byte) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max20826 *st = to_max20826(info); + struct i2c_client *rail; + + rail = max20826_select_rail(st, page, false); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + return i2c_smbus_write_byte(rail, byte); +} + +static int max20826_regulator_enabled(struct max20826 *st, int page) +{ + struct gpio_desc *gpio = page ? st->bvren : st->avren; + int on, ret; + + if (gpio) { + on = gpiod_get_value_cansleep(gpio); + if (on < 0) + return on; + } else { + /* If the gpios are not given, just assume it's on */ + on = 1; + } + + ret = __max20826_read_byte_data(st, page, PMBUS_OPERATION); + if (ret < 0) + return ret; + + if (st->on_off_ctrl[page]) + on = (PB_OPERATION_CONTROL_ON & ret) && on; + + ret &= ~PB_OPERATION_CONTROL_ON; + return ret | FIELD_PREP(PB_OPERATION_CONTROL_ON, on); +} + +static int max20826_iout_status(struct max20826 *st, int page) +{ + int status; + + status = __max20826_read_byte_data(st, page, PMBUS_STATUS_IOUT); + if (status < 0 || !st->chip_info->has_opl) + return status; + + /* + * If Over power limit is on, the fault condition is still set on the OC + * bit + */ + if (st->opl_enabled[page] && (status & PB_IOUT_OC_FAULT)) + return status | PB_POUT_OP_FAULT; + + return status; +} + +static int max20826_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max20826 *st = to_max20826(info); + + switch (reg) { + case PMBUS_OPERATION: + return max20826_regulator_enabled(st, page); + case PMBUS_STATUS_IOUT: + return max20826_iout_status(st, page); + default: + return __max20826_read_byte_data(st, page, reg); + } +} + +static int max20826_read_block_data(struct i2c_client *client, int page, u8 reg, + char *data_buf) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max20826 *st = to_max20826(info); + + switch (reg) { + case PMBUS_MFR_ID: + case PMBUS_MFR_MODEL: + return __max20826_read_block_data(st, page, reg, data_buf, 16); + case PMBUS_MFR_REVISION: + return __max20826_read_block_data(st, page, reg, data_buf, 2); + case PMBUS_MFR_DATE: + return __max20826_read_block_data(st, page, reg, data_buf, 8); + default: + return -EOPNOTSUPP; + } +} + +static struct pmbus_driver_info max20826_default_info = { + .pages = 1, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | + PMBUS_PHASE_VIRTUAL, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .m[PSC_VOLTAGE_OUT] = 2, + .R[PSC_VOLTAGE_OUT] = 3, + .write_byte_data = max20826_write_byte_data, + .write_byte = max20826_write_byte, + .read_byte_data = max20826_read_byte_data, + .read_word_data = max20826_read_word_data, + .write_word_data = max20826_write_word_data, + .read_block_data = max20826_read_block_data, +#if IS_ENABLED(CONFIG_SENSORS_MAX20826_REGULATOR) + .num_regulators = 1, + .reg_desc = max20826_reg_desc, +#endif +}; + +static int max20826_detect_addr_mode(struct max20826 *st) +{ + u8 val_buf[I2C_SMBUS_BLOCK_MAX]; + struct device *dev = &st->client->dev; + int ret, val; + + /* + * The idea is that after a POR, we are in page0 in which case we can + * read MAX20826_REG_ADDR_MODE. If we fail to read + * MAX20826_REG_ADDR_MODE, it might be due to a soft reset or unbinding + * the device in which case the device could be left in page 1. Hence, + * let's just try to change to page 0 and error out if we can't. + * + * OTOH, if the addressing mode is direct, we should be able to + * read MAX20826_REG_ADDR_MODE. If not, we'll fail setting the page. + * + * REG_ADDR_MODE, MAX20855B has 6 bytes, other devices have 1. Byte 3 is + * the equivalent of the single byte of the others. + */ + if (st->chip_info->is_reg_addr_mode_block) + val = max20826_rail_read_block_data(st->client, + MAX20826_REG_ADDR_MODE, + val_buf, 3); + else + val = i2c_smbus_read_byte_data(st->client, MAX20826_REG_ADDR_MODE); + + if (val < 0) { + ret = i2c_smbus_write_byte_data(st->client, PMBUS_PAGE, 0); + if (ret < 0) + return dev_err_probe(dev, ret, + "Failed to change to page 0\n"); + + /* try again! */ + if (st->chip_info->is_reg_addr_mode_block) + val = max20826_rail_read_block_data(st->client, + MAX20826_REG_ADDR_MODE, + val_buf, 3); + else + val = i2c_smbus_read_byte_data(st->client, + MAX20826_REG_ADDR_MODE); + if (val < 0) + return dev_err_probe(dev, val, + "Failed to read MAX20826_REG_ADDR_MODE\n"); + } + + if (st->chip_info->is_reg_addr_mode_block) + val = val_buf[2]; + + if (val & MAX20826_PAGE_MODE_MSK) + return 0; + + /* + * If in direct mode, rail b will be accessible from the addr of RAIL_A + * +1 + */ + st->client_b = devm_i2c_new_dummy_device(dev, st->client->adapter, + st->client->addr + 1); + if (IS_ERR(st->client_b)) + return PTR_ERR(st->client_b); + + return 0; +} + +static int max20826_set_vout_interface(struct max20826 *st, + struct i2c_client *rail, u8 page) +{ + static const char * const props[MAX20826_MAX_PAGES] = { + "adi,rail-a-high-speed", + "adi,rail-b-high-speed", + }; + bool high_speed; + int ret; + + /* + * Rail A 0xDC_1[7:6] sets the operating mode for both rails, so only look + * at it once. + */ + if (page == RAIL_A) { + if (st->chip_info->has_avsbus) { + ret = i2c_smbus_read_word_data(st->client, + MAX20826_REG_C_MODEAB); + if (ret < 0) + return ret; + + if (FIELD_GET(MAX20826_C_MODEAB_MASK, ret) == MAX20826_INTF_PWMVID) + st->vendor_bus = true; + } else { + st->vendor_bus = true; + } + } + + high_speed = device_property_read_bool(&st->client->dev, props[page]); + + return max20826_set_high_speed(st, rail, page, high_speed); +} + +static int max20826_detect_phases(struct max20826 *st, struct i2c_client *rail, + struct pmbus_driver_info *info, u8 page, + u8 expected) +{ + u8 status_mon[I2C_SMBUS_BLOCK_MAX]; + int ret, ret2 = 0, oper_save = -1; + u8 n_phases; + + /* + * In order to detect the number of phases, we need to be regulating. + * It is also assumed we're already in the page we want to detect the + * phases. + * + * If the vendor high speed interface controls the output voltage, the + * enable state is not ours to control (it is given by that interface + * together with the AVREN/BVREN pins) and writing PMBUS_OPERATION is a + * no-op, so just assume the rail is already regulating. + */ + if (!st->vendor_bus || !st->high_speed[page]) { + ret = i2c_smbus_read_byte_data(rail, PMBUS_OPERATION); + if (ret < 0) + return ret; + + if (!(ret & PB_OPERATION_CONTROL_ON)) { + oper_save = ret; + ret = max20826_regulator_do_enable(st, rail, page, + ret | PB_OPERATION_CONTROL_ON); + if (ret < 0) + return ret; + } + } + + ret = max20826_rail_read_block_data(rail, MAX20826_REG_STATUS_MON, + status_mon, 2); + if (ret < 0) + goto out_restore_oper; + + n_phases = field_get(st->chip_info->phase_num_mask, status_mon[1]); + if (n_phases != expected) { + ret = dev_err_probe(&st->client->dev, -EIO, + "Number of phases mismatch: expected=%u, detected=%u\n", + expected, n_phases); + goto out_restore_oper; + } + + info->phases[page] = n_phases; + for (unsigned int phase = 0; phase < n_phases; phase++) + info->pfunc[phase] = PMBUS_HAVE_IOUT | PMBUS_HAVE_IIN; + +out_restore_oper: + if (oper_save >= 0) + ret2 = max20826_regulator_do_enable(st, rail, page, oper_save); + + return ret < 0 ? ret : ret2; +} + +static int max20826_get_rail_config(struct max20826 *st, + struct i2c_client *rail, u8 page) +{ + u8 ctrl_misc[I2C_SMBUS_BLOCK_MAX]; + int ret; + + /* + * Check if we need to control PMBUS_OPERATION in addition to the + * CONTROL pin. + */ + ret = i2c_smbus_read_byte_data(rail, PMBUS_ON_OFF_CONFIG); + if (ret < 0) + return ret; + + st->on_off_ctrl[page] = !!(ret & PB_ON_OFF_CONFIG_OPERATION_REQ); + + /* + * See if Over Power Limit is enabled. This will impact how + * OC_FAULT_LIMIT is handled. + */ + if (st->chip_info->has_opl) { + ret = max20826_rail_read_block_data(rail, MAX20826_REG_CTRL_MISC, + ctrl_misc, 1); + if (ret < 0) + return ret; + + st->opl_enabled[page] = !!(ctrl_misc[0] & MAX20826_OPL_EN_MSK); + } + + return 0; +} + +static int max20826_setup_vout_format(struct max20826 *st) +{ + struct pmbus_driver_info *info = &st->info; + struct i2c_client *rail; + int ret; + + if (st->chip_info->is_vout_direct) { + u8 vout_res[I2C_SMBUS_BLOCK_MAX]; + + rail = max20826_select_rail(st, RAIL_A, true); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + ret = max20826_rail_read_block_data(rail, MAX20826_REG_VOUT_RES, + vout_res, 2); + if (ret < 0) + return ret; + + /* check for 1 mv/LSB */ + if (MAX20826_VOUT_RES_MSK & vout_res[1]) + info->m[PSC_VOLTAGE_OUT] = 1; + + return 0; + } + + st->info.format[PSC_VOLTAGE_OUT] = vid; + for (unsigned int page = 0; page < info->pages; page++) { + if (!st->chip_info->select_vrm) { + /* only vr12 in this case */ + st->info.vrm_version[page] = vr12; + continue; + } + + rail = max20826_select_rail(st, page, true); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + ret = i2c_smbus_read_byte_data(rail, MAX20855B_REG_VOUT_VRM); + if (ret < 0) + return ret; + + if (ret & MAX20855B_REG_VOUT_VRM_MASK) + st->info.vrm_version[page] = vr13; + else + st->info.vrm_version[page] = vr12; + } + + return 0; +} + +static int max20826_setup(struct max20826 *st) +{ + struct pmbus_driver_info *info = &st->info; + u8 config[I2C_SMBUS_BLOCK_MAX], expected_phases; + struct device *dev = &st->client->dev; + struct i2c_client *rail; + int ret; + + ret = max20826_detect_addr_mode(st); + if (ret < 0) + return ret; + + ret = max20826_set_vout_interface(st, st->client, RAIL_A); + if (ret < 0) + return ret; + + ret = max20826_rail_read_block_data(st->client, + MAX20826_REG_RAIL_PHASE_CFG, + config, 5); + if (ret < 0) + return ret; + + /* + * If we have phases in RAIL_B, we need to subtract them on config[1] as + * those bits are also set in case RAIL_B has phases. + */ + expected_phases = st->chip_info->count_phases(config, RAIL_A); + ret = max20826_detect_phases(st, st->client, info, RAIL_A, + expected_phases); + if (ret < 0) + return ret; + + ret = max20826_get_rail_config(st, st->client, RAIL_A); + if (ret < 0) + return ret; + + /* Let's see if RAIL_B is present */ + rail = max20826_select_rail(st, RAIL_B, true); + if (IS_ERR(rail)) + return PTR_ERR(rail); + + /* Let's see if there's something on RAIL_B */ + st->bvren = devm_gpiod_get_optional(dev, "bvren", GPIOD_OUT_HIGH); + if (IS_ERR(st->bvren)) + return PTR_ERR(st->bvren); + + ret = max20826_set_vout_interface(st, rail, RAIL_B); + if (ret < 0) + return ret; + + expected_phases = st->chip_info->count_phases(config, RAIL_B); + ret = max20826_detect_phases(st, rail, info, RAIL_B, expected_phases); + if (ret < 0) + return ret; + + if (info->phases[RAIL_B]) { + info->pages = MAX20826_MAX_PAGES; + info->func[RAIL_B] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | + PMBUS_PHASE_VIRTUAL; + + if (IS_ENABLED(CONFIG_SENSORS_MAX20826_REGULATOR)) + info->num_regulators = 2; + + ret = max20826_get_rail_config(st, rail, RAIL_B); + if (ret < 0) + return ret; + } + + ret = max20826_setup_vout_format(st); + if (ret < 0) + return ret; + + if (info->pages == 1) { + /* + * In this case we can be left in RAIL_B and given that we on + * only have one page, pmbus_set_page() will refuse to comeback + * to RAIL_A which is obviously problematic. Hence, let's just + * select RAIL_A here. + */ + rail = max20826_select_rail(st, RAIL_A, true); + if (IS_ERR(rail)) + return PTR_ERR(rail); + } + + return 0; +} + +static int max20826_high_speed_en_get(void *arg, u64 *val) +{ + const struct max20826_debugfs_entry *entry = arg; + + *val = entry->st->high_speed[entry->page]; + + return 0; +} + +static int max20826_high_speed_en_set(void *arg, u64 val) +{ + const struct max20826_debugfs_entry *entry = arg; + struct max20826 *st = entry->st; + + guard(pmbus_lock)(st->client); + + return max20826_set_high_speed(st, NULL, entry->page, !!val); +} +DEFINE_DEBUGFS_ATTRIBUTE(max20826_high_speed_en_fops, + max20826_high_speed_en_get, + max20826_high_speed_en_set, "%llu\n"); + +static int max20826_high_speed_bus_show(struct seq_file *s, void *data) +{ + struct device *dev = s->private; + struct i2c_client *client = to_i2c_client(dev); + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max20826 *st = to_max20826(info); + + seq_printf(s, "%s\n", st->vendor_bus ? st->chip_info->vendor_bus_name : "AVSBus"); + + return 0; +} + +static void max20826_init_debugfs(struct max20826 *st) +{ + struct device *dev = &st->client->dev; + struct dentry *dir; + + dir = pmbus_get_debugfs_dir(st->client); + if (!dir) + return; + + for (unsigned int page = 0; page < st->info.pages; page++) { + struct max20826_debugfs_entry *entry; + char name[32]; + + entry = devm_kzalloc(dev, sizeof(*entry), GFP_KERNEL); + if (!entry) + return; + + entry->st = st; + entry->page = page; + + scnprintf(name, sizeof(name), "in%u_high_speed_en", page + 2); + debugfs_create_file_unsafe(name, 0644, dir, entry, + &max20826_high_speed_en_fops); + } + + debugfs_create_devm_seqfile(dev, "in_high_speed_bus", dir, + max20826_high_speed_bus_show); +} + +static int max20826_probe(struct i2c_client *client) +{ + struct max20826 *st; + int ret; + + st = devm_kzalloc(&client->dev, sizeof(*st), GFP_KERNEL); + if (!st) + return -ENOMEM; + + st->client = client; + memcpy(&st->info, &max20826_default_info, sizeof(st->info)); + + st->chip_info = i2c_get_match_data(client); + if (!st->chip_info) + return -EINVAL; + + st->avren = devm_gpiod_get_optional(&client->dev, "avren", GPIOD_OUT_HIGH); + if (IS_ERR(st->avren)) + return PTR_ERR(st->avren); + + ret = max20826_setup(st); + if (ret) + return ret; + + ret = pmbus_do_probe(client, &st->info); + if (ret) + return ret; + + max20826_init_debugfs(st); + + return 0; +} + +static const struct max20826_chip_info chip_info_max20826 = { + .vendor_bus_name = "Nvidia PWMVID", + .max_phases = MAX20826_MAX_PHASES, + .phase_num_mask = MAX20826_PHASES_NUM_MASK, + .start_index_iin = 4, + .start_index_iout = 2, + .is_vout_direct = true, + .has_avsbus = true, + .has_opl = true, + .count_phases = __max20826_count_phases, +}; + +static const struct max20826_chip_info chip_info_max20855b = { + .vendor_bus_name = "Intel SVID", + .max_phases = MAX20855B_MAX_PHASES, + .phase_num_mask = MAX20855B_PHASES_NUM_MASK, + .start_index_iin = 2, + .start_index_iout = 0, + .is_reg_addr_mode_block = true, + .select_vrm = true, + .count_phases = __max20855b_count_phases, +}; + +static const struct max20826_chip_info chip_info_max20908 = { + .vendor_bus_name = "AMD SVI3", + .max_phases = MAX20908_MAX_PHASES, + .phase_num_mask = MAX20826_PHASES_NUM_MASK, + .start_index_iin = 2, + .start_index_iout = 0, + .count_phases = __max20908_count_phases, +}; + +static const struct max20826_chip_info chip_info_max20912 = { + .vendor_bus_name = "AMD SVI3", + .max_phases = MAX20912_MAX_PHASES, + .phase_num_mask = MAX20826_PHASES_NUM_MASK, + .start_index_iin = 2, + .start_index_iout = 0, + .count_phases = __max20912_count_phases, +}; + +static const struct max20826_chip_info chip_info_max20916 = { + .vendor_bus_name = "AMD SVI3", + .max_phases = MAX20916_MAX_PHASES, + .phase_num_mask = MAX20826_PHASES_NUM_MASK, + .start_index_iin = 2, + .start_index_iout = 0, + .count_phases = __max20826_count_phases, +}; + +static const struct i2c_device_id max20826_id[] = { + { "max20826", (kernel_ulong_t)&chip_info_max20826 }, + { "max20855b", (kernel_ulong_t)&chip_info_max20855b }, + { "max20908", (kernel_ulong_t)&chip_info_max20908 }, + { "max20912", (kernel_ulong_t)&chip_info_max20912 }, + { "max20916", (kernel_ulong_t)&chip_info_max20916 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max20826_id); + +static const struct of_device_id max20826_of_match[] = { + { .compatible = "adi,max20826", .data = &chip_info_max20826 }, + { .compatible = "adi,max20855b", .data = &chip_info_max20855b }, + { .compatible = "adi,max20908", .data = &chip_info_max20908 }, + { .compatible = "adi,max20912", .data = &chip_info_max20912 }, + { .compatible = "adi,max20916", .data = &chip_info_max20916 }, + { } +}; +MODULE_DEVICE_TABLE(of, max20826_of_match); + +static struct i2c_driver max20826_driver = { + .driver = { + .name = "max20826", + .of_match_table = max20826_of_match, + }, + .probe = max20826_probe, + .id_table = max20826_id, +}; +module_i2c_driver(max20826_driver); + +MODULE_AUTHOR("Nuno Sá "); +MODULE_DESCRIPTION("PMBus driver for MAX20826"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); From 38df742fb53415d2c933145b16e27a3851fdba58 Mon Sep 17 00:00:00 2001 From: Guenter Roeck Date: Sat, 12 Sep 2026 07:50:38 -0700 Subject: [PATCH 57/97] hwmon: (pmbus) Validate number of phases per page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is possible for a PMBus driver to configure a phase count larger than the PMBUS_PHASES limit and cause an out-of-bounds read. Add a bounds check for info->phases against PMBUS_PHASES in the core API to prevent the potential out-of-bounds read. Cc: Nuno Sá Signed-off-by: Guenter Roeck Reviewed-by: Nuno Sá Link: https://patch.msgid.link/20260912145038.2411447-1-linux@roeck-us.net --- drivers/hwmon/pmbus/pmbus_core.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/hwmon/pmbus/pmbus_core.c b/drivers/hwmon/pmbus/pmbus_core.c index 5104fd29307b..d69f1bde1795 100644 --- a/drivers/hwmon/pmbus/pmbus_core.c +++ b/drivers/hwmon/pmbus/pmbus_core.c @@ -2982,6 +2982,11 @@ static int pmbus_init_common(struct i2c_client *client, struct pmbus_data *data, } for (page = 0; page < info->pages; page++) { + if (info->phases[page] > PMBUS_PHASES) { + dev_err(dev, "Bad number of PMBus phases for page %d: %d\n", + page, info->phases[page]); + return -ENODEV; + } ret = pmbus_identify_common(client, data, page); if (ret < 0) { dev_err(dev, "Failed to identify chip capabilities\n"); From 2f74b0cb6aa3a493ef4b14f514e9b4a8b83a7202 Mon Sep 17 00:00:00 2001 From: Tze Yee Ng Date: Mon, 14 Sep 2026 17:13:35 +0800 Subject: [PATCH 58/97] hwmon: (socfpga) add Agilex 5 channel mapping Add temperature channel mapping for Agilex 5 and bind it to the "intel,agilex5-svc" compatible string. Reuse the Agilex voltage channels. Agilex 5 omits temperature channel 2 (top-left corner) because that sensor is not present; remaining sensors keep Agilex channel numbers. The "intel,agilex5-svc" compatible is added to the stratix10-svc match table by a separate patch [1]. Without that change the service layer does not bind on Agilex 5, so the socfpga-hwmon device is never created; this patch depends on it. Update the socfpga-hwmon documentation to list Agilex 5, the matching compatible string, and the SDM temperature/voltage channel tables. Signed-off-by: Tze Yee Ng Link: https://lore.kernel.org/all/20260907082203.2639395-4-adrian.ho.yin.ng@altera.com/ [1] Link: https://patch.msgid.link/41bb317cbfb6f4e2711581f2ccb22071db17b19e.1789373027.git.tze.yee.ng@altera.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/socfpga-hwmon.rst | 52 +++++++++++++++++++++++++++ drivers/hwmon/socfpga-hwmon.c | 22 ++++++++++++ 2 files changed, 74 insertions(+) diff --git a/Documentation/hwmon/socfpga-hwmon.rst b/Documentation/hwmon/socfpga-hwmon.rst index e5da42556a62..664103a38195 100644 --- a/Documentation/hwmon/socfpga-hwmon.rst +++ b/Documentation/hwmon/socfpga-hwmon.rst @@ -7,6 +7,7 @@ Supported chips: * Altera Stratix 10 SoC FPGA * Altera Agilex SoC FPGA + * Altera Agilex 5 SoC FPGA Authors: - Nazim Amirul @@ -32,3 +33,54 @@ driver based on the service layer compatible string: * intel,stratix10-svc * intel,agilex-svc + * intel,agilex5-svc + +Channel mappings are fixed in the driver (not described in DT). The tables +below list the SDM page/channel encodings used for each family. + +Temperature channels +~~~~~~~~~~~~~~~~~~~~ + +========== ==== ======= ================================= +Family Page Channel Label +========== ==== ======= ================================= +Stratix 10 0 0 Main Die SDM +Agilex 0 0 Main Die SDM +Agilex 1 0 Main Die corner bottom left max +Agilex 2 0 Main Die corner top left max +Agilex 3 0 Main Die corner bottom right max +Agilex 4 0 Main Die corner top right max +Agilex 5 0 0 Main Die SDM +Agilex 5 1 0 Main Die corner bottom left max +Agilex 5 3 0 Main Die corner bottom right max +Agilex 5 4 0 Main Die corner top right max +========== ==== ======= ================================= + +Agilex 5 omits SDM temperature channel 2 (top-left corner on Agilex) +because that sensor is not present in hardware. The remaining sensors keep +the same channel numbers as Agilex. + +Voltage channels +~~~~~~~~~~~~~~~~ + +========== ==== ======= ================= +Family Page Channel Label +========== ==== ======= ================= +Stratix 10 0 2 0.8V VCC +Stratix 10 0 3 1.8V VCCIO_SDM +Stratix 10 0 6 0.9V VCCERAM +Agilex 0 2 0.8V VCC +Agilex 0 3 1.8V VCCIO_SDM +Agilex 0 4 1.8V VCCPT +Agilex 0 5 1.2V VCCCRCORE +Agilex 0 6 0.9V VCCH +Agilex 0 7 0.8V VCCL +Agilex 5 0 2 0.8V VCC +Agilex 5 0 3 1.8V VCCIO_SDM +Agilex 5 0 4 1.8V VCCPT +Agilex 5 0 5 1.2V VCCCRCORE +Agilex 5 0 6 0.9V VCCH +Agilex 5 0 7 0.8V VCCL +========== ==== ======= ================= + +Agilex 5 reuses the Agilex voltage SDM page/channel layout and labels. diff --git a/drivers/hwmon/socfpga-hwmon.c b/drivers/hwmon/socfpga-hwmon.c index 5b43274d0aa2..5fe5ccab1e4f 100644 --- a/drivers/hwmon/socfpga-hwmon.c +++ b/drivers/hwmon/socfpga-hwmon.c @@ -433,6 +433,26 @@ static const struct socfpga_hwmon_board_data agilex_hwmon_board = { .num_volt = ARRAY_SIZE(agilex_hwmon_volt_channels), }; +/* + * Agilex 5 exposes the SDM and three corner temperature sensors. Channel 2 + * (top-left corner on Agilex) is not present in hardware, so the SDM channel + * numbering keeps the gap (0, 1, 3, 4) rather than renumbering. + */ +static const struct socfpga_hwmon_channel agilex5_hwmon_temp_channels[] = { + { SOCFPGA_HWMON_CHAN(0, 0), "Main Die SDM" }, + { SOCFPGA_HWMON_CHAN(1, 0), "Main Die corner bottom left max" }, + { SOCFPGA_HWMON_CHAN(3, 0), "Main Die corner bottom right max" }, + { SOCFPGA_HWMON_CHAN(4, 0), "Main Die corner top right max" }, +}; + +/* Agilex 5 reuses the Agilex voltage SDM page/channel encoding and labels. */ +static const struct socfpga_hwmon_board_data agilex5_hwmon_board = { + .temp = agilex5_hwmon_temp_channels, + .num_temp = ARRAY_SIZE(agilex5_hwmon_temp_channels), + .volt = agilex_hwmon_volt_channels, + .num_volt = ARRAY_SIZE(agilex_hwmon_volt_channels), +}; + static const struct socfpga_hwmon_board_data * socfpga_hwmon_get_board(struct device *dev) { @@ -443,6 +463,8 @@ socfpga_hwmon_get_board(struct device *dev) if (of_device_is_compatible(np, "intel,stratix10-svc")) return &s10_hwmon_board; + if (of_device_is_compatible(np, "intel,agilex5-svc")) + return &agilex5_hwmon_board; if (of_device_is_compatible(np, "intel,agilex-svc")) return &agilex_hwmon_board; From f4ba4f8cc8e8df45e57c1754955b3a6b9b089a97 Mon Sep 17 00:00:00 2001 From: Troy Mitchell Date: Mon, 14 Sep 2026 16:03:53 +0800 Subject: [PATCH 59/97] dt-bindings: vendor-prefixes: Add Sensylink Add the vendor prefix for Sensylink Microelectronics. Link: https://en.sensylink.com/ Acked-by: Krzysztof Kozlowski Signed-off-by: Troy Mitchell Link: https://patch.msgid.link/20260914-ctl2301-v5-1-b44b81cd2c3c@linux.dev Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml index ba2002969373..4a5cf436eb14 100644 --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml @@ -1495,6 +1495,8 @@ patternProperties: description: Sensirion AG "^sensortek,.*": description: Sensortek Technology Corporation + "^sensylink,.*": + description: Sensylink Microelectronics Technology Co., Ltd. "^sercomm,.*": description: Sercomm (Suzhou) Corporation "^sff,.*": From 2ebe47827efd944b7057e8987671b938736451d4 Mon Sep 17 00:00:00 2001 From: Troy Mitchell Date: Mon, 14 Sep 2026 16:03:54 +0800 Subject: [PATCH 60/97] dt-bindings: hwmon: Move LM63 family to a dedicated binding Move the National Semiconductor LM63, LM64, and LM96163 compatibles out of trivial-devices.yaml and into a dedicated binding. Describe their integrated fan control and preserve the optional interrupt used by the ALERT output. Permit use as thermal cooling devices via #cooling-cells. Reviewed-by: Krzysztof Kozlowski Signed-off-by: Troy Mitchell Link: https://patch.msgid.link/20260914-ctl2301-v5-2-b44b81cd2c3c@linux.dev Signed-off-by: Guenter Roeck --- .../bindings/hwmon/national,lm63.yaml | 53 +++++++++++++++++++ .../devicetree/bindings/trivial-devices.yaml | 6 --- 2 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 Documentation/devicetree/bindings/hwmon/national,lm63.yaml diff --git a/Documentation/devicetree/bindings/hwmon/national,lm63.yaml b/Documentation/devicetree/bindings/hwmon/national,lm63.yaml new file mode 100644 index 000000000000..f2e0993cfbef --- /dev/null +++ b/Documentation/devicetree/bindings/hwmon/national,lm63.yaml @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/hwmon/national,lm63.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: LM63-compatible temperature sensors with integrated fan control + +maintainers: + - Guenter Roeck + +description: | + The LM63 family provides local and remote temperature monitoring together + with fan speed monitoring and PWM fan control. Supported devices include + the National Semiconductor LM63, LM64 and LM96163. + +allOf: + - $ref: hwmon-common.yaml# + +properties: + compatible: + enum: + - national,lm63 + - national,lm64 + - national,lm96163 + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + "#cooling-cells": + const: 2 + +required: + - compatible + - reg + +unevaluatedProperties: false + +examples: + - | + i2c { + #address-cells = <1>; + #size-cells = <0>; + + temperature-sensor@4c { + compatible = "national,lm63"; + reg = <0x4c>; + #cooling-cells = <2>; + }; + }; diff --git a/Documentation/devicetree/bindings/trivial-devices.yaml b/Documentation/devicetree/bindings/trivial-devices.yaml index 94e63d8a58ad..7514025234fb 100644 --- a/Documentation/devicetree/bindings/trivial-devices.yaml +++ b/Documentation/devicetree/bindings/trivial-devices.yaml @@ -368,16 +368,10 @@ properties: - mps,mpq82d00 # Murata D1U74T-W power supply unit - murata,d1u74t - # Temperature sensor with integrated fan control - - national,lm63 - # Temperature sensor with integrated fan control - - national,lm64 # Temperature sensor - national,lm95235 # Temperature sensor - national,lm95245 - # Temperature sensor with integrated fan control - - national,lm96163 # Serial Interface ACPI-Compatible Microprocessor System Hardware Monitor - national,lm80 # Serial Interface ACPI-Compatible Microprocessor System Hardware Monitor From 12ad23ef7bea0331b19f1b203fa80df17fbaadf0 Mon Sep 17 00:00:00 2001 From: Troy Mitchell Date: Mon, 14 Sep 2026 16:18:03 +0800 Subject: [PATCH 61/97] dt-bindings: hwmon: Add Sensylink CTF2301 Add the Sensylink CTF2301 temperature sensor and fan controller to the LM63 family binding. The chip follows the LM63 register model and can provide a thermal cooling device when #cooling-cells is present. Do not provide a national,lm63 fallback because the CTF2301 is not fully compatible with LM63. It uses different conversion-rate encodings, extended local-temperature registers, additional fan lookup table entries, and device-specific PWM and ALERT/TACH configuration. An older LM63 driver would therefore not operate the device correctly. Acked-by: Krzysztof Kozlowski Signed-off-by: Troy Mitchell Link: https://patch.msgid.link/20260914-ctl2301-v5-3-522def70acb4@linux.dev Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/hwmon/national,lm63.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/hwmon/national,lm63.yaml b/Documentation/devicetree/bindings/hwmon/national,lm63.yaml index f2e0993cfbef..4c8d0a15fbd1 100644 --- a/Documentation/devicetree/bindings/hwmon/national,lm63.yaml +++ b/Documentation/devicetree/bindings/hwmon/national,lm63.yaml @@ -12,7 +12,8 @@ maintainers: description: | The LM63 family provides local and remote temperature monitoring together with fan speed monitoring and PWM fan control. Supported devices include - the National Semiconductor LM63, LM64 and LM96163. + the National Semiconductor LM63, LM64 and LM96163, and the Sensylink + CTF2301. allOf: - $ref: hwmon-common.yaml# @@ -23,6 +24,7 @@ properties: - national,lm63 - national,lm64 - national,lm96163 + - sensylink,ctf2301 reg: maxItems: 1 From d7f7e577c83795cd8480ee468fbe5a34948d0293 Mon Sep 17 00:00:00 2001 From: Troy Mitchell Date: Mon, 14 Sep 2026 16:18:04 +0800 Subject: [PATCH 62/97] hwmon: (lm63) Add Sensylink CTF2301 support Add Sensylink CTF2301 support to the LM63 driver. CTF2301 follows the LM63-style temperature, tachometer, and PWM register layout, while using a 12-bit local temperature register and requiring the ALERT/TACH pin to be configured for tachometer input. Register an optional thermal cooling device when the device tree node provides #cooling-cells, allowing thermal zones to control the fan through the existing PWM path without changing behavior for existing LM63-family users. Signed-off-by: Troy Mitchell Link: https://patch.msgid.link/20260914-ctl2301-v5-4-522def70acb4@linux.dev Signed-off-by: Guenter Roeck --- Documentation/hwmon/lm63.rst | 22 ++ drivers/hwmon/Kconfig | 8 +- drivers/hwmon/lm63.c | 426 +++++++++++++++++++++++++++++------ 3 files changed, 377 insertions(+), 79 deletions(-) diff --git a/Documentation/hwmon/lm63.rst b/Documentation/hwmon/lm63.rst index 9e27367d7405..c40bd0dfe591 100644 --- a/Documentation/hwmon/lm63.rst +++ b/Documentation/hwmon/lm63.rst @@ -33,6 +33,16 @@ Supported chips: http://www.national.com/pf/LM/LM96163.html + * Sensylink CTF2301 + + Prefix: 'ctf2301' + + Addresses scanned: none + + Datasheet: https://www.sensylink.com/upload/1/net.sensylink.portal/1689557281035.pdf + + Register description: https://github.com/TroyMitchell911/ctf2301-datasheet + Author: Jean Delvare @@ -62,6 +72,8 @@ value have to be masked out. The value is still 16 bit in width. All temperature values are given in degrees Celsius. Resolution is 1.0 degree for the local temperature, 0.125 degree for the remote temperature. +The CTF2301 local temperature input and limit have a resolution of 0.0625 +degree. The fan speed is measured using a tachometer. Contrary to most chips which store the value in an 8-bit register and have a selectable clock divider @@ -93,3 +105,13 @@ support these GPIO lines at present. The LM96163 is an enhanced version of LM63 with improved temperature accuracy and better PWM resolution. For LM96163, the external temperature sensor type is configurable as CPU embedded diode(1) or 3904 transistor(2). + +The CTF2301 is register-compatible with the LM63 family and provides 12 fan +control lookup table entries. It supports 8-bit PWM resolution when configured +for a 22.5 kHz PWM frequency. + +Device tree nodes may declare ``#cooling-cells`` to register the fan controller +with the thermal framework. The cooling state is mapped to the existing +``pwm1`` range from 0 to 255. The driver keeps the current automatic or manual +fan control mode during probe and switches to manual control when the thermal +framework first requests a cooling state. diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 4febde8ff43f..3d8b2c958b29 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -1548,10 +1548,10 @@ config SENSORS_LM63 depends on I2C help If you say yes here you get support for the National - Semiconductor LM63, LM64, and LM96163 remote diode digital temperature - sensors with integrated fan control. Such chips are found - on the Tyan S4882 (Thunder K8QS Pro) motherboard, among - others. + Semiconductor LM63, LM64, LM96163, and Sensylink CTF2301 + remote diode digital temperature sensors with integrated fan + control. Such chips are found on the Tyan S4882 (Thunder K8QS + Pro) motherboard, among others. This driver can also be built as a module. If so, the module will be called lm63. diff --git a/drivers/hwmon/lm63.c b/drivers/hwmon/lm63.c index da09770c05e7..607758bf7c58 100644 --- a/drivers/hwmon/lm63.c +++ b/drivers/hwmon/lm63.c @@ -35,13 +35,14 @@ #include #include #include +#include #include /* * Addresses to scan * Address is fully defined internally and cannot be changed except for * LM64 which has one pin dedicated to address selection. - * LM63 and LM96163 have address 0x4c. + * LM63, LM96163, and CTF2301 have address 0x4c. * LM64 can have address 0x18 or 0x4e. */ @@ -70,6 +71,9 @@ static const unsigned short normal_i2c[] = { 0x18, 0x4c, 0x4e, I2C_CLIENT_END }; #define LM63_REG_LOCAL_TEMP 0x00 #define LM63_REG_LOCAL_HIGH 0x05 +#define CTF2301_REG_LOCAL_TEMP_LSB 0x15 +#define CTF2301_REG_LOCAL_HIGH_LSB 0x06 + #define LM63_REG_REMOTE_TEMP_MSB 0x01 #define LM63_REG_REMOTE_TEMP_LSB 0x10 #define LM63_REG_REMOTE_OFFSET_MSB 0x11 @@ -94,6 +98,7 @@ static const unsigned short normal_i2c[] = { 0x18, 0x4c, 0x4e, I2C_CLIENT_END }; #define LM63_PWM_BASE_FAST_HZ 180000 #define LM63_PWM_BASE_SLOW_HZ 700 +#define LM63_MAX_PWM 255 #define LM63_MAX_CONVRATE 9 @@ -121,6 +126,9 @@ static const unsigned short normal_i2c[] = { 0x18, 0x4c, 0x4e, I2C_CLIENT_END }; 127000), 1000) #define TEMP8U_TO_REG(val) DIV_ROUND_CLOSEST(clamp_val((val), 0, \ 255000), 1000) +#define TEMP12_FROM_REG(reg) DIV_ROUND_CLOSEST((s16)(reg) * 1000, 256) +#define TEMP12_TO_REG(val) (DIV_ROUND_CLOSEST(clamp_val((val), -128000, \ + 127937) * 16, 1000) * 16) #define TEMP11_FROM_REG(reg) ((reg) / 32 * 125) #define TEMP11_TO_REG(val) (DIV_ROUND_CLOSEST(clamp_val((val), -128000, \ 127875), 125) * 32) @@ -132,7 +140,11 @@ static const unsigned short normal_i2c[] = { 0x18, 0x4c, 0x4e, I2C_CLIENT_END }; #define UPDATE_INTERVAL(max, rate) \ ((1000 << (LM63_MAX_CONVRATE - (rate))) / (max)) -enum chips { lm63, lm64, lm96163 }; +enum chips { ctf2301, lm63, lm64, lm96163 }; + +static const unsigned int ctf2301_update_intervals[] = { + 20000, 10000, 4902, 2463, 1000, 615, 307, 154, 107 +}; /* * Client data (each client gets its own) @@ -164,6 +176,8 @@ struct lm63_data { 1: local high limit 2: remote critical limit 3-14: lookup table */ + s16 temp1_input; /* local input for chips with extended resolution */ + s16 temp1_max; /* local limit for chips with extended resolution */ s16 temp11[4]; /* 0: remote input 1: remote low limit 2: remote high limit @@ -229,6 +243,7 @@ static struct lm63_data *lm63_update_device(struct device *dev) struct lm63_data *data = dev_get_drvdata(dev); struct i2c_client *client = data->client; unsigned long next_update; + int msb, lsb; mutex_lock(&data->update_lock); @@ -253,10 +268,26 @@ static struct lm63_data *lm63_update_device(struct device *dev) data->pwm1[0] = i2c_smbus_read_byte_data(client, LM63_REG_PWM_VALUE); - data->temp8[0] = i2c_smbus_read_byte_data(client, - LM63_REG_LOCAL_TEMP); - data->temp8[1] = i2c_smbus_read_byte_data(client, - LM63_REG_LOCAL_HIGH); + if (data->kind == ctf2301) { + msb = i2c_smbus_read_byte_data(client, + LM63_REG_LOCAL_TEMP); + lsb = i2c_smbus_read_byte_data(client, + CTF2301_REG_LOCAL_TEMP_LSB); + if (msb >= 0 && lsb >= 0) + data->temp1_input = ((u16)(u8)msb << 8) | lsb; + + msb = i2c_smbus_read_byte_data(client, + LM63_REG_LOCAL_HIGH); + lsb = i2c_smbus_read_byte_data(client, + CTF2301_REG_LOCAL_HIGH_LSB); + if (msb >= 0 && lsb >= 0) + data->temp1_max = ((u16)(u8)msb << 8) | lsb; + } else { + data->temp8[0] = i2c_smbus_read_byte_data(client, + LM63_REG_LOCAL_TEMP); + data->temp8[1] = i2c_smbus_read_byte_data(client, + LM63_REG_LOCAL_HIGH); + } /* order matters for temp2_input */ data->temp11[0] = i2c_smbus_read_byte_data(client, @@ -276,7 +307,7 @@ static struct lm63_data *lm63_update_device(struct device *dev) | i2c_smbus_read_byte_data(client, LM63_REG_REMOTE_OFFSET_LSB); - if (data->kind == lm96163) + if (data->kind == lm96163 || data->kind == ctf2301) data->temp11u = (i2c_smbus_read_byte_data(client, LM96163_REG_REMOTE_TEMP_U_MSB) << 8) | i2c_smbus_read_byte_data(client, @@ -366,6 +397,49 @@ static ssize_t set_fan(struct device *dev, struct device_attribute *dummy, return count; } +static int lm63_pwm_from_reg(struct lm63_data *data, u8 reg) +{ + if (data->pwm_highres) + return reg; + + return reg >= 2 * data->pwm1_freq ? + LM63_MAX_PWM : + (reg * LM63_MAX_PWM + data->pwm1_freq) / (2 * data->pwm1_freq); +} + +static u8 lm63_pwm_to_reg(struct lm63_data *data, unsigned long val) +{ + val = clamp_val(val, 0, LM63_MAX_PWM); + + if (data->pwm_highres) + return val; + + return (val * data->pwm1_freq * 2 + LM63_MAX_PWM / 2) / LM63_MAX_PWM; +} + +static void lm63_update_pwm_highres(struct lm63_data *data, + int config_enhanced) +{ + data->pwm_highres = !(data->config_fan & 0x08) && + data->pwm1_freq == 8 && + (config_enhanced & 0x10); +} + +static int lm63_write_pwm(struct lm63_data *data, int nr, unsigned long val) +{ + struct i2c_client *client = data->client; + u8 reg = nr ? LM63_REG_LUT_PWM(nr - 1) : LM63_REG_PWM_VALUE; + u8 regval = lm63_pwm_to_reg(data, val); + int ret; + + ret = i2c_smbus_write_byte_data(client, reg, regval); + if (ret) + return ret; + data->pwm1[nr] = regval; + + return 0; +} + static ssize_t show_pwm1(struct device *dev, struct device_attribute *devattr, char *buf) { @@ -375,12 +449,7 @@ static ssize_t show_pwm1(struct device *dev, struct device_attribute *devattr, int pwm; mutex_lock(&data->update_lock); - if (data->pwm_highres) - pwm = data->pwm1[nr]; - else - pwm = data->pwm1[nr] >= 2 * data->pwm1_freq ? - 255 : (data->pwm1[nr] * 255 + data->pwm1_freq) / - (2 * data->pwm1_freq); + pwm = lm63_pwm_from_reg(data, data->pwm1[nr]); mutex_unlock(&data->update_lock); return sprintf(buf, "%d\n", pwm); @@ -391,11 +460,9 @@ static ssize_t set_pwm1(struct device *dev, struct device_attribute *devattr, { struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); struct lm63_data *data = dev_get_drvdata(dev); - struct i2c_client *client = data->client; int nr = attr->index; unsigned long val; int err; - u8 reg; if (!(data->config_fan & 0x20)) /* register is read-only */ return -EPERM; @@ -404,15 +471,11 @@ static ssize_t set_pwm1(struct device *dev, struct device_attribute *devattr, if (err) return err; - reg = nr ? LM63_REG_LUT_PWM(nr - 1) : LM63_REG_PWM_VALUE; - val = clamp_val(val, 0, 255); - mutex_lock(&data->update_lock); - data->pwm1[nr] = data->pwm_highres ? val : - (val * data->pwm1_freq * 2 + 127) / 255; - i2c_smbus_write_byte_data(client, reg, data->pwm1[nr]); + err = lm63_write_pwm(data, nr, val); mutex_unlock(&data->update_lock); - return count; + + return err ? err : count; } static ssize_t pwm1_enable_show(struct device *dev, @@ -485,7 +548,9 @@ static ssize_t pwm1_freq_store(struct device *dev, struct lm63_data *data = dev_get_drvdata(dev); struct i2c_client *client = data->client; unsigned long val, pfr_fast, pfr_slow, err_fast, err_slow, pfr; + u8 config_fan; bool slow_clock; + int config_enhanced = 0; int ret; ret = kstrtoul(buf, 10, &val); @@ -511,35 +576,58 @@ static ssize_t pwm1_freq_store(struct device *dev, mutex_lock(&data->update_lock); ret = i2c_smbus_read_byte_data(client, LM63_REG_CONFIG_FAN); - if (ret < 0) { - mutex_unlock(&data->update_lock); - return ret; - } - data->config_fan = ret; + if (ret < 0) + goto unlock; + config_fan = ret; + data->config_fan = config_fan; - if (!(data->config_fan & 0x20)) { /* register is read-only */ - mutex_unlock(&data->update_lock); - return -EPERM; + if (!(config_fan & 0x20)) { /* register is read-only */ + ret = -EPERM; + goto unlock; } - if (data->kind == lm96163) { - ret = i2c_smbus_read_byte_data(client, LM96163_REG_CONFIG_ENHANCED); - if (ret < 0) { - mutex_unlock(&data->update_lock); - return ret; + if (data->kind == lm96163 || data->kind == ctf2301) { + ret = i2c_smbus_read_byte_data(client, + LM96163_REG_CONFIG_ENHANCED); + if (ret < 0) + goto unlock; + config_enhanced = ret; + lm63_update_pwm_highres(data, config_enhanced); + + if (data->kind == ctf2301) { + if (!slow_clock && pfr == 8) + config_enhanced |= 0x10; + else + config_enhanced &= ~0x10; + ret = i2c_smbus_write_byte_data(client, + LM96163_REG_CONFIG_ENHANCED, + config_enhanced); + if (ret) + goto unlock; + lm63_update_pwm_highres(data, config_enhanced); } - data->pwm_highres = !slow_clock && pfr == 8 && (ret & 0x10); } if (slow_clock) - data->config_fan |= 0x08; + config_fan |= 0x08; else - data->config_fan &= ~0x08; - i2c_smbus_write_byte_data(client, LM63_REG_CONFIG_FAN, data->config_fan); - i2c_smbus_write_byte_data(client, LM63_REG_PWM_FREQ, pfr); + config_fan &= ~0x08; + ret = i2c_smbus_write_byte_data(client, LM63_REG_PWM_FREQ, pfr); + if (ret) + goto unlock; data->pwm1_freq = pfr; + lm63_update_pwm_highres(data, config_enhanced); + ret = i2c_smbus_write_byte_data(client, LM63_REG_CONFIG_FAN, + config_fan); + if (ret) + goto unlock; + + data->config_fan = config_fan; + lm63_update_pwm_highres(data, config_enhanced); +unlock: mutex_unlock(&data->update_lock); - return count; + + return ret ? ret : count; } /* @@ -554,7 +642,17 @@ static ssize_t show_local_temp8(struct device *dev, { struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); struct lm63_data *data = lm63_update_device(dev); - return sprintf(buf, "%d\n", TEMP8_FROM_REG(data->temp8[attr->index])); + int temp; + + mutex_lock(&data->update_lock); + if (data->kind == ctf2301) + temp = TEMP12_FROM_REG(attr->index ? data->temp1_max : + data->temp1_input); + else + temp = TEMP8_FROM_REG(data->temp8[attr->index]); + mutex_unlock(&data->update_lock); + + return sprintf(buf, "%d\n", temp); } static ssize_t show_remote_temp8(struct device *dev, @@ -594,6 +692,21 @@ static ssize_t set_temp8(struct device *dev, struct device_attribute *devattr, return err; mutex_lock(&data->update_lock); + if (data->kind == ctf2301 && nr == 1) { + temp = TEMP12_TO_REG(val); + err = i2c_smbus_write_byte_data(client, LM63_REG_LOCAL_HIGH, + temp >> 8); + if (!err) + err = i2c_smbus_write_byte_data(client, + CTF2301_REG_LOCAL_HIGH_LSB, + temp & 0xf0); + if (!err) + data->temp1_max = temp; + mutex_unlock(&data->update_lock); + + return err ? err : count; + } + switch (nr) { case 2: reg = LM63_REG_REMOTE_TCRIT; @@ -610,10 +723,12 @@ static ssize_t set_temp8(struct device *dev, struct device_attribute *devattr, reg = LM63_REG_LUT_TEMP(nr - 3); temp = lut_temp_to_reg(data, val); } - data->temp8[nr] = temp; - i2c_smbus_write_byte_data(client, reg, temp); + err = i2c_smbus_write_byte_data(client, reg, temp); + if (!err) + data->temp8[nr] = temp; mutex_unlock(&data->update_lock); - return count; + + return err ? err : count; } static ssize_t show_temp11(struct device *dev, struct device_attribute *devattr, @@ -787,11 +902,40 @@ static ssize_t temp2_crit_hyst_store(struct device *dev, * Set conversion rate. * client->update_lock must be held when calling this function. */ -static void lm63_set_convrate(struct lm63_data *data, unsigned int interval) +static unsigned int lm63_update_interval(struct lm63_data *data, u8 rate) +{ + if (data->kind == ctf2301) + return ctf2301_update_intervals[min_t(unsigned int, rate, + ARRAY_SIZE(ctf2301_update_intervals) - 1)]; + + return UPDATE_INTERVAL(data->max_convrate_hz, rate); +} + +static int lm63_set_convrate(struct lm63_data *data, unsigned int interval) { struct i2c_client *client = data->client; - unsigned int update_interval; - int i; + unsigned int update_interval, error, best_error = ~0U; + unsigned int best = 0; + unsigned int i; + int ret; + + if (data->kind == ctf2301) { + for (i = 0; i < ARRAY_SIZE(ctf2301_update_intervals); i++) { + error = abs_diff(interval, ctf2301_update_intervals[i]); + if (error < best_error) { + best_error = error; + best = i; + } + } + + ret = i2c_smbus_write_byte_data(client, LM63_REG_CONVRATE, + best); + if (ret) + return ret; + data->update_interval = ctf2301_update_intervals[best]; + + return 0; + } /* Shift calculations to avoid rounding errors */ interval <<= 6; @@ -803,8 +947,12 @@ static void lm63_set_convrate(struct lm63_data *data, unsigned int interval) if (interval >= update_interval * 3 / 4) break; - i2c_smbus_write_byte_data(client, LM63_REG_CONVRATE, i); + ret = i2c_smbus_write_byte_data(client, LM63_REG_CONVRATE, i); + if (ret) + return ret; data->update_interval = UPDATE_INTERVAL(data->max_convrate_hz, i); + + return 0; } static ssize_t update_interval_show(struct device *dev, @@ -828,10 +976,10 @@ static ssize_t update_interval_store(struct device *dev, return err; mutex_lock(&data->update_lock); - lm63_set_convrate(data, clamp_val(val, 0, 100000)); + err = lm63_set_convrate(data, clamp_val(val, 0, 100000)); mutex_unlock(&data->update_lock); - return count; + return err ? err : count; } static ssize_t temp2_type_show(struct device *dev, @@ -996,6 +1144,75 @@ static DEVICE_ATTR_RO(alarms); static DEVICE_ATTR_RW(update_interval); +static int lm63_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + *state = LM63_MAX_PWM; + + return 0; +} + +static int lm63_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct lm63_data *data = cdev->devdata; + int ret; + + mutex_lock(&data->update_lock); + ret = i2c_smbus_read_byte_data(data->client, LM63_REG_PWM_VALUE); + if (ret < 0) + goto unlock; + + data->pwm1[0] = ret; + *state = lm63_pwm_from_reg(data, data->pwm1[0]); +unlock: + mutex_unlock(&data->update_lock); + + return ret < 0 ? ret : 0; +} + +static int lm63_set_cur_state(struct thermal_cooling_device *cdev, + unsigned long state) +{ + struct lm63_data *data = cdev->devdata; + int config_fan; + int ret; + + if (state > LM63_MAX_PWM) + return -EINVAL; + + mutex_lock(&data->update_lock); + config_fan = i2c_smbus_read_byte_data(data->client, + LM63_REG_CONFIG_FAN); + if (config_fan < 0) { + ret = config_fan; + goto unlock; + } + data->config_fan = config_fan; + + if (!(config_fan & 0x20)) { + config_fan |= 0x20; + ret = i2c_smbus_write_byte_data(data->client, + LM63_REG_CONFIG_FAN, + config_fan); + if (ret) + goto unlock; + data->config_fan = config_fan; + } + + ret = lm63_write_pwm(data, 0, state); +unlock: + mutex_unlock(&data->update_lock); + + return ret; +} + +static const struct thermal_cooling_device_ops lm63_cooling_ops = { + .get_max_state = lm63_get_max_state, + .get_cur_state = lm63_get_cur_state, + .set_cur_state = lm63_set_cur_state, +}; + static struct attribute *lm63_attributes[] = { &sensor_dev_attr_pwm1.dev_attr.attr, &dev_attr_pwm1_enable.attr, @@ -1078,8 +1295,8 @@ static const struct attribute_group lm63_group_extra_lut = { * On LM63, temp2_crit can be set only once, which should be job * of the bootloader. * On LM64, temp2_crit can always be set. - * On LM96163, temp2_crit can be set if bit 1 of the configuration - * register is true. + * On LM96163 and CTF2301, temp2_crit can be set if bit 1 of the + * configuration register is true. */ static umode_t lm63_attribute_mode(struct kobject *kobj, struct attribute *attr, int index) @@ -1089,7 +1306,8 @@ static umode_t lm63_attribute_mode(struct kobject *kobj, if (attr == &sensor_dev_attr_temp2_crit.dev_attr.attr && (data->kind == lm64 || - (data->kind == lm96163 && (data->config & 0x02)))) + ((data->kind == lm96163 || data->kind == ctf2301) && + (data->config & 0x02)))) return attr->mode | S_IWUSR; return attr->mode; @@ -1164,29 +1382,48 @@ static int lm63_detect(struct i2c_client *client, * Ideally we shouldn't have to initialize anything, since the BIOS * should have taken care of everything */ -static void lm63_init_client(struct lm63_data *data) +static int lm63_init_client(struct lm63_data *data) { struct i2c_client *client = data->client; struct device *dev = &client->dev; u8 convrate; + int ret; - data->config = i2c_smbus_read_byte_data(client, LM63_REG_CONFIG1); - data->config_fan = i2c_smbus_read_byte_data(client, - LM63_REG_CONFIG_FAN); + ret = i2c_smbus_read_byte_data(client, LM63_REG_CONFIG1); + if (ret < 0) + return ret; + data->config = ret; + ret = i2c_smbus_read_byte_data(client, LM63_REG_CONFIG_FAN); + if (ret < 0) + return ret; + data->config_fan = ret; /* Start converting if needed */ if (data->config & 0x40) { /* standby */ dev_dbg(dev, "Switching to operational mode\n"); data->config &= 0xA7; - i2c_smbus_write_byte_data(client, LM63_REG_CONFIG1, - data->config); + ret = i2c_smbus_write_byte_data(client, LM63_REG_CONFIG1, + data->config); + if (ret) + return ret; } /* Tachometer is always enabled on LM64 */ if (data->kind == lm64) data->config |= 0x04; + if (data->kind == ctf2301 && !client->irq && !(data->config & 0x04)) { + dev_dbg(dev, "Configuring ALERT/TACH pin for tachometer input\n"); + data->config |= 0x04; + ret = i2c_smbus_write_byte_data(client, LM63_REG_CONFIG1, + data->config); + if (ret) + return ret; + } /* We may need pwm1_freq before ever updating the client data */ - data->pwm1_freq = i2c_smbus_read_byte_data(client, LM63_REG_PWM_FREQ) & 0x1f; + ret = i2c_smbus_read_byte_data(client, LM63_REG_PWM_FREQ); + if (ret < 0) + return ret; + data->pwm1_freq = ret & 0x1f; if (data->pwm1_freq == 0) data->pwm1_freq = 1; @@ -1199,25 +1436,38 @@ static void lm63_init_client(struct lm63_data *data) case lm96163: data->max_convrate_hz = LM96163_MAX_CONVRATE_HZ; data->lut_size = 12; - data->trutherm - = i2c_smbus_read_byte_data(client, - LM96163_REG_TRUTHERM) & 0x02; + ret = i2c_smbus_read_byte_data(client, LM96163_REG_TRUTHERM); + if (ret < 0) + return ret; + data->trutherm = ret & 0x02; + break; + case ctf2301: + data->lut_size = 12; break; } - convrate = i2c_smbus_read_byte_data(client, LM63_REG_CONVRATE); - if (unlikely(convrate > LM63_MAX_CONVRATE)) + ret = i2c_smbus_read_byte_data(client, LM63_REG_CONVRATE); + if (ret < 0) + return ret; + convrate = ret; + if (data->kind == ctf2301 && + convrate >= ARRAY_SIZE(ctf2301_update_intervals)) + convrate = ARRAY_SIZE(ctf2301_update_intervals) - 1; + else if (unlikely(convrate > LM63_MAX_CONVRATE)) convrate = LM63_MAX_CONVRATE; - data->update_interval = UPDATE_INTERVAL(data->max_convrate_hz, - convrate); + data->update_interval = lm63_update_interval(data, convrate); /* - * For LM96163, check if high resolution PWM + * For LM96163 and CTF2301, check if high resolution PWM * and unsigned temperature format is enabled. */ - if (data->kind == lm96163) { - u8 config_enhanced - = i2c_smbus_read_byte_data(client, - LM96163_REG_CONFIG_ENHANCED); + if (data->kind == lm96163 || data->kind == ctf2301) { + u8 config_enhanced; + + ret = i2c_smbus_read_byte_data(client, + LM96163_REG_CONFIG_ENHANCED); + if (ret < 0) + return ret; + config_enhanced = ret; if (config_enhanced & 0x20) data->lut_temp_highres = true; if ((config_enhanced & 0x10) @@ -1238,6 +1488,8 @@ static void lm63_init_client(struct lm63_data *data) dev_dbg(dev, "PWM output active %s, %s mode\n", (data->config_fan & 0x10) ? "low" : "high", (data->config_fan & 0x20) ? "manual" : "auto"); + + return 0; } static const struct i2c_device_id lm63_id[]; @@ -1245,9 +1497,11 @@ static const struct i2c_device_id lm63_id[]; static int lm63_probe(struct i2c_client *client) { struct device *dev = &client->dev; + struct thermal_cooling_device *cdev; struct device *hwmon_dev; struct lm63_data *data; int groups = 0; + int ret; data = devm_kzalloc(dev, sizeof(struct lm63_data), GFP_KERNEL); if (!data) @@ -1262,7 +1516,9 @@ static int lm63_probe(struct i2c_client *client) data->temp2_offset = 16000; /* Initialize chip */ - lm63_init_client(data); + ret = lm63_init_client(data); + if (ret) + return dev_err_probe(dev, ret, "failed to initialize device\n"); /* Register sysfs hooks */ data->groups[groups++] = &lm63_group; @@ -1272,11 +1528,26 @@ static int lm63_probe(struct i2c_client *client) if (data->kind == lm96163) { data->groups[groups++] = &lm63_group_temp2_type; data->groups[groups++] = &lm63_group_extra_lut; + } else if (data->kind == ctf2301) { + data->groups[groups++] = &lm63_group_extra_lut; } hwmon_dev = devm_hwmon_device_register_with_groups(dev, client->name, data, data->groups); - return PTR_ERR_OR_ZERO(hwmon_dev); + if (IS_ERR(hwmon_dev)) + return PTR_ERR(hwmon_dev); + + if (IS_ENABLED(CONFIG_THERMAL_OF) && + of_property_present(dev->of_node, "#cooling-cells")) { + cdev = devm_thermal_of_cooling_device_register(dev, 0, + client->name, + data, + &lm63_cooling_ops); + if (IS_ERR(cdev)) + return PTR_ERR(cdev); + } + + return 0; } /* @@ -1287,6 +1558,7 @@ static const struct i2c_device_id lm63_id[] = { { .name = "lm63", .driver_data = lm63 }, { .name = "lm64", .driver_data = lm64 }, { .name = "lm96163", .driver_data = lm96163 }, + { .name = "ctf2301", .driver_data = ctf2301 }, { } }; MODULE_DEVICE_TABLE(i2c, lm63_id); @@ -1304,6 +1576,10 @@ static const struct of_device_id __maybe_unused lm63_of_match[] = { .compatible = "national,lm96163", .data = (void *)lm96163 }, + { + .compatible = "sensylink,ctf2301", + .data = (void *)ctf2301 + }, { }, }; MODULE_DEVICE_TABLE(of, lm63_of_match); From c6ac9f8615019b57d6b53f091479f5a541896aee Mon Sep 17 00:00:00 2001 From: Alexis Czezar Torreno Date: Mon, 14 Sep 2026 15:45:17 +0800 Subject: [PATCH 63/97] hwmon: (pmbus/max34440) Add support for ADPM12300 ADPM12300 is a quarter brick DC/DC Power Module. It is a high power non-isolated converter capable of delivering regulated 12V with continuous power level of 3000W. Uses PMBus. Signed-off-by: Alexis Czezar Torreno Link: https://patch.msgid.link/20260914-dev_adpm12300-v1-1-15fc8143d457@analog.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/max34440.rst | 28 ++++++++++++++++++---------- drivers/hwmon/pmbus/Kconfig | 4 ++-- drivers/hwmon/pmbus/max34440.c | 2 ++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Documentation/hwmon/max34440.rst b/Documentation/hwmon/max34440.rst index 866f22b88e9b..8ce10aa425d5 100644 --- a/Documentation/hwmon/max34440.rst +++ b/Documentation/hwmon/max34440.rst @@ -27,6 +27,14 @@ Supported chips: Datasheet: - + * ADI ADPM12300 + + Prefixes: 'adpm12300' + + Addresses scanned: - + + Datasheet: - + * Maxim MAX34440 Prefixes: 'max34440' @@ -105,11 +113,11 @@ This driver supports multiple devices: hardware monitoring for Maxim MAX34440 PMBus 6-Channel Power-Supply Manager, MAX34441 PMBus 5-Channel Power-Supply Manager and Intelligent Fan Controller, and MAX34446 PMBus Power-Supply Data Logger; PMBus Voltage Monitor and Sequencers for MAX34451, MAX34452, MAX34460, -and MAX34461; PMBus DC/DC Power Module ADPM12160, ADPM12200, and ADPM12250. The -MAX34451 and MAX34452 support monitoring voltage or current of 16 channels based -on GIN pins. The MAX34460 supports 12 voltage channels, and the MAX34461 supports -16 voltage channels. The ADPM12160, ADPM12200, and ADPM12250 also monitor both -input and output of voltage and current. +and MAX34461; PMBus DC/DC Power Module ADPM12160, ADPM12200, ADPM12250 and +ADPM12300. The MAX34451 and MAX34452 support monitoring voltage or current of 16 +channels based on GIN pins. The MAX34460 supports 12 voltage channels, and the +MAX34461 supports 16 voltage channels. The ADPM12160, ADPM12200, ADPM12250 and +ADPM12300 also monitor both input and output of voltage and current. The driver is a client driver to the core PMBus driver. Please see Documentation/hwmon/pmbus.rst for details on PMBus client drivers. @@ -167,8 +175,8 @@ in[1-6]_reset_history Write any value to reset history. .. note:: - MAX34446 only supports in[1-4]. - - ADPM12160, ADPM12200, and ADPM12250 only supports in[1-2]. Label is "vin1" - and "vout1" respectively. + - ADPM12160, ADPM12200, ADPM12250, and ADPM12300 only supports in[1-2]. + Label is "vin1" and "vout1" respectively. Curr ~~~~ @@ -190,9 +198,9 @@ curr[1-6]_reset_history Write any value to reset history. - in6 and curr6 attributes only exist for MAX34440. - MAX34446 only supports curr[1-4]. - - For ADPM12160, ADPM12200, and ADPM12250, curr[1] is "iin1" + - For ADPM12160, ADPM12200, ADPM12250, and ADPM12300, curr[1] is "iin1" - For ADPM12160, and ADPM12200 curr[2-6] are "iout[1-5]". - - For ADPM12250, curr[2-4] are "iout[1-3]". + - For ADPM12250, and ADPM12300 curr[2-4] are "iout[1-3]". Power ~~~~~ @@ -228,7 +236,7 @@ temp[1-8]_reset_history Write any value to reset history. .. note:: - temp7 and temp8 attributes only exist for MAX34440. - MAX34446 only supports temp[1-3]. - - ADPM12160, ADPM12200, and ADPM12250 only supports temp[1]. + - ADPM12160, ADPM12200, ADPM12250, and ADPM12300 only supports temp[1]. .. note:: diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig index 3289ab50a028..90305d5d59e8 100644 --- a/drivers/hwmon/pmbus/Kconfig +++ b/drivers/hwmon/pmbus/Kconfig @@ -454,8 +454,8 @@ config SENSORS_MAX34440 help If you say yes here you get hardware monitoring support for Maxim MAX34440, MAX34441, MAX34446, MAX34451, MAX34452, MAX34460, and - MAX34461. Other compatible devices are ADPM12160, ADPM12200, and - ADPM12250. + MAX34461. Other compatible devices are ADPM12160, ADPM12200, + ADPM12250, and ADPM12300. This driver can also be built as a module. If so, the module will be called max34440. diff --git a/drivers/hwmon/pmbus/max34440.c b/drivers/hwmon/pmbus/max34440.c index 866d25ae8971..7f71dfe48643 100644 --- a/drivers/hwmon/pmbus/max34440.c +++ b/drivers/hwmon/pmbus/max34440.c @@ -812,10 +812,12 @@ static int max34440_probe(struct i2c_client *client) return pmbus_do_probe(client, &data->info); } +/* ADPM12300 is just ADPM12250 with higher power capacity. Reuses driver data */ static const struct i2c_device_id max34440_id[] = { { .name = "adpm12160", .driver_data = adpm12160 }, { .name = "adpm12200", .driver_data = adpm12200 }, { .name = "adpm12250", .driver_data = adpm12250 }, + { .name = "adpm12300", .driver_data = adpm12250 }, { .name = "max34440", .driver_data = max34440 }, { .name = "max34441", .driver_data = max34441 }, { .name = "max34446", .driver_data = max34446 }, From e886b6788f58fd4696b7548a7d8d47b4ba18a167 Mon Sep 17 00:00:00 2001 From: Patrick Pepin Date: Mon, 14 Sep 2026 08:09:55 +0000 Subject: [PATCH 64/97] hwmon: (dell-smm) Add Dell Precision 3650 Tower to fan control whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the Dell Precision 3650 Tower, the EC ignores fan speed writes unless automatic BIOS fan control is disabled first. The I8K_FAN_30A3_31A3 SMM code pair works on this machine through the WMI-SMM backend: after writing 1 to pwm1_enable, all three fans (CPU, front, top) accept the three fan states and report matching RPM, and writing 2 hands control back to the BIOS. I8K_FAN_34A3_35A3 was tested as well and also works; I8K_FAN_30A3_31A3 is used since it matches recent OEM software. Tested on BIOS 1.48.0 with kernels 7.0.0-30 and 7.0.0-31 (Ubuntu 26.04). Signed-off-by: Patrick Pepin Acked-by: Pali Rohár Link: https://patch.msgid.link/20260914080955.87925-1-patpep@me.com Signed-off-by: Guenter Roeck --- drivers/hwmon/dell-smm-hwmon.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/hwmon/dell-smm-hwmon.c b/drivers/hwmon/dell-smm-hwmon.c index 7ed94a3db621..99a58ed263ab 100644 --- a/drivers/hwmon/dell-smm-hwmon.c +++ b/drivers/hwmon/dell-smm-hwmon.c @@ -1639,6 +1639,14 @@ static const struct dmi_system_id i8k_whitelist_fan_control[] __initconst = { }, .driver_data = (void *)&i8k_fan_control_data[I8K_FAN_34A3_35A3], }, + { + .ident = "Dell Precision 3650 Tower", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Precision 3650 Tower"), + }, + .driver_data = (void *)&i8k_fan_control_data[I8K_FAN_30A3_31A3], + }, { .ident = "Dell Optiplex 7090", .matches = { From 1001be13fa5c2adca3466b545a12f8557879f2bb Mon Sep 17 00:00:00 2001 From: Sanman Pradhan Date: Tue, 15 Sep 2026 16:48:41 +0000 Subject: [PATCH 65/97] dt-bindings: trivial-devices: Add TI TPS536C7 TPS536C7 is a PMBus-compliant dual-channel D-CAP+ multiphase step-down controller. Add its compatible string. Signed-off-by: Sanman Pradhan Acked-by: Conor Dooley Link: https://patch.msgid.link/20260915164823.160977-3-sanman.pradhan@hpe.com Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/trivial-devices.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/trivial-devices.yaml b/Documentation/devicetree/bindings/trivial-devices.yaml index 7514025234fb..5dd6a1aca440 100644 --- a/Documentation/devicetree/bindings/trivial-devices.yaml +++ b/Documentation/devicetree/bindings/trivial-devices.yaml @@ -524,6 +524,8 @@ properties: - ti,tps53685 # TI Dual channel DCAP+ multiphase controller TPS53688 - ti,tps53688 + # TI Dual channel DCAP+ multiphase controller TPS536C7 + - ti,tps536c7 # TI DC-DC converters on PMBus - ti,tps544b20 - ti,tps544b25 From f173bf7161def6ab83f95363857869b64cfc0c45 Mon Sep 17 00:00:00 2001 From: Sanman Pradhan Date: Tue, 15 Sep 2026 16:48:46 +0000 Subject: [PATCH 66/97] hwmon: (pmbus/tps53679) Add support for TPS536C7 TPS536C7 is a dual-channel D-CAP+ step-down controller whose channels are distributed across a configurable number of phases (N+M, up to 12 total). Whether channel B (PMBus page 1) exists depends on the phase configuration register, so the page count is derived at probe time. Factor the TPS53676 phase-map parsing into a shared helper and reuse it for TPS536C7. Only aggregate per-channel telemetry is exposed: leave info->phases[] unset so the PMBus core never programs the PHASE selector on every read (and because TPS536C7 can place up to 12 phases on channel A, exceeding PMBUS_PHASES). Report VOUT in linear format like TPS53676. Since the core never programs PHASE, set it to 0xff on each populated page so READ_IOUT reports the aggregate channel current, and verify it (PHASE can be write-protected). Fail identification if aggregate selection cannot be established, since otherwise a single phase current could be reported as the channel total. A configuration with no channel-A phase is rejected with -EOPNOTSUPP: the PMBus core models pages as the contiguous range 0..pages-1 and cannot represent a channel-A-absent topology. On the tested TPS536C7 (device ID 54 49 53 6c 70 00, CAPABILITY 0xd0), block reads of IC_DEVICE_ID and USER_DATA_03 return -EBADMSG with PEC enabled, while retrying those two reads without PEC returns the expected 6- and 24-byte payloads. Standard byte/word telemetry keeps using PEC, so only those two identification reads fall back to no-PEC. Co-developed-by: Senthil Muniyappan Co-developed-by: Vaibhav Agarwal Signed-off-by: Sanman Pradhan Link: https://patch.msgid.link/20260915164823.160977-4-sanman.pradhan@hpe.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/tps53679.rst | 23 ++++-- drivers/hwmon/pmbus/Kconfig | 2 +- drivers/hwmon/pmbus/tps53679.c | 128 +++++++++++++++++++++++++++---- 3 files changed, 134 insertions(+), 19 deletions(-) diff --git a/Documentation/hwmon/tps53679.rst b/Documentation/hwmon/tps53679.rst index 2280e043c4de..3d584197b3a0 100644 --- a/Documentation/hwmon/tps53679.rst +++ b/Documentation/hwmon/tps53679.rst @@ -75,6 +75,14 @@ Supported chips: Datasheet: Available under NDA + * Texas Instruments TPS536C7 + + Prefix: 'tps536c7' + + Addresses scanned: - + + Datasheet: https://www.ti.com/lit/gpn/TPS536C7 + Authors: Vadim Pasternak @@ -85,7 +93,12 @@ Description ----------- Chips in this series are multi-phase step-down converters with one or two -output channels and up to 8 phases per channel. +output channels and up to 12 phases in total, depending on the device. + +For TPS536C7 only aggregate per-channel telemetry is exposed; the +individual per-phase output currents the hardware can report are not. +Attributes for the second output channel are present only when channel B +is configured. Usage Notes @@ -124,7 +137,7 @@ in1_crit_alarm Input voltage critical high alarm. in[N]_label "vout[1-2]" - TPS53647, TPS53667: N=2 - - TPS53622, TPS53659, TPS53679, TPS53688: N=2,3 + - TPS53622, TPS53659, TPS53679, TPS53688, TPS536C7: N=2,3 in[N]_input Measured output voltage. @@ -151,7 +164,7 @@ in[N]_crit_alarm Output voltage critical high alarm. temp[N]_input Measured temperature. - TPS53647, TPS53667: N=1 - - TPS53622, TPS53659, TPS53679, TPS53681, TPS53688: N=1,2 + - TPS53622, TPS53659, TPS53679, TPS53681, TPS53688, TPS536C7: N=1,2 temp[N]_max Maximum temperature. @@ -168,7 +181,7 @@ power1_input Measured input power. power[N]_label "pout[1-2]". - TPS53647, TPS53667: N=2 - - TPS53622, TPS53659, TPS53676, TPS53679, TPS53681, TPS53688: N=2,3 + - TPS53622, TPS53659, TPS53676, TPS53679, TPS53681, TPS53688, TPS536C7: N=2,3 power[N]_input Measured output power. @@ -191,7 +204,7 @@ curr[N]_label "iout[1-2]" or "iout1.[0-5]". telemetry supported on TPS53676 and TPS53681 only. - TPS53647, TPS53667: N=2 - - TPS53622, TPS53659, TPS53679, TPS53688: N=2,3 + - TPS53622, TPS53659, TPS53679, TPS53688, TPS536C7: N=2,3 - TPS53676: N=2-8 - TPS53681: N=2-9 diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig index 90305d5d59e8..43abc5d226a8 100644 --- a/drivers/hwmon/pmbus/Kconfig +++ b/drivers/hwmon/pmbus/Kconfig @@ -813,7 +813,7 @@ config SENSORS_TPS53679 help If you say yes here you get hardware monitoring support for TI TPS53622, TPS53647, TPS53659, TPS53667, TPS53676, TPS53679, TPS53681, - TPS53685, and TPS53688. + TPS53685, TPS53688, and TPS536C7. This driver can also be built as a module. If so, the module will be called tps53679. diff --git a/drivers/hwmon/pmbus/tps53679.c b/drivers/hwmon/pmbus/tps53679.c index 0b94d81ccd13..3a798e89d1e4 100644 --- a/drivers/hwmon/pmbus/tps53679.c +++ b/drivers/hwmon/pmbus/tps53679.c @@ -17,13 +17,20 @@ enum chips { tps53622, tps53647, tps53659, tps53667, tps53676, tps53679, tps53681, - tps53685, tps53688 + tps53685, tps53688, tps536c7 }; #define TPS53647_PAGE_NUM 1 -#define TPS53676_USER_DATA_03 0xb3 +#define TPS536XX_PHASE_CONFIG 0xb3 #define TPS53676_MAX_PHASES 7 +#define TPS536C7_MAX_PHASES 12 + +#define TPS536XX_PHASE_ENABLE BIT(7) +#define TPS536XX_PHASE_PAGE BIT(4) + +#define TPS53676_DEVICE_ID "TI\x53\x67\x60\x00" +#define TPS536C7_DEVICE_ID "TI\x53\x6c\x70\x00" #define TPS53679_PROT_VR12_5MV 0x01 /* VR12.0 mode, 5-mV DAC */ #define TPS53679_PROT_VR12_5_10MV 0x02 /* VR12.5 mode, 10-mV DAC */ @@ -166,34 +173,72 @@ static int tps53681_identify(struct i2c_client *client, TPS53681_DEVICE_ID); } -static int tps53676_identify(struct i2c_client *client, - struct pmbus_driver_info *info) +static int tps536xx_read_block(struct i2c_client *client, u8 reg, u8 *buf, + bool retry_without_pec) +{ + int ret = i2c_smbus_read_block_data(client, reg, buf); + + /* + * Some TPS536C7 samples return an invalid PEC on the device-ID and + * phase-configuration block reads. Retry once without PEC; ordinary + * telemetry keeps using PEC. + */ + if (ret == -EBADMSG && retry_without_pec && + (client->flags & I2C_CLIENT_PEC)) { + client->flags &= ~I2C_CLIENT_PEC; + ret = i2c_smbus_read_block_data(client, reg, buf); + client->flags |= I2C_CLIENT_PEC; + } + return ret; +} + +static int tps536xx_read_phases(struct i2c_client *client, + const char *device_id, int max_phases, + bool retry_without_pec, int *phases_a, + int *phases_b) { u8 buf[I2C_SMBUS_BLOCK_MAX]; - int phases_a = 0, phases_b = 0; int i, ret; - ret = i2c_smbus_read_block_data(client, PMBUS_IC_DEVICE_ID, buf); + ret = tps536xx_read_block(client, PMBUS_IC_DEVICE_ID, buf, + retry_without_pec); if (ret < 0) return ret; - if (ret != 6 || memcmp(buf, "TI\x53\x67\x60\x00", 6)) { + if (ret != 6 || memcmp(buf, device_id, 6)) { dev_err(&client->dev, "Unexpected device ID: %*ph\n", ret, buf); return -ENODEV; } - ret = i2c_smbus_read_block_data(client, TPS53676_USER_DATA_03, buf); + ret = tps536xx_read_block(client, TPS536XX_PHASE_CONFIG, buf, + retry_without_pec); if (ret < 0) return ret; if (ret != 24) return -EIO; - for (i = 0; i < 2 * TPS53676_MAX_PHASES; i += 2) { - if (buf[i + 1] & 0x80) { - if (buf[i] & BIT(4)) - phases_b++; + + *phases_a = 0; + *phases_b = 0; + for (i = 0; i < 2 * max_phases; i += 2) { + if (buf[i + 1] & TPS536XX_PHASE_ENABLE) { + if (buf[i] & TPS536XX_PHASE_PAGE) + (*phases_b)++; else - phases_a++; + (*phases_a)++; } } + return 0; +} + +static int tps53676_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int phases_a, phases_b, ret; + + ret = tps536xx_read_phases(client, TPS53676_DEVICE_ID, + TPS53676_MAX_PHASES, false, + &phases_a, &phases_b); + if (ret) + return ret; info->format[PSC_VOLTAGE_OUT] = linear; info->pages = 1; @@ -214,6 +259,58 @@ static int tps53676_identify(struct i2c_client *client, return 0; } +static int tps536c7_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int phases_a, phases_b, page, ret; + + ret = tps536xx_read_phases(client, TPS536C7_DEVICE_ID, + TPS536C7_MAX_PHASES, true, + &phases_a, &phases_b); + if (ret) + return ret; + if (!phases_a) { + dev_err(&client->dev, + "TPS536C7 without channel A is not supported\n"); + return -EOPNOTSUPP; + } + + info->format[PSC_VOLTAGE_OUT] = linear; + /* + * TPS536C7 can place up to 12 phases on channel A, which exceeds + * PMBUS_PHASES. Report aggregate per-channel telemetry only and do + * not populate info->phases[]. + */ + info->pages = phases_b ? 2 : 1; + + /* + * With info->phases[] left unset the PMBus core never programs the + * PHASE selector, so make sure each page reports the aggregate + * current (PHASE = 0xff) rather than whatever a previous boot left. + */ + for (page = 0; page < info->pages; page++) { + ret = pmbus_read_byte_data(client, page, PMBUS_PHASE); + if (ret < 0) + return ret; + if (ret == 0xff) + continue; + ret = pmbus_write_byte_data(client, page, PMBUS_PHASE, 0xff); + if (ret < 0) + return ret; + /* PHASE may be write-protected; confirm it actually changed. */ + ret = pmbus_read_byte_data(client, page, PMBUS_PHASE); + if (ret < 0) + return ret; + if (ret != 0xff) { + dev_err(&client->dev, + "failed to select aggregate PHASE on page %d\n", + page); + return -EIO; + } + } + return 0; +} + static int tps53681_read_word_data(struct i2c_client *client, int page, int phase, int reg) { @@ -295,6 +392,9 @@ static int tps53679_probe(struct i2c_client *client) info->pages = TPS53679_PAGE_NUM; info->identify = tps53685_identify; break; + case tps536c7: + info->identify = tps536c7_identify; + break; default: return -ENODEV; } @@ -313,6 +413,7 @@ static const struct i2c_device_id tps53679_id[] = { { .name = "tps53681", .driver_data = tps53681 }, { .name = "tps53685", .driver_data = tps53685 }, { .name = "tps53688", .driver_data = tps53688 }, + { .name = "tps536c7", .driver_data = tps536c7 }, { } }; @@ -328,6 +429,7 @@ static const struct of_device_id __maybe_unused tps53679_of_match[] = { {.compatible = "ti,tps53681", .data = (void *)tps53681}, {.compatible = "ti,tps53685", .data = (void *)tps53685}, {.compatible = "ti,tps53688", .data = (void *)tps53688}, + {.compatible = "ti,tps536c7", .data = (void *)tps536c7}, {} }; MODULE_DEVICE_TABLE(of, tps53679_of_match); From 4810ec90c4a29e2a8883d63812b2d78aa945c3d7 Mon Sep 17 00:00:00 2001 From: Pawel Zalewski Date: Tue, 15 Sep 2026 14:48:20 +0100 Subject: [PATCH 67/97] hwmon: use named initializers for acpi_device_id Use a designated initializer for the acpi_device_id fields which makes the code more readable and consistent with how lists are initialized in the rest of the kernel code base. Also drop explicitly setting fields to 0 where it is redundant. Unify the list terminator to have a single space between the brackets and no trailing comma. Signed-off-by: Pawel Zalewski Link: https://patch.msgid.link/20260915-acpi-hwmon-v2-1-39d17c748eb6@thegoodpenguin.co.uk Signed-off-by: Guenter Roeck --- drivers/hwmon/acpi_power_meter.c | 4 ++-- drivers/hwmon/asus_atk0110.c | 4 ++-- drivers/hwmon/pt5161l.c | 4 ++-- drivers/hwmon/xgene-hwmon.c | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/drivers/hwmon/acpi_power_meter.c b/drivers/hwmon/acpi_power_meter.c index 8a539e8d1334..d8e871509405 100644 --- a/drivers/hwmon/acpi_power_meter.c +++ b/drivers/hwmon/acpi_power_meter.c @@ -54,8 +54,8 @@ static int can_cap_in_hardware(void) } static const struct acpi_device_id power_meter_ids[] = { - {"ACPI000D", 0}, - {"", 0}, + { .id = "ACPI000D" }, + { } }; MODULE_DEVICE_TABLE(acpi, power_meter_ids); diff --git a/drivers/hwmon/asus_atk0110.c b/drivers/hwmon/asus_atk0110.c index 92afb64c09df..868c4e14d26a 100644 --- a/drivers/hwmon/asus_atk0110.c +++ b/drivers/hwmon/asus_atk0110.c @@ -141,8 +141,8 @@ typedef ssize_t (*sysfs_show_func)(struct device *dev, struct device_attribute *attr, char *buf); static const struct acpi_device_id atk_ids[] = { - {ATK_HID, 0}, - {"", 0}, + { .id = ATK_HID }, + { } }; MODULE_DEVICE_TABLE(acpi, atk_ids); diff --git a/drivers/hwmon/pt5161l.c b/drivers/hwmon/pt5161l.c index 2b408a69b085..1638f54f8eae 100644 --- a/drivers/hwmon/pt5161l.c +++ b/drivers/hwmon/pt5161l.c @@ -611,8 +611,8 @@ static const struct of_device_id __maybe_unused pt5161l_of_match[] = { MODULE_DEVICE_TABLE(of, pt5161l_of_match); static const struct acpi_device_id __maybe_unused pt5161l_acpi_match[] = { - { "PT5161L", 0 }, - {}, + { .id = "PT5161L" }, + { } }; MODULE_DEVICE_TABLE(acpi, pt5161l_acpi_match); diff --git a/drivers/hwmon/xgene-hwmon.c b/drivers/hwmon/xgene-hwmon.c index 38b140c23c88..eebbb741deac 100644 --- a/drivers/hwmon/xgene-hwmon.c +++ b/drivers/hwmon/xgene-hwmon.c @@ -595,9 +595,9 @@ static void xgene_hwmon_tx_done(struct mbox_client *cl, void *msg, int ret) #ifdef CONFIG_ACPI static const struct acpi_device_id xgene_hwmon_acpi_match[] = { - {"APMC0D29", XGENE_HWMON_V1}, - {"APMC0D8A", XGENE_HWMON_V2}, - {}, + { .id = "APMC0D29", .driver_data = XGENE_HWMON_V1 }, + { .id = "APMC0D8A", .driver_data = XGENE_HWMON_V2 }, + { } }; MODULE_DEVICE_TABLE(acpi, xgene_hwmon_acpi_match); #endif From c3147b4d1bd09cc1dd7f2c0d31a5ebb900f22ab4 Mon Sep 17 00:00:00 2001 From: Sanman Pradhan Date: Wed, 16 Sep 2026 23:54:21 +0000 Subject: [PATCH 68/97] hwmon: (pmbus/tps53679) Select page 0 for single-page TPS536C7 tps536c7_identify() sets info->pages to 1 for a single-channel part and then accesses page 0 (writing PMBUS_PHASE) without ensuring PAGE is actually 0. pmbus_set_page() does not update the PAGE register when info->pages is 1, so if boot firmware left PAGE set to another value the PHASE writes and subsequent telemetry may target the wrong page. Select page 0 explicitly before configuring PHASE. Signed-off-by: Sanman Pradhan Link: https://patch.msgid.link/20260916235406.681131-3-sanman.pradhan@hpe.com Signed-off-by: Guenter Roeck --- drivers/hwmon/pmbus/tps53679.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/drivers/hwmon/pmbus/tps53679.c b/drivers/hwmon/pmbus/tps53679.c index 3a798e89d1e4..a04a632a5091 100644 --- a/drivers/hwmon/pmbus/tps53679.c +++ b/drivers/hwmon/pmbus/tps53679.c @@ -283,6 +283,17 @@ static int tps536c7_identify(struct i2c_client *client, */ info->pages = phases_b ? 2 : 1; + /* + * pmbus_set_page() does not update the PAGE register on single-page + * devices, so select page 0 explicitly in case the boot firmware + * left the device on another page. + */ + if (info->pages == 1) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + } + /* * With info->phases[] left unset the PMBus core never programs the * PHASE selector, so make sure each page reports the aggregate From bd6b9bbd82a01212536b57fa6d7ab7741da5d7cb Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Thu, 17 Sep 2026 14:13:33 -0700 Subject: [PATCH 69/97] hwmon: (asus_wmi_sensors) Fix info[] allocation type In preparation for making the devm_kmalloc family of allocators type aware, we need to make sure that the returned type from the allocation matches the type of the variable being assigned. (Before, the allocator would always return "void *", which can be implicitly cast to any pointer type.) The assigned type of "sensor_data->wmi.info[type]" is "const struct asus_wmi_sensor_info **", but the converted allocation type would be "const struct asus_wmi_sensor_info ***", as the size was taken from "*sensor_data->wmi.info", which is one level of indirection too many. Luckily both element types are pointers of the same size. Take the size from the element type of the assignment target. Build tested ARCH=x86_64 allmodconfig with GCC 16.2.0: drivers/hwmon/asus_wmi_sensors.o Assisted-by: LLM coccinelle Signed-off-by: Kees Cook Link: https://patch.msgid.link/20260917211332.i.711-kees@kernel.org Signed-off-by: Guenter Roeck --- drivers/hwmon/asus_wmi_sensors.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/asus_wmi_sensors.c b/drivers/hwmon/asus_wmi_sensors.c index c2dd7ff882f2..621e3506972d 100644 --- a/drivers/hwmon/asus_wmi_sensors.c +++ b/drivers/hwmon/asus_wmi_sensors.c @@ -573,7 +573,7 @@ static int asus_wmi_configure_sensor_setup(struct device *dev, sensor_data->wmi.info[type] = devm_kcalloc(dev, nr_count[type], - sizeof(*sensor_data->wmi.info), + sizeof(*sensor_data->wmi.info[type]), GFP_KERNEL); if (!sensor_data->wmi.info[type]) return -ENOMEM; From a1cbb277e3c470727adec3903bd1e986bcd79d05 Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Thu, 17 Sep 2026 14:14:18 -0700 Subject: [PATCH 70/97] hwmon: (pwm-fan) Add const to channels allocation type In preparation for making the devm_kmalloc family of allocators type aware, we need to make sure that the returned type from the allocation matches the type of the variable being assigned. (Before, the allocator would always return "void *", which can be implicitly cast to any pointer type.) The assigned type is "const struct hwmon_channel_info **", but the converted allocation type would be "struct hwmon_channel_info **", which is the same type without the const qualifier. As there is no general way to safely add const qualifiers, take the size from the assignment target instead. No change in allocation size results. Build tested ARCH=x86_64 allmodconfig with GCC 16.2.0: drivers/hwmon/pwm-fan.o Assisted-by: LLM coccinelle Signed-off-by: Kees Cook Link: https://patch.msgid.link/20260917211417.i.173-kees@kernel.org Signed-off-by: Guenter Roeck --- drivers/hwmon/pwm-fan.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/pwm-fan.c b/drivers/hwmon/pwm-fan.c index c633d7f6464c..9cabbe9f8462 100644 --- a/drivers/hwmon/pwm-fan.c +++ b/drivers/hwmon/pwm-fan.c @@ -619,7 +619,7 @@ static int pwm_fan_probe(struct platform_device *pdev) } channels = devm_kcalloc(dev, channel_count + 1, - sizeof(struct hwmon_channel_info *), GFP_KERNEL); + sizeof(*channels), GFP_KERNEL); if (!channels) return -ENOMEM; From d63754d87e9e065346d12d9cc4ad11df8ccb0f12 Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Thu, 17 Sep 2026 14:13:45 -0700 Subject: [PATCH 71/97] hwmon: (scmi) Fix info[] allocation type In preparation for making the devm_kmalloc family of allocators type aware, we need to make sure that the returned type from the allocation matches the type of the variable being assigned. (Before, the allocator would always return "void *", which can be implicitly cast to any pointer type.) The assigned type of "scmi_sensors->info[type]" is "const struct scmi_sensor_info **", but the converted allocation type would be "const struct scmi_sensor_info ***", as the size was taken from "*scmi_sensors->info", which is one level of indirection too many. Luckily both element types are pointers of the same size. Take the size from the element type of the assignment target. Build tested ARCH=x86_64 allmodconfig with GCC 16.2.0: drivers/hwmon/scmi-hwmon.o Assisted-by: LLM coccinelle Signed-off-by: Kees Cook Link: https://patch.msgid.link/20260917211344.i.911-kees@kernel.org Signed-off-by: Guenter Roeck --- drivers/hwmon/scmi-hwmon.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/hwmon/scmi-hwmon.c b/drivers/hwmon/scmi-hwmon.c index eec223d174c0..9ae8f774297e 100644 --- a/drivers/hwmon/scmi-hwmon.c +++ b/drivers/hwmon/scmi-hwmon.c @@ -305,7 +305,8 @@ static int scmi_hwmon_probe(struct scmi_device *sdev) scmi_sensors->info[type] = devm_kcalloc(dev, nr_count[type], - sizeof(*scmi_sensors->info), GFP_KERNEL); + sizeof(*scmi_sensors->info[type]), + GFP_KERNEL); if (!scmi_sensors->info[type]) return -ENOMEM; } From b702739a31e4250db2dc2bd232cacc1e555df365 Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Thu, 17 Sep 2026 14:13:39 -0700 Subject: [PATCH 72/97] hwmon: (hp-wmi-sensors) Fix info_map[] allocation type In preparation for making the devm_kmalloc family of allocators type aware, we need to make sure that the returned type from the allocation matches the type of the variable being assigned. (Before, the allocator would always return "void *", which can be implicitly cast to any pointer type.) The assigned type of "info_map[type]" is "struct hp_wmi_info **", but the converted allocation type would be "struct hp_wmi_info ***", as the size was taken from "*info_map", which is one level of indirection too many. Luckily both element types are pointers of the same size. Take the size from the element type of the assignment target. Build tested ARCH=x86_64 allmodconfig with GCC 16.2.0: drivers/hwmon/hp-wmi-sensors.o Assisted-by: LLM coccinelle Signed-off-by: Kees Cook Acked-by: James Seo Link: https://patch.msgid.link/20260917211338.i.682-kees@kernel.org Signed-off-by: Guenter Roeck --- drivers/hwmon/hp-wmi-sensors.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/hp-wmi-sensors.c b/drivers/hwmon/hp-wmi-sensors.c index cfa0d6ad0fb3..d8bcfeb8d5b2 100644 --- a/drivers/hwmon/hp-wmi-sensors.c +++ b/drivers/hwmon/hp-wmi-sensors.c @@ -1804,7 +1804,7 @@ static int init_numeric_sensors(struct hp_wmi_sensors *state, if (!info_map[type]) { info_map[type] = devm_kcalloc(dev, channel_count[type], - sizeof(*info_map), + sizeof(*info_map[type]), GFP_KERNEL); if (!info_map[type]) return -ENOMEM; From fbc5cab2e6b671749dd502fdb44803f82e2b7538 Mon Sep 17 00:00:00 2001 From: Quchaosheng Date: Thu, 17 Sep 2026 19:58:17 +0800 Subject: [PATCH 73/97] hwmon: documentation: vexpress: point the DT binding reference at the schema Documentation/hwmon/vexpress.rst points at Documentation/devicetree/bindings/hwmon/vexpress.txt for the device tree binding of the arm,vexpress-volt, -amp, -temp, -power and -energy sensors. That file only restates the compatibles and the label property, which the arm/vexpress-config.yaml schema describes now. Point the reference there. Assisted-by: LLM Signed-off-by: Quchaosheng Link: https://patch.msgid.link/20260917115818.540344-1-quchaosheng000406@163.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/vexpress.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/hwmon/vexpress.rst b/Documentation/hwmon/vexpress.rst index 7fc99f75b3b1..84b2a33e2d40 100644 --- a/Documentation/hwmon/vexpress.rst +++ b/Documentation/hwmon/vexpress.rst @@ -38,4 +38,4 @@ via a custom interface, abstracted by the "vexpress_config" API. As these devices are non-discoverable, they must be described in a Device Tree passed to the kernel. Details of the DT binding for them can be found -in Documentation/devicetree/bindings/hwmon/vexpress.txt. +in Documentation/devicetree/bindings/arm/vexpress-config.yaml. From ea863d783463e29ba5a666a32b588c32e7b0fc8c Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Fri, 18 Sep 2026 18:23:54 +0800 Subject: [PATCH 74/97] dt-bindings: hwmon: pmbus: Add Infineon tda38740 and tda38725 Add devicetree bindings for the Infineon TDA38740 and TDA38725 single-voltage synchronous buck regulators with PMBus interface. Signed-off-by: Colin Huang Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260918-add-tda38740-and-tda38725-v6-1-a8b11018d850@gmail.com Signed-off-by: Guenter Roeck --- .../hwmon/pmbus/infineon,tda38740.yaml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 Documentation/devicetree/bindings/hwmon/pmbus/infineon,tda38740.yaml diff --git a/Documentation/devicetree/bindings/hwmon/pmbus/infineon,tda38740.yaml b/Documentation/devicetree/bindings/hwmon/pmbus/infineon,tda38740.yaml new file mode 100644 index 000000000000..6733e44c37f6 --- /dev/null +++ b/Documentation/devicetree/bindings/hwmon/pmbus/infineon,tda38740.yaml @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/hwmon/pmbus/infineon,tda38740.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Infineon TDA38740 and TDA38725 PMBus Synchronous Buck Regulator + +maintainers: + - Colin Huang + +description: | + The Infineon TDA38740/TDA38725 is a 40A/25A single-voltage synchronous + buck regulator with a PMBus interface designed for industrial use. + + Datasheet: + https://www.infineon.com/row/public/documents/24/49/infineon-tda38740-tda38725-datasheet-en.pdf + +properties: + compatible: + oneOf: + - const: infineon,tda38740 + - items: + - const: infineon,tda38725 + - const: infineon,tda38740 + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + #include + i2c { + #address-cells = <1>; + #size-cells = <0>; + + regulator@40 { + compatible = "infineon,tda38725", + "infineon,tda38740"; + reg = <0x40>; + + interrupt-parent = <&sgpiom0>; + interrupts = <10 IRQ_TYPE_LEVEL_LOW>; + }; + }; + From 3f02ddc3771c93276f17bbcb0f4482f2bf67fe99 Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Fri, 18 Sep 2026 18:23:55 +0800 Subject: [PATCH 75/97] hwmon: (pmbus/tda38740) Add driver for Infineon TDA38740/TDA38725 Add a PMBus driver for Infineon TDA38740 and TDA38725 single-voltage synchronous buck regulators. Signed-off-by: Colin Huang Reviewed-by: Guenter Roeck Link: https://patch.msgid.link/20260918-add-tda38740-and-tda38725-v6-2-a8b11018d850@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/index.rst | 1 + Documentation/hwmon/tda38740.rst | 65 ++++++++++++++++++++++++++++++++ drivers/hwmon/pmbus/Kconfig | 9 +++++ drivers/hwmon/pmbus/Makefile | 1 + drivers/hwmon/pmbus/tda38740.c | 64 +++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 Documentation/hwmon/tda38740.rst create mode 100644 drivers/hwmon/pmbus/tda38740.c diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst index 21ad419fcbf1..b6679e68f905 100644 --- a/Documentation/hwmon/index.rst +++ b/Documentation/hwmon/index.rst @@ -267,6 +267,7 @@ Hardware Monitoring Kernel Drivers sy7636a-hwmon tc654 tc74 + tda38740 thmc50 tmp102 tmp103 diff --git a/Documentation/hwmon/tda38740.rst b/Documentation/hwmon/tda38740.rst new file mode 100644 index 000000000000..55ce12f14914 --- /dev/null +++ b/Documentation/hwmon/tda38740.rst @@ -0,0 +1,65 @@ +.. SPDX-License-Identifier: GPL-2.0-only + +==================================== +Kernel driver tda38740 +==================================== + +Supported chips: + * Infineon TDA38740 + * Infineon TDA38725 + + Prefix: 'tda38740' + + Addresses scanned: - + + Datasheet: + https://www.infineon.com/row/public/documents/24/49/infineon-tda38740-tda38725-datasheet-en.pdf + +Author: Colin Huang + +Description +----------- + +The TDA38740 and TDA38725 are single-output synchronous buck regulators with a +PMBus interface. They expose basic voltage, current, power and temperature +telemetry through the standard PMBus hwmon interfaces. + +Sysfs entries +------------- + +======================= ======================================================= +curr1_alarm Input current alarm +curr1_input Measured input current +curr1_label "iin" + +curr2_crit Critical maximum output current +curr2_crit_alarm Output current critical high alarm +curr2_input Measured output current +curr2_label "iout" + +in1_crit Critical maximum input voltage +in1_crit_alarm Input voltage critical high alarm +in1_input Measured input voltage +in1_label "vin" + +in2_crit Critical maximum output voltage +in2_crit_alarm Output voltage critical high alarm +in2_input Measured output voltage +in2_label "vout" +in2_lcrit Critical minimum output voltage +in2_lcrit_alarm Output voltage critical low alarm + +power1_alarm Input power alarm +power1_input Measured input power +power1_label "pin" + +power2_input Measured output power +power2_label "pout" + +temp1_crit Critical high temperature +temp1_crit_alarm Chip temperature critical high alarm +temp1_input Measured temperature +temp1_max Maximum temperature +temp1_max_alarm Chip temperature high alarm +======================= ======================================================= + diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig index 43abc5d226a8..77e036e96228 100644 --- a/drivers/hwmon/pmbus/Kconfig +++ b/drivers/hwmon/pmbus/Kconfig @@ -782,6 +782,15 @@ config SENSORS_TDA38640_REGULATOR If you say yes here you get regulator support for Infineon TDA38640 as regulator. +config SENSORS_TDA38740 + tristate "Infineon TDA38725/TDA38740" + help + If you say yes here you get hardware monitoring support for Infineon + TDA38725 and TDA38740. + + This driver can also be built as a module. If so, the module will + be called tda38740. + config SENSORS_TPS25990 tristate "TI TPS25990" help diff --git a/drivers/hwmon/pmbus/Makefile b/drivers/hwmon/pmbus/Makefile index e3fc23f88401..b67ec382328e 100644 --- a/drivers/hwmon/pmbus/Makefile +++ b/drivers/hwmon/pmbus/Makefile @@ -71,6 +71,7 @@ obj-$(CONFIG_SENSORS_STEF48H28) += stef48h28.o obj-$(CONFIG_SENSORS_SQ24860) += sq24860.o obj-$(CONFIG_SENSORS_STPDDC60) += stpddc60.o obj-$(CONFIG_SENSORS_TDA38640) += tda38640.o +obj-$(CONFIG_SENSORS_TDA38740) += tda38740.o obj-$(CONFIG_SENSORS_TPS25990) += tps25990.o obj-$(CONFIG_SENSORS_TPS40422) += tps40422.o obj-$(CONFIG_SENSORS_TPS53679) += tps53679.o diff --git a/drivers/hwmon/pmbus/tda38740.c b/drivers/hwmon/pmbus/tda38740.c new file mode 100644 index 000000000000..067de07ba9fb --- /dev/null +++ b/drivers/hwmon/pmbus/tda38740.c @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Infineon TDA38725/TDA38740 + * + * Copyright (c) 2023 9elements GmbH + * + */ + +#include +#include +#include +#include +#include +#include "pmbus.h" + +static struct pmbus_driver_info tda38740_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT | PMBUS_HAVE_PIN, +}; + +static int tda38740_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &tda38740_info); +} + +static const struct i2c_device_id tda38740_id[] = { + { .name = "tda38725"}, + { .name = "tda38740"}, + {} +}; +MODULE_DEVICE_TABLE(i2c, tda38740_id); + +static const struct of_device_id tda38740_of_match[] = { + { .compatible = "infineon,tda38725"}, + { .compatible = "infineon,tda38740"}, + {} +}; +MODULE_DEVICE_TABLE(of, tda38740_of_match); + +static struct i2c_driver tda38740_driver = { + .driver = { + .name = "tda38740", + .of_match_table = tda38740_of_match, + }, + .probe = tda38740_probe, + .id_table = tda38740_id, +}; + +module_i2c_driver(tda38740_driver); + +MODULE_DESCRIPTION("PMBus driver for Infineon TDA38725/TDA38740"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); From 91fc9698d9c5baad69e43ed16f4e61538e04d0a4 Mon Sep 17 00:00:00 2001 From: Petar Stepanovic Date: Mon, 21 Sep 2026 00:23:55 -0700 Subject: [PATCH 76/97] dt-bindings: hwmon: add Axiado AX3000 and AX3005 PWM fan controller Add a Devicetree binding for the PWM fan controller found on the Axiado AX3000 and AX3005 SoCs. The controller measures fan speed using a hardware tachometer block. Fan speed is controlled through a PWM signal supplied by an external PWM controller. Add a MAINTAINERS entry for the binding. Reviewed-by: Krzysztof Kozlowski Signed-off-by: Petar Stepanovic Link: https://patch.msgid.link/20260921-axiado-ax3000-pwm-fan-v3-1-3def8b6ffec9@axiado.com Signed-off-by: Guenter Roeck --- .../bindings/hwmon/axiado,ax3000-pwm-fan.yaml | 72 +++++++++++++++++++ MAINTAINERS | 8 +++ 2 files changed, 80 insertions(+) create mode 100644 Documentation/devicetree/bindings/hwmon/axiado,ax3000-pwm-fan.yaml diff --git a/Documentation/devicetree/bindings/hwmon/axiado,ax3000-pwm-fan.yaml b/Documentation/devicetree/bindings/hwmon/axiado,ax3000-pwm-fan.yaml new file mode 100644 index 000000000000..76a3e12fc656 --- /dev/null +++ b/Documentation/devicetree/bindings/hwmon/axiado,ax3000-pwm-fan.yaml @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/hwmon/axiado,ax3000-pwm-fan.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Axiado AX3000 and AX3005 PWM Fan Controller + +maintainers: + - Petar Stepanovic + - Akhila Kavi + - Prasad Bolisetty + +description: + The Axiado AX3000 and AX3005 fan controllers use a hardware tachometer + block to measure fan speed. Fan speed is controlled through a PWM signal + supplied by an external PWM controller. + +properties: + compatible: + oneOf: + - const: axiado,ax3000-pwm-fan + - items: + - const: axiado,ax3005-pwm-fan + - const: axiado,ax3000-pwm-fan + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + clocks: + maxItems: 1 + + fan: + type: object + $ref: fan-common.yaml# + unevaluatedProperties: false + properties: + pulses-per-revolution: + minimum: 1 + default: 2 + + required: + - pwms + +required: + - compatible + - reg + - interrupts + - clocks + - fan + +additionalProperties: false + +examples: + - | + #include + + fan-controller@33001400 { + compatible = "axiado,ax3000-pwm-fan"; + reg = <0x33001400 0x400>; + interrupts = ; + clocks = <&clk>; + + fan { + pwms = <&pwm0 0 400>; + pulses-per-revolution = <2>; + }; + }; +... diff --git a/MAINTAINERS b/MAINTAINERS index 9712f007eac5..eb3cc6c81bcf 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4497,6 +4497,14 @@ W: https://ez.analog.com/linux-software-drivers F: Documentation/devicetree/bindings/pwm/adi,axi-pwmgen.yaml F: drivers/pwm/pwm-axi-pwmgen.c +AXIADO PWM FAN CONTROLLER DRIVER +M: Petar Stepanovic +M: Akhila Kavi +M: Prasad Bolisetty +L: linux-hwmon@vger.kernel.org +S: Supported +F: Documentation/devicetree/bindings/hwmon/axiado,ax3000-pwm-fan.yaml + AXIADO SPI DB DRIVER M: Vladimir Moravcevic M: Tzu-Hao Wei From b9d5d15f8a05e2abb2f278a0b061873cc5f95a14 Mon Sep 17 00:00:00 2001 From: Petar Stepanovic Date: Mon, 21 Sep 2026 00:23:56 -0700 Subject: [PATCH 77/97] hwmon: add Axiado AX3000 and AX3005 PWM fan controller driver Add support for the PWM fan controller found on the Axiado AX3000 and AX3005 SoCs. The controller uses a hardware tachometer block and interrupt to measure fan speed. Fan speed is controlled through a PWM signal supplied by an external PWM controller. Register the controller with the hwmon subsystem and expose the standard fan1_input and pwm1 attributes. The physical fan is represented by a fan child node containing the PWM specifier and the optional pulses-per-revolution property. If pulses-per-revolution is not specified, the driver uses two pulses per revolution. The driver was tested on AX3000 and AX3005 boards. Signed-off-by: Petar Stepanovic Link: https://patch.msgid.link/20260921-axiado-ax3000-pwm-fan-v3-2-3def8b6ffec9@axiado.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/axiado-pwm-fan.rst | 38 +++ Documentation/hwmon/index.rst | 1 + MAINTAINERS | 2 + drivers/hwmon/Kconfig | 12 + drivers/hwmon/Makefile | 1 + drivers/hwmon/axiado-pwm-fan.c | 416 +++++++++++++++++++++++++ 6 files changed, 470 insertions(+) create mode 100644 Documentation/hwmon/axiado-pwm-fan.rst create mode 100644 drivers/hwmon/axiado-pwm-fan.c diff --git a/Documentation/hwmon/axiado-pwm-fan.rst b/Documentation/hwmon/axiado-pwm-fan.rst new file mode 100644 index 000000000000..d110e170b0df --- /dev/null +++ b/Documentation/hwmon/axiado-pwm-fan.rst @@ -0,0 +1,38 @@ +.. SPDX-License-Identifier: GPL-2.0 + +Kernel driver axiado-pwm-fan +============================ + +Supported chips: + + * Axiado AX3000 + * Axiado AX3005 + + Datasheet: Not publicly available + +Author: + + * Petar Stepanovic + +Description +----------- + +The Axiado AX3000 and AX3005 PWM fan controllers measure the rotational +speed of one fan using a hardware tachometer block. Fan speed is controlled +through a PWM signal supplied by an external PWM controller. + +The number of tachometer pulses generated per fan revolution is configured +through the ``pulses-per-revolution`` devicetree property. If the property +is not specified, the driver uses two pulses per revolution. + +Sysfs attributes +---------------- + +The driver provides the following standard hwmon attributes: + +=============== ====== ===================================================== +fan1_input RO Fan speed in revolutions per minute (RPM). + +pwm1 RW Relative PWM control value from 0 to 255. A value of + 255 selects the maximum PWM duty cycle. +=============== ====== ===================================================== diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst index b6679e68f905..199ca79621e9 100644 --- a/Documentation/hwmon/index.rst +++ b/Documentation/hwmon/index.rst @@ -51,6 +51,7 @@ Hardware Monitoring Kernel Drivers asus_ec_sensors asus_rog_ryujin asus_wmi_sensors + axiado-pwm-fan axiado-tsadc bcm54140 bel-pfe diff --git a/MAINTAINERS b/MAINTAINERS index eb3cc6c81bcf..5ad12c57ca5f 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4504,6 +4504,8 @@ M: Prasad Bolisetty L: linux-hwmon@vger.kernel.org S: Supported F: Documentation/devicetree/bindings/hwmon/axiado,ax3000-pwm-fan.yaml +F: Documentation/hwmon/axiado-pwm-fan.rst +F: drivers/hwmon/axiado-pwm-fan.c AXIADO SPI DB DRIVER M: Vladimir Moravcevic diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 3d8b2c958b29..18c682e1a475 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -311,6 +311,18 @@ config SENSORS_AXI_FAN_CONTROL This driver can also be built as a module. If so, the module will be called axi-fan-control +config SENSORS_AXIADO_PWM_FAN + tristate "Axiado PWM fan controller" + depends on ARCH_AXIADO || COMPILE_TEST + depends on PWM + help + This driver supports the Axiado PWM fan controller. It uses a PWM + output to control fan speed and a tachometer interrupt to report fan + speed as RPM through the hardware monitoring sysfs interface. + + This driver can also be built as a module. If so, the module will be + called axiado-pwm-fan. + config SENSORS_AXIADO_TSADC tristate "Axiado AX3000 TSADC temperature sensor" depends on ARCH_AXIADO || COMPILE_TEST diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index f84f9df27466..d4c345ed7261 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -60,6 +60,7 @@ obj-$(CONFIG_SENSORS_ASPEED_G6) += aspeed-g6-pwm-tach.o obj-$(CONFIG_SENSORS_ASUS_ROG_RYUJIN) += asus_rog_ryujin.o obj-$(CONFIG_SENSORS_ATXP1) += atxp1.o obj-$(CONFIG_SENSORS_AXI_FAN_CONTROL) += axi-fan-control.o +obj-$(CONFIG_SENSORS_AXIADO_PWM_FAN) += axiado-pwm-fan.o obj-$(CONFIG_SENSORS_AXIADO_TSADC) += axiado-tsadc.o obj-$(CONFIG_SENSORS_CGBC) += cgbc-hwmon.o obj-$(CONFIG_SENSORS_CHIPCAP2) += chipcap2.o diff --git a/drivers/hwmon/axiado-pwm-fan.c b/drivers/hwmon/axiado-pwm-fan.c new file mode 100644 index 000000000000..6fca5292eb25 --- /dev/null +++ b/drivers/hwmon/axiado-pwm-fan.c @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2021-2026 Axiado Corporation. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define AX_TACH_PWM_VAL_MAX 255 + +/* TACH Register offsets */ +#define AX_TACH_CTRL_REG 0x00 +#define AX_TACH_TIMER_COUNT_REG 0x04 +#define AX_TACH_COUNT_REG 0x08 +#define AX_TACH_INT_STATUS_REG 0x0c + +#define AX_TACH_CTRL_ENABLE BIT(0) +#define AX_TACH_CTRL_INT_ENABLE BIT(1) + +#define AX_TACH_INT_PENDING BIT(0) + +struct axiado_pwm_fan_tach { + int irq; + u32 pulses_per_revolution; + u32 timer_count; + u32 count; +}; + +struct axiado_pwm_fan_ctx { + /* Serializes PWM updates with PM and shutdown operations. */ + struct mutex pwm_lock; + /* Protects tachometer count updated from interrupt context. */ + spinlock_t tach_lock; + struct pwm_device *pwm; + struct pwm_state pwm_state; + void __iomem *tach_base; + struct axiado_pwm_fan_tach tach; + unsigned int pwm_value; +}; + +static const struct hwmon_channel_info * const pwm_fan_info[] = { + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT), + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT), + NULL +}; + +static irqreturn_t axiado_tach_irq_handler(int irq, void *dev) +{ + struct axiado_pwm_fan_ctx *ctx = dev; + u32 status; + + status = ioread32(ctx->tach_base + AX_TACH_INT_STATUS_REG); + + if (!(status & AX_TACH_INT_PENDING)) + return IRQ_NONE; + + scoped_guard(spinlock_irqsave, &ctx->tach_lock) { + ctx->tach.count = ioread32(ctx->tach_base + AX_TACH_COUNT_REG); + } + + iowrite32(AX_TACH_INT_PENDING, ctx->tach_base + AX_TACH_INT_STATUS_REG); + + return IRQ_HANDLED; +} + +static int axiado_set_pwm(struct axiado_pwm_fan_ctx *ctx, unsigned int pwm) +{ + struct pwm_state state; + int ret; + + guard(mutex)(&ctx->pwm_lock); + + if (ctx->pwm_value == pwm) + return 0; + + state = ctx->pwm_state; + state.duty_cycle = DIV_ROUND_UP_ULL((u64)pwm * state.period, + AX_TACH_PWM_VAL_MAX); + /* + * Keep the PWM enabled even at zero duty cycle so the output stays + * driven low instead of being left floating by the PWM hardware. + */ + state.enabled = true; + + ret = pwm_apply_might_sleep(ctx->pwm, &state); + if (ret) + return ret; + + ctx->pwm_state = state; + ctx->pwm_value = pwm; + + return 0; +} + +static int axiado_pwm_fan_write(struct device *dev, + enum hwmon_sensor_types type, u32 attr, + int channel, long val) +{ + struct axiado_pwm_fan_ctx *ctx = dev_get_drvdata(dev); + + if (type != hwmon_pwm || attr != hwmon_pwm_input) + return -EOPNOTSUPP; + + if (val < 0 || val > AX_TACH_PWM_VAL_MAX) + return -EINVAL; + + return axiado_set_pwm(ctx, val); +} + +static unsigned int axiado_tach_get_rpm(struct axiado_pwm_fan_ctx *ctx) +{ + u32 pulses_per_revolution = ctx->tach.pulses_per_revolution; + u64 pulses, rpm; + + scoped_guard(spinlock_irqsave, &ctx->tach_lock) + pulses = ctx->tach.count; + + if (!pulses_per_revolution) + return 0; + + rpm = pulses * 60; + do_div(rpm, pulses_per_revolution); + + return rpm; +} + +static int axiado_pwm_fan_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct axiado_pwm_fan_ctx *ctx = dev_get_drvdata(dev); + + switch (type) { + case hwmon_pwm: + if (attr != hwmon_pwm_input) + return -EOPNOTSUPP; + + *val = ctx->pwm_value; + + return 0; + + case hwmon_fan: + if (attr != hwmon_fan_input) + return -EOPNOTSUPP; + + *val = axiado_tach_get_rpm(ctx); + + return 0; + + default: + return -EOPNOTSUPP; + } +} + +static umode_t axiado_pwm_fan_is_visible(const void *data, + enum hwmon_sensor_types type, u32 attr, + int channel) +{ + if (type == hwmon_fan && attr == hwmon_fan_input) + return 0444; + + if (type == hwmon_pwm && attr == hwmon_pwm_input) + return 0644; + + return 0; +} + +static const struct hwmon_ops pwm_fan_hwmon_ops = { + .is_visible = axiado_pwm_fan_is_visible, + .read = axiado_pwm_fan_read, + .write = axiado_pwm_fan_write, +}; + +static const struct hwmon_chip_info pwm_fan_chip_info = { + .ops = &pwm_fan_hwmon_ops, + .info = pwm_fan_info, +}; + +static int axiado_pwm_apply_disabled(struct axiado_pwm_fan_ctx *ctx) +{ + struct pwm_state state; + + guard(mutex)(&ctx->pwm_lock); + + if (!ctx->pwm_state.enabled) + return 0; + + state = ctx->pwm_state; + state.duty_cycle = 0; + state.enabled = false; + + return pwm_apply_might_sleep(ctx->pwm, &state); +} + +static void axiado_pwm_disable(void *data) +{ + struct axiado_pwm_fan_ctx *ctx = data; + + axiado_pwm_apply_disabled(ctx); +} + +static void axiado_tach_enable(struct axiado_pwm_fan_ctx *ctx) +{ + iowrite32(AX_TACH_INT_PENDING, + ctx->tach_base + AX_TACH_INT_STATUS_REG); + iowrite32(ctx->tach.timer_count, + ctx->tach_base + AX_TACH_TIMER_COUNT_REG); + iowrite32(AX_TACH_CTRL_ENABLE | AX_TACH_CTRL_INT_ENABLE, + ctx->tach_base + AX_TACH_CTRL_REG); +} + +static void axiado_tach_disable(void *data) +{ + struct axiado_pwm_fan_ctx *ctx = data; + + iowrite32(0, ctx->tach_base + AX_TACH_CTRL_REG); + iowrite32(AX_TACH_INT_PENDING, + ctx->tach_base + AX_TACH_INT_STATUS_REG); +} + +static int axiado_pwm_fan_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct fwnode_handle *fan_node; + struct axiado_pwm_fan_ctx *ctx; + unsigned long tach_clk_rate; + struct device *hwmon; + struct clk *tach_clk; + int ret; + + ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return -ENOMEM; + + mutex_init(&ctx->pwm_lock); + spin_lock_init(&ctx->tach_lock); + + ctx->tach_base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(ctx->tach_base)) + return PTR_ERR(ctx->tach_base); + + fan_node = device_get_named_child_node(dev, "fan"); + if (!fan_node) + return dev_err_probe(dev, -EINVAL, + "Missing fan child node\n"); + + ctx->pwm = devm_fwnode_pwm_get(dev, fan_node, NULL); + if (IS_ERR(ctx->pwm)) { + ret = dev_err_probe(dev, PTR_ERR(ctx->pwm), + "Could not get PWM\n"); + goto put_fan_node; + } + + ctx->tach.pulses_per_revolution = 2; + + ret = fwnode_property_read_u32(fan_node, "pulses-per-revolution", + &ctx->tach.pulses_per_revolution); + if (ret && ret != -EINVAL) { + ret = dev_err_probe(dev, ret, + "Failed to read pulses-per-revolution\n"); + goto put_fan_node; + } + + if (!ctx->tach.pulses_per_revolution) { + ret = dev_err_probe(dev, -EINVAL, + "pulses-per-revolution cannot be zero\n"); + goto put_fan_node; + } + + fwnode_handle_put(fan_node); + + platform_set_drvdata(pdev, ctx); + + pwm_init_state(ctx->pwm, &ctx->pwm_state); + + if (!ctx->pwm_state.period) + return dev_err_probe(dev, -EINVAL, "PWM period is zero\n"); + + tach_clk = devm_clk_get_enabled(dev, NULL); + if (IS_ERR(tach_clk)) + return dev_err_probe(dev, PTR_ERR(tach_clk), + "Failed to get tachometer clock\n"); + + tach_clk_rate = clk_get_rate(tach_clk); + if (!tach_clk_rate || tach_clk_rate > U32_MAX) + return dev_err_probe(dev, -EINVAL, + "Invalid tachometer clock rate: %lu\n", + tach_clk_rate); + + ctx->tach.timer_count = tach_clk_rate; + + ctx->tach.irq = platform_get_irq(pdev, 0); + if (ctx->tach.irq < 0) + return dev_err_probe(dev, ctx->tach.irq, + "Failed to get tachometer IRQ\n"); + + ret = devm_request_irq(dev, ctx->tach.irq, axiado_tach_irq_handler, 0, + dev_name(dev), ctx); + if (ret) + return dev_err_probe(dev, ret, "Failed to request tach IRQ\n"); + + ret = devm_add_action_or_reset(dev, axiado_tach_disable, ctx); + if (ret) + return ret; + + dev_dbg(dev, "Fan tachometer: irq=%d, pulses_per_revolution=%u\n", + ctx->tach.irq, ctx->tach.pulses_per_revolution); + + axiado_tach_enable(ctx); + + ret = axiado_set_pwm(ctx, AX_TACH_PWM_VAL_MAX); + + if (ret) + return dev_err_probe(dev, ret, "Failed to configure PWM\n"); + + ret = devm_add_action_or_reset(dev, axiado_pwm_disable, ctx); + if (ret) + return dev_err_probe(dev, ret, + "Failed to add PWM disable action\n"); + + hwmon = devm_hwmon_device_register_with_info(dev, "axpwmfan", ctx, + &pwm_fan_chip_info, NULL); + if (IS_ERR(hwmon)) + return dev_err_probe(dev, PTR_ERR(hwmon), + "Failed to register hwmon device\n"); + + return 0; + +put_fan_node: + fwnode_handle_put(fan_node); + + return ret; +} + +static int axiado_pwm_fan_disable(struct device *dev) +{ + struct axiado_pwm_fan_ctx *ctx = dev_get_drvdata(dev); + int ret; + + ret = axiado_pwm_apply_disabled(ctx); + if (ret) + return ret; + + axiado_tach_disable(ctx); + synchronize_irq(ctx->tach.irq); + + return 0; +} + +static void axiado_pwm_fan_shutdown(struct platform_device *pdev) +{ + struct axiado_pwm_fan_ctx *ctx = platform_get_drvdata(pdev); + + /* Best effort during shutdown. */ + axiado_pwm_apply_disabled(ctx); + + axiado_tach_disable(ctx); + synchronize_irq(ctx->tach.irq); +} + +static int axiado_pwm_fan_suspend(struct device *dev) +{ + return axiado_pwm_fan_disable(dev); +} + +static int axiado_pwm_fan_resume(struct device *dev) +{ + struct axiado_pwm_fan_ctx *ctx = dev_get_drvdata(dev); + int ret; + + axiado_tach_enable(ctx); + + scoped_guard(mutex, &ctx->pwm_lock) + ret = pwm_apply_might_sleep(ctx->pwm, &ctx->pwm_state); + + if (ret) + axiado_tach_disable(ctx); + + return ret; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(axiado_pwm_fan_pm, + axiado_pwm_fan_suspend, + axiado_pwm_fan_resume); + +static const struct of_device_id axiado_pwm_fan_match[] = { + { .compatible = "axiado,ax3000-pwm-fan" }, + { } +}; +MODULE_DEVICE_TABLE(of, axiado_pwm_fan_match); + +static struct platform_driver axiado_pwm_fan_driver = { + .probe = axiado_pwm_fan_probe, + .shutdown = axiado_pwm_fan_shutdown, + .driver = { + .name = "axiado-pwm-fan", + .pm = pm_sleep_ptr(&axiado_pwm_fan_pm), + .of_match_table = axiado_pwm_fan_match, + }, +}; +module_platform_driver(axiado_pwm_fan_driver); + +MODULE_AUTHOR("Axiado Corporation"); +MODULE_DESCRIPTION("Axiado PWM fan controller driver"); +MODULE_LICENSE("GPL"); From f9c5d495e22f77c0455dacfca627e06219f05881 Mon Sep 17 00:00:00 2001 From: Tomasz Bojanowski Date: Mon, 21 Sep 2026 23:13:04 +0200 Subject: [PATCH 78/97] hwmon: (nct6683) Add MSI B850M to list of tested boards Tested on MSI MAG B850M MORTAR WIFI with NCT6687D EC firmware version 0.0 build 03/10/25. The driver loads without force=1 and reports voltages, fan speeds and temperatures. Signed-off-by: Tomasz Bojanowski Link: https://patch.msgid.link/20260921211304.74729-1-tomasz@bojanowski.me [groeck: Reordered to retain alphabetical order of boards] Signed-off-by: Guenter Roeck --- Documentation/hwmon/nct6683.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/hwmon/nct6683.rst b/Documentation/hwmon/nct6683.rst index 45eec9dd349a..9f415b740aec 100644 --- a/Documentation/hwmon/nct6683.rst +++ b/Documentation/hwmon/nct6683.rst @@ -67,6 +67,7 @@ ASRock X670E NCT6686D EC firmware version 1.0 build 05/19/22 ASRock B650 Steel Legend WiFi NCT6686D EC firmware version 1.0 build 11/09/23 ASRock Z590 Taichi NCT6686D EC firmware version 1.0 build 01/25/21 MSI B550 NCT6687D EC firmware version 1.0 build 05/07/20 +MSI B850M NCT6687D EC firmware version 0.0 build 03/10/25 MSI X670-P NCT6687D EC firmware version 0.0 build 09/27/22 MSI X870E NCT6687D EC firmware version 0.0 build 11/13/24 =============================== =============================================== From 04f4079ee61a76655781c7be5cf20dac90ebbfde Mon Sep 17 00:00:00 2001 From: Guenter Roeck Date: Tue, 22 Sep 2026 10:10:14 -0700 Subject: [PATCH 79/97] hwmon: (nct6683) Add more MSI boards to list of tested boards Tested with the following boards: - MSI MAG B550M MORTAR MAX WIFI (MS-7C94) - MSI MAG B650 TOMAHAWK WIFI (MS-7D75) - MSI MAG B650M MORTAR WIFI (MS-7D76) Signed-off-by: Guenter Roeck --- Documentation/hwmon/nct6683.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Documentation/hwmon/nct6683.rst b/Documentation/hwmon/nct6683.rst index 9f415b740aec..c47b0709c5e7 100644 --- a/Documentation/hwmon/nct6683.rst +++ b/Documentation/hwmon/nct6683.rst @@ -67,6 +67,9 @@ ASRock X670E NCT6686D EC firmware version 1.0 build 05/19/22 ASRock B650 Steel Legend WiFi NCT6686D EC firmware version 1.0 build 11/09/23 ASRock Z590 Taichi NCT6686D EC firmware version 1.0 build 01/25/21 MSI B550 NCT6687D EC firmware version 1.0 build 05/07/20 +MSI B550M NCT6687D EC firmware version 1.0 build 05/07/20 +MSI B650 NCT6687D EC firmware version 0.0 build 09/27/22 +MSI B650M NCT6687D EC firmware version 0.0 build 09/27/22 MSI B850M NCT6687D EC firmware version 0.0 build 03/10/25 MSI X670-P NCT6687D EC firmware version 0.0 build 09/27/22 MSI X870E NCT6687D EC firmware version 0.0 build 11/13/24 From f47e06715727b4ec545492ff1148e4c3e1681f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20=C3=81vila?= Date: Thu, 24 Sep 2026 01:48:29 -0300 Subject: [PATCH 80/97] hwmon: (dell-smm) Add Dell 16 DC16250 to fan control whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the Dell 16 DC16250, the BIOS keeps the CPU fan at its highest state (~4700 RPM) even when the system is idle at 45 °C, and fan speed writes through pwm1 are overridden by the BIOS within a few seconds. The I8K_FAN_30A3_31A3 SMM code pair works on this machine through the WMI-SMM backend: after writing 1 to pwm1_enable, the fan accepts all three fan states (0, 128 and 255 via pwm1) and reports matching RPM, and writing 2 hands control back to the BIOS. Tested on BIOS 1.8.1 with kernel 7.1.5 (Pop!_OS 24.04). Assisted-by: claude-opus-5-5 Signed-off-by: Emanuel Ávila Acked-by: Pali Rohár Link: https://patch.msgid.link/20260924044829.2889204-1-avilaemanueel@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/dell-smm-hwmon.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/hwmon/dell-smm-hwmon.c b/drivers/hwmon/dell-smm-hwmon.c index 99a58ed263ab..88837f028405 100644 --- a/drivers/hwmon/dell-smm-hwmon.c +++ b/drivers/hwmon/dell-smm-hwmon.c @@ -1679,6 +1679,14 @@ static const struct dmi_system_id i8k_whitelist_fan_control[] __initconst = { }, .driver_data = (void *)&i8k_fan_control_data[I8K_FAN_30A3_31A3], }, + { + .ident = "Dell 16 DC16250", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Dell Inc."), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Dell 16 DC16250"), + }, + .driver_data = (void *)&i8k_fan_control_data[I8K_FAN_30A3_31A3], + }, { } }; From aa8ae39a725347db3511076d2fc5d10caf0a5d8e Mon Sep 17 00:00:00 2001 From: Eduard Zateev Date: Wed, 23 Sep 2026 04:48:27 +0200 Subject: [PATCH 81/97] hwmon: (pmbus/ibm-cffps) Avoid format truncation warnings Building with W=1 reports format truncation warnings when formatting the firmware version values for debugfs. The SMBus helpers return the byte and word values as int, so the compiler cannot infer that the formatted values fit into the small temporary buffers. Cast the values to u8 and u16 before formatting them. This avoids the -Wformat-truncation warnings while keeping the existing output format unchanged. Signed-off-by: Eduard Zateev Link: https://patch.msgid.link/20260923024827.56445-1-hackerowskiy@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/pmbus/ibm-cffps.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/pmbus/ibm-cffps.c b/drivers/hwmon/pmbus/ibm-cffps.c index aad94bcb9ceb..2498c6f4778e 100644 --- a/drivers/hwmon/pmbus/ibm-cffps.c +++ b/drivers/hwmon/pmbus/ibm-cffps.c @@ -166,7 +166,7 @@ static ssize_t ibm_cffps_debugfs_read(struct file *file, char __user *buf, if (rc < 0) goto unlock; - snprintf(&data[i * 2], 3, "%02X", rc); + snprintf(&data[i * 2], 3, "%02X", (u8)rc); } rc = i * 2; @@ -177,7 +177,7 @@ static ssize_t ibm_cffps_debugfs_read(struct file *file, char __user *buf, if (rc < 0) goto unlock; - snprintf(&data[i * 4], 5, "%04X", rc); + snprintf(&data[i * 4], 5, "%04X", (u16)rc); } rc = i * 4; From 5407d2b7d5279b7100621d92f2181184c8f00d62 Mon Sep 17 00:00:00 2001 From: Ricardo Neri Date: Thu, 24 Sep 2026 19:33:21 -0700 Subject: [PATCH 82/97] hwmon: (coretemp) Read TjMax only when refreshing the temperature show_temp() reads TjMax before checking whether the cached temperature is still fresh, but consumes it only when recomputing that temperature. On a cache hit the value is discarded. Discarding it is not free. On CPUs that report TjMax in MSR_IA32_TEMPERATURE_TARGET, get_tjmax() returns the value it just read without storing it in tdata->tjmax, so every call reads the MSR again. That is a cross-CPU call whenever the CPU owning the sensor is not the current one, and it is made while holding tdata->update_lock. Read TjMax where it is used. The value cannot go stale in the meantime: bits 23:16 of MSR_IA32_TEMPERATURE_TARGET are read-only, and the register is package-scoped, so every CPU of the package reads the same value. The temperature reported to userspace does not change. Signed-off-by: Ricardo Neri Link: https://patch.msgid.link/20260924-coretemp-temp-fault-v1-2-1884f0ff97d5@linux.intel.com Signed-off-by: Guenter Roeck --- drivers/hwmon/coretemp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/coretemp.c b/drivers/hwmon/coretemp.c index bee2c9896d4c..a9edbb4924a7 100644 --- a/drivers/hwmon/coretemp.c +++ b/drivers/hwmon/coretemp.c @@ -406,9 +406,9 @@ static ssize_t show_temp(struct device *dev, mutex_lock(&tdata->update_lock); - tjmax = get_tjmax(tdata, dev); /* Check whether the time interval has elapsed */ if (time_after(jiffies, tdata->last_updated + HZ)) { + tjmax = get_tjmax(tdata, dev); rdmsrq_on_cpu(tdata->cpu, tdata->status_reg, &val.q); /* * Bit 31 indicates whether the reading is valid. Some CPUs From b531a295f1546ea8ef7149995418b5a047310744 Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Mon, 21 Sep 2026 15:16:35 +0800 Subject: [PATCH 83/97] dt-bindings: hwmon: pmbus/isl68137: Add Renesas RAA229639 and RAA229640 Add Device Tree compatible strings for Renesas RAA229639 and RAA229640 PMBus devices. They are compatible with existing 2-rail non-TC controllers and use renesas,raa228244 as fallback compatible Signed-off-by: Colin Huang Acked-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260921-add-raa229639-and-raa229640-v3-1-b104ee8a70b2@gmail.com Signed-off-by: Guenter Roeck --- .../devicetree/bindings/hwmon/pmbus/isil,isl68137.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/hwmon/pmbus/isil,isl68137.yaml b/Documentation/devicetree/bindings/hwmon/pmbus/isil,isl68137.yaml index 8216cdf758d8..a604b67b764d 100644 --- a/Documentation/devicetree/bindings/hwmon/pmbus/isil,isl68137.yaml +++ b/Documentation/devicetree/bindings/hwmon/pmbus/isil,isl68137.yaml @@ -65,6 +65,8 @@ properties: - enum: - renesas,raa228942 - renesas,raa228943 + - renesas,raa229639 + - renesas,raa229640 - const: renesas,raa228244 reg: From cbfdb237c3c5a35537a1ee4deceeb4756b9026b8 Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Mon, 21 Sep 2026 15:16:36 +0800 Subject: [PATCH 84/97] hwmon: (pmbus/isl68137) Add Renesas RAA229639 and RAA229640 Add support for the Renesas RAA229639 and RAA229640 PMBus voltage regulator devices. Both devices are supported by the existing raa_dmpvr2_2rail_nontc driver variant, so no functional changes are required beyond adding the corresponding device IDs and OF match entries. Signed-off-by: Colin Huang Link: https://patch.msgid.link/20260921-add-raa229639-and-raa229640-v3-2-b104ee8a70b2@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/pmbus/isl68137.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/hwmon/pmbus/isl68137.c b/drivers/hwmon/pmbus/isl68137.c index 2f7f825bfb69..15b4e3f15f11 100644 --- a/drivers/hwmon/pmbus/isl68137.c +++ b/drivers/hwmon/pmbus/isl68137.c @@ -456,6 +456,8 @@ static const struct i2c_device_id raa_dmpvr_id[] = { { .name = "raa229004", .driver_data = raa_dmpvr2_2rail }, { .name = "raa229141", .driver_data = raa_dmpvr2_2rail_pmbus }, { .name = "raa229621", .driver_data = raa_dmpvr2_2rail }, + { .name = "raa229639", .driver_data = raa_dmpvr2_2rail_nontc }, + { .name = "raa229640", .driver_data = raa_dmpvr2_2rail_nontc }, { } }; @@ -506,6 +508,8 @@ static const struct of_device_id isl68137_of_match[] = { { .compatible = "renesas,raa229001", .data = (void *)raa_dmpvr2_2rail }, { .compatible = "renesas,raa229004", .data = (void *)raa_dmpvr2_2rail }, { .compatible = "renesas,raa229621", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,raa229639", .data = (void *)raa_dmpvr2_2rail_nontc }, + { .compatible = "renesas,raa229640", .data = (void *)raa_dmpvr2_2rail_nontc }, { }, }; From 97d44d58c9f13bd56e80136a3201755afedea7f4 Mon Sep 17 00:00:00 2001 From: Ashish Yadav Date: Mon, 21 Sep 2026 12:27:01 +0530 Subject: [PATCH 85/97] dt-bindings: trivial-devices: Add support for XDPE1A2G7C Add Infineon Digital Multi-phase XDPE1A2G7C Controller to trivial devices. Signed-off-by: Ashish Yadav Acked-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260921065702.93196-2-Ashish.Yadav@infineon.com Signed-off-by: Guenter Roeck --- Documentation/devicetree/bindings/trivial-devices.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/trivial-devices.yaml b/Documentation/devicetree/bindings/trivial-devices.yaml index 5dd6a1aca440..42527c7ee9c8 100644 --- a/Documentation/devicetree/bindings/trivial-devices.yaml +++ b/Documentation/devicetree/bindings/trivial-devices.yaml @@ -174,6 +174,7 @@ properties: # Infineon Multi-phase Digital VR Controller xdpe1a2g7b - infineon,xdpe1a2g5b - infineon,xdpe1a2g7b + - infineon,xdpe1a2g7c # Injoinic IP5108 2.0A Power Bank IC with I2C - injoinic,ip5108 # Injoinic IP5109 2.1A Power Bank IC with I2C From 0fc3a31f627f05d2e0da59d30622c74d94071176 Mon Sep 17 00:00:00 2001 From: Ashish Yadav Date: Mon, 21 Sep 2026 12:27:02 +0530 Subject: [PATCH 86/97] hwmon:(pmbus/xdpe1a2g7b) Add support for xdpe1a2g7c controller Extend xdpe1a2g7b driver to support xdpe1a2g7c model. Signed-off-by: Ashish Yadav Link: https://patch.msgid.link/20260921065702.93196-3-Ashish.Yadav@infineon.com Signed-off-by: Guenter Roeck --- drivers/hwmon/pmbus/Kconfig | 4 ++-- drivers/hwmon/pmbus/xdpe1a2g7b.c | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig index 77e036e96228..fd6641316454 100644 --- a/drivers/hwmon/pmbus/Kconfig +++ b/drivers/hwmon/pmbus/Kconfig @@ -896,7 +896,7 @@ config SENSORS_XDPE1A2G7B tristate "Infineon XDPE1A2G7B" help If you say yes here you get hardware monitoring support for Infineon - XDPE1A2G5B and XDPE1A2G7B. + XDPE1A2G5B,XDPE1A2G7B and XDPE1A2G7C. This driver can also be built as a module. If so, the module will be called xdpe1a2g7b. @@ -906,7 +906,7 @@ config SENSORS_XDPE1A2G7B_REGULATOR depends on SENSORS_XDPE1A2G7B && REGULATOR help If you say yes here you get regulator support for Infineon - XDPE1A2G5B and XDPE1A2G7B multi-phase digital controllers. + XDPE1A2G5B,XDPE1A2G7B and XDPE1A2G7C multi-phase digital controllers. This enables the controllers to be used as regulator devices, providing voltage control through the regulator framework. diff --git a/drivers/hwmon/pmbus/xdpe1a2g7b.c b/drivers/hwmon/pmbus/xdpe1a2g7b.c index 9455ef443b2b..48f6f8081c4b 100644 --- a/drivers/hwmon/pmbus/xdpe1a2g7b.c +++ b/drivers/hwmon/pmbus/xdpe1a2g7b.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0+ /* - * Hardware monitoring driver for Infineon Multi-phase Digital XDPE1A2G5B - * and XDPE1A2G7B Controllers + * Hardware monitoring driver for Infineon Multi-phase Digital XDPE1A2G5B, + * XDPE1A2G7B and XDPE1A2G7C Controllers * * Copyright (c) 2026 Infineon Technologies. All rights reserved. */ @@ -23,7 +23,7 @@ static int xdpe1a2g7b_identify(struct i2c_client *client, int vout_mode; /* - * XDPE1A2G5B and XDPE1A2G7B support both Linear and NVIDIA PWM VID data + * XDPE1A2G5B and XDPE1A2G7B/7C support both Linear and NVIDIA PWM VID data * formats via VOUT_MODE. Note that the device pages/loops are not fully * independent: configuration is shared, so programming each page/loop * separately is not supported. @@ -100,6 +100,7 @@ static int xdpe1a2g7b_probe(struct i2c_client *client) static const struct i2c_device_id xdpe1a2g7b_id[] = { { .name = "xdpe1a2g5b" }, { .name = "xdpe1a2g7b" }, + { .name = "xdpe1a2g7c" }, { } }; @@ -108,6 +109,7 @@ MODULE_DEVICE_TABLE(i2c, xdpe1a2g7b_id); static const struct of_device_id __maybe_unused xdpe1a2g7b_of_match[] = { { .compatible = "infineon,xdpe1a2g5b" }, { .compatible = "infineon,xdpe1a2g7b" }, + { .compatible = "infineon,xdpe1a2g7c" }, {} }; @@ -125,6 +127,6 @@ static struct i2c_driver xdpe1a2g7b_driver = { module_i2c_driver(xdpe1a2g7b_driver); MODULE_AUTHOR("Ashish Yadav "); -MODULE_DESCRIPTION("PMBus driver for Infineon XDPE1A2G5B/7B"); +MODULE_DESCRIPTION("PMBus driver for Infineon XDPE1A2G5B/7B/7C"); MODULE_LICENSE("GPL"); MODULE_IMPORT_NS("PMBUS"); From c4f7b23fe52316438a9480052a51a0a5701e940b Mon Sep 17 00:00:00 2001 From: Petr Pavlu Date: Fri, 2 Oct 2026 10:31:01 +0200 Subject: [PATCH 87/97] hwmon: Replace __MODULE_STRING() with __stringify() __MODULE_STRING() has been an alias for __stringify() since "[PATCH] MODULE_PARM support for older modules" [1] in 2002-12. That change also moved it to the graveyard section at the end of linux/module.h Replace the remaining uses of __MODULE_STRING() with __stringify() in preparation for its removal. Link: https://git.kernel.org/tglx/history/c/136839a1b4 [1] Signed-off-by: Petr Pavlu Reviewed-by: Aleksandr Loktionov Link: https://patch.msgid.link/20261002-module-string-v2-1-fc7438acaf4d@suse.com Signed-off-by: Guenter Roeck --- drivers/hwmon/fschmd.c | 3 ++- drivers/hwmon/nct7904.c | 5 +++-- drivers/hwmon/sch56xx-common.c | 3 ++- drivers/hwmon/w83793.c | 5 +++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/drivers/hwmon/fschmd.c b/drivers/hwmon/fschmd.c index 019fc32bf318..df9418a86c83 100644 --- a/drivers/hwmon/fschmd.c +++ b/drivers/hwmon/fschmd.c @@ -35,6 +35,7 @@ #include #include #include +#include /* Addresses to scan */ static const unsigned short normal_i2c[] = { 0x73, I2C_CLIENT_END }; @@ -43,7 +44,7 @@ static const unsigned short normal_i2c[] = { 0x73, I2C_CLIENT_END }; static bool nowayout = WATCHDOG_NOWAYOUT; module_param(nowayout, bool, 0); MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=" - __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); + __stringify(WATCHDOG_NOWAYOUT) ")"); enum chips { fscpos, fscher, fscscy, fschrc, fschmd, fschds, fscsyl }; diff --git a/drivers/hwmon/nct7904.c b/drivers/hwmon/nct7904.c index 976b8a008e44..04e1a8e6db37 100644 --- a/drivers/hwmon/nct7904.c +++ b/drivers/hwmon/nct7904.c @@ -23,6 +23,7 @@ #include #include #include +#include #define VENDOR_ID_REG 0x7A /* Any bank */ #define NUVOTON_ID 0x50 @@ -113,12 +114,12 @@ static int timeout; module_param(timeout, int, 0); MODULE_PARM_DESC(timeout, "Watchdog timeout in minutes. 1 <= timeout <= 255, default=" - __MODULE_STRING(WATCHDOG_TIMEOUT) "."); + __stringify(WATCHDOG_TIMEOUT) "."); static bool nowayout = WATCHDOG_NOWAYOUT; module_param(nowayout, bool, 0); MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=" - __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); + __stringify(WATCHDOG_NOWAYOUT) ")"); static const unsigned short normal_i2c[] = { 0x2d, 0x2e, I2C_CLIENT_END diff --git a/drivers/hwmon/sch56xx-common.c b/drivers/hwmon/sch56xx-common.c index a385aae3bb0f..febb8ba7dc2c 100644 --- a/drivers/hwmon/sch56xx-common.c +++ b/drivers/hwmon/sch56xx-common.c @@ -18,13 +18,14 @@ #include #include #include +#include #include "sch56xx-common.h" /* Insmod parameters */ static bool nowayout = WATCHDOG_NOWAYOUT; module_param(nowayout, bool, 0); MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=" - __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); + __stringify(WATCHDOG_NOWAYOUT) ")"); #define SIO_SCH56XX_LD_EM 0x0C /* Embedded uController Logical Dev */ #define SIO_UNLOCK_KEY 0x55 /* Key to enable Super-I/O */ diff --git a/drivers/hwmon/w83793.c b/drivers/hwmon/w83793.c index c6ef04c69856..e1f2708b5db6 100644 --- a/drivers/hwmon/w83793.c +++ b/drivers/hwmon/w83793.c @@ -34,6 +34,7 @@ #include #include #include +#include /* Default values */ #define WATCHDOG_TIMEOUT 2 /* 2 minute default timeout */ @@ -57,13 +58,13 @@ static int timeout = WATCHDOG_TIMEOUT; /* default timeout in minutes */ module_param(timeout, int, 0); MODULE_PARM_DESC(timeout, "Watchdog timeout in minutes. 2<= timeout <=255 (default=" - __MODULE_STRING(WATCHDOG_TIMEOUT) ")"); + __stringify(WATCHDOG_TIMEOUT) ")"); static bool nowayout = WATCHDOG_NOWAYOUT; module_param(nowayout, bool, 0); MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=" - __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); + __stringify(WATCHDOG_NOWAYOUT) ")"); /* * Address 0x00, 0x0d, 0x0e, 0x0f in all three banks are reserved From 58d7d43e48044642d22c4c7afc4c5b99103dd684 Mon Sep 17 00:00:00 2001 From: Qian Xiao Date: Fri, 16 Oct 2026 09:35:08 -0700 Subject: [PATCH 88/97] hwmon: (asus_wmi_sensors) Reword BIOS upgrade advice The Known Issues section suggests that upgrading to a BIOS with WMI method version two or greater should rectify the problem. However, the driver refuses to bind when the BIOS reports a version below two, so every system using the driver already meets that condition and the advice does not help. Instead, suggest that upgrading to a more recent BIOS version might help. While at it, fix a typo in the preceding sentence. Link: https://lore.kernel.org/linux-hwmon/CAFGXJCm6ZWK0NsFhZ1_ycgYtTTT3nhqj9mose60t+Rrry1aPvw@mail.gmail.com/ Suggested-by: Guenter Roeck Signed-off-by: Qian Xiao Link: https://patch.msgid.link/20261016163509.249020-1-heyheyco@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/asus_wmi_sensors.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Documentation/hwmon/asus_wmi_sensors.rst b/Documentation/hwmon/asus_wmi_sensors.rst index 8f2096cf5183..741111c9d86c 100644 --- a/Documentation/hwmon/asus_wmi_sensors.rst +++ b/Documentation/hwmon/asus_wmi_sensors.rst @@ -72,7 +72,6 @@ Known Issues: X470 Pro seems particularly bad for this. The more frequently the WMI interface is polled the greater the potential for this to happen. Until you have subjected your computer to an extended soak test while polling the - sensors frequently, don't leave you computer unattended. Upgrading to new - BIOS version with method version greater than or equal to two should - rectify the issue. + sensors frequently, don't leave your computer unattended. Upgrading to a + more recent BIOS version might help. * A few boards report 12v voltages to be ~10v. From 8e9bd28236e633238a7773c9dd809bd87a4db1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asaf=20Nuri=20Bertizlio=C4=9Flu?= Date: Thu, 15 Oct 2026 20:17:59 +0300 Subject: [PATCH 89/97] hwmon: (coretemp) Document sysfs numbering and label meaning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The documentation says that all sysfs entries are named with their core_id, and that the "Core X" label contains the processor number. Neither is accurate. create_core_attrs() numbers the per-core attributes core_id + 2 and always uses temp1 for the package sensor, and show_label() prints topology_core_id(), not the logical CPU number. The difference is easy to see on hybrid CPUs. On an i7-14700F, the "Core 4" label is on temp6 and belongs to CPUs 2 and 3, and the E-cores report core IDs 32 to 43. Describe the actual numbering. The mismatch was found by an LLM comparing the documentation with the code, and verified on the i7-14700F above. Assisted-by: LLM Signed-off-by: Asaf Nuri Bertizlioğlu Tested-by: Ximing Zhang Reviewed-by: Ximing Zhang Link: https://patch.msgid.link/20261015171759.704526-1-ycistak@gmail.com Signed-off-by: Guenter Roeck --- Documentation/hwmon/coretemp.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Documentation/hwmon/coretemp.rst b/Documentation/hwmon/coretemp.rst index 09f885a847e3..0cd3438c00ee 100644 --- a/Documentation/hwmon/coretemp.rst +++ b/Documentation/hwmon/coretemp.rst @@ -46,7 +46,8 @@ mechanism will perform actions to forcibly cool down the processor. Alarm may be raised, if the temperature grows enough (more than TjMax) to trigger the Out-Of-Spec bit. The following table summarizes the exported sysfs files: -All sysfs entries are named with their core_id (represented here by 'X'). +Per-core sysfs entries are numbered core_id + 2 (represented here by 'X'). +The package entries, if available, are always temp1_*. ================= ======================================================== tempX_input Core temperature (in millidegrees Celsius). @@ -58,9 +59,12 @@ tempX_max Maximum recommended operating temperature (Tcontrol). tempX_crit Maximum junction temperature (in millidegrees Celsius). tempX_crit_alarm Set when Out-of-spec bit is set, never clears. Correct CPU operation is no longer guaranteed. -tempX_label Contains string "Core X", where X is processor - number. For Package temp, this will be "Package id Y", - where Y is the package number. +tempX_label Contains string "Core N", where N is the core_id + (X - 2), as shown in + /sys/devices/system/cpu/cpu*/topology/core_id. This + is not the logical CPU number, and core_id values + may be sparse. For Package temp, this will be + "Package id Y", where Y is the package number. ================= ======================================================== On modern CPUs (Nehalem and newer), TjMax is read from the From 37c1974a0ec4ec4624a270291a179c53f8f4fbca Mon Sep 17 00:00:00 2001 From: Ximing Zhang Date: Fri, 16 Oct 2026 12:36:37 +0400 Subject: [PATCH 90/97] hwmon: (coretemp) Fix handling of isolated SMT siblings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 0f8b916bc5b5 ("hwmon: (coretemp) avoid RDMSR interrupts to isolated CPUs") prevents create_core_data() from creating temperature data for an isolated CPU. coretemp_cpu_online(), however, still adds that CPU to pdata->cpumask. When an isolated SMT thread is processed before a housekeeping sibling, the mask makes coretemp_cpu_online() believe that the core is already represented. The housekeeping sibling then gets no temperature attributes. Package attributes are similarly not retried when the first CPU processed in a die is isolated. The mask is also used to select a replacement CPU when a sensor target goes offline. Including isolated CPUs permits the driver to retarget a sensor to one, causing subsequent sysfs reads to send an RDMSR IPI to an isolated CPU. Keep isolated CPUs out of pdata->cpumask by moving the housekeeping check to coretemp_cpu_online(). Sensor creation is then deferred to an eligible CPU, and hotplug replacement targets remain on housekeeping CPUs. Tested on an Intel Core i7-10510U running 6.18.50-3-cachyos-lts, with nohz_full=1 on CPU 1 and its housekeeping SMT sibling on CPU 5. The stock driver omitted Core 1 after module load and retained its readable sensor after CPU 5 was offlined. The patched driver exposed Core 1, removed it when CPU 5 went offline, and restored it when CPU 5 returned. Fixes: 0f8b916bc5b5 ("hwmon: (coretemp) avoid RDMSR interrupts to isolated CPUs") Cc: stable@vger.kernel.org Cc: Marcelo Tosatti Signed-off-by: Ximing Zhang Tested-by: Asaf Nuri Bertizlioğlu Link: https://patch.msgid.link/20261016-codex-coretemp-housekeeping-v1-1-96618c5676b2@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/coretemp.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/drivers/hwmon/coretemp.c b/drivers/hwmon/coretemp.c index a9edbb4924a7..3836beade59f 100644 --- a/drivers/hwmon/coretemp.c +++ b/drivers/hwmon/coretemp.c @@ -568,9 +568,6 @@ static int create_core_data(struct platform_device *pdev, unsigned int cpu, u64 val; int err; - if (!housekeeping_cpu(cpu, HK_TYPE_MISC)) - return 0; - tdata = init_temp_data(pdata, cpu, pkg_flag); if (!tdata) return -ENOMEM; @@ -690,6 +687,14 @@ static int coretemp_cpu_online(unsigned int cpu) if (!cpu_has(c, X86_FEATURE_DTHERM)) return -ENODEV; + /* + * Keep isolated CPUs out of pdata->cpumask. Besides tracking the CPUs + * available to this driver, the mask is used to select the CPU on which + * to read the temperature MSRs. + */ + if (!housekeeping_cpu(cpu, HK_TYPE_MISC)) + return 0; + pdata = platform_get_drvdata(pdev); if (!pdata->hwmon_dev) { struct device *hwmon; @@ -719,8 +724,8 @@ static int coretemp_cpu_online(unsigned int cpu) } /* - * Check whether a thread sibling is already online. If not add the - * interface for this CPU core. + * Check whether a housekeeping thread sibling is already online. If + * not, add the interface for this CPU core. */ if (!cpumask_intersects(&pdata->cpumask, topology_sibling_cpumask(cpu))) coretemp_add_core(pdev, cpu, 0); @@ -750,9 +755,9 @@ static int coretemp_cpu_offline(unsigned int cpu) cpumask_clear_cpu(cpu, &pd->cpumask); /* - * If this is the last thread sibling, remove the CPU core - * interface, If there is still a sibling online, transfer the - * target cpu of that core interface to it. + * If this is the last housekeeping thread sibling, remove the CPU core + * interface. If there is still a housekeeping sibling online, transfer + * the target CPU of that core interface to it. */ target = cpumask_any_and(&pd->cpumask, topology_sibling_cpumask(cpu)); if (target >= nr_cpu_ids) { @@ -764,7 +769,8 @@ static int coretemp_cpu_offline(unsigned int cpu) } /* - * If all cores in this pkg are offline, remove the interface. + * If all housekeeping cores in this pkg are offline, remove the + * interface. */ tdata = get_temp_data(pd, -1); if (cpumask_empty(&pd->cpumask)) { @@ -777,7 +783,7 @@ static int coretemp_cpu_offline(unsigned int cpu) /* * Check whether this core is the target for the package - * interface. We need to assign it to some other cpu. + * interface. We need to assign it to some other housekeeping CPU. */ if (tdata && tdata->cpu == cpu) { target = cpumask_first(&pd->cpumask); From 400be69515c327ced60082b27ede902a8d93cbdb Mon Sep 17 00:00:00 2001 From: Jiale Yao Date: Sun, 27 Sep 2026 16:49:59 +0800 Subject: [PATCH 91/97] hwmon: (gigabyte_waterforce) reject short input reports The HID raw_event callback runs before HID core validates the received report length. waterforce_raw_event() reads command bytes and then accesses fixed offsets for firmware or status data without any length checks. A truncated report can therefore cause out-of-bounds reads and update hwmon state with unrelated data. Require every report to contain the firmware response fields, which also covers both command bytes. After identifying a status response, require the byte at the highest status offset before accessing its payload. Fixes: 42ac68e3d4ba ("hwmon: Add driver for Gigabyte AORUS Waterforce AIO coolers") Cc: stable@vger.kernel.org Signed-off-by: Jiale Yao Link: https://patch.msgid.link/20260927084959.893708-1-yaojiale02@163.com Signed-off-by: Guenter Roeck --- drivers/hwmon/gigabyte_waterforce.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/hwmon/gigabyte_waterforce.c b/drivers/hwmon/gigabyte_waterforce.c index 4eea05f8b569..f4d0376dd68e 100644 --- a/drivers/hwmon/gigabyte_waterforce.c +++ b/drivers/hwmon/gigabyte_waterforce.c @@ -257,6 +257,9 @@ static int waterforce_raw_event(struct hid_device *hdev, struct hid_report *repo { struct waterforce_data *priv = hid_get_drvdata(hdev); + if (size <= FIRMWARE_VER_START_OFFSET_2) + return 0; + if (data[0] == get_firmware_ver_cmd[0] && data[1] == get_firmware_ver_cmd[1]) { /* Received a firmware version report */ priv->firmware_version = @@ -269,6 +272,8 @@ static int waterforce_raw_event(struct hid_device *hdev, struct hid_report *repo if (data[0] != get_status_cmd[0] || data[1] != get_status_cmd[1]) return 0; + if (size <= WATERFORCE_TEMP_SENSOR) + return 0; priv->temp_input[0] = data[WATERFORCE_TEMP_SENSOR] * 1000; priv->speed_input[0] = get_unaligned_le16(data + WATERFORCE_FAN_SPEED); From fcf9aa9beb773bed9d2183bf34f822de12b2c91c Mon Sep 17 00:00:00 2001 From: "Baek Kyu, Kim" Date: Sun, 18 Oct 2026 14:57:50 +0900 Subject: [PATCH 92/97] hwmon: (mcp9982) Update the interval cache only after successful writes mcp9982_write() updates interval_idx before writing the conversion-rate register. If the write fails without changing the register, subsequent update_interval reads report the requested interval instead of the previous setting. Update interval_idx only after regmap_write() succeeds, leaving the previous cached interval in place on error. Fixes: e2fe950f34e5 ("hwmon: add support for MCP998X") Cc: stable@vger.kernel.org Signed-off-by: Baek Kyu, Kim Acked-by: Duicu Victor Link: https://patch.msgid.link/20261018055750.1318723-1-pocj8ur4in@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/mcp9982.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/hwmon/mcp9982.c b/drivers/hwmon/mcp9982.c index 3918dc36c946..2cd41dcbd9af 100644 --- a/drivers/hwmon/mcp9982.c +++ b/drivers/hwmon/mcp9982.c @@ -624,6 +624,7 @@ static int mcp9982_write(struct device *dev, enum hwmon_sensor_types type, u32 a { struct mcp9982_priv *priv = dev_get_drvdata(dev); unsigned int idx; + int ret; u8 addr; switch (type) { @@ -640,9 +641,13 @@ static int mcp9982_write(struct device *dev, enum hwmon_sensor_types type, u32 a idx = find_closest_descending(val, mcp9982_update_interval, ARRAY_SIZE(mcp9982_update_interval)); + ret = regmap_write(priv->regmap, MCP9982_CONV_ADDR, idx); + if (ret) + return ret; + priv->interval_idx = idx; - return regmap_write(priv->regmap, MCP9982_CONV_ADDR, idx); + return 0; default: return -EINVAL; } From 817ac527100c8e20684211eb919ffd6d875adc7a Mon Sep 17 00:00:00 2001 From: "Baek Kyu, Kim" Date: Tue, 20 Oct 2026 00:47:18 +0900 Subject: [PATCH 93/97] hwmon: (sht3x) Return the consumed count for heater writes heater_enable_store() returns the result of i2c_master_send(), which is the two-byte command length on success rather than the sysfs input count. This can report more bytes than were supplied, or a partial write for a fully processed input. For example, writing "on\n" returns two instead of three, so a caller retrying the remaining newline gets -EINVAL even though the heater command succeeded. Retrying the remainder of "off\n" can also send the heater-off command twice. Return count after a complete command transfer. Preserve negative I2C errors and return -EIO for nonnegative incomplete transfers, so a zero-message transfer is not reported as a successful zero-byte write. Keep the existing boolean parsing, command selection and locking. Fixes: 50c22cd154e5 ("hwmon: (sht3x) add humidity heater element control") Cc: stable@vger.kernel.org Signed-off-by: Baek Kyu, Kim Link: https://patch.msgid.link/20261019154718.2328055-1-pocj8ur4in@gmail.com Signed-off-by: Guenter Roeck --- drivers/hwmon/sht3x.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/hwmon/sht3x.c b/drivers/hwmon/sht3x.c index b46de050e20a..a0f5ba5c888d 100644 --- a/drivers/hwmon/sht3x.c +++ b/drivers/hwmon/sht3x.c @@ -543,7 +543,10 @@ static ssize_t heater_enable_store(struct device *dev, mutex_unlock(&data->i2c_lock); - return ret; + if (ret != SHT3X_CMD_LENGTH) + return ret < 0 ? ret : -EIO; + + return count; } static int update_interval_read(struct device *dev) From 0b6eda22f9ab67ddb2a872b0c3fc578ad9f4e27e Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Wed, 7 Oct 2026 01:45:34 +0200 Subject: [PATCH 94/97] hwmon: (acpi_power_meter) Fix 18.45 TW bug The ACPI specification says: "If an error occurs while obtaining the meter reading or if the value is not available then an Integer with all bits set is returned" This integer can also be 64-bit wide on ACPI 2.0 and later systems, so we have to check for that too. Compile-tested only. Reported-by: Grant Slater Closes: https://github.com/lm-sensors/lm-sensors/issues/460 Fixes: 1fefca6c57fb ("hwmon: (acpi_power_meter) Fix 4.29 MW bug") Cc: stable@vger.kernel.org Signed-off-by: Armin Wolf Link: https://patch.msgid.link/20261006234618.10943-1-W_Armin@gmx.de Signed-off-by: Guenter Roeck --- drivers/hwmon/acpi_power_meter.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/acpi_power_meter.c b/drivers/hwmon/acpi_power_meter.c index d8e871509405..3891962f03db 100644 --- a/drivers/hwmon/acpi_power_meter.c +++ b/drivers/hwmon/acpi_power_meter.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,12 @@ #define POWER_METER_CAN_NOTIFY (1 << 3) #define POWER_METER_IS_BATTERY (1 << 8) #define UNKNOWN_HYSTERESIS 0xFFFFFFFF -#define UNKNOWN_POWER 0xFFFFFFFF +/* + * ACPI integers can either be 32-bit or 64-bit wide, so + * we have to check for both values. + */ +#define UNKNOWN_POWER_32 U32_MAX +#define UNKNOWN_POWER_64 U64_MAX #define METER_NOTIFY_CONFIG 0x80 #define METER_NOTIFY_TRIP 0x81 @@ -452,7 +458,7 @@ static int power_meter_read(struct device *dev, enum hwmon_sensor_types type, ret = update_meter(res); if (ret) return ret; - if (res->power == UNKNOWN_POWER) + if (res->power == UNKNOWN_POWER_32 || res->power == UNKNOWN_POWER_64) return -ENODATA; *val = res->power * 1000; break; From 2281f0473fa257d19d59c9e430e91b5a76f418bc Mon Sep 17 00:00:00 2001 From: Josef Bacik Date: Sun, 4 Oct 2026 22:15:41 +0000 Subject: [PATCH 95/97] hwmon: (ftsteutates) serialise the watchdog and sensors The registers of the chip are paged: fts_read_byte() and fts_write_byte() select the page and then access the register. access_lock used to make the two steps atomic. It has been removed because the hwmon core serialises the sensor callbacks, but the watchdog code uses the same two functions and is not called by the hwmon core. So the sensors can now be read, or an alarm be cleared, while the watchdog is being started or stopped. If one of them selects its page between the two steps of the other, the second step hits a register of the wrong page. Take the lock of the hwmon device in the watchdog callbacks, and for the accesses of fts_watchdog_init(), which runs when the hwmon device has already been registered. These are moved to a function of their own, so that the lock is not held while the watchdog is being registered. devres unregisters the watchdog before the hwmon device. Fixes: fa035d98dbae ("hwmon: (ftsteutates) Rely on subsystem locking") Assisted-by: LLM Signed-off-by: Josef Bacik Signed-off-by: Guenter Roeck --- drivers/hwmon/ftsteutates.c | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/ftsteutates.c b/drivers/hwmon/ftsteutates.c index 06be120a349d..10d786960ed6 100644 --- a/drivers/hwmon/ftsteutates.c +++ b/drivers/hwmon/ftsteutates.c @@ -61,6 +61,7 @@ enum WATCHDOG_RESOLUTION { struct fts_data { struct i2c_client *client; + struct device *hwmon_dev; unsigned long last_updated; /* in jiffies */ struct watchdog_device wdd; enum WATCHDOG_RESOLUTION resolution; @@ -240,6 +241,8 @@ static int fts_wd_set_timeout(struct watchdog_device *wdd, unsigned int timeout) resolution = minutes; } + guard(hwmon_lock)(data->hwmon_dev); + ret = fts_wd_set_resolution(data, resolution); if (ret < 0) return ret; @@ -252,6 +255,8 @@ static int fts_wd_start(struct watchdog_device *wdd) { struct fts_data *data = watchdog_get_drvdata(wdd); + guard(hwmon_lock)(data->hwmon_dev); + return fts_write_byte(data->client, FTS_WATCHDOG_TIME_PRESET, wdd->timeout / (u8)data->resolution); } @@ -261,6 +266,9 @@ static int fts_wd_stop(struct watchdog_device *wdd) struct fts_data *data; data = watchdog_get_drvdata(wdd); + + guard(hwmon_lock)(data->hwmon_dev); + return fts_write_byte(data->client, FTS_WATCHDOG_TIME_PRESET, 0); } @@ -276,11 +284,11 @@ static const struct watchdog_ops fts_wd_ops = { .set_timeout = fts_wd_set_timeout, }; -static int fts_watchdog_init(struct fts_data *data) +static int fts_watchdog_read_state(struct fts_data *data) { int timeout, ret; - watchdog_set_drvdata(&data->wdd, data); + guard(hwmon_lock)(data->hwmon_dev); timeout = fts_read_byte(data->client, FTS_WATCHDOG_TIME_PRESET); if (timeout < 0) @@ -302,6 +310,19 @@ static int fts_watchdog_init(struct fts_data *data) set_bit(WDOG_HW_RUNNING, &data->wdd.status); } + return 0; +} + +static int fts_watchdog_init(struct fts_data *data) +{ + int ret; + + watchdog_set_drvdata(&data->wdd, data); + + ret = fts_watchdog_read_state(data); + if (ret < 0) + return ret; + /* Register our watchdog part */ data->wdd.info = &fts_wd_info; data->wdd.ops = &fts_wd_ops; @@ -629,6 +650,9 @@ static int fts_probe(struct i2c_client *client) if (IS_ERR(hwmon_dev)) return PTR_ERR(hwmon_dev); + /* The watchdog code takes its lock */ + data->hwmon_dev = hwmon_dev; + err = fts_watchdog_init(data); if (err) return err; From b8249a3b6b36937e660d7217773ab18111c6b7fe Mon Sep 17 00:00:00 2001 From: Josef Bacik Date: Sun, 4 Oct 2026 22:15:39 +0000 Subject: [PATCH 96/97] hwmon: (nct7904) serialise the watchdog and sensors The registers of the chip are banked: each access selects the bank, which is cached in data->bank_sel, and then reads or writes the register. bank_lock used to make the two steps atomic. It has been removed because the hwmon core serialises the sensor callbacks, but the watchdog callbacks use the same access functions and are not called by the hwmon core. So a sensor can now be read while the watchdog is being pinged. If one of them selects its bank between the two steps of the other, the second step hits a register of the wrong bank: the watchdog is not fed, or its commands are written to some sensor or fan control register. Take the lock of the hwmon device in the watchdog callbacks. The watchdog is registered after the hwmon device, and devres unregisters it first. Fixes: 9ad270bf4dcc ("hwmon: (nct7904) Rely on subsystem locking") Assisted-by: LLM Signed-off-by: Josef Bacik Signed-off-by: Guenter Roeck --- drivers/hwmon/nct7904.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/drivers/hwmon/nct7904.c b/drivers/hwmon/nct7904.c index 04e1a8e6db37..00331e23224a 100644 --- a/drivers/hwmon/nct7904.c +++ b/drivers/hwmon/nct7904.c @@ -127,6 +127,7 @@ static const unsigned short normal_i2c[] = { struct nct7904_data { struct i2c_client *client; + struct device *hwmon_dev; struct watchdog_device wdt; int bank_sel; u32 fanin_mask; @@ -914,6 +915,8 @@ static int nct7904_wdt_start(struct watchdog_device *wdt) { struct nct7904_data *data = watchdog_get_drvdata(wdt); + guard(hwmon_lock)(data->hwmon_dev); + /* Enable soft watchdog timer */ return nct7904_write_reg(data, BANK_0, WDT_LOCK_REG, WDT_SOFT_EN); } @@ -922,6 +925,8 @@ static int nct7904_wdt_stop(struct watchdog_device *wdt) { struct nct7904_data *data = watchdog_get_drvdata(wdt); + guard(hwmon_lock)(data->hwmon_dev); + return nct7904_write_reg(data, BANK_0, WDT_LOCK_REG, WDT_SOFT_DIS); } @@ -940,6 +945,8 @@ static int nct7904_wdt_set_timeout(struct watchdog_device *wdt, */ wdt->timeout = timeout / 60 * 60; + guard(hwmon_lock)(data->hwmon_dev); + return nct7904_write_reg(data, BANK_0, WDT_TIMER_REG, wdt->timeout / 60); } @@ -955,6 +962,8 @@ static int nct7904_wdt_ping(struct watchdog_device *wdt) struct nct7904_data *data = watchdog_get_drvdata(wdt); int ret; + guard(hwmon_lock)(data->hwmon_dev); + /* Disable soft watchdog timer */ ret = nct7904_write_reg(data, BANK_0, WDT_LOCK_REG, WDT_SOFT_DIS); if (ret < 0) @@ -974,6 +983,8 @@ static unsigned int nct7904_wdt_get_timeleft(struct watchdog_device *wdt) struct nct7904_data *data = watchdog_get_drvdata(wdt); int ret; + guard(hwmon_lock)(data->hwmon_dev); + ret = nct7904_read_reg(data, BANK_0, WDT_TIMER_REG); if (ret < 0) return 0; @@ -1128,6 +1139,9 @@ static int nct7904_probe(struct i2c_client *client) if (ret) return ret; + /* The watchdog callbacks take its lock */ + data->hwmon_dev = hwmon_dev; + /* Watchdog initialization */ data->wdt.ops = &nct7904_wdt_ops; data->wdt.info = &nct7904_wdt_info; From e1b3c2630d5b8bc755fe4ba86d20d08c734eb504 Mon Sep 17 00:00:00 2001 From: Josef Bacik Date: Sat, 3 Oct 2026 18:16:42 +0000 Subject: [PATCH 97/97] hwmon: (npcm750-pwm-fan) serialize PWM register updates npcm7xx_pwm_config_set() writes the channel's CMR register and then does a read-modify-write of the CR register that holds the enable and invert bits of all four channels of a PWM module. It is called from the hwmon pwm write path and from npcm7xx_pwm_cz_set_cur_state(), the set_cur_state callback of the thermal cooling devices the driver registers for channels with "cooling-levels". The per-module pwm_lock serializing these updates was dropped on the assumption that the hwmon core serializes all attribute accesses. That only covers the hwmon attributes: the cooling devices are registered directly with the thermal core, which only holds the lock of the cooling device being updated. So a thermal zone update or a write to a cooling device's cur_state can run concurrently with a pwm write, or with the cooling device of another channel of the same module, and one CR update overwrites the other: CPU0 (pwmN write) CPU1 (cooling dev, same module) tmp = ioread32(CR) tmp = ioread32(CR) tmp |= ch1 enable iowrite32(tmp, CR) tmp |= ch0 enable iowrite32(tmp, CR) /* ch1 enable bit lost */ leaving a fan stopped or running at a duty cycle that no longer matches its enable state. The bug was found by code inspection and has not been reproduced. Fix by restoring a per-module mutex around the register updates; a driver lock is used instead of hwmon_lock() because the cooling devices are registered before the hwmon device exists. Fixes: abfb05085177 ("hwmon: (corsair-psu) Rely on subsystem locking") Assisted-by: LLM Signed-off-by: Josef Bacik Signed-off-by: Guenter Roeck --- drivers/hwmon/npcm750-pwm-fan.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/drivers/hwmon/npcm750-pwm-fan.c b/drivers/hwmon/npcm750-pwm-fan.c index d24242eea95e..9af3bd0b6fb6 100644 --- a/drivers/hwmon/npcm750-pwm-fan.c +++ b/drivers/hwmon/npcm750-pwm-fan.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -197,6 +198,8 @@ struct npcm7xx_pwm_fan_data { int pwm_modules; struct clk *pwm_clk; struct clk *fan_clk; + /* serializes CMR/CR updates from hwmon and the cooling devices */ + struct mutex pwm_lock[NPCM7XX_PWM_MAX_MODULES]; spinlock_t fan_lock[NPCM7XX_FAN_MAX_MODULE]; int fan_irq[NPCM7XX_FAN_MAX_MODULE]; bool pwm_present[NPCM7XX_PWM_MAX_CHN_NUM]; @@ -216,6 +219,8 @@ static int npcm7xx_pwm_config_set(struct npcm7xx_pwm_fan_data *data, u32 module = (channel / NPCM7XX_PWM_MAX_CHN_NUM_IN_A_MODULE); u32 tmp_buf, ctrl_en_bit, env_bit; + guard(mutex)(&data->pwm_lock[module]); + /* * Config PWM Comparator register for setting duty cycle */ @@ -986,6 +991,12 @@ static int npcm7xx_pwm_fan_probe(struct platform_device *pdev) output_freq = npcm7xx_pwm_init(data); npcm7xx_fan_init(data); + for (i = 0; i < ARRAY_SIZE(data->pwm_lock); i++) { + ret = devm_mutex_init(dev, &data->pwm_lock[i]); + if (ret) + return ret; + } + for (i = 0; i < NPCM7XX_FAN_MAX_MODULE; i++) { spin_lock_init(&data->fan_lock[i]);