mdm6600: Use pre-allocated, dma-coherent bulk buffer pool.
authorNick Pelly <npelly@google.com>
Tue, 10 Aug 2010 21:46:06 +0000 (14:46 -0700)
committerColin Cross <ccross@android.com>
Wed, 6 Oct 2010 23:33:35 +0000 (16:33 -0700)
The generic bulk handlers use dynamic buffer allocation. This is likely
to be a performance issue.

Change-Id: I9a66131d500b6152d9af118d739f3a2f3dea97c9
Signed-off-by: Nick Pelly <npelly@google.com>
drivers/usb/serial/mdm6600.c

index c20a4137716e3ffb4e3b4c96fc53083770644fd2..6b03d86f4d044160b6079071d45e6e4e775c14a9 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2010 Google, Inc.
- * Re-write of Motorola's mdm6600_modem driver
+ * Author: Nick Pelly <npelly@google.com>
+ * Based on Motorola's mdm6600_modem driver
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2 as
  */
 
 /*
- * TODO check suspend/resume/LP0/LP1
- * TODO remove dummy tiocmget handler
+ * TODO check if we need to implement throttling
+ * TODO handle suspend/resume/LP0/LP1
  */
 
+#include <linux/gfp.h>
 #include <linux/init.h>
 #include <linux/kernel.h>
 #include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/slab.h>
 #include <linux/tty.h>
+#include <linux/tty_flip.h>
 #include <linux/usb.h>
 #include <linux/usb/serial.h>
+#include <linux/workqueue.h>
+
+static bool debug = false;
+static bool debug_data = false;
+
+#define BP_MODEM_STATUS 0x20a1
+#define BP_RSP_AVAIL 0x01a1
+#define BP_SPEED_CHANGE 0x2aa1
+
+#define BP_STATUS_CAR 0x01
+#define BP_STATUS_DSR 0x02
+#define BP_STATUS_BREAK 0x04
+#define BP_STATUS_RNG 0x08
+
+#define POOL_SZ 16
 
 #define MODEM_INTERFACE_NUM 4
 
@@ -37,6 +57,338 @@ static const struct usb_device_id mdm6600_id_table[] = {
 };
 MODULE_DEVICE_TABLE(usb, mdm6600_id_table);
 
