All of lore.kernel.org
 help / color / mirror / Atom feed
From: Lee Jones <lee@kernel.org>
To: Fenglin Wu <fenglin.wu@oss.qualcomm.com>
Cc: linux-arm-msm@vger.kernel.org, Pavel Machek <pavel@kernel.org>,
	David Collins <david.collins@oss.qualcomm.com>,
	Subbaraman Narayanamurthy
	<subbaraman.narayanamurthy@oss.qualcomm.com>,
	Kamal Wadhwa <kamal.wadhwa@oss.qualcomm.com>,
	linux-leds@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH v3] leds: rgb: leds-qcom-lpg: Add PWM frequency mode support
Date: Thu, 3 Sep 2026 16:30:28 +0100	[thread overview]
Message-ID: <20260903153028.GU2133376@google.com> (raw)
In-Reply-To: <20260827-lpg-pwm-fm-support-v3-1-88ae5208c57b@oss.qualcomm.com>

On Thu, 27 Aug 2026, Fenglin Wu wrote:

> 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.
> 
> Also, guard mutex lock at the top of lpg_pwm_get_state(), to make the
> register read atomic against the register write in lpg_pwm_apply().

Would it be better to split this submission into two separate patches? One to
address the locking in 'lpg_pwm_get_state()' and another to introduce the new
Frequency Mode support.

> 
> 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.
> ---
> Changes in v3:
> - Fixed Sashiko review comments:
>   1. Instead of using an array with pre-rounded values in lpg_clk_period_ns[],
>      calculate the clk period by using the lpg_clk_rates[] to improve accuracy.
>   2. Move the subtype check in lpg_init_channels() so the check in lpg_calc_freq_fm()
>      could be removed along with the error message, also change the message
>      to a debug level when calculating the period in FM failed.
>   3. Check if the requsted period is out of the frequency mode support range
>      at the beginning of lpg_calc_freq_fm().
>   4. Update PWM_FM_ENABLE bit based on the channel enabling status in
>      lpg_apply_frequency_mode().
>   5. Add mutex gaurd in lpg_pwm_get_state() to protect the register read
>      from the register write in lpg_pwm_apply().
> - Link to v2: https://patch.msgid.link/20260804-lpg-pwm-fm-support-v2-1-bc4d1041eeda@oss.qualcomm.com
> 
> Changes in v2:
> - When assigning lsb/best_lsb value to period_actual, cast it to u64 1st then add 1.
> - Link to v1: https://patch.msgid.link/20260729-lpg-pwm-fm-support-v1-1-16d3c72a9921@oss.qualcomm.com
> ---
>  drivers/leds/rgb/leds-qcom-lpg.c | 203 ++++++++++++++++++++++++++++++++++++---
>  1 file changed, 187 insertions(+), 16 deletions(-)
> 
> diff --git "a/drivers/leds/rgb/leds-qcom-lpg.c" "b/drivers/leds/rgb/leds-qcom-lpg.c
> index 24b1f570f524..2907ddb3a568 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,10 @@
>  #define PWM_SEC_ACCESS_REG	0xd0
>  #define PWM_DTEST_REG(x)	(0xe2 + (x) - 1)
>  
> +/* offset 0x50: PWM_FM_MODE_REG on LPG_SUBTYPE_PWM */
> +#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 +116,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 +154,9 @@ struct lpg_channel {
>  
>  	unsigned int pattern_lo_idx;
>  	unsigned int pattern_hi_idx;
> +
> +	bool fm_capable;
> +	bool use_fm;

frequency_mode_enabled

>  };
>  
>  /**
> @@ -238,11 +249,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;
>  };
>  
>  /**
> @@ -435,6 +448,101 @@ 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)

What's "fm"?

> +{
> +	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;

What's the rational for lsb_tmp?  Why not just use lsb?

> +	u64 curr_err, last_err;
> +	u64 min_err = U64_MAX;
> +	u64 max_period_ns;
> +	bool found = false;
> +
> +	clk_len = ARRAY_SIZE(lpg_clk_rates);
> +
> +	/* Check if the requested period exceeds what freuqency mode can support */

Spelling error - please use spell-check throughout.

> +	max_period_ns = div64_u64((u64)NSEC_PER_SEC * (U8_MAX + 1) * (1 << (LPG_MAX_M + 1)),
> +				   lpg_clk_rates[1]);

This is pretty much unreadable.

May I suggest you use a couple of MACROs to make it easier to understand.

> +	if (period_ns > max_period_ns)
> +		return -EINVAL;
> +
> +	/*
> +	 * Formula (rearranged to solve for pwm_value_lsb):
> +	 *
> +	 *                      period_ns * clk_rate
> +	 * pwm_value_lsb = ------------------------------  - 1
> +	 *                 2 * (2^pwm_exp) * NSEC_PER_SEC
> +	 *
> +	 * 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.
> +	 *
> +	 */
> +

This line is superfluous.

