Staging: most: add MOST driver's aim-network module
authorChristian Gromm <christian.gromm@microchip.com>
Fri, 24 Jul 2015 14:11:50 +0000 (16:11 +0200)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Fri, 24 Jul 2015 20:49:17 +0000 (13:49 -0700)
This patch adds the aim-network module of the MOST driver to the kernel's
driver staging area. This module is part of the MOST driver and handles
user space interaction by means of network devices.

This patch is needed in order to have access to MOST Ethernet Packets (MEP)
through a networking device.

Signed-off-by: Christian Gromm <christian.gromm@microchip.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/staging/most/Kconfig
drivers/staging/most/Makefile
drivers/staging/most/aim-network/Kconfig [new file with mode: 0644]
drivers/staging/most/aim-network/Makefile [new file with mode: 0644]
drivers/staging/most/aim-network/networking.c [new file with mode: 0644]
drivers/staging/most/aim-network/networking.h [new file with mode: 0644]

index e29bee5..34a0b73 100644 (file)
@@ -15,4 +15,6 @@ source "drivers/staging/most/mostcore/Kconfig"
 
 source "drivers/staging/most/aim-cdev/Kconfig"
 
+source "drivers/staging/most/aim-network/Kconfig"
+
 endif
index b7b319b..61c1c96 100644 (file)
@@ -1,2 +1,3 @@
 obj-$(CONFIG_MOSTCORE) += mostcore/
 obj-$(CONFIG_AIM_CDEV) += aim-cdev/
