From: Jan-Henrik Bruhn Date: Thu, 21 May 2026 12:05:59 +0000 (+0200) Subject: kernel: hwmon lm63: make pwm1 frequency and LUT temp hysteresis writeable X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7a5eefe10896a3a3af1c293d96ce7c4afabaf6e2;p=thirdparty%2Fopenwrt.git kernel: hwmon lm63: make pwm1 frequency and LUT temp hysteresis writeable 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 Link: https://github.com/openwrt/openwrt/pull/23473 Signed-off-by: Jonas Jelonek --- 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 index 00000000000..a7b1353abfc --- /dev/null +++ b/target/linux/generic/pending-6.18/842-hwmon-lm63-make-pwm1_freq-and-lut-hyst-writable.patch @@ -0,0 +1,169 @@ +From: Jan-Henrik Bruhn +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 +--- 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,