> +	for (clk = 1; clk < clk_len; clk++) {

for (int clk = 1; ... for throwaway iterators.

> +		last_err = U64_MAX;
> +
> +		for (exp = 0; exp <= LPG_MAX_M; exp++) {

As above.

And what's 'exp'?

Can we improve the nomenclature to make this whole thing less obfuscated.

> +			/* Calculate pwm_value_lsb for this (clk, exp) pair */

Avoid variable names in comments - plain English please.

> +			lsb_tmp = div64_u64(period_ns * lpg_clk_rates[clk], NSEC_PER_SEC);

How about using mul_u64_u32_div() to avoid overflows?

> +			lsb_tmp >>= (exp + 1);
> +
> +			if (lsb_tmp == 0 || lsb_tmp - 1 > U8_MAX)

Why?  Comment please.

> +				continue;
> +
> +			lsb = lsb_tmp - 1;
> +
> +			period_actual = (u64)(lsb) + 1;
> +			period_actual <<= (exp + 1);
> +			period_actual = div64_u64(period_actual * NSEC_PER_SEC,
> +						  lpg_clk_rates[clk]);

As above.

> +			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_dbg(chan->lpg->dev,
> +			"FM: Cannot generate period %llu ns\n", period_ns);

If you're returning an error, this should be dev_err().

> +		return -EINVAL;
> +	}
> +
> +	chan->clk_sel = best_clk;
> +	chan->pre_div_exp = best_exp;
> +	chan->pwm_value = best_lsb;
> +
> +	/* pre-div and pwm-resolution don't impact frequency mode */

Again, English instead of variable names please and "do not" is better.

> +	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 = div64_u64(period_actual * NSEC_PER_SEC,
> +				  lpg_clk_rates[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, lpg_clk_rates[best_clk], best_clk, best_exp, best_lsb,
> +		period_actual, min_err);

These are seldom useful after initial development - please consider
removing it.
> +
> +	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 +910,26 @@ 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;
> +	bool use_fm;

Initialise this to false.

> +
> +	if (!chan->fm_capable)
> +		return;
> +
> +	use_fm = chan->use_fm && chan->enabled;

Then do:

	if (chan->use_fm && chan->enabled)
		enable_frequency_mode = true;
> +
> +	regmap_update_bits(lpg->map, chan->base + PWM_FM_MODE_REG,
> +			   PWM_FM_ENABLE, 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 +1441,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

"Fallback ..."

> +			 * mode couldn't satisfy the requested period
> +			 */
> +			if (ret < 0)
> +				use_fm = false;

Should we use 'if (ret)' to check for errors instead of 'if (ret < 0)' here?

> +		}
> +
> +		if (!use_fm) {
> +			ret = lpg_calc_freq(chan, state->period);
> +			if (ret < 0)
> +				return ret;

Is 'ret > 0' valid?  If not, just 'if (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,20 +1484,24 @@ 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;

This is different again?

>  	unsigned int resolution;
>  	unsigned int pre_div;
> +	unsigned int clk_idx;
>  	unsigned int refclk;
>  	unsigned int val;
>  	unsigned int m;
>  	u16 pwm_value;
>  	int ret;
>  
> +	guard(mutex)(&lpg->lock);
> +
>  	ret = regmap_read(lpg->map, chan->base + LPG_SIZE_CLK_REG, &val);
>  	if (ret)
>  		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 +1509,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 +1526,29 @@ 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) {

And one more, just for good luck!

> +			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) {
> +			/*
> +			 * FM period formula:
> +			 *   period = 2 * (pwm_value_lsb + 1) * (2^exp) * NSEC_PER_SEC / refclk
> +			 */
> +			pwm_value &= 0xff;
> +			state->period = DIV_ROUND_UP_ULL((u64)2 * (pwm_value + 1) * (1 << m) *

When 1 is shifted, it usually makes sense to use BIT().

> +							  NSEC_PER_SEC, refclk);
> +			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 +1722,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 +1735,14 @@ 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 (chan->subtype == LPG_SUBTYPE_PWM && 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 +2066,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>
> 
> 

-- 
Lee Jones

  parent reply	other threads:[~2026-09-03 15:30 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-28  2:06 [PATCH v3] leds: rgb: leds-qcom-lpg: Add PWM frequency mode support Fenglin Wu
2026-08-28  2:17 ` sashiko-bot
2026-09-03 15:30 ` Lee Jones [this message]
2026-09-08  2:27   ` Fenglin Wu
2026-09-09 16:19     ` Lee Jones

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260903153028.GU2133376@google.com \
    --to=lee@kernel.org \
    --cc=david.collins@oss.qualcomm.com \
    --cc=fenglin.wu@oss.qualcomm.com \
    --cc=kamal.wadhwa@oss.qualcomm.com \
    --cc=linux-arm-msm@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=pavel@kernel.org \
    --cc=subbaraman.narayanamurthy@oss.qualcomm.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.