leds: rgb: leds-qcom-lpg: Add support for high resolution PWM
authorAnjelique Melendez <quic_amelende@quicinc.com>
Fri, 7 Apr 2023 22:38:48 +0000 (15:38 -0700)
committerLee Jones <lee@kernel.org>
Thu, 20 Apr 2023 10:09:34 +0000 (11:09 +0100)
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
drivers/leds/rgb/leds-qcom-lpg.c

index 67f48f2..373bcf8 100644 (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>
 #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)
 #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;
-
-                       if (numerator < denominator)
-                               continue;
-
-                       ratio = div64_u64(numerator, denominator);
-                       m = ilog2(ratio);
-                       if (m > LPG_MAX_M)
-                               m = LPG_MAX_M;
-
-                       actual = DIV_ROUND_UP_ULL(denominator * (1 << m), lpg_clk_rates[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;
+       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++) {
+                               u64 denominator = (u64)NSEC_PER_SEC * lpg_pre_divs[div] *
+                                                 resolution;
+                               u64 actual;
+                               u64 ratio;
+
+                               if (numerator < denominator)
+                                       continue;
+
+                               ratio = div64_u64(numerator, denominator);
+                               m = ilog2(ratio);
+                               if (m > LPG_MAX_M)
+                                       m = LPG_MAX_M;
+
+                               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;
+
+       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 * 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"))