1 /*
2  * Copyright (C) 2016 Free Electrons
3  * Copyright (C) 2016 NextThing Co
4  *
5  * Maxime Ripard <maxime.ripard@free-electrons.com>
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License as
9  * published by the Free Software Foundation; either version 2 of
10  * the License, or (at your option) any later version.
11  */
12 
13 #include <linux/clk-provider.h>
14 
15 #include "sun4i_tcon.h"
16 #include "sun4i_hdmi.h"
17 
18 struct sun4i_tmds {
19 	struct clk_hw		hw;
20 	struct sun4i_hdmi	*hdmi;
21 };
22 
23 static inline struct sun4i_tmds *hw_to_tmds(struct clk_hw *hw)
24 {
25 	return container_of(hw, struct sun4i_tmds, hw);
26 }
27 
28 
29 static unsigned long sun4i_tmds_calc_divider(unsigned long rate,
30 					     unsigned long parent_rate,
31 					     u8 *div,
32 					     bool *half)
33 {
34 	unsigned long best_rate = 0;
35 	u8 best_m = 0, m;
36 	bool is_double;
37 
38 	for (m = 1; m < 16; m++) {
39 		u8 d;
40 
41 		for (d = 1; d < 3; d++) {
42 			unsigned long tmp_rate;
43 
44 			tmp_rate = parent_rate / m / d;
45 
46 			if (tmp_rate > rate)
47 				continue;
48 
49 			if (!best_rate ||
50 			    (rate - tmp_rate) < (rate - best_rate)) {
51 				best_rate = tmp_rate;
52 				best_m = m;
53 				is_double = d;
54 			}
55 		}
56 	}
57 
58 	if (div && half) {
59 		*div = best_m;
60 		*half = is_double;
61 	}
62 
63 	return best_rate;
64 }
65 
66 
67 static int sun4i_tmds_determine_rate(struct clk_hw *hw,
68 				     struct clk_rate_request *req)
69 {
70 	struct clk_hw *parent;
71 	unsigned long best_parent = 0;
72 	unsigned long rate = req->rate;
73 	int best_div = 1, best_half = 1;
74 	int i, j;
75 
76 	/*
77 	 * We only consider PLL3, since the TCON is very likely to be
78 	 * clocked from it, and to have the same rate than our HDMI
79 	 * clock, so we should not need to do anything.
80 	 */
81 
82 	parent = clk_hw_get_parent_by_index(hw, 0);
83 	if (!parent)
84 		return -EINVAL;
85 
86 	for (i = 1; i < 3; i++) {
87 		for (j = 1; j < 16; j++) {
88 			unsigned long ideal = rate * i * j;
89 			unsigned long rounded;
90 
91 			rounded = clk_hw_round_rate(parent, ideal);
92 
93 			if (rounded == ideal) {
94 				best_parent = rounded;
95 				best_half = i;
96 				best_div = j;
97 				goto out;
98 			}
99 
100 			if (abs(rate - rounded / i) <
101 			    abs(rate - best_parent / best_div)) {
102 				best_parent = rounded;
103 				best_div = i;
104 			}
105 		}
106 	}
107 
108 out:
109 	req->rate = best_parent / best_half / best_div;
110 	req->best_parent_rate = best_parent;
111 	req->best_parent_hw = parent;
112 
113 	return 0;
114 }
115 
116 static unsigned long sun4i_tmds_recalc_rate(struct clk_hw *hw,
117 					    unsigned long parent_rate)
118 {
119 	struct sun4i_tmds *tmds = hw_to_tmds(hw);
120 	u32 reg;
121 
122 	reg = readl(tmds->hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
123 	if (reg & SUN4I_HDMI_PAD_CTRL1_HALVE_CLK)
124 		parent_rate /= 2;
125 
126 	reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
127 	reg = (reg >> 4) & 0xf;
128 	if (!reg)
129 		reg = 1;
130 
131 	return parent_rate / reg;
132 }
133 
134 static int sun4i_tmds_set_rate(struct clk_hw *hw, unsigned long rate,
135 			       unsigned long parent_rate)
136 {
137 	struct sun4i_tmds *tmds = hw_to_tmds(hw);
138 	bool half;
139 	u32 reg;
140 	u8 div;
141 
142 	sun4i_tmds_calc_divider(rate, parent_rate, &div, &half);
143 
144 	reg = readl(tmds->hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
145 	reg &= ~SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
146 	if (half)
147 		reg |= SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
148 	writel(reg, tmds->hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
149 
150 	reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
151 	reg &= ~SUN4I_HDMI_PLL_CTRL_DIV_MASK;
152 	writel(reg | SUN4I_HDMI_PLL_CTRL_DIV(div),
153 	       tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
154 
155 	return 0;
156 }
157 
158 static u8 sun4i_tmds_get_parent(struct clk_hw *hw)
159 {
160 	struct sun4i_tmds *tmds = hw_to_tmds(hw);
161 	u32 reg;
162 
163 	reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_DBG0_REG);
164 	return ((reg & SUN4I_HDMI_PLL_DBG0_TMDS_PARENT_MASK) >>
165 		SUN4I_HDMI_PLL_DBG0_TMDS_PARENT_SHIFT);
166 }
167 
168 static int sun4i_tmds_set_parent(struct clk_hw *hw, u8 index)
169 {
170 	struct sun4i_tmds *tmds = hw_to_tmds(hw);
171 	u32 reg;
172 
173 	if (index > 1)
174 		return -EINVAL;
175 
176 	reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_DBG0_REG);
177 	reg &= ~SUN4I_HDMI_PLL_DBG0_TMDS_PARENT_MASK;
178 	writel(reg | SUN4I_HDMI_PLL_DBG0_TMDS_PARENT(index),
179 	       tmds->hdmi->base + SUN4I_HDMI_PLL_DBG0_REG);
180 
181 	return 0;
182 }
183 
184 static const struct clk_ops sun4i_tmds_ops = {
185 	.determine_rate	= sun4i_tmds_determine_rate,
186 	.recalc_rate	= sun4i_tmds_recalc_rate,
187 	.set_rate	= sun4i_tmds_set_rate,
188 
189 	.get_parent	= sun4i_tmds_get_parent,
190 	.set_parent	= sun4i_tmds_set_parent,
191 };
192 
193 int sun4i_tmds_create(struct sun4i_hdmi *hdmi)
194 {
195 	struct clk_init_data init;
196 	struct sun4i_tmds *tmds;
197 	const char *parents[2];
198 
199 	parents[0] = __clk_get_name(hdmi->pll0_clk);
200 	if (!parents[0])
201 		return -ENODEV;
202 
203 	parents[1] = __clk_get_name(hdmi->pll1_clk);
204 	if (!parents[1])
205 		return -ENODEV;
206 
207 	tmds = devm_kzalloc(hdmi->dev, sizeof(*tmds), GFP_KERNEL);
208 	if (!tmds)
209 		return -ENOMEM;
210 
211 	init.name = "hdmi-tmds";
212 	init.ops = &sun4i_tmds_ops;
213 	init.parent_names = parents;
214 	init.num_parents = 2;
215 	init.flags = CLK_SET_RATE_PARENT;
216 
217 	tmds->hdmi = hdmi;
218 	tmds->hw.init = &init;
219 
220 	hdmi->tmds_clk = devm_clk_register(hdmi->dev, &tmds->hw);
221 	if (IS_ERR(hdmi->tmds_clk))
222 		return PTR_ERR(hdmi->tmds_clk);
223 
224 	return 0;
225 }
226