| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2020-21 Intel Corporation. |
| */ |
| |
| #include <linux/etherdevice.h> |
| #include <linux/if_arp.h> |
| #include <linux/if_link.h> |
| #include <linux/rtnetlink.h> |
| #include <linux/wwan.h> |
| #include <net/pkt_sched.h> |
| |
| #include "iosm_ipc_chnl_cfg.h" |
| #include "iosm_ipc_imem_ops.h" |
| #include "iosm_ipc_wwan.h" |
| |
| #define IOSM_IP_TYPE_MASK 0xF0 |
| #define IOSM_IP_TYPE_IPV4 0x40 |
| #define IOSM_IP_TYPE_IPV6 0x60 |
| |
| /** |
| * struct iosm_netdev_priv - netdev WWAN driver specific private data |
| * @ipc_wwan: Pointer to iosm_wwan struct |
| * @netdev: Pointer to network interface device structure |
| * @if_id: Interface id for device. |
| * @ch_id: IPC channel number for which interface device is created. |
| */ |
| struct iosm_netdev_priv { |
| struct iosm_wwan *ipc_wwan; |
| struct net_device *netdev; |
| int if_id; |
| int ch_id; |
| }; |
| |
| /** |
| * struct iosm_wwan - This structure contains information about WWAN root device |
| * and interface to the IPC layer. |
| * @ipc_imem: Pointer to imem data-struct |
| * @sub_netlist: List of active netdevs |
| * @dev: Pointer device structure |
| */ |
| struct iosm_wwan { |
| struct iosm_imem *ipc_imem; |
| struct iosm_netdev_priv __rcu *sub_netlist[IP_MUX_SESSION_END + 1]; |
| struct device *dev; |
| }; |
| |
| /* Bring-up the wwan net link */ |
| static int ipc_wwan_link_open(struct net_device *netdev) |
| { |
| struct iosm_netdev_priv *priv = wwan_netdev_drvpriv(netdev); |
| struct iosm_wwan *ipc_wwan = priv->ipc_wwan; |
| int if_id = priv->if_id; |
| |
| if (if_id < IP_MUX_SESSION_START || |
| if_id >= ARRAY_SIZE(ipc_wwan->sub_netlist)) |
| return -EINVAL; |
| |
| /* get channel id */ |
| priv->ch_id = ipc_imem_sys_wwan_open(ipc_wwan->ipc_imem, if_id); |
| |
| if (priv->ch_id < 0) { |
| dev_err(ipc_wwan->dev, |
| "cannot connect wwan0 & id %d to the IPC mem layer", |
| if_id); |
| return -ENODEV; |
| } |
| |
| /* enable tx path, DL data may follow */ |
| netif_start_queue(netdev); |
| |
| dev_dbg(ipc_wwan->dev, "Channel id %d allocated to if_id %d", |
| priv->ch_id, priv->if_id); |
| |
| return 0; |
| } |
| |
| /* Bring-down the wwan net link */ |
| static int ipc_wwan_link_stop(struct net_device *netdev) |
| { |
| struct iosm_netdev_priv *priv = wwan_netdev_drvpriv(netdev); |
| |
| netif_stop_queue(netdev); |
| |
| ipc_imem_sys_wwan_close(priv->ipc_wwan->ipc_imem, priv->if_id, |
| priv->ch_id); |
| priv->ch_id = -1; |
| |
| return 0; |
| } |
| |
| /* Transmit a packet */ |
| static netdev_tx_t ipc_wwan_link_transmit(struct sk_buff *skb, |
| struct net_device *netdev) |
| { |
| struct iosm_netdev_priv *priv = wwan_netdev_drvpriv(netdev); |
| struct iosm_wwan *ipc_wwan = priv->ipc_wwan; |
| unsigned int len = skb->len; |
| int if_id = priv->if_id; |
| int ret; |
| |
| /* Interface IDs from 1 to 8 are for IP data |
| * & from 257 to 261 are for non-IP data |
| */ |
| if (if_id < IP_MUX_SESSION_START || |
| if_id >= ARRAY_SIZE(ipc_wwan->sub_netlist)) |
| return -EINVAL; |
| |
| /* Send the SKB to device for transmission */ |
| ret = ipc_imem_sys_wwan_transmit(ipc_wwan->ipc_imem, |
| if_id, priv->ch_id, skb); |
| |
| /* Return code of zero is success */ |
| if (ret == 0) { |
| netdev->stats.tx_packets++; |
| netdev->stats.tx_bytes += len; |
| ret = NETDEV_TX_OK; |
| } else if (ret == -EBUSY) { |
| ret = NETDEV_TX_BUSY; |
| dev_err(ipc_wwan->dev, "unable to push packets"); |
| } else { |
| goto exit; |
| } |
| |
| return ret; |
| |
| exit: |
| /* Log any skb drop */ |
| if (if_id) |
| dev_dbg(ipc_wwan->dev, "skb dropped. IF_ID: %d, ret: %d", if_id, |
| ret); |
| |
| dev_kfree_skb_any(skb); |
| netdev->stats.tx_dropped++; |
| return NETDEV_TX_OK; |
| } |
| |
| /* Ops structure for wwan net link */ |
| static const struct net_device_ops ipc_inm_ops = { |
| .ndo_open = ipc_wwan_link_open, |
| .ndo_stop = ipc_wwan_link_stop, |
| .ndo_start_xmit = ipc_wwan_link_transmit, |
| }; |
| |
| /* Setup function for creating new net link */ |
| static void ipc_wwan_setup(struct net_device *iosm_dev) |
| { |
| iosm_dev->header_ops = NULL; |
| iosm_dev->hard_header_len = 0; |
| iosm_dev->tx_queue_len = DEFAULT_TX_QUEUE_LEN; |
| |
| iosm_dev->type = ARPHRD_NONE; |
| iosm_dev->mtu = ETH_DATA_LEN; |
| iosm_dev->min_mtu = ETH_MIN_MTU; |
| iosm_dev->max_mtu = ETH_MAX_MTU; |
| |
| iosm_dev->flags = IFF_POINTOPOINT | IFF_NOARP; |
| iosm_dev->needs_free_netdev = true; |
| |
| iosm_dev->netdev_ops = &ipc_inm_ops; |
| } |
| |
| /* Create new wwan net link */ |
| static int ipc_wwan_newlink(void *ctxt, struct net_device *dev, |
| u32 if_id, struct netlink_ext_ack *extack) |
| { |
| struct iosm_wwan *ipc_wwan = ctxt; |
| struct iosm_netdev_priv *priv; |
| int err; |
| |
| if (if_id < IP_MUX_SESSION_START || |
| if_id >= ARRAY_SIZE(ipc_wwan->sub_netlist)) |
| return -EINVAL; |
| |
| priv = wwan_netdev_drvpriv(dev); |
| priv->if_id = if_id; |
| priv->netdev = dev; |
| priv->ipc_wwan = ipc_wwan; |
| |
| if (rcu_access_pointer(ipc_wwan->sub_netlist[if_id])) |
| return -EBUSY; |
| |
| err = register_netdevice(dev); |
| if (err) |
| return err; |
| |
| rcu_assign_pointer(ipc_wwan->sub_netlist[if_id], priv); |
| netif_device_attach(dev); |
| |
| return 0; |
| } |
| |
| static void ipc_wwan_dellink(void *ctxt, struct net_device *dev, |
| struct list_head *head) |
| { |
| struct iosm_netdev_priv *priv = wwan_netdev_drvpriv(dev); |
| struct iosm_wwan *ipc_wwan = ctxt; |
| int if_id = priv->if_id; |
| |
| if (WARN_ON(if_id < IP_MUX_SESSION_START || |
| if_id >= ARRAY_SIZE(ipc_wwan->sub_netlist))) |
| return; |
| |
| if (WARN_ON(rcu_access_pointer(ipc_wwan->sub_netlist[if_id]) != priv)) |
| return; |
| |
| RCU_INIT_POINTER(ipc_wwan->sub_netlist[if_id], NULL); |
| /* unregistering includes synchronize_net() */ |
| unregister_netdevice_queue(dev, head); |
| } |
| |
| static const struct wwan_ops iosm_wwan_ops = { |
| .priv_size = sizeof(struct iosm_netdev_priv), |
| .setup = ipc_wwan_setup, |
| .newlink = ipc_wwan_newlink, |
| .dellink = ipc_wwan_dellink, |
| }; |
| |
| int ipc_wwan_receive(struct iosm_wwan *ipc_wwan, struct sk_buff *skb_arg, |
| bool dss, int if_id) |
| { |
| struct sk_buff *skb = skb_arg; |
| struct net_device_stats *stats; |
| struct iosm_netdev_priv *priv; |
| int ret; |
| |
| if ((skb->data[0] & IOSM_IP_TYPE_MASK) == IOSM_IP_TYPE_IPV4) |
| skb->protocol = htons(ETH_P_IP); |
| else if ((skb->data[0] & IOSM_IP_TYPE_MASK) == |
| IOSM_IP_TYPE_IPV6) |
| skb->protocol = htons(ETH_P_IPV6); |
| |
| skb->pkt_type = PACKET_HOST; |
| |
| if (if_id < IP_MUX_SESSION_START || |
| if_id > IP_MUX_SESSION_END) { |
| ret = -EINVAL; |
| goto free; |
| } |
| |
| rcu_read_lock(); |
| priv = rcu_dereference(ipc_wwan->sub_netlist[if_id]); |
| if (!priv) { |
| ret = -EINVAL; |
| goto unlock; |
| } |
| skb->dev = priv->netdev; |
| stats = &priv->netdev->stats; |
| stats->rx_packets++; |
| stats->rx_bytes += skb->len; |
| |
| ret = netif_rx(skb); |
| skb = NULL; |
| unlock: |
| rcu_read_unlock(); |
| free: |
| dev_kfree_skb(skb); |
| return ret; |
| } |
| |
| void ipc_wwan_tx_flowctrl(struct iosm_wwan *ipc_wwan, int if_id, bool on) |
| { |
| struct net_device *netdev; |
| struct iosm_netdev_priv *priv; |
| bool is_tx_blk; |
| |
| rcu_read_lock(); |
| priv = rcu_dereference(ipc_wwan->sub_netlist[if_id]); |
| if (!priv) { |
| rcu_read_unlock(); |
| return; |
| } |
| |
| netdev = priv->netdev; |
| |
| is_tx_blk = netif_queue_stopped(netdev); |
| |
| if (on) |
| dev_dbg(ipc_wwan->dev, "session id[%d]: flowctrl enable", |
| if_id); |
| |
| if (on && !is_tx_blk) |
| netif_stop_queue(netdev); |
| else if (!on && is_tx_blk) |
| netif_wake_queue(netdev); |
| rcu_read_unlock(); |
| } |
| |
| struct iosm_wwan *ipc_wwan_init(struct iosm_imem *ipc_imem, struct device *dev) |
| { |
| struct iosm_wwan *ipc_wwan; |
| |
| ipc_wwan = kzalloc(sizeof(*ipc_wwan), GFP_KERNEL); |
| if (!ipc_wwan) |
| return NULL; |
| |
| ipc_wwan->dev = dev; |
| ipc_wwan->ipc_imem = ipc_imem; |
| |
| /* WWAN core will create a netdev for the default IP MUX channel */ |
| if (wwan_register_ops(ipc_wwan->dev, &iosm_wwan_ops, ipc_wwan, |
| IP_MUX_SESSION_DEFAULT)) { |
| kfree(ipc_wwan); |
| return NULL; |
| } |
| |
| return ipc_wwan; |
| } |
| |
| void ipc_wwan_deinit(struct iosm_wwan *ipc_wwan) |
| { |
| /* This call will remove all child netdev(s) */ |
| wwan_unregister_ops(ipc_wwan->dev); |
| |
| kfree(ipc_wwan); |
| } |