[PATCH v1 3/4] mhi_bus: dev: netdev: add network interface driver

From: Sujeev Dias
Date: Thu Apr 26 2018 - 22:30:09 EST


MHI based net device driver is used for transferring IP
traffic between host and modem. Driver allows clients to
transfer data using standard network interface.

Signed-off-by: Sujeev Dias <sdias@xxxxxxxxxxxxxx>
---
Documentation/devicetree/bindings/bus/mhi.txt | 36 ++
drivers/bus/Kconfig | 1 +
drivers/bus/mhi/Makefile | 2 +-
drivers/bus/mhi/devices/Kconfig | 10 +
drivers/bus/mhi/devices/Makefile | 1 +
drivers/bus/mhi/devices/mhi_netdev.c | 893 ++++++++++++++++++++++++++
6 files changed, 942 insertions(+), 1 deletion(-)
create mode 100644 drivers/bus/mhi/devices/Kconfig
create mode 100644 drivers/bus/mhi/devices/Makefile
create mode 100644 drivers/bus/mhi/devices/mhi_netdev.c

diff --git a/Documentation/devicetree/bindings/bus/mhi.txt b/Documentation/devicetree/bindings/bus/mhi.txt
index ea1b620..172ae7b 100644
--- a/Documentation/devicetree/bindings/bus/mhi.txt
+++ b/Documentation/devicetree/bindings/bus/mhi.txt
@@ -139,3 +139,39 @@ mhi_controller {
<driver specific properties>
};
};
+
+================
+Children Devices
+================
+
+MHI netdev properties
+
+- mhi,chan
+ Usage: required
+ Value type: <string>
+ Definition: Channel name MHI netdev support
+
+- mhi,mru
+ Usage: required
+ Value type: <u32>
+ Definition: Largest packet size interface can receive in bytes.
+
+- mhi,interface-name
+ Usage: optional
+ Value type: <string>
+ Definition: Interface name to be given so clients can identify it
+
+- mhi,recycle-buf
+ Usage: optional
+ Value type: <bool>
+ Definition: Set true if interface support recycling buffers.
+
+========
+Example:
+========
+
+mhi_rmnet@0 {
+ mhi,chan = "IP_HW0";
+ mhi,interface-name = "rmnet_mhi";
+ mhi,mru = <0x4000>;
+};
diff --git a/drivers/bus/Kconfig b/drivers/bus/Kconfig
index fb28002..cc03762 100644
--- a/drivers/bus/Kconfig
+++ b/drivers/bus/Kconfig
@@ -190,5 +190,6 @@ config MHI_DEBUG

source "drivers/bus/fsl-mc/Kconfig"
source drivers/bus/mhi/controllers/Kconfig
+source drivers/bus/mhi/devices/Kconfig

