summaryrefslogtreecommitdiff
path: root/drivers/usb/class/cdc-wdm.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/usb/class/cdc-wdm.c')
-rw-r--r--drivers/usb/class/cdc-wdm.c352
1 files changed, 227 insertions, 125 deletions
diff --git a/drivers/usb/class/cdc-wdm.c b/drivers/usb/class/cdc-wdm.c
index d2b3cffca3f7..c6f6560d436c 100644
--- a/drivers/usb/class/cdc-wdm.c
+++ b/drivers/usb/class/cdc-wdm.c
@@ -23,6 +23,7 @@
#include <linux/usb/cdc.h>
#include <asm/byteorder.h>
#include <asm/unaligned.h>
+#include <linux/usb/cdc-wdm.h>
/*
* Version Information
@@ -31,6 +32,8 @@
#define DRIVER_AUTHOR "Oliver Neukum"
#define DRIVER_DESC "USB Abstract Control Model driver for USB WCM Device Management"
+#define HUAWEI_VENDOR_ID 0x12D1
+
static const struct usb_device_id wdm_ids[] = {
{
.match_flags = USB_DEVICE_ID_MATCH_INT_CLASS |
@@ -38,6 +41,20 @@ static const struct usb_device_id wdm_ids[] = {
.bInterfaceClass = USB_CLASS_COMM,
.bInterfaceSubClass = USB_CDC_SUBCLASS_DMM
},
+ {
+ /*
+ * Huawei E392, E398 and possibly other Qualcomm based modems
+ * embed the Qualcomm QMI protocol inside CDC on CDC ECM like
+ * control interfaces. Userspace access to this is required
+ * to configure the accompanying data interface
+ */
+ .match_flags = USB_DEVICE_ID_MATCH_VENDOR |
+ USB_DEVICE_ID_MATCH_INT_INFO,
+ .idVendor = HUAWEI_VENDOR_ID,
+ .bInterfaceClass = USB_CLASS_VENDOR_SPEC,
+ .bInterfaceSubClass = 1,
+ .bInterfaceProtocol = 9, /* NOTE: CDC ECM control interface! */
+ },
{ }
};
@@ -54,6 +71,7 @@ MODULE_DEVICE_TABLE (usb, wdm_ids);
#define WDM_POLL_RUNNING 6
#define WDM_RESPONDING 7
#define WDM_SUSPENDING 8
+#define WDM_RESETTING 9
#define WDM_MAX 16
@@ -61,6 +79,8 @@ MODULE_DEVICE_TABLE (usb, wdm_ids);
#define WDM_DEFAULT_BUFSIZE 256
static DEFINE_MUTEX(wdm_mutex);
+static DEFINE_SPINLOCK(wdm_device_list_lock);
+static LIST_HEAD(wdm_device_list);
/* --- method tables --- */
@@ -82,7 +102,6 @@ struct wdm_device {
u16 bufsize;
u16 wMaxCommand;
u16 wMaxPacketSize;
- u16 bMaxPacketSize0;
__le16 inum;
int reslength;
int length;
@@ -96,10 +115,40 @@ struct wdm_device {
struct work_struct rxwork;
int werr;
int rerr;
+
+ struct list_head device_list;
+ int (*manage_power)(struct usb_interface *, int);
};
static struct usb_driver wdm_driver;
+/* return intfdata if we own the interface, else look up intf in the list */
+static struct wdm_device *wdm_find_device(struct usb_interface *intf)
+{
+ struct wdm_device *desc = NULL;
+
+ spin_lock(&wdm_device_list_lock);
+ list_for_each_entry(desc, &wdm_device_list, device_list)
+ if (desc->intf == intf)
+ break;
+ spin_unlock(&wdm_device_list_lock);
+
+ return desc;
+}
+
+static struct wdm_device *wdm_find_device_by_minor(int minor)
+{
+ struct wdm_device *desc = NULL;
+
+ spin_lock(&wdm_device_list_lock);
+ list_for_each_entry(desc, &wdm_device_list, device_list)
+ if (desc->intf->minor == minor)
+ break;
+ spin_unlock(&wdm_device_list_lock);
+
+ return desc;
+}
+
/* --- callbacks --- */
static void wdm_out_callback(struct urb *urb)
{
@@ -162,11 +211,9 @@ static void wdm_int_callback(struct urb *urb)
int rv = 0;
int status = urb->status;
struct wdm_device *desc;
- struct usb_ctrlrequest *req;
struct usb_cdc_notification *dr;
desc = urb->context;
- req = desc->irq;
dr = (struct usb_cdc_notification *)desc->sbuf;
if (status) {
@@ -213,24 +260,6 @@ static void wdm_int_callback(struct urb *urb)
goto exit;
}
- req->bRequestType = (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE);
- req->bRequest = USB_CDC_GET_ENCAPSULATED_RESPONSE;
- req->wValue = 0;
- req->wIndex = desc->inum;
- req->wLength = cpu_to_le16(desc->wMaxCommand);
-
- usb_fill_control_urb(
- desc->response,
- interface_to_usbdev(desc->intf),
- /* using common endpoint 0 */
- usb_rcvctrlpipe(interface_to_usbdev(desc->intf), 0),
- (unsigned char *)req,
- desc->inbuf,
- desc->wMaxCommand,
- wdm_in_callback,
- desc
- );
- desc->response->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
spin_lock(&desc->iuspin);
clear_bit(WDM_READ, &desc->flags);
set_bit(WDM_RESPONDING, &desc->flags);
@@ -279,14 +308,11 @@ static void free_urbs(struct wdm_device *desc)
static void cleanup(struct wdm_device *desc)
{
- usb_free_coherent(interface_to_usbdev(desc->intf),
- desc->wMaxPacketSize,
- desc->sbuf,
- desc->validity->transfer_dma);
- usb_free_coherent(interface_to_usbdev(desc->intf),
- desc->bMaxPacketSize0,
- desc->inbuf,
- desc->response->transfer_dma);
+ spin_lock(&wdm_device_list_lock);
+ list_del(&desc->device_list);
+ spin_unlock(&wdm_device_list_lock);
+ kfree(desc->sbuf);
+ kfree(desc->inbuf);
kfree(desc->orq);
kfree(desc->irq);
kfree(desc->ubuf);
@@ -351,6 +377,10 @@ static ssize_t wdm_write
else
if (test_bit(WDM_IN_USE, &desc->flags))
r = -EAGAIN;
+
+ if (test_bit(WDM_RESETTING, &desc->flags))
+ r = -EIO;
+
if (r < 0) {
kfree(buf);
goto out;
@@ -397,7 +427,7 @@ outnl:
static ssize_t wdm_read
(struct file *file, char __user *buffer, size_t count, loff_t *ppos)
{
- int rv, cntr = 0;
+ int rv, cntr;
int i = 0;
struct wdm_device *desc = file->private_data;
@@ -406,7 +436,8 @@ static ssize_t wdm_read
if (rv < 0)
return -ERESTARTSYS;
- if (desc->length == 0) {
+ cntr = ACCESS_ONCE(desc->length);
+ if (cntr == 0) {
desc->read = 0;
retry:
if (test_bit(WDM_DISCONNECTING, &desc->flags)) {
@@ -430,6 +461,10 @@ retry:
rv = -ENODEV;
goto err;
}
+ if (test_bit(WDM_RESETTING, &desc->flags)) {
+ rv = -EIO;
+ goto err;
+ }
usb_mark_last_busy(interface_to_usbdev(desc->intf));
if (rv < 0) {
rv = -ERESTARTSYS;
@@ -456,26 +491,30 @@ retry:
spin_unlock_irq(&desc->iuspin);
goto retry;
}
- clear_bit(WDM_READ, &desc->flags);
+ cntr = desc->length;
spin_unlock_irq(&desc->iuspin);
}
- cntr = count > desc->length ? desc->length : count;
+ if (cntr > count)
+ cntr = count;
rv = copy_to_user(buffer, desc->ubuf, cntr);
if (rv > 0) {
rv = -EFAULT;
goto err;
}
+ spin_lock_irq(&desc->iuspin);
+
for (i = 0; i < desc->length - cntr; i++)
desc->ubuf[i] = desc->ubuf[i + cntr];
- spin_lock_irq(&desc->iuspin);
desc->length -= cntr;
- spin_unlock_irq(&desc->iuspin);
/* in case we had outstanding data */
if (!desc->length)
clear_bit(WDM_READ, &desc->flags);
+
+ spin_unlock_irq(&desc->iuspin);
+
rv = cntr;
err:
@@ -529,11 +568,11 @@ static int wdm_open(struct inode *inode, struct file *file)
struct wdm_device *desc;
mutex_lock(&wdm_mutex);
- intf = usb_find_interface(&wdm_driver, minor);
- if (!intf)
+ desc = wdm_find_device_by_minor(minor);
+ if (!desc)
goto out;
- desc = usb_get_intfdata(intf);
+ intf = desc->intf;
if (test_bit(WDM_DISCONNECTING, &desc->flags))
goto out;
file->private_data = desc;
@@ -543,7 +582,6 @@ static int wdm_open(struct inode *inode, struct file *file)
dev_err(&desc->intf->dev, "Error autopm - %d\n", rv);
goto out;
}
- intf->needs_remote_wakeup = 1;
/* using write lock to protect desc->count */
mutex_lock(&desc->wlock);
@@ -560,6 +598,8 @@ static int wdm_open(struct inode *inode, struct file *file)
rv = 0;
}
mutex_unlock(&desc->wlock);
+ if (desc->count == 1)
+ desc->manage_power(intf, 1);
usb_autopm_put_interface(desc->intf);
out:
mutex_unlock(&wdm_mutex);
@@ -581,7 +621,7 @@ static int wdm_release(struct inode *inode, struct file *file)
dev_dbg(&desc->intf->dev, "wdm_release: cleanup");
kill_urbs(desc);
if (!test_bit(WDM_DISCONNECTING, &desc->flags))
- desc->intf->needs_remote_wakeup = 0;
+ desc->manage_power(desc->intf, 0);
}
mutex_unlock(&wdm_mutex);
return 0;
@@ -628,71 +668,31 @@ static void wdm_rxwork(struct work_struct *work)
/* --- hotplug --- */
-static int wdm_probe(struct usb_interface *intf, const struct usb_device_id *id)
+static int wdm_create(struct usb_interface *intf, struct usb_endpoint_descriptor *ep,
+ u16 bufsize, int (*manage_power)(struct usb_interface *, int))
{
- int rv = -EINVAL;
- struct usb_device *udev = interface_to_usbdev(intf);
+ int rv = -ENOMEM;
struct wdm_device *desc;
- struct usb_host_interface *iface;
- struct usb_endpoint_descriptor *ep;
- struct usb_cdc_dmm_desc *dmhd;
- u8 *buffer = intf->altsetting->extra;
- int buflen = intf->altsetting->extralen;
- u16 maxcom = WDM_DEFAULT_BUFSIZE;
-
- if (!buffer)
- goto out;
- while (buflen > 2) {
- if (buffer [1] != USB_DT_CS_INTERFACE) {
- dev_err(&intf->dev, "skipping garbage\n");
- goto next_desc;
- }
-
- switch (buffer [2]) {
- case USB_CDC_HEADER_TYPE:
- break;
- case USB_CDC_DMM_TYPE:
- dmhd = (struct usb_cdc_dmm_desc *)buffer;
- maxcom = le16_to_cpu(dmhd->wMaxCommand);
- dev_dbg(&intf->dev,
- "Finding maximum buffer length: %d", maxcom);
- break;
- default:
- dev_err(&intf->dev,
- "Ignoring extra header, type %d, length %d\n",
- buffer[2], buffer[0]);
- break;
- }
-next_desc:
- buflen -= buffer[0];
- buffer += buffer[0];
- }
-
- rv = -ENOMEM;
desc = kzalloc(sizeof(struct wdm_device), GFP_KERNEL);
if (!desc)
goto out;
+ INIT_LIST_HEAD(&desc->device_list);
mutex_init(&desc->rlock);
mutex_init(&desc->wlock);
spin_lock_init(&desc->iuspin);
init_waitqueue_head(&desc->wait);
- desc->wMaxCommand = maxcom;
+ desc->wMaxCommand = bufsize;
/* this will be expanded and needed in hardware endianness */
desc->inum = cpu_to_le16((u16)intf->cur_altsetting->desc.bInterfaceNumber);
desc->intf = intf;
INIT_WORK(&desc->rxwork, wdm_rxwork);
rv = -EINVAL;
- iface = intf->cur_altsetting;
- if (iface->desc.bNumEndpoints != 1)
- goto err;
- ep = &iface->endpoint[0].desc;
- if (!ep || !usb_endpoint_is_int_in(ep))
+ if (!usb_endpoint_is_int_in(ep))
goto err;
desc->wMaxPacketSize = usb_endpoint_maxp(ep);
- desc->bMaxPacketSize0 = udev->descriptor.bMaxPacketSize0;
desc->orq = kmalloc(sizeof(struct usb_ctrlrequest), GFP_KERNEL);
if (!desc->orq)
@@ -717,19 +717,13 @@ next_desc:
if (!desc->ubuf)
goto err;
- desc->sbuf = usb_alloc_coherent(interface_to_usbdev(intf),
- desc->wMaxPacketSize,
- GFP_KERNEL,
- &desc->validity->transfer_dma);
+ desc->sbuf = kmalloc(desc->wMaxPacketSize, GFP_KERNEL);
if (!desc->sbuf)
goto err;
- desc->inbuf = usb_alloc_coherent(interface_to_usbdev(intf),
- desc->wMaxCommand,
- GFP_KERNEL,
- &desc->response->transfer_dma);
+ desc->inbuf = kmalloc(desc->wMaxCommand, GFP_KERNEL);
if (!desc->inbuf)
- goto err2;
+ goto err;
usb_fill_int_urb(
desc->validity,
@@ -741,45 +735,149 @@ next_desc:
desc,
ep->bInterval
);
- desc->validity->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
- usb_set_intfdata(intf, desc);
+ desc->irq->bRequestType = (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE);
+ desc->irq->bRequest = USB_CDC_GET_ENCAPSULATED_RESPONSE;
+ desc->irq->wValue = 0;
+ desc->irq->wIndex = desc->inum;
+ desc->irq->wLength = cpu_to_le16(desc->wMaxCommand);
+
+ usb_fill_control_urb(
+ desc->response,
+ interface_to_usbdev(intf),
+ /* using common endpoint 0 */
+ usb_rcvctrlpipe(interface_to_usbdev(desc->intf), 0),
+ (unsigned char *)desc->irq,
+ desc->inbuf,
+ desc->wMaxCommand,
+ wdm_in_callback,
+ desc
+ );
+
+ desc->manage_power = manage_power;
+
+ spin_lock(&wdm_device_list_lock);
+ list_add(&desc->device_list, &wdm_device_list);
+ spin_unlock(&wdm_device_list_lock);
+
rv = usb_register_dev(intf, &wdm_class);
if (rv < 0)
- goto err3;
+ goto err;
else
- dev_info(&intf->dev, "cdc-wdm%d: USB WDM device\n",
- intf->minor - WDM_MINOR_BASE);
+ dev_info(&intf->dev, "%s: USB WDM device\n", dev_name(intf->usb_dev));
out:
return rv;
-err3:
- usb_set_intfdata(intf, NULL);
- usb_free_coherent(interface_to_usbdev(desc->intf),
- desc->bMaxPacketSize0,
- desc->inbuf,
- desc->response->transfer_dma);
-err2:
- usb_free_coherent(interface_to_usbdev(desc->intf),
- desc->wMaxPacketSize,
- desc->sbuf,
- desc->validity->transfer_dma);
err:
- free_urbs(desc);
- kfree(desc->ubuf);
- kfree(desc->orq);
- kfree(desc->irq);
- kfree(desc);
+ cleanup(desc);
+ return rv;
+}
+
+static int wdm_manage_power(struct usb_interface *intf, int on)
+{
+ /* need autopm_get/put here to ensure the usbcore sees the new value */
+ int rv = usb_autopm_get_interface(intf);
+ if (rv < 0)
+ goto err;
+
+ intf->needs_remote_wakeup = on;
+ usb_autopm_put_interface(intf);
+err:
+ return rv;
+}
+
+static int wdm_probe(struct usb_interface *intf, const struct usb_device_id *id)
+{
+ int rv = -EINVAL;
+ struct usb_host_interface *iface;
+ struct usb_endpoint_descriptor *ep;
+ struct usb_cdc_dmm_desc *dmhd;
+ u8 *buffer = intf->altsetting->extra;
+ int buflen = intf->altsetting->extralen;
+ u16 maxcom = WDM_DEFAULT_BUFSIZE;
+
+ if (!buffer)
+ goto err;
+ while (buflen > 2) {
+ if (buffer[1] != USB_DT_CS_INTERFACE) {
+ dev_err(&intf->dev, "skipping garbage\n");
+ goto next_desc;
+ }
+
+ switch (buffer[2]) {
+ case USB_CDC_HEADER_TYPE:
+ break;
+ case USB_CDC_DMM_TYPE:
+ dmhd = (struct usb_cdc_dmm_desc *)buffer;
+ maxcom = le16_to_cpu(dmhd->wMaxCommand);
+ dev_dbg(&intf->dev,
+ "Finding maximum buffer length: %d", maxcom);
+ break;
+ default:
+ dev_err(&intf->dev,
+ "Ignoring extra header, type %d, length %d\n",
+ buffer[2], buffer[0]);
+ break;
+ }
+next_desc:
+ buflen -= buffer[0];
+ buffer += buffer[0];
+ }
+
+ iface = intf->cur_altsetting;
+ if (iface->desc.bNumEndpoints != 1)
+ goto err;
+ ep = &iface->endpoint[0].desc;
+
+ rv = wdm_create(intf, ep, maxcom, &wdm_manage_power);
+
+err:
return rv;
}
+/**
+ * usb_cdc_wdm_register - register a WDM subdriver
+ * @intf: usb interface the subdriver will associate with
+ * @ep: interrupt endpoint to monitor for notifications
+ * @bufsize: maximum message size to support for read/write
+ *
+ * Create WDM usb class character device and associate it with intf
+ * without binding, allowing another driver to manage the interface.
+ *
+ * The subdriver will manage the given interrupt endpoint exclusively
+ * and will issue control requests referring to the given intf. It
+ * will otherwise avoid interferring, and in particular not do
+ * usb_set_intfdata/usb_get_intfdata on intf.
+ *
+ * The return value is a pointer to the subdriver's struct usb_driver.
+ * The registering driver is responsible for calling this subdriver's
+ * disconnect, suspend, resume, pre_reset and post_reset methods from
+ * its own.
+ */
+struct usb_driver *usb_cdc_wdm_register(struct usb_interface *intf,
+ struct usb_endpoint_descriptor *ep,
+ int bufsize,
+ int (*manage_power)(struct usb_interface *, int))
+{
+ int rv = -EINVAL;
+
+ rv = wdm_create(intf, ep, bufsize, manage_power);
+ if (rv < 0)
+ goto err;
+
+ return &wdm_driver;
+err:
+ return ERR_PTR(rv);
+}
+EXPORT_SYMBOL(usb_cdc_wdm_register);
+
static void wdm_disconnect(struct usb_interface *intf)
{
struct wdm_device *desc;
unsigned long flags;
usb_deregister_dev(intf, &wdm_class);
+ desc = wdm_find_device(intf);
mutex_lock(&wdm_mutex);
- desc = usb_get_intfdata(intf);
/* the spinlock makes sure no new urbs are generated in the callbacks */
spin_lock_irqsave(&desc->iuspin, flags);
@@ -803,7 +901,7 @@ static void wdm_disconnect(struct usb_interface *intf)
#ifdef CONFIG_PM
static int wdm_suspend(struct usb_interface *intf, pm_message_t message)
{
- struct wdm_device *desc = usb_get_intfdata(intf);
+ struct wdm_device *desc = wdm_find_device(intf);
int rv = 0;
dev_dbg(&desc->intf->dev, "wdm%d_suspend\n", intf->minor);
@@ -853,7 +951,7 @@ static int recover_from_urb_loss(struct wdm_device *desc)
#ifdef CONFIG_PM
static int wdm_resume(struct usb_interface *intf)
{
- struct wdm_device *desc = usb_get_intfdata(intf);
+ struct wdm_device *desc = wdm_find_device(intf);
int rv;
dev_dbg(&desc->intf->dev, "wdm%d_resume\n", intf->minor);
@@ -867,11 +965,7 @@ static int wdm_resume(struct usb_interface *intf)
static int wdm_pre_reset(struct usb_interface *intf)
{
- struct wdm_device *desc = usb_get_intfdata(intf);
-
- mutex_lock(&desc->rlock);
- mutex_lock(&desc->wlock);
- kill_urbs(desc);
+ struct wdm_device *desc = wdm_find_device(intf);
/*
* we notify everybody using poll of
@@ -880,17 +974,25 @@ static int wdm_pre_reset(struct usb_interface *intf)
* message from the device is lost
*/
spin_lock_irq(&desc->iuspin);
+ set_bit(WDM_RESETTING, &desc->flags); /* inform read/write */
+ set_bit(WDM_READ, &desc->flags); /* unblock read */
+ clear_bit(WDM_IN_USE, &desc->flags); /* unblock write */
desc->rerr = -EINTR;
spin_unlock_irq(&desc->iuspin);
wake_up_all(&desc->wait);
+ mutex_lock(&desc->rlock);
+ mutex_lock(&desc->wlock);
+ kill_urbs(desc);
+ cancel_work_sync(&desc->rxwork);
return 0;
}
static int wdm_post_reset(struct usb_interface *intf)
{
- struct wdm_device *desc = usb_get_intfdata(intf);
+ struct wdm_device *desc = wdm_find_device(intf);
int rv;
+ clear_bit(WDM_RESETTING, &desc->flags);
rv = recover_from_urb_loss(desc);
mutex_unlock(&desc->wlock);
mutex_unlock(&desc->rlock);