162306a36Sopenharmony_ci// SPDX-License-Identifier: GPL-2.0-or-later
262306a36Sopenharmony_ci/*
362306a36Sopenharmony_ci * Driver for USB ethernet port of Conexant CX82310-based ADSL routers
462306a36Sopenharmony_ci * Copyright (C) 2010 by Ondrej Zary
562306a36Sopenharmony_ci * some parts inspired by the cxacru driver
662306a36Sopenharmony_ci */
762306a36Sopenharmony_ci
862306a36Sopenharmony_ci#include <linux/module.h>
962306a36Sopenharmony_ci#include <linux/netdevice.h>
1062306a36Sopenharmony_ci#include <linux/etherdevice.h>
1162306a36Sopenharmony_ci#include <linux/ethtool.h>
1262306a36Sopenharmony_ci#include <linux/workqueue.h>
1362306a36Sopenharmony_ci#include <linux/mii.h>
1462306a36Sopenharmony_ci#include <linux/usb.h>
1562306a36Sopenharmony_ci#include <linux/usb/usbnet.h>
1662306a36Sopenharmony_ci
1762306a36Sopenharmony_cienum cx82310_cmd {
1862306a36Sopenharmony_ci	CMD_START		= 0x84,	/* no effect? */
1962306a36Sopenharmony_ci	CMD_STOP		= 0x85,	/* no effect? */
2062306a36Sopenharmony_ci	CMD_GET_STATUS		= 0x90,	/* returns nothing? */
2162306a36Sopenharmony_ci	CMD_GET_MAC_ADDR	= 0x91,	/* read MAC address */
2262306a36Sopenharmony_ci	CMD_GET_LINK_STATUS	= 0x92,	/* not useful, link is always up */
2362306a36Sopenharmony_ci	CMD_ETHERNET_MODE	= 0x99,	/* unknown, needed during init */
2462306a36Sopenharmony_ci};
2562306a36Sopenharmony_ci
2662306a36Sopenharmony_cienum cx82310_status {
2762306a36Sopenharmony_ci	STATUS_UNDEFINED,
2862306a36Sopenharmony_ci	STATUS_SUCCESS,
2962306a36Sopenharmony_ci	STATUS_ERROR,
3062306a36Sopenharmony_ci	STATUS_UNSUPPORTED,
3162306a36Sopenharmony_ci	STATUS_UNIMPLEMENTED,
3262306a36Sopenharmony_ci	STATUS_PARAMETER_ERROR,
3362306a36Sopenharmony_ci	STATUS_DBG_LOOPBACK,
3462306a36Sopenharmony_ci};
3562306a36Sopenharmony_ci
3662306a36Sopenharmony_ci#define CMD_PACKET_SIZE	64
3762306a36Sopenharmony_ci#define CMD_TIMEOUT	100
3862306a36Sopenharmony_ci#define CMD_REPLY_RETRY 5
3962306a36Sopenharmony_ci
4062306a36Sopenharmony_ci#define CX82310_MTU	1514
4162306a36Sopenharmony_ci#define CMD_EP		0x01
4262306a36Sopenharmony_ci
4362306a36Sopenharmony_cistruct cx82310_priv {
4462306a36Sopenharmony_ci	struct work_struct reenable_work;
4562306a36Sopenharmony_ci	struct usbnet *dev;
4662306a36Sopenharmony_ci};
4762306a36Sopenharmony_ci
4862306a36Sopenharmony_ci/*
4962306a36Sopenharmony_ci * execute control command
5062306a36Sopenharmony_ci *  - optionally send some data (command parameters)
5162306a36Sopenharmony_ci *  - optionally wait for the reply
5262306a36Sopenharmony_ci *  - optionally read some data from the reply
5362306a36Sopenharmony_ci */
5462306a36Sopenharmony_cistatic int cx82310_cmd(struct usbnet *dev, enum cx82310_cmd cmd, bool reply,
5562306a36Sopenharmony_ci		       u8 *wdata, int wlen, u8 *rdata, int rlen)
5662306a36Sopenharmony_ci{
5762306a36Sopenharmony_ci	int actual_len, retries, ret;
5862306a36Sopenharmony_ci	struct usb_device *udev = dev->udev;
5962306a36Sopenharmony_ci	u8 *buf = kzalloc(CMD_PACKET_SIZE, GFP_KERNEL);
6062306a36Sopenharmony_ci
6162306a36Sopenharmony_ci	if (!buf)
6262306a36Sopenharmony_ci		return -ENOMEM;
6362306a36Sopenharmony_ci
6462306a36Sopenharmony_ci	/* create command packet */
6562306a36Sopenharmony_ci	buf[0] = cmd;
6662306a36Sopenharmony_ci	if (wdata)
6762306a36Sopenharmony_ci		memcpy(buf + 4, wdata, min_t(int, wlen, CMD_PACKET_SIZE - 4));
6862306a36Sopenharmony_ci
6962306a36Sopenharmony_ci	/* send command packet */
7062306a36Sopenharmony_ci	ret = usb_bulk_msg(udev, usb_sndbulkpipe(udev, CMD_EP), buf,
7162306a36Sopenharmony_ci			   CMD_PACKET_SIZE, &actual_len, CMD_TIMEOUT);
7262306a36Sopenharmony_ci	if (ret < 0) {
7362306a36Sopenharmony_ci		if (cmd != CMD_GET_LINK_STATUS)
7462306a36Sopenharmony_ci			netdev_err(dev->net, "send command %#x: error %d\n",
7562306a36Sopenharmony_ci				   cmd, ret);
7662306a36Sopenharmony_ci		goto end;
7762306a36Sopenharmony_ci	}
7862306a36Sopenharmony_ci
7962306a36Sopenharmony_ci	if (reply) {
8062306a36Sopenharmony_ci		/* wait for reply, retry if it's empty */
8162306a36Sopenharmony_ci		for (retries = 0; retries < CMD_REPLY_RETRY; retries++) {
8262306a36Sopenharmony_ci			ret = usb_bulk_msg(udev, usb_rcvbulkpipe(udev, CMD_EP),
8362306a36Sopenharmony_ci					   buf, CMD_PACKET_SIZE, &actual_len,
8462306a36Sopenharmony_ci					   CMD_TIMEOUT);
8562306a36Sopenharmony_ci			if (ret < 0) {
8662306a36Sopenharmony_ci				if (cmd != CMD_GET_LINK_STATUS)
8762306a36Sopenharmony_ci					netdev_err(dev->net, "reply receive error %d\n",
8862306a36Sopenharmony_ci						   ret);
8962306a36Sopenharmony_ci				goto end;
9062306a36Sopenharmony_ci			}
9162306a36Sopenharmony_ci			if (actual_len > 0)
9262306a36Sopenharmony_ci				break;
9362306a36Sopenharmony_ci		}
9462306a36Sopenharmony_ci		if (actual_len == 0) {
9562306a36Sopenharmony_ci			netdev_err(dev->net, "no reply to command %#x\n", cmd);
9662306a36Sopenharmony_ci			ret = -EIO;
9762306a36Sopenharmony_ci			goto end;
9862306a36Sopenharmony_ci		}
9962306a36Sopenharmony_ci		if (buf[0] != cmd) {
10062306a36Sopenharmony_ci			netdev_err(dev->net, "got reply to command %#x, expected: %#x\n",
10162306a36Sopenharmony_ci				   buf[0], cmd);
10262306a36Sopenharmony_ci			ret = -EIO;
10362306a36Sopenharmony_ci			goto end;
10462306a36Sopenharmony_ci		}
10562306a36Sopenharmony_ci		if (buf[1] != STATUS_SUCCESS) {
10662306a36Sopenharmony_ci			netdev_err(dev->net, "command %#x failed: %#x\n", cmd,
10762306a36Sopenharmony_ci				   buf[1]);
10862306a36Sopenharmony_ci			ret = -EIO;
10962306a36Sopenharmony_ci			goto end;
11062306a36Sopenharmony_ci		}
11162306a36Sopenharmony_ci		if (rdata)
11262306a36Sopenharmony_ci			memcpy(rdata, buf + 4,
11362306a36Sopenharmony_ci			       min_t(int, rlen, CMD_PACKET_SIZE - 4));
11462306a36Sopenharmony_ci	}
11562306a36Sopenharmony_ciend:
11662306a36Sopenharmony_ci	kfree(buf);
11762306a36Sopenharmony_ci	return ret;
11862306a36Sopenharmony_ci}
11962306a36Sopenharmony_ci
12062306a36Sopenharmony_cistatic int cx82310_enable_ethernet(struct usbnet *dev)
12162306a36Sopenharmony_ci{
12262306a36Sopenharmony_ci	int ret = cx82310_cmd(dev, CMD_ETHERNET_MODE, true, "\x01", 1, NULL, 0);
12362306a36Sopenharmony_ci
12462306a36Sopenharmony_ci	if (ret)
12562306a36Sopenharmony_ci		netdev_err(dev->net, "unable to enable ethernet mode: %d\n",
12662306a36Sopenharmony_ci			   ret);
12762306a36Sopenharmony_ci	return ret;
12862306a36Sopenharmony_ci}
12962306a36Sopenharmony_ci
13062306a36Sopenharmony_cistatic void cx82310_reenable_work(struct work_struct *work)
13162306a36Sopenharmony_ci{
13262306a36Sopenharmony_ci	struct cx82310_priv *priv = container_of(work, struct cx82310_priv,
13362306a36Sopenharmony_ci						 reenable_work);
13462306a36Sopenharmony_ci	cx82310_enable_ethernet(priv->dev);
13562306a36Sopenharmony_ci}
13662306a36Sopenharmony_ci
13762306a36Sopenharmony_ci#define partial_len	data[0]		/* length of partial packet data */
13862306a36Sopenharmony_ci#define partial_rem	data[1]		/* remaining (missing) data length */
13962306a36Sopenharmony_ci#define partial_data	data[2]		/* partial packet data */
14062306a36Sopenharmony_ci
14162306a36Sopenharmony_cistatic int cx82310_bind(struct usbnet *dev, struct usb_interface *intf)
14262306a36Sopenharmony_ci{
14362306a36Sopenharmony_ci	int ret;
14462306a36Sopenharmony_ci	char buf[15];
14562306a36Sopenharmony_ci	struct usb_device *udev = dev->udev;
14662306a36Sopenharmony_ci	u8 link[3];
14762306a36Sopenharmony_ci	int timeout = 50;
14862306a36Sopenharmony_ci	struct cx82310_priv *priv;
14962306a36Sopenharmony_ci	u8 addr[ETH_ALEN];
15062306a36Sopenharmony_ci
15162306a36Sopenharmony_ci	/* avoid ADSL modems - continue only if iProduct is "USB NET CARD" */
15262306a36Sopenharmony_ci	if (usb_string(udev, udev->descriptor.iProduct, buf, sizeof(buf)) > 0
15362306a36Sopenharmony_ci	    && strcmp(buf, "USB NET CARD")) {
15462306a36Sopenharmony_ci		dev_info(&udev->dev, "ignoring: probably an ADSL modem\n");
15562306a36Sopenharmony_ci		return -ENODEV;
15662306a36Sopenharmony_ci	}
15762306a36Sopenharmony_ci
15862306a36Sopenharmony_ci	ret = usbnet_get_endpoints(dev, intf);
15962306a36Sopenharmony_ci	if (ret)
16062306a36Sopenharmony_ci		return ret;
16162306a36Sopenharmony_ci
16262306a36Sopenharmony_ci	/*
16362306a36Sopenharmony_ci	 * this must not include ethernet header as the device can send partial
16462306a36Sopenharmony_ci	 * packets with no header (and sometimes even empty URBs)
16562306a36Sopenharmony_ci	 */
16662306a36Sopenharmony_ci	dev->net->hard_header_len = 0;
16762306a36Sopenharmony_ci	/* we can send at most 1514 bytes of data (+ 2-byte header) per URB */
16862306a36Sopenharmony_ci	dev->hard_mtu = CX82310_MTU + 2;
16962306a36Sopenharmony_ci	/* we can receive URBs up to 4KB from the device */
17062306a36Sopenharmony_ci	dev->rx_urb_size = 4096;
17162306a36Sopenharmony_ci
17262306a36Sopenharmony_ci	dev->partial_data = (unsigned long) kmalloc(dev->hard_mtu, GFP_KERNEL);
17362306a36Sopenharmony_ci	if (!dev->partial_data)
17462306a36Sopenharmony_ci		return -ENOMEM;
17562306a36Sopenharmony_ci
17662306a36Sopenharmony_ci	priv = kzalloc(sizeof(*priv), GFP_KERNEL);
17762306a36Sopenharmony_ci	if (!priv) {
17862306a36Sopenharmony_ci		ret = -ENOMEM;
17962306a36Sopenharmony_ci		goto err_partial;
18062306a36Sopenharmony_ci	}
18162306a36Sopenharmony_ci	dev->driver_priv = priv;
18262306a36Sopenharmony_ci	INIT_WORK(&priv->reenable_work, cx82310_reenable_work);
18362306a36Sopenharmony_ci	priv->dev = dev;
18462306a36Sopenharmony_ci
18562306a36Sopenharmony_ci	/* wait for firmware to become ready (indicated by the link being up) */
18662306a36Sopenharmony_ci	while (--timeout) {
18762306a36Sopenharmony_ci		ret = cx82310_cmd(dev, CMD_GET_LINK_STATUS, true, NULL, 0,
18862306a36Sopenharmony_ci				  link, sizeof(link));
18962306a36Sopenharmony_ci		/* the command can time out during boot - it's not an error */
19062306a36Sopenharmony_ci		if (!ret && link[0] == 1 && link[2] == 1)
19162306a36Sopenharmony_ci			break;
19262306a36Sopenharmony_ci		msleep(500);
19362306a36Sopenharmony_ci	}
19462306a36Sopenharmony_ci	if (!timeout) {
19562306a36Sopenharmony_ci		netdev_err(dev->net, "firmware not ready in time\n");
19662306a36Sopenharmony_ci		ret = -ETIMEDOUT;
19762306a36Sopenharmony_ci		goto err;
19862306a36Sopenharmony_ci	}
19962306a36Sopenharmony_ci
20062306a36Sopenharmony_ci	/* enable ethernet mode (?) */
20162306a36Sopenharmony_ci	ret = cx82310_enable_ethernet(dev);
20262306a36Sopenharmony_ci	if (ret)
20362306a36Sopenharmony_ci		goto err;
20462306a36Sopenharmony_ci
20562306a36Sopenharmony_ci	/* get the MAC address */
20662306a36Sopenharmony_ci	ret = cx82310_cmd(dev, CMD_GET_MAC_ADDR, true, NULL, 0, addr, ETH_ALEN);
20762306a36Sopenharmony_ci	if (ret) {
20862306a36Sopenharmony_ci		netdev_err(dev->net, "unable to read MAC address: %d\n", ret);
20962306a36Sopenharmony_ci		goto err;
21062306a36Sopenharmony_ci	}
21162306a36Sopenharmony_ci	eth_hw_addr_set(dev->net, addr);
21262306a36Sopenharmony_ci
21362306a36Sopenharmony_ci	/* start (does not seem to have any effect?) */
21462306a36Sopenharmony_ci	ret = cx82310_cmd(dev, CMD_START, false, NULL, 0, NULL, 0);
21562306a36Sopenharmony_ci	if (ret)
21662306a36Sopenharmony_ci		goto err;
21762306a36Sopenharmony_ci
21862306a36Sopenharmony_ci	return 0;
21962306a36Sopenharmony_cierr:
22062306a36Sopenharmony_ci	kfree(dev->driver_priv);
22162306a36Sopenharmony_cierr_partial:
22262306a36Sopenharmony_ci	kfree((void *)dev->partial_data);
22362306a36Sopenharmony_ci	return ret;
22462306a36Sopenharmony_ci}
22562306a36Sopenharmony_ci
22662306a36Sopenharmony_cistatic void cx82310_unbind(struct usbnet *dev, struct usb_interface *intf)
22762306a36Sopenharmony_ci{
22862306a36Sopenharmony_ci	struct cx82310_priv *priv = dev->driver_priv;
22962306a36Sopenharmony_ci
23062306a36Sopenharmony_ci	kfree((void *)dev->partial_data);
23162306a36Sopenharmony_ci	cancel_work_sync(&priv->reenable_work);
23262306a36Sopenharmony_ci	kfree(dev->driver_priv);
23362306a36Sopenharmony_ci}
23462306a36Sopenharmony_ci
23562306a36Sopenharmony_ci/*
23662306a36Sopenharmony_ci * RX is NOT easy - we can receive multiple packets per skb, each having 2-byte
23762306a36Sopenharmony_ci * packet length at the beginning.
23862306a36Sopenharmony_ci * The last packet might be incomplete (when it crosses the 4KB URB size),
23962306a36Sopenharmony_ci * continuing in the next skb (without any headers).
24062306a36Sopenharmony_ci * If a packet has odd length, there is one extra byte at the end (before next
24162306a36Sopenharmony_ci * packet or at the end of the URB).
24262306a36Sopenharmony_ci */
24362306a36Sopenharmony_cistatic int cx82310_rx_fixup(struct usbnet *dev, struct sk_buff *skb)
24462306a36Sopenharmony_ci{
24562306a36Sopenharmony_ci	int len;
24662306a36Sopenharmony_ci	struct sk_buff *skb2;
24762306a36Sopenharmony_ci	struct cx82310_priv *priv = dev->driver_priv;
24862306a36Sopenharmony_ci
24962306a36Sopenharmony_ci	/*
25062306a36Sopenharmony_ci	 * If the last skb ended with an incomplete packet, this skb contains
25162306a36Sopenharmony_ci	 * end of that packet at the beginning.
25262306a36Sopenharmony_ci	 */
25362306a36Sopenharmony_ci	if (dev->partial_rem) {
25462306a36Sopenharmony_ci		len = dev->partial_len + dev->partial_rem;
25562306a36Sopenharmony_ci		skb2 = alloc_skb(len, GFP_ATOMIC);
25662306a36Sopenharmony_ci		if (!skb2)
25762306a36Sopenharmony_ci			return 0;
25862306a36Sopenharmony_ci		skb_put(skb2, len);
25962306a36Sopenharmony_ci		memcpy(skb2->data, (void *)dev->partial_data,
26062306a36Sopenharmony_ci		       dev->partial_len);
26162306a36Sopenharmony_ci		memcpy(skb2->data + dev->partial_len, skb->data,
26262306a36Sopenharmony_ci		       dev->partial_rem);
26362306a36Sopenharmony_ci		usbnet_skb_return(dev, skb2);
26462306a36Sopenharmony_ci		skb_pull(skb, (dev->partial_rem + 1) & ~1);
26562306a36Sopenharmony_ci		dev->partial_rem = 0;
26662306a36Sopenharmony_ci		if (skb->len < 2)
26762306a36Sopenharmony_ci			return 1;
26862306a36Sopenharmony_ci	}
26962306a36Sopenharmony_ci
27062306a36Sopenharmony_ci	/* a skb can contain multiple packets */
27162306a36Sopenharmony_ci	while (skb->len > 1) {
27262306a36Sopenharmony_ci		/* first two bytes are packet length */
27362306a36Sopenharmony_ci		len = skb->data[0] | (skb->data[1] << 8);
27462306a36Sopenharmony_ci		skb_pull(skb, 2);
27562306a36Sopenharmony_ci
27662306a36Sopenharmony_ci		/* if last packet in the skb, let usbnet to process it */
27762306a36Sopenharmony_ci		if (len == skb->len || len + 1 == skb->len) {
27862306a36Sopenharmony_ci			skb_trim(skb, len);
27962306a36Sopenharmony_ci			break;
28062306a36Sopenharmony_ci		}
28162306a36Sopenharmony_ci
28262306a36Sopenharmony_ci		if (len == 0xffff) {
28362306a36Sopenharmony_ci			netdev_info(dev->net, "router was rebooted, re-enabling ethernet mode");
28462306a36Sopenharmony_ci			schedule_work(&priv->reenable_work);
28562306a36Sopenharmony_ci		} else if (len > CX82310_MTU) {
28662306a36Sopenharmony_ci			netdev_err(dev->net, "RX packet too long: %d B\n", len);
28762306a36Sopenharmony_ci			return 0;
28862306a36Sopenharmony_ci		}
28962306a36Sopenharmony_ci
29062306a36Sopenharmony_ci		/* incomplete packet, save it for the next skb */
29162306a36Sopenharmony_ci		if (len > skb->len) {
29262306a36Sopenharmony_ci			dev->partial_len = skb->len;
29362306a36Sopenharmony_ci			dev->partial_rem = len - skb->len;
29462306a36Sopenharmony_ci			memcpy((void *)dev->partial_data, skb->data,
29562306a36Sopenharmony_ci			       dev->partial_len);
29662306a36Sopenharmony_ci			skb_pull(skb, skb->len);
29762306a36Sopenharmony_ci			break;
29862306a36Sopenharmony_ci		}
29962306a36Sopenharmony_ci
30062306a36Sopenharmony_ci		skb2 = alloc_skb(len, GFP_ATOMIC);
30162306a36Sopenharmony_ci		if (!skb2)
30262306a36Sopenharmony_ci			return 0;
30362306a36Sopenharmony_ci		skb_put(skb2, len);
30462306a36Sopenharmony_ci		memcpy(skb2->data, skb->data, len);
30562306a36Sopenharmony_ci		/* process the packet */
30662306a36Sopenharmony_ci		usbnet_skb_return(dev, skb2);
30762306a36Sopenharmony_ci
30862306a36Sopenharmony_ci		skb_pull(skb, (len + 1) & ~1);
30962306a36Sopenharmony_ci	}
31062306a36Sopenharmony_ci
31162306a36Sopenharmony_ci	/* let usbnet process the last packet */
31262306a36Sopenharmony_ci	return 1;
31362306a36Sopenharmony_ci}
31462306a36Sopenharmony_ci
31562306a36Sopenharmony_ci/* TX is easy, just add 2 bytes of length at the beginning */
31662306a36Sopenharmony_cistatic struct sk_buff *cx82310_tx_fixup(struct usbnet *dev, struct sk_buff *skb,
31762306a36Sopenharmony_ci				       gfp_t flags)
31862306a36Sopenharmony_ci{
31962306a36Sopenharmony_ci	int len = skb->len;
32062306a36Sopenharmony_ci
32162306a36Sopenharmony_ci	if (skb_cow_head(skb, 2)) {
32262306a36Sopenharmony_ci		dev_kfree_skb_any(skb);
32362306a36Sopenharmony_ci		return NULL;
32462306a36Sopenharmony_ci	}
32562306a36Sopenharmony_ci	skb_push(skb, 2);
32662306a36Sopenharmony_ci
32762306a36Sopenharmony_ci	skb->data[0] = len;
32862306a36Sopenharmony_ci	skb->data[1] = len >> 8;
32962306a36Sopenharmony_ci
33062306a36Sopenharmony_ci	return skb;
33162306a36Sopenharmony_ci}
33262306a36Sopenharmony_ci
33362306a36Sopenharmony_ci
33462306a36Sopenharmony_cistatic const struct driver_info	cx82310_info = {
33562306a36Sopenharmony_ci	.description	= "Conexant CX82310 USB ethernet",
33662306a36Sopenharmony_ci	.flags		= FLAG_ETHER,
33762306a36Sopenharmony_ci	.bind		= cx82310_bind,
33862306a36Sopenharmony_ci	.unbind		= cx82310_unbind,
33962306a36Sopenharmony_ci	.rx_fixup	= cx82310_rx_fixup,
34062306a36Sopenharmony_ci	.tx_fixup	= cx82310_tx_fixup,
34162306a36Sopenharmony_ci};
34262306a36Sopenharmony_ci
34362306a36Sopenharmony_ci#define USB_DEVICE_CLASS(vend, prod, cl, sc, pr) \
34462306a36Sopenharmony_ci	.match_flags = USB_DEVICE_ID_MATCH_DEVICE | \
34562306a36Sopenharmony_ci		       USB_DEVICE_ID_MATCH_DEV_INFO, \
34662306a36Sopenharmony_ci	.idVendor = (vend), \
34762306a36Sopenharmony_ci	.idProduct = (prod), \
34862306a36Sopenharmony_ci	.bDeviceClass = (cl), \
34962306a36Sopenharmony_ci	.bDeviceSubClass = (sc), \
35062306a36Sopenharmony_ci	.bDeviceProtocol = (pr)
35162306a36Sopenharmony_ci
35262306a36Sopenharmony_cistatic const struct usb_device_id products[] = {
35362306a36Sopenharmony_ci	{
35462306a36Sopenharmony_ci		USB_DEVICE_CLASS(0x0572, 0xcb01, 0xff, 0, 0),
35562306a36Sopenharmony_ci		.driver_info = (unsigned long) &cx82310_info
35662306a36Sopenharmony_ci	},
35762306a36Sopenharmony_ci	{ },
35862306a36Sopenharmony_ci};
35962306a36Sopenharmony_ciMODULE_DEVICE_TABLE(usb, products);
36062306a36Sopenharmony_ci
36162306a36Sopenharmony_cistatic struct usb_driver cx82310_driver = {
36262306a36Sopenharmony_ci	.name		= "cx82310_eth",
36362306a36Sopenharmony_ci	.id_table	= products,
36462306a36Sopenharmony_ci	.probe		= usbnet_probe,
36562306a36Sopenharmony_ci	.disconnect	= usbnet_disconnect,
36662306a36Sopenharmony_ci	.suspend	= usbnet_suspend,
36762306a36Sopenharmony_ci	.resume		= usbnet_resume,
36862306a36Sopenharmony_ci	.disable_hub_initiated_lpm = 1,
36962306a36Sopenharmony_ci};
37062306a36Sopenharmony_ci
37162306a36Sopenharmony_cimodule_usb_driver(cx82310_driver);
37262306a36Sopenharmony_ci
37362306a36Sopenharmony_ciMODULE_AUTHOR("Ondrej Zary");
37462306a36Sopenharmony_ciMODULE_DESCRIPTION("Conexant CX82310-based ADSL router USB ethernet driver");
37562306a36Sopenharmony_ciMODULE_LICENSE("GPL");
376