endmenu
diff --git a/drivers/bus/mhi/Makefile b/drivers/bus/mhi/Makefile
index c6a2a91..2382e04 100644
--- a/drivers/bus/mhi/Makefile
+++ b/drivers/bus/mhi/Makefile
@@ -5,4 +5,4 @@
# core layer
obj-y += core/
obj-y += controllers/
-#obj-y += devices/
+obj-y += devices/
diff --git a/drivers/bus/mhi/devices/Kconfig b/drivers/bus/mhi/devices/Kconfig
new file mode 100644
index 0000000..40f964d
--- /dev/null
+++ b/drivers/bus/mhi/devices/Kconfig
@@ -0,0 +1,10 @@
+menu "MHI device support"
+
+config MHI_NETDEV
+ tristate "MHI NETDEV"
+ depends on MHI_BUS
+ help
+ MHI based net device driver for transferring IP traffic
+ between host and modem. By enabling this driver, clients
+ can transfer data using standard network interface.
+endmenu
diff --git a/drivers/bus/mhi/devices/Makefile b/drivers/bus/mhi/devices/Makefile
new file mode 100644
index 0000000..ee12a64
--- /dev/null
+++ b/drivers/bus/mhi/devices/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_MHI_NETDEV) +=mhi_netdev.o
diff --git a/drivers/bus/mhi/devices/mhi_netdev.c b/drivers/bus/mhi/devices/mhi_netdev.c
new file mode 100644
index 0000000..23881a9
--- /dev/null
+++ b/drivers/bus/mhi/devices/mhi_netdev.c
@@ -0,0 +1,893 @@
+/* Copyright (c) 2018, The Linux Foundation. All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 and
+ * only version 2 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/skbuff.h>
+#include <linux/if_arp.h>
+#include <linux/dma-mapping.h>
+#include <linux/debugfs.h>
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/of_device.h>
+#include <linux/rtnetlink.h>
+#include <linux/mhi.h>
+
+#define MHI_NETDEV_DRIVER_NAME "mhi_netdev"
+#define WATCHDOG_TIMEOUT (30 * HZ)
+
+#ifdef CONFIG_MHI_DEBUG
+
+#define MHI_ASSERT(cond, msg) do { \
+ if (cond) \
+ panic(msg); \
+} while (0)
+
+#define MSG_VERB(fmt, ...) do { \
+ if (mhi_netdev->msg_lvl <= MHI_MSG_LVL_VERBOSE) \
+ pr_err("[D][%s] " fmt, __func__, ##__VA_ARGS__);\
+} while (0)
+
+#else
+
+#define MHI_ASSERT(cond, msg) do { \
+ if (cond) { \
+ MSG_ERR(msg); \
+ WARN_ON(cond); \
+ } \
+} while (0)
+
+#define MSG_VERB(fmt, ...)
+
+#endif
+
+#define MSG_LOG(fmt, ...) do { \
+ if (mhi_netdev->msg_lvl <= MHI_MSG_LVL_INFO) \
+ pr_err("[I][%s] " fmt, __func__, ##__VA_ARGS__);\
+} while (0)
+
+#define MSG_ERR(fmt, ...) do { \
+ if (mhi_netdev->msg_lvl <= MHI_MSG_LVL_ERROR) \
+ pr_err("[E][%s] " fmt, __func__, ##__VA_ARGS__); \
+} while (0)
+
+struct mhi_stats {
+ u32 rx_int;
+ u32 tx_full;
+ u32 tx_pkts;
+ u32 rx_budget_overflow;
+ u32 rx_frag;
+ u32 alloc_failed;
+};
+
+/* important: do not exceed sk_buf->cb (48 bytes) */
+struct mhi_skb_priv {
+ void *buf;
+ size_t size;
+ struct mhi_netdev *mhi_netdev;
+};
+
+struct mhi_netdev {
+ int alias;
+ struct mhi_device *mhi_dev;
+ spinlock_t rx_lock;
+ bool enabled;
+ rwlock_t pm_lock; /* state change lock */
+ int (*rx_queue)(struct mhi_netdev *mhi_netdev, gfp_t gfp_t);
+ struct work_struct alloc_work;
+ int wake;
+
+ struct sk_buff_head rx_allocated;
+
+ u32 mru;
+ const char *interface_name;
+ struct napi_struct napi;
+ struct net_device *ndev;
+ struct sk_buff *frag_skb;
+ bool recycle_buf;
+
+ struct mhi_stats stats;
+ struct dentry *dentry;
+ enum MHI_DEBUG_LEVEL msg_lvl;
+};
+
+struct mhi_netdev_priv {
+ struct mhi_netdev *mhi_netdev;
+};
+
+static struct mhi_driver mhi_netdev_driver;
+static void mhi_netdev_create_debugfs(struct mhi_netdev *mhi_netdev);
+
+static __be16 mhi_netdev_ip_type_trans(struct sk_buff *skb)
+{
+ __be16 protocol = 0;
+
+ /* determine L3 protocol */
+ switch (skb->data[0] & 0xf0) {
+ case 0x40:
+ protocol = htons(ETH_P_IP);
+ break;
+ case 0x60:
+ protocol = htons(ETH_P_IPV6);
+ break;
+ default:
+ /* default is QMAP */
+ protocol = htons(ETH_P_MAP);
+ break;
+ }
+ return protocol;
+}
+
+static void mhi_netdev_skb_destructor(struct sk_buff *skb)
+{
+ struct mhi_skb_priv *skb_priv = (struct mhi_skb_priv *)(skb->cb);
+ struct mhi_netdev *mhi_netdev = skb_priv->mhi_netdev;
+
+ skb->data = skb->head;
+ skb_reset_tail_pointer(skb);
+ skb->len = 0;
+ MHI_ASSERT(skb->data != skb_priv->buf, "incorrect buf");
+ skb_queue_tail(&mhi_netdev->rx_allocated, skb);
+}
+
+static int mhi_netdev_alloc_skb(struct mhi_netdev *mhi_netdev, gfp_t gfp_t)
+{
+ u32 cur_mru = mhi_netdev->mru;
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+ struct mhi_skb_priv *skb_priv;
+ int ret;
+ struct sk_buff *skb;
+ int no_tre = mhi_get_no_free_descriptors(mhi_dev, DMA_FROM_DEVICE);
+ int i;
+
+ for (i = 0; i < no_tre; i++) {
+ skb = alloc_skb(cur_mru, gfp_t);
+ if (!skb)
+ return -ENOMEM;
+
+ read_lock_bh(&mhi_netdev->pm_lock);
+ if (unlikely(!mhi_netdev->enabled)) {
+ MSG_ERR("Interface not enabled\n");
+ ret = -EIO;
+ goto error_queue;
+ }
+
+ skb_priv = (struct mhi_skb_priv *)skb->cb;
+ skb_priv->buf = skb->data;
+ skb_priv->size = cur_mru;
+ skb_priv->mhi_netdev = mhi_netdev;
+ skb->dev = mhi_netdev->ndev;
+
+ if (mhi_netdev->recycle_buf)
+ skb->destructor = mhi_netdev_skb_destructor;
+
+ spin_lock_bh(&mhi_netdev->rx_lock);
+ ret = mhi_queue_transfer(mhi_dev, DMA_FROM_DEVICE, skb,
+ skb_priv->size, MHI_EOT);
+ spin_unlock_bh(&mhi_netdev->rx_lock);
+
+ if (ret) {
+ MSG_ERR("Failed to queue skb, ret:%d\n", ret);
+ ret = -EIO;
+ goto error_queue;
+ }
+
+ read_unlock_bh(&mhi_netdev->pm_lock);
+ }
+
+ return 0;
+
+error_queue:
+ skb->destructor = NULL;
+ read_unlock_bh(&mhi_netdev->pm_lock);
+ dev_kfree_skb_any(skb);
+
+ return ret;
+}
+
+static void mhi_netdev_alloc_work(struct work_struct *work)
+{
+ struct mhi_netdev *mhi_netdev = container_of(work, struct mhi_netdev,
+ alloc_work);
+ /* sleep about 1 sec and retry, that should be enough time
+ * for system to reclaim freed memory back.
+ */
+ const int sleep_ms = 1000;
+ int retry = 60;
+ int ret;
+
+ MSG_LOG("Entered\n");
+ do {
+ ret = mhi_netdev_alloc_skb(mhi_netdev, GFP_KERNEL);
+ /* sleep and try again */
+ if (ret == -ENOMEM) {
+ msleep(sleep_ms);
+ retry--;
+ }
+ } while (ret == -ENOMEM && retry);
+
+ MSG_LOG("Exit with status:%d retry:%d\n", ret, retry);
+}
+
+/* we will recycle buffers */
+static int mhi_netdev_skb_recycle(struct mhi_netdev *mhi_netdev, gfp_t gfp_t)
+{
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+ int no_tre;
+ int ret = 0;
+ struct sk_buff *skb;
+ struct mhi_skb_priv *skb_priv;
+
+ read_lock_bh(&mhi_netdev->pm_lock);
+ if (!mhi_netdev->enabled) {
+ read_unlock_bh(&mhi_netdev->pm_lock);
+ return -EIO;
+ }
+
+ no_tre = mhi_get_no_free_descriptors(mhi_dev, DMA_FROM_DEVICE);
+
+ spin_lock_bh(&mhi_netdev->rx_lock);
+ while (no_tre) {
+ skb = skb_dequeue(&mhi_netdev->rx_allocated);
+
+ /* no free buffers to recycle, reschedule work */
+ if (!skb) {
+ ret = -ENOMEM;
+ goto error_queue;
+ }
+
+ skb_priv = (struct mhi_skb_priv *)(skb->cb);
+ ret = mhi_queue_transfer(mhi_dev, DMA_FROM_DEVICE, skb,
+ skb_priv->size, MHI_EOT);
+
+ /* failed to queue buffer */
+ if (ret) {
+ MSG_ERR("Failed to queue skb, ret:%d\n", ret);
+ skb_queue_tail(&mhi_netdev->rx_allocated, skb);
+ goto error_queue;
+ }
+
+ no_tre--;
+ }
+
+error_queue:
+ spin_unlock_bh(&mhi_netdev->rx_lock);
+ read_unlock_bh(&mhi_netdev->pm_lock);
+
+ return ret;
+}
+
+static void mhi_netdev_dealloc(struct mhi_netdev *mhi_netdev)
+{
+ struct sk_buff *skb;
+
+ skb = skb_dequeue(&mhi_netdev->rx_allocated);
+ while (skb) {
+ skb->destructor = NULL;
+ kfree_skb(skb);
+ skb = skb_dequeue(&mhi_netdev->rx_allocated);
+ }
+}
+
+static int mhi_netdev_poll(struct napi_struct *napi, int budget)
+{
+ struct net_device *dev = napi->dev;
+ struct mhi_netdev_priv *mhi_netdev_priv = netdev_priv(dev);
+ struct mhi_netdev *mhi_netdev = mhi_netdev_priv->mhi_netdev;
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+ int rx_work = 0;
+ int ret;
+
+ MSG_VERB("Entered\n");
+
+ read_lock_bh(&mhi_netdev->pm_lock);
+
+ if (!mhi_netdev->enabled) {
+ MSG_LOG("interface is disabled!\n");
+ napi_complete(napi);
+ read_unlock_bh(&mhi_netdev->pm_lock);
+ return 0;
+ }
+
+ mhi_device_get(mhi_dev);
+
+ rx_work = mhi_poll(mhi_dev, budget);
+ if (rx_work < 0) {
+ MSG_ERR("Error polling ret:%d\n", rx_work);
+ rx_work = 0;
+ napi_complete(napi);
+ goto exit_poll;
+ }
+
+ /* queue new buffers */
+ ret = mhi_netdev->rx_queue(mhi_netdev, GFP_ATOMIC);
+ if (ret == -ENOMEM) {
+ MSG_LOG("out of tre, queuing bg worker\n");
+ mhi_netdev->stats.alloc_failed++;
+ schedule_work(&mhi_netdev->alloc_work);
+
+ }
+
+ /* complete work if # of packet processed less than allocated budget */
+ if (rx_work < budget)
+ napi_complete(napi);
+ else
+ mhi_netdev->stats.rx_budget_overflow++;
+
+exit_poll:
+ mhi_device_put(mhi_dev);
+ read_unlock_bh(&mhi_netdev->pm_lock);
+
+ MSG_VERB("polled %d pkts\n", rx_work);
+
+ return rx_work;
+}
+
+static int mhi_netdev_open(struct net_device *dev)
+{
+ struct mhi_netdev_priv *mhi_netdev_priv = netdev_priv(dev);
+ struct mhi_netdev *mhi_netdev = mhi_netdev_priv->mhi_netdev;
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+
+ MSG_LOG("Opened net dev interface\n");
+
+ /* tx queue may not necessarily be stopped already
+ * so stop the queue if tx path is not enabled
+ */
+ if (!mhi_dev->ul_chan)
+ netif_stop_queue(dev);
+ else
+ netif_start_queue(dev);
+
+ return 0;
+
+}
+
+static int mhi_netdev_change_mtu(struct net_device *dev, int new_mtu)
+{
+ struct mhi_netdev_priv *mhi_netdev_priv = netdev_priv(dev);
+ struct mhi_netdev *mhi_netdev = mhi_netdev_priv->mhi_netdev;
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+
+ if (new_mtu < 0 || mhi_dev->mtu < new_mtu)
+ return -EINVAL;
+
+ dev->mtu = new_mtu;
+ return 0;
+}
+
+static int mhi_netdev_xmit(struct sk_buff *skb, struct net_device *dev)
+{
+ struct mhi_netdev_priv *mhi_netdev_priv = netdev_priv(dev);
+ struct mhi_netdev *mhi_netdev = mhi_netdev_priv->mhi_netdev;
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+ int res = 0;
+ struct mhi_skb_priv *tx_priv;
+
+ MSG_VERB("Entered\n");
+
+ tx_priv = (struct mhi_skb_priv *)(skb->cb);
+ tx_priv->mhi_netdev = mhi_netdev;
+ read_lock_bh(&mhi_netdev->pm_lock);
+
+ if (unlikely(!mhi_netdev->enabled)) {
+ /* Only reason interface could be disabled and we get data
+ * is due to an SSR. We do not want to stop the queue and
+ * return error. Instead we will flush all the uplink packets
+ * and return successful
+ */
+ res = NETDEV_TX_OK;
+ dev_kfree_skb_any(skb);
+ goto mhi_xmit_exit;
+ }
+
+ res = mhi_queue_transfer(mhi_dev, DMA_TO_DEVICE, skb, skb->len,
+ MHI_EOT);
+ if (res) {
+ MSG_VERB("Failed to queue with reason:%d\n", res);
+ netif_stop_queue(dev);
+ mhi_netdev->stats.tx_full++;
+ res = NETDEV_TX_BUSY;
+ goto mhi_xmit_exit;
+ }
+
+ mhi_netdev->stats.tx_pkts++;
+
+mhi_xmit_exit:
+ read_unlock_bh(&mhi_netdev->pm_lock);
+ MSG_VERB("Exited\n");
+
+ return res;
+}
+
+static int mhi_netdev_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd)
+{
+ int rc = 0;
+
+ switch (cmd) {
+ default:
+ /* don't fail any IOCTL right now */
+ rc = 0;
+ break;
+ }
+
+ return rc;
+}
+
+static const struct net_device_ops mhi_netdev_ops_ip = {
+ .ndo_open = mhi_netdev_open,
+ .ndo_start_xmit = mhi_netdev_xmit,
+ .ndo_do_ioctl = mhi_netdev_ioctl,
+ .ndo_change_mtu = mhi_netdev_change_mtu,
+ .ndo_set_mac_address = 0,
+ .ndo_validate_addr = 0,
+};
+
+static void mhi_netdev_setup(struct net_device *dev)
+{
+ dev->netdev_ops = &mhi_netdev_ops_ip;
+ ether_setup(dev);
+
+ /* set this after calling ether_setup */
+ dev->header_ops = 0; /* No header */
+ dev->type = ARPHRD_RAWIP;
+ dev->hard_header_len = 0;
+ dev->addr_len = 0;
+ dev->flags &= ~(IFF_BROADCAST | IFF_MULTICAST);
+ dev->watchdog_timeo = WATCHDOG_TIMEOUT;
+}
+
+/* enable mhi_netdev netdev, call only after grabbing mhi_netdev.mutex */
+static int mhi_netdev_enable_iface(struct mhi_netdev *mhi_netdev)
+{
+ int ret = 0;
+ char ifalias[IFALIASZ];
+ char ifname[IFNAMSIZ];
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+ int no_tre;
+
+ MSG_LOG("Prepare the channels for transfer\n");
+
+ ret = mhi_prepare_for_transfer(mhi_dev);
+ if (ret) {
+ MSG_ERR("Failed to start TX chan ret %d\n", ret);
+ goto mhi_failed_to_start;
+ }
+
+ /* first time enabling the node */
+ if (!mhi_netdev->ndev) {
+ struct mhi_netdev_priv *mhi_netdev_priv;
+
+ snprintf(ifalias, sizeof(ifalias), "%s_%04x_%02u.%02u.%02u_%u",
+ mhi_netdev->interface_name, mhi_dev->dev_id,
+ mhi_dev->domain, mhi_dev->bus, mhi_dev->slot,
+ mhi_netdev->alias);
+
+ snprintf(ifname, sizeof(ifname), "%s%%d",
+ mhi_netdev->interface_name);
+
+ rtnl_lock();
+ mhi_netdev->ndev = alloc_netdev(sizeof(*mhi_netdev_priv),
+ ifname, NET_NAME_PREDICTABLE,
+ mhi_netdev_setup);
+
+ if (!mhi_netdev->ndev) {
+ ret = -ENOMEM;
+ rtnl_unlock();
+ goto net_dev_alloc_fail;
+ }
+
+ mhi_netdev->ndev->mtu = mhi_dev->mtu;
+ SET_NETDEV_DEV(mhi_netdev->ndev, &mhi_dev->dev);
+ dev_set_alias(mhi_netdev->ndev, ifalias, strlen(ifalias));
+ mhi_netdev_priv = netdev_priv(mhi_netdev->ndev);
+ mhi_netdev_priv->mhi_netdev = mhi_netdev;
+ rtnl_unlock();
+
+ netif_napi_add(mhi_netdev->ndev, &mhi_netdev->napi,
+ mhi_netdev_poll, NAPI_POLL_WEIGHT);
+ ret = register_netdev(mhi_netdev->ndev);
+ if (ret) {
+ MSG_ERR("Network device registration failed\n");
+ goto net_dev_reg_fail;
+ }
+
+ skb_queue_head_init(&mhi_netdev->rx_allocated);
+ }
+
+ write_lock_irq(&mhi_netdev->pm_lock);
+ mhi_netdev->enabled = true;
+ write_unlock_irq(&mhi_netdev->pm_lock);
+
+ /* queue buffer for rx path */
+ no_tre = mhi_get_no_free_descriptors(mhi_dev, DMA_FROM_DEVICE);
+ ret = mhi_netdev_alloc_skb(mhi_netdev, GFP_KERNEL);
+ if (ret)
+ schedule_work(&mhi_netdev->alloc_work);
+
+ /* if we recycle prepare one more set */
+ if (mhi_netdev->recycle_buf)
+ for (; no_tre >= 0; no_tre--) {
+ struct sk_buff *skb = alloc_skb(mhi_netdev->mru,
+ GFP_KERNEL);
+ struct mhi_skb_priv *skb_priv;
+
+ if (!skb)
+ break;
+
+ skb_priv = (struct mhi_skb_priv *)skb->cb;
+ skb_priv->buf = skb->data;
+ skb_priv->size = mhi_netdev->mru;
+ skb_priv->mhi_netdev = mhi_netdev;
+ skb->dev = mhi_netdev->ndev;
+ skb->destructor = mhi_netdev_skb_destructor;
+ skb_queue_tail(&mhi_netdev->rx_allocated, skb);
+ }
+
+ napi_enable(&mhi_netdev->napi);
+
+ MSG_LOG("Exited.\n");
+
+ return 0;
+
+net_dev_reg_fail:
+ netif_napi_del(&mhi_netdev->napi);
+ free_netdev(mhi_netdev->ndev);
+ mhi_netdev->ndev = NULL;
+
+net_dev_alloc_fail:
+ mhi_unprepare_from_transfer(mhi_dev);
+
+mhi_failed_to_start:
+ MSG_ERR("Exited ret %d.\n", ret);
+
+ return ret;
+}
+
+static void mhi_netdev_xfer_ul_cb(struct mhi_device *mhi_dev,
+ struct mhi_result *mhi_result)
+{
+ struct mhi_netdev *mhi_netdev = mhi_device_get_devdata(mhi_dev);
+ struct sk_buff *skb = mhi_result->buf_addr;
+ struct net_device *ndev = mhi_netdev->ndev;
+
+ ndev->stats.tx_packets++;
+ ndev->stats.tx_bytes += skb->len;
+ dev_kfree_skb(skb);
+
+ if (netif_queue_stopped(ndev))
+ netif_wake_queue(ndev);
+}
+
+static int mhi_netdev_process_fragment(struct mhi_netdev *mhi_netdev,
+ struct sk_buff *skb)
+{
+ struct sk_buff *temp_skb;
+
+ if (mhi_netdev->frag_skb) {
+ /* merge the new skb into the old fragment */
+ temp_skb = skb_copy_expand(mhi_netdev->frag_skb, 0, skb->len,
+ GFP_ATOMIC);
+ if (!temp_skb) {
+ dev_kfree_skb(mhi_netdev->frag_skb);
+ mhi_netdev->frag_skb = NULL;
+ return -ENOMEM;
+ }
+
+ dev_kfree_skb_any(mhi_netdev->frag_skb);
+ mhi_netdev->frag_skb = temp_skb;
+ memcpy(skb_put(mhi_netdev->frag_skb, skb->len), skb->data,
+ skb->len);
+ } else {
+ mhi_netdev->frag_skb = skb_copy(skb, GFP_ATOMIC);
+ if (!mhi_netdev->frag_skb)
+ return -ENOMEM;
+ }
+
+ mhi_netdev->stats.rx_frag++;
+
+ return 0;
+}
+
+static void mhi_netdev_xfer_dl_cb(struct mhi_device *mhi_dev,
+ struct mhi_result *mhi_result)
+{
+ struct mhi_netdev *mhi_netdev = mhi_device_get_devdata(mhi_dev);
+ struct sk_buff *skb = mhi_result->buf_addr;
+ struct net_device *dev = mhi_netdev->ndev;
+ int ret = 0;
+
+ if (mhi_result->transaction_status == -ENOTCONN) {
+ dev_kfree_skb(skb);
+ return;
+ }
+
+ skb_put(skb, mhi_result->bytes_xferd);
+ dev->stats.rx_packets++;
+ dev->stats.rx_bytes += mhi_result->bytes_xferd;
+
+ /* merge skb's together, it's a chain transfer */
+ if (mhi_result->transaction_status == -EOVERFLOW ||
+ mhi_netdev->frag_skb) {
+ ret = mhi_netdev_process_fragment(mhi_netdev, skb);
+
+ /* recycle the skb */
+ if (mhi_netdev->recycle_buf)
+ mhi_netdev_skb_destructor(skb);
+ else
+ dev_kfree_skb(skb);
+
+ if (ret)
+ return;
+ }
+
+ /* more data will come, don't submit the buffer */
+ if (mhi_result->transaction_status == -EOVERFLOW)
+ return;
+
+ if (mhi_netdev->frag_skb) {
+ skb = mhi_netdev->frag_skb;
+ skb->dev = dev;
+ mhi_netdev->frag_skb = NULL;
+ }
+
+ skb->protocol = mhi_netdev_ip_type_trans(skb);
+ netif_receive_skb(skb);
+}
+
+static void mhi_netdev_status_cb(struct mhi_device *mhi_dev, enum MHI_CB mhi_cb)
+{
+ struct mhi_netdev *mhi_netdev = mhi_device_get_devdata(mhi_dev);
+
+ if (mhi_cb != MHI_CB_PENDING_DATA)
+ return;
+
+ if (napi_schedule_prep(&mhi_netdev->napi)) {
+ __napi_schedule(&mhi_netdev->napi);
+ mhi_netdev->stats.rx_int++;
+ return;
+ }
+
+}
+
+#ifdef CONFIG_DEBUG_FS
+
+struct dentry *dentry;
+
+static int mhi_netdev_debugfs_trigger_reset(void *data, u64 val)
+{
+ struct mhi_netdev *mhi_netdev = data;
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+ int ret;
+
+ MSG_LOG("Triggering channel reset\n");
+
+ /* disable the interface so no data processing */
+ write_lock_irq(&mhi_netdev->pm_lock);
+ mhi_netdev->enabled = false;
+ write_unlock_irq(&mhi_netdev->pm_lock);
+ napi_disable(&mhi_netdev->napi);
+
+ /* disable all hardware channels */
+ mhi_unprepare_from_transfer(mhi_dev);
+
+ /* clean up all alocated buffers */
+ mhi_netdev_dealloc(mhi_netdev);
+
+ MSG_LOG("Restarting iface\n");
+
+ ret = mhi_netdev_enable_iface(mhi_netdev);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+DEFINE_SIMPLE_ATTRIBUTE(mhi_netdev_debugfs_trigger_reset_fops, NULL,
+ mhi_netdev_debugfs_trigger_reset, "%llu\n");
+
+static void mhi_netdev_create_debugfs(struct mhi_netdev *mhi_netdev)
+{
+ char node_name[32];
+ int i;
+ const umode_t mode = 0600;
+ struct dentry *file;
+ struct mhi_device *mhi_dev = mhi_netdev->mhi_dev;
+
+ const struct {
+ char *name;
+ u32 *ptr;
+ } debugfs_table[] = {
+ {
+ "rx_int",
+ &mhi_netdev->stats.rx_int
+ },
+ {
+ "tx_full",
+ &mhi_netdev->stats.tx_full
+ },
+ {
+ "tx_pkts",
+ &mhi_netdev->stats.tx_pkts
+ },
+ {
+ "rx_budget_overflow",
+ &mhi_netdev->stats.rx_budget_overflow
+ },
+ {
+ "rx_fragmentation",
+ &mhi_netdev->stats.rx_frag
+ },
+ {
+ "alloc_failed",
+ &mhi_netdev->stats.alloc_failed
+ },
+ {
+ NULL, NULL
+ },
+ };
+
+ /* Both tx & rx client handle contain same device info */
+ snprintf(node_name, sizeof(node_name), "%s_%04x_%02u.%02u.%02u_%u",
+ mhi_netdev->interface_name, mhi_dev->dev_id, mhi_dev->domain,
+ mhi_dev->bus, mhi_dev->slot, mhi_netdev->alias);
+
+ if (IS_ERR_OR_NULL(dentry))
+ return;
+
+ mhi_netdev->dentry = debugfs_create_dir(node_name, dentry);
+ if (IS_ERR_OR_NULL(mhi_netdev->dentry))
+ return;
+
+ file = debugfs_create_u32("msg_lvl", mode, mhi_netdev->dentry,
+ (u32 *)&mhi_netdev->msg_lvl);
+ if (IS_ERR_OR_NULL(file))
+ return;
+
+ /* Add debug stats table */
+ for (i = 0; debugfs_table[i].name; i++) {
+ file = debugfs_create_u32(debugfs_table[i].name, mode,
+ mhi_netdev->dentry,
+ debugfs_table[i].ptr);
+ if (IS_ERR_OR_NULL(file))
+ return;
+ }
+
+ debugfs_create_file("reset", mode, mhi_netdev->dentry, mhi_netdev,
+ &mhi_netdev_debugfs_trigger_reset_fops);
+}
+
+static void mhi_netdev_create_debugfs_dir(void)
+{
+ dentry = debugfs_create_dir(MHI_NETDEV_DRIVER_NAME, 0);
+}
+
+#else
+
+static void mhi_netdev_create_debugfs(struct mhi_netdev_private *mhi_netdev)
+{
+}
+
+static void mhi_netdev_create_debugfs_dir(void)
+{
+}
+
+#endif
+
+static void mhi_netdev_remove(struct mhi_device *mhi_dev)
+{
+ struct mhi_netdev *mhi_netdev = mhi_device_get_devdata(mhi_dev);
+
+ MSG_LOG("Remove notification received\n");
+
+ write_lock_irq(&mhi_netdev->pm_lock);
+ mhi_netdev->enabled = false;
+ write_unlock_irq(&mhi_netdev->pm_lock);
+
+ napi_disable(&mhi_netdev->napi);
+ netif_napi_del(&mhi_netdev->napi);
+ mhi_netdev_dealloc(mhi_netdev);
+ unregister_netdev(mhi_netdev->ndev);
+ free_netdev(mhi_netdev->ndev);
+ flush_work(&mhi_netdev->alloc_work);
+
+ if (!IS_ERR_OR_NULL(mhi_netdev->dentry))
+ debugfs_remove_recursive(mhi_netdev->dentry);
+}
+
+static int mhi_netdev_probe(struct mhi_device *mhi_dev,
+ const struct mhi_device_id *id)
+{
+ int ret;
+ struct mhi_netdev *mhi_netdev;
+ struct device_node *of_node = mhi_dev->dev.of_node;
+ char node_name[32];
+
+ if (!of_node)
+ return -ENODEV;
+
+ mhi_netdev = devm_kzalloc(&mhi_dev->dev, sizeof(*mhi_netdev),
+ GFP_KERNEL);
+ if (!mhi_netdev)
+ return -ENOMEM;
+
+ mhi_netdev->alias = of_alias_get_id(of_node, "mhi_netdev");
+ if (mhi_netdev->alias < 0)
+ return -ENODEV;
+
+ mhi_netdev->mhi_dev = mhi_dev;
+ mhi_device_set_devdata(mhi_dev, mhi_netdev);
+
+ ret = of_property_read_u32(of_node, "mhi,mru", &mhi_netdev->mru);
+ if (ret)
+ return -ENODEV;
+
+ ret = of_property_read_string(of_node, "mhi,interface-name",
+ &mhi_netdev->interface_name);
+ if (ret)
+ mhi_netdev->interface_name = mhi_netdev_driver.driver.name;
+
+ mhi_netdev->recycle_buf = of_property_read_bool(of_node,
+ "mhi,recycle-buf");
+ mhi_netdev->rx_queue = mhi_netdev->recycle_buf ?
+ mhi_netdev_skb_recycle : mhi_netdev_alloc_skb;
+
+ spin_lock_init(&mhi_netdev->rx_lock);
+ rwlock_init(&mhi_netdev->pm_lock);
+ INIT_WORK(&mhi_netdev->alloc_work, mhi_netdev_alloc_work);
+
+ /* create ipc log buffer */
+ snprintf(node_name, sizeof(node_name), "%s_%04x_%02u.%02u.%02u_%u",
+ mhi_netdev->interface_name, mhi_dev->dev_id, mhi_dev->domain,
+ mhi_dev->bus, mhi_dev->slot, mhi_netdev->alias);
+
+ mhi_netdev->msg_lvl = MHI_MSG_LVL_ERROR;
+
+ /* setup network interface */
+ ret = mhi_netdev_enable_iface(mhi_netdev);
+ if (ret)
+ return ret;
+
+ mhi_netdev_create_debugfs(mhi_netdev);
+
+ return 0;
+}
+
+static const struct mhi_device_id mhi_netdev_match_table[] = {
+ { .chan = "IP_HW0" },
+ { .chan = "IP_HW_ADPL" },
+ { NULL },
+};
+
+static struct mhi_driver mhi_netdev_driver = {
+ .id_table = mhi_netdev_match_table,
+ .probe = mhi_netdev_probe,
+ .remove = mhi_netdev_remove,
+ .ul_xfer_cb = mhi_netdev_xfer_ul_cb,
+ .dl_xfer_cb = mhi_netdev_xfer_dl_cb,
+ .status_cb = mhi_netdev_status_cb,
+ .driver = {
+ .name = "mhi_netdev",
+ .owner = THIS_MODULE,
+ }
+};
+
+static int __init mhi_netdev_init(void)
+{
+ mhi_netdev_create_debugfs_dir();
+
+ return mhi_driver_register(&mhi_netdev_driver);
+}
+module_init(mhi_netdev_init);
+
+MODULE_DESCRIPTION("MHI NETDEV Network Interface");
+MODULE_LICENSE("GPL v2");
--
The Qualcomm Innovation Center, Inc. is a member of the Code Aurora Forum,
a Linux Foundation Collaborative Project