/* * tegra_hv_net.c: ethernet emulation over Tegra HV * * Very loosely based on virtio_net.c * * Copyright (C) 2014-2018, NVIDIA CORPORATION. All rights reserved. * * This file is licensed under the terms of the GNU General Public License * version 2. This program is licensed "as is" without any warranty of any * kind, whether express or implied. */ #undef DEBUG #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DRV_NAME "tegra_hv_net" #define DRV_VERSION "0.1" #include #include /* frame format is * 0000: * 0004: data * * data frame * 0000: [16-bit frame size][13-bit pad][Last-bit][First-bit][F_CNTRL=0] * 0004: [packet-size 32 bit] * 0008: data * * control frame * 0000: (F_CNTRL == 1) F_CNTRL_CMD(x) * 0004: control frame data (deduced from F_CNTRL_CMD) */ /* data header size */ #define HDR_SIZE 8 #define F_CNTRL (1 << 0) /* control frame (0 = data frame) */ #define F_CNTRL_CMD(x) ((u32)((x) & 0xff) << 24) /* control frame command */ #define F_CNTRL_CMD_STATUS F_CNTRL_CMD(0) /* link status cmd */ #define F_STATUS_UP (1 << 1) /* link status is up */ #define F_STATUS_PAUSE (1 << 2) /* link status is pause */ #define F_STATUS_PENDING (1 << 23) /* pending link status update */ #define F_DATA_FIRST (1 << 1) /* first chunk of a frame */ #define F_DATA_LAST (1 << 2) /* last chunk of a frame */ #define F_DATA_FSIZE_SHIFT 16 #define F_DATA_FSIZE_MASK (~0 << F_DATA_FSIZE_SHIFT) #define F_DATA_FSIZE(x) (((u32)(x) << F_DATA_FSIZE_SHIFT) & F_DATA_FSIZE_MASK) /* jumbo frame limit */ #define MAX_MTU 9000 #define DEFAULT_HIGH_WATERMARK_MULT 50 #define DEFAULT_LOW_WATERMARK_MULT 25 #define DEFAULT_MAX_TX_DELAY_MSECS 10 enum drop_kind { dk_none, /* tx */ dk_linearize, dk_full, dk_wq, dk_write, /* rx */ dk_frame, dk_packet, dk_unexpected, dk_alloc, dk_overflow, }; struct tegra_hv_net_stats { struct u64_stats_sync tx_syncp; struct u64_stats_sync rx_syncp; u64 tx_bytes; u64 tx_packets; u64 tx_drops; u64 rx_bytes; u64 rx_packets; u64 rx_drops; /* internal tx stats */ u64 tx_linearize_fail; u64 tx_queue_full; u64 tx_wq_fail; u64 tx_ivc_write_fail; /* internal rx stats */ u64 rx_bad_frame; u64 rx_bad_packet; u64 rx_unexpected_packet; u64 rx_alloc_fail; u64 rx_overflow; }; struct tegra_hv_net { struct platform_device *pdev; struct net_device *ndev; struct tegra_hv_ivc_cookie *ivck; const void *mac_address; struct napi_struct napi; struct tegra_hv_net_stats __percpu *stats; struct sk_buff *rx_skb; struct sk_buff_head tx_q; struct work_struct xmit_work; struct workqueue_struct *xmit_wq; wait_queue_head_t wq; unsigned int high_watermark; /* mult * framesize */ unsigned int low_watermark; unsigned int max_tx_delay; }; static int tegra_hv_net_open(struct net_device *ndev) { struct tegra_hv_net *hvn = netdev_priv(ndev); napi_enable(&hvn->napi); netif_start_queue(ndev); /* * check if there are already packets in our queue, * and if so, we need to schedule a call to handle them */ if (tegra_hv_ivc_can_read(hvn->ivck)) napi_schedule(&hvn->napi); return 0; } static irqreturn_t tegra_hv_net_interrupt(int irq, void *data) { struct net_device *ndev = data; struct tegra_hv_net *hvn = netdev_priv(ndev); /* until this function returns 0, the channel is unusable */ if (tegra_hv_ivc_channel_notified(hvn->ivck) != 0) return IRQ_HANDLED; if (tegra_hv_ivc_can_write(hvn->ivck)) wake_up_interruptible_all(&hvn->wq); if (tegra_hv_ivc_can_read(hvn->ivck)) napi_schedule(&hvn->napi); return IRQ_HANDLED; } static void *tegra_hv_net_xmit_get_buffer(struct tegra_hv_net *hvn) { void *p; int ret; /* * grabbing a frame can fail for the following reasons: * 1. the channel is full / peer is uncooperative * 2. the channel is under reset / peer has restarted */ p = tegra_hv_ivc_write_get_next_frame(hvn->ivck); if (IS_ERR(p)) { ret = wait_event_interruptible_timeout(hvn->wq, !IS_ERR(p = tegra_hv_ivc_write_get_next_frame( hvn->ivck)), msecs_to_jiffies(hvn->max_tx_delay)); if (ret <= 0) { net_warn_ratelimited( "%s: timed out after %u ms\n", hvn->ndev->name, hvn->max_tx_delay); } } return p; } static void tegra_hv_net_xmit_work(struct work_struct *work) { struct tegra_hv_net *hvn = container_of(work, struct tegra_hv_net, xmit_work); struct tegra_hv_net_stats *stats = raw_cpu_ptr(hvn->stats); struct net_device *ndev = hvn->ndev; struct sk_buff *skb; int ret, max_frame, count, first, last, orig_len; u32 *p, p0, p1; enum drop_kind dk; max_frame = hvn->ivck->frame_size - HDR_SIZE; dk = dk_none; while ((skb = skb_dequeue(&hvn->tx_q)) != NULL) { /* start the queue if it is short again */ if (netif_queue_stopped(ndev) && skb_queue_len(&hvn->tx_q) < hvn->low_watermark) netif_start_queue(ndev); ret = skb_linearize(skb); if (ret != 0) { netdev_err(hvn->ndev, "%s: skb_linearize error=%d\n", __func__, ret); dk = dk_linearize; goto drop; } /* print_hex_dump(KERN_INFO, "tx-", DUMP_PREFIX_OFFSET, * 16, 1, skb->data, skb->len, true); */ /* copy the fragments */ orig_len = skb->len; first = 1; while (skb->len > 0) { count = skb->len; if (count > max_frame) count = max_frame; /* wait up to the maximum send timeout */ p = tegra_hv_net_xmit_get_buffer(hvn); if (IS_ERR(p)) { dk = dk_wq; goto drop; } last = skb->len == count; p0 = F_DATA_FSIZE(count); if (first) p0 |= F_DATA_FIRST; if (last) p0 |= F_DATA_LAST; p1 = orig_len; netdev_dbg(ndev, "F: %c%c F%d P%d [%08x %08x]\n", first ? 'F' : '.', last ? 'L' : '.', count, orig_len, p[0], p[1]); first = 0; p[0] = p0; p[1] = p1; skb_copy_from_linear_data(skb, &p[2], count); /* advance the tx queue */ (void)tegra_hv_ivc_write_advance(hvn->ivck); skb_pull(skb, count); } /* all OK */ dk = dk_none; drop: dev_kfree_skb(skb); u64_stats_update_begin(&stats->tx_syncp); if (dk == dk_none) { stats->tx_packets++; stats->tx_bytes += orig_len; } else { stats->tx_drops++; switch (dk) { default: /* never happens but gcc sometimes whines */ break; case dk_linearize: stats->tx_linearize_fail++; break; case dk_full: stats->tx_queue_full++; break; case dk_wq: stats->tx_wq_fail++; break; case dk_write: stats->tx_ivc_write_fail++; break; } } u64_stats_update_end(&stats->tx_syncp); } } /* xmit is dummy, we just add the skb to the tx_q and queue work */ static netdev_tx_t tegra_hv_net_xmit(struct sk_buff *skb, struct net_device *ndev) { struct tegra_hv_net *hvn = netdev_priv(ndev); skb_orphan(skb); nf_reset(skb); skb_queue_tail(&hvn->tx_q, skb); queue_work_on(WORK_CPU_UNBOUND, hvn->xmit_wq, &hvn->xmit_work); /* stop the queue if it gets too long */ if (!netif_queue_stopped(ndev) && skb_queue_len(&hvn->tx_q) >= hvn->high_watermark) netif_stop_queue(ndev); else if (netif_queue_stopped(ndev) && skb_queue_len(&hvn->tx_q) < hvn->low_watermark) netif_start_queue(ndev); return NETDEV_TX_OK; } static int tegra_hv_net_stop(struct net_device *ndev) { struct tegra_hv_net *hvn = netdev_priv(ndev); netif_stop_queue(ndev); napi_disable(&hvn->napi); return 0; } static int tegra_hv_net_change_mtu(struct net_device *ndev, int new_mtu) { if (new_mtu < 14 || new_mtu > MAX_MTU) { netdev_err(ndev, "invalid MTU, max MTU is: %d\n", MAX_MTU); return -EINVAL; } if (ndev->mtu == new_mtu) return 0; /* we can really handle any MTU size */ return 0; } static void tegra_hv_net_set_rx_mode(struct net_device *ndev) { /* we don't do any kind of filtering */ } static void tegra_hv_net_tx_timeout(struct net_device *ndev) { netdev_err(ndev, "%s\n", __func__); } #if LINUX_VERSION_CODE >= KERNEL_VERSION(4,14,0) void #else static struct rtnl_link_stats64 * #endif tegra_hv_net_get_stats64(struct net_device *ndev, struct rtnl_link_stats64 *tot) { struct tegra_hv_net *hvn = netdev_priv(ndev); struct tegra_hv_net_stats *stats; u64 tx_packets, tx_bytes, tx_drops, rx_packets, rx_bytes, rx_drops; unsigned int start; int cpu; for_each_possible_cpu(cpu) { stats = per_cpu_ptr(hvn->stats, cpu); do { start = u64_stats_fetch_begin_irq(&stats->tx_syncp); tx_packets = stats->tx_packets; tx_bytes = stats->tx_bytes; tx_drops = stats->tx_drops; } while (u64_stats_fetch_retry_irq(&stats->tx_syncp, start)); do { start = u64_stats_fetch_begin_irq(&stats->rx_syncp); rx_packets = stats->rx_packets; rx_bytes = stats->rx_bytes; rx_drops = stats->rx_drops; } while (u64_stats_fetch_retry_irq(&stats->rx_syncp, start)); tot->tx_packets += tx_packets; tot->tx_bytes += tx_bytes; tot->tx_dropped += tx_drops; tot->rx_packets += rx_packets; tot->rx_bytes += rx_bytes; tot->rx_dropped += rx_drops; } #if LINUX_VERSION_CODE < KERNEL_VERSION(4,14,0) return tot; #endif } static int tegra_hv_net_set_mac_address(struct net_device *dev, void *p) { return 0; } static const struct net_device_ops tegra_hv_netdev_ops = { .ndo_open = tegra_hv_net_open, .ndo_start_xmit = tegra_hv_net_xmit, .ndo_stop = tegra_hv_net_stop, .ndo_change_mtu = tegra_hv_net_change_mtu, .ndo_set_rx_mode = tegra_hv_net_set_rx_mode, .ndo_tx_timeout = tegra_hv_net_tx_timeout, .ndo_get_stats64 = tegra_hv_net_get_stats64, .ndo_set_mac_address = tegra_hv_net_set_mac_address, }; static void tegra_hv_net_ethtool_get_drvinfo(struct net_device *ndev, struct ethtool_drvinfo *info) { struct tegra_hv_net *hvn = netdev_priv(ndev); strlcpy(info->driver, DRV_NAME, sizeof(info->driver)); strlcpy(info->version, DRV_VERSION, sizeof(info->version)); strlcpy(info->bus_info, dev_name(&hvn->pdev->dev), sizeof(info->bus_info)); } static const struct ethtool_ops tegra_hv_ethtool_ops = { .get_drvinfo = tegra_hv_net_ethtool_get_drvinfo, .get_link = ethtool_op_get_link, }; static void tegra_hv_net_tx_complete(struct tegra_hv_net *hvn) { struct net_device *ndev = hvn->ndev; /* wake queue if no more tx buffers */ if (skb_queue_len(&hvn->tx_q) == 0) netif_wake_queue(ndev); } static int tegra_hv_net_rx(struct tegra_hv_net *hvn, int limit) { struct tegra_hv_net_stats *stats = this_cpu_ptr(hvn->stats); struct net_device *ndev = hvn->ndev; struct sk_buff *skb; int nr, frame_size, max_frame, count, first, last; u32 *p, p0; enum drop_kind dk; max_frame = hvn->ivck->frame_size - HDR_SIZE; nr = 0; dk = dk_none; while (nr < limit) { /* * grabbing a frame can fail for the following reasons: * 1. the channel is empty / peer is uncooperative * 2. the channel is under reset / peer has restarted */ p = tegra_hv_ivc_read_get_next_frame(hvn->ivck); if (IS_ERR(p)) break; nr++; p0 = p[0]; first = !!(p0 & F_DATA_FIRST); last = !!(p0 & F_DATA_LAST); frame_size = (p0 & F_DATA_FSIZE_MASK) >> F_DATA_FSIZE_SHIFT; count = p[1]; netdev_dbg(ndev, "F: %c%c F%d P%d [%08x %08x]\n", first ? 'F' : '.', last ? 'L' : '.', frame_size, count, p[0], p[1]); if (frame_size > max_frame) { netdev_err(ndev, "Bad fragment size %d\n", frame_size); dk = dk_frame; goto drop; } /* verify that packet is sane */ if (count < 14 || count > MAX_MTU) { netdev_err(ndev, "Bad packet size %d\n", count); dk = dk_packet; goto drop; } /* receive state machine */ if (hvn->rx_skb == NULL) { if (!first) { netdev_err(ndev, "unexpected fragment\n"); dk = dk_unexpected; goto drop; } hvn->rx_skb = netdev_alloc_skb(ndev, count); if (hvn->rx_skb == NULL) { netdev_err(ndev, "failed to allocate packet\n"); dk = dk_alloc; goto drop; } } /* verify that skb still can receive the data */ if (skb_tailroom(hvn->rx_skb) < frame_size) { netdev_err(ndev, "skb overflow\n"); dev_kfree_skb(hvn->rx_skb); hvn->rx_skb = NULL; dk = dk_overflow; goto drop; } /* append the data */ skb = hvn->rx_skb; skb_copy_to_linear_data_offset(skb, skb->len, p + 2, frame_size); skb_put(skb, frame_size); if (last) { /* print_hex_dump(KERN_INFO, "rx-", DUMP_PREFIX_OFFSET, * 16, 1, skb->data, skb->len, true); */ count = skb->len; skb->protocol = eth_type_trans(skb, ndev); skb->ip_summed = CHECKSUM_NONE; netif_receive_skb(skb); hvn->rx_skb = NULL; } dk = dk_none; drop: (void)tegra_hv_ivc_read_advance(hvn->ivck); u64_stats_update_begin(&stats->rx_syncp); if (dk == dk_none) { if (last) { stats->rx_packets++; stats->rx_bytes += count; } } else { stats->rx_drops++; switch (dk) { default: /* never happens but gcc sometimes whines */ break; case dk_frame: stats->rx_bad_frame++; break; case dk_packet: stats->rx_bad_packet++; break; case dk_unexpected: stats->rx_unexpected_packet++; break; case dk_alloc: stats->rx_alloc_fail++; break; case dk_overflow: stats->rx_overflow++; break; } } u64_stats_update_end(&stats->rx_syncp); } return nr; } static int tegra_hv_net_poll(struct napi_struct *napi, int budget) { struct tegra_hv_net *hvn = container_of(napi, struct tegra_hv_net, napi); int work_done = 0; tegra_hv_net_tx_complete(hvn); work_done = tegra_hv_net_rx(hvn, budget); if (work_done < budget) { napi_complete(napi); /* * if an interrupt occurs after tegra_hv_net_rx() but before * napi_complete(), we lose the call to napi_schedule(). */ if (tegra_hv_ivc_can_read(hvn->ivck)) napi_reschedule(napi); } return work_done; } static int tegra_hv_net_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct device_node *dn, *hv_dn; struct net_device *ndev = NULL; struct tegra_hv_net *hvn = NULL; int ret; u32 id; u32 highmark, lowmark, txdelay; if (!is_tegra_hypervisor_mode()) { dev_info(dev, "Hypervisor is not present\n"); return -ENODEV; } dn = dev->of_node; if (dn == NULL) { dev_err(dev, "No OF data\n"); return -EINVAL; } hv_dn = of_parse_phandle(dn, "ivc", 0); if (hv_dn == NULL) { dev_err(dev, "Failed to parse phandle of ivc prop\n"); return -EINVAL; } ret = of_property_read_u32_index(dn, "ivc", 1, &id); if (ret != 0) { dev_err(dev, "Failed to read IVC property ID\n"); goto out_of_put; } ret = of_property_read_u32(dn, "high-watermark-mult", &highmark); if (ret != 0) highmark = DEFAULT_HIGH_WATERMARK_MULT; ret = of_property_read_u32(dn, "low-watermark-mult", &lowmark); if (ret != 0) lowmark = DEFAULT_LOW_WATERMARK_MULT; if (highmark <= lowmark) { dev_err(dev, "Bad watermark configuration (high <= low = %u < %u)\n", highmark, lowmark); goto out_of_put; } ret = of_property_read_u32(dn, "max-tx-delay-msecs", &txdelay); if (ret != 0) txdelay = DEFAULT_MAX_TX_DELAY_MSECS; ndev = alloc_netdev(sizeof(*hvn), "hv%d", NET_NAME_UNKNOWN, ether_setup); if (ndev == NULL) { dev_err(dev, "Failed to allocate netdev\n"); ret = -ENOMEM; goto out_of_put; } hvn = netdev_priv(ndev); hvn->stats = alloc_percpu(struct tegra_hv_net_stats); if (hvn->stats == NULL) { dev_err(dev, "Failed to allocate per-cpu stats\n"); ret = -ENOMEM; goto out_free_ndev; } hvn->ivck = tegra_hv_ivc_reserve(hv_dn, id, NULL); of_node_put(hv_dn); hv_dn = NULL; if (IS_ERR_OR_NULL(hvn->ivck)) { dev_err(dev, "Failed to reserve IVC channel %d\n", id); ret = PTR_ERR(hvn->ivck); hvn->ivck = NULL; goto out_free_stats; } hvn->high_watermark = highmark * hvn->ivck->nframes; hvn->low_watermark = lowmark * hvn->ivck->nframes; hvn->max_tx_delay = txdelay; /* make sure the frame size is sufficient */ if (hvn->ivck->frame_size <= HDR_SIZE + 4) { dev_err(dev, "frame size too small to support COMM\n"); ret = -EINVAL; goto out_unreserve; } dev_info(dev, "Reserved IVC channel #%d - frame_size=%d\n", id, hvn->ivck->frame_size); SET_NETDEV_DEV(ndev, dev); platform_set_drvdata(pdev, ndev); ether_setup(ndev); ndev->netdev_ops = &tegra_hv_netdev_ops; ndev->ethtool_ops = &tegra_hv_ethtool_ops; skb_queue_head_init(&hvn->tx_q); INIT_WORK(&hvn->xmit_work, tegra_hv_net_xmit_work); hvn->pdev = pdev; hvn->ndev = ndev; ndev->irq = hvn->ivck->irq; init_waitqueue_head(&hvn->wq); ndev->priv_flags |= IFF_UNICAST_FLT | IFF_LIVE_ADDR_CHANGE; ndev->hw_features = 0; /* we're a really dumb device for now */ ndev->features |= ndev->hw_features; /* get mac address from the DT */ hvn->mac_address = of_get_mac_address(dev->of_node); if (hvn->mac_address == NULL) { if (of_property_read_bool(dev->of_node, "use-random-mac-addr")) eth_hw_addr_random(ndev); else { unsigned int gid; dev_warn(dev, "No valid mac-address found, using fixed local address\n"); ndev->dev_addr[0] = 0x0a; ndev->dev_addr[1] = 0x86; ndev->dev_addr[2] = 0x4c; ndev->dev_addr[3] = 0xf8; ndev->dev_addr[4] = (uint8_t)id; ret = hyp_read_gid(&gid); if (ret != 0) { dev_err(dev, "Failed to read guest id\n"); goto out_unreserve; } ndev->dev_addr[5] = (uint8_t)(gid); } } else { /* Set the MAC address. */ ether_addr_copy(ndev->dev_addr, hvn->mac_address); } hvn->xmit_wq = alloc_workqueue("tgvnet-wq-%d", WQ_UNBOUND | WQ_MEM_RECLAIM, 1, /* FIXME: from DT? */ pdev->id); if (hvn->xmit_wq == NULL) { dev_err(dev, "Failed to allocate workqueue\n"); ret = -ENOMEM; goto out_unreserve; } netif_napi_add(ndev, &hvn->napi, tegra_hv_net_poll, 64); ret = register_netdev(ndev); if (ret) { dev_err(dev, "Failed to register netdev\n"); goto out_free_wq; } /* * start the channel reset process asynchronously. until the reset * process completes, any attempt to use the ivc channel will return * an error (e.g., all transmits will fail). */ tegra_hv_ivc_channel_reset(hvn->ivck); /* the interrupt request must be the last action */ ret = devm_request_irq(dev, ndev->irq, tegra_hv_net_interrupt, 0, dev_name(dev), ndev); if (ret != 0) { dev_err(dev, "Could not request irq #%d\n", ndev->irq); goto out_unreg_netdev; } dev_info(dev, "ready\n"); return 0; out_unreg_netdev: unregister_netdev(ndev); out_free_wq: netif_napi_del(&hvn->napi); destroy_workqueue(hvn->xmit_wq); out_unreserve: tegra_hv_ivc_unreserve(hvn->ivck); out_free_stats: free_percpu(hvn->stats); out_free_ndev: free_netdev(ndev); out_of_put: of_node_put(hv_dn); return ret; } static int tegra_hv_net_remove(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct net_device *ndev = platform_get_drvdata(pdev); struct tegra_hv_net *hvn = netdev_priv(ndev); platform_set_drvdata(pdev, NULL); devm_free_irq(dev, ndev->irq, dev); unregister_netdev(ndev); netif_napi_del(&hvn->napi); destroy_workqueue(hvn->xmit_wq); tegra_hv_ivc_unreserve(hvn->ivck); free_percpu(hvn->stats); free_netdev(ndev); return 0; } #ifdef CONFIG_PM static int tegra_hv_net_suspend(struct platform_device *pdev, pm_message_t state) { struct net_device *ndev = platform_get_drvdata(pdev); struct tegra_hv_net *hvn = netdev_priv(ndev); /* If the netdev is not even running, no action */ if (!netif_running(ndev)) return 0; /* As this device is going to suspend, * link can't be considered up, although we are not resetting * the IVC channel. Therefore mark the link detached * This would stop tx from getting queued as well */ netif_device_detach(ndev); ndev->netdev_ops->ndo_stop(ndev); /* tegra_hv_net_stop uses netif_stop_queue to disable the queue. * netif_stop_queue doesn't prevent xmit_transfer running on another * cpu, so additionally we need netif_tx_disable */ netif_tx_disable(ndev); /* Now no further job should be coming in, but * there could be one queued or running already. * Cancel or wait for such a job */ cancel_work_sync(&hvn->xmit_work); /* Workqueue should not be running at this point, * so disable irq */ disable_irq(ndev->irq); return 0; } static int tegra_hv_net_resume(struct platform_device *pdev) { struct net_device *ndev = platform_get_drvdata(pdev); struct tegra_hv_net *hvn = netdev_priv(ndev); if (!netif_running(ndev)) return 0; enable_irq(ndev->irq); ndev->netdev_ops->ndo_open(ndev); /* Would wake the queue and mark the link enabled */ netif_device_attach(ndev); /* Start the queue blindly, in case the previous * work was cancelled during suspend * If there is no pending xmit, * the workqueue will wake up then exit gracefully */ queue_work_on(WORK_CPU_UNBOUND, hvn->xmit_wq, &hvn->xmit_work); return 0; } #endif #ifdef CONFIG_OF static struct of_device_id tegra_hv_net_match[] = { { .compatible = "nvidia,tegra-hv-net", }, {}, }; MODULE_DEVICE_TABLE(of, tegra_hv_net_match); #endif /* CONFIG_OF */ static struct platform_driver tegra_hv_net_driver = { .probe = tegra_hv_net_probe, .remove = tegra_hv_net_remove, #ifdef CONFIG_PM_SLEEP .suspend = tegra_hv_net_suspend, .resume = tegra_hv_net_resume, #endif .driver = { .name = DRV_NAME, .owner = THIS_MODULE, .of_match_table = of_match_ptr(tegra_hv_net_match), }, }; module_platform_driver(tegra_hv_net_driver); MODULE_AUTHOR("Pantelis Antoniou "); MODULE_DESCRIPTION("Ethernet network device over Tegra Hypervisor IVC channel"); MODULE_LICENSE("GPL");