mbox series

[v6,0/4] Qualcomm Light Pulse Generator

Message ID 20201021201224.3430546-1-bjorn.andersson@linaro.org
Headers show
Series Qualcomm Light Pulse Generator | expand

Message

Bjorn Andersson Oct. 21, 2020, 8:12 p.m. UTC
This series introduces a generic pattern interface in the LED class and
a driver for the Qualcomm Light Pulse Generator.

It seems like it's been almost 3 years since I posted v3, which was hung
up on the lack of conclusion on the hw_pattern and multicolor support.
Now that those are concluded I hope we can make some progress on the LPG
support again.

The dts patches are included in the series as "examples", ultimately my
expectation is that the dt binding and driver patches are picked up
through the leds tree, while Andy or myself take the dts patches.

Bjorn Andersson (4):
  dt-bindings: leds: Add Qualcomm Light Pulse Generator binding
  leds: Add driver for Qualcomm LPG
  arm64: dts: qcom: pm(i)8994: Add mpp and lpg blocks
  arm64: dts: qcom: Add user LEDs on db820c

 .../bindings/leds/leds-qcom-lpg.yaml          |  170 +++
 arch/arm64/boot/dts/qcom/apq8096-db820c.dtsi  |   49 +
 arch/arm64/boot/dts/qcom/pm8994.dtsi          |    9 +
 arch/arm64/boot/dts/qcom/pmi8994.dtsi         |   20 +
 drivers/leds/Kconfig                          |    9 +
 drivers/leds/Makefile                         |    1 +
 drivers/leds/leds-qcom-lpg.c                  | 1190 +++++++++++++++++
 7 files changed, 1448 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/leds/leds-qcom-lpg.yaml
 create mode 100644 drivers/leds/leds-qcom-lpg.c

-- 
2.28.0

Comments

Marijn Suijten April 18, 2021, 9:54 p.m. UTC | #1
Hi Bjorn,

On 10/21/20 10:12 PM, Bjorn Andersson wrote:
> The Light Pulse Generator (LPG) is a PWM-block found in a wide range of
> PMICs from Qualcomm. It can operate on fixed parameters or based on a
> lookup-table, altering the duty cycle over time - which provides the
> means for e.g. hardware assisted transitions of LED brightness.
> 
> Signed-off-by: Bjorn Andersson <bjorn.andersson@linaro.org>
> Tested-by: Luca Weiss <luca@z3ntu.xyz>


Thanks for these patches.  I have tested them successfully on the Sony 
Xperia XA2 (Discovery, Nile platform) with the leds on the PM660l - feel 
free to add my Tested-by.  Should I send the configuration your way for 
inclusion in this patch, or submit them separately (either applied 
after, or included as separate patches in the next version of this series)?

> +/**
> + * struct lpg_data - initialization data
> + * @lut_base:		base address of LUT block
> + * @lut_size:		number of entries in LUT
> + * @triled_base:	base address of TRILED
> + * @pwm_9bit_mask:	bitmask for switching from 6bit to 9bit pwm


Our downstream kernel derives this from the "LPG subtype" for each 
distinct channel, read from register offset +0x5.  A value of 0xb is 
subtype "PWM" with a shift of 2, a value of 0x11 is subtype "LPG_LITE" 
with a shift of 4.  Can we do the same here instead of hardcoding it for 
all channels in the LPG at once?  How should we determine if the mask is 
one or two bits wide, for the 3<<4 case?

> + * @num_channels:	number of channels in LPG
> + * @channels:		list of channel initialization data
> + */

> +	if (ping_pong) {
> +		if (len % 2)
> +			hi_pause = 0;
> +		else
> +			hi_pause = pattern[len + 1 / 2].delta_t;


len + 1 should be wrapped in parentheses just like the reassignment to 
len= below, otherwise this is always an out of bounds read (at len + 0).

> +		lo_pause = pattern[len - 1].delta_t;
> +
> +		len = (len + 1) / 2;
> +	} else {
> +		hi_pause = pattern[len - 1].delta_t;
> +		lo_pause = 0;
> +	}
> +
> +	ret = lpg_lut_store(lpg, pattern, len, &lo_idx, &hi_idx);
> +	if (ret < 0)
> +		goto out;
> +
> +	for (i = 0; i < led->num_channels; i++) {
> +		chan = led->channels[i];
> +
> +		chan->ramp_duration_ms = pattern[0].delta_t * len;


Perhaps this could store the duration of a single step instead, since 
the only use in lpg_apply_lut_control divides it by pattern length again?

> +		chan->ramp_ping_pong = ping_pong;
> +		chan->ramp_oneshot = repeat != -1;
> +
> +		chan->ramp_lo_pause_ms = lo_pause;
> +		chan->ramp_hi_pause_ms = hi_pause;
> +
> +		chan->pattern_lo_idx = lo_idx;
> +		chan->pattern_hi_idx = hi_idx;
> +	}
> +
> +out:
> +	return ret;
> +}

