* [PATCH] leds: rgb: leds-qcom-lpg: Add PWM frequency mode support
@ 2026-07-29 9:48 Fenglin Wu
2026-07-29 9:58 ` sashiko-bot
0 siblings, 1 reply; 2+ messages in thread
From: Fenglin Wu @ 2026-07-29 9:48 UTC (permalink / raw)
To: linux-arm-msm, Lee Jones, Pavel Machek
Cc: David Collins, Subbaraman Narayanamurthy, Kamal Wadhwa,
linux-leds, linux-kernel, Fenglin Wu
Certain PWM channels on a PMIC (e.g. PM8350C PWM4) support a Frequency
Mode (FM) that can generate waveforms with more frequency points than
the standard LPG PWM mode. The trade-off is that the duty cycle can
only be fixed at 50%. Add the FM support. When the PWM channel is
requested to set a duty cycle to exactly 50%, use FM mode by default
as it provides a finer-grained frequency resolution in that case.
Signed-off-by: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
---
Dependency:
This change was made based on the LED color balance fix change which
is under review and has not yet been merged:
https://lore.kernel.org/linux-arm-msm/20260716-lpg-rgb-color-balance-fix-v6-1-b49d51528f61@oss.qualcomm.com/
This change should be applied on top of that one.
---
drivers/leds/rgb/leds-qcom-lpg.c | 199 +++++++++++++++++++++++++++++++++++----
1 file changed, 183 insertions(+), 16 deletions(-)
diff --git a/drivers/leds/rgb/leds-qcom-lpg.c b/drivers/leds/rgb/leds-qcom-lpg.c
index 24b1f570f524..be3398b60445 100644
--- a/drivers/leds/rgb/leds-qcom-lpg.c
+++ b/drivers/leds/rgb/leds-qcom-lpg.c
@@ -21,6 +21,8 @@
#define LPG_SUBTYPE_PWM 0xb
#define LPG_SUBTYPE_HI_RES_PWM 0xc
#define LPG_SUBTYPE_LPG_LITE 0x11
+#define PWM_STATUS1_REG 0x08
+#define PWM_FM_PRESENT BIT(0)
#define LPG_PATTERN_CONFIG_REG 0x40
#define LPG_SIZE_CLK_REG 0x41
#define PWM_CLK_SELECT_MASK GENMASK(1, 0)
@@ -42,6 +44,9 @@
#define PWM_SEC_ACCESS_REG 0xd0
#define PWM_DTEST_REG(x) (0xe2 + (x) - 1)
+#define PWM_FM_MODE_REG 0x50
+#define PWM_FM_ENABLE BIT(7)
+
#define SDAM_REG_PBS_SEQ_EN 0x42
#define SDAM_PBS_TRIG_SET 0xe5
#define SDAM_PBS_TRIG_CLR 0xe6
@@ -110,6 +115,8 @@ struct lpg_data;
* @ramp_hi_pause_ms: pause (in milliseconds) after iterating over pattern
* @pattern_lo_idx: start index of associated pattern
* @pattern_hi_idx: last index of associated pattern
+ * @fm_capable: hardware supports Frequency Mode
+ * @use_fm: set the period using Frequency Mode
*/
struct lpg_channel {
struct lpg *lpg;
@@ -146,6 +153,9 @@ struct lpg_channel {
unsigned int pattern_lo_idx;
unsigned int pattern_hi_idx;
+
+ bool fm_capable;
+ bool use_fm;
};
/**
@@ -238,11 +248,13 @@ struct lpg {
* @sdam_offset: Channel offset in LPG SDAM
* @base: base address for PWM channel registers
* @triled_mask: bitmask for controlling this channel in TRILED
+ * @fm_capable: channel hardware supports Frequency Mode
*/
struct lpg_channel_data {
unsigned int sdam_offset;
unsigned int base;
u8 triled_mask;
+ bool fm_capable;
};
/**
@@ -431,10 +443,105 @@ static int lpg_lut_sync(struct lpg *lpg, unsigned int mask)
static const unsigned int lpg_clk_rates[] = {0, 1024, 32768, 19200000};
static const unsigned int lpg_clk_rates_hi_res[] = {0, 1024, 32768, 19200000, 76800000};
+static const unsigned int lpg_clk_period_ns[] = {0, 976562, 30517, 52};
static const unsigned int lpg_pre_divs[] = {1, 3, 5, 6};
static const unsigned int lpg_pwm_resolution[] = {6, 9};
static const unsigned int lpg_pwm_resolution_hi_res[] = {8, 9, 10, 11, 12, 13, 14, 15};
+static int lpg_calc_freq_fm(struct lpg_channel *chan, uint64_t period_ns)
+{
+ const unsigned int *clk_rate_arr, *clk_period_arr;
+ unsigned int best_clk = 0, best_exp = 0, best_lsb = 0;
+ unsigned int clk, exp, lsb;
+ unsigned int clk_len;
+ u64 lsb_tmp, period_actual;
+ u64 curr_err, last_err;
+ u64 min_err = U64_MAX;
+ bool found = false;
+
+ if (chan->subtype != LPG_SUBTYPE_PWM) {
+ dev_err(chan->lpg->dev, "Only SUBTYPE_PWM support frequency mode\n");
+ return -EOPNOTSUPP;
+ }
+
+ clk_rate_arr = lpg_clk_rates;
+ clk_len = ARRAY_SIZE(lpg_clk_rates);
+ clk_period_arr = lpg_clk_period_ns;
+
+ /*
+ * Formula (rearranged to solve for pwm_value_lsb):
+ *
+ * period_ns
+ * pwm_value_lsb = ------------------------------- - 1
+ * 2 * (2^pwm_exp) * clk_period_ns
+ *
+ * For each (clk, exp) combination, calculate pwm_value_lsb and then
+ * use it to calculate the actual period. Store the combination that
+ * yields the closest match to the desired period.
+ *
+ */
+
+ for (clk = 1; clk < clk_len; clk++) {
+ last_err = U64_MAX;
+
+ for (exp = 0; exp <= LPG_MAX_M; exp++) {
+ /* Calculate pwm_value_lsb for this (clk, exp) pair */
+ lsb_tmp = period_ns;
+ lsb_tmp = div64_u64(lsb_tmp, clk_period_arr[clk]);
+ lsb_tmp >>= (exp + 1);
+
+ if (lsb_tmp == 0 || lsb_tmp - 1 > U8_MAX)
+ continue;
+
+ lsb = lsb_tmp - 1;
+
+ period_actual = (u64)(lsb + 1);
+ period_actual <<= (exp + 1);
+ period_actual *= clk_period_arr[clk];
+
+ curr_err = period_ns - period_actual;
+ if (curr_err < min_err) {
+ min_err = curr_err;
+ best_clk = clk;
+ best_exp = exp;
+ best_lsb = lsb;
+ found = true;
+ }
+
+ if (curr_err > last_err)
+ break;
+
+ last_err = curr_err;
+ }
+ }
+
+ if (!found) {
+ dev_err(chan->lpg->dev,
+ "FM: Cannot generate period %llu ns\n", period_ns);
+ return -EINVAL;
+ }
+
+ chan->clk_sel = best_clk;
+ chan->pre_div_exp = best_exp;
+ chan->pwm_value = best_lsb;
+
+ chan->pre_div_sel = 0;
+ chan->pwm_resolution_sel = 0;
+
+ /* Calculate actual period for reference */
+ period_actual = (u64)(best_lsb + 1);
+ period_actual <<= (best_exp + 1);
+ period_actual *= clk_period_arr[best_clk];
+ chan->period = period_actual;
+
+ dev_dbg(chan->lpg->dev,
+ "Frequency mode: period=%llu ns -> clk=%u Hz (idx=%u), exp=%u, lsb=%u (actual=%llu ns, err=%llu ns)\n",
+ period_ns, clk_rate_arr[best_clk], best_clk, best_exp, best_lsb,
+ period_actual, min_err);
+
+ return 0;
+}
+
static int lpg_calc_freq(struct lpg_channel *chan, uint64_t period)
{
unsigned int i, pwm_resolution_count, best_pwm_resolution_sel = 0;
@@ -802,11 +909,23 @@ static void lpg_apply_dtest(struct lpg_channel *chan)
chan->dtest_value);
}
+static void lpg_apply_frequency_mode(struct lpg_channel *chan)
+{
+ struct lpg *lpg = chan->lpg;
+
+ if (!chan->fm_capable)
+ return;
+
+ regmap_write(lpg->map, chan->base + PWM_FM_MODE_REG,
+ chan->use_fm ? PWM_FM_ENABLE : 0);
+}
+
static void lpg_apply(struct lpg_channel *chan)
{
lpg_disable_glitch(chan);
lpg_apply_freq(chan);
lpg_apply_pwm_value(chan);
+ lpg_apply_frequency_mode(chan);
lpg_apply_control(chan);
lpg_apply_sync(chan);
if (chan->lpg->lpg_chan_sdam)
@@ -1318,28 +1437,42 @@ static int lpg_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
{
struct lpg *lpg = lpg_pwm_from_chip(chip);
struct lpg_channel *chan = &lpg->channels[pwm->hwpwm];
+ bool use_fm = false;
int ret = 0;
if (state->polarity != PWM_POLARITY_NORMAL)
return -EINVAL;
- mutex_lock(&lpg->lock);
+ guard(mutex)(&lpg->lock);
if (state->enabled) {
- ret = lpg_calc_freq(chan, state->period);
- if (ret < 0)
- goto out_unlock;
+ use_fm = chan->fm_capable && (state->duty_cycle == state->period / 2);
- lpg_calc_duty(chan, state->duty_cycle);
+ if (use_fm) {
+ ret = lpg_calc_freq_fm(chan, state->period);
+ /*
+ * fallback to use the standard mode if frequency
+ * mode couldn't satisfy the requested period
+ */
+ if (ret < 0)
+ use_fm = false;
+ }
+
+ if (!use_fm) {
+ ret = lpg_calc_freq(chan, state->period);
+ if (ret < 0)
+ return ret;
+
+ lpg_calc_duty(chan, state->duty_cycle);
+ }
}
+
+ chan->use_fm = use_fm;
chan->enabled = state->enabled;
lpg_apply(chan);
-out_unlock:
- mutex_unlock(&lpg->lock);
-
- return ret;
+ return 0;
}
static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
@@ -1347,8 +1480,10 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
{
struct lpg *lpg = lpg_pwm_from_chip(chip);
struct lpg_channel *chan = &lpg->channels[pwm->hwpwm];
+ bool fm_active = false;
unsigned int resolution;
unsigned int pre_div;
+ unsigned int clk_idx;
unsigned int refclk;
unsigned int val;
unsigned int m;
@@ -1360,7 +1495,7 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
return ret;
if (chan->subtype == LPG_SUBTYPE_HI_RES_PWM) {
- unsigned int clk_idx = FIELD_GET(PWM_CLK_SELECT_HI_RES_MASK, val);
+ clk_idx = FIELD_GET(PWM_CLK_SELECT_HI_RES_MASK, val);
if (clk_idx >= ARRAY_SIZE(lpg_clk_rates_hi_res))
return -EINVAL;
@@ -1368,7 +1503,8 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
refclk = lpg_clk_rates_hi_res[clk_idx];
resolution = lpg_pwm_resolution_hi_res[FIELD_GET(PWM_SIZE_HI_RES_MASK, val)];
} else {
- refclk = lpg_clk_rates[FIELD_GET(PWM_CLK_SELECT_MASK, val)];
+ clk_idx = FIELD_GET(PWM_CLK_SELECT_MASK, val);
+ refclk = lpg_clk_rates[clk_idx];
resolution = lpg_pwm_resolution[FIELD_GET(PWM_SIZE_SELECT_MASK, val)];
}
@@ -1384,9 +1520,32 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
if (ret)
return ret;
- state->period = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC * ((1 << resolution) - 1) *
- pre_div * (1 << m), refclk);
- state->duty_cycle = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC * pwm_value * pre_div * (1 << m), refclk);
+ if (chan->fm_capable) {
+ ret = regmap_read(lpg->map, chan->base + PWM_FM_MODE_REG, &val);
+ if (ret)
+ return ret;
+
+ fm_active = !!(val & PWM_FM_ENABLE);
+ }
+
+ if (fm_active) {
+ if (clk_idx >= ARRAY_SIZE(lpg_clk_period_ns))
+ return -EINVAL;
+
+ /*
+ * FM period formula:
+ * period = 2 * (pwm_value_lsb + 1) * (2^exp) * clk_period_ns
+ */
+ pwm_value &= 0xff;
+ state->period = (u64)2 * (pwm_value + 1) * (1 << m) *
+ lpg_clk_period_ns[clk_idx];
+ state->duty_cycle = state->period / 2;
+ } else {
+ state->period = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC *
+ ((1 << resolution) - 1) * pre_div * (1 << m), refclk);
+ state->duty_cycle = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC *
+ pwm_value * pre_div * (1 << m), refclk);
+ }
} else {
state->period = 0;
state->duty_cycle = 0;
@@ -1560,7 +1719,8 @@ static int lpg_init_channels(struct lpg *lpg)
{
const struct lpg_data *data = lpg->data;
struct lpg_channel *chan;
- int i;
+ unsigned int status;
+ int ret, i;
for (i = 0; i < data->num_channels; i++) {
chan = &lpg->channels[i];
@@ -1572,6 +1732,13 @@ static int lpg_init_channels(struct lpg *lpg)
chan->sdam_offset = data->channels[i].sdam_offset;
regmap_read(lpg->map, chan->base + LPG_SUBTYPE_REG, &chan->subtype);
+ if (data->channels[i].fm_capable) {
+ ret = regmap_read(lpg->map, chan->base + PWM_STATUS1_REG, &status);
+ if (ret < 0)
+ return ret;
+
+ chan->fm_capable = !!(status & PWM_FM_PRESENT);
+ }
}
return 0;
@@ -1895,7 +2062,7 @@ static const struct lpg_data pm8350c_pwm_data = {
{ .base = 0xe800, .triled_mask = BIT(7), .sdam_offset = 0x48 },
{ .base = 0xe900, .triled_mask = BIT(6), .sdam_offset = 0x56 },
{ .base = 0xea00, .triled_mask = BIT(5), .sdam_offset = 0x64 },
- { .base = 0xeb00 },
+ { .base = 0xeb00, .fm_capable = true },
},
};
---
base-commit: 1795fd2dbe84ef4d393b69a0b2a3b371f810bde5
change-id: 20260728-lpg-pwm-fm-support-5f0a4636d3dc
prerequisite-change-id: 20260605-lpg-rgb-color-balance-fix-82436649abf3:v6
prerequisite-patch-id: 931a69abc7b07f7f5b5603e8159c092d9a54703f
Best regards,
--
Fenglin Wu <fenglin.wu@oss.qualcomm.com>
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH] leds: rgb: leds-qcom-lpg: Add PWM frequency mode support
2026-07-29 9:48 [PATCH] leds: rgb: leds-qcom-lpg: Add PWM frequency mode support Fenglin Wu
@ 2026-07-29 9:58 ` sashiko-bot
0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-07-29 9:58 UTC (permalink / raw)
To: Fenglin Wu; +Cc: lee, linux-leds
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] The FM mode frequency calculation uses truncated clock periods, causing the driver to select configurations that generate a hardware period longer than requested, which violates the PWM API contract.
- [Medium] The driver unconditionally logs a `dev_err` when the opportunistic FM mode fallback fails, allowing unprivileged userspace to spam the kernel log (DoS vector).
- [Medium] Disabling the PWM unconditionally clears the FM mode hardware bit, causing subsequent state readbacks to return severely corrupted period and duty cycle values.
--
--- Patch [1]: [PATCH] leds: rgb: leds-qcom-lpg: Add PWM frequency mode support ---
commit 7ebdd4f9eacf70734e7346f7608c74a404663422
Author: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
leds: rgb: leds-qcom-lpg: Add PWM frequency mode support
Certain PWM channels on a PMIC (e.g. PM8350C PWM4) support a Frequency Mode
(FM) that can generate waveforms with more frequency points than the standard
LPG PWM mode. Add FM support so that when the duty cycle is requested to be
exactly 50%, FM mode is used by default for finer-grained frequency resolution.
> diff --git a/drivers/leds/rgb/leds-qcom-lpg.c b/drivers/leds/rgb/leds-qcom-lpg.c
> index d7d6518de30f2..166009ef53af8 100644
> --- a/drivers/leds/rgb/leds-qcom-lpg.c
> +++ b/drivers/leds/rgb/leds-qcom-lpg.c
[ ... ]
> @@ -413,10 +425,105 @@ static int lpg_lut_sync(struct lpg *lpg, unsigned int mask)
>
> static const unsigned int lpg_clk_rates[] = {0, 1024, 32768, 19200000};
> static const unsigned int lpg_clk_rates_hi_res[] = {0, 1024, 32768, 19200000, 76800000};
> +static const unsigned int lpg_clk_period_ns[] = {0, 976562, 30517, 52};
> static const unsigned int lpg_pre_divs[] = {1, 3, 5, 6};
> static const unsigned int lpg_pwm_resolution[] = {6, 9};
> static const unsigned int lpg_pwm_resolution_hi_res[] = {8, 9, 10, 11, 12, 13, 14, 15};
>
> +static int lpg_calc_freq_fm(struct lpg_channel *chan, uint64_t period_ns)
> +{
[ ... ]
> + for (clk = 1; clk < clk_len; clk++) {
> + last_err = U64_MAX;
> +
> + for (exp = 0; exp <= LPG_MAX_M; exp++) {
> + /* Calculate pwm_value_lsb for this (clk, exp) pair */
> + lsb_tmp = period_ns;
> + lsb_tmp = div64_u64(lsb_tmp, clk_period_arr[clk]);
[Severity: Medium]
Will this division by a truncated nanosecond value (e.g. 52 ns instead of
52.0833 ns for the 19.2MHz clock) artificially increase the computed LSB?
The PWM framework API contract expects the actual generated period to be
equal to or less than the requested period, but this calculation causes the
hardware period to exceed the requested period.
> + lsb_tmp >>= (exp + 1);
[ ... ]
> + if (!found) {
> + dev_err(chan->lpg->dev,
> + "FM: Cannot generate period %llu ns\n", period_ns);
> + return -EINVAL;
> + }
[Severity: Medium]
Since the caller lpg_pwm_apply() gracefully falls back to standard mode
when this returns an error, will unconditionally logging a dev_err allow
unprivileged userspace to spam the kernel log with period requests outside
the FM mode bounds?
> +
> + chan->clk_sel = best_clk;
> + chan->pre_div_exp = best_exp;
[ ... ]
> +static void lpg_apply_frequency_mode(struct lpg_channel *chan)
> +{
> + struct lpg *lpg = chan->lpg;
> +
> + if (!chan->fm_capable)
> + return;
> +
> + regmap_write(lpg->map, chan->base + PWM_FM_MODE_REG,
> + chan->use_fm ? PWM_FM_ENABLE : 0);
> +}
[Severity: Medium]
When disabling the PWM in lpg_pwm_apply(), state->enabled is false, which
causes chan->use_fm to be set to false.
Will this unconditionally write 0 to the register, clearing the FM mode
status bit while leaving the clock values intact as FM values?
If the driver is unbound and rebound, or if the hardware state is read back
upon export, this could cause the driver to incorrectly interpret the FM
values using standard mode formulas, yielding corrupted period and duty cycle
values.
[ ... ]
> @@ -1230,28 +1349,42 @@ static int lpg_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
[ ... ]
> if (state->enabled) {
> - ret = lpg_calc_freq(chan, state->period);
> - if (ret < 0)
> - goto out_unlock;
> + use_fm = chan->fm_capable && (state->duty_cycle == state->period / 2);
>
> - lpg_calc_duty(chan, state->duty_cycle);
> + if (use_fm) {
> + ret = lpg_calc_freq_fm(chan, state->period);
> + /*
> + * fallback to use the standard mode if frequency
> + * mode couldn't satisfy the requested period
> + */
> + if (ret < 0)
> + use_fm = false;
> + }
> +
> + if (!use_fm) {
> + ret = lpg_calc_freq(chan, state->period);
> + if (ret < 0)
> + return ret;
> +
> + lpg_calc_duty(chan, state->duty_cycle);
> + }
> }
> +
> + chan->use_fm = use_fm;
> chan->enabled = state->enabled;
>
> lpg_apply(chan);
[ ... ]
> @@ -1296,9 +1432,32 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
[ ... ]
> + if (fm_active) {
> + if (clk_idx >= ARRAY_SIZE(lpg_clk_period_ns))
> + return -EINVAL;
> +
> + /*
> + * FM period formula:
> + * period = 2 * (pwm_value_lsb + 1) * (2^exp) * clk_period_ns
> + */
> + pwm_value &= 0xff;
> + state->period = (u64)2 * (pwm_value + 1) * (1 << m) *
> + lpg_clk_period_ns[clk_idx];
[Severity: Medium]
Does calculating the state back into nanoseconds using the same lossy clock
period array also cause inaccurate state readbacks here?
> + state->duty_cycle = state->period / 2;
> + } else {
> + state->period = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC *
> + ((1 << resolution) - 1) * pre_div * (1 << m), refclk);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260729-lpg-pwm-fm-support-v1-1-16d3c72a9921@oss.qualcomm.com?part=1
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-07-29 9:58 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-29 9:48 [PATCH] leds: rgb: leds-qcom-lpg: Add PWM frequency mode support Fenglin Wu
2026-07-29 9:58 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.