| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (c) 2017 BayLibre, SAS |
| * Author: Neil Armstrong <narmstrong@baylibre.com> |
| * Author: Jerome Brunet <jbrunet@baylibre.com> |
| */ |
| |
| /* |
| * The AO Domain embeds a dual/divider to generate a more precise |
| * 32,768KHz clock for low-power suspend mode and CEC. |
| * ______ ______ |
| * | | | | |
| * | Div1 |-| Cnt1 | |
| * /|______| |______|\ |
| * -| ______ ______ X--> Out |
| * \| | | |/ |
| * | Div2 |-| Cnt2 | |
| * |______| |______| |
| * |
| * The dividing can be switched to single or dual, with a counter |
| * for each divider to set when the switching is done. |
| */ |
| |
| #include <linux/clk-provider.h> |
| #include <linux/module.h> |
| |
| #include "clk-regmap.h" |
| #include "clk-dualdiv.h" |
| |
| static inline struct meson_clk_dualdiv_data * |
| meson_clk_dualdiv_data(struct clk_regmap *clk) |
| { |
| return (struct meson_clk_dualdiv_data *)clk->data; |
| } |
| |
| static unsigned long |
| __dualdiv_param_to_rate(unsigned long parent_rate, |
| const struct meson_clk_dualdiv_param *p) |
| { |
| if (!p->dual) |
| return DIV_ROUND_CLOSEST(parent_rate, p->n1); |
| |
| return DIV_ROUND_CLOSEST(parent_rate * (p->m1 + p->m2), |
| p->n1 * p->m1 + p->n2 * p->m2); |
| } |
| |
| static unsigned long meson_clk_dualdiv_recalc_rate(struct clk_hw *hw, |
| unsigned long parent_rate) |
| { |
| struct clk_regmap *clk = to_clk_regmap(hw); |
| struct meson_clk_dualdiv_data *dualdiv = meson_clk_dualdiv_data(clk); |
| struct meson_clk_dualdiv_param setting; |
| |
| setting.dual = meson_parm_read(clk->map, &dualdiv->dual); |
| setting.n1 = meson_parm_read(clk->map, &dualdiv->n1) + 1; |
| setting.m1 = meson_parm_read(clk->map, &dualdiv->m1) + 1; |
| setting.n2 = meson_parm_read(clk->map, &dualdiv->n2) + 1; |
| setting.m2 = meson_parm_read(clk->map, &dualdiv->m2) + 1; |
| |
| return __dualdiv_param_to_rate(parent_rate, &setting); |
| } |
| |
| static const struct meson_clk_dualdiv_param * |
| __dualdiv_get_setting(unsigned long rate, unsigned long parent_rate, |
| struct meson_clk_dualdiv_data *dualdiv) |
| { |
| const struct meson_clk_dualdiv_param *table = dualdiv->table; |
| unsigned long best = 0, now = 0; |
| unsigned int i, best_i = 0; |
| |
| if (!table) |
| return NULL; |
| |
| for (i = 0; table[i].n1; i++) { |
| now = __dualdiv_param_to_rate(parent_rate, &table[i]); |
| |
| /* If we get an exact match, don't bother any further */ |
| if (now == rate) { |
| return &table[i]; |
| } else if (abs(now - rate) < abs(best - rate)) { |
| best = now; |
| best_i = i; |
| } |
| } |
| |
| return (struct meson_clk_dualdiv_param *)&table[best_i]; |
| } |
| |
| static long meson_clk_dualdiv_round_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long *parent_rate) |
| { |
| struct clk_regmap *clk = to_clk_regmap(hw); |
| struct meson_clk_dualdiv_data *dualdiv = meson_clk_dualdiv_data(clk); |
| const struct meson_clk_dualdiv_param *setting = |
| __dualdiv_get_setting(rate, *parent_rate, dualdiv); |
| |
| if (!setting) |
| return meson_clk_dualdiv_recalc_rate(hw, *parent_rate); |
| |
| return __dualdiv_param_to_rate(*parent_rate, setting); |
| } |
| |
| static int meson_clk_dualdiv_set_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long parent_rate) |
| { |
| struct clk_regmap *clk = to_clk_regmap(hw); |
| struct meson_clk_dualdiv_data *dualdiv = meson_clk_dualdiv_data(clk); |
| const struct meson_clk_dualdiv_param *setting = |
| __dualdiv_get_setting(rate, parent_rate, dualdiv); |
| |
| if (!setting) |
| return -EINVAL; |
| |
| meson_parm_write(clk->map, &dualdiv->dual, setting->dual); |
| meson_parm_write(clk->map, &dualdiv->n1, setting->n1 - 1); |
| meson_parm_write(clk->map, &dualdiv->m1, setting->m1 - 1); |
| meson_parm_write(clk->map, &dualdiv->n2, setting->n2 - 1); |
| meson_parm_write(clk->map, &dualdiv->m2, setting->m2 - 1); |
| |
| return 0; |
| } |
| |
| const struct clk_ops meson_clk_dualdiv_ops = { |
| .recalc_rate = meson_clk_dualdiv_recalc_rate, |
| .round_rate = meson_clk_dualdiv_round_rate, |
| .set_rate = meson_clk_dualdiv_set_rate, |
| }; |
| EXPORT_SYMBOL_GPL(meson_clk_dualdiv_ops); |
| |
| const struct clk_ops meson_clk_dualdiv_ro_ops = { |
| .recalc_rate = meson_clk_dualdiv_recalc_rate, |
| }; |
| EXPORT_SYMBOL_GPL(meson_clk_dualdiv_ro_ops); |
| |
| MODULE_DESCRIPTION("Amlogic dual divider driver"); |
| MODULE_AUTHOR("Neil Armstrong <narmstrong@baylibre.com>"); |
| MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>"); |
| MODULE_LICENSE("GPL v2"); |