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