leds: rgb: leds-qcom-lpg: Add support for high resolution PWM
Certain PMICs like PMK8550 have a high resolution PWM module which can support from 8-bit to 15-bit PWM. Add support for it. Signed-off-by: Anjelique Melendez <quic_amelende@quicinc.com> Acked-by: Pavel Machek <pavel@ucw.cz> Signed-off-by: Lee Jones <lee@kernel.org> Link: https://lore.kernel.org/r/20230407223849.17623-3-quic_amelende@quicinc.com
This commit is contained in:
parent
03a85ab3ac
commit
b00d2ed376
1 changed files with 102 additions and 41 deletions
|
@ -2,6 +2,7 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2017-2022 Linaro Ltd
|
* Copyright (c) 2017-2022 Linaro Ltd
|
||||||
* Copyright (c) 2010-2012, The Linux Foundation. All rights reserved.
|
* Copyright (c) 2010-2012, The Linux Foundation. All rights reserved.
|
||||||
|
* Copyright (c) 2023, Qualcomm Innovation Center, Inc. All rights reserved.
|
||||||
*/
|
*/
|
||||||
#include <linux/bits.h>
|
#include <linux/bits.h>
|
||||||
#include <linux/bitfield.h>
|
#include <linux/bitfield.h>
|
||||||
|
@ -17,10 +18,13 @@
|
||||||
#define LPG_SUBTYPE_REG 0x05
|
#define LPG_SUBTYPE_REG 0x05
|
||||||
#define LPG_SUBTYPE_LPG 0x2
|
#define LPG_SUBTYPE_LPG 0x2
|
||||||
#define LPG_SUBTYPE_PWM 0xb
|
#define LPG_SUBTYPE_PWM 0xb
|
||||||
|
#define LPG_SUBTYPE_HI_RES_PWM 0xc
|
||||||
#define LPG_SUBTYPE_LPG_LITE 0x11
|
#define LPG_SUBTYPE_LPG_LITE 0x11
|
||||||
#define LPG_PATTERN_CONFIG_REG 0x40
|
#define LPG_PATTERN_CONFIG_REG 0x40
|
||||||
#define LPG_SIZE_CLK_REG 0x41
|
#define LPG_SIZE_CLK_REG 0x41
|
||||||
#define PWM_CLK_SELECT_MASK GENMASK(1, 0)
|
#define PWM_CLK_SELECT_MASK GENMASK(1, 0)
|
||||||
|
#define PWM_CLK_SELECT_HI_RES_MASK GENMASK(2, 0)
|
||||||
|
#define PWM_SIZE_HI_RES_MASK GENMASK(6, 4)
|
||||||
#define LPG_PREDIV_CLK_REG 0x42
|
#define LPG_PREDIV_CLK_REG 0x42
|
||||||
#define PWM_FREQ_PRE_DIV_MASK GENMASK(6, 5)
|
#define PWM_FREQ_PRE_DIV_MASK GENMASK(6, 5)
|
||||||
#define PWM_FREQ_EXP_MASK GENMASK(2, 0)
|
#define PWM_FREQ_EXP_MASK GENMASK(2, 0)
|
||||||
|
@ -43,8 +47,10 @@
|
||||||
#define LPG_LUT_REG(x) (0x40 + (x) * 2)
|
#define LPG_LUT_REG(x) (0x40 + (x) * 2)
|
||||||
#define RAMP_CONTROL_REG 0xc8
|
#define RAMP_CONTROL_REG 0xc8
|
||||||
|
|
||||||
#define LPG_RESOLUTION 512
|
#define LPG_RESOLUTION_9BIT BIT(9)
|
||||||
|
#define LPG_RESOLUTION_15BIT BIT(15)
|
||||||
#define LPG_MAX_M 7
|
#define LPG_MAX_M 7
|
||||||
|
#define LPG_MAX_PREDIV 6
|
||||||
|
|
||||||
struct lpg_channel;
|
struct lpg_channel;
|
||||||
struct lpg_data;
|
struct lpg_data;
|
||||||
|
@ -106,6 +112,7 @@ struct lpg {
|
||||||
* @clk_sel: reference clock frequency selector
|
* @clk_sel: reference clock frequency selector
|
||||||
* @pre_div_sel: divider selector of the reference clock
|
* @pre_div_sel: divider selector of the reference clock
|
||||||
* @pre_div_exp: exponential divider of the reference clock
|
* @pre_div_exp: exponential divider of the reference clock
|
||||||
|
* @pwm_resolution_sel: pwm resolution selector
|
||||||
* @ramp_enabled: duty cycle is driven by iterating over lookup table
|
* @ramp_enabled: duty cycle is driven by iterating over lookup table
|
||||||
* @ramp_ping_pong: reverse through pattern, rather than wrapping to start
|
* @ramp_ping_pong: reverse through pattern, rather than wrapping to start
|
||||||
* @ramp_oneshot: perform only a single pass over the pattern
|
* @ramp_oneshot: perform only a single pass over the pattern
|
||||||
|
@ -138,6 +145,7 @@ struct lpg_channel {
|
||||||
unsigned int clk_sel;
|
unsigned int clk_sel;
|
||||||
unsigned int pre_div_sel;
|
unsigned int pre_div_sel;
|
||||||
unsigned int pre_div_exp;
|
unsigned int pre_div_exp;
|
||||||
|
unsigned int pwm_resolution_sel;
|
||||||
|
|
||||||
bool ramp_enabled;
|
bool ramp_enabled;
|
||||||
bool ramp_ping_pong;
|
bool ramp_ping_pong;
|
||||||
|
@ -253,17 +261,24 @@ 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[] = {0, 1024, 32768, 19200000};
|
||||||
|
static const unsigned int lpg_clk_rates_hi_res[] = {0, 1024, 32768, 19200000, 76800000};
|
||||||
static const unsigned int lpg_pre_divs[] = {1, 3, 5, 6};
|
static const unsigned int lpg_pre_divs[] = {1, 3, 5, 6};
|
||||||
|
static const unsigned int lpg_pwm_resolution[] = {9};
|
||||||
|
static const unsigned int lpg_pwm_resolution_hi_res[] = {8, 9, 10, 11, 12, 13, 14, 15};
|
||||||
|
|
||||||
static int lpg_calc_freq(struct lpg_channel *chan, uint64_t period)
|
static int lpg_calc_freq(struct lpg_channel *chan, uint64_t period)
|
||||||
{
|
{
|
||||||
unsigned int clk_sel, best_clk = 0;
|
unsigned int i, pwm_resolution_count, best_pwm_resolution_sel = 0;
|
||||||
|
const unsigned int *clk_rate_arr, *pwm_resolution_arr;
|
||||||
|
unsigned int clk_sel, clk_len, best_clk = 0;
|
||||||
unsigned int div, best_div = 0;
|
unsigned int div, best_div = 0;
|
||||||
unsigned int m, best_m = 0;
|
unsigned int m, best_m = 0;
|
||||||
|
unsigned int resolution;
|
||||||
unsigned int error;
|
unsigned int error;
|
||||||
unsigned int best_err = UINT_MAX;
|
unsigned int best_err = UINT_MAX;
|
||||||
|
u64 max_period, min_period;
|
||||||
u64 best_period = 0;
|
u64 best_period = 0;
|
||||||
u64 max_period;
|
u64 max_res;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The PWM period is determined by:
|
* The PWM period is determined by:
|
||||||
|
@ -272,34 +287,59 @@ static int lpg_calc_freq(struct lpg_channel *chan, uint64_t period)
|
||||||
* period = --------------------------
|
* period = --------------------------
|
||||||
* refclk
|
* refclk
|
||||||
*
|
*
|
||||||
* With resolution fixed at 2^9 bits, pre_div = {1, 3, 5, 6} and
|
* Resolution = 2^9 bits for PWM or
|
||||||
|
* 2^{8, 9, 10, 11, 12, 13, 14, 15} bits for high resolution PWM
|
||||||
|
* pre_div = {1, 3, 5, 6} and
|
||||||
* M = [0..7].
|
* M = [0..7].
|
||||||
*
|
*
|
||||||
* This allows for periods between 27uS and 384s, as the PWM framework
|
* This allows for periods between 27uS and 384s for PWM channels and periods between
|
||||||
* wants a period of equal or lower length than requested, reject
|
* 3uS and 24576s for high resolution PWMs.
|
||||||
* anything below 27uS.
|
* The PWM framework wants a period of equal or lower length than requested,
|
||||||
|
* reject anything below minimum period.
|
||||||
*/
|
*/
|
||||||
if (period <= (u64)NSEC_PER_SEC * LPG_RESOLUTION / 19200000)
|
|
||||||
|
if (chan->subtype == LPG_SUBTYPE_HI_RES_PWM) {
|
||||||
|
clk_rate_arr = lpg_clk_rates_hi_res;
|
||||||
|
clk_len = ARRAY_SIZE(lpg_clk_rates_hi_res);
|
||||||
|
pwm_resolution_arr = lpg_pwm_resolution_hi_res;
|
||||||
|
pwm_resolution_count = ARRAY_SIZE(lpg_pwm_resolution_hi_res);
|
||||||
|
max_res = LPG_RESOLUTION_15BIT;
|
||||||
|
} else {
|
||||||
|
clk_rate_arr = lpg_clk_rates;
|
||||||
|
clk_len = ARRAY_SIZE(lpg_clk_rates);
|
||||||
|
pwm_resolution_arr = lpg_pwm_resolution;
|
||||||
|
pwm_resolution_count = ARRAY_SIZE(lpg_pwm_resolution);
|
||||||
|
max_res = LPG_RESOLUTION_9BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
min_period = (u64)NSEC_PER_SEC *
|
||||||
|
div64_u64((1 << pwm_resolution_arr[0]), clk_rate_arr[clk_len - 1]);
|
||||||
|
if (period <= min_period)
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
|
|
||||||
/* Limit period to largest possible value, to avoid overflows */
|
/* Limit period to largest possible value, to avoid overflows */
|
||||||
max_period = (u64)NSEC_PER_SEC * LPG_RESOLUTION * 6 * (1 << LPG_MAX_M) / 1024;
|
max_period = (u64)NSEC_PER_SEC * max_res * LPG_MAX_PREDIV *
|
||||||
|
div64_u64((1 << LPG_MAX_M), 1024);
|
||||||
if (period > max_period)
|
if (period > max_period)
|
||||||
period = max_period;
|
period = max_period;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Search for the pre_div, refclk and M by solving the rewritten formula
|
* Search for the pre_div, refclk, resolution and M by solving the rewritten formula
|
||||||
* for each refclk and pre_div value:
|
* for each refclk, resolution and pre_div value:
|
||||||
*
|
*
|
||||||
* period * refclk
|
* period * refclk
|
||||||
* M = log2 -------------------------------------
|
* M = log2 -------------------------------------
|
||||||
* NSEC_PER_SEC * pre_div * resolution
|
* NSEC_PER_SEC * pre_div * resolution
|
||||||
*/
|
*/
|
||||||
for (clk_sel = 1; clk_sel < ARRAY_SIZE(lpg_clk_rates); clk_sel++) {
|
|
||||||
u64 numerator = period * lpg_clk_rates[clk_sel];
|
for (i = 0; i < pwm_resolution_count; i++) {
|
||||||
|
resolution = 1 << pwm_resolution_arr[i];
|
||||||
|
for (clk_sel = 1; clk_sel < clk_len; clk_sel++) {
|
||||||
|
u64 numerator = period * clk_rate_arr[clk_sel];
|
||||||
|
|
||||||
for (div = 0; div < ARRAY_SIZE(lpg_pre_divs); div++) {
|
for (div = 0; div < ARRAY_SIZE(lpg_pre_divs); div++) {
|
||||||
u64 denominator = (u64)NSEC_PER_SEC * lpg_pre_divs[div] * LPG_RESOLUTION;
|
u64 denominator = (u64)NSEC_PER_SEC * lpg_pre_divs[div] *
|
||||||
|
resolution;
|
||||||
u64 actual;
|
u64 actual;
|
||||||
u64 ratio;
|
u64 ratio;
|
||||||
|
|
||||||
|
@ -311,34 +351,43 @@ static int lpg_calc_freq(struct lpg_channel *chan, uint64_t period)
|
||||||
if (m > LPG_MAX_M)
|
if (m > LPG_MAX_M)
|
||||||
m = LPG_MAX_M;
|
m = LPG_MAX_M;
|
||||||
|
|
||||||
actual = DIV_ROUND_UP_ULL(denominator * (1 << m), lpg_clk_rates[clk_sel]);
|
actual = DIV_ROUND_UP_ULL(denominator * (1 << m),
|
||||||
|
clk_rate_arr[clk_sel]);
|
||||||
error = period - actual;
|
error = period - actual;
|
||||||
if (error < best_err) {
|
if (error < best_err) {
|
||||||
best_err = error;
|
best_err = error;
|
||||||
|
|
||||||
best_div = div;
|
best_div = div;
|
||||||
best_m = m;
|
best_m = m;
|
||||||
best_clk = clk_sel;
|
best_clk = clk_sel;
|
||||||
best_period = actual;
|
best_period = actual;
|
||||||
|
best_pwm_resolution_sel = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chan->clk_sel = best_clk;
|
chan->clk_sel = best_clk;
|
||||||
chan->pre_div_sel = best_div;
|
chan->pre_div_sel = best_div;
|
||||||
chan->pre_div_exp = best_m;
|
chan->pre_div_exp = best_m;
|
||||||
chan->period = best_period;
|
chan->period = best_period;
|
||||||
|
chan->pwm_resolution_sel = best_pwm_resolution_sel;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void lpg_calc_duty(struct lpg_channel *chan, uint64_t duty)
|
static void lpg_calc_duty(struct lpg_channel *chan, uint64_t duty)
|
||||||
{
|
{
|
||||||
unsigned int max = LPG_RESOLUTION - 1;
|
unsigned int max;
|
||||||
unsigned int val;
|
unsigned int val;
|
||||||
|
unsigned int clk_rate;
|
||||||
|
|
||||||
val = div64_u64(duty * lpg_clk_rates[chan->clk_sel],
|
if (chan->subtype == LPG_SUBTYPE_HI_RES_PWM) {
|
||||||
|
max = LPG_RESOLUTION_15BIT - 1;
|
||||||
|
clk_rate = lpg_clk_rates_hi_res[chan->clk_sel];
|
||||||
|
} else {
|
||||||
|
max = LPG_RESOLUTION_9BIT - 1;
|
||||||
|
clk_rate = lpg_clk_rates[chan->clk_sel];
|
||||||
|
}
|
||||||
|
|
||||||
|
val = div64_u64(duty * clk_rate,
|
||||||
(u64)NSEC_PER_SEC * lpg_pre_divs[chan->pre_div_sel] * (1 << chan->pre_div_exp));
|
(u64)NSEC_PER_SEC * lpg_pre_divs[chan->pre_div_sel] * (1 << chan->pre_div_exp));
|
||||||
|
|
||||||
chan->pwm_value = min(val, max);
|
chan->pwm_value = min(val, max);
|
||||||
|
@ -354,7 +403,7 @@ static void lpg_apply_freq(struct lpg_channel *chan)
|
||||||
|
|
||||||
val = chan->clk_sel;
|
val = chan->clk_sel;
|
||||||
|
|
||||||
/* Specify 9bit resolution, based on the subtype of the channel */
|
/* Specify resolution, based on the subtype of the channel */
|
||||||
switch (chan->subtype) {
|
switch (chan->subtype) {
|
||||||
case LPG_SUBTYPE_LPG:
|
case LPG_SUBTYPE_LPG:
|
||||||
val |= GENMASK(5, 4);
|
val |= GENMASK(5, 4);
|
||||||
|
@ -362,6 +411,9 @@ static void lpg_apply_freq(struct lpg_channel *chan)
|
||||||
case LPG_SUBTYPE_PWM:
|
case LPG_SUBTYPE_PWM:
|
||||||
val |= BIT(2);
|
val |= BIT(2);
|
||||||
break;
|
break;
|
||||||
|
case LPG_SUBTYPE_HI_RES_PWM:
|
||||||
|
val |= FIELD_PREP(PWM_SIZE_HI_RES_MASK, chan->pwm_resolution_sel);
|
||||||
|
break;
|
||||||
case LPG_SUBTYPE_LPG_LITE:
|
case LPG_SUBTYPE_LPG_LITE:
|
||||||
default:
|
default:
|
||||||
val |= BIT(4);
|
val |= BIT(4);
|
||||||
|
@ -670,7 +722,7 @@ static int lpg_blink_set(struct lpg_led *led,
|
||||||
triled_set(lpg, triled_mask, triled_mask);
|
triled_set(lpg, triled_mask, triled_mask);
|
||||||
|
|
||||||
chan = led->channels[0];
|
chan = led->channels[0];
|
||||||
duty = div_u64(chan->pwm_value * chan->period, LPG_RESOLUTION);
|
duty = div_u64(chan->pwm_value * chan->period, LPG_RESOLUTION_9BIT);
|
||||||
*delay_on = div_u64(duty, NSEC_PER_MSEC);
|
*delay_on = div_u64(duty, NSEC_PER_MSEC);
|
||||||
*delay_off = div_u64(chan->period - duty, NSEC_PER_MSEC);
|
*delay_off = div_u64(chan->period - duty, NSEC_PER_MSEC);
|
||||||
|
|
||||||
|
@ -977,6 +1029,7 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
{
|
{
|
||||||
struct lpg *lpg = container_of(chip, struct lpg, pwm);
|
struct lpg *lpg = container_of(chip, struct lpg, pwm);
|
||||||
struct lpg_channel *chan = &lpg->channels[pwm->hwpwm];
|
struct lpg_channel *chan = &lpg->channels[pwm->hwpwm];
|
||||||
|
unsigned int resolution;
|
||||||
unsigned int pre_div;
|
unsigned int pre_div;
|
||||||
unsigned int refclk;
|
unsigned int refclk;
|
||||||
unsigned int val;
|
unsigned int val;
|
||||||
|
@ -988,7 +1041,14 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
if (ret)
|
if (ret)
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
refclk = lpg_clk_rates[val & PWM_CLK_SELECT_MASK];
|
if (chan->subtype == LPG_SUBTYPE_HI_RES_PWM) {
|
||||||
|
refclk = lpg_clk_rates_hi_res[FIELD_GET(PWM_CLK_SELECT_HI_RES_MASK, val)];
|
||||||
|
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)];
|
||||||
|
resolution = 9;
|
||||||
|
}
|
||||||
|
|
||||||
if (refclk) {
|
if (refclk) {
|
||||||
ret = regmap_read(lpg->map, chan->base + LPG_PREDIV_CLK_REG, &val);
|
ret = regmap_read(lpg->map, chan->base + LPG_PREDIV_CLK_REG, &val);
|
||||||
if (ret)
|
if (ret)
|
||||||
|
@ -1001,7 +1061,8 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
|
||||||
if (ret)
|
if (ret)
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
state->period = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC * LPG_RESOLUTION * pre_div * (1 << m), refclk);
|
state->period = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC * (1 << resolution) *
|
||||||
|
pre_div * (1 << m), refclk);
|
||||||
state->duty_cycle = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC * pwm_value * pre_div * (1 << m), refclk);
|
state->duty_cycle = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC * pwm_value * pre_div * (1 << m), refclk);
|
||||||
} else {
|
} else {
|
||||||
state->period = 0;
|
state->period = 0;
|
||||||
|
@ -1149,7 +1210,7 @@ static int lpg_add_led(struct lpg *lpg, struct device_node *np)
|
||||||
}
|
}
|
||||||
|
|
||||||
cdev->default_trigger = of_get_property(np, "linux,default-trigger", NULL);
|
cdev->default_trigger = of_get_property(np, "linux,default-trigger", NULL);
|
||||||
cdev->max_brightness = LPG_RESOLUTION - 1;
|
cdev->max_brightness = LPG_RESOLUTION_9BIT - 1;
|
||||||
|
|
||||||
if (!of_property_read_string(np, "default-state", &state) &&
|
if (!of_property_read_string(np, "default-state", &state) &&
|
||||||
!strcmp(state, "on"))
|
!strcmp(state, "on"))
|
||||||
|
|
Loading…
Add table
Reference in a new issue