code review request: simple usb stream interface (susi)

Edwin Olson <[email protected]>
Newsgroups gmane.linux.usb.devel
Message-ID <[email protected]>
I'm writing firmware for a number of USB devices, and it seems that most 
of the time, I simply want a ordered/reliable communications stream 
between host and device: I can build my actual protocol on top of this 
in userspace. I've previously used usb-serial, but the performance 
wasn't satisfactory: I couldn't read more than 64KB/s from the device.

Attached is a fairly simple new driver ("susi" for "simple usb stream 
interface") that provides an ordered/reliable stream interface, 
compatible with usb-serial, except that it dispenses with the tty layer 
and it doesn't care about CDC interfaces. In time, I would like to have 
it instantiate as many /dev/susiXXX devices as there are bulk endpoint 
pairs in a device, and make it easy for other usb gadget creators like 
myself to have their device recognized as a susi device (via a modprobe 
option like usb-serial?). I've adopted an ftdi_sio style read method to 
increase performance. On a simple micro-benchmark in which my device 
echos, I've been able to send and receive 500+KB/s in each direction 
simultaneously (this is USB1.1).

The implementation is heavily derived from usb-skeleton and ftdi_sio, 
though there are some new bits as well. I am a relative new comer to 
linux kernel development, so I would appreciate any 
corrections/suggestions you might have. My goal is to incorporate those 
suggestions and then submit a more polished module for inclusion in the 
kernel.

A few questions:

* Why does usb-serial set MAX_TRANFER to (PAGE_SIZE - 512)? What's the 
512 for?
* Is there a method for querying the value of a semaphore? (see poll())
* Is there a way of dequeuing from a kfifo directly into userspace, 
without an intermediate buffer?
* Is my locking correct?
* I've left existing copyrights intact and added an additional line. Is 
that the right thing to do?
* After an abrupt device disconnect with writes pending, my machine 
seems to "lock up" for about 30 seconds before the writes are aborted. 
How can I address this?

You can also grab the code from my repository, which will get you a 
simple Makefile for building a module outside the kernel tree (chopped 
up from the atl1 driver):

svn co svn://edo.csail.mit.edu/home/svn/susi

This is built and tested against 2.6.19-1.

Thank you!

-Ed

-------------------------------------------------------------------------
Using Tomcat but need to do more? Need to support web services, security?
Get stuff done quickly with pre-integrated technology to make your job easier.
Download IBM WebSphere Application Server v.1.0.1 based on Apache Geronimo
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=120709&bid=263057&dat=121642

_______________________________________________
[email protected]
To unsubscribe, use the last form field at:
https://lists.sourceforge.net/lists/listinfo/linux-usb-devel
susi.c (text/x-csrc, 13.1 KB)
/*
 * Simple USB Stream Interface (SUSI)
 *
 * Copyright (C) 2007 Edwin Olson ([email protected])
 * Copyright (C) 2001-2004 Greg Kroah-Hartman ([email protected])
 *
 *	This program is free software; you can redistribute it and/or
 *	modify it under the terms of the GNU General Public License as
 *	published by the Free Software Foundation, version 2.
 *
 * This driver is based on the 2.6.3 version of drivers/usb/usb-skeleton.c
 * but has been rewritten to be easier to read and use.
 *
 */

#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/kref.h>
#include <linux/kfifo.h>
#include <asm/uaccess.h>
#include <linux/usb.h>
#include <linux/mutex.h>
#include <linux/poll.h>

/* Define these values to match your devices */
#define USB_SUSI_VENDOR_ID	0x00ed
#define USB_SUSI_PRODUCT_ID	0x0001

/* table of devices that work with this driver */
static struct usb_device_id susi_table [] = {
	{ USB_DEVICE(USB_SUSI_VENDOR_ID, USB_SUSI_PRODUCT_ID) },
	{ }					/* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, susi_table);


/* Get a minor range for your devices from the usb maintainer */
#define USB_SUSI_MINOR_BASE	192

/* our private defines. if this grows any larger, use your own .h file */
#define MAX_TRANSFER		(PAGE_SIZE - 512)
#define WRITES_IN_FLIGHT	8

/* Structure to hold all of our device specific stuff */
struct usb_susi {
	struct usb_device       *udev;			/* the usb device for this device */
	struct usb_interface    *interface;		/* the interface for this device */
	struct semaphore	write_sem;		/* limiting the number of writes in progress */
	struct semaphore        read_sem;               /* block readers until we have data */
	unsigned char           *bulk_in_buffer;	/* the buffer to receive data */
	size_t			bulk_in_size;		/* the size of the receive buffer */
	__u8			bulk_in_endpointAddr;	/* the address of the bulk in endpoint */
	__u8			bulk_out_endpointAddr;	/* the address of the bulk out endpoint */
	struct kref		kref;
	struct mutex		io_mutex;		/* synchronize I/O with disconnect */

	struct urb              *read_urb;
	int                     read_pending;           /* is a read URB outstanding? */

	struct kfifo            *read_fifo;
	spinlock_t              spinlock;

	wait_queue_head_t       readq, writeq;          /* read and write queues */
};

#define to_susi_dev(d) container_of(d, struct usb_susi, kref)

static struct usb_driver susi_driver;

static void susi_delete(struct kref *kref)
{
	struct usb_susi *dev = to_susi_dev(kref);

	usb_put_dev(dev->udev);
	kfifo_free(dev->read_fifo);
	kfree(dev->bulk_in_buffer);
	kfree(dev);
}

static int susi_open(struct inode *inode, struct file *file)
{
	struct usb_susi *dev;
	struct usb_interface *interface;
	int subminor;
	int retval = 0;

	subminor = iminor(inode);

	interface = usb_find_interface(&susi_driver, subminor);
	if (!interface) {
		err ("%s - error, can't find device for minor %d",
		     __FUNCTION__, subminor);
		retval = -ENODEV;
		goto exit;
	}

	dev = usb_get_intfdata(interface);
	if (!dev) {
		retval = -ENODEV;
		goto exit;
	}

	/* prevent the device from being autosuspended */
	retval = usb_autopm_get_interface(interface);
	if (retval)
		goto exit;

	/* increment our usage count for the device */
	kref_get(&dev->kref);

	/* save our object in the file's private structure */
	file->private_data = dev;

exit:
	return retval;
}

static int susi_release(struct inode *inode, struct file *file)
{
	struct usb_susi *dev;

	dev = (struct usb_susi *)file->private_data;
	if (dev == NULL)
		return -ENODEV;

	/* allow the device to be autosuspended */
	mutex_lock(&dev->io_mutex);
	if (dev->interface)
		usb_autopm_put_interface(dev->interface);
	mutex_unlock(&dev->io_mutex);

	/* decrement the count on our device */
	kref_put(&dev->kref, susi_delete);
	return 0;
}

static int susi_read_more(struct usb_susi *dev);

static void susi_read_bulk_callback (struct urb *urb)
{
	struct usb_susi *dev;

	dev = (struct usb_susi *)urb->context;

	if (urb != dev->read_urb) {
		err("not our read erb");
	}

	dev->read_pending = 0;

	/* sync/async unlink faults aren't errors */
	if (urb->status &&
	    !(urb->status == -ENOENT ||
	      urb->status == -ECONNRESET ||
	      urb->status == -ESHUTDOWN)) {
		err("%s - nonzero read bulk status received: %d",
		    __FUNCTION__, urb->status);
	}

	kfifo_put(dev->read_fifo, urb->transfer_buffer, urb->actual_length);
	wake_up_interruptible(&dev->readq);

	susi_read_more(dev);
}

static int susi_read_more(struct usb_susi *dev)
{
	int retval;
	int fifo_space = dev->read_fifo->size - kfifo_len(dev->read_fifo);

	if (dev->read_pending)
		return 0;

	if (fifo_space < dev->bulk_in_size)
		return 0;

	dev->read_pending = 1;
	// XXX what is correct requested read length?
	usb_fill_bulk_urb(dev->read_urb, dev->udev,
			  usb_rcvbulkpipe(dev->udev, dev->bulk_in_endpointAddr),
			  dev->bulk_in_buffer, dev->bulk_in_size,
			  susi_read_bulk_callback, dev);

	retval = usb_submit_urb(dev->read_urb, GFP_ATOMIC);
	if (retval)
		err("%s - failed submitting read urb, error %d", __FUNCTION__, retval);

	return retval;
}

#define TBUF_LEN 256

static ssize_t susi_read(struct file *file, char *buffer, size_t count, loff_t *ppos)
{
	struct usb_susi *dev;
	char tbuf[TBUF_LEN];
	int retval;
	int xfersize;

	dev = (struct usb_susi *)file->private_data;

	mutex_lock(&dev->io_mutex);

	if (!dev->interface) {		/* disconnect() was called */
		retval = -ENODEV;
		goto exit;
	}

	while (kfifo_len(dev->read_fifo) == 0) {
		mutex_unlock(&dev->io_mutex);
		if (file->f_flags & O_NONBLOCK)
			return -EAGAIN;
		if (wait_event_interruptible(dev->readq, kfifo_len(dev->read_fifo)!=0))
			return -ERESTARTSYS;
		mutex_lock(&dev->io_mutex);
	}

	xfersize = count < TBUF_LEN ? count : TBUF_LEN; // min(count, TBUF_LEN);

	xfersize = kfifo_get(dev->read_fifo, tbuf, TBUF_LEN);

	if (copy_to_user(buffer, tbuf, xfersize))
		retval = -EFAULT;
	else
		retval = xfersize;

	susi_read_more(dev);
	
exit:
	mutex_unlock(&dev->io_mutex);
	return retval;
}

static void susi_write_bulk_callback(struct urb *urb)
{
	struct usb_susi *dev;

	dev = (struct usb_susi *)urb->context;

	/* sync/async unlink faults aren't errors */
	if (urb->status &&
	    !(urb->status == -ENOENT ||
	      urb->status == -ECONNRESET ||
	      urb->status == -ESHUTDOWN)) {
		err("%s - nonzero write bulk status received: %d",
		    __FUNCTION__, urb->status);
	}

	/* free up our allocated buffer */
	usb_buffer_free(urb->dev, urb->transfer_buffer_length,
			urb->transfer_buffer, urb->transfer_dma);

	/* we can write more, so wake up the queue. */
	wake_up_interruptible(&dev->writeq);

	up(&dev->write_sem);
}

static ssize_t susi_write(struct file *file, const char *user_buffer, size_t count, loff_t *ppos)
{
	struct usb_susi *dev;
	int retval = 0;
	struct urb *urb = NULL;
	char *buf = NULL;
	size_t writesize = min(count, (size_t)MAX_TRANSFER);

	dev = (struct usb_susi *)file->private_data;

	/* verify that we actually have some data to write */
	if (count == 0)
		goto exit;

	/* limit the number of URBs in flight to stop a user from using up all RAM */
	if (down_interruptible(&dev->write_sem)) {
		retval = -ERESTARTSYS;
		goto exit;
	}

	mutex_lock(&dev->io_mutex);
	if (!dev->interface) {		/* disconnect() was called */
		retval = -ENODEV;
		goto error;
	}

	/* create a urb, and a buffer for it, and copy the data to the urb */
	urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!urb) {
		retval = -ENOMEM;
		goto error;
	}

	buf = usb_buffer_alloc(dev->udev, writesize, GFP_KERNEL, &urb->transfer_dma);
	if (!buf) {
		retval = -ENOMEM;
		goto error;
	}

	if (copy_from_user(buf, user_buffer, writesize)) {
		retval = -EFAULT;
		goto error;
	}

	/* initialize the urb properly */
	usb_fill_bulk_urb(urb, dev->udev,
			  usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
			  buf, writesize, susi_write_bulk_callback, dev);
	urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;

	/* send the data out the bulk port */
	retval = usb_submit_urb(urb, GFP_KERNEL);
	if (retval) {
		err("%s - failed submitting write urb, error %d", __FUNCTION__, retval);
		goto error;
	}

	/* release our reference to this urb, the USB core will eventually free it entirely */
	usb_free_urb(urb);

	mutex_unlock(&dev->io_mutex);
	return writesize;

error:
	if (urb) {
		usb_buffer_free(dev->udev, writesize, buf, urb->transfer_dma);
		usb_free_urb(urb);
	}
	mutex_unlock(&dev->io_mutex);
	up(&dev->write_sem);

exit:
	return retval;
}

static unsigned int susi_poll(struct file *file, poll_table *wait)
{
  	struct usb_susi *dev;
	unsigned int mask = 0;

	dev = (struct usb_susi *)file->private_data;
	mutex_lock(&dev->io_mutex);

	poll_wait(file, &dev->readq, wait);
	poll_wait(file, &dev->writeq, wait);

	if (kfifo_len(dev->read_fifo))
		mask |= POLLIN | POLLRDNORM;
	if (down_trylock(&dev->write_sem) == 0) {
		// this would be prettier if there was a semaphore_value() call
		mask |= POLLOUT | POLLWRNORM;
		up(&dev->write_sem); 
	}
	mutex_unlock(&dev->io_mutex);

	return mask;
}

static const struct file_operations susi_fops = {
	.owner =	THIS_MODULE,
	.read =		susi_read,
	.write =	susi_write,
	.open =		susi_open,
	.poll =         susi_poll,
	.release =	susi_release,
};

/*
 * usb class driver info in order to get a minor number from the usb core,
 * and to have the device registered with the driver core
 */
static struct usb_class_driver susi_class = {
	.name =		"susi%d",
	.fops =		&susi_fops,
	.minor_base =	USB_SUSI_MINOR_BASE,
};

static int susi_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
	struct usb_susi *dev;
	struct usb_host_interface *iface_desc;
	struct usb_endpoint_descriptor *endpoint;
	size_t buffer_size;
	int i;
	int retval = -ENOMEM;

	/* allocate memory for our device state and initialize it */
	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
	if (!dev) {
		err("Out of memory");
		goto error;
	}
	kref_init(&dev->kref);
	sema_init(&dev->write_sem, WRITES_IN_FLIGHT);
	sema_init(&dev->read_sem, 0);
	mutex_init(&dev->io_mutex);

	dev->udev = usb_get_dev(interface_to_usbdev(interface));
	dev->interface = interface;

	/* set up the endpoint information */
	/* use only the first bulk-in and bulk-out endpoints */
	iface_desc = interface->cur_altsetting;
	for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
		endpoint = &iface_desc->endpoint[i].desc;

		if (!dev->bulk_in_endpointAddr &&
		    usb_endpoint_is_bulk_in(endpoint)) {
			/* we found a bulk in endpoint */
			buffer_size = le16_to_cpu(endpoint->wMaxPacketSize);
			dev->bulk_in_size = buffer_size;
			dev->bulk_in_size = 512; // XXXX
			dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
			dev->bulk_in_buffer = kmalloc(dev->bulk_in_size, GFP_KERNEL);
			if (!dev->bulk_in_buffer) {
				err("Could not allocate bulk_in_buffer");
				goto error;
			}
		}

		if (!dev->bulk_out_endpointAddr &&
		    usb_endpoint_is_bulk_out(endpoint)) {
			/* we found a bulk out endpoint */
			dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
		}
	}
	if (!(dev->bulk_in_endpointAddr && dev->bulk_out_endpointAddr)) {
		err("Could not find both bulk-in and bulk-out endpoints");
		goto error;
	}

	/* save our data pointer in this interface device */
	usb_set_intfdata(interface, dev);

	/* we can register the device now, as it is ready */
	retval = usb_register_dev(interface, &susi_class);
	if (retval) {
		/* something prevented us from registering this driver */
		err("Not able to get a minor for this device.");
		usb_set_intfdata(interface, NULL);
		goto error;
	}

	/* allocate the read urb */
	dev->read_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!dev->read_urb) {
		retval = -ENOMEM;
		goto error;
	}

	dev->spinlock = SPIN_LOCK_UNLOCKED;
	dev->read_fifo = kfifo_alloc(4096, GFP_KERNEL, &dev->spinlock);
	if (!dev->read_fifo) {
		retval = -ENOMEM;
		goto error;
	}

	init_waitqueue_head(&dev->readq);
	init_waitqueue_head(&dev->writeq);

	/* start reading... */
	susi_read_more(dev);

	/* let the user know what node this device is now attached to */
	info("susi device now attached to susi-%d", interface->minor);
	return 0;

