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