From cbc30118d7a376dab4113f299c0c8f035737a5c3 Mon Sep 17 00:00:00 2001 From: Stephen Ware Date: Tue, 30 Sep 2008 11:39:38 -0700 Subject: usb: vstusb.c : new driver for spectrometers used by Vernier Software & Technology, Inc. This patch adds the vstusb driver to the drivers/usb/misc directory. This driver provides support for Vernier Software & Technology spectrometers, all made by Ocean Optics. The driver provides both IOCTL and read()/write() methods for sending raw data to spectrometers across the bulk channel. Each method allows for a configured timeout. From: Stephen Ware Signed-off-by: Dennis O'Brien Signed-off-by: Greg Kroah-Hartman --- drivers/usb/misc/Kconfig | 15 + drivers/usb/misc/Makefile | 1 + drivers/usb/misc/vstusb.c | 768 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 784 insertions(+) create mode 100644 drivers/usb/misc/vstusb.c (limited to 'drivers/usb') diff --git a/drivers/usb/misc/Kconfig b/drivers/usb/misc/Kconfig index 25e1157ab17..e463db5d818 100644 --- a/drivers/usb/misc/Kconfig +++ b/drivers/usb/misc/Kconfig @@ -280,3 +280,18 @@ config USB_ISIGHTFW The firmware for this driver must be extracted from the MacOS driver beforehand. Tools for doing so are available at http://bersace03.free.fr + +config USB_VST + tristate "USB VST driver" + depends on USB + help + This driver is intended for Vernier Software Technologies + bulk usb devices such as their Ocean-Optics spectrometers or + Labquest. + It is a bulk channel driver with configurable read and write + timeouts. + + To compile this driver as a module, choose M here: the + module will be called vstusb. + + diff --git a/drivers/usb/misc/Makefile b/drivers/usb/misc/Makefile index 39ce4a16b3d..1334f7bdd7b 100644 --- a/drivers/usb/misc/Makefile +++ b/drivers/usb/misc/Makefile @@ -27,6 +27,7 @@ obj-$(CONFIG_USB_TEST) += usbtest.o obj-$(CONFIG_USB_TRANCEVIBRATOR) += trancevibrator.o obj-$(CONFIG_USB_USS720) += uss720.o obj-$(CONFIG_USB_SEVSEG) += usbsevseg.o +obj-$(CONFIG_USB_VST) += vstusb.o obj-$(CONFIG_USB_SISUSBVGA) += sisusbvga/ diff --git a/drivers/usb/misc/vstusb.c b/drivers/usb/misc/vstusb.c new file mode 100644 index 00000000000..5ad75e4a032 --- /dev/null +++ b/drivers/usb/misc/vstusb.c @@ -0,0 +1,768 @@ +/***************************************************************************** + * File: drivers/usb/misc/vstusb.c + * + * Purpose: Support for the bulk USB Vernier Spectrophotometers + * + * Author: Johnnie Peters + * Axian Consulting + * Beaverton, OR, USA 97005 + * + * Modified by: EQware Engineering, Inc. + * Oregon City, OR, USA 97045 + * + * Copyright: 2007, 2008 + * Vernier Software & Technology + * Beaverton, OR, USA 97005 + * + * Web: www.vernier.com + * + * 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 + * published by the Free Software Foundation. + * + *****************************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define DRIVER_VERSION "VST USB Driver Version 1.5" +#define DRIVER_DESC "Vernier Software Technology Bulk USB Driver" + +#ifdef CONFIG_USB_DYNAMIC_MINORS + #define VSTUSB_MINOR_BASE 0 +#else + #define VSTUSB_MINOR_BASE 199 +#endif + +#define USB_VENDOR_OCEANOPTICS 0x2457 +#define USB_VENDOR_VERNIER 0x08F7 /* Vernier Software & Technology */ + +#define USB_PRODUCT_USB2000 0x1002 +#define USB_PRODUCT_ADC1000_FW 0x1003 /* firmware download (renumerates) */ +#define USB_PRODUCT_ADC1000 0x1004 +#define USB_PRODUCT_HR2000_FW 0x1009 /* firmware download (renumerates) */ +#define USB_PRODUCT_HR2000 0x100A +#define USB_PRODUCT_HR4000_FW 0x1011 /* firmware download (renumerates) */ +#define USB_PRODUCT_HR4000 0x1012 +#define USB_PRODUCT_USB650 0x1014 /* "Red Tide" */ +#define USB_PRODUCT_QE65000 0x1018 +#define USB_PRODUCT_USB4000 0x1022 +#define USB_PRODUCT_USB325 0x1024 /* "Vernier Spectrometer" */ + +#define USB_PRODUCT_LABPRO 0x0001 +#define USB_PRODUCT_LABQUEST 0x0005 + +static struct usb_device_id id_table[] = { + { USB_DEVICE(USB_VENDOR_OCEANOPTICS, USB_PRODUCT_USB2000)}, + { USB_DEVICE(USB_VENDOR_OCEANOPTICS, USB_PRODUCT_HR4000)}, + { USB_DEVICE(USB_VENDOR_OCEANOPTICS, USB_PRODUCT_USB650)}, + { USB_DEVICE(USB_VENDOR_OCEANOPTICS, USB_PRODUCT_USB4000)}, + { USB_DEVICE(USB_VENDOR_OCEANOPTICS, USB_PRODUCT_USB325)}, + { USB_DEVICE(USB_VENDOR_VERNIER, USB_PRODUCT_LABQUEST)}, + { USB_DEVICE(USB_VENDOR_VERNIER, USB_PRODUCT_LABPRO)}, + {}, +}; + +MODULE_DEVICE_TABLE(usb, id_table); + +struct vstusb_device { + struct mutex lock; + struct usb_device *usb_dev; + char present; + char isopen; + struct usb_anchor submitted; + int rd_pipe; + int rd_timeout_ms; + int wr_pipe; + int wr_timeout_ms; +}; + +static struct usb_driver vstusb_driver; + +static int vstusb_open(struct inode *inode, struct file *file) +{ + struct vstusb_device *vstdev; + struct usb_interface *interface; + + interface = usb_find_interface(&vstusb_driver, iminor(inode)); + + if (!interface) { + printk(KERN_ERR KBUILD_MODNAME + ": %s - error, can't find device for minor %d\n", + __func__, iminor(inode)); + return -ENODEV; + } + + vstdev = usb_get_intfdata(interface); + + if (!vstdev) + return -ENODEV; + + /* lock this device */ + mutex_lock(&vstdev->lock); + + /* can only open one time */ + if ((!vstdev->present) || (vstdev->isopen)) { + mutex_unlock(&vstdev->lock); + return -EBUSY; + } + + vstdev->isopen = 1; + + /* save device in the file's private structure */ + file->private_data = vstdev; + + dev_dbg(&vstdev->usb_dev->dev, "%s: opened\n", __func__); + + mutex_unlock(&vstdev->lock); + + return 0; +} + +static int vstusb_close(struct inode *inode, struct file *file) +{ + struct vstusb_device *vstdev; + + vstdev = file->private_data; + + if (vstdev == NULL) + return -ENODEV; + + mutex_lock(&vstdev->lock); + + vstdev->isopen = 0; + file->private_data = NULL; + + /* if device is no longer present */ + if (!vstdev->present) { + mutex_unlock(&vstdev->lock); + kfree(vstdev); + } else + mutex_unlock(&vstdev->lock); + + return 0; +} + +static void usb_api_blocking_completion(struct urb *urb) +{ + struct completion *completeit = urb->context; + + complete(completeit); +} + +static int vstusb_fill_and_send_urb(struct urb *urb, + struct usb_device *usb_dev, + unsigned int pipe, void *data, + unsigned int len, struct completion *done) +{ + struct usb_host_endpoint *ep; + struct usb_host_endpoint **hostep; + unsigned int pipend; + + int status; + + hostep = usb_pipein(pipe) ? usb_dev->ep_in : usb_dev->ep_out; + pipend = usb_pipeendpoint(pipe); + ep = hostep[pipend]; + + if (!ep || (len == 0)) + return -EINVAL; + + if ((ep->desc.bmAttributes & USB_ENDPOINT_XFERTYPE_MASK) + == USB_ENDPOINT_XFER_INT) { + pipe = (pipe & ~(3 << 30)) | (PIPE_INTERRUPT << 30); + usb_fill_int_urb(urb, usb_dev, pipe, data, len, + (usb_complete_t)usb_api_blocking_completion, + NULL, ep->desc.bInterval); + } else + usb_fill_bulk_urb(urb, usb_dev, pipe, data, len, + (usb_complete_t)usb_api_blocking_completion, + NULL); + + init_completion(done); + urb->context = done; + urb->actual_length = 0; + status = usb_submit_urb(urb, GFP_KERNEL); + + return status; +} + +static int vstusb_complete_urb(struct urb *urb, struct completion *done, + int timeout, int *actual_length) +{ + unsigned long expire; + int status; + + expire = timeout ? msecs_to_jiffies(timeout) : MAX_SCHEDULE_TIMEOUT; + if (!wait_for_completion_interruptible_timeout(done, expire)) { + usb_kill_urb(urb); + status = urb->status == -ENOENT ? -ETIMEDOUT : urb->status; + + dev_dbg(&urb->dev->dev, + "%s timed out on ep%d%s len=%d/%d, urb status = %d\n", + current->comm, + usb_pipeendpoint(urb->pipe), + usb_pipein(urb->pipe) ? "in" : "out", + urb->actual_length, + urb->transfer_buffer_length, + urb->status); + + } else { + if (signal_pending(current)) { + /* if really an error */ + if (urb->status && !((urb->status == -ENOENT) || + (urb->status == -ECONNRESET) || + (urb->status == -ESHUTDOWN))) { + status = -EINTR; + usb_kill_urb(urb); + } else { + status = 0; + } + + dev_dbg(&urb->dev->dev, + "%s: signal pending on ep%d%s len=%d/%d," + "urb status = %d\n", + current->comm, + usb_pipeendpoint(urb->pipe), + usb_pipein(urb->pipe) ? "in" : "out", + urb->actual_length, + urb->transfer_buffer_length, + urb->status); + + } else { + status = urb->status; + } + } + + if (actual_length) + *actual_length = urb->actual_length; + + return status; +} + +static ssize_t vstusb_read(struct file *file, char __user *buffer, + size_t count, loff_t *ppos) +{ + struct vstusb_device *vstdev; + int cnt = -1; + void *buf; + int retval = 0; + + struct urb *urb; + struct usb_device *dev; + unsigned int pipe; + int timeout; + + DECLARE_COMPLETION_ONSTACK(done); + + vstdev = file->private_data; + + if (vstdev == NULL) + return -ENODEV; + + /* verify that we actually want to read some data */ + if (count == 0) + return -EINVAL; + + /* lock this object */ + if (mutex_lock_interruptible(&vstdev->lock)) + return -ERESTARTSYS; + + /* anyone home */ + if (!vstdev->present) { + mutex_unlock(&vstdev->lock); + printk(KERN_ERR KBUILD_MODNAME + ": %s: device not present\n", __func__); + return -ENODEV; + } + + /* pull out the necessary data */ + dev = vstdev->usb_dev; + pipe = usb_rcvbulkpipe(dev, vstdev->rd_pipe); + timeout = vstdev->rd_timeout_ms; + + buf = kmalloc(count, GFP_KERNEL); + if (buf == NULL) { + mutex_unlock(&vstdev->lock); + return -ENOMEM; + } + + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) { + kfree(buf); + mutex_unlock(&vstdev->lock); + return -ENOMEM; + } + + usb_anchor_urb(urb, &vstdev->submitted); + retval = vstusb_fill_and_send_urb(urb, dev, pipe, buf, count, &done); + mutex_unlock(&vstdev->lock); + if (retval) { + usb_unanchor_urb(urb); + dev_err(&dev->dev, "%s: error %d filling and sending urb %d\n", + __func__, retval, pipe); + goto exit; + } + + retval = vstusb_complete_urb(urb, &done, timeout, &cnt); + if (retval) { + dev_err(&dev->dev, "%s: error %d completing urb %d\n", + __func__, retval, pipe); + goto exit; + } + + if (copy_to_user(buffer, buf, cnt)) { + dev_err(&dev->dev, "%s: can't copy_to_user\n", __func__); + retval = -EFAULT; + } else { + retval = cnt; + dev_dbg(&dev->dev, "%s: read %d bytes from pipe %d\n", + __func__, cnt, pipe); + } + +exit: + usb_free_urb(urb); + kfree(buf); + return retval; +} + +static ssize_t vstusb_write(struct file *file, const char __user *buffer, + size_t count, loff_t *ppos) +{ + struct vstusb_device *vstdev; + int cnt = -1; + void *buf; + int retval = 0; + + struct urb *urb; + struct usb_device *dev; + unsigned int pipe; + int timeout; + + DECLARE_COMPLETION_ONSTACK(done); + + vstdev = file->private_data; + + if (vstdev == NULL) + return -ENODEV; + + /* verify that we actually have some data to write */ + if (count == 0) + return retval; + + /* lock this object */ + if (mutex_lock_interruptible(&vstdev->lock)) + return -ERESTARTSYS; + + /* anyone home */ + if (!vstdev->present) { + mutex_unlock(&vstdev->lock); + printk(KERN_ERR KBUILD_MODNAME + ": %s: device not present\n", __func__); + return -ENODEV; + } + + /* pull out the necessary data */ + dev = vstdev->usb_dev; + pipe = usb_sndbulkpipe(dev, vstdev->wr_pipe); + timeout = vstdev->wr_timeout_ms; + + buf = kmalloc(count, GFP_KERNEL); + if (buf == NULL) { + mutex_unlock(&vstdev->lock); + return -ENOMEM; + } + + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) { + kfree(buf); + mutex_unlock(&vstdev->lock); + return -ENOMEM; + } + + if (copy_from_user(buf, buffer, count)) { + dev_err(&dev->dev, "%s: can't copy_from_user\n", __func__); + retval = -EFAULT; + goto exit; + } + + usb_anchor_urb(urb, &vstdev->submitted); + retval = vstusb_fill_and_send_urb(urb, dev, pipe, buf, count, &done); + mutex_unlock(&vstdev->lock); + if (retval) { + usb_unanchor_urb(urb); + dev_err(&dev->dev, "%s: error %d filling and sending urb %d\n", + __func__, retval, pipe); + goto exit; + } + + retval = vstusb_complete_urb(urb, &done, timeout, &cnt); + if (retval) { + dev_err(&dev->dev, "%s: error %d completing urb %d\n", + __func__, retval, pipe); + goto exit; + } else { + retval = cnt; + dev_dbg(&dev->dev, "%s: sent %d bytes to pipe %d\n", + __func__, cnt, pipe); + } + +exit: + usb_free_urb(urb); + kfree(buf); + return retval; +} + +static long vstusb_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + int retval = 0; + int cnt = -1; + void __user *data = (void __user *)arg; + struct vstusb_args usb_data; + + struct vstusb_device *vstdev; + void *buffer = NULL; /* must be initialized. buffer is + * referenced on exit but not all + * ioctls allocate it */ + + struct urb *urb = NULL; /* must be initialized. urb is + * referenced on exit but not all + * ioctls allocate it */ + struct usb_device *dev; + unsigned int pipe; + int timeout; + + DECLARE_COMPLETION_ONSTACK(done); + + vstdev = file->private_data; + + if (_IOC_TYPE(cmd) != VST_IOC_MAGIC) { + dev_warn(&vstdev->usb_dev->dev, + "%s: ioctl command %x, bad ioctl magic %x, " + "expected %x\n", __func__, cmd, + _IOC_TYPE(cmd), VST_IOC_MAGIC); + return -EINVAL; + } + + if (vstdev == NULL) + return -ENODEV; + + if (copy_from_user(&usb_data, data, sizeof(struct vstusb_args))) { + dev_err(&vstdev->usb_dev->dev, "%s: can't copy_from_user\n", + __func__); + return -EFAULT; + } + + /* lock this object */ + if (mutex_lock_interruptible(&vstdev->lock)) { + retval = -ERESTARTSYS; + goto exit; + } + + /* anyone home */ + if (!vstdev->present) { + mutex_unlock(&vstdev->lock); + dev_err(&vstdev->usb_dev->dev, "%s: device not present\n", + __func__); + retval = -ENODEV; + goto exit; + } + + /* pull out the necessary data */ + dev = vstdev->usb_dev; + + switch (cmd) { + + case IOCTL_VSTUSB_CONFIG_RW: + + vstdev->rd_pipe = usb_data.rd_pipe; + vstdev->rd_timeout_ms = usb_data.rd_timeout_ms; + vstdev->wr_pipe = usb_data.wr_pipe; + vstdev->wr_timeout_ms = usb_data.wr_timeout_ms; + + mutex_unlock(&vstdev->lock); + + dev_dbg(&dev->dev, "%s: setting pipes/timeouts, " + "rdpipe = %d, rdtimeout = %d, " + "wrpipe = %d, wrtimeout = %d\n", __func__, + vstdev->rd_pipe, vstdev->rd_timeout_ms, + vstdev->wr_pipe, vstdev->wr_timeout_ms); + break; + + case IOCTL_VSTUSB_SEND_PIPE: + + if (usb_data.count == 0) { + mutex_unlock(&vstdev->lock); + retval = -EINVAL; + goto exit; + } + + buffer = kmalloc(usb_data.count, GFP_KERNEL); + if (buffer == NULL) { + mutex_unlock(&vstdev->lock); + retval = -ENOMEM; + goto exit; + } + + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) { + mutex_unlock(&vstdev->lock); + retval = -ENOMEM; + goto exit; + } + + timeout = usb_data.timeout_ms; + + pipe = usb_sndbulkpipe(dev, usb_data.pipe); + + if (copy_from_user(buffer, usb_data.buffer, usb_data.count)) { + dev_err(&dev->dev, "%s: can't copy_from_user\n", + __func__); + mutex_unlock(&vstdev->lock); + retval = -EFAULT; + goto exit; + } + + usb_anchor_urb(urb, &vstdev->submitted); + retval = vstusb_fill_and_send_urb(urb, dev, pipe, buffer, + usb_data.count, &done); + mutex_unlock(&vstdev->lock); + if (retval) { + usb_unanchor_urb(urb); + dev_err(&dev->dev, + "%s: error %d filling and sending urb %d\n", + __func__, retval, pipe); + goto exit; + } + + retval = vstusb_complete_urb(urb, &done, timeout, &cnt); + if (retval) { + dev_err(&dev->dev, "%s: error %d completing urb %d\n", + __func__, retval, pipe); + } + + break; + case IOCTL_VSTUSB_RECV_PIPE: + + if (usb_data.count == 0) { + mutex_unlock(&vstdev->lock); + retval = -EINVAL; + goto exit; + } + + buffer = kmalloc(usb_data.count, GFP_KERNEL); + if (buffer == NULL) { + mutex_unlock(&vstdev->lock); + retval = -ENOMEM; + goto exit; + } + + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) { + mutex_unlock(&vstdev->lock); + retval = -ENOMEM; + goto exit; + } + + timeout = usb_data.timeout_ms; + + pipe = usb_rcvbulkpipe(dev, usb_data.pipe); + + usb_anchor_urb(urb, &vstdev->submitted); + retval = vstusb_fill_and_send_urb(urb, dev, pipe, buffer, + usb_data.count, &done); + mutex_unlock(&vstdev->lock); + if (retval) { + usb_unanchor_urb(urb); + dev_err(&dev->dev, + "%s: error %d filling and sending urb %d\n", + __func__, retval, pipe); + goto exit; + } + + retval = vstusb_complete_urb(urb, &done, timeout, &cnt); + if (retval) { + dev_err(&dev->dev, "%s: error %d completing urb %d\n", + __func__, retval, pipe); + goto exit; + } + + if (copy_to_user(usb_data.buffer, buffer, cnt)) { + dev_err(&dev->dev, "%s: can't copy_to_user\n", + __func__); + retval = -EFAULT; + goto exit; + } + + usb_data.count = cnt; + if (copy_to_user(data, &usb_data, sizeof(struct vstusb_args))) { + dev_err(&dev->dev, "%s: can't copy_to_user\n", + __func__); + retval = -EFAULT; + } else { + dev_dbg(&dev->dev, "%s: recv %d bytes from pipe %d\n", + __func__, usb_data.count, usb_data.pipe); + } + + break; + + default: + mutex_unlock(&vstdev->lock); + dev_warn(&dev->dev, "ioctl_vstusb: invalid ioctl cmd %x\n", + cmd); + return -EINVAL; + break; + } +exit: + usb_free_urb(urb); + kfree(buffer); + return retval; +} + +static const struct file_operations vstusb_fops = { + .owner = THIS_MODULE, + .read = vstusb_read, + .write = vstusb_write, + .unlocked_ioctl = vstusb_ioctl, + .compat_ioctl = vstusb_ioctl, + .open = vstusb_open, + .release = vstusb_close, +}; + +static struct usb_class_driver usb_vstusb_class = { + .name = "usb/vstusb%d", + .fops = &vstusb_fops, + .minor_base = VSTUSB_MINOR_BASE, +}; + +static int vstusb_probe(struct usb_interface *intf, + const struct usb_device_id *id) +{ + struct usb_device *dev = interface_to_usbdev(intf); + struct vstusb_device *vstdev; + int i; + int retval = 0; + + /* allocate memory for our device state and intialize it */ + + vstdev = kzalloc(sizeof(*vstdev), GFP_KERNEL); + if (vstdev == NULL) + return -ENOMEM; + + mutex_init(&vstdev->lock); + + i = dev->descriptor.bcdDevice; + + dev_dbg(&intf->dev, "Version %1d%1d.%1d%1d found at address %d\n", + (i & 0xF000) >> 12, (i & 0xF00) >> 8, + (i & 0xF0) >> 4, (i & 0xF), dev->devnum); + + vstdev->present = 1; + vstdev->isopen = 0; + vstdev->usb_dev = dev; + init_usb_anchor(&vstdev->submitted); + + usb_set_intfdata(intf, vstdev); + retval = usb_register_dev(intf, &usb_vstusb_class); + if (retval) { + dev_err(&intf->dev, + "%s: Not able to get a minor for this device.\n", + __func__); + usb_set_intfdata(intf, NULL); + kfree(vstdev); + return retval; + } + + /* let the user know what node this device is now attached to */ + dev_info(&intf->dev, + "VST USB Device #%d now attached to major %d minor %d\n", + (intf->minor - VSTUSB_MINOR_BASE), USB_MAJOR, intf->minor); + + dev_info(&intf->dev, "%s, %s\n", DRIVER_DESC, DRIVER_VERSION); + + return retval; +} + +static void vstusb_disconnect(struct usb_interface *intf) +{ + struct vstusb_device *vstdev = usb_get_intfdata(intf); + + usb_deregister_dev(intf, &usb_vstusb_class); + usb_set_intfdata(intf, NULL); + + if (vstdev) { + + mutex_lock(&vstdev->lock); + vstdev->present = 0; + + usb_kill_anchored_urbs(&vstdev->submitted); + + /* if the device is not opened, then we clean up right now */ + if (!vstdev->isopen) { + mutex_unlock(&vstdev->lock); + kfree(vstdev); + } else + mutex_unlock(&vstdev->lock); + + } +} + +static int vstusb_suspend(struct usb_interface *intf, pm_message_t message) +{ + struct vstusb_device *vstdev = usb_get_intfdata(intf); + int time; + if (!vstdev) + return 0; + + mutex_lock(&vstdev->lock); + time = usb_wait_anchor_empty_timeout(&vstdev->submitted, 1000); + if (!time) + usb_kill_anchored_urbs(&vstdev->submitted); + mutex_unlock(&vstdev->lock); + + return 0; +} + +static int vstusb_resume(struct usb_interface *intf) +{ + return 0; +} + +static struct usb_driver vstusb_driver = { + .name = "vstusb", + .probe = vstusb_probe, + .disconnect = vstusb_disconnect, + .suspend = vstusb_suspend, + .resume = vstusb_resume, + .id_table = id_table, +}; + +static int __init vstusb_init(void) +{ + int rc; + + rc = usb_register(&vstusb_driver); + if (rc) + printk(KERN_ERR "%s: failed to register (%d)", __func__, rc); + + return rc; +} + +static void __exit vstusb_exit(void) +{ + usb_deregister(&vstusb_driver); +} + +module_init(vstusb_init); +module_exit(vstusb_exit); + +MODULE_AUTHOR("Dennis O'Brien/Stephen Ware"); +MODULE_DESCRIPTION(DRIVER_VERSION); +MODULE_LICENSE("GPL"); -- cgit v1.2.3