error:
	if (dev)
		kref_put(&dev->kref, susi_delete);
	return retval;
}

static void susi_disconnect(struct usb_interface *interface)
{
	struct usb_susi *dev;
	int minor = interface->minor;

	/* prevent susi_open() from racing susi_disconnect() */
	lock_kernel();

	dev = usb_get_intfdata(interface);
	usb_set_intfdata(interface, NULL);

	/* give back our minor */
	usb_deregister_dev(interface, &susi_class);

	/* prevent more I/O from starting */
	mutex_lock(&dev->io_mutex);
	dev->interface = NULL;
	mutex_unlock(&dev->io_mutex);

	unlock_kernel();

	/* decrement our usage count */
	kref_put(&dev->kref, susi_delete);

	info("USB Susieton #%d now disconnected", minor);
}

static struct usb_driver susi_driver = {
	.name =		"susieton",
	.probe =	susi_probe,
	.disconnect =	susi_disconnect,
	.id_table =	susi_table,
};

static int __init usb_susi_init(void)
{
	int result;

	/* register this driver with the USB subsystem */
	result = usb_register(&susi_driver);
	if (result)
		err("usb_register failed. Error number %d", result);

	return result;
}

static void __exit usb_susi_exit(void)
{
	/* deregister this driver with the USB subsystem */
	usb_deregister(&susi_driver);
}

module_init(usb_susi_init);
module_exit(usb_susi_exit);

MODULE_LICENSE("GPL");
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.