1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Copyright (c) 2017 BayLibre, SAS |
4 | * Author: Neil Armstrong <narmstrong@baylibre.com> |
5 | * Author: Jerome Brunet <jbrunet@baylibre.com> |
6 | */ |
7 | |
8 | /* |
9 | * The AO Domain embeds a dual/divider to generate a more precise |
10 | * 32,768KHz clock for low-power suspend mode and CEC. |
11 | * ______ ______ |
12 | * | | | | |
13 | * | Div1 |-| Cnt1 | |
14 | * /|______| |______|\ |
15 | * -| ______ ______ X--> Out |
16 | * \| | | |/ |
17 | * | Div2 |-| Cnt2 | |
18 | * |______| |______| |
19 | * |
20 | * The dividing can be switched to single or dual, with a counter |
21 | * for each divider to set when the switching is done. |
22 | */ |
23 | |
24 | #include <linux/clk-provider.h> |
25 | #include <linux/module.h> |
26 | |
27 | #include "clk-regmap.h" |
28 | #include "clk-dualdiv.h" |
29 | |
30 | static inline struct meson_clk_dualdiv_data * |
31 | meson_clk_dualdiv_data(struct clk_regmap *clk) |
32 | { |
33 | return (struct meson_clk_dualdiv_data *)clk->data; |
34 | } |
35 | |
36 | static unsigned long |
37 | __dualdiv_param_to_rate(unsigned long parent_rate, |
38 | const struct meson_clk_dualdiv_param *p) |
39 | { |
40 | if (!p->dual) |
41 | return DIV_ROUND_CLOSEST(parent_rate, p->n1); |
42 | |
43 | return DIV_ROUND_CLOSEST(parent_rate * (p->m1 + p->m2), |
44 | p->n1 * p->m1 + p->n2 * p->m2); |
45 | } |
46 | |
47 | static unsigned long meson_clk_dualdiv_recalc_rate(struct clk_hw *hw, |
48 | unsigned long parent_rate) |
49 | { |
50 | struct clk_regmap *clk = to_clk_regmap(hw); |
51 | struct meson_clk_dualdiv_data *dualdiv = meson_clk_dualdiv_data(clk); |
52 | struct meson_clk_dualdiv_param setting; |
53 | |
54 | setting.dual = meson_parm_read(map: clk->map, p: &dualdiv->dual); |
55 | setting.n1 = meson_parm_read(map: clk->map, p: &dualdiv->n1) + 1; |
56 | setting.m1 = meson_parm_read(map: clk->map, p: &dualdiv->m1) + 1; |
57 | setting.n2 = meson_parm_read(map: clk->map, p: &dualdiv->n2) + 1; |
58 | setting.m2 = meson_parm_read(map: clk->map, p: &dualdiv->m2) + 1; |
59 | |
60 | return __dualdiv_param_to_rate(parent_rate, p: &setting); |
61 | } |
62 | |
63 | static const struct meson_clk_dualdiv_param * |
64 | __dualdiv_get_setting(unsigned long rate, unsigned long parent_rate, |
65 | struct meson_clk_dualdiv_data *dualdiv) |
66 | { |
67 | const struct meson_clk_dualdiv_param *table = dualdiv->table; |
68 | unsigned long best = 0, now = 0; |
69 | unsigned int i, best_i = 0; |
70 | |
71 | if (!table) |
72 | return NULL; |
73 | |
74 | for (i = 0; table[i].n1; i++) { |
75 | now = __dualdiv_param_to_rate(parent_rate, p: &table[i]); |
76 | |
77 | /* If we get an exact match, don't bother any further */ |
78 | if (now == rate) { |
79 | return &table[i]; |
80 | } else if (abs(now - rate) < abs(best - rate)) { |
81 | best = now; |
82 | best_i = i; |
83 | } |
84 | } |
85 | |
86 | return (struct meson_clk_dualdiv_param *)&table[best_i]; |
87 | } |
88 | |
89 | static int meson_clk_dualdiv_determine_rate(struct clk_hw *hw, |
90 | struct clk_rate_request *req) |
91 | { |
92 | struct clk_regmap *clk = to_clk_regmap(hw); |
93 | struct meson_clk_dualdiv_data *dualdiv = meson_clk_dualdiv_data(clk); |
94 | const struct meson_clk_dualdiv_param *setting; |
95 | |
96 | setting = __dualdiv_get_setting(rate: req->rate, parent_rate: req->best_parent_rate, |
97 | dualdiv); |
98 | if (setting) |
99 | req->rate = __dualdiv_param_to_rate(parent_rate: req->best_parent_rate, |
100 | p: setting); |
101 | else |
102 | req->rate = meson_clk_dualdiv_recalc_rate(hw, |
103 | parent_rate: req->best_parent_rate); |
104 | |
105 | return 0; |
106 | } |
107 | |
108 | static int meson_clk_dualdiv_set_rate(struct clk_hw *hw, unsigned long rate, |
109 | unsigned long parent_rate) |
110 | { |
111 | struct clk_regmap *clk = to_clk_regmap(hw); |
112 | struct meson_clk_dualdiv_data *dualdiv = meson_clk_dualdiv_data(clk); |
113 | const struct meson_clk_dualdiv_param *setting = |
114 | __dualdiv_get_setting(rate, parent_rate, dualdiv); |
115 | |
116 | if (!setting) |
117 | return -EINVAL; |
118 | |
119 | meson_parm_write(map: clk->map, p: &dualdiv->dual, val: setting->dual); |
120 | meson_parm_write(map: clk->map, p: &dualdiv->n1, val: setting->n1 - 1); |
121 | meson_parm_write(map: clk->map, p: &dualdiv->m1, val: setting->m1 - 1); |
122 | meson_parm_write(map: clk->map, p: &dualdiv->n2, val: setting->n2 - 1); |
123 | meson_parm_write(map: clk->map, p: &dualdiv->m2, val: setting->m2 - 1); |
124 | |
125 | return 0; |
126 | } |
127 | |
128 | const struct clk_ops meson_clk_dualdiv_ops = { |
129 | .recalc_rate = meson_clk_dualdiv_recalc_rate, |
130 | .determine_rate = meson_clk_dualdiv_determine_rate, |
131 | .set_rate = meson_clk_dualdiv_set_rate, |
132 | }; |
133 | EXPORT_SYMBOL_GPL(meson_clk_dualdiv_ops); |
134 | |
135 | const struct clk_ops meson_clk_dualdiv_ro_ops = { |
136 | .recalc_rate = meson_clk_dualdiv_recalc_rate, |
137 | }; |
138 | EXPORT_SYMBOL_GPL(meson_clk_dualdiv_ro_ops); |
139 | |
140 | MODULE_DESCRIPTION("Amlogic dual divider driver" ); |
141 | MODULE_AUTHOR("Neil Armstrong <narmstrong@baylibre.com>" ); |
142 | MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>" ); |
143 | MODULE_LICENSE("GPL v2" ); |
144 | |