+struct mdm6600_urb_write_pool {
+       spinlock_t busy_lock;  /* protects busy flags */
+       bool busy[POOL_SZ];
+       struct urb *urb[POOL_SZ];
+       struct usb_anchor in_flight;
+       int buffer_sz;  /* allocated urb buffer size */
+};
+
+struct mdm6600_urb_read_pool {
+       struct urb *urb[POOL_SZ];
+       struct usb_anchor in_flight;  /* urb's owned by USB core */
+       struct work_struct work;  /* bottom half */
+       struct usb_anchor pending;  /* urb's waiting for driver bottom half */
+       int buffer_sz;  /* allocated urb buffer size */
+};
+
+struct mdm6600_port {
+       struct usb_serial *serial;
+       struct usb_serial_port *port;
+
+       struct mdm6600_urb_write_pool write;
+       struct mdm6600_urb_read_pool read;
+
+       u16 tiocm_status;
+};
+
+static void mdm6600_read_bulk_work(struct work_struct *work);
+static void mdm6600_read_bulk_cb(struct urb *urb);
+static void mdm6600_write_bulk_cb(struct urb *urb);
+
+/* called after probe for each of 5 usb_serial interfaces */
+static int mdm6600_attach(struct usb_serial *serial)
+{
+       int i;
+       struct mdm6600_port *modem;
+       struct usb_host_interface *host_iface =
+               serial->interface->cur_altsetting;
+       struct usb_endpoint_descriptor *epwrite = NULL;
+       struct usb_endpoint_descriptor *epread = NULL;
+
+       modem = kzalloc(sizeof(*modem), GFP_KERNEL);
+       if (!modem)
+               return -ENOMEM;
+       usb_set_serial_data(serial, modem);
+
+       modem->serial = serial;
+       modem->port = modem->serial->port[0]; /* always 1 port per usb_serial */
+       modem->tiocm_status = 0;
+
+       /* find endpoints */
+       for (i = 0; i < host_iface->desc.bNumEndpoints; i++) {
+               struct usb_endpoint_descriptor *ep =
+                       &host_iface->endpoint[i].desc;
+               if (usb_endpoint_is_bulk_out(ep))
+                       epwrite = ep;
+               if (usb_endpoint_is_bulk_in(ep))
+                       epread = ep;
+       }
+       if (!epwrite) {
+               dev_err(&serial->dev->dev, "%s No bulk out endpoint\n",
+                       __func__);
+               return -EIO;
+       }
+       if (!epread) {
+               dev_err(&serial->dev->dev, "%s No bulk in endpoint\n",
+                       __func__);
+               return -EIO;
+       }
+
+       /* setup write pool */
+       spin_lock_init(&modem->write.busy_lock);
+       init_usb_anchor(&modem->write.in_flight);
+       /* The * 20 calculation is from Motorola's original driver, I do not
+        * know the reasoning */
+       modem->write.buffer_sz = le16_to_cpu(epwrite->wMaxPacketSize) * 20;
+       for (i = 0; i < POOL_SZ; i++) {
+               struct urb *u = usb_alloc_urb(0, GFP_KERNEL);
+               if (!u)
+                       return -ENOMEM;
+               u->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+               u->transfer_buffer = usb_alloc_coherent(serial->dev,
+                       modem->write.buffer_sz, GFP_KERNEL, &u->transfer_dma);
+               if (!u->transfer_buffer)
+                       return -ENOMEM;
+               usb_fill_bulk_urb(u, serial->dev,
+                       usb_sndbulkpipe(serial->dev, epwrite->bEndpointAddress),
+                       u->transfer_buffer, modem->write.buffer_sz,
+                       mdm6600_write_bulk_cb, modem);
+               modem->write.urb[i] = u;
+               modem->write.busy[i] = false;
+       }
+
+       /* read pool */
+       INIT_WORK(&modem->read.work, mdm6600_read_bulk_work);
+       init_usb_anchor(&modem->read.in_flight);
+       init_usb_anchor(&modem->read.pending);
+       /* The * 2 calculation is from Motorola's original driver, I do not
+        * know the reasoning */
+       modem->read.buffer_sz = le16_to_cpu(epwrite->wMaxPacketSize) * 2;
+       for (i = 0; i < POOL_SZ; i++) {
+               struct urb *u = usb_alloc_urb(0, GFP_KERNEL);
+               if (!u)
+                       return -ENOMEM;
+               u->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+               u->transfer_buffer = usb_alloc_coherent(serial->dev,
+                       modem->read.buffer_sz, GFP_KERNEL, &u->transfer_dma);
+               if (!u->transfer_buffer)
+                       return -ENOMEM;
+               usb_fill_bulk_urb(u, serial->dev,
+                       usb_rcvbulkpipe(serial->dev, epread->bEndpointAddress),
+                       u->transfer_buffer, modem->read.buffer_sz,
+                       mdm6600_read_bulk_cb, modem);
+               modem->read.urb[i] = u;
+       }
+
+       return 0;
+}
+
+static void mdm6600_disconnect(struct usb_serial *serial)
+{
+       struct mdm6600_port *modem = usb_get_serial_data(serial);
+
+       dbg("%s: port %d", __func__, modem->port->number);
+
+       /* cancel pending writes */
+       usb_kill_anchored_urbs(&modem->write.in_flight);
+
+       /* stop reading from mdm6600 */
+       usb_kill_anchored_urbs(&modem->read.in_flight);
+       usb_kill_urb(modem->port->interrupt_in_urb);
+
+       /* cancel read bottom half */
+       cancel_work_sync(&modem->read.work);
+
+       /* drop pending reads */
+       usb_scuttle_anchored_urbs(&modem->read.pending);
+
+       modem->tiocm_status = 0;
+}
+
+static void mdm6600_release_urb(struct urb *u, int sz)
+{
+       usb_free_coherent(u->dev, sz, u->transfer_buffer, u->transfer_dma);
+       u->transfer_buffer = NULL;
+       usb_free_urb(u);
+}
+
+static void mdm6600_release(struct usb_serial *serial)
+{
+       struct mdm6600_port *modem = usb_get_serial_data(serial);
+       int i;
+
+       for (i = 0; i < POOL_SZ; i++) {
+               mdm6600_release_urb(modem->write.urb[i],
+                       modem->write.buffer_sz);
+               modem->write.urb[i] = NULL;
+               mdm6600_release_urb(modem->read.urb[i], modem->read.buffer_sz);
+               modem->read.urb[i] = NULL;
+       }
+}
+
+/* called when tty is opened */
+static int mdm6600_open(struct tty_struct *tty, struct usb_serial_port *port)
+{
+       int i;
+       int rc = 0;
+       struct mdm6600_port *modem = usb_get_serial_data(port->serial);
+
+       dbg("%s: port %d", __func__, port->number);
+
+       BUG_ON(modem->port != port);
+
+       modem->tiocm_status = 0;
+
+       if (port->number == MODEM_INTERFACE_NUM) {
+               BUG_ON(!port->interrupt_in_urb);
+               rc = usb_submit_urb(port->interrupt_in_urb, GFP_KERNEL);
+               if (rc) {
+                       dev_err(&port->dev,
+                           "%s: failed to submit interrupt urb, error %d\n",
+                           __func__, rc);
+                       return rc;
+               }
+       }
+       for (i = 0; i < POOL_SZ; i++) {
+               rc = usb_submit_urb(modem->read.urb[i], GFP_KERNEL);
+               if (rc) {
+                       dev_err(&port->dev,
+                           "%s: failed to submit bulk read urb, error %d\n",
+                           __func__, rc);
+                       return rc;
+               }
+       }
+
+       return rc;
+}
+
+static void mdm6600_close(struct usb_serial_port *port)
+{
+       struct mdm6600_port *modem = usb_get_serial_data(port->serial);
+
+       dbg("%s: port %d", __func__, port->number);
+
+       /* cancel pending writes */
+       usb_kill_anchored_urbs(&modem->write.in_flight);
+
+       /* stop reading from mdm6600 */
+       usb_kill_anchored_urbs(&modem->read.in_flight);
+       usb_kill_urb(port->interrupt_in_urb);
+
+       /* cancel read bottom half */
+       cancel_work_sync(&modem->read.work);
+
+       /* drop pending reads */
+       usb_scuttle_anchored_urbs(&modem->read.pending);
+
+       modem->tiocm_status = 0;
+}
+
+static struct urb *mdm6600_get_unused_write_urb(
+       struct mdm6600_urb_write_pool *p)
+{
+       int i;
+       unsigned long flags;
+       struct urb *u = NULL;
+
+       spin_lock_irqsave(&p->busy_lock, flags);
+
+       for (i = 0; i < POOL_SZ; i++)
+               if (!p->busy[i])
+                       break;
+       if (i >= POOL_SZ)
+               goto out;
+
+       u = p->urb[i];
+       p->busy[i] = true;
+
+out:
+       spin_unlock_irqrestore(&p->busy_lock, flags);
+       return u;
+}
+
+static int mdm6600_mark_write_urb_unused(struct mdm6600_urb_write_pool *p,
+       struct urb *u)
+{
+       int i;
+       unsigned long flags;
+       int rc = -EINVAL;
+
+       spin_lock_irqsave(&p->busy_lock, flags);
+
+       for (i = 0; i < POOL_SZ; i++)
+               if (p->urb[i] == u)
+                       break;
+       if (i >= POOL_SZ)
+               goto out;
+
+       p->busy[i] = false;
+       rc = 0;
+
+out:
+       spin_unlock_irqrestore(&p->busy_lock, flags);
+       return rc;
+}
+
+static void mdm6600_write_bulk_cb(struct urb *u)
+{
+       int status;
+       struct mdm6600_port *modem = u->context;
+
+       dbg("%s: urb %p status %d", __func__, u, u->status);
+
+       status = u->status;
+       if (status)
+               dev_warn(&modem->serial->dev->dev, "%s non-zero status %d\n",
+                       __func__, u->status);
+
+       if (mdm6600_mark_write_urb_unused(&modem->write, u))
+               dev_warn(&modem->serial->dev->dev, "%s unknown urb %p\n",
+                       __func__, u);
+
+       if (!status)
+               usb_serial_port_softint(modem->port);
+}
+
+static int mdm6600_write(struct tty_struct *tty, struct usb_serial_port *port,
+                       const unsigned char *buf, int count)
+{
+       int rc;
+       struct urb *u;
+       struct usb_serial *serial = port->serial;
+       struct mdm6600_port *modem = usb_get_serial_data(port->serial);
+
+       dbg("%s: port %d count %d pool %p", __func__, port->number, count,
+               &modem->write);
+
+       if (!count || !serial->num_bulk_out)
+               return 0;
+
+       u = mdm6600_get_unused_write_urb(&modem->write);
+       if (!u) {
+               dev_info(&port->dev, "%s: all buffers busy!\n", __func__);
+               return 0;
+       }
+
+       count = min(count, modem->write.buffer_sz);
+       memcpy(u->transfer_buffer, buf, count);
+       u->transfer_buffer_length = count;
+       usb_serial_debug_data(debug_data, &port->dev, __func__,
+               u->transfer_buffer_length, u->transfer_buffer);
+
+       rc = usb_submit_urb(u, GFP_ATOMIC);
+       if (rc < 0) {
+               dev_err(&port->dev, "%s: submit bulk urb failed %d\n",
+                       __func__, rc);
+               return rc;
+       }
+
+       return count;
+}
+
+static int mdm6600_tiocmget(struct tty_struct *tty, struct file *file)
+{
+       struct usb_serial_port *port = tty->driver_data;
+       struct mdm6600_port *modem = usb_get_serial_data(port->serial);
+
+       dbg("%s: port %d modem_status %x\n", __func__, port->number,
+               modem->tiocm_status);
+
+       return modem->tiocm_status;
+}
+
 static int mdm6600_dtr_control(struct usb_serial_port *port, int ctrl)
 {
        struct usb_device *dev = port->serial->dev;
@@ -61,17 +413,14 @@ static int mdm6600_dtr_control(struct usb_serial_port *port, int ctrl)
        return rc;
 }
 
