blob: a6eb044ff262c4a0de7518701d94a93da33dd48b [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2016-2019, The Linux Foundation. All rights reserved.
*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/platform_device.h>
#include <linux/of_device.h>
#include <linux/vmalloc.h>
#include <linux/rpmsg.h>
#include "sound/wcd-dsp-glink.h"
#define WDSP_GLINK_DRIVER_NAME "wcd-dsp-glink"
#define WDSP_MAX_WRITE_SIZE (256 * 1024)
#define WDSP_MAX_READ_SIZE (4 * 1024)
#define WDSP_WRITE_PKT_SIZE (sizeof(struct wdsp_write_pkt))
#define WDSP_CMD_PKT_SIZE (sizeof(struct wdsp_cmd_pkt))
#define MINOR_NUMBER_COUNT 1
#define RESP_QUEUE_SIZE 3
#define TIMEOUT_MS 2000
enum wdsp_ch_state {
WDSP_CH_DISCONNECTED,
WDSP_CH_CONNECTED,
};
struct wdsp_glink_dev {
struct class *cls;
struct device *dev;
struct cdev cdev;
dev_t dev_num;
};
struct wdsp_rsp_que {
/* Size of valid data in buffer */
u32 buf_size;
/* Response buffer */
u8 buf[WDSP_MAX_READ_SIZE];
};
struct wdsp_ch {
struct wdsp_glink_priv *wpriv;
/* rpmsg handle */
void *handle;
/* Channel states like connect, disconnect */
int ch_state;
char ch_name[RPMSG_NAME_SIZE];
spinlock_t ch_lock;
};
struct wdsp_tx_buf {
struct work_struct tx_work;
/* Glink channel information */
struct wdsp_ch *ch;
/* Tx buffer to send to glink */
u8 buf[0];
};
struct wdsp_glink_priv {
/* Respone buffer related */
u8 rsp_cnt;
struct wdsp_rsp_que rsp[RESP_QUEUE_SIZE];
u8 write_idx;
u8 read_idx;
struct completion rsp_complete;
spinlock_t rsp_lock;
/* Glink channel related */
int no_of_channels;
struct wdsp_ch **ch;
struct workqueue_struct *work_queue;
/* Wait for all channels state before sending any command */
wait_queue_head_t ch_state_wait;
struct wdsp_glink_dev *wdev;
struct device *dev;
};
static struct wdsp_glink_priv *wpriv;
static struct wdsp_ch *wdsp_get_ch(char *ch_name)
{
int i;
for (i = 0; i < wpriv->no_of_channels; i++) {
if (!strcmp(ch_name, wpriv->ch[i]->ch_name))
return wpriv->ch[i];
}
return NULL;
}
/*
* wdsp_rpmsg_callback - Rpmsg callback for responses
* rpdev: Rpmsg device structure
* data: Pointer to the Rx data
* len: Size of the Rx data
* priv: Private pointer to the channel
* addr: Address variable
* Returns 0 on success and an appropriate error value on failure
*/
static int wdsp_rpmsg_callback(struct rpmsg_device *rpdev, void *data,
int len, void *priv, u32 addr__unused)
{
struct wdsp_ch *ch = dev_get_drvdata(&rpdev->dev);
struct wdsp_glink_priv *wpriv;
unsigned long flags;
u8 *rx_buf;
u8 rsp_cnt = 0;
if (!ch || !data) {
pr_err("%s: Invalid ch or data\n", __func__);
return -EINVAL;
}
wpriv = ch->wpriv;
rx_buf = (u8 *)data;
if (len > WDSP_MAX_READ_SIZE) {
dev_info_ratelimited(wpriv->dev, "%s: Size %d is greater than allowed %d\n",
__func__, len, WDSP_MAX_READ_SIZE);
len = WDSP_MAX_READ_SIZE;
}
dev_dbg_ratelimited(wpriv->dev, "%s: copy into buffer %d\n", __func__,
wpriv->rsp_cnt);
if (wpriv->rsp_cnt >= RESP_QUEUE_SIZE) {
dev_info_ratelimited(wpriv->dev, "%s: Resp Queue is Full. Ignore new one.\n",
__func__);
return -EINVAL;
}
spin_lock_irqsave(&wpriv->rsp_lock, flags);
rsp_cnt = wpriv->rsp_cnt;
memcpy(wpriv->rsp[wpriv->write_idx].buf, rx_buf, len);
wpriv->rsp[wpriv->write_idx].buf_size = len;
wpriv->write_idx = (wpriv->write_idx + 1) % RESP_QUEUE_SIZE;
wpriv->rsp_cnt = ++rsp_cnt;
spin_unlock_irqrestore(&wpriv->rsp_lock, flags);
complete(&wpriv->rsp_complete);
return 0;
}
/*
* wdsp_rpmsg_probe - Rpmsg channel probe function
* rpdev: Rpmsg device structure
* Returns 0 on success and an appropriate error value on failure
*/
static int wdsp_rpmsg_probe(struct rpmsg_device *rpdev)
{
struct wdsp_ch *ch;
ch = wdsp_get_ch(rpdev->id.name);
if (!ch) {
dev_err(&rpdev->dev, "%s, Invalid Channel [%s]\n",
__func__, rpdev->id.name);
return -EINVAL;
}
dev_dbg(&rpdev->dev, "%s: Channel[%s] state[Up]\n",
__func__, rpdev->id.name);
spin_lock(&ch->ch_lock);
ch->handle = rpdev;
ch->ch_state = WDSP_CH_CONNECTED;
spin_unlock(&ch->ch_lock);
dev_set_drvdata(&rpdev->dev, ch);
wake_up(&wpriv->ch_state_wait);
return 0;
}
/*
* wdsp_rpmsg_remove - Rpmsg channel remove function
* rpdev: Rpmsg device structure
*/
static void wdsp_rpmsg_remove(struct rpmsg_device *rpdev)
{
struct wdsp_ch *ch = dev_get_drvdata(&rpdev->dev);
if (!ch) {
dev_err(&rpdev->dev, "%s: Invalid ch\n", __func__);
return;
}
dev_dbg(&rpdev->dev, "%s: Channel[%s] state[Down]\n",
__func__, rpdev->id.name);
spin_lock(&ch->ch_lock);
ch->handle = NULL;
ch->ch_state = WDSP_CH_DISCONNECTED;
spin_unlock(&ch->ch_lock);
dev_set_drvdata(&rpdev->dev, NULL);
}
static bool wdsp_is_ch_connected(struct wdsp_glink_priv *wpriv)
{
int i;
for (i = 0; i < wpriv->no_of_channels; i++) {
spin_lock(&wpriv->ch[i]->ch_lock);
if (wpriv->ch[i]->ch_state != WDSP_CH_CONNECTED) {
spin_unlock(&wpriv->ch[i]->ch_lock);
return false;
}
spin_unlock(&wpriv->ch[i]->ch_lock);
}
return true;
}
static int wdsp_wait_for_all_ch_connect(struct wdsp_glink_priv *wpriv)
{
int ret;
ret = wait_event_timeout(wpriv->ch_state_wait,
wdsp_is_ch_connected(wpriv),
msecs_to_jiffies(TIMEOUT_MS));
if (!ret) {
dev_err_ratelimited(wpriv->dev, "%s: All channels are not connected\n",
__func__);
ret = -ETIMEDOUT;
goto err;
}
ret = 0;
err:
return ret;
}
/*
* wdsp_tx_buf_work - Work queue function to send tx buffer to glink
* work: Work structure
*/
static void wdsp_tx_buf_work(struct work_struct *work)
{
struct wdsp_glink_priv *wpriv;
struct wdsp_ch *ch;
struct wdsp_tx_buf *tx_buf;
struct wdsp_write_pkt *wpkt;
struct wdsp_cmd_pkt *cpkt;
int ret = 0;
struct rpmsg_device *rpdev = NULL;
tx_buf = container_of(work, struct wdsp_tx_buf,
tx_work);
ch = tx_buf->ch;
wpriv = ch->wpriv;
wpkt = (struct wdsp_write_pkt *)tx_buf->buf;
cpkt = (struct wdsp_cmd_pkt *)wpkt->payload;
dev_dbg(wpriv->dev, "%s: ch name = %s, payload size = %d\n",
__func__, cpkt->ch_name, cpkt->payload_size);
spin_lock(&ch->ch_lock);
rpdev = ch->handle;
if (rpdev && ch->ch_state == WDSP_CH_CONNECTED) {
spin_unlock(&ch->ch_lock);
ret = rpmsg_send(rpdev->ept, cpkt->payload,
cpkt->payload_size);
if (ret < 0)
dev_err(wpriv->dev, "%s: rpmsg send failed, ret = %d\n",
__func__, ret);
} else {
spin_unlock(&ch->ch_lock);
if (rpdev)
dev_err(wpriv->dev, "%s: channel %s is not in connected state\n",
__func__, ch->ch_name);
else
dev_err(wpriv->dev, "%s: rpdev is NULL\n", __func__);
}
vfree(tx_buf);
}
/*
* wdsp_glink_read - Read API to send the data to userspace
* file: Pointer to the file structure
* buf: Pointer to the userspace buffer
* count: Number bytes to read from the file
* ppos: Pointer to the position into the file
* Returns 0 on success and an appropriate error value on failure
*/
static ssize_t wdsp_glink_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
int ret = 0, ret1 = 0;
struct wdsp_rsp_que *read_rsp = NULL;
struct wdsp_glink_priv *wpriv;
unsigned long flags;
wpriv = (struct wdsp_glink_priv *)file->private_data;
if (!wpriv) {
pr_err("%s: Invalid private data\n", __func__);
return -EINVAL;
}
if (count > WDSP_MAX_READ_SIZE) {
dev_info_ratelimited(wpriv->dev, "%s: count = %zd is more than WDSP_MAX_READ_SIZE\n",
__func__, count);
count = WDSP_MAX_READ_SIZE;
}
/*
* Complete signal has given from gwdsp_rpmsg_callback()
* or from flush API. Also use interruptible wait_for_completion API
* to allow the system to go in suspend.
*/
ret = wait_for_completion_interruptible(&wpriv->rsp_complete);
if (ret < 0)
return ret;
read_rsp = kzalloc(sizeof(struct wdsp_rsp_que), GFP_KERNEL);
if (!read_rsp)
return -ENOMEM;
spin_lock_irqsave(&wpriv->rsp_lock, flags);
if (wpriv->rsp_cnt) {
wpriv->rsp_cnt--;
dev_dbg(wpriv->dev, "%s: rsp_cnt=%d read from buffer %d\n",
__func__, wpriv->rsp_cnt, wpriv->read_idx);
memcpy(read_rsp, &wpriv->rsp[wpriv->read_idx],
sizeof(struct wdsp_rsp_que));
wpriv->read_idx = (wpriv->read_idx + 1) % RESP_QUEUE_SIZE;
spin_unlock_irqrestore(&wpriv->rsp_lock, flags);
if (count < read_rsp->buf_size) {
ret1 = copy_to_user(buf, read_rsp->buf, count);
/* Return the number of bytes copied */
ret = count;
} else {
ret1 = copy_to_user(buf, read_rsp->buf,
read_rsp->buf_size);
/* Return the number of bytes copied */
ret = read_rsp->buf_size;
}
if (ret1) {
dev_err_ratelimited(wpriv->dev, "%s: copy_to_user failed %d\n",
__func__, ret);
ret = -EFAULT;
goto done;
}
} else {
/*
* This will execute only if flush API is called or
* something wrong with ref_cnt
*/
dev_dbg(wpriv->dev, "%s: resp count = %d\n", __func__,
wpriv->rsp_cnt);
spin_unlock_irqrestore(&wpriv->rsp_lock, flags);
ret = -EINVAL;
}
done:
kfree(read_rsp);
return ret;
}
/*
* wdsp_glink_write - Write API to receive the data from userspace
* file: Pointer to the file structure
* buf: Pointer to the userspace buffer
* count: Number bytes to read from the file
* ppos: Pointer to the position into the file
* Returns 0 on success and an appropriate error value on failure
*/
static ssize_t wdsp_glink_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret = 0, i, tx_buf_size;
struct wdsp_write_pkt *wpkt;
struct wdsp_cmd_pkt *cpkt;
struct wdsp_tx_buf *tx_buf;
struct wdsp_glink_priv *wpriv;
size_t pkt_max_size;
wpriv = (struct wdsp_glink_priv *)file->private_data;
if (!wpriv) {
pr_err("%s: Invalid private data\n", __func__);
ret = -EINVAL;
goto done;
}
if ((count < WDSP_WRITE_PKT_SIZE) ||
(count > WDSP_MAX_WRITE_SIZE)) {
dev_err_ratelimited(wpriv->dev, "%s: Invalid count = %zd\n",
__func__, count);
ret = -EINVAL;
goto done;
}
dev_dbg(wpriv->dev, "%s: count = %zd\n", __func__, count);
tx_buf_size = count + sizeof(struct wdsp_tx_buf);
tx_buf = vzalloc(tx_buf_size);
if (!tx_buf) {
ret = -ENOMEM;
goto done;
}
ret = copy_from_user(tx_buf->buf, buf, count);
if (ret) {
dev_err_ratelimited(wpriv->dev, "%s: copy_from_user failed %d\n",
__func__, ret);
ret = -EFAULT;
goto free_buf;
}
wpkt = (struct wdsp_write_pkt *)tx_buf->buf;
switch (wpkt->pkt_type) {
case WDSP_REG_PKT:
/* Keep this case to support backward compatibility */
vfree(tx_buf);
break;
case WDSP_READY_PKT:
ret = wdsp_wait_for_all_ch_connect(wpriv);
if (ret < 0)
dev_err_ratelimited(wpriv->dev, "%s: Channels not in connected state\n",
__func__);
vfree(tx_buf);
break;
case WDSP_CMD_PKT:
if (count <= (WDSP_WRITE_PKT_SIZE + WDSP_CMD_PKT_SIZE)) {
dev_err_ratelimited(wpriv->dev, "%s: Invalid cmd pkt size = %zd\n",
__func__, count);
ret = -EINVAL;
goto free_buf;
}
cpkt = (struct wdsp_cmd_pkt *)wpkt->payload;
pkt_max_size = sizeof(struct wdsp_write_pkt) +
sizeof(struct wdsp_cmd_pkt) +
cpkt->payload_size;
if (count < pkt_max_size) {
dev_err_ratelimited(wpriv->dev, "%s: Invalid cmd pkt count = %zd, pkt_size = %zd\n",
__func__, count, pkt_max_size);
ret = -EINVAL;
goto free_buf;
}
for (i = 0; i < wpriv->no_of_channels; i++) {
if (!strcmp(cpkt->ch_name, wpriv->ch[i]->ch_name)) {
tx_buf->ch = wpriv->ch[i];
break;
}
}
if (!tx_buf->ch) {
dev_err_ratelimited(wpriv->dev, "%s: Failed to get channel\n",
__func__);
ret = -EINVAL;
goto free_buf;
}
dev_dbg(wpriv->dev, "%s: requested ch_name: %s, pkt_size: %zd\n",
__func__, cpkt->ch_name, pkt_max_size);
spin_lock(&tx_buf->ch->ch_lock);
if (tx_buf->ch->ch_state != WDSP_CH_CONNECTED) {
spin_unlock(&tx_buf->ch->ch_lock);
ret = -ENETRESET;
dev_err_ratelimited(wpriv->dev, "%s: Channels are not in connected state\n",
__func__);
goto free_buf;
}
spin_unlock(&tx_buf->ch->ch_lock);
INIT_WORK(&tx_buf->tx_work, wdsp_tx_buf_work);
queue_work(wpriv->work_queue, &tx_buf->tx_work);
break;
default:
dev_err_ratelimited(wpriv->dev, "%s: Invalid packet type\n",
__func__);
ret = -EINVAL;
vfree(tx_buf);
break;
}
goto done;
free_buf:
vfree(tx_buf);
done:
return ret;
}
/*
* wdsp_glink_open - Open API to initialize private data
* inode: Pointer to the inode structure
* file: Pointer to the file structure
* Returns 0 on success and an appropriate error value on failure
*/
static int wdsp_glink_open(struct inode *inode, struct file *file)
{
pr_debug("%s: wpriv = %pK\n", __func__, wpriv);
file->private_data = wpriv;
return 0;
}
/*
* wdsp_glink_flush - Flush API to unblock read.
* file: Pointer to the file structure
* id: Lock owner ID
* Returns 0 on success and an appropriate error value on failure
*/
static int wdsp_glink_flush(struct file *file, fl_owner_t id)
{
struct wdsp_glink_priv *wpriv;
wpriv = (struct wdsp_glink_priv *)file->private_data;
if (!wpriv) {
pr_err("%s: Invalid private data\n", __func__);
return -EINVAL;
}
complete(&wpriv->rsp_complete);
return 0;
}
/*
* wdsp_glink_release - Release API to clean up resources.
* Whenever a file structure is shared across multiple threads,
* release won't be invoked until all copies are closed
* (file->f_count.counter should be 0). If we need to flush pending
* data when any copy is closed, you should implement the flush method.
*
* inode: Pointer to the inode structure
* file: Pointer to the file structure
* Returns 0 on success and an appropriate error value on failure
*/
static int wdsp_glink_release(struct inode *inode, struct file *file)
{
pr_debug("%s: file->private_data = %pK\n", __func__,
file->private_data);
file->private_data = NULL;
return 0;
}
static struct rpmsg_driver wdsp_rpmsg_driver = {
.probe = wdsp_rpmsg_probe,
.remove = wdsp_rpmsg_remove,
.callback = wdsp_rpmsg_callback,
/* Update this dynamically before register_rpmsg() */
.id_table = NULL,
.drv = {
.name = "wdsp_rpmsg",
},
};
static int wdsp_register_rpmsg(struct platform_device *pdev,
struct wdsp_glink_dev *wdev)
{
int ret = 0;
int i, no_of_channels;
struct rpmsg_device_id *wdsp_rpmsg_id_table, *id_table;
const char *ch_name = NULL;
wpriv = devm_kzalloc(&pdev->dev,
sizeof(struct wdsp_glink_priv), GFP_KERNEL);
if (!wpriv)
return -ENOMEM;
no_of_channels = of_property_count_strings(pdev->dev.of_node,
"qcom,wdsp-channels");
if (no_of_channels < 0) {
dev_err(&pdev->dev, "%s: channel name parse error %d\n",
__func__, no_of_channels);
return -EINVAL;
}
wpriv->ch = devm_kzalloc(&pdev->dev,
(sizeof(struct wdsp_glink_priv *) * no_of_channels),
GFP_KERNEL);
if (!wpriv->ch)
return -ENOMEM;
for (i = 0; i < no_of_channels; i++) {
ret = of_property_read_string_index(pdev->dev.of_node,
"qcom,wdsp-channels", i,
&ch_name);
if (ret) {
dev_err(&pdev->dev, "%s: channel name parse error %d\n",
__func__, ret);
return -EINVAL;
}
wpriv->ch[i] = devm_kzalloc(&pdev->dev,
sizeof(struct wdsp_glink_priv),
GFP_KERNEL);
if (!wpriv->ch[i])
return -ENOMEM;
strlcpy(wpriv->ch[i]->ch_name, ch_name, RPMSG_NAME_SIZE);
wpriv->ch[i]->wpriv = wpriv;
spin_lock_init(&wpriv->ch[i]->ch_lock);
}
init_waitqueue_head(&wpriv->ch_state_wait);
init_completion(&wpriv->rsp_complete);
spin_lock_init(&wpriv->rsp_lock);
wpriv->wdev = wdev;
wpriv->dev = wdev->dev;
wpriv->work_queue = create_singlethread_workqueue("wdsp_glink_wq");
if (!wpriv->work_queue) {
dev_err(&pdev->dev, "%s: Error creating wdsp_glink_wq\n",
__func__);
return -EINVAL;
}
wdsp_rpmsg_id_table = devm_kzalloc(&pdev->dev,
(sizeof(struct rpmsg_device_id) *
(no_of_channels + 1)),
GFP_KERNEL);
if (!wdsp_rpmsg_id_table) {
ret = -ENOMEM;
goto err;
}
wpriv->no_of_channels = no_of_channels;
id_table = wdsp_rpmsg_id_table;
for (i = 0; i < no_of_channels; i++) {
strlcpy(id_table->name, wpriv->ch[i]->ch_name,
RPMSG_NAME_SIZE);
id_table++;
}
wdsp_rpmsg_driver.id_table = wdsp_rpmsg_id_table;
ret = register_rpmsg_driver(&wdsp_rpmsg_driver);
if (ret < 0) {
dev_err(&pdev->dev, "%s: Rpmsg driver register failed, err = %d\n",
__func__, ret);
goto err;
}
return 0;
err:
destroy_workqueue(wpriv->work_queue);
return ret;
}
static const struct file_operations wdsp_glink_fops = {
.owner = THIS_MODULE,
.open = wdsp_glink_open,
.read = wdsp_glink_read,
.write = wdsp_glink_write,
.flush = wdsp_glink_flush,
.release = wdsp_glink_release,
};
static int wdsp_glink_probe(struct platform_device *pdev)
{
int ret;
struct wdsp_glink_dev *wdev;
wdev = devm_kzalloc(&pdev->dev, sizeof(*wdev), GFP_KERNEL);
if (!wdev) {
ret = -ENOMEM;
goto done;
}
ret = alloc_chrdev_region(&wdev->dev_num, 0, MINOR_NUMBER_COUNT,
WDSP_GLINK_DRIVER_NAME);
if (ret < 0) {
dev_err(&pdev->dev, "%s: Failed to alloc char dev, err = %d\n",
__func__, ret);
goto err_chrdev;
}
wdev->cls = class_create(THIS_MODULE, WDSP_GLINK_DRIVER_NAME);
if (IS_ERR(wdev->cls)) {
ret = PTR_ERR(wdev->cls);
dev_err(&pdev->dev, "%s: Failed to create class, err = %d\n",
__func__, ret);
goto err_class;
}
wdev->dev = device_create(wdev->cls, NULL, wdev->dev_num,
NULL, WDSP_GLINK_DRIVER_NAME);
if (IS_ERR(wdev->dev)) {
ret = PTR_ERR(wdev->dev);
dev_err(&pdev->dev, "%s: Failed to create device, err = %d\n",
__func__, ret);
goto err_dev_create;
}
cdev_init(&wdev->cdev, &wdsp_glink_fops);
ret = cdev_add(&wdev->cdev, wdev->dev_num, MINOR_NUMBER_COUNT);
if (ret < 0) {
dev_err(&pdev->dev, "%s: Failed to register char dev, err = %d\n",
__func__, ret);
goto err_cdev_add;
}
ret = wdsp_register_rpmsg(pdev, wdev);
if (ret < 0) {
dev_err(&pdev->dev, "%s: Failed to register with rpmsg, err = %d\n",
__func__, ret);
goto err_cdev_add;
}
platform_set_drvdata(pdev, wpriv);
goto done;
err_cdev_add:
device_destroy(wdev->cls, wdev->dev_num);
err_dev_create:
class_destroy(wdev->cls);
err_class:
unregister_chrdev_region(0, MINOR_NUMBER_COUNT);
err_chrdev:
done:
return ret;
}
static int wdsp_glink_remove(struct platform_device *pdev)
{
struct wdsp_glink_priv *wpriv = platform_get_drvdata(pdev);
unregister_rpmsg_driver(&wdsp_rpmsg_driver);
if (wpriv) {
flush_workqueue(wpriv->work_queue);
destroy_workqueue(wpriv->work_queue);
if (wpriv->wdev) {
cdev_del(&wpriv->wdev->cdev);
device_destroy(wpriv->wdev->cls, wpriv->wdev->dev_num);
class_destroy(wpriv->wdev->cls);
unregister_chrdev_region(0, MINOR_NUMBER_COUNT);
}
}
return 0;
}
static const struct of_device_id wdsp_glink_of_match[] = {
{.compatible = "qcom,wcd-dsp-glink"},
{ }
};
MODULE_DEVICE_TABLE(of, wdsp_glink_of_match);
static struct platform_driver wdsp_glink_driver = {
.probe = wdsp_glink_probe,
.remove = wdsp_glink_remove,
.driver = {
.name = WDSP_GLINK_DRIVER_NAME,
.owner = THIS_MODULE,
.of_match_table = wdsp_glink_of_match,
.suppress_bind_attrs = true,
},
};
static int __init wdsp_glink_init(void)
{
return platform_driver_register(&wdsp_glink_driver);
}
static void __exit wdsp_glink_exit(void)
{
platform_driver_unregister(&wdsp_glink_driver);
}
module_init(wdsp_glink_init);
module_exit(wdsp_glink_exit);
MODULE_DESCRIPTION("SoC WCD_DSP GLINK Driver");
MODULE_LICENSE("GPL v2");