1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* Copyright (c) 2018 Jernej Skrabec <jernej.skrabec@siol.net> */ |
3 | |
4 | |
5 | #include <linux/bitfield.h> |
6 | #include <linux/component.h> |
7 | #include <linux/device.h> |
8 | #include <linux/io.h> |
9 | #include <linux/module.h> |
10 | #include <linux/of.h> |
11 | #include <linux/of_graph.h> |
12 | #include <linux/platform_device.h> |
13 | |
14 | #include <dt-bindings/clock/sun8i-tcon-top.h> |
15 | |
16 | #include "sun8i_tcon_top.h" |
17 | |
18 | struct sun8i_tcon_top_quirks { |
19 | bool has_tcon_tv1; |
20 | bool has_dsi; |
21 | }; |
22 | |
23 | static bool sun8i_tcon_top_node_is_tcon_top(struct device_node *node) |
24 | { |
25 | return !!of_match_node(matches: sun8i_tcon_top_of_table, node); |
26 | } |
27 | |
28 | int sun8i_tcon_top_set_hdmi_src(struct device *dev, int tcon) |
29 | { |
30 | struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev); |
31 | unsigned long flags; |
32 | u32 val; |
33 | |
34 | if (!sun8i_tcon_top_node_is_tcon_top(node: dev->of_node)) { |
35 | dev_err(dev, "Device is not TCON TOP!\n" ); |
36 | return -EINVAL; |
37 | } |
38 | |
39 | if (tcon < 2 || tcon > 3) { |
40 | dev_err(dev, "TCON index must be 2 or 3!\n" ); |
41 | return -EINVAL; |
42 | } |
43 | |
44 | spin_lock_irqsave(&tcon_top->reg_lock, flags); |
45 | |
46 | val = readl(addr: tcon_top->regs + TCON_TOP_GATE_SRC_REG); |
47 | val &= ~TCON_TOP_HDMI_SRC_MSK; |
48 | val |= FIELD_PREP(TCON_TOP_HDMI_SRC_MSK, tcon - 1); |
49 | writel(val, addr: tcon_top->regs + TCON_TOP_GATE_SRC_REG); |
50 | |
51 | spin_unlock_irqrestore(lock: &tcon_top->reg_lock, flags); |
52 | |
53 | return 0; |
54 | } |
55 | EXPORT_SYMBOL(sun8i_tcon_top_set_hdmi_src); |
56 | |
57 | int sun8i_tcon_top_de_config(struct device *dev, int mixer, int tcon) |
58 | { |
59 | struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev); |
60 | unsigned long flags; |
61 | u32 reg; |
62 | |
63 | if (!sun8i_tcon_top_node_is_tcon_top(node: dev->of_node)) { |
64 | dev_err(dev, "Device is not TCON TOP!\n" ); |
65 | return -EINVAL; |
66 | } |
67 | |
68 | if (mixer > 1) { |
69 | dev_err(dev, "Mixer index is too high!\n" ); |
70 | return -EINVAL; |
71 | } |
72 | |
73 | if (tcon > 3) { |
74 | dev_err(dev, "TCON index is too high!\n" ); |
75 | return -EINVAL; |
76 | } |
77 | |
78 | spin_lock_irqsave(&tcon_top->reg_lock, flags); |
79 | |
80 | reg = readl(addr: tcon_top->regs + TCON_TOP_PORT_SEL_REG); |
81 | if (mixer == 0) { |
82 | reg &= ~TCON_TOP_PORT_DE0_MSK; |
83 | reg |= FIELD_PREP(TCON_TOP_PORT_DE0_MSK, tcon); |
84 | } else { |
85 | reg &= ~TCON_TOP_PORT_DE1_MSK; |
86 | reg |= FIELD_PREP(TCON_TOP_PORT_DE1_MSK, tcon); |
87 | } |
88 | writel(val: reg, addr: tcon_top->regs + TCON_TOP_PORT_SEL_REG); |
89 | |
90 | spin_unlock_irqrestore(lock: &tcon_top->reg_lock, flags); |
91 | |
92 | return 0; |
93 | } |
94 | EXPORT_SYMBOL(sun8i_tcon_top_de_config); |
95 | |
96 | |
97 | static struct clk_hw *sun8i_tcon_top_register_gate(struct device *dev, |
98 | const char *parent, |
99 | void __iomem *regs, |
100 | spinlock_t *lock, |
101 | u8 bit, int name_index) |
102 | { |
103 | const char *clk_name, *parent_name; |
104 | int ret, index; |
105 | |
106 | index = of_property_match_string(np: dev->of_node, propname: "clock-names" , string: parent); |
107 | if (index < 0) |
108 | return ERR_PTR(error: index); |
109 | |
110 | parent_name = of_clk_get_parent_name(np: dev->of_node, index); |
111 | |
112 | ret = of_property_read_string_index(np: dev->of_node, |
113 | propname: "clock-output-names" , index: name_index, |
114 | output: &clk_name); |
115 | if (ret) |
116 | return ERR_PTR(error: ret); |
117 | |
118 | return clk_hw_register_gate(dev, clk_name, parent_name, |
119 | CLK_SET_RATE_PARENT, |
120 | regs + TCON_TOP_GATE_SRC_REG, |
121 | bit, 0, lock); |
122 | }; |
123 | |
124 | static int sun8i_tcon_top_bind(struct device *dev, struct device *master, |
125 | void *data) |
126 | { |
127 | struct platform_device *pdev = to_platform_device(dev); |
128 | struct clk_hw_onecell_data *clk_data; |
129 | struct sun8i_tcon_top *tcon_top; |
130 | const struct sun8i_tcon_top_quirks *quirks; |
131 | void __iomem *regs; |
132 | int ret, i; |
133 | |
134 | quirks = of_device_get_match_data(dev: &pdev->dev); |
135 | |
136 | tcon_top = devm_kzalloc(dev, size: sizeof(*tcon_top), GFP_KERNEL); |
137 | if (!tcon_top) |
138 | return -ENOMEM; |
139 | |
140 | clk_data = devm_kzalloc(dev, struct_size(clk_data, hws, CLK_NUM), |
141 | GFP_KERNEL); |
142 | if (!clk_data) |
143 | return -ENOMEM; |
144 | clk_data->num = CLK_NUM; |
145 | tcon_top->clk_data = clk_data; |
146 | |
147 | spin_lock_init(&tcon_top->reg_lock); |
148 | |
149 | tcon_top->rst = devm_reset_control_get(dev, NULL); |
150 | if (IS_ERR(ptr: tcon_top->rst)) { |
151 | dev_err(dev, "Couldn't get our reset line\n" ); |
152 | return PTR_ERR(ptr: tcon_top->rst); |
153 | } |
154 | |
155 | tcon_top->bus = devm_clk_get(dev, id: "bus" ); |
156 | if (IS_ERR(ptr: tcon_top->bus)) { |
157 | dev_err(dev, "Couldn't get the bus clock\n" ); |
158 | return PTR_ERR(ptr: tcon_top->bus); |
159 | } |
160 | |
161 | regs = devm_platform_ioremap_resource(pdev, index: 0); |
162 | tcon_top->regs = regs; |
163 | if (IS_ERR(ptr: regs)) |
164 | return PTR_ERR(ptr: regs); |
165 | |
166 | ret = reset_control_deassert(rstc: tcon_top->rst); |
167 | if (ret) { |
168 | dev_err(dev, "Could not deassert ctrl reset control\n" ); |
169 | return ret; |
170 | } |
171 | |
172 | ret = clk_prepare_enable(clk: tcon_top->bus); |
173 | if (ret) { |
174 | dev_err(dev, "Could not enable bus clock\n" ); |
175 | goto err_assert_reset; |
176 | } |
177 | |
178 | /* |
179 | * At least on H6, some registers have some bits set by default |
180 | * which may cause issues. Clear them here. |
181 | */ |
182 | writel(val: 0, addr: regs + TCON_TOP_PORT_SEL_REG); |
183 | writel(val: 0, addr: regs + TCON_TOP_GATE_SRC_REG); |
184 | |
185 | /* |
186 | * TCON TOP has two muxes, which select parent clock for each TCON TV |
187 | * channel clock. Parent could be either TCON TV or TVE clock. For now |
188 | * we leave this fixed to TCON TV, since TVE driver for R40 is not yet |
189 | * implemented. Once it is, graph needs to be traversed to determine |
190 | * if TVE is active on each TCON TV. If it is, mux should be switched |
191 | * to TVE clock parent. |
192 | */ |
193 | i = 0; |
194 | clk_data->hws[CLK_TCON_TOP_TV0] = |
195 | sun8i_tcon_top_register_gate(dev, parent: "tcon-tv0" , regs, |
196 | lock: &tcon_top->reg_lock, |
197 | TCON_TOP_TCON_TV0_GATE, name_index: i++); |
198 | |
199 | if (quirks->has_tcon_tv1) |
200 | clk_data->hws[CLK_TCON_TOP_TV1] = |
201 | sun8i_tcon_top_register_gate(dev, parent: "tcon-tv1" , regs, |
202 | lock: &tcon_top->reg_lock, |
203 | TCON_TOP_TCON_TV1_GATE, name_index: i++); |
204 | |
205 | if (quirks->has_dsi) |
206 | clk_data->hws[CLK_TCON_TOP_DSI] = |
207 | sun8i_tcon_top_register_gate(dev, parent: "dsi" , regs, |
208 | lock: &tcon_top->reg_lock, |
209 | TCON_TOP_TCON_DSI_GATE, name_index: i++); |
210 | |
211 | for (i = 0; i < CLK_NUM; i++) |
212 | if (IS_ERR(ptr: clk_data->hws[i])) { |
213 | ret = PTR_ERR(ptr: clk_data->hws[i]); |
214 | goto err_unregister_gates; |
215 | } |
216 | |
217 | ret = of_clk_add_hw_provider(np: dev->of_node, get: of_clk_hw_onecell_get, |
218 | data: clk_data); |
219 | if (ret) |
220 | goto err_unregister_gates; |
221 | |
222 | dev_set_drvdata(dev, data: tcon_top); |
223 | |
224 | return 0; |
225 | |
226 | err_unregister_gates: |
227 | for (i = 0; i < CLK_NUM; i++) |
228 | if (!IS_ERR_OR_NULL(ptr: clk_data->hws[i])) |
229 | clk_hw_unregister_gate(hw: clk_data->hws[i]); |
230 | clk_disable_unprepare(clk: tcon_top->bus); |
231 | err_assert_reset: |
232 | reset_control_assert(rstc: tcon_top->rst); |
233 | |
234 | return ret; |
235 | } |
236 | |
237 | static void sun8i_tcon_top_unbind(struct device *dev, struct device *master, |
238 | void *data) |
239 | { |
240 | struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev); |
241 | struct clk_hw_onecell_data *clk_data = tcon_top->clk_data; |
242 | int i; |
243 | |
244 | of_clk_del_provider(np: dev->of_node); |
245 | for (i = 0; i < CLK_NUM; i++) |
246 | if (clk_data->hws[i]) |
247 | clk_hw_unregister_gate(hw: clk_data->hws[i]); |
248 | |
249 | clk_disable_unprepare(clk: tcon_top->bus); |
250 | reset_control_assert(rstc: tcon_top->rst); |
251 | } |
252 | |
253 | static const struct component_ops sun8i_tcon_top_ops = { |
254 | .bind = sun8i_tcon_top_bind, |
255 | .unbind = sun8i_tcon_top_unbind, |
256 | }; |
257 | |
258 | static int sun8i_tcon_top_probe(struct platform_device *pdev) |
259 | { |
260 | return component_add(&pdev->dev, &sun8i_tcon_top_ops); |
261 | } |
262 | |
263 | static void sun8i_tcon_top_remove(struct platform_device *pdev) |
264 | { |
265 | component_del(&pdev->dev, &sun8i_tcon_top_ops); |
266 | } |
267 | |
268 | static const struct sun8i_tcon_top_quirks sun8i_r40_tcon_top_quirks = { |
269 | .has_tcon_tv1 = true, |
270 | .has_dsi = true, |
271 | }; |
272 | |
273 | static const struct sun8i_tcon_top_quirks sun20i_d1_tcon_top_quirks = { |
274 | .has_dsi = true, |
275 | }; |
276 | |
277 | static const struct sun8i_tcon_top_quirks sun50i_h6_tcon_top_quirks = { |
278 | /* Nothing special */ |
279 | }; |
280 | |
281 | /* sun4i_drv uses this list to check if a device node is a TCON TOP */ |
282 | const struct of_device_id sun8i_tcon_top_of_table[] = { |
283 | { |
284 | .compatible = "allwinner,sun8i-r40-tcon-top" , |
285 | .data = &sun8i_r40_tcon_top_quirks |
286 | }, |
287 | { |
288 | .compatible = "allwinner,sun20i-d1-tcon-top" , |
289 | .data = &sun20i_d1_tcon_top_quirks |
290 | }, |
291 | { |
292 | .compatible = "allwinner,sun50i-h6-tcon-top" , |
293 | .data = &sun50i_h6_tcon_top_quirks |
294 | }, |
295 | { /* sentinel */ } |
296 | }; |
297 | MODULE_DEVICE_TABLE(of, sun8i_tcon_top_of_table); |
298 | EXPORT_SYMBOL(sun8i_tcon_top_of_table); |
299 | |
300 | static struct platform_driver sun8i_tcon_top_platform_driver = { |
301 | .probe = sun8i_tcon_top_probe, |
302 | .remove_new = sun8i_tcon_top_remove, |
303 | .driver = { |
304 | .name = "sun8i-tcon-top" , |
305 | .of_match_table = sun8i_tcon_top_of_table, |
306 | }, |
307 | }; |
308 | module_platform_driver(sun8i_tcon_top_platform_driver); |
309 | |
310 | MODULE_AUTHOR("Jernej Skrabec <jernej.skrabec@siol.net>" ); |
311 | MODULE_DESCRIPTION("Allwinner R40 TCON TOP driver" ); |
312 | MODULE_LICENSE("GPL" ); |
313 | |