+obj-$(CONFIG_AIM_NETWORK)      += aim-network/
diff --git a/drivers/staging/most/aim-network/Kconfig b/drivers/staging/most/aim-network/Kconfig
new file mode 100644 (file)
index 0000000..507232a
--- /dev/null
@@ -0,0 +1,12 @@
+#
+# MOST Networking configuration
+#
+
+config AIM_NETWORK
+       tristate "Networking AIM"
+
+       ---help---
+         Say Y here if you want to commumicate via a networking device.
+
+         To compile this driver as a module, choose M here: the
+         module will be called aim_networking.
diff --git a/drivers/staging/most/aim-network/Makefile b/drivers/staging/most/aim-network/Makefile
new file mode 100644 (file)
index 0000000..840c1dd
--- /dev/null
@@ -0,0 +1,4 @@
+obj-$(CONFIG_AIM_NETWORK) += aim_network.o
+
+aim_network-objs := networking.o
+ccflags-y += -Idrivers/staging/most/mostcore/
diff --git a/drivers/staging/most/aim-network/networking.c b/drivers/staging/most/aim-network/networking.c
new file mode 100644 (file)
index 0000000..4639c49
--- /dev/null
@@ -0,0 +1,567 @@
+/*
+ * Networking AIM - Networking Application Interface Module for MostCore
+ *
+ * Copyright (C) 2015, Microchip Technology Germany II GmbH & Co. KG
+ *
+ * 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.
+ *
+ * This file is licensed under GPLv2.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/etherdevice.h>
+#include <linux/slab.h>
+#include <linux/init.h>
+#include <linux/list.h>
+#include <linux/wait.h>
+#include <linux/kobject.h>
+#include "mostcore.h"
+#include "networking.h"
+
+
+#define MEP_HDR_LEN 8
+#define MDP_HDR_LEN 16
+#define MAMAC_DATA_LEN (1024 - MDP_HDR_LEN)
+
+#define PMHL 5
+
+#define PMS_TELID_UNSEGM_MAMAC 0x0A
+#define PMS_FIFONO_MDP         0x01
+#define PMS_FIFONO_MEP         0x04
+#define PMS_MSGTYPE_DATA       0x04
+#define PMS_DEF_PRIO           0
+#define MEP_DEF_RETRY          15
+
+#define PMS_FIFONO_MASK                0x07
+#define PMS_FIFONO_SHIFT       3
+#define PMS_RETRY_SHIFT                4
+#define PMS_TELID_MASK         0x0F
+#define PMS_TELID_SHIFT                4
+
+#define HB(value)              ((u8)((u16)(value) >> 8))
+#define LB(value)              ((u8)(value))
+
+
+
+#define EXTRACT_BIT_SET(bitset_name, value) \
+       (((value) >> bitset_name##_SHIFT) & bitset_name##_MASK)
+
+#define PMS_IS_MEP(buf, len) \
+       ((len) > MEP_HDR_LEN && \
+        EXTRACT_BIT_SET(PMS_FIFONO, (buf)[3]) == PMS_FIFONO_MEP)
+
+#define PMS_IS_MAMAC(buf, len) \
+       ((len) > MDP_HDR_LEN && \
+        EXTRACT_BIT_SET(PMS_FIFONO, (buf)[3]) == PMS_FIFONO_MDP && \
+        EXTRACT_BIT_SET(PMS_TELID, (buf)[14]) == PMS_TELID_UNSEGM_MAMAC)
+
+struct net_dev_channel {
+       bool linked;
+       int ch_id;
+};
+
+struct net_dev_context {
+       struct most_interface *iface;
+       bool channels_opened;
+       bool is_mamac;
+       unsigned char link_stat;
+       struct net_device *dev;
+       struct net_dev_channel rx;
+       struct net_dev_channel tx;
+       struct list_head list;
+};
+
+static struct list_head net_devices = LIST_HEAD_INIT(net_devices);
+static struct spinlock list_lock;
+static struct most_aim aim;
+
+
+static int skb_to_mamac(const struct sk_buff *skb, struct mbo *mbo)
+{
+       u8 *buff = mbo->virt_address;
+       const u8 broadcast[] = { 0x03, 0xFF };
+       const u8 *dest_addr = skb->data + 4;
+       const u8 *eth_type = skb->data + 12;
+       unsigned int payload_len = skb->len - ETH_HLEN;
+       unsigned int mdp_len = payload_len + MDP_HDR_LEN;
+
+       if (mbo->buffer_length < mdp_len) {
+               pr_err("drop: too small buffer! (%d for %d)\n",
+                      mbo->buffer_length, mdp_len);
+               return -EINVAL;
+       }
+
+       if (skb->len < ETH_HLEN) {
+               pr_err("drop: too small packet! (%d)\n", skb->len);
+               return -EINVAL;
+       }
+
+       if (dest_addr[0] == 0xFF && dest_addr[1] == 0xFF)
+               dest_addr = broadcast;
+
+       *buff++ = HB(mdp_len - 2);
+       *buff++ = LB(mdp_len - 2);
+
+       *buff++ = PMHL;
+       *buff++ = (PMS_FIFONO_MDP << PMS_FIFONO_SHIFT) | PMS_MSGTYPE_DATA;
+       *buff++ = PMS_DEF_PRIO;
+       *buff++ = dest_addr[0];
+       *buff++ = dest_addr[1];
+       *buff++ = 0x00;
+
+       *buff++ = HB(payload_len + 6);
+       *buff++ = LB(payload_len + 6);
+
+       /* end of FPH here */
+
+       *buff++ = eth_type[0];
+       *buff++ = eth_type[1];
+       *buff++ = 0;
+       *buff++ = 0;
+
+       *buff++ = PMS_TELID_UNSEGM_MAMAC << 4 | HB(payload_len);
+       *buff++ = LB(payload_len);
+
+       memcpy(buff, skb->data + ETH_HLEN, payload_len);
+       mbo->buffer_length = mdp_len;
+       return 0;
+}
+
+static int skb_to_mep(const struct sk_buff *skb, struct mbo *mbo)
+{
+       u8 *buff = mbo->virt_address;
+       unsigned int mep_len = skb->len + MEP_HDR_LEN;
+
+       if (mbo->buffer_length < mep_len) {
+               pr_err("drop: too small buffer! (%d for %d)\n",
+                      mbo->buffer_length, mep_len);
+               return -EINVAL;
+       }
+
+       *buff++ = HB(mep_len - 2);
+       *buff++ = LB(mep_len - 2);
+
+       *buff++ = PMHL;
+       *buff++ = (PMS_FIFONO_MEP << PMS_FIFONO_SHIFT) | PMS_MSGTYPE_DATA;
+       *buff++ = (MEP_DEF_RETRY << PMS_RETRY_SHIFT) | PMS_DEF_PRIO;
+       *buff++ = 0;
+       *buff++ = 0;
+       *buff++ = 0;
+
+       memcpy(buff, skb->data, skb->len);
+       mbo->buffer_length = mep_len;
+       return 0;
+}
+
+static int most_nd_set_mac_address(struct net_device *dev, void *p)
+{
+       struct net_dev_context *nd = dev->ml_priv;
+       int err = eth_mac_addr(dev, p);
+
+       if (err)
+               return err;
+
+       BUG_ON(nd->dev != dev);
+
+       nd->is_mamac =
+               (dev->dev_addr[0] == 0 && dev->dev_addr[1] == 0 &&
+                dev->dev_addr[2] == 0 && dev->dev_addr[3] == 0);
+
+       /*
+        * Set default MTU for the given packet type.
+        * It is still possible to change MTU using ip tools afterwards.
+        */
+       dev->mtu = nd->is_mamac ? MAMAC_DATA_LEN : ETH_DATA_LEN;
+
+       return 0;
+}
+
+static int most_nd_open(struct net_device *dev)
+{
+       struct net_dev_context *nd = dev->ml_priv;
+
+       pr_info("open net device %s\n", dev->name);
+
+       BUG_ON(nd->dev != dev);
+
+       if (nd->channels_opened)
+               return -EFAULT;
+
+       BUG_ON(!nd->tx.linked || !nd->rx.linked);
+
+       if (most_start_channel(nd->iface, nd->rx.ch_id)) {
+               pr_err("most_start_channel() failed\n");
+               return -EBUSY;
+       }
+
+       if (most_start_channel(nd->iface, nd->tx.ch_id)) {
+               pr_err("most_start_channel() failed\n");
+               most_stop_channel(nd->iface, nd->rx.ch_id);
+               return -EBUSY;
+       }
+
+       nd->channels_opened = true;
+
+       if (nd->is_mamac) {
+               nd->link_stat = 1;
+               netif_wake_queue(dev);
+       } else {
+               nd->iface->request_netinfo(nd->iface, nd->tx.ch_id);
+       }
+
+       return 0;
+}
+
+static int most_nd_stop(struct net_device *dev)
+{
+       struct net_dev_context *nd = dev->ml_priv;
+
+       pr_info("stop net device %s\n", dev->name);
+
+       BUG_ON(nd->dev != dev);
+       netif_stop_queue(dev);
+
+       if (nd->channels_opened) {
+               most_stop_channel(nd->iface, nd->rx.ch_id);
+               most_stop_channel(nd->iface, nd->tx.ch_id);
+               nd->channels_opened = false;
+       }
+
+       return 0;
+}
+
+static netdev_tx_t most_nd_start_xmit(struct sk_buff *skb,
+                                     struct net_device *dev)
+{
+       struct net_dev_context *nd = dev->ml_priv;
+       struct mbo *mbo;
+       int ret;
+
+       BUG_ON(nd->dev != dev);
+
+       mbo = most_get_mbo(nd->iface, nd->tx.ch_id);
+
+       if (!mbo) {
+               netif_stop_queue(dev);
+               dev->stats.tx_fifo_errors++;
+               return NETDEV_TX_BUSY;
+       }
+
+       if (nd->is_mamac)
+               ret = skb_to_mamac(skb, mbo);
+       else
+               ret = skb_to_mep(skb, mbo);
+
+       if (ret) {
+               most_put_mbo(mbo);
+               dev->stats.tx_dropped++;
+               kfree_skb(skb);
+               return NETDEV_TX_OK;
+       }
+
+       most_submit_mbo(mbo);
+       dev->stats.tx_packets++;
+       dev->stats.tx_bytes += skb->len;
+       kfree_skb(skb);
+       return NETDEV_TX_OK;
+}
+
+static const struct net_device_ops most_nd_ops = {
+       .ndo_open = most_nd_open,
+       .ndo_stop = most_nd_stop,
+       .ndo_start_xmit = most_nd_start_xmit,
+       .ndo_set_mac_address = most_nd_set_mac_address,
+};
+
+static void most_nd_setup(struct net_device *dev)
+{
+       pr_info("setup net device %s\n", dev->name);
+       ether_setup(dev);
+       dev->netdev_ops = &most_nd_ops;
+}
+
+static void most_net_rm_netdev_safe(struct net_dev_context *nd)
+{
+       if (!nd->dev)
+               return;
+
+       pr_info("remove net device %p\n", nd->dev);
+
+       unregister_netdev(nd->dev);
+       free_netdev(nd->dev);
+       nd->dev = 0;
+}
+
+static struct net_dev_context *get_net_dev_context(
+       struct most_interface *iface)
+{
+       struct net_dev_context *nd, *tmp;
+
+       spin_lock(&list_lock);
+       list_for_each_entry_safe(nd, tmp, &net_devices, list) {
+               if (nd->iface == iface) {
+                       spin_unlock(&list_lock);
+                       return nd;
+               }
+       }
+       spin_unlock(&list_lock);
+       return 0;
+}
+
+static int aim_probe_channel(struct most_interface *iface, int channel_idx,
+                            struct most_channel_config *ccfg,
+                            struct kobject *parent, char *name)
+{
+       struct net_dev_context *nd;
+       struct net_dev_channel *ch;
+
+       if (!iface)
+               return -EINVAL;
+
+       if (ccfg->data_type != MOST_CH_ASYNC)
+               return -EINVAL;
+
+       nd = get_net_dev_context(iface);
+
+       if (!nd) {
+               nd = kzalloc(sizeof(*nd), GFP_KERNEL);
+               if (!nd)
+                       return -ENOMEM;
+
+               nd->iface = iface;
+
+               spin_lock(&list_lock);
+               list_add(&nd->list, &net_devices);
+               spin_unlock(&list_lock);
+       }
+
+       ch = ccfg->direction == MOST_CH_TX ? &nd->tx : &nd->rx;
+       if (ch->linked) {
+               pr_err("only one channel per instance & direction allowed\n");
+               return -EINVAL;
+       }
+
+       if (nd->tx.linked || nd->rx.linked) {
+               struct net_device *dev =
+                       alloc_netdev(0, "meth%d", NET_NAME_UNKNOWN, most_nd_setup);
+
+               if (!dev) {
+                       pr_err("no memory for net_device\n");
+                       return -ENOMEM;
+               }
+
+               nd->dev = dev;
+
+               dev->ml_priv = nd;
+               if (register_netdev(dev)) {
+                       pr_err("registering net device failed\n");
+                       free_netdev(dev);
+                       return -EINVAL;
+               }
+       }
+
+       ch->ch_id = channel_idx;
+       ch->linked = true;
+
+       return 0;
+}
+
+static int aim_disconnect_channel(struct most_interface *iface,
+                                 int channel_idx)
+{
+       struct net_dev_context *nd;
+       struct net_dev_channel *ch;
+
+       nd = get_net_dev_context(iface);
+       if (!nd)
+               return -EINVAL;
+
+       if (nd->rx.linked && channel_idx == nd->rx.ch_id)
+               ch = &nd->rx;
+       else if (nd->tx.linked && channel_idx == nd->tx.ch_id)
+               ch = &nd->tx;
+       else
+               return -EINVAL;
+
+       ch->linked = false;
+
+       /*
+        * do not call most_stop_channel() here, because channels are
+        * going to be closed in ndo_stop() after unregister_netdev()
+        */
+       most_net_rm_netdev_safe(nd);
+
+       if (!nd->rx.linked && !nd->tx.linked) {
+               spin_lock(&list_lock);
+               list_del(&nd->list);
+               spin_unlock(&list_lock);
+               kfree(nd);
+       }
+
+       return 0;
+}
+
+static int aim_resume_tx_channel(struct most_interface *iface,
+                                int channel_idx)
+{
+       struct net_dev_context *nd;
+
+       nd = get_net_dev_context(iface);
+       if (!nd || !nd->channels_opened || nd->tx.ch_id != channel_idx)
+               return 0;
+
+       if (!nd->dev)
+               return 0;
+
+       netif_wake_queue(nd->dev);
+       return 0;
+}
+
+static int aim_rx_data(struct mbo *mbo)
+{
+       const u32 zero = 0;
+       struct net_dev_context *nd;
+       char *buf = mbo->virt_address;
+       uint32_t len = mbo->processed_length;
+       struct sk_buff *skb;
+       struct net_device *dev;
+
+       nd = get_net_dev_context(mbo->ifp);
+       if (!nd || !nd->channels_opened || nd->rx.ch_id != mbo->hdm_channel_id)
+               return -EIO;
+
+       dev = nd->dev;
+       if (!dev) {
+               pr_err_once("drop packet: missing net_device\n");
+               return -EIO;
+       }
+
+       if (nd->is_mamac) {
+               if (!PMS_IS_MAMAC(buf, len))
+                       return -EIO;
+
+               skb = dev_alloc_skb(len - MDP_HDR_LEN + 2 * ETH_ALEN + 2);
+       } else {
+               if (!PMS_IS_MEP(buf, len))
+                       return -EIO;
+
+               skb = dev_alloc_skb(len - MEP_HDR_LEN);
+       }
+
+       if (!skb) {
+               dev->stats.rx_dropped++;
+               pr_err_once("drop packet: no memory for skb\n");
+               goto out;
+       }
+
+       skb->dev = dev;
+
+       if (nd->is_mamac) {
+               /* dest */
+               memcpy(skb_put(skb, ETH_ALEN), dev->dev_addr, ETH_ALEN);
+
+               /* src */
+               memcpy(skb_put(skb, 4), &zero, 4);
+               memcpy(skb_put(skb, 2), buf + 5, 2);
+
+               /* eth type */
+               memcpy(skb_put(skb, 2), buf + 10, 2);
+
+               buf += MDP_HDR_LEN;
+               len -= MDP_HDR_LEN;
+       } else {
+               buf += MEP_HDR_LEN;
+               len -= MEP_HDR_LEN;
+       }
+
+       memcpy(skb_put(skb, len), buf, len);
+       skb->protocol = eth_type_trans(skb, dev);
+       dev->stats.rx_packets++;
+       dev->stats.rx_bytes += skb->len;
+       netif_rx(skb);
+
+out:
+       most_put_mbo(mbo);
+       return 0;
+}
+
+static int __init most_net_init(void)
+{
+       pr_info("most_net_init()\n");
+       spin_lock_init(&list_lock);
+       aim.name = "networking";
+       aim.probe_channel = aim_probe_channel;
+       aim.disconnect_channel = aim_disconnect_channel;
+       aim.tx_completion = aim_resume_tx_channel;
+       aim.rx_completion = aim_rx_data;
+       return most_register_aim(&aim);
+}
+
+static void __exit most_net_exit(void)
+{
+       struct net_dev_context *nd, *tmp;
+
+       spin_lock(&list_lock);
+       list_for_each_entry_safe(nd, tmp, &net_devices, list) {
+               list_del(&nd->list);
+               spin_unlock(&list_lock);
+               /*
+                * do not call most_stop_channel() here, because channels are
+                * going to be closed in ndo_stop() after unregister_netdev()
+                */
+               most_net_rm_netdev_safe(nd);
+               kfree(nd);
+               spin_lock(&list_lock);
+       }
+       spin_unlock(&list_lock);
+
+       most_deregister_aim(&aim);
+       pr_info("most_net_exit()\n");
+}
+
+/**
+ * most_deliver_netinfo - callback for HDM to be informed about HW's MAC
+ * @param iface - most interface instance
+ * @param link_stat - link status
+ * @param mac_addr - MAC address
+ */
+void most_deliver_netinfo(struct most_interface *iface,
+                         unsigned char link_stat, unsigned char *mac_addr)
+{
+       struct net_dev_context *nd;
+       struct net_device *dev;
+
+       pr_info("Received netinfo from %s\n", iface->description);
+
+       nd = get_net_dev_context(iface);
+       if (!nd)
+               return;
+
+       dev = nd->dev;
+       if (!dev)
+               return;
+
+       if (mac_addr)
+               memcpy(dev->dev_addr, mac_addr, ETH_ALEN);
+
+       if (nd->link_stat != link_stat) {
+               nd->link_stat = link_stat;
+               if (nd->link_stat)
+                       netif_wake_queue(dev);
+               else
+                       netif_stop_queue(dev);
+       }
+}
+EXPORT_SYMBOL(most_deliver_netinfo);
+
+module_init(most_net_init);
+module_exit(most_net_exit);
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Andrey Shvetsov <andrey.shvetsov@k2l.de>");
+MODULE_DESCRIPTION("Networking Application Interface Module for MostCore");
diff --git a/drivers/staging/most/aim-network/networking.h b/drivers/staging/most/aim-network/networking.h
new file mode 100644 (file)
index 0000000..1b8b434
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Networking AIM - Networking Application Interface Module for MostCore
+ *
+ * Copyright (C) 2015, Microchip Technology Germany II GmbH & Co. KG
+ *
+ * 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.
+ *
+ * This file is licensed under GPLv2.
+ */
+#ifndef _NETWORKING_H_
+#define _NETWORKING_H_
+
+#include "mostcore.h"
+
+
+void most_deliver_netinfo(struct most_interface *iface,
+                         unsigned char link_stat, unsigned char *mac_addr);
+
+
+#endif