1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * Copyright (C) 2015 Free Electrons |
4 | * Copyright (C) 2015 NextThing Co |
5 | * |
6 | * Maxime Ripard <maxime.ripard@free-electrons.com> |
7 | */ |
8 | |
9 | #include <linux/component.h> |
10 | #include <linux/dma-mapping.h> |
11 | #include <linux/kfifo.h> |
12 | #include <linux/module.h> |
13 | #include <linux/of_graph.h> |
14 | #include <linux/of_reserved_mem.h> |
15 | #include <linux/platform_device.h> |
16 | |
17 | #include <drm/drm_aperture.h> |
18 | #include <drm/drm_atomic_helper.h> |
19 | #include <drm/drm_drv.h> |
20 | #include <drm/drm_fbdev_dma.h> |
21 | #include <drm/drm_gem_dma_helper.h> |
22 | #include <drm/drm_module.h> |
23 | #include <drm/drm_of.h> |
24 | #include <drm/drm_probe_helper.h> |
25 | #include <drm/drm_vblank.h> |
26 | |
27 | #include "sun4i_drv.h" |
28 | #include "sun4i_frontend.h" |
29 | #include "sun4i_framebuffer.h" |
30 | #include "sun4i_tcon.h" |
31 | #include "sun8i_tcon_top.h" |
32 | |
33 | static int drm_sun4i_gem_dumb_create(struct drm_file *file_priv, |
34 | struct drm_device *drm, |
35 | struct drm_mode_create_dumb *args) |
36 | { |
37 | /* The hardware only allows even pitches for YUV buffers. */ |
38 | args->pitch = ALIGN(DIV_ROUND_UP(args->width * args->bpp, 8), 2); |
39 | |
40 | return drm_gem_dma_dumb_create_internal(file_priv, drm, args); |
41 | } |
42 | |
43 | DEFINE_DRM_GEM_DMA_FOPS(sun4i_drv_fops); |
44 | |
45 | static const struct drm_driver sun4i_drv_driver = { |
46 | .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC, |
47 | |
48 | /* Generic Operations */ |
49 | .fops = &sun4i_drv_fops, |
50 | .name = "sun4i-drm" , |
51 | .desc = "Allwinner sun4i Display Engine" , |
52 | .date = "20150629" , |
53 | .major = 1, |
54 | .minor = 0, |
55 | |
56 | /* GEM Operations */ |
57 | DRM_GEM_DMA_DRIVER_OPS_WITH_DUMB_CREATE(drm_sun4i_gem_dumb_create), |
58 | }; |
59 | |
60 | static int sun4i_drv_bind(struct device *dev) |
61 | { |
62 | struct drm_device *drm; |
63 | struct sun4i_drv *drv; |
64 | int ret; |
65 | |
66 | drm = drm_dev_alloc(driver: &sun4i_drv_driver, parent: dev); |
67 | if (IS_ERR(ptr: drm)) |
68 | return PTR_ERR(ptr: drm); |
69 | |
70 | drv = devm_kzalloc(dev, size: sizeof(*drv), GFP_KERNEL); |
71 | if (!drv) { |
72 | ret = -ENOMEM; |
73 | goto free_drm; |
74 | } |
75 | |
76 | drm->dev_private = drv; |
77 | INIT_LIST_HEAD(list: &drv->frontend_list); |
78 | INIT_LIST_HEAD(list: &drv->engine_list); |
79 | INIT_LIST_HEAD(list: &drv->tcon_list); |
80 | |
81 | ret = of_reserved_mem_device_init(dev); |
82 | if (ret && ret != -ENODEV) { |
83 | dev_err(drm->dev, "Couldn't claim our memory region\n" ); |
84 | goto free_drm; |
85 | } |
86 | |
87 | drm_mode_config_init(dev: drm); |
88 | |
89 | ret = component_bind_all(parent: drm->dev, data: drm); |
90 | if (ret) { |
91 | dev_err(drm->dev, "Couldn't bind all pipelines components\n" ); |
92 | goto cleanup_mode_config; |
93 | } |
94 | |
95 | /* drm_vblank_init calls kcalloc, which can fail */ |
96 | ret = drm_vblank_init(dev: drm, num_crtcs: drm->mode_config.num_crtc); |
97 | if (ret) |
98 | goto unbind_all; |
99 | |
100 | /* Remove early framebuffers (ie. simplefb) */ |
101 | ret = drm_aperture_remove_framebuffers(req_driver: &sun4i_drv_driver); |
102 | if (ret) |
103 | goto unbind_all; |
104 | |
105 | sun4i_framebuffer_init(drm); |
106 | |
107 | /* Enable connectors polling */ |
108 | drm_kms_helper_poll_init(dev: drm); |
109 | |
110 | ret = drm_dev_register(dev: drm, flags: 0); |
111 | if (ret) |
112 | goto finish_poll; |
113 | |
114 | drm_fbdev_dma_setup(dev: drm, preferred_bpp: 32); |
115 | |
116 | dev_set_drvdata(dev, data: drm); |
117 | |
118 | return 0; |
119 | |
120 | finish_poll: |
121 | drm_kms_helper_poll_fini(dev: drm); |
122 | unbind_all: |
123 | component_unbind_all(parent: dev, NULL); |
124 | cleanup_mode_config: |
125 | drm_mode_config_cleanup(dev: drm); |
126 | of_reserved_mem_device_release(dev); |
127 | free_drm: |
128 | drm_dev_put(dev: drm); |
129 | return ret; |
130 | } |
131 | |
132 | static void sun4i_drv_unbind(struct device *dev) |
133 | { |
134 | struct drm_device *drm = dev_get_drvdata(dev); |
135 | |
136 | dev_set_drvdata(dev, NULL); |
137 | drm_dev_unregister(dev: drm); |
138 | drm_kms_helper_poll_fini(dev: drm); |
139 | drm_atomic_helper_shutdown(dev: drm); |
140 | drm_mode_config_cleanup(dev: drm); |
141 | |
142 | component_unbind_all(parent: dev, NULL); |
143 | of_reserved_mem_device_release(dev); |
144 | |
145 | drm_dev_put(dev: drm); |
146 | } |
147 | |
148 | static const struct component_master_ops sun4i_drv_master_ops = { |
149 | .bind = sun4i_drv_bind, |
150 | .unbind = sun4i_drv_unbind, |
151 | }; |
152 | |
153 | static bool sun4i_drv_node_is_connector(struct device_node *node) |
154 | { |
155 | return of_device_is_compatible(device: node, "hdmi-connector" ); |
156 | } |
157 | |
158 | static bool sun4i_drv_node_is_frontend(struct device_node *node) |
159 | { |
160 | return of_device_is_compatible(device: node, "allwinner,sun4i-a10-display-frontend" ) || |
161 | of_device_is_compatible(device: node, "allwinner,sun5i-a13-display-frontend" ) || |
162 | of_device_is_compatible(device: node, "allwinner,sun6i-a31-display-frontend" ) || |
163 | of_device_is_compatible(device: node, "allwinner,sun7i-a20-display-frontend" ) || |
164 | of_device_is_compatible(device: node, "allwinner,sun8i-a23-display-frontend" ) || |
165 | of_device_is_compatible(device: node, "allwinner,sun8i-a33-display-frontend" ) || |
166 | of_device_is_compatible(device: node, "allwinner,sun9i-a80-display-frontend" ); |
167 | } |
168 | |
169 | static bool sun4i_drv_node_is_deu(struct device_node *node) |
170 | { |
171 | return of_device_is_compatible(device: node, "allwinner,sun9i-a80-deu" ); |
172 | } |
173 | |
174 | static bool sun4i_drv_node_is_supported_frontend(struct device_node *node) |
175 | { |
176 | if (IS_ENABLED(CONFIG_DRM_SUN4I_BACKEND)) |
177 | return !!of_match_node(matches: sun4i_frontend_of_table, node); |
178 | |
179 | return false; |
180 | } |
181 | |
182 | static bool sun4i_drv_node_is_tcon(struct device_node *node) |
183 | { |
184 | return !!of_match_node(matches: sun4i_tcon_of_table, node); |
185 | } |
186 | |
187 | static bool sun4i_drv_node_is_tcon_with_ch0(struct device_node *node) |
188 | { |
189 | const struct of_device_id *match; |
190 | |
191 | match = of_match_node(matches: sun4i_tcon_of_table, node); |
192 | if (match) { |
193 | struct sun4i_tcon_quirks *quirks; |
194 | |
195 | quirks = (struct sun4i_tcon_quirks *)match->data; |
196 | |
197 | return quirks->has_channel_0; |
198 | } |
199 | |
200 | return false; |
201 | } |
202 | |
203 | static bool sun4i_drv_node_is_tcon_top(struct device_node *node) |
204 | { |
205 | return IS_ENABLED(CONFIG_DRM_SUN8I_TCON_TOP) && |
206 | !!of_match_node(matches: sun8i_tcon_top_of_table, node); |
207 | } |
208 | |
209 | /* |
210 | * The encoder drivers use drm_of_find_possible_crtcs to get upstream |
211 | * crtcs from the device tree using of_graph. For the results to be |
212 | * correct, encoders must be probed/bound after _all_ crtcs have been |
213 | * created. The existing code uses a depth first recursive traversal |
214 | * of the of_graph, which means the encoders downstream of the TCON |
215 | * get add right after the first TCON. The second TCON or CRTC will |
216 | * never be properly associated with encoders connected to it. |
217 | * |
218 | * Also, in a dual display pipeline setup, both frontends can feed |
219 | * either backend, and both backends can feed either TCON, we want |
220 | * all components of the same type to be added before the next type |
221 | * in the pipeline. Fortunately, the pipelines are perfectly symmetric, |
222 | * i.e. components of the same type are at the same depth when counted |
223 | * from the frontend. The only exception is the third pipeline in |
224 | * the A80 SoC, which we do not support anyway. |
225 | * |
226 | * Hence we can use a breadth first search traversal order to add |
227 | * components. We do not need to check for duplicates. The component |
228 | * matching system handles this for us. |
229 | */ |
230 | struct endpoint_list { |
231 | DECLARE_KFIFO(fifo, struct device_node *, 16); |
232 | }; |
233 | |
234 | static void sun4i_drv_traverse_endpoints(struct endpoint_list *list, |
235 | struct device_node *node, |
236 | int port_id) |
237 | { |
238 | struct device_node *ep, *remote, *port; |
239 | |
240 | port = of_graph_get_port_by_id(node, id: port_id); |
241 | if (!port) { |
242 | DRM_DEBUG_DRIVER("No output to bind on port %d\n" , port_id); |
243 | return; |
244 | } |
245 | |
246 | for_each_available_child_of_node(port, ep) { |
247 | remote = of_graph_get_remote_port_parent(node: ep); |
248 | if (!remote) { |
249 | DRM_DEBUG_DRIVER("Error retrieving the output node\n" ); |
250 | continue; |
251 | } |
252 | |
253 | if (sun4i_drv_node_is_tcon(node)) { |
254 | /* |
255 | * TCON TOP is always probed before TCON. However, TCON |
256 | * points back to TCON TOP when it is source for HDMI. |
257 | * We have to skip it here to prevent infinite looping |
258 | * between TCON TOP and TCON. |
259 | */ |
260 | if (sun4i_drv_node_is_tcon_top(node: remote)) { |
261 | DRM_DEBUG_DRIVER("TCON output endpoint is TCON TOP... skipping\n" ); |
262 | of_node_put(node: remote); |
263 | continue; |
264 | } |
265 | |
266 | /* |
267 | * If the node is our TCON with channel 0, the first |
268 | * port is used for panel or bridges, and will not be |
269 | * part of the component framework. |
270 | */ |
271 | if (sun4i_drv_node_is_tcon_with_ch0(node)) { |
272 | struct of_endpoint endpoint; |
273 | |
274 | if (of_graph_parse_endpoint(node: ep, endpoint: &endpoint)) { |
275 | DRM_DEBUG_DRIVER("Couldn't parse endpoint\n" ); |
276 | of_node_put(node: remote); |
277 | continue; |
278 | } |
279 | |
280 | if (!endpoint.id) { |
281 | DRM_DEBUG_DRIVER("Endpoint is our panel... skipping\n" ); |
282 | of_node_put(node: remote); |
283 | continue; |
284 | } |
285 | } |
286 | } |
287 | |
288 | kfifo_put(&list->fifo, remote); |
289 | } |
290 | } |
291 | |
292 | static int sun4i_drv_add_endpoints(struct device *dev, |
293 | struct endpoint_list *list, |
294 | struct component_match **match, |
295 | struct device_node *node) |
296 | { |
297 | int count = 0; |
298 | |
299 | /* |
300 | * The frontend has been disabled in some of our old device |
301 | * trees. If we find a node that is the frontend and is |
302 | * disabled, we should just follow through and parse its |
303 | * child, but without adding it to the component list. |
304 | * Otherwise, we obviously want to add it to the list. |
305 | */ |
306 | if (!sun4i_drv_node_is_frontend(node) && |
307 | !of_device_is_available(device: node)) |
308 | return 0; |
309 | |
310 | /* |
311 | * The connectors will be the last nodes in our pipeline, we |
312 | * can just bail out. |
313 | */ |
314 | if (sun4i_drv_node_is_connector(node)) |
315 | return 0; |
316 | |
317 | /* |
318 | * If the device is either just a regular device, or an |
319 | * enabled frontend supported by the driver, we add it to our |
320 | * component list. |
321 | */ |
322 | if (!(sun4i_drv_node_is_frontend(node) || |
323 | sun4i_drv_node_is_deu(node)) || |
324 | (sun4i_drv_node_is_supported_frontend(node) && |
325 | of_device_is_available(device: node))) { |
326 | /* Add current component */ |
327 | DRM_DEBUG_DRIVER("Adding component %pOF\n" , node); |
328 | drm_of_component_match_add(master: dev, matchptr: match, compare: component_compare_of, node); |
329 | count++; |
330 | } |
331 | |
332 | /* each node has at least one output */ |
333 | sun4i_drv_traverse_endpoints(list, node, port_id: 1); |
334 | |
335 | /* TCON TOP has second and third output */ |
336 | if (sun4i_drv_node_is_tcon_top(node)) { |
337 | sun4i_drv_traverse_endpoints(list, node, port_id: 3); |
338 | sun4i_drv_traverse_endpoints(list, node, port_id: 5); |
339 | } |
340 | |
341 | return count; |
342 | } |
343 | |
344 | #ifdef CONFIG_PM_SLEEP |
345 | static int sun4i_drv_drm_sys_suspend(struct device *dev) |
346 | { |
347 | struct drm_device *drm = dev_get_drvdata(dev); |
348 | |
349 | return drm_mode_config_helper_suspend(dev: drm); |
350 | } |
351 | |
352 | static int sun4i_drv_drm_sys_resume(struct device *dev) |
353 | { |
354 | struct drm_device *drm = dev_get_drvdata(dev); |
355 | |
356 | return drm_mode_config_helper_resume(dev: drm); |
357 | } |
358 | #endif |
359 | |
360 | static const struct dev_pm_ops sun4i_drv_drm_pm_ops = { |
361 | SET_SYSTEM_SLEEP_PM_OPS(sun4i_drv_drm_sys_suspend, |
362 | sun4i_drv_drm_sys_resume) |
363 | }; |
364 | |
365 | static int sun4i_drv_probe(struct platform_device *pdev) |
366 | { |
367 | struct component_match *match = NULL; |
368 | struct device_node *np = pdev->dev.of_node, *endpoint; |
369 | struct endpoint_list list; |
370 | int i, ret, count = 0; |
371 | |
372 | INIT_KFIFO(list.fifo); |
373 | |
374 | /* |
375 | * DE2 and DE3 cores actually supports 40-bit addresses, but |
376 | * driver does not. |
377 | */ |
378 | dma_set_mask_and_coherent(dev: &pdev->dev, DMA_BIT_MASK(32)); |
379 | dma_set_max_seg_size(dev: &pdev->dev, UINT_MAX); |
380 | |
381 | for (i = 0;; i++) { |
382 | struct device_node *pipeline = of_parse_phandle(np, |
383 | phandle_name: "allwinner,pipelines" , |
384 | index: i); |
385 | if (!pipeline) |
386 | break; |
387 | |
388 | kfifo_put(&list.fifo, pipeline); |
389 | } |
390 | |
391 | while (kfifo_get(&list.fifo, &endpoint)) { |
392 | /* process this endpoint */ |
393 | ret = sun4i_drv_add_endpoints(dev: &pdev->dev, list: &list, match: &match, |
394 | node: endpoint); |
395 | |
396 | /* sun4i_drv_add_endpoints can fail to allocate memory */ |
397 | if (ret < 0) |
398 | return ret; |
399 | |
400 | count += ret; |
401 | } |
402 | |
403 | if (count) |
404 | return component_master_add_with_match(&pdev->dev, |
405 | &sun4i_drv_master_ops, |
406 | match); |
407 | else |
408 | return 0; |
409 | } |
410 | |
411 | static void sun4i_drv_remove(struct platform_device *pdev) |
412 | { |
413 | component_master_del(&pdev->dev, &sun4i_drv_master_ops); |
414 | } |
415 | |
416 | static void sun4i_drv_shutdown(struct platform_device *pdev) |
417 | { |
418 | drm_atomic_helper_shutdown(dev: platform_get_drvdata(pdev)); |
419 | } |
420 | |
421 | static const struct of_device_id sun4i_drv_of_table[] = { |
422 | { .compatible = "allwinner,sun4i-a10-display-engine" }, |
423 | { .compatible = "allwinner,sun5i-a10s-display-engine" }, |
424 | { .compatible = "allwinner,sun5i-a13-display-engine" }, |
425 | { .compatible = "allwinner,sun6i-a31-display-engine" }, |
426 | { .compatible = "allwinner,sun6i-a31s-display-engine" }, |
427 | { .compatible = "allwinner,sun7i-a20-display-engine" }, |
428 | { .compatible = "allwinner,sun8i-a23-display-engine" }, |
429 | { .compatible = "allwinner,sun8i-a33-display-engine" }, |
430 | { .compatible = "allwinner,sun8i-a83t-display-engine" }, |
431 | { .compatible = "allwinner,sun8i-h3-display-engine" }, |
432 | { .compatible = "allwinner,sun8i-r40-display-engine" }, |
433 | { .compatible = "allwinner,sun8i-v3s-display-engine" }, |
434 | { .compatible = "allwinner,sun9i-a80-display-engine" }, |
435 | { .compatible = "allwinner,sun20i-d1-display-engine" }, |
436 | { .compatible = "allwinner,sun50i-a64-display-engine" }, |
437 | { .compatible = "allwinner,sun50i-h6-display-engine" }, |
438 | { } |
439 | }; |
440 | MODULE_DEVICE_TABLE(of, sun4i_drv_of_table); |
441 | |
442 | static struct platform_driver sun4i_drv_platform_driver = { |
443 | .probe = sun4i_drv_probe, |
444 | .remove_new = sun4i_drv_remove, |
445 | .shutdown = sun4i_drv_shutdown, |
446 | .driver = { |
447 | .name = "sun4i-drm" , |
448 | .of_match_table = sun4i_drv_of_table, |
449 | .pm = &sun4i_drv_drm_pm_ops, |
450 | }, |
451 | }; |
452 | drm_module_platform_driver(sun4i_drv_platform_driver); |
453 | |
454 | MODULE_AUTHOR("Boris Brezillon <boris.brezillon@free-electrons.com>" ); |
455 | MODULE_AUTHOR("Maxime Ripard <maxime.ripard@free-electrons.com>" ); |
456 | MODULE_DESCRIPTION("Allwinner A10 Display Engine DRM/KMS Driver" ); |
457 | MODULE_LICENSE("GPL" ); |
458 | |