From: Stephane Lepain Date: Wed, 29 Jul 2026 20:32:48 +0000 (+0200) Subject: qualcommax: fix pwm period calculation X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=5feee05dbbb6d49b0011e32abc87616666f44df7;p=thirdparty%2Fopenwrt.git qualcommax: fix pwm period calculation The GL.iNet GL-AXT1800 (ipq6018) requests a 40,000 ns (25 kHz) PWM period for its four-wire fan: pwms = <&pwm 1 40000 0>; with the IPQ6018 PWM node clocked at 100 MHz. ipq_pwm_apply() pins pwm_div at its maximum and derives only pre_div from the requested period: pre_div = period_ns * clk_rate / (NSEC_PER_SEC * (pwm_div + 1)); if (!pre_div) return -ERANGE; For 40,000 ns at 100 MHz this is floor(0.061) == 0, so the driver deterministically returns -ERANGE and pwm-fan fails to probe on every boot: pwm-fan pwm-fan: failed to enable PWM pwm-fan pwm-fan: Failed to configure PWM: -34 pwm-fan pwm-fan: probe with driver pwm-fan failed with error -34 Probe returns before the tachometer IRQ is requested and before fan-supply is claimed, so the board loses fan RPM reporting and the vcc_fan 5V regulator stays disabled. The fan never spins and the DTS cooling-maps (trips at 50/75/100 C) have no cooling device to bind to. This is the same defect fixed for qualcommbe in commit 8db23dc91a01 ("qualcommbe: fix pwm period calculation") by Kenneth Kasilag, whose rationale explicitly calls out 25 kHz four-wire fan PWM. qualcommax carries its own copy of the pwm-ipq driver and was not covered by that fix. The patch added here is that work backported to qualcommax, with authorship preserved; the base driver differs slightly between targets so the hunks were rebased onto the qualcommax copy. Confirmed on hardware. The board was tested on the 6.12 kernel, which this target has since dropped; the pwm-ipq driver source is identical under 6.12 and 6.18, so the patch and its effect are unchanged. Before, driving the PWM directly from userspace on a GL-AXT1800 running r35591-d0110a25ed: # echo 40000 > period; echo 1 > enable -> write error (-ERANGE) # echo 2700000 > period; echo 1 > enable -> succeeds After building and flashing an image with this patch, pwm-fan probes cleanly and the fan is verified spinning by its own tachometer: /sys/class/hwmon/hwmon7/name = pwmfan /sys/devices/platform/pwm-fan/hwmon/hwmon7/fan1_input = 3548 /sys/class/regulator/regulator.3 (vcc_fan) = enabled /sys/class/thermal/cooling_device1 = pwm-fan and no PWM errors remain in dmesg. Link: https://github.com/openwrt/openwrt/commit/8db23dc91a015bf843f1e3fbd0891574594e86f9 Signed-off-by: Stephane Lepain Link: https://github.com/openwrt/openwrt/pull/24479 Signed-off-by: Robert Marko --- diff --git a/target/linux/qualcommax/patches-6.18/0103-pwm-ipq-fix-period-calculation.patch b/target/linux/qualcommax/patches-6.18/0103-pwm-ipq-fix-period-calculation.patch new file mode 100644 index 00000000000..62b74dbc641 --- /dev/null +++ b/target/linux/qualcommax/patches-6.18/0103-pwm-ipq-fix-period-calculation.patch @@ -0,0 +1,151 @@ +From 314b696c26589f53cd9c1d1ca35edcfaddd362fb Mon Sep 17 00:00:00 2001 +From: Kenneth Kasilag +Date: Sun, 21 Jun 2026 23:26:43 +0000 +Subject: [PATCH] pwm: ipq: fix period calculation + +Comparing the proposed upstream pwm-ipq driver to the downstream vendor +driver, `ipq_pwm_apply()` fixed pwm_div at its maximum and derived only +pre_div from the requested period. Since the period spans +`(pre_div + 1) * (pwm_div + 1)` input clocks, pinning pwm_div near its +maximum forces pre_div towards zero for short periods: once pre_div +rounds to 0 the shortest representable period is +`(pwm_div + 1) / clk_rate`, and any shorter request is rejected with +-ERANGE or silently stretched. The high duration then truncates to 0, +so the output collapses to ~0% duty. + +Since 4-wire fans commonly expect a ~25kHz PWM, it was effectively +unusable, since every duty cycle programs a ~zero high time. + +Search for the (pre_div, pwm_div) pair whose period best approximates +the request instead of fixing pwm_div. Starting pre_div at the smallest +value that keeps pwm_div within its field and stopping once pre_div +exceeds pwm_div bounds the loop and keeps pwm_div as large as possible +for fine duty resolution. + +While reworking the high-duration computation, round it to nearest +rather than truncating, so mid-range duty cycles are not biased low, and +clamp it to pwm_div + 1. Rounding, or a 100% duty request, could +otherwise push hi_dur past the period length and overflow the 16-bit +HI_DURATION field. + +Backported to qualcommax from OpenWrt commit 8db23dc91a015bf843f1e3fbd0891574594e86f9 +("qualcommbe: fix pwm period calculation"). + +Signed-off-by: Kenneth Kasilag +--- + drivers/pwm/pwm-ipq.c | 89 +++++++++++++++++++++++++++++-------------- + 1 file changed, 64 insertions(+), 25 deletions(-) + +--- a/drivers/pwm/pwm-ipq.c ++++ b/drivers/pwm/pwm-ipq.c +@@ -89,10 +89,10 @@ static int ipq_pwm_apply(struct pwm_chip + const struct pwm_state *state) + { + struct ipq_pwm_chip *ipq_chip = ipq_pwm_from_chip(chip); +- unsigned int pre_div, pwm_div; +- u64 period_ns, duty_ns; ++ unsigned int pre_div, pwm_div, best_pre_div, best_pwm_div; ++ u64 period_ns, duty_ns, period_rate, min_diff; + unsigned long val = 0; +- unsigned long hi_dur; ++ u64 hi_dur; + + if (!state->enabled) { + /* clear IPQ_PWM_REG1_ENABLE */ +@@ -112,35 +112,74 @@ static int ipq_pwm_apply(struct pwm_chip + period_ns = min(state->period, IPQ_PWM_MAX_PERIOD_NS); + duty_ns = min(state->duty_cycle, period_ns); + +- /* +- * Pick the maximal value for PWM_DIV that still allows a +- * 100% relative duty cycle. This allows a fine grained +- * selection of duty cycles. +- */ +- pwm_div = IPQ_PWM_MAX_DIV - 1; ++ period_rate = period_ns * ipq_chip->clk_rate; ++ ++ best_pre_div = IPQ_PWM_MAX_DIV; ++ best_pwm_div = IPQ_PWM_MAX_DIV; ++ min_diff = period_rate; + + /* +- * although mul_u64_u64_div_u64 returns a u64, in practice it +- * won't overflow due to above constraints. Take the max period +- * of 10^9 (NSEC_PER_SEC) and the pwm_div + 1 (IPQ_PWM_MAX_DIV) +- * 10^9 * 10^8 +- * ------------- => which fits well into a 32-bit unsigned int. +- * 10^9 * 65,535 ++ * Smaller pre_div than this cannot represent the period (pwm_div would ++ * have to exceed its field), so start the search there. + */ +- pre_div = mul_u64_u64_div_u64(period_ns, ipq_chip->clk_rate, +- (u64)NSEC_PER_SEC * (pwm_div + 1)); ++ pre_div = div64_u64(period_rate, ++ (u64)NSEC_PER_SEC * (IPQ_PWM_MAX_DIV + 1)); + +- if (!pre_div) +- return -ERANGE; ++ for (; pre_div <= IPQ_PWM_MAX_DIV; pre_div++) { ++ u64 remainder; ++ ++ pwm_div = div64_u64_rem(period_rate, ++ (u64)NSEC_PER_SEC * (pre_div + 1), ++ &remainder); ++ /* pwm_div is unsigned; the swap check below catches underflow */ ++ pwm_div--; ++ ++ /* ++ * Swapping pre_div and pwm_div yields the same period but a ++ * larger pwm_div gives finer duty resolution, so once pre_div ++ * exceeds pwm_div every further candidate is strictly worse. ++ */ ++ if (pre_div > pwm_div) ++ break; ++ ++ /* need room for 100% duty, where hi_dur == pwm_div + 1 */ ++ if (pwm_div > IPQ_PWM_MAX_DIV - 1) ++ continue; ++ ++ if (remainder < min_diff) { ++ best_pre_div = pre_div; ++ best_pwm_div = pwm_div; ++ min_diff = remainder; ++ ++ if (min_diff == 0) ++ break; ++ } ++ } + +- pre_div -= 1; ++ pre_div = best_pre_div; ++ pwm_div = best_pwm_div; + +- if (pre_div > IPQ_PWM_MAX_DIV) +- pre_div = IPQ_PWM_MAX_DIV; ++ /* ++ * If the search found no usable candidate, best_pwm_div is left at ++ * IPQ_PWM_MAX_DIV; cap it so pwm_div + 1 still fits the 16-bit field ++ * and 100% duty remains expressible. ++ */ ++ if (pwm_div > IPQ_PWM_MAX_DIV - 1) ++ pwm_div = IPQ_PWM_MAX_DIV - 1; + +- /* pwm duty = HI_DUR * (PRE_DIV + 1) / clk_rate */ +- hi_dur = mul_u64_u64_div_u64(duty_ns, ipq_chip->clk_rate, +- (u64)NSEC_PER_SEC * (pre_div + 1)); ++ /* ++ * high duration = duty_ratio * (pwm_div + 1) ++ * = duty_ns * clk_rate / ((pre_div + 1) * NSEC_PER_SEC) ++ * ++ * Round to nearest to avoid biasing every duty cycle low, then clamp ++ * to (pwm_div + 1): rounding or a 100% request can otherwise push ++ * hi_dur past the period, overflowing the 16-bit HI_DURATION field ++ * and asking the hardware to stay high beyond one period. ++ */ ++ hi_dur = DIV64_U64_ROUND_CLOSEST(duty_ns * ipq_chip->clk_rate, ++ (u64)(pre_div + 1) * NSEC_PER_SEC); ++ if (hi_dur > (u64)pwm_div + 1) ++ hi_dur = (u64)pwm_div + 1; + + val = FIELD_PREP(IPQ_PWM_REG0_HI_DURATION, hi_dur) | + FIELD_PREP(IPQ_PWM_REG0_PWM_DIV, pwm_div);