| /* |
| * Copyright (c) 2017 BayLibre, SAS. |
| * Author: Neil Armstrong <narmstrong@baylibre.com> |
| * |
| * SPDX-License-Identifier: GPL-2.0+ |
| */ |
| |
| #include <linux/clk-provider.h> |
| #include <linux/bitfield.h> |
| #include <linux/regmap.h> |
| #include "gxbb-aoclk.h" |
| |
| /* |
| * The AO Domain embeds a dual/divider to generate a more precise |
| * 32,768KHz clock for low-power suspend mode and CEC. |
| * ______ ______ |
| * | | | | |
| * ______ | Div1 |-| Cnt1 | ______ |
| * | | /|______| |______|\ | | |
| * Xtal-->| Gate |---| ______ ______ X-X--| Gate |--> |
| * |______| | \| | | |/ | |______| |
| * | | Div2 |-| Cnt2 | | |
| * | |______| |______| | |
| * |_______________________| |
| * |
| * The dividing can be switched to single or dual, with a counter |
| * for each divider to set when the switching is done. |
| * The entire dividing mechanism can be also bypassed. |
| */ |
| |
| #define CLK_CNTL0_N1_MASK GENMASK(11, 0) |
| #define CLK_CNTL0_N2_MASK GENMASK(23, 12) |
| #define CLK_CNTL0_DUALDIV_EN BIT(28) |
| #define CLK_CNTL0_OUT_GATE_EN BIT(30) |
| #define CLK_CNTL0_IN_GATE_EN BIT(31) |
| |
| #define CLK_CNTL1_M1_MASK GENMASK(11, 0) |
| #define CLK_CNTL1_M2_MASK GENMASK(23, 12) |
| #define CLK_CNTL1_BYPASS_EN BIT(24) |
| #define CLK_CNTL1_SELECT_OSC BIT(27) |
| |
| #define PWR_CNTL_ALT_32K_SEL GENMASK(13, 10) |
| |
| struct cec_32k_freq_table { |
| unsigned long parent_rate; |
| unsigned long target_rate; |
| bool dualdiv; |
| unsigned int n1; |
| unsigned int n2; |
| unsigned int m1; |
| unsigned int m2; |
| }; |
| |
| static const struct cec_32k_freq_table aoclk_cec_32k_table[] = { |
| [0] = { |
| .parent_rate = 24000000, |
| .target_rate = 32768, |
| .dualdiv = true, |
| .n1 = 733, |
| .n2 = 732, |
| .m1 = 8, |
| .m2 = 11, |
| }, |
| }; |
| |
| /* |
| * If CLK_CNTL0_DUALDIV_EN == 0 |
| * - will use N1 divider only |
| * If CLK_CNTL0_DUALDIV_EN == 1 |
| * - hold M1 cycles of N1 divider then changes to N2 |
| * - hold M2 cycles of N2 divider then changes to N1 |
| * Then we can get more accurate division. |
| */ |
| static unsigned long aoclk_cec_32k_recalc_rate(struct clk_hw *hw, |
| unsigned long parent_rate) |
| { |
| struct aoclk_cec_32k *cec_32k = to_aoclk_cec_32k(hw); |
| unsigned long n1; |
| u32 reg0, reg1; |
| |
| regmap_read(cec_32k->regmap, AO_RTC_ALT_CLK_CNTL0, ®0); |
| regmap_read(cec_32k->regmap, AO_RTC_ALT_CLK_CNTL1, ®1); |
| |
| if (reg1 & CLK_CNTL1_BYPASS_EN) |
| return parent_rate; |
| |
| if (reg0 & CLK_CNTL0_DUALDIV_EN) { |
| unsigned long n2, m1, m2, f1, f2, p1, p2; |
| |
| n1 = FIELD_GET(CLK_CNTL0_N1_MASK, reg0) + 1; |
| n2 = FIELD_GET(CLK_CNTL0_N2_MASK, reg0) + 1; |
| |
| m1 = FIELD_GET(CLK_CNTL1_M1_MASK, reg1) + 1; |
| m2 = FIELD_GET(CLK_CNTL1_M2_MASK, reg1) + 1; |
| |
| f1 = DIV_ROUND_CLOSEST(parent_rate, n1); |
| f2 = DIV_ROUND_CLOSEST(parent_rate, n2); |
| |
| p1 = DIV_ROUND_CLOSEST(100000000 * m1, f1 * (m1 + m2)); |
| p2 = DIV_ROUND_CLOSEST(100000000 * m2, f2 * (m1 + m2)); |
| |
| return DIV_ROUND_UP(100000000, p1 + p2); |
| } |
| |
| n1 = FIELD_GET(CLK_CNTL0_N1_MASK, reg0) + 1; |
| |
| return DIV_ROUND_CLOSEST(parent_rate, n1); |
| } |
| |
| static const struct cec_32k_freq_table *find_cec_32k_freq(unsigned long rate, |
| unsigned long prate) |
| { |
| int i; |
| |
| for (i = 0 ; i < ARRAY_SIZE(aoclk_cec_32k_table) ; ++i) |
| if (aoclk_cec_32k_table[i].parent_rate == prate && |
| aoclk_cec_32k_table[i].target_rate == rate) |
| return &aoclk_cec_32k_table[i]; |
| |
| return NULL; |
| } |
| |
| static long aoclk_cec_32k_round_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long *prate) |
| { |
| const struct cec_32k_freq_table *freq = find_cec_32k_freq(rate, |
| *prate); |
| |
| /* If invalid return first one */ |
| if (!freq) |
| return aoclk_cec_32k_table[0].target_rate; |
| |
| return freq->target_rate; |
| } |
| |
| /* |
| * From the Amlogic init procedure, the IN and OUT gates needs to be handled |
| * in the init procedure to avoid any glitches. |
| */ |
| |
| static int aoclk_cec_32k_set_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long parent_rate) |
| { |
| const struct cec_32k_freq_table *freq = find_cec_32k_freq(rate, |
| parent_rate); |
| struct aoclk_cec_32k *cec_32k = to_aoclk_cec_32k(hw); |
| u32 reg = 0; |
| |
| if (!freq) |
| return -EINVAL; |
| |
| /* Disable clock */ |
| regmap_update_bits(cec_32k->regmap, AO_RTC_ALT_CLK_CNTL0, |
| CLK_CNTL0_IN_GATE_EN | CLK_CNTL0_OUT_GATE_EN, 0); |
| |
| reg = FIELD_PREP(CLK_CNTL0_N1_MASK, freq->n1 - 1); |
| if (freq->dualdiv) |
| reg |= CLK_CNTL0_DUALDIV_EN | |
| FIELD_PREP(CLK_CNTL0_N2_MASK, freq->n2 - 1); |
| |
| regmap_write(cec_32k->regmap, AO_RTC_ALT_CLK_CNTL0, reg); |
| |
| reg = FIELD_PREP(CLK_CNTL1_M1_MASK, freq->m1 - 1); |
| if (freq->dualdiv) |
| reg |= FIELD_PREP(CLK_CNTL1_M2_MASK, freq->m2 - 1); |
| |
| regmap_write(cec_32k->regmap, AO_RTC_ALT_CLK_CNTL1, reg); |
| |
| /* Enable clock */ |
| regmap_update_bits(cec_32k->regmap, AO_RTC_ALT_CLK_CNTL0, |
| CLK_CNTL0_IN_GATE_EN, CLK_CNTL0_IN_GATE_EN); |
| |
| udelay(200); |
| |
| regmap_update_bits(cec_32k->regmap, AO_RTC_ALT_CLK_CNTL0, |
| CLK_CNTL0_OUT_GATE_EN, CLK_CNTL0_OUT_GATE_EN); |
| |
| regmap_update_bits(cec_32k->regmap, AO_CRT_CLK_CNTL1, |
| CLK_CNTL1_SELECT_OSC, CLK_CNTL1_SELECT_OSC); |
| |
| /* Select 32k from XTAL */ |
| regmap_update_bits(cec_32k->regmap, |
| AO_RTI_PWR_CNTL_REG0, |
| PWR_CNTL_ALT_32K_SEL, |
| FIELD_PREP(PWR_CNTL_ALT_32K_SEL, 4)); |
| |
| return 0; |
| } |
| |
| const struct clk_ops meson_aoclk_cec_32k_ops = { |
| .recalc_rate = aoclk_cec_32k_recalc_rate, |
| .round_rate = aoclk_cec_32k_round_rate, |
| .set_rate = aoclk_cec_32k_set_rate, |
| }; |