oleavr-rgl-a500-mini-linux-.../drivers/char/sunxi_tr/sunxi_transform.c
Ole André Vadla Ravnås 169c65d57e Initial commit
2022-05-07 01:01:45 +02:00

908 lines
20 KiB
C
Executable file

/* linux/drivers/char/sunxi_tr/sunxi_tr.c
*
* Copyright (c) 2014 Allwinnertech Co., Ltd.
* Author: Tyle <tyle@allwinnertech.com>
*
* Transform driver for sunxi platform
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include "transform.h"
#include <linux/compat.h>
#include <linux/idr.h>
#define TR_CHAN_MAX 32
//#define TR_POLL
//#define TR_CHECK_THREAD
struct sunxi_transform
{
int id; /* chan id */
struct rb_node node;
struct list_head list;
bool requested; /* indicate if have request */
bool busy; /* at busy state when transforming */
bool error;/* indicate some error happens in this channel */
unsigned long start_time; /* the time when starting transform */
unsigned long timeout;//ms
tr_info info;
};
struct sunxi_trdev
{
struct device *dev;
void __iomem *base;
int irq;
struct clk *clk; /* clock gate for tr */
spinlock_t slock;
struct mutex mlock;
struct list_head trs; /* transform chan list */
unsigned int count; /* transform channel counter */
struct sunxi_transform *cur_tr; /* curent transform channel processing */
bool busy;
struct task_struct *task;
struct idr idr;
struct rb_node node;
struct rb_root handles;
};
static struct sunxi_trdev *gsunxi_dev = NULL;
static struct cdev *tr_cdev;
static dev_t devid ;
static struct class *tr_class;
static struct device *tr_dev;
static bool dev_init = false;
static struct mutex id_lock;
//#define struct sunxi_trdev *to_sunxi_trdev(dev) container_of(dev, struct sunxi_trdev, dev)
static int sunxi_tr_finish_procss(void);
#if !defined(CONFIG_OF)
static struct resource tr_resource[] = {
[0] = {
.start = (int __force)SUNXI_DE_VBASE,
.end = (int __force)(SUNXI_DE_VBASE + SUNXI_DE_SIZE),
.flags = IORESOURCE_MEM,
},
[1] = {
.start = (int __force)SUNXI_IRQ_DEIRQ1,
.end = (int __force)SUNXI_IRQ_DEIRQ1,
.flags = IORESOURCE_IRQ,
},
};
#endif
#if defined(TR_CHECK_THREAD)
static int tr_thread(void *parg)
{
while (1) {
struct sunxi_transform *tr = NULL;
unsigned long timeout = 0;
if(kthread_should_stop()) {
break;
}
tr = gsunxi_dev->cur_tr;
if(tr) {
timeout = tr->start_time + msecs_to_jiffies(tr->timeout);
if (time_after_eq(jiffies, timeout)) {
pr_warn("%s, timeout(%d ms)\n", __func__, jiffies_to_msecs(jiffies - tr->start_time));
de_tr_reset();
tr->busy = false;
tr->error = true;
sunxi_tr_finish_procss();
}
}
msleep(10);
}
return 0;
}
#endif
static int tr_check_timeout(void)
{
struct sunxi_transform *tr = NULL;
unsigned long timeout = 0;
unsigned long flags;
tr = gsunxi_dev->cur_tr;
if(NULL == tr) {
return 0;
}
spin_lock_irqsave(&gsunxi_dev->slock, flags);
timeout = tr->start_time + msecs_to_jiffies(tr->timeout);
if (tr->busy && time_after_eq(jiffies, timeout)) {
tr->busy = false;
tr->error = true;
spin_unlock_irqrestore(&gsunxi_dev->slock, flags);
de_tr_exception();
pr_warn("%s, timeout(%d ms)\n", __func__, jiffies_to_msecs(jiffies - tr->start_time));
sunxi_tr_finish_procss();
} else {
spin_unlock_irqrestore(&gsunxi_dev->slock, flags);
}
return 0;
}
/* find a tr which has request and a longest time to process */
static struct sunxi_transform * tr_find_proper_task(void)
{
struct sunxi_transform *tr = NULL, *proper_tr = NULL;
unsigned long min_time = jiffies;
list_for_each_entry(tr, &gsunxi_dev->trs, list) {
bool condition1 = (true == tr->requested);
bool condition2 = time_after_eq(min_time, tr->start_time);
if(condition1 && condition2) {
min_time = tr->start_time;
proper_tr = tr;
} else {
//printk("find_task: %d,%d, %ld,%ld\n", condition1, condition2, min_time, tr->start_time);
}
}
return proper_tr;
}
/* protect by @slock */
static int tr_process_next_proper_task(u32 from)
{
unsigned long flags;
struct sunxi_transform *tr = NULL;
int ret = -1;
spin_lock_irqsave(&gsunxi_dev->slock, flags);
if(gsunxi_dev->busy) {
spin_unlock_irqrestore(&gsunxi_dev->slock, flags);
return -1;
}
/* find a tr which has request */
tr = tr_find_proper_task();
if(NULL != tr) {
/* process request */
gsunxi_dev->busy = true;
tr->busy = true;
tr->error = false;
tr->start_time = jiffies;
tr->requested = false;
gsunxi_dev->cur_tr = tr;
}
spin_unlock_irqrestore(&gsunxi_dev->slock, flags);
if(NULL != tr)
ret = de_tr_set_cfg(&tr->info);
return ret;
}
static int sunxi_tr_finish_procss(void)
{
unsigned long flags;
/* correct interrupt */
spin_lock_irqsave(&gsunxi_dev->slock, flags);
if(gsunxi_dev->cur_tr) {
gsunxi_dev->cur_tr->busy = false;
gsunxi_dev->cur_tr = NULL;
gsunxi_dev->busy = false;
}
spin_unlock_irqrestore(&gsunxi_dev->slock, flags);
tr_process_next_proper_task(0);
return 0;
}
static struct sunxi_transform *tr_get_by_id(int id)
{
struct sunxi_transform *tr;
mutex_lock(&gsunxi_dev->mlock);
tr = idr_find(&gsunxi_dev->idr, id);
mutex_unlock(&gsunxi_dev->mlock);
return tr ? tr : ERR_PTR(-EINVAL);
}
/*
* sunxi_tr_request - request transform channel
* On success, returns transform handle. On failure, returns 0.
*/
unsigned int sunxi_tr_request(void)
{
struct sunxi_transform* tr = NULL;
unsigned long flags;
unsigned int count = 0;
struct rb_node **p = &gsunxi_dev->handles.rb_node;
struct rb_node *parent = NULL;
struct sunxi_transform *entry;
int id;
if (gsunxi_dev->count > TR_CHAN_MAX) {
pr_warn("%s(), user number have exceed max number %d\n",
__func__, TR_CHAN_MAX);
return 0;
}
tr = kzalloc(sizeof(struct sunxi_transform), GFP_KERNEL);
if (!tr) {
pr_warn("alloc fail\n");
return 0;
}
RB_CLEAR_NODE(&tr->node);
tr->requested = false;
tr->busy = false;
tr->error = false;
tr->timeout = 50; //default 50ms timeout
tr->start_time = jiffies;
id = idr_alloc(&gsunxi_dev->idr, tr, 1, 0, GFP_KERNEL);
if (id < 0)
return 0;
tr->id = id;
mutex_lock(&gsunxi_dev->mlock);
while (*p) {
parent = *p;
entry = rb_entry(parent, struct sunxi_transform, node);
if (tr < entry)
p = &(*p)->rb_left;
else if (tr > entry)
p = &(*p)->rb_right;
else
pr_warn("%s: tr already found.\n", __func__);
}
rb_link_node(&tr->node, parent, p);
rb_insert_color(&tr->node, &gsunxi_dev->handles);
mutex_unlock(&gsunxi_dev->mlock);
spin_lock_irqsave(&gsunxi_dev->slock, flags);
list_add_tail(&tr->list, &gsunxi_dev->trs);
gsunxi_dev->count ++;
count = gsunxi_dev->count;
spin_unlock_irqrestore(&gsunxi_dev->slock, flags);
mutex_lock(&gsunxi_dev->mlock);
if(1 == count) {
if(gsunxi_dev->clk)
clk_prepare_enable(gsunxi_dev->clk);
de_tr_init();
#if defined(TR_CHECK_THREAD)
gsunxi_dev->task = kthread_create(tr_thread, (void*)0, "tr_thread");
if(IS_ERR(gsunxi_dev->task)) {
pr_warn("Unable to start kernel thread %s.\n","tr_thread");
gsunxi_dev->task = NULL;
} else
wake_up_process(gsunxi_dev->task);
#endif
}
mutex_unlock(&gsunxi_dev->mlock);
pr_info("%s, count=%d\n", __func__, count);
return id;
}
EXPORT_SYMBOL_GPL(sunxi_tr_request);
/*
* sunxi_tr_release - release transform channel
* @hdl: transform handle which return by sunxi_tr_request
* On success, returns 0. On failure, returns ERR_PTR(-errno).
*/
int sunxi_tr_release(unsigned int id)
{
struct sunxi_transform *tr = tr_get_by_id(id);
unsigned long flags;
unsigned int count = 0;
if (IS_ERR_OR_NULL(tr)) {
pr_warn("%s, hdl is invalid!\n", __func__);
return -EINVAL;
}
mutex_lock(&gsunxi_dev->mlock);
idr_remove(&gsunxi_dev->idr, tr->id);
if (!RB_EMPTY_NODE(&tr->node))
rb_erase(&tr->node, &gsunxi_dev->handles);
mutex_unlock(&gsunxi_dev->mlock);
spin_lock_irqsave(&gsunxi_dev->slock, flags);
list_del(&tr->list);
gsunxi_dev->count --;
count = gsunxi_dev->count;
kfree((void*)tr);
spin_unlock_irqrestore(&gsunxi_dev->slock, flags);
mutex_lock(&gsunxi_dev->mlock);
if(0 == count) {
#if defined(TR_CHECK_THREAD)
kthread_stop(gsunxi_dev->task);
gsunxi_dev->task = NULL;
#endif
de_tr_exit();
if(gsunxi_dev->clk)
clk_disable(gsunxi_dev->clk);
}
mutex_unlock(&gsunxi_dev->mlock);
pr_info("%s, count=%d\n", __func__, count);
return 0;
}
EXPORT_SYMBOL_GPL(sunxi_tr_release);
/*
* sunxi_tr_commit - commit an transform request
* @hdl: transform handle which return by sunxi_tr_request
* On success, returns 0. On failure, returns ERR_PTR(-errno).
*/
int sunxi_tr_commit(unsigned int id, tr_info *info)
{
int ret = 0;
struct sunxi_transform *tr = tr_get_by_id(id);
if (IS_ERR_OR_NULL(tr)) {
pr_warn("%s, hdl is invalid\n", __func__);
return -EINVAL;
}
#if 0
pr_info("in: fmt=%d, pitch=%d,%d,%d, rect=%d,%d,%d,%d\n",
info->src_frame.fmt, info->src_frame.pitch[0], info->src_frame.pitch[1], info->src_frame.pitch[2],
info->src_rect.x, info->src_rect.y, info->src_rect.w, info->src_rect.h);
pr_info("out: fmt=%d, pitch=%d,%d,%d, rect=%d,%d,%d,%d\n",
info->dst_frame.fmt, info->dst_frame.pitch[0], info->dst_frame.pitch[1], info->dst_frame.pitch[2],
info->dst_rect.x, info->dst_rect.y, info->dst_rect.w, info->dst_rect.h);
#endif
if(!tr->requested && !tr->busy) {
memcpy(&tr->info, info, sizeof(tr_info));
tr->requested = true;
ret = tr_process_next_proper_task(1);
}
#if defined(TR_POLL)
{
int wait_cnt = 5, delay_ms = 10, i = 0;
while((i < wait_cnt) && (0 != de_tr_irq_query())) {
msleep(delay_ms);
i ++;
}
if(wait_cnt >= 5)
pr_warn("%s, timeout !!!\n", __func__);
sunxi_tr_finish_procss();
}
#endif
return ret;
}
EXPORT_SYMBOL_GPL(sunxi_tr_commit);
/*
* sunxi_tr_query - query transform status
* @hdl: transform handle which return by sunxi_tr_request
* On finish, returns 0. On failure, returns ERR_PTR(-errno).
* On busy,returns 1.
*/
int sunxi_tr_query(unsigned int id)
{
int status = 0;
struct sunxi_transform *tr = tr_get_by_id(id);
if (IS_ERR_OR_NULL(tr)) {
pr_warn("%s, hdl is invalid!\n", __func__);
return 0;
}
#if !defined(TR_CHECK_THREAD)
tr_check_timeout();
#endif
if(tr->requested || tr->busy)
status = 1; /* busy */
else if(tr->error)
status = -1; /*error:timeout */
else
status = 0;/* finish */
return status;
#if 0
timeout = tr->start_time + msecs_to_jiffies(tr->timeout);
if (time_after_eq(jiffies, timeout)) {
pr_warn("%s, timeout(%d ms)\n", __func__, jiffies_to_msecs(jiffies - tr->start_time));
de_tr_reset();
tr->busy = false;
return -1;
}
return (tr->busy?1:0);
#endif
}
EXPORT_SYMBOL_GPL(sunxi_tr_query);
/*
* sunxi_tr_set_timeout - set transform timeout(ms)
* @hdl: transform hdl
* @timeout: time(ms)
* On success, returns 0. On failure, returns ERR_PTR(-errno).
*/
int sunxi_tr_set_timeout(unsigned int id, unsigned long timeout /* ms */)
{
struct sunxi_transform *tr = tr_get_by_id(id);
if (IS_ERR_OR_NULL(tr)) {
pr_warn("%s, hdl is invalid!\n", __func__);
return -EINVAL;
}
if(timeout == 0) {
pr_warn("%s, para error(timeout=0)!\n", __func__);
return -EINVAL;
}
tr->timeout = timeout;
return 0;
}
static irqreturn_t sunxi_tr_interrupt(int irq, void *dev_id)
{
int ret = 0;
/* get irq status */
ret = de_tr_irq_query();
if(0 == ret) {
sunxi_tr_finish_procss();
}
/* clear irq status */
return IRQ_HANDLED;
}
static int tr_open(struct inode *inode, struct file *file)
{
return 0;
}
static int tr_release(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t tr_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
return 0;
}
static ssize_t tr_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
return 0;
}
//static int __devinit tr_probe(struct platform_device *pdev)
static int tr_probe(struct platform_device *pdev)
{
#if !defined(CONFIG_OF)
struct resource *res;
#endif
struct sunxi_trdev *sunxi_dev = NULL;
int ret = 0;
int irq;
pr_info("enter %s\n", __func__);
sunxi_dev = kzalloc(sizeof(struct sunxi_trdev), GFP_KERNEL);
if (!sunxi_dev)
return -ENOMEM;
/* register base */
#if !defined(CONFIG_OF)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "get reource MEM fail\n");
ret = -EINVAL;
goto io_err;
}
sunxi_dev->base = (void __iomem *)res->start;
#else
sunxi_dev->base = of_iomap(pdev->dev.of_node, 0);
if (NULL == sunxi_dev->base) {
dev_err(&pdev->dev, "unable to map transform registers\n");
ret = -EINVAL;
goto io_err;
}
#endif
/* irq */
#if !defined(CONFIG_OF)
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto io_err;
}
#else
irq = irq_of_parse_and_map(pdev->dev.of_node, 0);
if (!irq) {
dev_err(&pdev->dev, "irq_of_parse_and_map irq fail for transform\n");
}
#endif
sunxi_dev->irq = irq;
ret = request_irq(irq, sunxi_tr_interrupt, IRQF_SHARED,
dev_name(&pdev->dev), sunxi_dev);
if (ret) {
dev_err(&pdev->dev, "NO IRQ found!!!\n");
goto iomap_err;
}
/* clk init */
sunxi_dev->clk = of_clk_get(pdev->dev.of_node, 0);
if (IS_ERR(sunxi_dev->clk)) {
dev_err(&pdev->dev, "fail to get clk\n");
}
gsunxi_dev = sunxi_dev;
platform_set_drvdata(pdev, sunxi_dev);
INIT_LIST_HEAD(&sunxi_dev->trs);
spin_lock_init(&sunxi_dev->slock);
mutex_init(&sunxi_dev->mlock);
mutex_init(&id_lock);
idr_init(&sunxi_dev->idr);
sunxi_dev->dev = &pdev->dev;
sunxi_dev->handles = RB_ROOT;
dev_init = true;
/* init hw */
de_tr_set_base((uintptr_t)sunxi_dev->base);
pr_info("exit %s\n", __func__);
return 0;
iomap_err:
iounmap(sunxi_dev->base);
io_err:
kfree(sunxi_dev);
return ret;
}
static int tr_remove(struct platform_device *pdev)
{
struct sunxi_trdev *sunxi_dev = NULL;
pr_info("tr_remove enter\n");
sunxi_dev = (struct sunxi_trdev *)platform_get_drvdata(pdev);
if (sunxi_dev && (sunxi_dev->count != 0)) {
struct sunxi_transform *tr = NULL, *tr_tmp = NULL;
unsigned long flags;
pr_warn("%s(), there are still %d t'users, force release them\n",
__func__, sunxi_dev->count);
spin_lock_irqsave(&sunxi_dev->slock, flags);
list_for_each_entry_safe(tr, tr_tmp, &sunxi_dev->trs, list) {
list_del(&tr->list);
sunxi_dev->count--;
kfree((void *)tr);
}
spin_unlock_irqrestore(&sunxi_dev->slock, flags);
if (sunxi_dev->clk)
clk_disable(sunxi_dev->clk);
}
if (sunxi_dev) {
dev_init = false;
free_irq(sunxi_dev->irq, sunxi_dev);
clk_put(sunxi_dev->clk);
#if defined(CONFIG_OF)
iounmap(sunxi_dev->base);
#endif
kfree(sunxi_dev);
platform_set_drvdata(pdev, NULL);
return 0;
}
return -1;
}
static int tr_suspend(struct platform_device *pdev, pm_message_t state)
{
pr_info("enter %s\n", __func__);
pr_info("exit %s\n", __func__);
return 0;
}
static int tr_resume(struct platform_device *pdev)
{
pr_info("%s\n", __func__);
if(gsunxi_dev->count != 0)
de_tr_init();
pr_info("exit %s\n", __func__);
return 0;
}
static void tr_shutdown(struct platform_device *pdev)
{
return ;
}
static long tr_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
unsigned long karg[4];
unsigned long ubuffer[4] = {0};
long ret = -1;
if (copy_from_user((void*)karg,(void __user*)arg,4*sizeof(unsigned long))) {
pr_warn("copy_from_user fail\n");
return -EFAULT;
}
ubuffer[0] = *(unsigned long*)karg;
ubuffer[1] = (*(unsigned long*)(karg+1));
ubuffer[2] = (*(unsigned long*)(karg+2));
ubuffer[3] = (*(unsigned long*)(karg+3));
switch(cmd) {
case TR_REQUEST:
{
/* request a chan */
unsigned int id;
id = sunxi_tr_request();
if (put_user(id, (unsigned int __user *)ubuffer[0])) {
pr_err("%s: put_user fail\n", __func__);
return -EFAULT;
}
if (0 == id)
ret = -EFAULT;
else
ret = 0;
break;
}
case TR_COMMIT:
{
tr_info info;
if(copy_from_user(&info, (void __user *)ubuffer[1],sizeof(tr_info))) {
pr_warn("%s, copy_from_user fail\n", __func__);
return -EFAULT;
}
ret = sunxi_tr_commit(ubuffer[0], &info);
break;
}
case TR_RELEASE:
{
/* release a chan */
ret = sunxi_tr_release(ubuffer[0]);
break;
}
case TR_QUERY:
{
/* query status */
ret = sunxi_tr_query(ubuffer[0]);
break;
}
case TR_SET_TIMEOUT:
{
/* set timeout */
ret = sunxi_tr_set_timeout(ubuffer[0], ubuffer[1]);
break;
}
default:
break;
}
return ret;
}
#ifdef CONFIG_COMPAT
static long tr_compat_ioctl32(struct file *file, unsigned int cmd,
unsigned long arg)
{
compat_uptr_t karg[4];
unsigned long __user *ubuffer;
int ret = 0;
if (copy_from_user((void *)karg, (void __user *)arg,
4 * sizeof(compat_uptr_t))) {
pr_err("copy_from_user fail\n");
return -EFAULT;
}
ubuffer = compat_alloc_user_space(4 * sizeof(unsigned long));
if (!access_ok(VERIFY_WRITE, ubuffer, 4 * sizeof(unsigned long)))
return -EFAULT;
if (put_user(karg[0], &ubuffer[0])
|| put_user(karg[1], &ubuffer[1])
|| put_user(karg[2], &ubuffer[2])
|| put_user(karg[3], &ubuffer[3])) {
pr_err("put_user fail\n");
return -EFAULT;
}
switch (cmd) {
case TR_REQUEST:
{
/* request a chan */
compat_uint_t id = 0;
id = sunxi_tr_request();
if (put_user(id, (compat_uint_t __user *)ubuffer[0])) {
pr_err("%s, put tr user failed.", __func__);
ret = -EFAULT;
} else {
ret = 0;
}
break;
}
case TR_COMMIT:
{
tr_info info;
compat_uint_t id = karg[0];
ret = copy_from_user(&info, (void __user *)ubuffer[1],
sizeof(tr_info));
if (ret) {
pr_warn("%s, tr copy_from_user fail\n", __func__);
return -EFAULT;
}
ret = sunxi_tr_commit(id, &info);
break;
}
case TR_RELEASE:
{
/* release a chan */
compat_uint_t id = karg[0];
ret = sunxi_tr_release(id);
break;
}
case TR_QUERY:
{
/* query status */
compat_uint_t id = karg[0];
ret = sunxi_tr_query((unsigned long)id);
break;
}
case TR_SET_TIMEOUT:
{
/* set timeout */
compat_uint_t id = karg[0];
compat_uptr_t timeout = karg[1];
ret = sunxi_tr_set_timeout(id, (unsigned long)timeout);
break;
}
default:
break;
}
return ret;
}
#endif
static const struct file_operations tr_fops = {
.owner = THIS_MODULE,
.open = tr_open,
.release = tr_release,
.write = tr_write,
.read = tr_read,
.unlocked_ioctl = tr_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = tr_compat_ioctl32,
#endif
};
#if !defined(CONFIG_OF)
static struct platform_device tr_device = {
.name = "transform",
.id = -1,
.num_resources = ARRAY_SIZE(tr_resource),
.resource = tr_resource,
.dev =
{
},
};
#else
static const struct of_device_id sunxi_tr_match[] = {
{ .compatible = "allwinner,sun50i-tr", },
{ .compatible = "allwinner,sun8iw11-tr", },
{},
};
#endif
static struct platform_driver tr_driver = {
.probe = tr_probe,
.remove = tr_remove,
.suspend = tr_suspend,
.resume = tr_resume,
.shutdown = tr_shutdown,
.driver =
{
.name = "transform",
.owner = THIS_MODULE,
.of_match_table = sunxi_tr_match,
},
};
static int __init tr_module_init(void)
{
int ret = 0, err;
alloc_chrdev_region(&devid, 0, 1, "transform");
tr_cdev = cdev_alloc();
cdev_init(tr_cdev, &tr_fops);
tr_cdev->owner = THIS_MODULE;
err = cdev_add(tr_cdev, devid, 1);
if (err) {
pr_warn("cdev_add fail\n");
return -1;
}
tr_class = class_create(THIS_MODULE, "transform");
if (IS_ERR(tr_class)) {
pr_warn("class_create fail\n");
return -1;
}
tr_dev = device_create(tr_class, NULL, devid, NULL, "transform");
#if !defined(CONFIG_OF)
ret = platform_device_register(&tr_device);
#endif
if (ret == 0) {
ret = platform_driver_register(&tr_driver);
}
return ret;
}
static void __exit tr_module_exit(void)
{
platform_driver_unregister(&tr_driver);
#if !defined(CONFIG_OF)
platform_device_unregister(&tr_device);
#endif
device_destroy(tr_class, devid);
class_destroy(tr_class);
cdev_del(tr_cdev);
}
subsys_initcall_sync(tr_module_init);
module_exit(tr_module_exit);
MODULE_AUTHOR("tyle");
MODULE_DESCRIPTION("transform driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:transform");