· 8 years ago · Jul 24, 2018, 09:36 PM
1/*
2 * OpenFIMG Kernel Interface
3 *
4 * Copyright 2010-2011 Tomasz Figa <tomasz.figa at gmail.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
20
21#include <linux/slab.h>
22#include <linux/clk.h>
23#include <linux/completion.h>
24#include <linux/delay.h>
25#include <linux/fs.h>
26#include <linux/hrtimer.h>
27#include <linux/interrupt.h>
28#include <linux/io.h>
29#include <linux/irq.h>
30#include <linux/miscdevice.h>
31#include <linux/mm.h>
32#include <linux/platform_device.h>
33#include <linux/poll.h>
34#include <linux/sched.h>
35#include <linux/uaccess.h>
36#include <linux/workqueue.h>
37
38#include <mach/hardware.h>
39#include <mach/map.h>
40
41#include <plat/pm.h>
42#include <plat/power-clock-domain.h>
43#include <plat/reserved_mem.h>
44
45#include "s3c_g3d.h"
46
47#ifdef CONFIG_S3C64XX_DOMAIN_GATING
48 //#define USE_G3D_DOMAIN_GATING
49
50#define DOMAIN_POWER_ON do { \
51 s3c_set_normal_cfg(S3C64XX_DOMAIN_G, S3C64XX_ACTIVE_MODE, S3C64XX_3D); \
52 if(s3c_wait_blk_pwr_ready(S3C64XX_BLK_G)) { \
53 return -1; \
54 } \
55} while (0)
56
57#define DOMAIN_POWER_OFF do { \
58 s3c_set_normal_cfg(S3C64XX_DOMAIN_G, S3C64XX_LP_MODE, S3C64XX_3D); \
59} while (0)
60
61#endif
62
63/*
64 * Various definitions
65 */
66#define G3D_AUTOSUSPEND_DELAY (1000)
67#define G3D_TIMEOUT (1*HZ)
68#define G3D_IDLE_TIME_SECS (10)
69
70/*
71 * Registers
72 */
73#define G3D_FGGB_PIPESTAT_REG (0x00)
74#define G3D_FGGB_PIPESTAT_MSK (0x0005171f)
75
76#define G3D_FGGB_CACHECTL_REG (0x04)
77#define G3D_FGGB_FLUSH_MSK (0x00000033)
78#define G3D_FGGB_INVAL_MSK (0x00001300)
79
80#define G3D_FGGB_RESET_REG (0x08)
81#define G3D_FGGB_VERSION (0x10)
82#define G3D_FGGB_INTPENDING_REG (0x40)
83#define G3D_FGGB_INTMASK_REG (0x44)
84#define G3D_FGGB_PIPEMASK_REG (0x48)
85#define G3D_FGGB_PIPETGTSTATE_REG (0x4c)
86
87/*
88 * Private structures
89 */
90struct g3d_context;
91
92struct g3d_drvdata {
93 void __iomem *base;
94
95 uint32_t mask;
96 struct mutex lock;
97 struct mutex hw_lock;
98 struct g3d_context *hw_owner;
99 struct completion completion;
100
101 int irq;
102 struct resource *mem;
103 struct clk *clock;
104 struct device *dev;
105 struct miscdevice mdev;
106
107#ifdef USE_G3D_DOMAIN_GATING
108 struct hrtimer timer; // idle timer
109 int state; // power state
110#endif
111};
112
113struct g3d_context {
114 struct g3d_drvdata *data;
115 /* More to come */
116};
117
118static int g3d_pm_flag = 0;
119static inline void clk_g3d_enable(struct g3d_drvdata *data)
120{
121 if (g3d_pm_flag > 0) {
122 g3d_pm_flag++;
123 return;
124 }
125
126 clk_enable(data->clock);
127 g3d_pm_flag++;
128}
129
130static inline void clk_g3d_disable(struct g3d_drvdata *data)
131{
132 if (g3d_pm_flag <= 0) {
133 return;
134 }
135
136 g3d_pm_flag--;
137 clk_disable(data->clock);
138}
139
140/*
141 * Register accessors
142 */
143static inline void g3d_write(struct g3d_drvdata *d, uint32_t b, uint32_t r)
144{
145 writel(b, d->base + r);
146}
147
148static inline uint32_t g3d_read(struct g3d_drvdata *d, uint32_t r)
149{
150 return readl(d->base + r);
151}
152
153/*
154 * Hardware operations
155 */
156static inline void g3d_soft_reset(struct g3d_drvdata *data)
157{
158 g3d_write(data, 1, G3D_FGGB_RESET_REG);
159 udelay(1);
160 g3d_write(data, 0, G3D_FGGB_RESET_REG);
161}
162
163static inline int g3d_flush_pipeline(struct g3d_drvdata *data, unsigned int mask)
164{
165 int ret = 0;
166
167 if((g3d_read(data, G3D_FGGB_PIPESTAT_REG) & mask) == 0)
168 return 0;
169
170 /* Setup the interrupt */
171 data->mask = mask;
172 init_completion(&data->completion);
173 g3d_write(data, 0, G3D_FGGB_PIPEMASK_REG);
174 g3d_write(data, 0, G3D_FGGB_PIPETGTSTATE_REG);
175 g3d_write(data, mask, G3D_FGGB_PIPEMASK_REG);
176 g3d_write(data, 1, G3D_FGGB_INTMASK_REG);
177
178 /* Check if the condition isn't already met */
179 if((g3d_read(data, G3D_FGGB_PIPESTAT_REG) & mask) == 0) {
180 /* Disable the interrupt */
181 g3d_write(data, 0, G3D_FGGB_INTMASK_REG);
182 return 0;
183 }
184
185 if(!wait_for_completion_interruptible_timeout(&data->completion,
186 G3D_TIMEOUT)) {
187 dev_err(data->dev, "timeout while waiting for interrupt, resetting (stat=%08x)\n",
188 g3d_read(data, G3D_FGGB_PIPESTAT_REG));
189 g3d_soft_reset(data);
190 ret = -EFAULT;
191 }
192
193 /* Disable the interrupt */
194 g3d_write(data, 0, G3D_FGGB_INTMASK_REG);
195
196 return ret;
197}
198
199static inline void g3d_flush_caches(struct g3d_drvdata *data)
200{
201 int timeout = 1000000000;
202 g3d_write(data, G3D_FGGB_FLUSH_MSK, G3D_FGGB_CACHECTL_REG);
203
204 do {
205 if(!g3d_read(data, G3D_FGGB_CACHECTL_REG))
206 break;
207 } while (--timeout);
208}
209
210static inline void g3d_invalidate_caches(struct g3d_drvdata *data)
211{
212 int timeout = 1000000000;
213 g3d_write(data, G3D_FGGB_INVAL_MSK, G3D_FGGB_CACHECTL_REG);
214
215 do {
216 if(!g3d_read(data, G3D_FGGB_CACHECTL_REG))
217 break;
218 } while (--timeout);
219}
220
221/*
222 * State processing
223 */
224static irqreturn_t g3d_handle_irq(int irq, void *dev_id)
225{
226 struct g3d_drvdata *data = (struct g3d_drvdata *)dev_id;
227 uint32_t stat;
228
229 g3d_write(data, 0, G3D_FGGB_INTPENDING_REG);
230 stat = g3d_read(data, G3D_FGGB_PIPESTAT_REG) & data->mask;
231
232 if(!stat)
233 complete(&data->completion);
234
235 return IRQ_HANDLED;
236}
237
238static inline int ctx_has_lock(struct g3d_context *ctx)
239{
240 struct g3d_drvdata *data = ctx->data;
241
242 return mutex_is_locked(&data->hw_lock) && (data->hw_owner == ctx);
243}
244
245/*
246 Power management
247*/
248
249static inline int g3d_do_power_up(struct g3d_drvdata *data)
250{
251#ifdef CONFIG_S3C64XX_DOMAIN_GATING
252 DOMAIN_POWER_ON;
253#endif
254 clk_g3d_enable(data);
255 g3d_soft_reset(data);
256
257 return 1;
258}
259
260static inline void g3d_do_power_down(struct g3d_drvdata *data)
261{
262 clk_g3d_disable(data);
263#ifdef CONFIG_S3C64XX_DOMAIN_GATING
264 DOMAIN_POWER_OFF;
265#endif
266}
267
268#ifdef USE_G3D_DOMAIN_GATING
269/* Called with mutex locked */
270static inline int g3d_power_up(struct g3d_drvdata *data)
271{
272 int ret;
273
274 if (data->state)
275 return 0;
276
277 dev_info(data->dev, "Requesting power up.\n");
278 if((ret = g3d_do_power_up(data)) > 0)
279 data->state = 1;
280
281 return ret;
282}
283
284/* Called with mutex locked */
285static inline void g3d_power_down(struct g3d_drvdata *data)
286{
287 if(!data->state)
288 return;
289
290 dev_info(data->dev, "Requesting power down.\n");
291
292 g3d_flush_pipeline(data, G3D_FGGB_PIPESTAT_MSK);
293 g3d_flush_caches(data);
294 g3d_do_power_down(data);
295 data->state = 0;
296}
297
298/* Called with mutex locked */
299static enum hrtimer_restart g3d_idle_func(struct hrtimer *t)
300{
301 struct g3d_drvdata *data = container_of(t, struct g3d_drvdata, timer);
302 g3d_power_down(data);
303
304 return HRTIMER_NORESTART;
305}
306#endif
307
308/*
309 * File operations
310 */
311static int s3c_g3d_unlock(struct g3d_context *ctx)
312{
313 struct g3d_drvdata *data = ctx->data;
314 int ret = 0;
315
316 mutex_lock(&data->lock);
317
318 if (unlikely(!ctx_has_lock(ctx))) {
319 dev_err(data->dev, "called S3C_G3D_UNLOCK without holding the hardware lock\n");
320 mutex_unlock(&data->lock);
321 return -EPERM;
322 }
323
324#ifdef USE_G3D_DOMAIN_GATING
325 hrtimer_start(&data->timer, ktime_set(G3D_IDLE_TIME_SECS, 0), HRTIMER_MODE_REL);
326#endif /* USE_G3D_DOMAIN_GATING */
327
328 mutex_unlock(&data->lock);
329
330 dev_dbg(data->dev, "hardware lock released by %p\n", ctx);
331
332 mutex_unlock(&data->hw_lock);
333
334 return ret;
335}
336
337static int s3c_g3d_lock(struct g3d_context *ctx)
338{
339 struct g3d_drvdata *data = ctx->data;
340 int ret = 0;
341
342 mutex_lock(&data->hw_lock);
343
344 dev_dbg(data->dev, "hardware lock acquired by %p\n", ctx);
345
346 mutex_lock(&data->lock);
347
348#ifdef USE_G3D_DOMAIN_GATING
349 if(!hrtimer_cancel(&data->timer)) {
350 ret = g3d_power_up(data);
351 if (ret < 0) {
352 dev_err(data->dev, "Timeout while waiting for G3D power up\n");
353 mutex_unlock(&data->hw_lock);
354 goto exit;
355 }
356 }
357#endif /* USE_G3D_DOMAIN_GATING */
358
359 if (likely(data->hw_owner == ctx)) {
360 mutex_unlock(&data->lock);
361 return 0;
362 }
363
364 ret = 1;
365
366 if (data->hw_owner) {
367 g3d_flush_pipeline(data, G3D_FGGB_PIPESTAT_MSK);
368 ret = 2;
369 }
370
371 data->hw_owner = ctx;
372
373exit:
374 mutex_unlock(&data->lock);
375
376 return ret;
377}
378
379static int s3c_g3d_flush(struct g3d_context *ctx, u32 mask)
380{
381 struct g3d_drvdata *data = ctx->data;
382 int ret = 0;
383
384 mutex_lock(&data->lock);
385
386 if (unlikely(!ctx_has_lock(ctx))) {
387 dev_err(data->dev, "called S3C_G3D_FLUSH without holding the hardware lock\n");
388 ret = -EPERM;
389 goto exit;
390 }
391
392 ret = g3d_flush_pipeline(data, mask & G3D_FGGB_PIPESTAT_MSK);
393
394exit:
395 mutex_unlock(&data->lock);
396
397 return ret;
398}
399
400static long s3c_g3d_ioctl(struct file *file,
401 unsigned int cmd, unsigned long arg)
402{
403 struct g3d_context *ctx = file->private_data;
404 int ret = 0;
405
406 switch(cmd) {
407 /* Prepare and lock the hardware */
408 case S3C_G3D_LOCK:
409 ret = s3c_g3d_lock(ctx);
410 break;
411
412 /* Unlock the hardware and start idle timer */
413 case S3C_G3D_UNLOCK:
414 ret = s3c_g3d_unlock(ctx);
415 break;
416
417 /* Wait for the hardware to finish its work */
418 case S3C_G3D_FLUSH:
419 ret = s3c_g3d_flush(ctx, arg & G3D_FGGB_PIPESTAT_MSK);
420 break;
421
422 default:
423 ret = -EINVAL;
424 }
425
426 return ret;
427}
428
429static int s3c_g3d_open(struct inode *inode, struct file *file)
430{
431 struct miscdevice *mdev = file->private_data;
432 struct g3d_drvdata *data = container_of(mdev, struct g3d_drvdata, mdev);
433 struct g3d_context *ctx;
434
435 ctx = kmalloc(sizeof(struct g3d_context), GFP_KERNEL);
436 ctx->data = data;
437
438 file->private_data = ctx;
439
440 dev_dbg(data->dev, "device opened\n");
441
442 return 0;
443}
444
445static int s3c_g3d_release(struct inode *inode, struct file *file)
446{
447 struct g3d_context *ctx = file->private_data;
448 struct g3d_drvdata *data = ctx->data;
449 int unlock = 0;
450
451 /* Do this atomically */
452 mutex_lock(&data->lock);
453
454 unlock = ctx_has_lock(ctx);
455
456 mutex_unlock(&data->lock);
457
458 /* Unlock if we have the lock */
459 if(unlock)
460 s3c_g3d_ioctl(file, S3C_G3D_UNLOCK, 0);
461
462 kfree(ctx);
463 dev_dbg(data->dev, "device released\n");
464
465 return 0;
466}
467
468int s3c_g3d_mmap(struct file* file, struct vm_area_struct *vma)
469{
470 struct g3d_context *ctx = file->private_data;
471 struct g3d_drvdata *data = ctx->data;
472 unsigned long pfn;
473 size_t size = vma->vm_end - vma->vm_start;
474
475 pfn = __phys_to_pfn(data->mem->start);
476
477 if(size > resource_size(data->mem)) {
478 dev_err(data->dev, "mmap size bigger than G3D SFR block\n");
479 return -EINVAL;
480 }
481
482 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
483
484 if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)) {
485 dev_err(data->dev, "mmap of G3D SFR block must be shared\n");
486 return -EINVAL;
487 }
488
489 if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot)) {
490 dev_err(data->dev, "remap_pfn range failed\n");
491 return -EINVAL;
492 }
493
494 dev_dbg(data->dev, "hardware mapped by %p\n", ctx);
495
496 return 0;
497}
498
499static struct file_operations s3c_g3d_fops = {
500 .owner = THIS_MODULE,
501 .unlocked_ioctl = s3c_g3d_ioctl,
502 .open = s3c_g3d_open,
503 .release = s3c_g3d_release,
504 .mmap = s3c_g3d_mmap,
505};
506
507/*
508 * Platform device operations
509 */
510static int __init s3c_g3d_probe(struct platform_device *pdev)
511{
512 struct resource *res;
513 struct g3d_drvdata *data;
514 int ret;
515 uint32_t version;
516
517 if (pdev->id != -1) {
518 dev_err(&pdev->dev, "only single instance is allowed.\n");
519 return -EINVAL;
520 }
521
522 data = kzalloc(sizeof(struct g3d_drvdata), GFP_KERNEL);
523 if(data == NULL) {
524 dev_err(&pdev->dev, "failed to allocate driver data.\n");
525 return -ENOMEM;
526 }
527
528 /* initialize the miscdevice struct */
529 data->mdev.minor = MISC_DYNAMIC_MINOR;
530 data->mdev.name = "s3c-g3d";
531 data->mdev.fops = &s3c_g3d_fops;
532
533 s3c_set_normal_cfg(S3C64XX_DOMAIN_G, S3C64XX_ACTIVE_MODE, S3C64XX_3D);
534
535 /* get device clock */
536 data->clock = clk_get(&pdev->dev, "hclk_g3d");
537 if (data->clock == NULL) {
538 dev_err(&pdev->dev, "failed to find g3d clock source\n");
539 ret = -ENOENT;
540 goto err_clock;
541 }
542
543 /* get the memory region for the post processor driver */
544 res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
545 if(res == NULL) {
546 dev_err(&pdev->dev, "failed to get memory region resource.\n");
547 ret = -ENOENT;
548 goto err_mem;
549 }
550
551 /* reserve the memory */
552 data->mem = request_mem_region(res->start, resource_size(res),
553 pdev->name);
554 if (data->mem == NULL) {
555 dev_err(&pdev->dev, "failed to reserve memory region\n");
556 ret = -ENOENT;
557 goto err_mem;
558 }
559
560 /* map the memory */
561 data->base = ioremap(data->mem->start, resource_size(data->mem));
562 if (data->base == NULL) {
563 dev_err(&pdev->dev, "ioremap failed\n");
564 ret = -ENOENT;
565 goto err_ioremap;
566 }
567
568 /* get the IRQ */
569 data->irq = platform_get_irq(pdev, 0);
570 if (data->irq <= 0) {
571 dev_err(&pdev->dev,
572 "failed to get irq resource (%d).\n", data->irq);
573 ret = data->irq;
574 goto err_irq;
575 }
576
577 /* request the IRQ */
578 ret = request_irq(data->irq, g3d_handle_irq, 0, pdev->name, data);
579 if (ret) {
580 dev_err(&pdev->dev, "request_irq failed (%d).\n", ret);
581 goto err_irq;
582 }
583
584 data->dev = &pdev->dev;
585 data->hw_owner = NULL;
586 mutex_init(&data->lock);
587 mutex_init(&data->hw_lock);
588 init_completion(&data->completion);
589
590 platform_set_drvdata(pdev, data);
591
592#ifdef USE_G3D_DOMAIN_GATING
593 hrtimer_init(&data->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
594 data->timer.function = g3d_idle_func;
595 data->state = 1;
596#endif
597
598 if(g3d_do_power_up(data) < 0) {
599 dev_err(&pdev->dev, "G3D power up failed\n");
600 ret = -EFAULT;
601 goto err_pm;
602 }
603
604 g3d_soft_reset(data);
605
606 version = g3d_read(data, G3D_FGGB_VERSION);
607 dev_info(&pdev->dev, "detected FIMG-3DSE version %d.%d.%d\n",
608 version >> 24, (version >> 16) & 0xff, (version >> 8) & 0xff);
609
610#ifdef USE_G3D_DOMAIN_GATING
611 hrtimer_start(&data->timer, ktime_set(G3D_IDLE_TIME_SECS, 0), HRTIMER_MODE_REL);
612#endif
613
614 ret = misc_register(&data->mdev);
615 if (ret < 0) {
616 dev_err(&pdev->dev, "could not register miscdev (%d)\n", ret);
617 goto err_misc_register;
618 }
619
620 return 0;
621
622err_misc_register:
623#ifndef USE_G3D_DOMAIN_GATING
624 g3d_do_power_down(data);
625#else
626 if(!hrtimer_cancel(&data->timer))
627 g3d_power_down(data);
628#endif
629 free_irq(data->irq, pdev);
630err_pm:
631 free_irq(data->irq, pdev);
632err_irq:
633 iounmap(data->base);
634err_ioremap:
635 release_resource(data->mem);
636err_mem:
637 clk_g3d_enable(data);
638err_clock:
639 kfree(data);
640
641 return ret;
642}
643
644static int __devexit s3c_g3d_remove(struct platform_device *pdev)
645{
646 struct g3d_drvdata *data = platform_get_drvdata(pdev);
647
648 misc_deregister(&data->mdev);
649 clk_g3d_disable(data);
650
651#ifdef USE_G3D_DOMAIN_GATING
652 if(!hrtimer_cancel(&data->timer))
653 g3d_power_down(data);
654#else
655 g3d_flush_pipeline(data, G3D_FGGB_PIPESTAT_MSK);
656 g3d_flush_caches(data);
657 g3d_do_power_down(data);
658#endif
659
660 free_irq(data->irq, data);
661 iounmap(data->base);
662 release_resource(data->mem);
663 clk_put(data->clock);
664 kfree(data);
665
666 return 0;
667}
668
669static int s3c_g3d_runtime_suspend(struct device *dev)
670{
671 struct g3d_drvdata *data = dev_get_drvdata(dev);
672 clk_g3d_disable(data);
673
674#ifdef USE_G3D_DOMAIN_GATING
675 if(hrtimer_cancel(&data->timer))
676 g3d_power_down(data);
677#else
678 g3d_flush_pipeline(data, G3D_FGGB_PIPESTAT_MSK);
679 g3d_flush_caches(data);
680 g3d_do_power_down(data);
681#endif
682
683 data->hw_owner = NULL;
684
685 return 0;
686}
687
688static int s3c_g3d_runtime_resume(struct device *dev)
689{
690 struct g3d_drvdata *data = dev_get_drvdata(dev);
691 clk_g3d_enable(data);
692
693#ifndef USE_G3D_DOMAIN_GATING
694 if(g3d_do_power_up(data) < 0) {
695 dev_err(data->dev, "G3D power up failed\n");
696 return -EFAULT;
697 }
698#endif
699
700 return 0;
701}
702
703static int s3c_g3d_suspend(struct device *dev)
704{
705 struct g3d_drvdata *data = dev_get_drvdata(dev);
706
707 if (mutex_is_locked(&data->hw_lock)) {
708 dev_err(dev, "suspend requested with locked hardware (broken userspace?)\n");
709 return -EAGAIN;
710 }
711
712 clk_g3d_disable(data);
713
714#ifdef USE_G3D_DOMAIN_GATING
715 if(hrtimer_cancel(&data->timer))
716 g3d_power_down(data);
717#else
718 g3d_flush_pipeline(data, G3D_FGGB_PIPESTAT_MSK);
719 g3d_flush_caches(data);
720 g3d_do_power_down(data);
721#endif
722
723 data->hw_owner = NULL;
724
725 return 0;
726}
727
728static int s3c_g3d_resume(struct device *dev)
729{
730 struct g3d_drvdata *data = dev_get_drvdata(dev);
731 clk_g3d_enable(data);
732
733#ifndef USE_G3D_DOMAIN_GATING
734 if(g3d_do_power_up(data) < 0) {
735 dev_err(data->dev, "G3D power up failed\n");
736 return -EFAULT;
737 }
738#endif
739
740 return 0;
741}
742
743static struct dev_pm_ops s3c_g3d_pm_ops = {
744 .suspend = s3c_g3d_suspend,
745 .resume = s3c_g3d_resume,
746 .runtime_suspend = s3c_g3d_runtime_suspend,
747 .runtime_resume = s3c_g3d_runtime_resume,
748};
749
750static struct platform_driver s3c_g3d_driver = {
751 .remove = __devexit_p(s3c_g3d_remove),
752 .driver = {
753 .owner = THIS_MODULE,
754 .name = "s3c-g3d",
755 .pm = &s3c_g3d_pm_ops,
756 },
757};
758
759/*
760 * Module operations
761 */
762int __init s3c_g3d_init(void)
763{
764 return platform_driver_probe(&s3c_g3d_driver, s3c_g3d_probe);
765}
766
767void __exit s3c_g3d_exit(void)
768{
769 platform_driver_unregister(&s3c_g3d_driver);
770}
771
772module_init(s3c_g3d_init);
773module_exit(s3c_g3d_exit);
774
775MODULE_AUTHOR("Tomasz Figa <tomasz.figa@gmail.com>");
776MODULE_DESCRIPTION("OpenFIMG Kernel Interface");
777MODULE_LICENSE("GPL");