| /* |
| * * Copyright (c) 2014, The Linux Foundation. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * * Neither the name of The Linux Foundation nor the names of its |
| * contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR |
| * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE |
| * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN |
| * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include <debug.h> |
| #include <pm_pwm.h> |
| #include <pm8x41_hw.h> |
| |
| #define NSEC_PER_USEC 1000L |
| #define USEC_PER_SEC 1000000L |
| #define NSEC_PER_SEC 1000000000L |
| |
| #define NUM_REF_CLOCKS 3 |
| #define NSEC_1024HZ (NSEC_PER_SEC / 1024) |
| #define NSEC_32768HZ (NSEC_PER_SEC / 32768) |
| #define NSEC_19P2MHZ (NSEC_PER_SEC / 19200000) |
| |
| #define NUM_PRE_DIVIDE 4 |
| #define PRE_DIVIDE_1 1 |
| #define PRE_DIVIDE_3 3 |
| #define PRE_DIVIDE_5 5 |
| #define PRE_DIVIDE_6 6 |
| static unsigned int pt_t[NUM_PRE_DIVIDE][NUM_REF_CLOCKS] = { |
| { PRE_DIVIDE_1 * NSEC_1024HZ, |
| PRE_DIVIDE_1 * NSEC_32768HZ, |
| PRE_DIVIDE_1 * NSEC_19P2MHZ, |
| }, |
| { PRE_DIVIDE_3 * NSEC_1024HZ, |
| PRE_DIVIDE_3 * NSEC_32768HZ, |
| PRE_DIVIDE_3 * NSEC_19P2MHZ, |
| }, |
| { PRE_DIVIDE_5 * NSEC_1024HZ, |
| PRE_DIVIDE_5 * NSEC_32768HZ, |
| PRE_DIVIDE_5 * NSEC_19P2MHZ, |
| }, |
| { PRE_DIVIDE_6 * NSEC_1024HZ, |
| PRE_DIVIDE_6 * NSEC_32768HZ, |
| PRE_DIVIDE_6 * NSEC_19P2MHZ, |
| }, |
| }; |
| |
| enum pwm_ctl_reg { |
| SIZE_CLK, |
| FREQ_PREDIV_CLK, |
| TYPE_CONFIG, |
| VALUE_LSB, |
| VALUE_MSB, |
| }; |
| |
| #define NUM_PWM_CTL_REGS 5 |
| struct pm_pwm_config { |
| int pwm_size; /* round up to 6 or 9 for 6/9-bit PWM SIZE */ |
| int clk; |
| int pre_div; |
| int pre_div_exp; |
| int pwm_value; |
| uint8_t pwm_ctl[NUM_PWM_CTL_REGS]; |
| }; |
| |
| static void pm_pwm_reg_write(uint8_t off, uint8_t val) |
| { |
| REG_WRITE(PM_PWM_BASE(off), val); |
| } |
| |
| /* |
| * 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 |
| */ |
| |
| #define PRE_DIVIDE_MAX 6 |
| #define CLK_PERIOD_MAX NSEC_1024HZ |
| #define PM_PWM_M_MAX 7 |
| #define MAX_MPT ((PRE_DIVIDE_MAX * CLK_PERIOD_MAX) << PM_PWM_M_MAX) |
| static void pm_pwm_calc_period(unsigned int period_us, |
| struct pm_pwm_config *pwm_config) |
| { |
| 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; |
| |
| n = 6; |
| |
| if (period_us < ((unsigned)(-1) / NSEC_PER_USEC)) |
| period_n = (period_us * NSEC_PER_USEC) >> n; |
| else |
| period_n = (period_us >> n) * NSEC_PER_USEC; |
| |
| if (period_n >= MAX_MPT) { |
| n = 9; |
| period_n >>= 3; |
| } |
| |
| min_err = last_err = (unsigned)(-1); |
| best_m = 0; |
| best_clk = 0; |
| best_div = 0; |
| for (clk = 0; clk < NUM_REF_CLOCKS; clk++) { |
| for (div = 0; div < NUM_PRE_DIVIDE; div++) { |
| /* period_n = (PWM Period / N) */ |
| /* tmp_p = (Pre-divide * Clock Period) * 2^m */ |
| tmp_p = pt_t[div][clk]; |
| for (m = 0; m <= PM_PWM_M_MAX; m++) { |
| if (period_n > tmp_p) |
| cur_err = period_n - tmp_p; |
| else |
| cur_err = tmp_p - period_n; |
| |
| 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; |
| } |
| } |
| } |
| |
| pwm_config->pwm_size = n; |
| pwm_config->clk = best_clk; |
| pwm_config->pre_div = best_div; |
| pwm_config->pre_div_exp = best_m; |
| } |
| |
| static void pm_pwm_calc_pwm_value(struct pm_pwm_config *pwm_config, |
| unsigned int period_us, |
| unsigned int duty_us) |
| { |
| int max_pwm_value; |
| unsigned int tmp; |
| |
| /* Figure out pwm_value with overflow handling */ |
| tmp = 1 << (sizeof(tmp) * 8 - pwm_config->pwm_size); |
| if (duty_us < tmp) { |
| tmp = duty_us << pwm_config->pwm_size; |
| pwm_config->pwm_value = tmp / period_us; |
| } else { |
| tmp = period_us >> pwm_config->pwm_size; |
| pwm_config->pwm_value = duty_us / tmp; |
| } |
| max_pwm_value = (1 << pwm_config->pwm_size) - 1; |
| if (pwm_config->pwm_value > max_pwm_value) |
| pwm_config->pwm_value = max_pwm_value; |
| } |
| |
| #define PM_PWM_SIZE_9_BIT 1 |
| #define PM_PWM_SIZE_6_BIT 0 |
| static void pm_pwm_config_regs(struct pm_pwm_config *pwm_config) |
| { |
| int i; |
| uint8_t reg; |
| |
| reg = ((pwm_config->pwm_size > 6 ? PM_PWM_SIZE_9_BIT : PM_PWM_SIZE_6_BIT) |
| << PM_PWM_SIZE_SEL_SHIFT) |
| & PM_PWM_SIZE_SEL_MASK; |
| reg |= (pwm_config->clk + 1) & PM_PWM_CLK_SEL_MASK; |
| pwm_config->pwm_ctl[SIZE_CLK] = reg; |
| |
| reg = (pwm_config->pre_div << PM_PWM_PREDIVIDE_SHIFT) |
| & PM_PWM_PREDIVIDE_MASK; |
| reg |= pwm_config->pre_div_exp & PM_PWM_M_MASK; |
| pwm_config->pwm_ctl[FREQ_PREDIV_CLK] = reg; |
| |
| /* Enable glitch removal by default */ |
| reg = 1 << PM_PWM_EN_GLITCH_REMOVAL_SHIFT |
| & PM_PWM_EN_GLITCH_REMOVAL_MASK; |
| pwm_config->pwm_ctl[TYPE_CONFIG] = reg; |
| |
| if (pwm_config->pwm_size > 6) { |
| pwm_config->pwm_ctl[VALUE_LSB] = pwm_config->pwm_value |
| & PM_PWM_VALUE_BIT7_0; |
| pwm_config->pwm_ctl[VALUE_MSB] = (pwm_config->pwm_value >> 8) |
| & PM_PWM_VALUE_BIT8; |
| } else |
| pwm_config->pwm_ctl[VALUE_LSB] = pwm_config->pwm_value |
| & PM_PWM_VALUE_BIT5_0; |
| |
| for (i = 0; i < NUM_PWM_CTL_REGS; i++) |
| pm_pwm_reg_write(PM_PWM_CTL_REG_OFFSET + i, pwm_config->pwm_ctl[i]); |
| |
| reg = 1 & PM_PWM_SYNC_MASK; |
| pm_pwm_reg_write(PM_PWM_SYNC_REG_OFFSET, reg); |
| } |
| |
| /* usec: 19.2M, n=6, m=0, pre=2 */ |
| #define PM_PWM_PERIOD_MIN 7 |
| /* 1K, n=9, m=7, pre=6 */ |
| #define PM_PWM_PERIOD_MAX (384 * USEC_PER_SEC) |
| int pm_pwm_config(unsigned int duty_us, unsigned int period_us) |
| { |
| struct pm_pwm_config pwm_config; |
| |
| if ((duty_us > period_us) || (period_us > PM_PWM_PERIOD_MAX) || |
| (period_us < PM_PWM_PERIOD_MIN)) { |
| dprintf(CRITICAL, "Error in duty cycle and period\n"); |
| return -1; |
| } |
| |
| pm_pwm_calc_period(period_us, &pwm_config); |
| pm_pwm_calc_pwm_value(&pwm_config, period_us, duty_us); |
| |
| dprintf(SPEW, "duty/period=%u/%u usec: pwm_value=%d (of %d)\n", |
| duty_us, period_us, pwm_config.pwm_value, 1 << pwm_config.pwm_size); |
| |
| pm_pwm_config_regs(&pwm_config); |
| |
| return 0; |
| } |
| |
| void pm_pwm_enable(bool enable) |
| { |
| uint8_t reg; |
| |
| reg = enable << PM_PWM_ENABLE_CTL_SHIFT |
| & PM_PWM_ENABLE_CTL_MASK; |
| |
| pm_pwm_reg_write(PM_PWM_ENABLE_CTL_REG_OFFSET, reg); |
| } |