[PATCH net-next v6 2/2] net: Add Qcom WWAN control driver

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



The MHI WWWAN control driver allows MHI QCOM-based modems to expose
different modem control protocols/ports to userspace, so that userspace
modem tools or daemon (e.g. ModemManager) can control WWAN config
and state (APN config, SMS, provider selection...). A QCOM-based
modem can expose one or several of the following protocols:
- AT: Well known AT commands interactive protocol (microcom, minicom...)
- MBIM: Mobile Broadband Interface Model (libmbim, mbimcli)
- QMI: QCOM MSM/Modem Interface (libqmi, qmicli)
- QCDM: QCOM Modem diagnostic interface (libqcdm)
- FIREHOSE: XML-based protocol for Modem firmware management
        (qmi-firmware-update)

The different interfaces are exposed as character devices through the
WWAN subsystem, in a similar way as for USB modem variants (cdc-wdm).

Note that this patch is mostly a rework of the earlier MHI UCI
tentative that was a generic interface for accessing MHI bus from
userspace. As suggested, this new version is WWAN specific and is
dedicated to only expose channels used for controlling a modem, and
for which related opensource userpace support exist.

Signed-off-by: Loic Poulain <loic.poulain@xxxxxxxxxx>
---
 v2: not part of the series
 v3: not part of the series
 v4: Introduce WWAN framework/subsystem
 v5: Specify WWAN_CORE module name in Kconfig
 v6: - Move to unified wwan_core.c file
     - Make wwan_port a device
     - Get rid of useless various dev lists (use wwan class)
     - Get rid of useless wwan dev usage counter and mutex
     - do not expose wwan_create_device, it's indirectly called via create_port
     - Increase minor count to the whole available minor range
     - private_data as wwan_create_port parameter

 drivers/net/wwan/Kconfig         |  14 +
 drivers/net/wwan/Makefile        |   2 +
 drivers/net/wwan/mhi_wwan_ctrl.c | 534 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 550 insertions(+)
 create mode 100644 drivers/net/wwan/mhi_wwan_ctrl.c

diff --git a/drivers/net/wwan/Kconfig b/drivers/net/wwan/Kconfig
index 545fe54..ce0bbfb 100644
--- a/drivers/net/wwan/Kconfig
+++ b/drivers/net/wwan/Kconfig
@@ -19,4 +19,18 @@ config WWAN_CORE
 	  To compile this driver as a module, choose M here: the module will be
 	  called wwan.
 
+config MHI_WWAN_CTRL
+	tristate "MHI WWAN control driver for QCOM-based PCIe modems"
+	select WWAN_CORE
+	depends on MHI_BUS
+	help
+	  MHI WWAN CTRL allows QCOM-based PCIe modems to expose different modem
+	  control protocols/ports to userspace, including AT, MBIM, QMI, DIAG
+	  and FIREHOSE. These protocols can be accessed directly from userspace
+	  (e.g. AT commands) or via libraries/tools (e.g. libmbim, libqmi,
+	  libqcdm...).
+
+	  To compile this driver as a module, choose M here: the module will be
+	  called mhi_wwan_ctrl
+
 endif # WWAN
diff --git a/drivers/net/wwan/Makefile b/drivers/net/wwan/Makefile
index 934590b..556cd90 100644
--- a/drivers/net/wwan/Makefile
+++ b/drivers/net/wwan/Makefile
@@ -5,3 +5,5 @@
 
 obj-$(CONFIG_WWAN_CORE) += wwan.o
 wwan-objs += wwan_core.o
