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:
Anjelique Melendez 2023-04-07 15:38:48 -07:00 committed by Lee Jones
parent 03a85ab3ac
commit b00d2ed376
1 changed files with 102 additions and 41 deletions

View File

@ -2,6 +2,7 @@
/*
* Copyright (c) 2017-2022 Linaro Ltd
* 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/bitfield.h>
@ -17,10 +18,13 @@
#define LPG_SUBTYPE_REG 0x05
#define LPG_SUBTYPE_LPG 0x2
#define LPG_SUBTYPE_PWM 0xb
#define LPG_SUBTYPE_HI_RES_PWM 0xc
#define LPG_SUBTYPE_LPG_LITE 0x11
#define LPG_PATTERN_CONFIG_REG 0x40
#define LPG_SIZE_CLK_REG 0x41
#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 PWM_FREQ_PRE_DIV_MASK GENMASK(6, 5)
#define PWM_FREQ_EXP_MASK GENMASK(2, 0)
@ -43,8 +47,10 @@
#define LPG_LUT_REG(x) (0x40 + (x) * 2)
#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_PREDIV 6
struct lpg_channel;
struct lpg_data;
@ -106,6 +112,7 @@ struct lpg {
* @clk_sel: reference clock frequency selector
* @pre_div_sel: divider selector 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_ping_pong: reverse through pattern, rather than wrapping to start
* @ramp_oneshot: perform only a single pass over the pattern
@ -138,6 +145,7 @@ struct lpg_channel {
unsigned int clk_sel;
unsigned int pre_div_sel;
unsigned int pre_div_exp;
unsigned int pwm_resolution_sel;
bool ramp_enabled;
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_hi_res[] = {0, 1024, 32768, 19200000, 76800000};
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)
{
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 m, best_m = 0;
unsigned int resolution;
unsigned int error;
unsigned int best_err = UINT_MAX;
u64 max_period, min_period;
u64 best_period = 0;
u64 max_period;
u64 max_res;
/*
* The PWM period is determined by:
@ -272,73 +287,107 @@ static int lpg_calc_freq(struct lpg_channel *chan, uint64_t period)
* period = --------------------------
* 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].
*
* This allows for periods between 27uS and 384s, as the PWM framework
* wants a period of equal or lower length than requested, reject
* anything below 27uS.
* This allows for periods between 27uS and 384s for PWM channels and periods between
* 3uS and 24576s for high resolution PWMs.
* 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;
/* 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)
period = max_period;
/*
* Search for the pre_div, refclk and M by solving the rewritten formula
* for each refclk and pre_div value:
* Search for the pre_div, refclk, resolution and M by solving the rewritten formula
* for each refclk, resolution and pre_div value:
*
* period * refclk
* M = log2 -------------------------------------
* 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 (div = 0; div < ARRAY_SIZE(lpg_pre_divs); div++) {
u64 denominator = (u64)NSEC_PER_SEC * lpg_pre_divs[div] * LPG_RESOLUTION;
u64 actual;
u64 ratio;
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];
if (numerator < denominator)
continue;
for (div = 0; div < ARRAY_SIZE(lpg_pre_divs); div++) {
u64 denominator = (u64)NSEC_PER_SEC * lpg_pre_divs[div] *
resolution;
u64 actual;
u64 ratio;
ratio = div64_u64(numerator, denominator);
m = ilog2(ratio);
if (m > LPG_MAX_M)
m = LPG_MAX_M;
if (numerator < denominator)
continue;
actual = DIV_ROUND_UP_ULL(denominator * (1 << m), lpg_clk_rates[clk_sel]);
ratio = div64_u64(numerator, denominator);
m = ilog2(ratio);
if (m > LPG_MAX_M)
m = LPG_MAX_M;
error = period - actual;
if (error < best_err) {
best_err = error;
best_div = div;
best_m = m;
best_clk = clk_sel;
best_period = actual;
actual = DIV_ROUND_UP_ULL(denominator * (1 << m),
clk_rate_arr[clk_sel]);
error = period - actual;
if (error < best_err) {
best_err = error;
best_div = div;
best_m = m;
best_clk = clk_sel;
best_period = actual;
best_pwm_resolution_sel = i;
}
}
}
}
chan->clk_sel = best_clk;
chan->pre_div_sel = best_div;
chan->pre_div_exp = best_m;
chan->period = best_period;
chan->pwm_resolution_sel = best_pwm_resolution_sel;
return 0;
}
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 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));
chan->pwm_value = min(val, max);
@ -354,7 +403,7 @@ static void lpg_apply_freq(struct lpg_channel *chan)
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) {
case LPG_SUBTYPE_LPG:
val |= GENMASK(5, 4);
@ -362,6 +411,9 @@ static void lpg_apply_freq(struct lpg_channel *chan)
case LPG_SUBTYPE_PWM:
val |= BIT(2);
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:
default:
val |= BIT(4);
@ -670,7 +722,7 @@ static int lpg_blink_set(struct lpg_led *led,
triled_set(lpg, triled_mask, triled_mask);
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_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_channel *chan = &lpg->channels[pwm->hwpwm];
unsigned int resolution;
unsigned int pre_div;
unsigned int refclk;
unsigned int val;
@ -988,7 +1041,14 @@ static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
if (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) {
ret = regmap_read(lpg->map, chan->base + LPG_PREDIV_CLK_REG, &val);
if (ret)
@ -1001,7 +1061,8 @@ 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 * 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);
} else {
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->max_brightness = LPG_RESOLUTION - 1;
cdev->max_brightness = LPG_RESOLUTION_9BIT - 1;
if (!of_property_read_string(np, "default-state", &state) &&
!strcmp(state, "on"))