]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
kernel: hwmon lm63: make pwm1 frequency and LUT temp hysteresis writeable 23473/head
authorJan-Henrik Bruhn <git@jhbruhn.de>
Thu, 21 May 2026 12:05:59 +0000 (14:05 +0200)
committerJonas Jelonek <jelonek.jonas@gmail.com>
Sat, 23 May 2026 15:38:38 +0000 (17:38 +0200)
This adds a patch that makes the pwm1 frequency and LUT temperature
hysteresis of lm63 fan controllers writeable, to be able to replicate
vendor cooling behaviour for fans that need a lower PWM frequency
than the default.

Signed-off-by: Jan-Henrik Bruhn <git@jhbruhn.de>
Link: https://github.com/openwrt/openwrt/pull/23473
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
target/linux/generic/pending-6.18/842-hwmon-lm63-make-pwm1_freq-and-lut-hyst-writable.patch [new file with mode: 0644]

diff --git a/target/linux/generic/pending-6.18/842-hwmon-lm63-make-pwm1_freq-and-lut-hyst-writable.patch b/target/linux/generic/pending-6.18/842-hwmon-lm63-make-pwm1_freq-and-lut-hyst-writable.patch
new file mode 100644 (file)
index 0000000..a7b1353
--- /dev/null
@@ -0,0 +1,169 @@
+From: Jan-Henrik Bruhn <kernel@jhbruhn.de>
+Date: Wed, 21 May 2026 00:00:00 +0200
+Subject: [PATCH] hwmon: lm63: expose PWM frequency and LUT hysteresis as writable
+
+The driver caches the PWM frequency register and the CONFIG_FAN slow-clock
+select bit, but never lets userspace pick a different output frequency.
+Add a pwm1_freq sysfs attribute that selects the closest SCS + PFR
+combination for the requested value in Hz, gated by manual mode like
+set_pwm1(). PFR is clamped to 31 so that 2*PFR fits in the chip's 6-bit
+PWM register (matching the existing scaling assumption in show_pwm1).
+
+The hardware LUT hysteresis register is shared by all LUT entries, so
+the per-point pwm1_auto_pointN_temp_hyst attributes can't be made RW
+without N-to-1 cross-attribute side effects. Following the max31760
+precedent, expose a single chip-wide pwm1_auto_point_temp_hyst attribute
+holding the hysteresis amount in millidegrees; the per-point attributes
+stay RO and continue to show the resulting absolute trip-down
+temperature for each entry.
+
+Signed-off-by: Jan-Henrik Bruhn <kernel@jhbruhn.de>
+--- a/drivers/hwmon/lm63.c
++++ b/drivers/hwmon/lm63.c
+@@ -92,6 +92,9 @@ static const unsigned short normal_i2c[]
+ #define LM96163_REG_REMOTE_TEMP_U_LSB 0x32
+ #define LM96163_REG_CONFIG_ENHANCED   0x45
++#define LM63_PWM_BASE_FAST_HZ         180000
++#define LM63_PWM_BASE_SLOW_HZ         700
++
+ #define LM63_MAX_CONVRATE             9
+ #define LM63_MAX_CONVRATE_HZ          32
+@@ -447,6 +450,75 @@ static ssize_t pwm1_enable_store(struct
+       return count;
+ }
++static ssize_t pwm1_freq_show(struct device *dev,
++                            struct device_attribute *dummy, char *buf)
++{
++      struct lm63_data *data = lm63_update_device(dev);
++      unsigned int base = (data->config_fan & 0x08) ?
++                          LM63_PWM_BASE_SLOW_HZ : LM63_PWM_BASE_FAST_HZ;
++
++      return sprintf(buf, "%u\n", base / data->pwm1_freq);
++}
++
++/*
++ * Pick the closest CONFIG_FAN.SCS + PFR for the requested frequency.
++ * PWM_FREQ writes need the LUT unlocked, same as set_pwm1(). LUT PWM
++ * bytes are register-relative; rewrite them after a frequency change
++ * if duty cycles must be preserved.
++ */
++static ssize_t pwm1_freq_store(struct device *dev,
++                             struct device_attribute *dummy,
++                             const char *buf, size_t count)
++{
++      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;
++      bool slow_clock;
++      int err;
++
++      if (!(data->config_fan & 0x20)) /* register is read-only */
++              return -EPERM;
++
++      err = kstrtoul(buf, 10, &val);
++      if (err)
++              return err;
++      if (val == 0)
++              return -EINVAL;
++
++      pfr_fast = clamp_val(DIV_ROUND_CLOSEST((unsigned long)LM63_PWM_BASE_FAST_HZ, val),
++                           1UL, 31UL);
++      pfr_slow = clamp_val(DIV_ROUND_CLOSEST((unsigned long)LM63_PWM_BASE_SLOW_HZ, val),
++                           1UL, 31UL);
++      err_fast = abs_diff(LM63_PWM_BASE_FAST_HZ / pfr_fast, val);
++      err_slow = abs_diff(LM63_PWM_BASE_SLOW_HZ / pfr_slow, val);
++
++      if (err_slow < err_fast) {
++              slow_clock = true;
++              pfr = pfr_slow;
++      } else {
++              slow_clock = false;
++              pfr = pfr_fast;
++      }
++
++      mutex_lock(&data->update_lock);
++      data->config_fan = i2c_smbus_read_byte_data(client, LM63_REG_CONFIG_FAN);
++      if (slow_clock)
++              data->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);
++      data->pwm1_freq = pfr;
++
++      if (data->kind == lm96163) {
++              u8 enh = i2c_smbus_read_byte_data(client,
++                                                LM96163_REG_CONFIG_ENHANCED);
++              data->pwm_highres = !slow_clock && pfr == 8 && (enh & 0x10);
++      }
++      mutex_unlock(&data->update_lock);
++      return count;
++}
++
+ /*
+  * There are 8bit registers for both local(temp1) and remote(temp2) sensor.
+  * For remote sensor registers temp2_offset has to be considered,
+@@ -609,6 +681,42 @@ static ssize_t show_lut_temp_hyst(struct
+ }
+ /*
++ * The LM63 has a single hysteresis register shared by all LUT entries.
++ * Expose it as a chip-wide hysteresis amount in millidegrees; the
++ * per-point pwm1_auto_pointN_temp_hyst attributes remain read-only and
++ * show the resulting absolute trip-down temperature for each entry.
++ */
++static ssize_t pwm1_auto_point_temp_hyst_show(struct device *dev,
++                                            struct device_attribute *dummy,
++                                            char *buf)
++{
++      struct lm63_data *data = lm63_update_device(dev);
++
++      return sprintf(buf, "%d\n", TEMP8_FROM_REG(data->lut_temp_hyst));
++}
++
++static ssize_t pwm1_auto_point_temp_hyst_store(struct device *dev,
++                                             struct device_attribute *dummy,
++                                             const char *buf, size_t count)
++{
++      struct lm63_data *data = dev_get_drvdata(dev);
++      struct i2c_client *client = data->client;
++      unsigned long val;
++      int err;
++
++      err = kstrtoul(buf, 10, &val);
++      if (err)
++              return err;
++
++      mutex_lock(&data->update_lock);
++      data->lut_temp_hyst = HYST_TO_REG(val);
++      i2c_smbus_write_byte_data(client, LM63_REG_LUT_TEMP_HYST,
++                                data->lut_temp_hyst);
++      mutex_unlock(&data->update_lock);
++      return count;
++}
++
++/*
+  * And now the other way around, user-space provides an absolute
+  * hysteresis value and we have to store a relative one
+  */
+@@ -743,6 +851,8 @@ static SENSOR_DEVICE_ATTR(fan1_min, S_IW
+ static SENSOR_DEVICE_ATTR(pwm1, S_IWUSR | S_IRUGO, show_pwm1, set_pwm1, 0);
+ static DEVICE_ATTR_RW(pwm1_enable);
++static DEVICE_ATTR_RW(pwm1_freq);
++static DEVICE_ATTR_RW(pwm1_auto_point_temp_hyst);
+ static SENSOR_DEVICE_ATTR(pwm1_auto_point1_pwm, S_IWUSR | S_IRUGO,
+       show_pwm1, set_pwm1, 1);
+ static SENSOR_DEVICE_ATTR(pwm1_auto_point1_temp, S_IWUSR | S_IRUGO,
+@@ -848,6 +958,8 @@ static DEVICE_ATTR_RW(update_interval);
+ static struct attribute *lm63_attributes[] = {
+       &sensor_dev_attr_pwm1.dev_attr.attr,
+       &dev_attr_pwm1_enable.attr,
++      &dev_attr_pwm1_freq.attr,
++      &dev_attr_pwm1_auto_point_temp_hyst.attr,
+       &sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr,
+       &sensor_dev_attr_pwm1_auto_point1_temp.dev_attr.attr,
+       &sensor_dev_attr_pwm1_auto_point1_temp_hyst.dev_attr.attr,