+
+obj-$(CONFIG_MHI_WWAN_CTRL) += mhi_wwan_ctrl.o
diff --git a/drivers/net/wwan/mhi_wwan_ctrl.c b/drivers/net/wwan/mhi_wwan_ctrl.c
new file mode 100644
index 0000000..0f70b07
--- /dev/null
+++ b/drivers/net/wwan/mhi_wwan_ctrl.c
@@ -0,0 +1,534 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2018-2021, The Linux Foundation. All rights reserved.*/
+
+#include <linux/kernel.h>
+#include <linux/mhi.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/wwan.h>
+
+/* MHI wwan device flags */
+#define MHI_WWAN_DL_CAP		BIT(0)
+#define MHI_WWAN_UL_CAP		BIT(1)
+#define MHI_WWAN_CONNECTED	BIT(2)
+#define MHI_WWAN_TX_FULL	BIT(3)
+
+#define MHI_WWAN_MAX_MTU	0x8000
+
+struct mhi_wwan_buf {
+	struct list_head node;
+	void *data;
+	size_t len;
+	size_t consumed;
+};
+
+struct mhi_wwan_dev {
+	/* Lower level is a mhi dev, upper level is a wwan port */
+	struct mhi_device *mhi_dev;
+	struct wwan_port *wwan_port;
+
+	/* Protect mhi device against concurrent accesses (queue, remove...) */
+	struct mutex mhi_dev_lock;
+	unsigned int mhi_dev_start_count;
+
+	/* read/write wait queues */
+	wait_queue_head_t ul_wq;
+	wait_queue_head_t dl_wq;
+
+	/* Protect local download queue against concurent update (read/xfer_cb) */
+	spinlock_t dl_queue_lock;
+	struct list_head dl_queue;
+
+	unsigned long flags;
+	size_t mtu;
+
+	/* kref is used to safely refcount and release a mhi_wwan_dev instance,
+	 * which is shared between mhi-dev probe/remove and wwan-port fops.
+	 */
+	struct kref refcnt;
+};
+
+static void mhi_wwan_ctrl_dev_release(struct kref *ref)
+{
+	struct mhi_wwan_dev *mhiwwan = container_of(ref, struct mhi_wwan_dev, refcnt);
+	struct mhi_wwan_buf *buf_itr, *tmp;
+
+	/* Release non consumed buffers (lock-free safe in release)  */
+	list_for_each_entry_safe(buf_itr, tmp, &mhiwwan->dl_queue, node) {
+		list_del(&buf_itr->node);
+		kfree(buf_itr->data);
+	}
+
+	mutex_destroy(&mhiwwan->mhi_dev_lock);
+
+	kfree(mhiwwan);
+}
+
+static int mhi_wwan_ctrl_fill_inbound(struct mhi_wwan_dev *mhiwwan)
+{
+	struct mhi_device *mhi_dev = mhiwwan->mhi_dev;
+	int nr_desc, ret = -EIO;
+
+	lockdep_assert_held(&mhiwwan->mhi_dev_lock);
+
+	/* skip queuing without error if dl channel is not supported. This
+	 * allows open to succeed for devices supporting ul channels only.
+	 */
+	if (!mhi_dev->dl_chan)
+		return 0;
+
+	nr_desc = mhi_get_free_desc_count(mhi_dev, DMA_FROM_DEVICE);
+
+	while (nr_desc--) {
+		struct mhi_wwan_buf *mhibuf;
+		void *buf;
+
+		buf = kmalloc(mhiwwan->mtu + sizeof(*mhibuf), GFP_KERNEL);
+		if (!buf)
+			return -ENOMEM;
+
+		/* save mhi_wwan_buf meta info at the end of the buffer */
+		mhibuf = buf + mhiwwan->mtu;
+		mhibuf->data = buf;
+
+		ret = mhi_queue_buf(mhi_dev, DMA_FROM_DEVICE, buf, mhiwwan->mtu, MHI_EOT);
+		if (ret) {
+			dev_err(&mhi_dev->dev, "Failed to queue buffer\n");
+			kfree(buf);
+			return ret;
+		}
+	}
+
+	return ret;
+}
+
+static int mhi_wwan_ctrl_start(struct mhi_wwan_dev *mhiwwan)
+{
+	struct mhi_device *mhi_dev;
+	int ret = 0;
+
+	mutex_lock(&mhiwwan->mhi_dev_lock);
+
+	mhi_dev = mhiwwan->mhi_dev;
+	if (!mhi_dev) { /* mhi dev got hot-unplugged */
+		ret = -ENODEV;
+		goto exit_unlock;
+	}
+
+	/* Do not start if already started (by previous open) */
+	if (mhiwwan->mhi_dev_start_count)
+		goto exit_started;
+
+	/* Start mhi device's channel(s) */
+	ret = mhi_prepare_for_transfer(mhi_dev);
+	if (ret)
+		goto exit_unlock;
+
+	/* Add buffers to inbound queue */
+	ret = mhi_wwan_ctrl_fill_inbound(mhiwwan);
+	if (ret) {
+		mhi_unprepare_from_transfer(mhi_dev);
+		goto exit_unlock;
+	}
+
+exit_started:
+	mhiwwan->mhi_dev_start_count++;
+exit_unlock:
+	mutex_unlock(&mhiwwan->mhi_dev_lock);
+	return ret;
+}
+
+static void mhi_wwan_ctrl_stop(struct mhi_wwan_dev *mhiwwan)
+{
+	mutex_lock(&mhiwwan->mhi_dev_lock);
+
+	mhiwwan->mhi_dev_start_count--;
+
+	if (mhiwwan->mhi_dev && !mhiwwan->mhi_dev_start_count)
+		mhi_unprepare_from_transfer(mhiwwan->mhi_dev);
+
+	mutex_unlock(&mhiwwan->mhi_dev_lock);
+}
+
+static int mhi_wwan_ctrl_open(struct inode *inode, struct file *filp)
+{
+	struct mhi_wwan_dev *mhiwwan = filp->private_data;
+	int ret = 0;
+
+	kref_get(&mhiwwan->refcnt);
+
+	ret = mhi_wwan_ctrl_start(mhiwwan);
+	if (ret)
+		kref_put(&mhiwwan->refcnt, mhi_wwan_ctrl_dev_release);
+
+	return ret;
+}
+
+static int mhi_wwan_ctrl_release(struct inode *inode, struct file *filp)
+{
+	struct mhi_wwan_dev *mhiwwan = filp->private_data;
+
+	mhi_wwan_ctrl_stop(mhiwwan);
+	kref_put(&mhiwwan->refcnt, mhi_wwan_ctrl_dev_release);
+
+	return 0;
+}
+
+static __poll_t mhi_wwan_ctrl_poll(struct file *filp, poll_table *wait)
+{
+	struct mhi_wwan_dev *mhiwwan = filp->private_data;
+	__poll_t mask = 0;
+
+	poll_wait(filp, &mhiwwan->ul_wq, wait);
+	poll_wait(filp, &mhiwwan->dl_wq, wait);
+
+	/* Any buffer in the DL queue ? */
+	spin_lock_bh(&mhiwwan->dl_queue_lock);
+	if (!list_empty(&mhiwwan->dl_queue))
+		mask |= EPOLLIN | EPOLLRDNORM;
+	spin_unlock_bh(&mhiwwan->dl_queue_lock);
+
+	/* If MHI queue is not full, write is possible */
+	mutex_lock(&mhiwwan->mhi_dev_lock);
+	if (!mhiwwan->mhi_dev) /* mhi dev got hot-unplugged */
+		mask = EPOLLHUP;
+	else if (!mhi_queue_is_full(mhiwwan->mhi_dev, DMA_TO_DEVICE))
+		mask |= EPOLLOUT | EPOLLWRNORM;
+	mutex_unlock(&mhiwwan->mhi_dev_lock);
+
+	return mask;
+}
+
+static bool is_write_blocked(struct mhi_wwan_dev *mhiwwan)
+{
+	return test_bit(MHI_WWAN_TX_FULL, &mhiwwan->flags) &&
+		test_bit(MHI_WWAN_CONNECTED, &mhiwwan->flags);
+}
+
+static int mhi_wwan_wait_writable(struct mhi_wwan_dev *mhiwwan, bool nonblock)
+{
+	int ret;
+
+	if (is_write_blocked(mhiwwan)) {
+		if (nonblock)
+			return -EAGAIN;
+
+		ret = wait_event_interruptible(mhiwwan->ul_wq,
+					       !is_write_blocked(mhiwwan));
+		if (ret < 0)
+			return -ERESTARTSYS;
+	}
+
+	if (!test_bit(MHI_WWAN_CONNECTED, &mhiwwan->flags))
+		return -ENODEV;
+
+	return 0;
+}
+
+static ssize_t mhi_wwan_ctrl_write(struct file *filp, const char __user *buf,
+				   size_t count, loff_t *offp)
+{
+	struct mhi_wwan_dev *mhiwwan = filp->private_data;
+	size_t xfer_size = min_t(size_t, count, mhiwwan->mtu);
+	void *kbuf;
+	int ret;
+
+	if (!test_bit(MHI_WWAN_UL_CAP, &mhiwwan->flags))
+		return -EOPNOTSUPP;
+
+	if (!buf || !count)
+		return -EINVAL;
+
+	ret = mhi_wwan_wait_writable(mhiwwan, filp->f_flags & O_NONBLOCK);
+	if (ret)
+		return ret;
+
+	kbuf = kmalloc(xfer_size, GFP_KERNEL);
+	if (!kbuf)
+		return -ENOMEM;
+
+	if (copy_from_user(kbuf, buf, xfer_size)) {
+		ret = -EFAULT;
+		goto exit_free_buf;
+	}
+
+	mutex_lock(&mhiwwan->mhi_dev_lock);
+
+	if (!mhiwwan->mhi_dev) { /* mhi dev got hot-unplugged */
+		ret = -ENODEV;
+		goto exit_unlock;
+	}
+
+	ret = mhi_queue_buf(mhiwwan->mhi_dev, DMA_TO_DEVICE, kbuf, xfer_size, MHI_EOT);
+	if (ret)
+		goto exit_unlock;
+
+	kbuf = NULL; /* Buffer is owned by MHI core now, prevent freeing */
+
+	if (mhi_queue_is_full(mhiwwan->mhi_dev, DMA_TO_DEVICE))
+		set_bit(MHI_WWAN_TX_FULL, &mhiwwan->flags);
+
+exit_unlock:
+	mutex_unlock(&mhiwwan->mhi_dev_lock);
+exit_free_buf:
+	kfree(kbuf);
+
+	return ret ? ret : xfer_size;
+}
+
+static void mhi_wwan_ctrl_recycle_mhibuf(struct mhi_wwan_dev *mhiwwan,
+					 struct mhi_wwan_buf *mhibuf)
+{
+	struct mhi_device *mhi_dev;
+	int ret;
+
+	mutex_lock(&mhiwwan->mhi_dev_lock);
+
+	mhi_dev = mhiwwan->mhi_dev;
+	if (!mhi_dev) /* mhi dev got hot-unplugged */
+		kfree(mhibuf->data);
+
+	ret = mhi_queue_buf(mhi_dev, DMA_FROM_DEVICE, mhibuf->data,
+			    mhiwwan->mtu, MHI_EOT);
+	if (ret) {
+		dev_warn(&mhi_dev->dev, "Unable to recycle buffer (%d)\n", ret);
+		kfree(mhibuf->data);
+	}
+
+	mutex_unlock(&mhiwwan->mhi_dev_lock);
+}
+
+static bool is_read_blocked(struct mhi_wwan_dev *mhiwwan)
+{
+	return test_bit(MHI_WWAN_CONNECTED, &mhiwwan->flags) &&
+		list_empty(&mhiwwan->dl_queue);
+}
+
+/* Called with dl_queue lock to wait for buffer in the dl_queue */
+static int mhi_wwan_wait_dlqueue_lock_irq(struct mhi_wwan_dev *mhiwwan, bool nonblock)
+{
+	int ret;
+
+	lockdep_assert_held(&mhiwwan->dl_queue_lock);
+
+	if (is_read_blocked(mhiwwan)) {
+		if (nonblock)
+			return -EAGAIN;
+
+		ret = wait_event_interruptible_lock_irq(mhiwwan->dl_wq,
+							!is_read_blocked(mhiwwan),
+							mhiwwan->dl_queue_lock);
+		if (ret < 0)
+			return -ERESTARTSYS;
+	}
+
+	if (!test_bit(MHI_WWAN_CONNECTED, &mhiwwan->flags))
+		return -ENODEV;
+
+	return 0;
+}
+
+static ssize_t mhi_wwan_ctrl_read(struct file *filp, char __user *buf,
+				  size_t count, loff_t *ppos)
+{
+	struct mhi_wwan_dev *mhiwwan = filp->private_data;
+	struct mhi_wwan_buf *mhibuf;
+	size_t copy_len;
+	char *copy_ptr;
+	int ret = 0;
+
+	if (!test_bit(MHI_WWAN_DL_CAP, &mhiwwan->flags))
+		return -EOPNOTSUPP;
+
+	if (!buf)
+		return -EINVAL;
+
+	/* _irq variant to use wait_event_interruptible_lock_irq() */
+	spin_lock_irq(&mhiwwan->dl_queue_lock);
+
+	ret = mhi_wwan_wait_dlqueue_lock_irq(mhiwwan, filp->f_flags & O_NONBLOCK);
+	if (ret)
+		goto err_unlock;
+
+	mhibuf = list_first_entry_or_null(&mhiwwan->dl_queue, struct mhi_wwan_buf, node);
+	if (!mhibuf) {
+		ret = -EIO;
+		goto err_unlock;
+	}
+
+	/* Consume the buffer */
+	copy_len = min_t(size_t, count, mhibuf->len - mhibuf->consumed);
+	copy_ptr = mhibuf->data + mhibuf->consumed;
+	mhibuf->consumed += copy_len;
+
+	if (mhibuf->consumed != mhibuf->len) {
+		/* buffer has not been fully consumed, and stay in the local
+		 * dl_queue.
+		 */
+		if (copy_to_user(buf, copy_ptr, copy_len))
+			ret = -EFAULT;
+		spin_unlock_irq(&mhiwwan->dl_queue_lock);
+	} else {
+		/* buffer has been fully consumed, it can be removed from
+		 * the local dl queue and recycled for a new mhi transfer.
+		 */
+		list_del(&mhibuf->node);
+		spin_unlock_irq(&mhiwwan->dl_queue_lock);
+
+		if (copy_to_user(buf, copy_ptr, copy_len))
+			ret = -EFAULT;
+
+		mhi_wwan_ctrl_recycle_mhibuf(mhiwwan, mhibuf);
+	}
+
+	return ret ? ret : copy_len;
+
+err_unlock:
+	spin_unlock_irq(&mhiwwan->dl_queue_lock);
+
+	return ret;
+}
+
+static const struct file_operations mhidev_fops = {
+	.owner = THIS_MODULE,
+	.open = mhi_wwan_ctrl_open,
+	.release = mhi_wwan_ctrl_release,
+	.read = mhi_wwan_ctrl_read,
+	.write = mhi_wwan_ctrl_write,
+	.poll = mhi_wwan_ctrl_poll,
+};
+
+static void mhi_ul_xfer_cb(struct mhi_device *mhi_dev,
+			   struct mhi_result *mhi_result)
+{
+	struct mhi_wwan_dev *mhiwwan = dev_get_drvdata(&mhi_dev->dev);
+
+	dev_dbg(&mhi_dev->dev, "%s: status: %d xfer_len: %zu\n", __func__,
+		mhi_result->transaction_status, mhi_result->bytes_xferd);
+
+	kfree(mhi_result->buf_addr);
+
+	if (!mhi_queue_is_full(mhiwwan->mhi_dev, DMA_TO_DEVICE)) {
+		clear_bit(MHI_WWAN_TX_FULL, &mhiwwan->flags);
+		wake_up_interruptible(&mhiwwan->ul_wq);
+	}
+}
+
+static void mhi_dl_xfer_cb(struct mhi_device *mhi_dev,
+			   struct mhi_result *mhi_result)
+{
+	struct mhi_wwan_dev *mhiwwan = dev_get_drvdata(&mhi_dev->dev);
+	struct mhi_wwan_buf *mhibuf;
+
+	dev_dbg(&mhi_dev->dev, "%s: status: %d receive_len: %zu\n", __func__,
+		mhi_result->transaction_status, mhi_result->bytes_xferd);
+
+	if (mhi_result->transaction_status &&
+	    mhi_result->transaction_status != -EOVERFLOW) {
+		kfree(mhi_result->buf_addr);
+		return;
+	}
+
+	/* mhibuf is placed at the end of the buffer (cf mhi_wwan_ctrl_fill_inbound) */
+	mhibuf = mhi_result->buf_addr + mhiwwan->mtu;
+	mhibuf->data = mhi_result->buf_addr;
+	mhibuf->len = mhi_result->bytes_xferd;
+	mhibuf->consumed = 0;
+
+	spin_lock_bh(&mhiwwan->dl_queue_lock);
+	list_add_tail(&mhibuf->node, &mhiwwan->dl_queue);
+	spin_unlock_bh(&mhiwwan->dl_queue_lock);
+
+	wake_up_interruptible(&mhiwwan->dl_wq);
+}
+
+static int mhi_wwan_ctrl_probe(struct mhi_device *mhi_dev,
+			       const struct mhi_device_id *id)
+{
+	struct mhi_controller *cntrl = mhi_dev->mhi_cntrl;
+	struct mhi_wwan_dev *mhiwwan;
+
+	/* Create mhi_wwan data context */
+	mhiwwan = kzalloc(sizeof(*mhiwwan), GFP_KERNEL);
+	if (!mhiwwan)
+		return -ENOMEM;
+
+	/* Init mhi_wwan data */
+	kref_init(&mhiwwan->refcnt);
+	mutex_init(&mhiwwan->mhi_dev_lock);
+	init_waitqueue_head(&mhiwwan->ul_wq);
+	init_waitqueue_head(&mhiwwan->dl_wq);
+	spin_lock_init(&mhiwwan->dl_queue_lock);
+	INIT_LIST_HEAD(&mhiwwan->dl_queue);
+	mhiwwan->mhi_dev = mhi_dev;
+	mhiwwan->mtu = MHI_WWAN_MAX_MTU;
+	set_bit(MHI_WWAN_CONNECTED, &mhiwwan->flags);
+
+	if (mhi_dev->dl_chan)
+		set_bit(MHI_WWAN_DL_CAP, &mhiwwan->flags);
+	if (mhi_dev->ul_chan)
+		set_bit(MHI_WWAN_UL_CAP, &mhiwwan->flags);
+
+	dev_set_drvdata(&mhi_dev->dev, mhiwwan);
+
+	/* Register as a wwan port, id->driver_data contains wwan port type */
+	mhiwwan->wwan_port = wwan_create_port(&cntrl->mhi_dev->dev,
+					      id->driver_data,
+					      &mhidev_fops, mhiwwan);
+	if (IS_ERR(mhiwwan->wwan_port)) {
+		mutex_destroy(&mhiwwan->mhi_dev_lock);
+		kfree(mhiwwan);
+		return PTR_ERR(mhiwwan->wwan_port);
+	}
+
+	return 0;
+};
+
+static void mhi_wwan_ctrl_remove(struct mhi_device *mhi_dev)
+{
+	struct mhi_wwan_dev *mhiwwan = dev_get_drvdata(&mhi_dev->dev);
+
+	wwan_remove_port(mhiwwan->wwan_port);
+	dev_set_drvdata(&mhi_dev->dev, NULL);
+	clear_bit(MHI_WWAN_CONNECTED, &mhiwwan->flags);
+
+	/* Unlink mhi_dev from mhi_wwan_dev */
+	mutex_lock(&mhiwwan->mhi_dev_lock);
+	mhiwwan->mhi_dev = NULL;
+	mutex_unlock(&mhiwwan->mhi_dev_lock);
+
+	/* wake up any blocked user */
+	wake_up_interruptible(&mhiwwan->dl_wq);
+	wake_up_interruptible(&mhiwwan->ul_wq);
+
+	kref_put(&mhiwwan->refcnt, mhi_wwan_ctrl_dev_release);
+}
+
+static const struct mhi_device_id mhi_wwan_ctrl_match_table[] = {
+	{ .chan = "DUN", .driver_data = WWAN_PORT_AT },
+	{ .chan = "MBIM", .driver_data = WWAN_PORT_MBIM },
+	{ .chan = "QMI", .driver_data = WWAN_PORT_QMI },
+	{ .chan = "DIAG", .driver_data = WWAN_PORT_QCDM },
+	{ .chan = "FIREHOSE", .driver_data = WWAN_PORT_FIREHOSE },
+	{},
+};
+MODULE_DEVICE_TABLE(mhi, mhi_wwan_ctrl_match_table);
+
+static struct mhi_driver mhi_wwan_ctrl_driver = {
+	.id_table = mhi_wwan_ctrl_match_table,
+	.remove = mhi_wwan_ctrl_remove,
+	.probe = mhi_wwan_ctrl_probe,
+	.ul_xfer_cb = mhi_ul_xfer_cb,
+	.dl_xfer_cb = mhi_dl_xfer_cb,
+	.driver = {
+		.name = "mhi_wwan_ctrl",
+	},
+};
+
+module_mhi_driver(mhi_wwan_ctrl_driver);
+
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("MHI WWAN CTRL Driver");
+MODULE_AUTHOR("Hemant Kumar <hemantk@xxxxxxxxxxxxxx>");
+MODULE_AUTHOR("Loic Poulain <loic.poulain@xxxxxxxxxx>");
-- 
2.7.4




[Index of Archives]     [Linux ARM Kernel]     [Linux ARM]     [Linux Omap]     [Fedora ARM]     [Linux for Sparc]     [IETF Annouce]     [Security]     [Bugtraq]     [Linux MIPS]     [ECOS]     [Asterisk Internet PBX]     [Linux API]

  Powered by Linux