> +static int lpg_init_lut(struct lpg *lpg)
> +{
> +	const struct lpg_data *data = lpg->data;
> +	size_t bitmap_size;
> +
> +	if (!data->lut_base)
> +		return 0;
> +
> +	lpg->lut_base = data->lut_base;
> +	lpg->lut_size = data->lut_size;
> +
> +	bitmap_size = BITS_TO_LONGS(lpg->lut_size) * sizeof(unsigned long);
> +	lpg->lut_bitmap = devm_kzalloc(lpg->dev, bitmap_size, GFP_KERNEL);


Would it be nicer to use BITS_TO_BYTES here, or otherwise 
devm_kcalloc(..., bitmap_size, sizeof(long), ...) without mutiplying 
with sizeof(unsigned long)?

> +
> +	bitmap_clear(lpg->lut_bitmap, 0, lpg->lut_size);
> +	return lpg->lut_bitmap ? 0 : -ENOMEM;
> +}
> +
> +static int lpg_probe(struct platform_device *pdev)
> +{
> +	struct device_node *np;
> +	struct lpg *lpg;
> +	int ret;
> +	int i;
> +
> +	lpg = devm_kzalloc(&pdev->dev, sizeof(*lpg), GFP_KERNEL);
> +	if (!lpg)
> +		return -ENOMEM;
> +
> +	lpg->data = of_device_get_match_data(&pdev->dev);
> +	if (!lpg->data)
> +		return -EINVAL;
> +
> +	lpg->dev = &pdev->dev;
> +
> +	lpg->map = dev_get_regmap(pdev->dev.parent, NULL);
> +	if (!lpg->map) {
> +		dev_err(&pdev->dev, "parent regmap unavailable\n");
> +		return -ENXIO;
> +	}
> +
> +	ret = lpg_init_channels(lpg);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = lpg_init_triled(lpg);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = lpg_init_lut(lpg);
> +	if (ret < 0)
> +		return ret;


How about turning these returns into dev_err_probe?  I'm not sure if 
that's the expected way to go nowadays, but having some form of logging 
when a driver fails to probe is always good to have.

Thanks!
Marijn

> +
> +	for_each_available_child_of_node(pdev->dev.of_node, np) {
> +		ret = lpg_add_led(lpg, np);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	for (i = 0; i < lpg->num_channels; i++)
> +		lpg_apply_dtest(&lpg->channels[i]);
> +
> +	ret = lpg_add_pwm(lpg);
> +	if (ret)
> +		return ret;
> +
> +	platform_set_drvdata(pdev, lpg);
> +
> +	return 0;
> +}
Bjorn Andersson April 28, 2021, 10:39 p.m. UTC | #2
On Sun 18 Apr 16:54 CDT 2021, Marijn Suijten wrote:

> Hi Bjorn,
> 
> On 10/21/20 10:12 PM, Bjorn Andersson wrote:
> > The Light Pulse Generator (LPG) is a PWM-block found in a wide range of
> > PMICs from Qualcomm. It can operate on fixed parameters or based on a
> > lookup-table, altering the duty cycle over time - which provides the
> > means for e.g. hardware assisted transitions of LED brightness.
> > 
> > Signed-off-by: Bjorn Andersson <bjorn.andersson@linaro.org>
> > Tested-by: Luca Weiss <luca@z3ntu.xyz>
> 
> 
> Thanks for these patches.  I have tested them successfully on the Sony
> Xperia XA2 (Discovery, Nile platform) with the leds on the PM660l - feel
> free to add my Tested-by.  Should I send the configuration your way for
> inclusion in this patch, or submit them separately (either applied after, or
> included as separate patches in the next version of this series)?
> 

Thanks for testing, let's try to land this first iteration first and
then we can add PM660l and PM8150* definitions/support on top.

> > +/**
> > + * struct lpg_data - initialization data
> > + * @lut_base:		base address of LUT block
> > + * @lut_size:		number of entries in LUT
> > + * @triled_base:	base address of TRILED
> > + * @pwm_9bit_mask:	bitmask for switching from 6bit to 9bit pwm
> 
> 
> Our downstream kernel derives this from the "LPG subtype" for each distinct
> channel, read from register offset +0x5.  A value of 0xb is subtype "PWM"
> with a shift of 2, a value of 0x11 is subtype "LPG_LITE" with a shift of 4.
> Can we do the same here instead of hardcoding it for all channels in the LPG
> at once?  How should we determine if the mask is one or two bits wide, for
> the 3<<4 case?
> 

I don't see any obvious solution to the latter, so perhaps we should
just stick with defining this per compatible? Or am I reading your
suggestion wrong?

> > + * @num_channels:	number of channels in LPG
> > + * @channels:		list of channel initialization data
> > + */
> 
> > +	if (ping_pong) {
> > +		if (len % 2)
> > +			hi_pause = 0;
> > +		else
> > +			hi_pause = pattern[len + 1 / 2].delta_t;
> 
> 
> len + 1 should be wrapped in parentheses just like the reassignment to len=
> below, otherwise this is always an out of bounds read (at len + 0).
> 

Thanks for spotting this.

> > +		lo_pause = pattern[len - 1].delta_t;
> > +
> > +		len = (len + 1) / 2;
> > +	} else {
> > +		hi_pause = pattern[len - 1].delta_t;
> > +		lo_pause = 0;
> > +	}
> > +
> > +	ret = lpg_lut_store(lpg, pattern, len, &lo_idx, &hi_idx);
> > +	if (ret < 0)
> > +		goto out;
> > +
> > +	for (i = 0; i < led->num_channels; i++) {
> > +		chan = led->channels[i];
> > +
> > +		chan->ramp_duration_ms = pattern[0].delta_t * len;
> 
> 
> Perhaps this could store the duration of a single step instead, since the
> only use in lpg_apply_lut_control divides it by pattern length again?
> 

Sounds like a good idea.

> > +		chan->ramp_ping_pong = ping_pong;
> > +		chan->ramp_oneshot = repeat != -1;
> > +
> > +		chan->ramp_lo_pause_ms = lo_pause;
> > +		chan->ramp_hi_pause_ms = hi_pause;
> > +
> > +		chan->pattern_lo_idx = lo_idx;
> > +		chan->pattern_hi_idx = hi_idx;
> > +	}
> > +
> > +out:
> > +	return ret;
> > +}
> 
> > +static int lpg_init_lut(struct lpg *lpg)
> > +{
> > +	const struct lpg_data *data = lpg->data;
> > +	size_t bitmap_size;
> > +
> > +	if (!data->lut_base)
> > +		return 0;
> > +
> > +	lpg->lut_base = data->lut_base;
> > +	lpg->lut_size = data->lut_size;
> > +
> > +	bitmap_size = BITS_TO_LONGS(lpg->lut_size) * sizeof(unsigned long);
> > +	lpg->lut_bitmap = devm_kzalloc(lpg->dev, bitmap_size, GFP_KERNEL);
> 
> 
> Would it be nicer to use BITS_TO_BYTES here, or otherwise devm_kcalloc(...,
> bitmap_size, sizeof(long), ...) without mutiplying with sizeof(unsigned
> long)?
> 

Yes, that's cleaner.

> > +
> > +	bitmap_clear(lpg->lut_bitmap, 0, lpg->lut_size);
> > +	return lpg->lut_bitmap ? 0 : -ENOMEM;
> > +}
> > +
> > +static int lpg_probe(struct platform_device *pdev)
> > +{
> > +	struct device_node *np;
> > +	struct lpg *lpg;
> > +	int ret;
> > +	int i;
> > +
> > +	lpg = devm_kzalloc(&pdev->dev, sizeof(*lpg), GFP_KERNEL);
> > +	if (!lpg)
> > +		return -ENOMEM;
> > +
> > +	lpg->data = of_device_get_match_data(&pdev->dev);
> > +	if (!lpg->data)
> > +		return -EINVAL;
> > +
> > +	lpg->dev = &pdev->dev;
> > +
> > +	lpg->map = dev_get_regmap(pdev->dev.parent, NULL);
> > +	if (!lpg->map) {
> > +		dev_err(&pdev->dev, "parent regmap unavailable\n");
> > +		return -ENXIO;
> > +	}
> > +
> > +	ret = lpg_init_channels(lpg);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	ret = lpg_init_triled(lpg);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	ret = lpg_init_lut(lpg);
> > +	if (ret < 0)
> > +		return ret;
> 
> 
> How about turning these returns into dev_err_probe?  I'm not sure if that's
> the expected way to go nowadays, but having some form of logging when a
> driver fails to probe is always good to have.
> 

The intention is that each code path through these functions will either
pass or spit out an error in the log. I looked through them again and
think I cover all paths...

Thanks,
Bjorn
Bjorn Andersson April 29, 2021, 12:12 a.m. UTC | #3
On Thu 29 Oct 13:13 CDT 2020, Pavel Machek wrote:

> Hi!
> 
> > The Light Pulse Generator (LPG) is a PWM-block found in a wide range of
> > PMICs from Qualcomm. It can operate on fixed parameters or based on a
> > lookup-table, altering the duty cycle over time - which provides the
> > means for e.g. hardware assisted transitions of LED brightness.
> > 
> > Signed-off-by: Bjorn Andersson <bjorn.andersson@linaro.org>
> > ---
> > 
> > Changes since v5:
> > - Make sure to not used the state of the last channel in a group to determine
> >   if the current sink should be active for all channels in the group.
> > - Replacement of unsigned -1 with UINT_MAX
> > - Work around potential overflow by using larger data types, instead of separate code paths
> > - Use cpu_to_l16() rather than hand rolling them
> > - Minor style cleanups
> > 
> >  drivers/leds/Kconfig         |    9 +
> >  drivers/leds/Makefile        |    1 +
> >  drivers/leds/leds-qcom-lpg.c | 1190 ++++++++++++++++++++++++++++++++++
> >  3 files changed, 1200 insertions(+)
> >  create mode 100644 drivers/leds/leds-qcom-lpg.c
> 
> Let's put this into drivers/leds/rgb/. You may need to create it.
> 

Will do so.

> 
> > +static int lpg_lut_store(struct lpg *lpg, struct led_pattern *pattern,
> > +			 size_t len, unsigned int *lo_idx, unsigned int *hi_idx)
> > +{
> > +	unsigned int idx;
> > +	__le16 val;
> 
> No need for __XX variants outside of headers meant for userspace.
> 

__le16 is the in-kernel return type for cpu_to_le16(), but after further
review I believe I don't need to do this.

> > +#define LPG_ENABLE_GLITCH_REMOVAL	BIT(5)
> > +
> > +static void lpg_enable_glitch(struct lpg_channel *chan)
> > +{
> > +	struct lpg *lpg = chan->lpg;
> > +
> > +	regmap_update_bits(lpg->map, chan->base + PWM_TYPE_CONFIG_REG,
> > +			   LPG_ENABLE_GLITCH_REMOVAL, 0);
> > +}
> > +
> > +static void lpg_disable_glitch(struct lpg_channel *chan)
> > +{
> > +	struct lpg *lpg = chan->lpg;
> > +
> > +	regmap_update_bits(lpg->map, chan->base + PWM_TYPE_CONFIG_REG,
> > +			   LPG_ENABLE_GLITCH_REMOVAL,
> > +			   LPG_ENABLE_GLITCH_REMOVAL);
> > +}
> 
> Helper functions for single register write is kind of overkill...
> 

Yes, it is, but it keep lpg_apply() tidy.

> > +static int lpg_blink_set(struct lpg_led *led,
> > +			 unsigned long delay_on, unsigned long delay_off)
> > +{
> > +	struct lpg_channel *chan;
> > +	unsigned int period_us;
> > +	unsigned int duty_us;
> > +	int i;
> > +
> > +	if (!delay_on && !delay_off) {
> > +		delay_on = 500;
> > +		delay_off = 500;
> > +	}
> 
> Aren't you supposed to modify the values passed to you, so that
> userspace knows what the default rate is?
> 

I had missed this.

> 
> > +	ret = lpg_lut_store(lpg, pattern, len, &lo_idx, &hi_idx);
> > +	if (ret < 0)
> > +		goto out;
> 
> Just do direct return.
> 

Will do.

> > +out:
> > +	return ret;
> > +}
> 
> > +static const struct pwm_ops lpg_pwm_ops = {
> > +	.request = lpg_pwm_request,
> > +	.apply = lpg_pwm_apply,
> > +	.owner = THIS_MODULE,
> > +};
> > +
> > +static int lpg_add_pwm(struct lpg *lpg)
> > +{
> > +	int ret;
> > +
> > +	lpg->pwm.base = -1;
> > +	lpg->pwm.dev = lpg->dev;
> > +	lpg->pwm.npwm = lpg->num_channels;
> > +	lpg->pwm.ops = &lpg_pwm_ops;
> > +
> > +	ret = pwmchip_add(&lpg->pwm);
> > +	if (ret)
> > +		dev_err(lpg->dev, "failed to add PWM chip: ret %d\n", ret);
> > +
> > +	return ret;
> > +}
> 
> Do we need to do this? I'd rather have LED driver, than LED+PWM
> driver...
> 

Yes, I believe we need to do this.

Because each piece of hardware has N channels, which can be wired to
LEDs, grouped with other channels and wired to multicolor LEDs or be
used as PWM signals. And this configuration is board specific.

One such example is the laptop in front of me, which has 3 channels
wired to an RGB LED and 1 channel wired as a backlight control signal
(i.e. using pwm-backlight).  Another example is a devboard where the
4 channels are wired to 4 LEDs.

Regards,
Bjorn
Marijn Suijten April 29, 2021, 7:31 p.m. UTC | #4
On 4/29/21 12:39 AM, Bjorn Andersson wrote:
> On Sun 18 Apr 16:54 CDT 2021, Marijn Suijten wrote:
> 
>> Hi Bjorn,
>>
>> On 10/21/20 10:12 PM, Bjorn Andersson wrote:
>>> The Light Pulse Generator (LPG) is a PWM-block found in a wide range of
>>> PMICs from Qualcomm. It can operate on fixed parameters or based on a
>>> lookup-table, altering the duty cycle over time - which provides the
>>> means for e.g. hardware assisted transitions of LED brightness.
>>>
>>> Signed-off-by: Bjorn Andersson <bjorn.andersson@linaro.org>
>>> Tested-by: Luca Weiss <luca@z3ntu.xyz>
>>
>>
>> Thanks for these patches.  I have tested them successfully on the Sony
>> Xperia XA2 (Discovery, Nile platform) with the leds on the PM660l - feel
>> free to add my Tested-by.  Should I send the configuration your way for
>> inclusion in this patch, or submit them separately (either applied after, or
>> included as separate patches in the next version of this series)?
>>
> 
> Thanks for testing, let's try to land this first iteration first and
> then we can add PM660l and PM8150* definitions/support on top.


I'll keep an eye out for when these patches land and send them on top. 
Feel free to add me to the CC list for future revisions.

>>> +/**
>>> + * struct lpg_data - initialization data
>>> + * @lut_base:		base address of LUT block
>>> + * @lut_size:		number of entries in LUT
>>> + * @triled_base:	base address of TRILED
>>> + * @pwm_9bit_mask:	bitmask for switching from 6bit to 9bit pwm
>>
>>
>> Our downstream kernel derives this from the "LPG subtype" for each distinct
>> channel, read from register offset +0x5.  A value of 0xb is subtype "PWM"
>> with a shift of 2, a value of 0x11 is subtype "LPG_LITE" with a shift of 4.
>> Can we do the same here instead of hardcoding it for all channels in the LPG
>> at once?  How should we determine if the mask is one or two bits wide, for
>> the 3<<4 case?
>>
> 
> I don't see any obvious solution to the latter, so perhaps we should
> just stick with defining this per compatible? Or am I reading your
> suggestion wrong?


Assuming these devices have a different "LPG subtype" you should be able 
to read their value and add it to the list as third option. 
Alternatively, can you point out the driver this `3<<4` mask was based 
on?  With all the information available it should be possible to derive 
this from hardware for every channel instead of hardcoding it.

>>> +
>>> +	bitmap_clear(lpg->lut_bitmap, 0, lpg->lut_size);
>>> +	return lpg->lut_bitmap ? 0 : -ENOMEM;
>>> +}
>>> +
>>> +static int lpg_probe(struct platform_device *pdev)
>>> +{
>>> +	struct device_node *np;
>>> +	struct lpg *lpg;
>>> +	int ret;
>>> +	int i;
>>> +
>>> +	lpg = devm_kzalloc(&pdev->dev, sizeof(*lpg), GFP_KERNEL);
>>> +	if (!lpg)
>>> +		return -ENOMEM;
>>> +
>>> +	lpg->data = of_device_get_match_data(&pdev->dev);
>>> +	if (!lpg->data)
>>> +		return -EINVAL;
>>> +
>>> +	lpg->dev = &pdev->dev;
>>> +
>>> +	lpg->map = dev_get_regmap(pdev->dev.parent, NULL);
>>> +	if (!lpg->map) {
>>> +		dev_err(&pdev->dev, "parent regmap unavailable\n");
>>> +		return -ENXIO;
>>> +	}
>>> +
>>> +	ret = lpg_init_channels(lpg);
>>> +	if (ret < 0)
>>> +		return ret;
>>> +
>>> +	ret = lpg_init_triled(lpg);
>>> +	if (ret < 0)
>>> +		return ret;
>>> +
>>> +	ret = lpg_init_lut(lpg);
>>> +	if (ret < 0)
>>> +		return ret;
>>
>>
>> How about turning these returns into dev_err_probe?  I'm not sure if that's
>> the expected way to go nowadays, but having some form of logging when a
>> driver fails to probe is always good to have.
>>
> 
> The intention is that each code path through these functions will either
> pass or spit out an error in the log. I looked through them again and
> think I cover all paths...


That is true, all the errors not covered are extremely unlikely like 
-ENOMEM.  I vaguely recall having to insert extra logging to get through 
initial probe, but that might have been something inside lpg_add_led as 
well.  Fine to leave this as it is.

- Marijn
Bjorn Andersson April 29, 2021, 8:54 p.m. UTC | #5
On Thu 29 Apr 14:31 CDT 2021, Marijn Suijten wrote:

> On 4/29/21 12:39 AM, Bjorn Andersson wrote:

> > On Sun 18 Apr 16:54 CDT 2021, Marijn Suijten wrote:

[..]
> > > > +	ret = lpg_init_lut(lpg);

> > > > +	if (ret < 0)

> > > > +		return ret;

> > > 

> > > 

> > > How about turning these returns into dev_err_probe?  I'm not sure if that's

> > > the expected way to go nowadays, but having some form of logging when a

> > > driver fails to probe is always good to have.

> > > 

> > 

> > The intention is that each code path through these functions will either

> > pass or spit out an error in the log. I looked through them again and

> > think I cover all paths...

> 

> 

> That is true, all the errors not covered are extremely unlikely like

> -ENOMEM.  I vaguely recall having to insert extra logging to get through

> initial probe, but that might have been something inside lpg_add_led as

> well.  Fine to leave this as it is.

> 


When kzalloc et al returns -ENOMEM it will be done with an error print,
so that does not need an additional print. That said, another pass
through lpg_add_led() made me spot that if you get a parse error on
the "color" property we would silently return -EINVAL. I've corrected
this.

Thanks,
Bjorn
Pavel Machek April 29, 2021, 9:12 p.m. UTC | #6
Hi!

> > > +static int lpg_add_pwm(struct lpg *lpg)
> > > +{
> > > +	int ret;
> > > +
> > > +	lpg->pwm.base = -1;
> > > +	lpg->pwm.dev = lpg->dev;
> > > +	lpg->pwm.npwm = lpg->num_channels;
> > > +	lpg->pwm.ops = &lpg_pwm_ops;
> > > +
> > > +	ret = pwmchip_add(&lpg->pwm);
> > > +	if (ret)
> > > +		dev_err(lpg->dev, "failed to add PWM chip: ret %d\n", ret);
> > > +
> > > +	return ret;
> > > +}
> > 
> > Do we need to do this? I'd rather have LED driver, than LED+PWM
> > driver...
> > 
> 
> Yes, I believe we need to do this.
> 
> Because each piece of hardware has N channels, which can be wired to
> LEDs, grouped with other channels and wired to multicolor LEDs or be
> used as PWM signals. And this configuration is board specific.
> 
> One such example is the laptop in front of me, which has 3 channels
> wired to an RGB LED and 1 channel wired as a backlight control signal
> (i.e. using pwm-backlight).  Another example is a devboard where the
> 4 channels are wired to 4 LEDs.

Ok, so this is actually important. In this case you should have PWM
layer, exporting PWMs, and then rgb-LED driver that takes three of
those PWMs and turns them into LED, no?

And ... surprise ... that is likely to help other people, as LEDs
connected to PWMs are quite common.

Hmm.?

If you can't do this for some reason, you should probably explain in
the changelog, because this is going to be FAQ.

Best regards,
								Pavel
Bjorn Andersson April 29, 2021, 9:29 p.m. UTC | #7
On Thu 29 Apr 16:12 CDT 2021, Pavel Machek wrote:

> Hi!
> 
> > > > +static int lpg_add_pwm(struct lpg *lpg)
> > > > +{
> > > > +	int ret;
> > > > +
> > > > +	lpg->pwm.base = -1;
> > > > +	lpg->pwm.dev = lpg->dev;
> > > > +	lpg->pwm.npwm = lpg->num_channels;
> > > > +	lpg->pwm.ops = &lpg_pwm_ops;
> > > > +
> > > > +	ret = pwmchip_add(&lpg->pwm);
> > > > +	if (ret)
> > > > +		dev_err(lpg->dev, "failed to add PWM chip: ret %d\n", ret);
> > > > +
> > > > +	return ret;
> > > > +}
> > > 
> > > Do we need to do this? I'd rather have LED driver, than LED+PWM
> > > driver...
> > > 
> > 
> > Yes, I believe we need to do this.
> > 
> > Because each piece of hardware has N channels, which can be wired to
> > LEDs, grouped with other channels and wired to multicolor LEDs or be
> > used as PWM signals. And this configuration is board specific.
> > 
> > One such example is the laptop in front of me, which has 3 channels
> > wired to an RGB LED and 1 channel wired as a backlight control signal
> > (i.e. using pwm-backlight).  Another example is a devboard where the
> > 4 channels are wired to 4 LEDs.
> 
> Ok, so this is actually important. In this case you should have PWM
> layer, exporting PWMs, and then rgb-LED driver that takes three of
> those PWMs and turns them into LED, no?
> 
> And ... surprise ... that is likely to help other people, as LEDs
> connected to PWMs are quite common.
> 
> Hmm.?
> 
> If you can't do this for some reason, you should probably explain in
> the changelog, because this is going to be FAQ.
> 

This is exactly what the downstream implementation does and in the case
of a solid color LED this works fine.

But the hardware has a shared chunk of memory where you can write
duty-cycle values, then for each PWM channel you can specify the
start/stop index and pace for the PWM to read and update the configured
duty-cycle. This is how the hardware implements pattern support.


So downstream they have (last time I looked at the code) an addition in
the PWM API where the LED driver can inform the PWM driver part about
the indices to use. Naturally I don't think that's a good idea.


Additionally, representing this as individual PWM channels means we're
loosing the grouping that now comes from the description of multicolor
LEDs, which serves the basis for synchronizing the pattern traversal
between the involved channels.


I just posted v7, but I'd be happy to incorporate such reasoning in the
commit message (or do you really mean changelog in the email?) when we
agree on a solution.

Regards,
Bjorn
Pavel Machek May 4, 2021, 3:43 p.m. UTC | #8
Hi!

> > > One such example is the laptop in front of me, which has 3 channels
> > > wired to an RGB LED and 1 channel wired as a backlight control signal
> > > (i.e. using pwm-backlight).  Another example is a devboard where the
> > > 4 channels are wired to 4 LEDs.
> > 
> > Ok, so this is actually important. In this case you should have PWM
> > layer, exporting PWMs, and then rgb-LED driver that takes three of
> > those PWMs and turns them into LED, no?
> > 
> > And ... surprise ... that is likely to help other people, as LEDs
> > connected to PWMs are quite common.
> > 
> > Hmm.?
> > 
> > If you can't do this for some reason, you should probably explain in
> > the changelog, because this is going to be FAQ.
> > 
> 
> This is exactly what the downstream implementation does and in the case
> of a solid color LED this works fine.
> 
> But the hardware has a shared chunk of memory where you can write
> duty-cycle values, then for each PWM channel you can specify the
> start/stop index and pace for the PWM to read and update the configured
> duty-cycle. This is how the hardware implements pattern support.

Ok.

> So downstream they have (last time I looked at the code) an addition in
> the PWM API where the LED driver can inform the PWM driver part about
> the indices to use. Naturally I don't think that's a good idea.

Dunno. Is it bad idea?

pattern support for other PWMs (vibration?) seems useful, too. Yes, it
means more discussion and extending PWMs properly..

> Additionally, representing this as individual PWM channels means we're
> loosing the grouping that now comes from the description of multicolor
> LEDs, which serves the basis for synchronizing the pattern traversal
> between the involved channels.

Yes, keeping grouping would be nice, but perhaps pattern API for PWMs
can do that too?

You can have solid-color-only driver now, with patterns being added
as discussion with PWM people progresses...

Best regards,
									Pavel

--
Bjorn Andersson May 4, 2021, 4:13 p.m. UTC | #9
On Tue 04 May 10:43 CDT 2021, Pavel Machek wrote:

> Hi!

> 

> > > > One such example is the laptop in front of me, which has 3 channels

> > > > wired to an RGB LED and 1 channel wired as a backlight control signal

> > > > (i.e. using pwm-backlight).  Another example is a devboard where the

> > > > 4 channels are wired to 4 LEDs.

> > > 

> > > Ok, so this is actually important. In this case you should have PWM

> > > layer, exporting PWMs, and then rgb-LED driver that takes three of

> > > those PWMs and turns them into LED, no?

> > > 

> > > And ... surprise ... that is likely to help other people, as LEDs

> > > connected to PWMs are quite common.

> > > 

> > > Hmm.?

> > > 

> > > If you can't do this for some reason, you should probably explain in

> > > the changelog, because this is going to be FAQ.

> > > 

> > 

> > This is exactly what the downstream implementation does and in the case

> > of a solid color LED this works fine.

> > 

> > But the hardware has a shared chunk of memory where you can write

> > duty-cycle values, then for each PWM channel you can specify the

> > start/stop index and pace for the PWM to read and update the configured

> > duty-cycle. This is how the hardware implements pattern support.

> 

> Ok.

> 

> > So downstream they have (last time I looked at the code) an addition in

> > the PWM API where the LED driver can inform the PWM driver part about

> > the indices to use. Naturally I don't think that's a good idea.

> 

> Dunno. Is it bad idea?

> 

> pattern support for other PWMs (vibration?) seems useful, too. Yes, it

> means more discussion and extending PWMs properly..

> 


@Thierry, @Lee, @Uwe, are you interested in extending the PWM api with
some sort of support for specifying an array of "duty_cycle" and some
property for how fast the hardware should cycle through those duty
cycles? (And I need a bit/bool to indicate if the pattern should run
once of be repeated)

The (current) use case relates to being able to alter the duty cycle
over time to create "effects" such as pulsing an LED.

> > Additionally, representing this as individual PWM channels means we're

> > loosing the grouping that now comes from the description of multicolor

> > LEDs, which serves the basis for synchronizing the pattern traversal

> > between the involved channels.

> 

> Yes, keeping grouping would be nice, but perhaps pattern API for PWMs

> can do that too?

> 


I don't think it's viable to push this concept down to the PWM core, the
LED framework needs to know which channels are grouped as LEDs (and
which aren't) and the thing that drives the hardware needs to know which
channels to sync with when starting the pattern. So afaict we'd have to
describe these properties in two places - or ignore some properties of
the problem.

> You can have solid-color-only driver now, with patterns being added

> as discussion with PWM people progresses...

> 


I don't see how that would work, the overall design of the driver and
the devicetree binding depends heavily on this fundamental decision.

Regards,
Bjorn
Uwe Kleine-König May 5, 2021, 5:15 a.m. UTC | #10
Hello Bjorn,

On Wed, Oct 21, 2020 at 01:12:22PM -0700, Bjorn Andersson wrote:
> +static const unsigned int lpg_clk_table[NUM_PWM_PREDIV][NUM_PWM_CLK] = {

> +	{

> +		1 * (NSEC_PER_SEC / 1024),

> +		1 * (NSEC_PER_SEC / 32768),

> +		1 * (NSEC_PER_SEC / 19200000),

> +	},

> +	{

> +		3 * (NSEC_PER_SEC / 1024),

> +		3 * (NSEC_PER_SEC / 32768),


1000000000 / 32768 is 30517.578125. Because of the parenthesis this is
truncated to 30517. Multiplied by 3 this results in 91551. The exact
result is 91552.734375 however.

> +		3 * (NSEC_PER_SEC / 19200000),

> +	},

> +	{

> +		5 * (NSEC_PER_SEC / 1024),

> +		5 * (NSEC_PER_SEC / 32768),

> +		5 * (NSEC_PER_SEC / 19200000),

> +	},

> +	{

> +		6 * (NSEC_PER_SEC / 1024),

> +		6 * (NSEC_PER_SEC / 32768),

> +		6 * (NSEC_PER_SEC / 19200000),

> +	},

> +};

> +

> +/*

> + * PWM Frequency = Clock Frequency / (N * T)

> + *      or

> + * PWM Period = Clock Period * (N * T)

> + *      where

> + * N = 2^9 or 2^6 for 9-bit or 6-bit PWM size

> + * T = Pre-divide * 2^m, where m = 0..7 (exponent)

> + *

> + * This is the formula to figure out m for the best pre-divide and clock:

> + * (PWM Period / N) = (Pre-divide * Clock Period) * 2^m

> + */

> +static void lpg_calc_freq(struct lpg_channel *chan, unsigned int period_us)

> +{

> +	int             n, m, clk, div;

> +	int             best_m, best_div, best_clk;

> +	unsigned int    last_err, cur_err, min_err;

> +	unsigned int    tmp_p, period_n;

> +

> +	if (period_us == chan->period_us)

> +		return;

> +

> +	/* PWM Period / N */

> +	if (period_us < UINT_MAX / NSEC_PER_USEC)

> +		n = 6;

> +	else

> +		n = 9;

> +

> +	period_n = ((u64)period_us * NSEC_PER_USEC) >> n;

> +

> +	min_err = UINT_MAX;

> +	last_err = UINT_MAX;

> +	best_m = 0;

> +	best_clk = 0;

> +	best_div = 0;

> +	for (clk = 0; clk < NUM_PWM_CLK; clk++) {

> +		for (div = 0; div < NUM_PWM_PREDIV; div++) {

> +			/* period_n = (PWM Period / N) */

> +			/* tmp_p = (Pre-divide * Clock Period) * 2^m */

> +			tmp_p = lpg_clk_table[div][clk];

> +			for (m = 0; m <= NUM_EXP; m++) {

> +				cur_err = abs(period_n - tmp_p);

> +				if (cur_err < min_err) {

> +					min_err = cur_err;

> +					best_m = m;

> +					best_clk = clk;

> +					best_div = div;

> +				}

> +

> +				if (m && cur_err > last_err)

> +					/* Break for bigger cur_err */

> +					break;

> +

> +				last_err = cur_err;

> +				tmp_p <<= 1;


This is inexact. Consider again the case where tmp_p is
3 * (NSEC_PER_SEC / 32768). The values you use and the exact values are:

	m     |       0        |      1       |      2      |      3     | ... |        7 |
	tmp_p |   91551        | 183102       | 366204      | 732408     |     | 11718528 |
        actual|   91552.734375 | 183105.46875 | 366210.9375 | 732421.875 | ... | 11718750 |

So while you save some cycles by precalculating the values in
lpg_clk_table, you trade that for lost precision.

> +			}

> +		}

> +	}


Please don't pick a period that is longer than the requested period (for
the PWM functionality that is).

This can be simplified, you can at least calculate the optimal m
directly.

> +	/* Use higher resolution */

> +	if (best_m >= 3 && n == 6) {

> +		n += 3;

> +		best_m -= 3;

> +	}

> +

> +	chan->clk = best_clk;

> +	chan->pre_div = best_div;

> +	chan->pre_div_exp = best_m;

> +	chan->pwm_size = n;

> +

> +	chan->period_us = period_us;

> +}

> +

> +static void lpg_calc_duty(struct lpg_channel *chan, unsigned int duty_us)

> +{

> +	unsigned int max = (1 << chan->pwm_size) - 1;

> +	unsigned int val = div_u64((u64)duty_us << chan->pwm_size, chan->period_us);


Please use the actually implemented period here instead of the
requested. This improves precision, see commit
8035e6c66a5e98f098edf7441667de74affb4e78 for a similar case.

> +

> +	chan->pwm_value = min(val, max);

> +}

> +

> [...]

> +static const struct pwm_ops lpg_pwm_ops = {

> +	.request = lpg_pwm_request,

> +	.apply = lpg_pwm_apply,


Can you please test your driver with PWM_DEBUG enabled? The first thing
this will critizise is that there is no .get_state callback.

> +	.owner = THIS_MODULE,

> +};

> +

> +static int lpg_add_pwm(struct lpg *lpg)

> +{

> +	int ret;

> +

> +	lpg->pwm.base = -1;


Please drop this assignment.

> +	lpg->pwm.dev = lpg->dev;

> +	lpg->pwm.npwm = lpg->num_channels;

> +	lpg->pwm.ops = &lpg_pwm_ops;

> +

> +	ret = pwmchip_add(&lpg->pwm);

> +	if (ret)

> +		dev_err(lpg->dev, "failed to add PWM chip: ret %d\n", ret);

> +

> +	return ret;

> +}


Best regards
Uwe

-- 
Pengutronix e.K.                           | Uwe Kleine-König            |
Industrial Linux Solutions                 | https://www.pengutronix.de/ |
Uwe Kleine-König May 5, 2021, 5:19 a.m. UTC | #11
Hello again,

another feedback that only came to me after hitting "Send": It might be
sensible to split the patch. In the first part add the LED stuff and in
the second the PWM stuff.

It would also be good if the PWM code could live in drivers/pwm.

Best regards
Uwe