1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Copyright (C) 2012 Texas Instruments |
4 | * Author: Rob Clark <robdclark@gmail.com> |
5 | */ |
6 | |
7 | /* LCDC DRM driver, based on da8xx-fb */ |
8 | |
9 | #include <linux/component.h> |
10 | #include <linux/mod_devicetable.h> |
11 | #include <linux/module.h> |
12 | #include <linux/pinctrl/consumer.h> |
13 | #include <linux/platform_device.h> |
14 | #include <linux/pm_runtime.h> |
15 | |
16 | #include <drm/drm_atomic_helper.h> |
17 | #include <drm/drm_debugfs.h> |
18 | #include <drm/drm_drv.h> |
19 | #include <drm/drm_fbdev_dma.h> |
20 | #include <drm/drm_fourcc.h> |
21 | #include <drm/drm_gem_dma_helper.h> |
22 | #include <drm/drm_gem_framebuffer_helper.h> |
23 | #include <drm/drm_mm.h> |
24 | #include <drm/drm_probe_helper.h> |
25 | #include <drm/drm_vblank.h> |
26 | |
27 | |
28 | #include "tilcdc_drv.h" |
29 | #include "tilcdc_external.h" |
30 | #include "tilcdc_panel.h" |
31 | #include "tilcdc_regs.h" |
32 | |
33 | static LIST_HEAD(module_list); |
34 | |
35 | static const u32 tilcdc_rev1_formats[] = { DRM_FORMAT_RGB565 }; |
36 | |
37 | static const u32 tilcdc_straight_formats[] = { DRM_FORMAT_RGB565, |
38 | DRM_FORMAT_BGR888, |
39 | DRM_FORMAT_XBGR8888 }; |
40 | |
41 | static const u32 tilcdc_crossed_formats[] = { DRM_FORMAT_BGR565, |
42 | DRM_FORMAT_RGB888, |
43 | DRM_FORMAT_XRGB8888 }; |
44 | |
45 | static const u32 tilcdc_legacy_formats[] = { DRM_FORMAT_RGB565, |
46 | DRM_FORMAT_RGB888, |
47 | DRM_FORMAT_XRGB8888 }; |
48 | |
49 | void tilcdc_module_init(struct tilcdc_module *mod, const char *name, |
50 | const struct tilcdc_module_ops *funcs) |
51 | { |
52 | mod->name = name; |
53 | mod->funcs = funcs; |
54 | INIT_LIST_HEAD(list: &mod->list); |
55 | list_add(new: &mod->list, head: &module_list); |
56 | } |
57 | |
58 | void tilcdc_module_cleanup(struct tilcdc_module *mod) |
59 | { |
60 | list_del(entry: &mod->list); |
61 | } |
62 | |
63 | static int tilcdc_atomic_check(struct drm_device *dev, |
64 | struct drm_atomic_state *state) |
65 | { |
66 | int ret; |
67 | |
68 | ret = drm_atomic_helper_check_modeset(dev, state); |
69 | if (ret) |
70 | return ret; |
71 | |
72 | ret = drm_atomic_helper_check_planes(dev, state); |
73 | if (ret) |
74 | return ret; |
75 | |
76 | /* |
77 | * tilcdc ->atomic_check can update ->mode_changed if pixel format |
78 | * changes, hence will we check modeset changes again. |
79 | */ |
80 | ret = drm_atomic_helper_check_modeset(dev, state); |
81 | if (ret) |
82 | return ret; |
83 | |
84 | return ret; |
85 | } |
86 | |
87 | static const struct drm_mode_config_funcs mode_config_funcs = { |
88 | .fb_create = drm_gem_fb_create, |
89 | .atomic_check = tilcdc_atomic_check, |
90 | .atomic_commit = drm_atomic_helper_commit, |
91 | }; |
92 | |
93 | static void modeset_init(struct drm_device *dev) |
94 | { |
95 | struct tilcdc_drm_private *priv = dev->dev_private; |
96 | struct tilcdc_module *mod; |
97 | |
98 | list_for_each_entry(mod, &module_list, list) { |
99 | DBG("loading module: %s" , mod->name); |
100 | mod->funcs->modeset_init(mod, dev); |
101 | } |
102 | |
103 | dev->mode_config.min_width = 0; |
104 | dev->mode_config.min_height = 0; |
105 | dev->mode_config.max_width = priv->max_width; |
106 | dev->mode_config.max_height = 2048; |
107 | dev->mode_config.funcs = &mode_config_funcs; |
108 | } |
109 | |
110 | #ifdef CONFIG_CPU_FREQ |
111 | static int cpufreq_transition(struct notifier_block *nb, |
112 | unsigned long val, void *data) |
113 | { |
114 | struct tilcdc_drm_private *priv = container_of(nb, |
115 | struct tilcdc_drm_private, freq_transition); |
116 | |
117 | if (val == CPUFREQ_POSTCHANGE) |
118 | tilcdc_crtc_update_clk(crtc: priv->crtc); |
119 | |
120 | return 0; |
121 | } |
122 | #endif |
123 | |
124 | static irqreturn_t tilcdc_irq(int irq, void *arg) |
125 | { |
126 | struct drm_device *dev = arg; |
127 | struct tilcdc_drm_private *priv = dev->dev_private; |
128 | |
129 | return tilcdc_crtc_irq(crtc: priv->crtc); |
130 | } |
131 | |
132 | static int tilcdc_irq_install(struct drm_device *dev, unsigned int irq) |
133 | { |
134 | struct tilcdc_drm_private *priv = dev->dev_private; |
135 | int ret; |
136 | |
137 | ret = request_irq(irq, handler: tilcdc_irq, flags: 0, name: dev->driver->name, dev); |
138 | if (ret) |
139 | return ret; |
140 | |
141 | priv->irq_enabled = true; |
142 | |
143 | return 0; |
144 | } |
145 | |
146 | static void tilcdc_irq_uninstall(struct drm_device *dev) |
147 | { |
148 | struct tilcdc_drm_private *priv = dev->dev_private; |
149 | |
150 | if (!priv->irq_enabled) |
151 | return; |
152 | |
153 | free_irq(priv->irq, dev); |
154 | priv->irq_enabled = false; |
155 | } |
156 | |
157 | /* |
158 | * DRM operations: |
159 | */ |
160 | |
161 | static void tilcdc_fini(struct drm_device *dev) |
162 | { |
163 | struct tilcdc_drm_private *priv = dev->dev_private; |
164 | |
165 | #ifdef CONFIG_CPU_FREQ |
166 | if (priv->freq_transition.notifier_call) |
167 | cpufreq_unregister_notifier(nb: &priv->freq_transition, |
168 | CPUFREQ_TRANSITION_NOTIFIER); |
169 | #endif |
170 | |
171 | if (priv->crtc) |
172 | tilcdc_crtc_shutdown(crtc: priv->crtc); |
173 | |
174 | if (priv->is_registered) |
175 | drm_dev_unregister(dev); |
176 | |
177 | drm_kms_helper_poll_fini(dev); |
178 | drm_atomic_helper_shutdown(dev); |
179 | tilcdc_irq_uninstall(dev); |
180 | drm_mode_config_cleanup(dev); |
181 | |
182 | if (priv->clk) |
183 | clk_put(clk: priv->clk); |
184 | |
185 | if (priv->wq) |
186 | destroy_workqueue(wq: priv->wq); |
187 | |
188 | dev->dev_private = NULL; |
189 | |
190 | pm_runtime_disable(dev: dev->dev); |
191 | |
192 | drm_dev_put(dev); |
193 | } |
194 | |
195 | static int tilcdc_init(const struct drm_driver *ddrv, struct device *dev) |
196 | { |
197 | struct drm_device *ddev; |
198 | struct platform_device *pdev = to_platform_device(dev); |
199 | struct device_node *node = dev->of_node; |
200 | struct tilcdc_drm_private *priv; |
201 | u32 bpp = 0; |
202 | int ret; |
203 | |
204 | priv = devm_kzalloc(dev, size: sizeof(*priv), GFP_KERNEL); |
205 | if (!priv) |
206 | return -ENOMEM; |
207 | |
208 | ddev = drm_dev_alloc(driver: ddrv, parent: dev); |
209 | if (IS_ERR(ptr: ddev)) |
210 | return PTR_ERR(ptr: ddev); |
211 | |
212 | ddev->dev_private = priv; |
213 | platform_set_drvdata(pdev, data: ddev); |
214 | drm_mode_config_init(dev: ddev); |
215 | |
216 | priv->is_componentized = |
217 | tilcdc_get_external_components(dev, NULL) > 0; |
218 | |
219 | priv->wq = alloc_ordered_workqueue("tilcdc" , 0); |
220 | if (!priv->wq) { |
221 | ret = -ENOMEM; |
222 | goto init_failed; |
223 | } |
224 | |
225 | priv->mmio = devm_platform_ioremap_resource(pdev, index: 0); |
226 | if (IS_ERR(ptr: priv->mmio)) { |
227 | dev_err(dev, "failed to request / ioremap\n" ); |
228 | ret = PTR_ERR(ptr: priv->mmio); |
229 | goto init_failed; |
230 | } |
231 | |
232 | priv->clk = clk_get(dev, id: "fck" ); |
233 | if (IS_ERR(ptr: priv->clk)) { |
234 | dev_err(dev, "failed to get functional clock\n" ); |
235 | ret = -ENODEV; |
236 | goto init_failed; |
237 | } |
238 | |
239 | pm_runtime_enable(dev); |
240 | |
241 | /* Determine LCD IP Version */ |
242 | pm_runtime_get_sync(dev); |
243 | switch (tilcdc_read(dev: ddev, LCDC_PID_REG)) { |
244 | case 0x4c100102: |
245 | priv->rev = 1; |
246 | break; |
247 | case 0x4f200800: |
248 | case 0x4f201000: |
249 | priv->rev = 2; |
250 | break; |
251 | default: |
252 | dev_warn(dev, "Unknown PID Reg value 0x%08x, " |
253 | "defaulting to LCD revision 1\n" , |
254 | tilcdc_read(ddev, LCDC_PID_REG)); |
255 | priv->rev = 1; |
256 | break; |
257 | } |
258 | |
259 | pm_runtime_put_sync(dev); |
260 | |
261 | if (priv->rev == 1) { |
262 | DBG("Revision 1 LCDC supports only RGB565 format" ); |
263 | priv->pixelformats = tilcdc_rev1_formats; |
264 | priv->num_pixelformats = ARRAY_SIZE(tilcdc_rev1_formats); |
265 | bpp = 16; |
266 | } else { |
267 | const char *str = "\0" ; |
268 | |
269 | of_property_read_string(np: node, propname: "blue-and-red-wiring" , out_string: &str); |
270 | if (0 == strcmp(str, "crossed" )) { |
271 | DBG("Configured for crossed blue and red wires" ); |
272 | priv->pixelformats = tilcdc_crossed_formats; |
273 | priv->num_pixelformats = |
274 | ARRAY_SIZE(tilcdc_crossed_formats); |
275 | bpp = 32; /* Choose bpp with RGB support for fbdef */ |
276 | } else if (0 == strcmp(str, "straight" )) { |
277 | DBG("Configured for straight blue and red wires" ); |
278 | priv->pixelformats = tilcdc_straight_formats; |
279 | priv->num_pixelformats = |
280 | ARRAY_SIZE(tilcdc_straight_formats); |
281 | bpp = 16; /* Choose bpp with RGB support for fbdef */ |
282 | } else { |
283 | DBG("Blue and red wiring '%s' unknown, use legacy mode" , |
284 | str); |
285 | priv->pixelformats = tilcdc_legacy_formats; |
286 | priv->num_pixelformats = |
287 | ARRAY_SIZE(tilcdc_legacy_formats); |
288 | bpp = 16; /* This is just a guess */ |
289 | } |
290 | } |
291 | |
292 | if (of_property_read_u32(np: node, propname: "max-bandwidth" , out_value: &priv->max_bandwidth)) |
293 | priv->max_bandwidth = TILCDC_DEFAULT_MAX_BANDWIDTH; |
294 | |
295 | DBG("Maximum Bandwidth Value %d" , priv->max_bandwidth); |
296 | |
297 | if (of_property_read_u32(np: node, propname: "max-width" , out_value: &priv->max_width)) { |
298 | if (priv->rev == 1) |
299 | priv->max_width = TILCDC_DEFAULT_MAX_WIDTH_V1; |
300 | else |
301 | priv->max_width = TILCDC_DEFAULT_MAX_WIDTH_V2; |
302 | } |
303 | |
304 | DBG("Maximum Horizontal Pixel Width Value %dpixels" , priv->max_width); |
305 | |
306 | if (of_property_read_u32(np: node, propname: "max-pixelclock" , |
307 | out_value: &priv->max_pixelclock)) |
308 | priv->max_pixelclock = TILCDC_DEFAULT_MAX_PIXELCLOCK; |
309 | |
310 | DBG("Maximum Pixel Clock Value %dKHz" , priv->max_pixelclock); |
311 | |
312 | ret = tilcdc_crtc_create(dev: ddev); |
313 | if (ret < 0) { |
314 | dev_err(dev, "failed to create crtc\n" ); |
315 | goto init_failed; |
316 | } |
317 | modeset_init(dev: ddev); |
318 | |
319 | #ifdef CONFIG_CPU_FREQ |
320 | priv->freq_transition.notifier_call = cpufreq_transition; |
321 | ret = cpufreq_register_notifier(nb: &priv->freq_transition, |
322 | CPUFREQ_TRANSITION_NOTIFIER); |
323 | if (ret) { |
324 | dev_err(dev, "failed to register cpufreq notifier\n" ); |
325 | priv->freq_transition.notifier_call = NULL; |
326 | goto init_failed; |
327 | } |
328 | #endif |
329 | |
330 | if (priv->is_componentized) { |
331 | ret = component_bind_all(parent: dev, data: ddev); |
332 | if (ret < 0) |
333 | goto init_failed; |
334 | |
335 | ret = tilcdc_add_component_encoder(dev: ddev); |
336 | if (ret < 0) |
337 | goto init_failed; |
338 | } else { |
339 | ret = tilcdc_attach_external_device(ddev); |
340 | if (ret) |
341 | goto init_failed; |
342 | } |
343 | |
344 | if (!priv->external_connector && |
345 | ((priv->num_encoders == 0) || (priv->num_connectors == 0))) { |
346 | dev_err(dev, "no encoders/connectors found\n" ); |
347 | ret = -EPROBE_DEFER; |
348 | goto init_failed; |
349 | } |
350 | |
351 | ret = drm_vblank_init(dev: ddev, num_crtcs: 1); |
352 | if (ret < 0) { |
353 | dev_err(dev, "failed to initialize vblank\n" ); |
354 | goto init_failed; |
355 | } |
356 | |
357 | ret = platform_get_irq(pdev, 0); |
358 | if (ret < 0) |
359 | goto init_failed; |
360 | priv->irq = ret; |
361 | |
362 | ret = tilcdc_irq_install(dev: ddev, irq: priv->irq); |
363 | if (ret < 0) { |
364 | dev_err(dev, "failed to install IRQ handler\n" ); |
365 | goto init_failed; |
366 | } |
367 | |
368 | drm_mode_config_reset(dev: ddev); |
369 | |
370 | drm_kms_helper_poll_init(dev: ddev); |
371 | |
372 | ret = drm_dev_register(dev: ddev, flags: 0); |
373 | if (ret) |
374 | goto init_failed; |
375 | priv->is_registered = true; |
376 | |
377 | drm_fbdev_dma_setup(dev: ddev, preferred_bpp: bpp); |
378 | return 0; |
379 | |
380 | init_failed: |
381 | tilcdc_fini(dev: ddev); |
382 | platform_set_drvdata(pdev, NULL); |
383 | |
384 | return ret; |
385 | } |
386 | |
387 | #if defined(CONFIG_DEBUG_FS) |
388 | static const struct { |
389 | const char *name; |
390 | uint8_t rev; |
391 | uint8_t save; |
392 | uint32_t reg; |
393 | } registers[] = { |
394 | #define REG(rev, save, reg) { #reg, rev, save, reg } |
395 | /* exists in revision 1: */ |
396 | REG(1, false, LCDC_PID_REG), |
397 | REG(1, true, LCDC_CTRL_REG), |
398 | REG(1, false, LCDC_STAT_REG), |
399 | REG(1, true, LCDC_RASTER_CTRL_REG), |
400 | REG(1, true, LCDC_RASTER_TIMING_0_REG), |
401 | REG(1, true, LCDC_RASTER_TIMING_1_REG), |
402 | REG(1, true, LCDC_RASTER_TIMING_2_REG), |
403 | REG(1, true, LCDC_DMA_CTRL_REG), |
404 | REG(1, true, LCDC_DMA_FB_BASE_ADDR_0_REG), |
405 | REG(1, true, LCDC_DMA_FB_CEILING_ADDR_0_REG), |
406 | REG(1, true, LCDC_DMA_FB_BASE_ADDR_1_REG), |
407 | REG(1, true, LCDC_DMA_FB_CEILING_ADDR_1_REG), |
408 | /* new in revision 2: */ |
409 | REG(2, false, LCDC_RAW_STAT_REG), |
410 | REG(2, false, LCDC_MASKED_STAT_REG), |
411 | REG(2, true, LCDC_INT_ENABLE_SET_REG), |
412 | REG(2, false, LCDC_INT_ENABLE_CLR_REG), |
413 | REG(2, false, LCDC_END_OF_INT_IND_REG), |
414 | REG(2, true, LCDC_CLK_ENABLE_REG), |
415 | #undef REG |
416 | }; |
417 | |
418 | #endif |
419 | |
420 | #ifdef CONFIG_DEBUG_FS |
421 | static int tilcdc_regs_show(struct seq_file *m, void *arg) |
422 | { |
423 | struct drm_info_node *node = (struct drm_info_node *) m->private; |
424 | struct drm_device *dev = node->minor->dev; |
425 | struct tilcdc_drm_private *priv = dev->dev_private; |
426 | unsigned i; |
427 | |
428 | pm_runtime_get_sync(dev: dev->dev); |
429 | |
430 | seq_printf(m, fmt: "revision: %d\n" , priv->rev); |
431 | |
432 | for (i = 0; i < ARRAY_SIZE(registers); i++) |
433 | if (priv->rev >= registers[i].rev) |
434 | seq_printf(m, fmt: "%s:\t %08x\n" , registers[i].name, |
435 | tilcdc_read(dev, reg: registers[i].reg)); |
436 | |
437 | pm_runtime_put_sync(dev: dev->dev); |
438 | |
439 | return 0; |
440 | } |
441 | |
442 | static int tilcdc_mm_show(struct seq_file *m, void *arg) |
443 | { |
444 | struct drm_info_node *node = (struct drm_info_node *) m->private; |
445 | struct drm_device *dev = node->minor->dev; |
446 | struct drm_printer p = drm_seq_file_printer(f: m); |
447 | drm_mm_print(mm: &dev->vma_offset_manager->vm_addr_space_mm, p: &p); |
448 | return 0; |
449 | } |
450 | |
451 | static struct drm_info_list tilcdc_debugfs_list[] = { |
452 | { "regs" , tilcdc_regs_show, 0, NULL }, |
453 | { "mm" , tilcdc_mm_show, 0, NULL }, |
454 | }; |
455 | |
456 | static void tilcdc_debugfs_init(struct drm_minor *minor) |
457 | { |
458 | struct tilcdc_module *mod; |
459 | |
460 | drm_debugfs_create_files(files: tilcdc_debugfs_list, |
461 | ARRAY_SIZE(tilcdc_debugfs_list), |
462 | root: minor->debugfs_root, minor); |
463 | |
464 | list_for_each_entry(mod, &module_list, list) |
465 | if (mod->funcs->debugfs_init) |
466 | mod->funcs->debugfs_init(mod, minor); |
467 | } |
468 | #endif |
469 | |
470 | DEFINE_DRM_GEM_DMA_FOPS(fops); |
471 | |
472 | static const struct drm_driver tilcdc_driver = { |
473 | .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC, |
474 | DRM_GEM_DMA_DRIVER_OPS, |
475 | #ifdef CONFIG_DEBUG_FS |
476 | .debugfs_init = tilcdc_debugfs_init, |
477 | #endif |
478 | .fops = &fops, |
479 | .name = "tilcdc" , |
480 | .desc = "TI LCD Controller DRM" , |
481 | .date = "20121205" , |
482 | .major = 1, |
483 | .minor = 0, |
484 | }; |
485 | |
486 | /* |
487 | * Power management: |
488 | */ |
489 | |
490 | static int tilcdc_pm_suspend(struct device *dev) |
491 | { |
492 | struct drm_device *ddev = dev_get_drvdata(dev); |
493 | int ret = 0; |
494 | |
495 | ret = drm_mode_config_helper_suspend(dev: ddev); |
496 | |
497 | /* Select sleep pin state */ |
498 | pinctrl_pm_select_sleep_state(dev); |
499 | |
500 | return ret; |
501 | } |
502 | |
503 | static int tilcdc_pm_resume(struct device *dev) |
504 | { |
505 | struct drm_device *ddev = dev_get_drvdata(dev); |
506 | |
507 | /* Select default pin state */ |
508 | pinctrl_pm_select_default_state(dev); |
509 | return drm_mode_config_helper_resume(dev: ddev); |
510 | } |
511 | |
512 | static DEFINE_SIMPLE_DEV_PM_OPS(tilcdc_pm_ops, |
513 | tilcdc_pm_suspend, tilcdc_pm_resume); |
514 | |
515 | /* |
516 | * Platform driver: |
517 | */ |
518 | static int tilcdc_bind(struct device *dev) |
519 | { |
520 | return tilcdc_init(ddrv: &tilcdc_driver, dev); |
521 | } |
522 | |
523 | static void tilcdc_unbind(struct device *dev) |
524 | { |
525 | struct drm_device *ddev = dev_get_drvdata(dev); |
526 | |
527 | /* Check if a subcomponent has already triggered the unloading. */ |
528 | if (!ddev->dev_private) |
529 | return; |
530 | |
531 | tilcdc_fini(dev: ddev); |
532 | dev_set_drvdata(dev, NULL); |
533 | } |
534 | |
535 | static const struct component_master_ops tilcdc_comp_ops = { |
536 | .bind = tilcdc_bind, |
537 | .unbind = tilcdc_unbind, |
538 | }; |
539 | |
540 | static int tilcdc_pdev_probe(struct platform_device *pdev) |
541 | { |
542 | struct component_match *match = NULL; |
543 | int ret; |
544 | |
545 | /* bail out early if no DT data: */ |
546 | if (!pdev->dev.of_node) { |
547 | dev_err(&pdev->dev, "device-tree data is missing\n" ); |
548 | return -ENXIO; |
549 | } |
550 | |
551 | ret = tilcdc_get_external_components(dev: &pdev->dev, match: &match); |
552 | if (ret < 0) |
553 | return ret; |
554 | else if (ret == 0) |
555 | return tilcdc_init(ddrv: &tilcdc_driver, dev: &pdev->dev); |
556 | else |
557 | return component_master_add_with_match(&pdev->dev, |
558 | &tilcdc_comp_ops, |
559 | match); |
560 | } |
561 | |
562 | static void tilcdc_pdev_remove(struct platform_device *pdev) |
563 | { |
564 | int ret; |
565 | |
566 | ret = tilcdc_get_external_components(dev: &pdev->dev, NULL); |
567 | if (ret < 0) |
568 | dev_err(&pdev->dev, "tilcdc_get_external_components() failed (%pe)\n" , |
569 | ERR_PTR(ret)); |
570 | else if (ret == 0) |
571 | tilcdc_fini(dev: platform_get_drvdata(pdev)); |
572 | else |
573 | component_master_del(&pdev->dev, &tilcdc_comp_ops); |
574 | } |
575 | |
576 | static void tilcdc_pdev_shutdown(struct platform_device *pdev) |
577 | { |
578 | drm_atomic_helper_shutdown(dev: platform_get_drvdata(pdev)); |
579 | } |
580 | |
581 | static const struct of_device_id tilcdc_of_match[] = { |
582 | { .compatible = "ti,am33xx-tilcdc" , }, |
583 | { .compatible = "ti,da850-tilcdc" , }, |
584 | { }, |
585 | }; |
586 | MODULE_DEVICE_TABLE(of, tilcdc_of_match); |
587 | |
588 | static struct platform_driver tilcdc_platform_driver = { |
589 | .probe = tilcdc_pdev_probe, |
590 | .remove_new = tilcdc_pdev_remove, |
591 | .shutdown = tilcdc_pdev_shutdown, |
592 | .driver = { |
593 | .name = "tilcdc" , |
594 | .pm = pm_sleep_ptr(&tilcdc_pm_ops), |
595 | .of_match_table = tilcdc_of_match, |
596 | }, |
597 | }; |
598 | |
599 | static int __init tilcdc_drm_init(void) |
600 | { |
601 | if (drm_firmware_drivers_only()) |
602 | return -ENODEV; |
603 | |
604 | DBG("init" ); |
605 | tilcdc_panel_init(); |
606 | return platform_driver_register(&tilcdc_platform_driver); |
607 | } |
608 | |
609 | static void __exit tilcdc_drm_fini(void) |
610 | { |
611 | DBG("fini" ); |
612 | platform_driver_unregister(&tilcdc_platform_driver); |
613 | tilcdc_panel_fini(); |
614 | } |
615 | |
616 | module_init(tilcdc_drm_init); |
617 | module_exit(tilcdc_drm_fini); |
618 | |
619 | MODULE_AUTHOR("Rob Clark <robdclark@gmail.com" ); |
620 | MODULE_DESCRIPTION("TI LCD Controller DRM Driver" ); |
621 | MODULE_LICENSE("GPL" ); |
622 | |