-static int mdm6600_tiocmget(struct tty_struct *tty, struct file *file)
-{
-       /* testing if this ever really gets called */
-       BUG_ON(1);
-}
-
 static int mdm6600_tiocmset(struct tty_struct *tty, struct file *file,
-                                       unsigned int set, unsigned int clear)
+                       unsigned int set, unsigned int clear)
 {
        struct usb_serial_port *port = tty->driver_data;
 
+       dbg("%s: port %d set %x clear %x\n", __func__, port->number, set,
+               clear);
+
        if (port->number != MODEM_INTERFACE_NUM)
                return 0;
        if (clear & TIOCM_DTR)
@@ -81,6 +430,176 @@ static int mdm6600_tiocmset(struct tty_struct *tty, struct file *file,
        return 0;
 }
 
+static void mdm6600_apply_bp_status(u8 bp_status, u16 *tiocm_status)
+{
+       if (bp_status & BP_STATUS_CAR)
+               *tiocm_status |= TIOCM_CAR;
+       else
+               *tiocm_status &= ~TIOCM_CAR;
+       if (bp_status & BP_STATUS_DSR)
+               *tiocm_status |= TIOCM_DSR;
+       else
+               *tiocm_status &= ~TIOCM_DSR;
+       if (bp_status & BP_STATUS_RNG)
+               *tiocm_status |= TIOCM_RNG;
+       else
+               *tiocm_status &= ~TIOCM_RNG;
+}
+
+static void mdm6600_read_int_callback(struct urb *u)
+{
+       int rc;
+       u16 request;
+       u8 *data = u->transfer_buffer;
+       struct usb_serial_port *port = u->context;
+       struct mdm6600_port *modem = usb_get_serial_data(port->serial);
+
+       dbg("%s: urb %p", __func__, u);
+
+       switch (u->status) {
+       case 0:
+               /* success */
+               break;
+       case -ECONNRESET:
+       case -ENOENT:
+       case -ESHUTDOWN:
+               dbg("%s: urb terminated, status %d", __func__, u->status);
+               goto exit;
+       default:
+               dbg("%s: urb status non-zero %d", __func__, u->status);
+               goto exit;
+       }
+
+       usb_serial_debug_data(debug_data, &port->dev, __func__,
+               u->actual_length, data);
+
+       if (u->actual_length < 2) {
+               dbg("%s: interrupt transfer too small %d",
+                       __func__, u->actual_length);
+               goto exit;
+       }
+       request = *((u16 *)data);
+
+       switch (request) {
+       case BP_MODEM_STATUS:
+               if (u->actual_length < 9) {
+                       dev_err(&port->dev,
+                               "%s: modem status urb too small %d\n",
+                               __func__, u->actual_length);
+                       break;
+               }
+               if (port->number != MODEM_INTERFACE_NUM)
+                       break;
+               mdm6600_apply_bp_status(data[8], &modem->tiocm_status);
+               dbg("%s: modem_status now %x", __func__, modem->tiocm_status);
+               break;
+       case BP_RSP_AVAIL:
+               dbg("%s: BP_RSP_AVAIL", __func__);
+               break;
+       case BP_SPEED_CHANGE:
+               dbg("%s: BP_SPEED_CHANGE", __func__);
+               break;
+       default:
+               dbg("%s: undefined BP request type %d", __func__, request);
+               break;
+       }
+
+exit:
+       rc = usb_submit_urb(u, GFP_ATOMIC);
+       if (rc)
+               dev_err(&u->dev->dev,
+                       "%s: Error %d re-submitting interrupt urb\n",
+                       __func__, rc);
+}
+
+static size_t mdm6600_pass_to_tty(struct tty_struct *tty, void *buf, size_t sz)
+{
+       unsigned char *b = buf;
+       size_t c;
+       size_t s = sz;
+
+       tty_buffer_request_room(tty, sz);
+       while (s > 0) {
+               c = tty_insert_flip_string(tty, b, s);
+               if (c != s)
+                       dbg("%s passed only %u of %u bytes\n",
+                               __func__, c, s);
+               if (c == 0)
+                       break;
+               tty_flip_buffer_push(tty);
+               s -= c;
+               b += c;
+       }
+
+       return sz - s;
+}
+
+static void mdm6600_read_bulk_work(struct work_struct *work)
+{
+       int rc;
+       size_t c;
+       struct urb *u;
+       struct tty_struct *tty;
+       struct mdm6600_port *modem = container_of(work, struct mdm6600_port,
+               read.work);
+
+       dbg("%s", __func__);
+
+       while (true) {
+               u = usb_get_from_anchor(&modem->read.pending);
+               if (!u)
+                       break;
+               dbg("%s: processing urb %p len %u", __func__, u,
+                       u->actual_length);
+               usb_serial_debug_data(debug_data, &modem->port->dev, __func__,
+                       u->actual_length, u->transfer_buffer);
+               tty = tty_port_tty_get(&modem->port->port);
+               if (!tty) {
+                       dev_warn(&modem->port->dev, "%s: could not find tty\n",
+                               __func__);
+                       goto next;
+               }
+               c = mdm6600_pass_to_tty(tty, u->transfer_buffer,
+                       u->actual_length);
+               if (c != u->actual_length)
+                       dev_warn(&modem->port->dev,
+                               "%s: dropped %u of %u bytes\n",
+                               __func__, u->actual_length - c,
+                               u->actual_length);
+               tty_kref_put(tty);
+
+next:
+               rc = usb_submit_urb(u, GFP_KERNEL);
+               if (rc)
+                       dev_err(&u->dev->dev,
+                               "%s: Error %d re-submitting read urb\n",
+                               __func__, rc);
+       }
+}
+
+static void mdm6600_read_bulk_cb(struct urb *u)
+{
+       struct mdm6600_port *modem = u->context;
+
+       dbg("%s: urb %p", __func__, u);
+
+       if (u->status) {
+               int rc;
+               dev_warn(&modem->serial->dev->dev, "%s non-zero status %d\n",
+                       __func__, u->status);
+               /* straight back into use */
+               rc = usb_submit_urb(u, GFP_ATOMIC);
+               if (rc)
+                       dev_err(&u->dev->dev,
+                               "%s: Error %d re-submitting read urb\n",
+                               __func__, rc);
+               return;
+       }
+
+       usb_anchor_urb(u, &modem->read.pending);
+       schedule_work(&modem->read.work);
+}
+
 static struct usb_driver mdm6600_usb_driver = {
        .name =         "mdm6600",
        .probe =        usb_serial_probe,
@@ -94,12 +613,19 @@ static struct usb_serial_driver mdm6600_usb_serial_driver = {
                .owner =        THIS_MODULE,
                .name =         "mdm6600",
        },
+       .num_ports =            1,
        .description =          "MDM 6600 modem usb-serial driver",
        .id_table =             mdm6600_id_table,
-       .num_ports =            1,  /* TODO: confirm this number is useless */
        .usb_driver =           &mdm6600_usb_driver,
+       .attach =               mdm6600_attach,
+       .disconnect =           mdm6600_disconnect,
+       .release =              mdm6600_release,
+       .open =                 mdm6600_open,
+       .close =                mdm6600_close,
+       .write =                mdm6600_write,
        .tiocmset =             mdm6600_tiocmset,
        .tiocmget =             mdm6600_tiocmget,
+       .read_int_callback =    mdm6600_read_int_callback,
 };
 
 static int __init mdm6600_init(void)
@@ -124,3 +650,8 @@ static void __exit mdm6600_exit(void)
 module_init(mdm6600_init);
 module_exit(mdm6600_exit);
 MODULE_LICENSE("GPL");
+
+module_param(debug, bool, S_IRUGO | S_IWUSR);
+MODULE_PARM_DESC(debug, "Debug enabled or not");
+module_param(debug_data, bool, S_IRUGO | S_IWUSR);
+MODULE_PARM_DESC(debug_data, "Debug enabled or not");