| // 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"); |