// SPDX-License-Identifier: GPL-2.0 /* * Copyright (c) 2017 BayLibre, SAS * Author: Neil Armstrong * Author: Jerome Brunet */ /* * 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 #include #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 int meson_clk_dualdiv_determine_rate(struct clk_hw *hw, struct clk_rate_request *req) { 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; setting = __dualdiv_get_setting(req->rate, req->best_parent_rate, dualdiv); if (setting) req->rate = __dualdiv_param_to_rate(req->best_parent_rate, setting); else req->rate = meson_clk_dualdiv_recalc_rate(hw, req->best_parent_rate); return 0; } 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, .determine_rate = meson_clk_dualdiv_determine_rate, .set_rate = meson_clk_dualdiv_set_rate, }; EXPORT_SYMBOL_NS_GPL(meson_clk_dualdiv_ops, "CLK_MESON"); const struct clk_ops meson_clk_dualdiv_ro_ops = { .recalc_rate = meson_clk_dualdiv_recalc_rate, }; EXPORT_SYMBOL_NS_GPL(meson_clk_dualdiv_ro_ops, "CLK_MESON"); MODULE_DESCRIPTION("Amlogic dual divider driver"); MODULE_AUTHOR("Neil Armstrong "); MODULE_AUTHOR("Jerome Brunet "); MODULE_LICENSE("GPL"); MODULE_IMPORT_NS("CLK_MESON");