Commit | Line | Data |
---|---|---|
8da3dc28 DT |
1 | /* |
2 | * Frontier Designs Tranzport driver | |
3 | * | |
4 | * Copyright (C) 2007 Michael Taht (m@taht.net) | |
5 | * | |
6 | * Based on the usbled driver and ldusb drivers by | |
7 | * | |
8 | * Copyright (C) 2004 Greg Kroah-Hartman (greg@kroah.com) | |
9 | * Copyright (C) 2005 Michael Hund <mhund@ld-didactic.de> | |
10 | * | |
11 | * The ldusb driver was, in turn, derived from Lego USB Tower driver | |
12 | * Copyright (C) 2003 David Glance <advidgsf@sourceforge.net> | |
13 | * 2001-2004 Juergen Stuber <starblue@users.sourceforge.net> | |
14 | * | |
15 | * This program is free software; you can redistribute it and/or | |
16 | * modify it under the terms of the GNU General Public License as | |
17 | * published by the Free Software Foundation, version 2. | |
18 | * | |
19 | */ | |
20 | ||
7c68d6b7 | 21 | /* |
8da3dc28 DT |
22 | * This driver uses a ring buffer for time critical reading of |
23 | * interrupt in reports and provides read and write methods for | |
24 | * raw interrupt reports. | |
25 | */ | |
26 | ||
27 | /* Note: this currently uses a dumb ringbuffer for reads and writes. | |
28 | * A more optimal driver would cache and kill off outstanding urbs that are | |
29 | * now invalid, and ignore ones that already were in the queue but valid | |
30 | * as we only have 17 commands for the tranzport. In particular this is | |
31 | * key for getting lights to flash in time as otherwise many commands | |
32 | * can be buffered up before the light change makes it to the interface. | |
7c68d6b7 | 33 | */ |
8da3dc28 DT |
34 | |
35 | #include <linux/kernel.h> | |
36 | #include <linux/errno.h> | |
37 | #include <linux/init.h> | |
38 | #include <linux/slab.h> | |
39 | #include <linux/module.h> | |
40 | #include <linux/mutex.h> | |
8da3dc28 | 41 | |
7c68d6b7 | 42 | #include <linux/uaccess.h> |
8da3dc28 DT |
43 | #include <linux/input.h> |
44 | #include <linux/usb.h> | |
45 | #include <linux/poll.h> | |
46 | ||
8da3dc28 DT |
47 | /* Define these values to match your devices */ |
48 | #define VENDOR_ID 0x165b | |
7c68d6b7 | 49 | #define PRODUCT_ID 0x8101 |
8da3dc28 DT |
50 | |
51 | #ifdef CONFIG_USB_DYNAMIC_MINORS | |
52 | #define USB_TRANZPORT_MINOR_BASE 0 | |
dab8c359 | 53 | #else /* FIXME 177- is the another driver's minor - apply for a minor soon */ |
8da3dc28 DT |
54 | #define USB_TRANZPORT_MINOR_BASE 177 |
55 | #endif | |
56 | ||
57 | /* table of devices that work with this driver */ | |
a457732b | 58 | static const struct usb_device_id usb_tranzport_table[] = { |
7c68d6b7 DT |
59 | {USB_DEVICE(VENDOR_ID, PRODUCT_ID)}, |
60 | {} /* Terminating entry */ | |
8da3dc28 DT |
61 | }; |
62 | ||
63 | MODULE_DEVICE_TABLE(usb, usb_tranzport_table); | |
dab8c359 | 64 | MODULE_VERSION("0.35"); |
8da3dc28 DT |
65 | MODULE_AUTHOR("Mike Taht <m@taht.net>"); |
66 | MODULE_DESCRIPTION("Tranzport USB Driver"); | |
67 | MODULE_LICENSE("GPL"); | |
68 | MODULE_SUPPORTED_DEVICE("Frontier Designs Tranzport Control Surface"); | |
69 | ||
8da3dc28 DT |
70 | #define SUPPRESS_EXTRA_OFFLINE_EVENTS 1 |
71 | #define COMPRESS_WHEEL_EVENTS 1 | |
72 | #define BUFFERED_READS 1 | |
73 | #define RING_BUFFER_SIZE 1000 | |
74 | #define WRITE_BUFFER_SIZE 34 | |
75 | #define TRANZPORT_USB_TIMEOUT 10 | |
7c68d6b7 | 76 | #define TRANZPORT_DEBUG 0 |
8da3dc28 | 77 | |
7c68d6b7 | 78 | static int debug = TRANZPORT_DEBUG; |
8da3dc28 DT |
79 | |
80 | /* Use our own dbg macro */ | |
7c68d6b7 DT |
81 | #define dbg_info(dev, format, arg...) do \ |
82 | { if (debug) dev_info(dev , format , ## arg); } while (0) | |
8da3dc28 DT |
83 | |
84 | /* Module parameters */ | |
85 | ||
86 | module_param(debug, int, S_IRUGO | S_IWUSR); | |
87 | MODULE_PARM_DESC(debug, "Debug enabled or not"); | |
88 | ||
89 | /* All interrupt in transfers are collected in a ring buffer to | |
90 | * avoid racing conditions and get better performance of the driver. | |
91 | */ | |
92 | ||
93 | static int ring_buffer_size = RING_BUFFER_SIZE; | |
94 | ||
7c68d6b7 | 95 | module_param(ring_buffer_size, int, S_IRUGO); |
8da3dc28 DT |
96 | MODULE_PARM_DESC(ring_buffer_size, "Read ring buffer size in reports"); |
97 | ||
98 | /* The write_buffer can one day contain more than one interrupt out transfer. | |
99 | */ | |
100 | static int write_buffer_size = WRITE_BUFFER_SIZE; | |
7c68d6b7 | 101 | module_param(write_buffer_size, int, S_IRUGO); |
8da3dc28 DT |
102 | MODULE_PARM_DESC(write_buffer_size, "Write buffer size"); |
103 | ||
104 | /* | |
105 | * Increase the interval for debugging purposes. | |
106 | * or set to 1 to use the standard interval from the endpoint descriptors. | |
107 | */ | |
108 | ||
109 | static int min_interrupt_in_interval = TRANZPORT_USB_TIMEOUT; | |
110 | module_param(min_interrupt_in_interval, int, 0); | |
7c68d6b7 DT |
111 | MODULE_PARM_DESC(min_interrupt_in_interval, |
112 | "Minimum interrupt in interval in ms"); | |
8da3dc28 DT |
113 | |
114 | static int min_interrupt_out_interval = TRANZPORT_USB_TIMEOUT; | |
115 | module_param(min_interrupt_out_interval, int, 0); | |
7c68d6b7 DT |
116 | MODULE_PARM_DESC(min_interrupt_out_interval, |
117 | "Minimum interrupt out interval in ms"); | |
8da3dc28 DT |
118 | |
119 | struct tranzport_cmd { | |
7c68d6b7 | 120 | unsigned char cmd[8]; |
8da3dc28 DT |
121 | }; |
122 | ||
8da3dc28 DT |
123 | /* Structure to hold all of our device specific stuff */ |
124 | ||
125 | struct usb_tranzport { | |
3504e0c8 | 126 | struct mutex mtx; /* locks this structure */ |
7c68d6b7 DT |
127 | struct usb_interface *intf; /* save off the usb interface pointer */ |
128 | int open_count; /* number of times this port opened */ | |
129 | struct tranzport_cmd (*ring_buffer)[RING_BUFFER_SIZE]; | |
130 | unsigned int ring_head; | |
131 | unsigned int ring_tail; | |
132 | wait_queue_head_t read_wait; | |
133 | wait_queue_head_t write_wait; | |
134 | unsigned char *interrupt_in_buffer; | |
135 | struct usb_endpoint_descriptor *interrupt_in_endpoint; | |
136 | struct urb *interrupt_in_urb; | |
137 | int interrupt_in_interval; | |
138 | size_t interrupt_in_endpoint_size; | |
139 | int interrupt_in_running; | |
140 | int interrupt_in_done; | |
141 | char *interrupt_out_buffer; | |
142 | struct usb_endpoint_descriptor *interrupt_out_endpoint; | |
143 | struct urb *interrupt_out_urb; | |
144 | int interrupt_out_interval; | |
145 | size_t interrupt_out_endpoint_size; | |
146 | int interrupt_out_busy; | |
8da3dc28 | 147 | |
dab8c359 | 148 | /* Sysfs support */ |
8da3dc28 | 149 | |
7c68d6b7 DT |
150 | unsigned char enable; /* 0 if disabled 1 if enabled */ |
151 | unsigned char offline; /* if the device is out of range or asleep */ | |
152 | unsigned char compress_wheel; /* flag to compress wheel events */ | |
8da3dc28 DT |
153 | }; |
154 | ||
155 | /* prevent races between open() and disconnect() */ | |
156 | static DEFINE_MUTEX(disconnect_mutex); | |
157 | ||
158 | static struct usb_driver usb_tranzport_driver; | |
159 | ||
160 | /** | |
161 | * usb_tranzport_abort_transfers | |
162 | * aborts transfers and frees associated data structures | |
163 | */ | |
164 | static void usb_tranzport_abort_transfers(struct usb_tranzport *dev) | |
165 | { | |
166 | /* shutdown transfer */ | |
167 | if (dev->interrupt_in_running) { | |
168 | dev->interrupt_in_running = 0; | |
169 | if (dev->intf) | |
170 | usb_kill_urb(dev->interrupt_in_urb); | |
171 | } | |
172 | if (dev->interrupt_out_busy) | |
173 | if (dev->intf) | |
174 | usb_kill_urb(dev->interrupt_out_urb); | |
175 | } | |
176 | ||
bd384c27 GS |
177 | #define show_int(value) \ |
178 | static ssize_t show_##value(struct device *dev, \ | |
dab8c359 | 179 | struct device_attribute *attr, char *buf) \ |
bd384c27 GS |
180 | { \ |
181 | struct usb_interface *intf = to_usb_interface(dev); \ | |
182 | struct usb_tranzport *t = usb_get_intfdata(intf); \ | |
183 | return sprintf(buf, "%d\n", t->value); \ | |
184 | } \ | |
185 | static DEVICE_ATTR(value, S_IRUGO, show_##value, NULL); | |
186 | ||
187 | #define show_set_int(value) \ | |
188 | static ssize_t show_##value(struct device *dev, \ | |
7c68d6b7 | 189 | struct device_attribute *attr, char *buf) \ |
bd384c27 GS |
190 | { \ |
191 | struct usb_interface *intf = to_usb_interface(dev); \ | |
192 | struct usb_tranzport *t = usb_get_intfdata(intf); \ | |
193 | return sprintf(buf, "%d\n", t->value); \ | |
194 | } \ | |
195 | static ssize_t set_##value(struct device *dev, \ | |
7c68d6b7 DT |
196 | struct device_attribute *attr, \ |
197 | const char *buf, size_t count) \ | |
bd384c27 GS |
198 | { \ |
199 | struct usb_interface *intf = to_usb_interface(dev); \ | |
200 | struct usb_tranzport *t = usb_get_intfdata(intf); \ | |
201 | unsigned long temp; \ | |
202 | if (strict_strtoul(buf, 10, &temp)) \ | |
203 | return -EINVAL; \ | |
204 | t->value = temp; \ | |
205 | return count; \ | |
206 | } \ | |
2a767fda | 207 | static DEVICE_ATTR(value, S_IWUSR | S_IRUGO, show_##value, set_##value); |
8da3dc28 | 208 | |
dab8c359 DT |
209 | show_int(enable); |
210 | show_int(offline); | |
8da3dc28 | 211 | show_set_int(compress_wheel); |
8da3dc28 DT |
212 | |
213 | /** | |
214 | * usb_tranzport_delete | |
215 | */ | |
216 | static void usb_tranzport_delete(struct usb_tranzport *dev) | |
217 | { | |
218 | usb_tranzport_abort_transfers(dev); | |
7c68d6b7 | 219 | if (dev->intf != NULL) { |
7c68d6b7 | 220 | device_remove_file(&dev->intf->dev, &dev_attr_enable); |
7c68d6b7 DT |
221 | device_remove_file(&dev->intf->dev, &dev_attr_offline); |
222 | device_remove_file(&dev->intf->dev, &dev_attr_compress_wheel); | |
8da3dc28 DT |
223 | } |
224 | ||
225 | /* free data structures */ | |
226 | usb_free_urb(dev->interrupt_in_urb); | |
227 | usb_free_urb(dev->interrupt_out_urb); | |
228 | kfree(dev->ring_buffer); | |
229 | kfree(dev->interrupt_in_buffer); | |
230 | kfree(dev->interrupt_out_buffer); | |
231 | kfree(dev); | |
232 | } | |
233 | ||
234 | /** | |
235 | * usb_tranzport_interrupt_in_callback | |
236 | */ | |
237 | ||
238 | static void usb_tranzport_interrupt_in_callback(struct urb *urb) | |
239 | { | |
240 | struct usb_tranzport *dev = urb->context; | |
241 | unsigned int next_ring_head; | |
242 | int retval = -1; | |
243 | ||
244 | if (urb->status) { | |
245 | if (urb->status == -ENOENT || | |
7c68d6b7 DT |
246 | urb->status == -ECONNRESET || |
247 | urb->status == -ESHUTDOWN) { | |
8da3dc28 DT |
248 | goto exit; |
249 | } else { | |
7c68d6b7 DT |
250 | dbg_info(&dev->intf->dev, |
251 | "%s: nonzero status received: %d\n", | |
d599edca | 252 | __func__, urb->status); |
7c68d6b7 | 253 | goto resubmit; /* maybe we can recover */ |
8da3dc28 DT |
254 | } |
255 | } | |
256 | ||
257 | if (urb->actual_length != 8) { | |
258 | dev_warn(&dev->intf->dev, | |
7c68d6b7 | 259 | "Urb length was %d bytes!!" |
1e1d25cb | 260 | "Do something intelligent\n", |
7c68d6b7 | 261 | urb->actual_length); |
8da3dc28 | 262 | } else { |
7c68d6b7 DT |
263 | dbg_info(&dev->intf->dev, |
264 | "%s: received: %02x%02x%02x%02x%02x%02x%02x%02x\n", | |
265 | __func__, dev->interrupt_in_buffer[0], | |
266 | dev->interrupt_in_buffer[1], | |
267 | dev->interrupt_in_buffer[2], | |
268 | dev->interrupt_in_buffer[3], | |
269 | dev->interrupt_in_buffer[4], | |
270 | dev->interrupt_in_buffer[5], | |
271 | dev->interrupt_in_buffer[6], | |
272 | dev->interrupt_in_buffer[7]); | |
8da3dc28 | 273 | #if SUPPRESS_EXTRA_OFFLINE_EVENTS |
7c68d6b7 DT |
274 | if (dev->offline == 2 && dev->interrupt_in_buffer[1] == 0xff) |
275 | goto resubmit; | |
276 | if (dev->offline == 1 && dev->interrupt_in_buffer[1] == 0xff) { | |
277 | dev->offline = 2; | |
278 | goto resubmit; | |
279 | } | |
8da3dc28 | 280 | |
7c68d6b7 DT |
281 | /* Always pass one offline event up the stack */ |
282 | if (dev->offline > 0 && dev->interrupt_in_buffer[1] != 0xff) | |
283 | dev->offline = 0; | |
284 | if (dev->offline == 0 && dev->interrupt_in_buffer[1] == 0xff) | |
285 | dev->offline = 1; | |
8da3dc28 | 286 | |
7c68d6b7 DT |
287 | #endif /* SUPPRESS_EXTRA_OFFLINE_EVENTS */ |
288 | dbg_info(&dev->intf->dev, "%s: head, tail are %x, %x\n", | |
289 | __func__, dev->ring_head, dev->ring_tail); | |
8da3dc28 | 290 | |
7c68d6b7 | 291 | next_ring_head = (dev->ring_head + 1) % ring_buffer_size; |
8da3dc28 DT |
292 | |
293 | if (next_ring_head != dev->ring_tail) { | |
7c68d6b7 DT |
294 | memcpy(&((*dev->ring_buffer)[dev->ring_head]), |
295 | dev->interrupt_in_buffer, urb->actual_length); | |
8da3dc28 DT |
296 | dev->ring_head = next_ring_head; |
297 | retval = 0; | |
298 | memset(dev->interrupt_in_buffer, 0, urb->actual_length); | |
299 | } else { | |
300 | dev_warn(&dev->intf->dev, | |
301 | "Ring buffer overflow, %d bytes dropped\n", | |
302 | urb->actual_length); | |
303 | memset(dev->interrupt_in_buffer, 0, urb->actual_length); | |
304 | } | |
305 | } | |
306 | ||
307 | resubmit: | |
7c68d6b7 | 308 | /* resubmit if we're still running */ |
8da3dc28 DT |
309 | if (dev->interrupt_in_running && dev->intf) { |
310 | retval = usb_submit_urb(dev->interrupt_in_urb, GFP_ATOMIC); | |
311 | if (retval) | |
312 | dev_err(&dev->intf->dev, | |
313 | "usb_submit_urb failed (%d)\n", retval); | |
314 | } | |
315 | ||
316 | exit: | |
317 | dev->interrupt_in_done = 1; | |
318 | wake_up_interruptible(&dev->read_wait); | |
319 | } | |
320 | ||
321 | /** | |
322 | * usb_tranzport_interrupt_out_callback | |
323 | */ | |
324 | static void usb_tranzport_interrupt_out_callback(struct urb *urb) | |
325 | { | |
326 | struct usb_tranzport *dev = urb->context; | |
8da3dc28 DT |
327 | /* sync/async unlink faults aren't errors */ |
328 | if (urb->status && !(urb->status == -ENOENT || | |
7c68d6b7 DT |
329 | urb->status == -ECONNRESET || |
330 | urb->status == -ESHUTDOWN)) | |
8da3dc28 | 331 | dbg_info(&dev->intf->dev, |
7c68d6b7 DT |
332 | "%s - nonzero write interrupt status received: %d\n", |
333 | __func__, urb->status); | |
8da3dc28 DT |
334 | |
335 | dev->interrupt_out_busy = 0; | |
336 | wake_up_interruptible(&dev->write_wait); | |
337 | } | |
8da3dc28 DT |
338 | /** |
339 | * usb_tranzport_open | |
340 | */ | |
341 | static int usb_tranzport_open(struct inode *inode, struct file *file) | |
342 | { | |
343 | struct usb_tranzport *dev; | |
344 | int subminor; | |
345 | int retval = 0; | |
346 | struct usb_interface *interface; | |
347 | ||
348 | nonseekable_open(inode, file); | |
349 | subminor = iminor(inode); | |
350 | ||
351 | mutex_lock(&disconnect_mutex); | |
352 | ||
353 | interface = usb_find_interface(&usb_tranzport_driver, subminor); | |
354 | ||
355 | if (!interface) { | |
356 | err("%s - error, can't find device for minor %d\n", | |
7c68d6b7 | 357 | __func__, subminor); |
8da3dc28 DT |
358 | retval = -ENODEV; |
359 | goto unlock_disconnect_exit; | |
360 | } | |
361 | ||
362 | dev = usb_get_intfdata(interface); | |
363 | ||
364 | if (!dev) { | |
365 | retval = -ENODEV; | |
366 | goto unlock_disconnect_exit; | |
367 | } | |
368 | ||
369 | /* lock this device */ | |
3504e0c8 | 370 | if (mutex_lock_interruptible(&dev->mtx)) { |
8da3dc28 DT |
371 | retval = -ERESTARTSYS; |
372 | goto unlock_disconnect_exit; | |
373 | } | |
374 | ||
375 | /* allow opening only once */ | |
376 | if (dev->open_count) { | |
377 | retval = -EBUSY; | |
378 | goto unlock_exit; | |
379 | } | |
380 | dev->open_count = 1; | |
381 | ||
382 | /* initialize in direction */ | |
383 | dev->ring_head = 0; | |
384 | dev->ring_tail = 0; | |
385 | usb_fill_int_urb(dev->interrupt_in_urb, | |
7c68d6b7 DT |
386 | interface_to_usbdev(interface), |
387 | usb_rcvintpipe(interface_to_usbdev(interface), | |
388 | dev->interrupt_in_endpoint-> | |
389 | bEndpointAddress), | |
390 | dev->interrupt_in_buffer, | |
391 | dev->interrupt_in_endpoint_size, | |
392 | usb_tranzport_interrupt_in_callback, dev, | |
393 | dev->interrupt_in_interval); | |
8da3dc28 DT |
394 | |
395 | dev->interrupt_in_running = 1; | |
396 | dev->interrupt_in_done = 0; | |
397 | dev->enable = 1; | |
398 | dev->offline = 0; | |
399 | dev->compress_wheel = 1; | |
400 | ||
401 | retval = usb_submit_urb(dev->interrupt_in_urb, GFP_KERNEL); | |
402 | if (retval) { | |
7c68d6b7 DT |
403 | dev_err(&interface->dev, |
404 | "Couldn't submit interrupt_in_urb %d\n", retval); | |
8da3dc28 DT |
405 | dev->interrupt_in_running = 0; |
406 | dev->open_count = 0; | |
407 | goto unlock_exit; | |
408 | } | |
409 | ||
410 | /* save device in the file's private structure */ | |
411 | file->private_data = dev; | |
412 | ||
8da3dc28 | 413 | unlock_exit: |
3504e0c8 | 414 | mutex_unlock(&dev->mtx); |
8da3dc28 DT |
415 | |
416 | unlock_disconnect_exit: | |
417 | mutex_unlock(&disconnect_mutex); | |
418 | ||
419 | return retval; | |
420 | } | |
421 | ||
422 | /** | |
423 | * usb_tranzport_release | |
424 | */ | |
425 | static int usb_tranzport_release(struct inode *inode, struct file *file) | |
426 | { | |
427 | struct usb_tranzport *dev; | |
428 | int retval = 0; | |
429 | ||
430 | dev = file->private_data; | |
431 | ||
432 | if (dev == NULL) { | |
433 | retval = -ENODEV; | |
434 | goto exit; | |
435 | } | |
436 | ||
3504e0c8 | 437 | if (mutex_lock_interruptible(&dev->mtx)) { |
8da3dc28 DT |
438 | retval = -ERESTARTSYS; |
439 | goto exit; | |
440 | } | |
441 | ||
442 | if (dev->open_count != 1) { | |
443 | retval = -ENODEV; | |
444 | goto unlock_exit; | |
445 | } | |
446 | ||
447 | if (dev->intf == NULL) { | |
448 | /* the device was unplugged before the file was released */ | |
3504e0c8 | 449 | mutex_unlock(&dev->mtx); |
8da3dc28 DT |
450 | /* unlock here as usb_tranzport_delete frees dev */ |
451 | usb_tranzport_delete(dev); | |
452 | retval = -ENODEV; | |
453 | goto exit; | |
454 | } | |
455 | ||
456 | /* wait until write transfer is finished */ | |
457 | if (dev->interrupt_out_busy) | |
7c68d6b7 DT |
458 | wait_event_interruptible_timeout(dev->write_wait, |
459 | !dev->interrupt_out_busy, | |
460 | 2 * HZ); | |
8da3dc28 DT |
461 | usb_tranzport_abort_transfers(dev); |
462 | dev->open_count = 0; | |
463 | ||
464 | unlock_exit: | |
3504e0c8 | 465 | mutex_unlock(&dev->mtx); |
8da3dc28 DT |
466 | |
467 | exit: | |
468 | return retval; | |
469 | } | |
470 | ||
471 | /** | |
472 | * usb_tranzport_poll | |
473 | */ | |
7c68d6b7 | 474 | static unsigned int usb_tranzport_poll(struct file *file, poll_table * wait) |
8da3dc28 DT |
475 | { |
476 | struct usb_tranzport *dev; | |
477 | unsigned int mask = 0; | |
8da3dc28 | 478 | dev = file->private_data; |
8da3dc28 DT |
479 | poll_wait(file, &dev->read_wait, wait); |
480 | poll_wait(file, &dev->write_wait, wait); | |
8da3dc28 DT |
481 | if (dev->ring_head != dev->ring_tail) |
482 | mask |= POLLIN | POLLRDNORM; | |
483 | if (!dev->interrupt_out_busy) | |
484 | mask |= POLLOUT | POLLWRNORM; | |
8da3dc28 DT |
485 | return mask; |
486 | } | |
8da3dc28 DT |
487 | /** |
488 | * usb_tranzport_read | |
489 | */ | |
7c68d6b7 DT |
490 | |
491 | static ssize_t usb_tranzport_read(struct file *file, char __user *buffer, | |
492 | size_t count, loff_t *ppos) | |
8da3dc28 DT |
493 | { |
494 | struct usb_tranzport *dev; | |
8da3dc28 | 495 | int retval = 0; |
8da3dc28 DT |
496 | #if BUFFERED_READS |
497 | int c = 0; | |
498 | #endif | |
8da3dc28 DT |
499 | #if COMPRESS_WHEEL_EVENTS |
500 | signed char oldwheel; | |
501 | signed char newwheel; | |
502 | int cancompress = 1; | |
503 | int next_tail; | |
504 | #endif | |
505 | ||
7c68d6b7 | 506 | /* do I have such a thing as a null event? */ |
8da3dc28 DT |
507 | |
508 | dev = file->private_data; | |
509 | ||
510 | /* verify that we actually have some data to read */ | |
511 | if (count == 0) | |
512 | goto exit; | |
513 | ||
514 | /* lock this object */ | |
3504e0c8 | 515 | if (mutex_lock_interruptible(&dev->mtx)) { |
8da3dc28 DT |
516 | retval = -ERESTARTSYS; |
517 | goto exit; | |
518 | } | |
519 | ||
7c68d6b7 | 520 | /* verify that the device wasn't unplugged */ if (dev->intf == NULL) { |
8da3dc28 DT |
521 | retval = -ENODEV; |
522 | err("No device or device unplugged %d\n", retval); | |
523 | goto unlock_exit; | |
524 | } | |
525 | ||
526 | while (dev->ring_head == dev->ring_tail) { | |
527 | ||
528 | if (file->f_flags & O_NONBLOCK) { | |
529 | retval = -EAGAIN; | |
530 | goto unlock_exit; | |
531 | } | |
7c68d6b7 DT |
532 | /* tiny race - FIXME: make atomic? */ |
533 | /* atomic_cmp_exchange(&dev->interrupt_in_done,0,0); */ | |
534 | dev->interrupt_in_done = 0; | |
535 | retval = wait_event_interruptible(dev->read_wait, | |
536 | dev->interrupt_in_done); | |
537 | if (retval < 0) | |
8da3dc28 | 538 | goto unlock_exit; |
8da3dc28 DT |
539 | } |
540 | ||
7c68d6b7 DT |
541 | dbg_info(&dev->intf->dev, |
542 | "%s: copying to userspace: " | |
543 | "%02x%02x%02x%02x%02x%02x%02x%02x\n", | |
544 | __func__, | |
545 | (*dev->ring_buffer)[dev->ring_tail].cmd[0], | |
546 | (*dev->ring_buffer)[dev->ring_tail].cmd[1], | |
547 | (*dev->ring_buffer)[dev->ring_tail].cmd[2], | |
548 | (*dev->ring_buffer)[dev->ring_tail].cmd[3], | |
549 | (*dev->ring_buffer)[dev->ring_tail].cmd[4], | |
550 | (*dev->ring_buffer)[dev->ring_tail].cmd[5], | |
551 | (*dev->ring_buffer)[dev->ring_tail].cmd[6], | |
552 | (*dev->ring_buffer)[dev->ring_tail].cmd[7]); | |
8da3dc28 DT |
553 | |
554 | #if BUFFERED_READS | |
7c68d6b7 DT |
555 | c = 0; |
556 | while ((c < count) && (dev->ring_tail != dev->ring_head)) { | |
8da3dc28 | 557 | |
8da3dc28 DT |
558 | #if COMPRESS_WHEEL_EVENTS |
559 | next_tail = (dev->ring_tail+1) % ring_buffer_size; | |
7c68d6b7 DT |
560 | if (dev->compress_wheel) |
561 | cancompress = 1; | |
562 | while (dev->ring_head != next_tail && cancompress == 1) { | |
8da3dc28 DT |
563 | newwheel = (*dev->ring_buffer)[next_tail].cmd[6]; |
564 | oldwheel = (*dev->ring_buffer)[dev->ring_tail].cmd[6]; | |
7c68d6b7 DT |
565 | /* if both are wheel events, and |
566 | no buttons have changes (FIXME, do I have to check?), | |
567 | and we are the same sign, we can compress +- 7F | |
568 | */ | |
569 | dbg_info(&dev->intf->dev, | |
570 | "%s: trying to compress: " | |
571 | "%02x%02x%02x%02x%02x%02x%02x%02x\n", | |
572 | __func__, | |
573 | (*dev->ring_buffer)[dev->ring_tail].cmd[0], | |
574 | (*dev->ring_buffer)[dev->ring_tail].cmd[1], | |
575 | (*dev->ring_buffer)[dev->ring_tail].cmd[2], | |
576 | (*dev->ring_buffer)[dev->ring_tail].cmd[3], | |
577 | (*dev->ring_buffer)[dev->ring_tail].cmd[4], | |
578 | (*dev->ring_buffer)[dev->ring_tail].cmd[5], | |
579 | (*dev->ring_buffer)[dev->ring_tail].cmd[6], | |
580 | (*dev->ring_buffer)[dev->ring_tail].cmd[7]); | |
581 | ||
582 | if (((*dev->ring_buffer)[dev->ring_tail].cmd[6] != 0 && | |
583 | (*dev->ring_buffer)[next_tail].cmd[6] != 0) && | |
8da3dc28 | 584 | ((newwheel > 0 && oldwheel > 0) || |
7c68d6b7 DT |
585 | (newwheel < 0 && oldwheel < 0)) && |
586 | ((*dev->ring_buffer)[dev->ring_tail].cmd[2] == | |
587 | (*dev->ring_buffer)[next_tail].cmd[2]) && | |
588 | ((*dev->ring_buffer)[dev->ring_tail].cmd[3] == | |
589 | (*dev->ring_buffer)[next_tail].cmd[3]) && | |
590 | ((*dev->ring_buffer)[dev->ring_tail].cmd[4] == | |
591 | (*dev->ring_buffer)[next_tail].cmd[4]) && | |
592 | ((*dev->ring_buffer)[dev->ring_tail].cmd[5] == | |
593 | (*dev->ring_buffer)[next_tail].cmd[5])) { | |
594 | dbg_info(&dev->intf->dev, | |
595 | "%s: should compress: " | |
596 | "%02x%02x%02x%02x%02x%02x%02x%02x\n", | |
597 | __func__, | |
598 | (*dev->ring_buffer)[dev->ring_tail]. | |
599 | cmd[0], | |
600 | (*dev->ring_buffer)[dev->ring_tail]. | |
601 | cmd[1], | |
602 | (*dev->ring_buffer)[dev->ring_tail]. | |
603 | cmd[2], | |
604 | (*dev->ring_buffer)[dev->ring_tail]. | |
605 | cmd[3], | |
606 | (*dev->ring_buffer)[dev->ring_tail]. | |
607 | cmd[4], | |
608 | (*dev->ring_buffer)[dev->ring_tail]. | |
609 | cmd[5], | |
610 | (*dev->ring_buffer)[dev->ring_tail]. | |
611 | cmd[6], | |
612 | (*dev->ring_buffer)[dev->ring_tail]. | |
613 | cmd[7]); | |
8da3dc28 | 614 | newwheel += oldwheel; |
7c68d6b7 | 615 | if (oldwheel > 0 && !(newwheel > 0)) { |
8da3dc28 DT |
616 | newwheel = 0x7f; |
617 | cancompress = 0; | |
618 | } | |
7c68d6b7 | 619 | if (oldwheel < 0 && !(newwheel < 0)) { |
8da3dc28 DT |
620 | newwheel = 0x80; |
621 | cancompress = 0; | |
622 | } | |
623 | ||
7c68d6b7 DT |
624 | (*dev->ring_buffer)[next_tail].cmd[6] = |
625 | newwheel; | |
8da3dc28 | 626 | dev->ring_tail = next_tail; |
7c68d6b7 DT |
627 | next_tail = |
628 | (dev->ring_tail + 1) % ring_buffer_size; | |
8da3dc28 DT |
629 | } else { |
630 | cancompress = 0; | |
631 | } | |
632 | } | |
633 | #endif /* COMPRESS_WHEEL_EVENTS */ | |
7c68d6b7 DT |
634 | if (copy_to_user( |
635 | &buffer[c], | |
636 | &(*dev->ring_buffer)[dev->ring_tail], 8)) { | |
8da3dc28 DT |
637 | retval = -EFAULT; |
638 | goto unlock_exit; | |
639 | } | |
7c68d6b7 DT |
640 | dev->ring_tail = (dev->ring_tail + 1) % ring_buffer_size; |
641 | c += 8; | |
642 | dbg_info(&dev->intf->dev, | |
643 | "%s: head, tail are %x, %x\n", | |
644 | __func__, dev->ring_head, dev->ring_tail); | |
645 | } | |
646 | retval = c; | |
8da3dc28 DT |
647 | |
648 | #else | |
7c68d6b7 DT |
649 | /* if (copy_to_user(buffer, &(*dev->ring_buffer)[dev->ring_tail], 8)) { */ |
650 | retval = -EFAULT; | |
651 | goto unlock_exit; | |
652 | } | |
8da3dc28 | 653 | |
7c68d6b7 DT |
654 | dev->ring_tail = (dev->ring_tail + 1) % ring_buffer_size; |
655 | dbg_info(&dev->intf->dev, "%s: head, tail are %x, %x\n", | |
656 | __func__, dev->ring_head, dev->ring_tail); | |
8da3dc28 | 657 | |
7c68d6b7 | 658 | retval = 8; |
8da3dc28 DT |
659 | #endif /* BUFFERED_READS */ |
660 | ||
661 | unlock_exit: | |
7c68d6b7 | 662 | /* unlock the device */ |
3504e0c8 | 663 | mutex_unlock(&dev->mtx); |
8da3dc28 DT |
664 | |
665 | exit: | |
7c68d6b7 | 666 | return retval; |
8da3dc28 DT |
667 | } |
668 | ||
669 | /** | |
670 | * usb_tranzport_write | |
671 | */ | |
7c68d6b7 DT |
672 | static ssize_t usb_tranzport_write(struct file *file, |
673 | const char __user *buffer, size_t count, | |
674 | loff_t *ppos) | |
8da3dc28 DT |
675 | { |
676 | struct usb_tranzport *dev; | |
677 | size_t bytes_to_write; | |
678 | int retval = 0; | |
679 | ||
680 | dev = file->private_data; | |
681 | ||
682 | /* verify that we actually have some data to write */ | |
683 | if (count == 0) | |
684 | goto exit; | |
685 | ||
686 | /* lock this object */ | |
3504e0c8 | 687 | if (mutex_lock_interruptible(&dev->mtx)) { |
8da3dc28 DT |
688 | retval = -ERESTARTSYS; |
689 | goto exit; | |
690 | } | |
8da3dc28 DT |
691 | /* verify that the device wasn't unplugged */ |
692 | if (dev->intf == NULL) { | |
693 | retval = -ENODEV; | |
694 | err("No device or device unplugged %d\n", retval); | |
695 | goto unlock_exit; | |
696 | } | |
697 | ||
698 | /* wait until previous transfer is finished */ | |
699 | if (dev->interrupt_out_busy) { | |
700 | if (file->f_flags & O_NONBLOCK) { | |
701 | retval = -EAGAIN; | |
702 | goto unlock_exit; | |
703 | } | |
7c68d6b7 DT |
704 | retval = wait_event_interruptible(dev->write_wait, |
705 | !dev->interrupt_out_busy); | |
706 | if (retval < 0) | |
8da3dc28 | 707 | goto unlock_exit; |
8da3dc28 DT |
708 | } |
709 | ||
710 | /* write the data into interrupt_out_buffer from userspace */ | |
7c68d6b7 DT |
711 | bytes_to_write = min(count, |
712 | write_buffer_size * | |
713 | dev->interrupt_out_endpoint_size); | |
8da3dc28 | 714 | if (bytes_to_write < count) |
7c68d6b7 DT |
715 | dev_warn(&dev->intf->dev, |
716 | "Write buffer overflow, %zd bytes dropped\n", | |
717 | count - bytes_to_write); | |
8da3dc28 | 718 | |
7c68d6b7 DT |
719 | dbg_info(&dev->intf->dev, |
720 | "%s: count = %zd, bytes_to_write = %zd\n", __func__, | |
721 | count, bytes_to_write); | |
8da3dc28 DT |
722 | |
723 | if (copy_from_user(dev->interrupt_out_buffer, buffer, bytes_to_write)) { | |
724 | retval = -EFAULT; | |
725 | goto unlock_exit; | |
726 | } | |
727 | ||
728 | if (dev->interrupt_out_endpoint == NULL) { | |
1e1d25cb | 729 | err("Endpoint should not be be null!\n"); |
8da3dc28 DT |
730 | goto unlock_exit; |
731 | } | |
732 | ||
733 | /* send off the urb */ | |
734 | usb_fill_int_urb(dev->interrupt_out_urb, | |
7c68d6b7 DT |
735 | interface_to_usbdev(dev->intf), |
736 | usb_sndintpipe(interface_to_usbdev(dev->intf), | |
737 | dev->interrupt_out_endpoint-> | |
738 | bEndpointAddress), | |
739 | dev->interrupt_out_buffer, bytes_to_write, | |
740 | usb_tranzport_interrupt_out_callback, dev, | |
741 | dev->interrupt_out_interval); | |
8da3dc28 DT |
742 | |
743 | dev->interrupt_out_busy = 1; | |
744 | wmb(); | |
745 | ||
746 | retval = usb_submit_urb(dev->interrupt_out_urb, GFP_KERNEL); | |
747 | if (retval) { | |
748 | dev->interrupt_out_busy = 0; | |
749 | err("Couldn't submit interrupt_out_urb %d\n", retval); | |
750 | goto unlock_exit; | |
751 | } | |
752 | retval = bytes_to_write; | |
753 | ||
754 | unlock_exit: | |
755 | /* unlock the device */ | |
3504e0c8 | 756 | mutex_unlock(&dev->mtx); |
8da3dc28 DT |
757 | |
758 | exit: | |
759 | return retval; | |
760 | } | |
761 | ||
762 | /* file operations needed when we register this driver */ | |
763 | static const struct file_operations usb_tranzport_fops = { | |
7c68d6b7 DT |
764 | .owner = THIS_MODULE, |
765 | .read = usb_tranzport_read, | |
766 | .write = usb_tranzport_write, | |
767 | .open = usb_tranzport_open, | |
768 | .release = usb_tranzport_release, | |
769 | .poll = usb_tranzport_poll, | |
6038f373 | 770 | .llseek = no_llseek, |
8da3dc28 DT |
771 | }; |
772 | ||
773 | /* | |
774 | * usb class driver info in order to get a minor number from the usb core, | |
775 | * and to have the device registered with the driver core | |
776 | */ | |
777 | static struct usb_class_driver usb_tranzport_class = { | |
7c68d6b7 DT |
778 | .name = "tranzport%d", |
779 | .fops = &usb_tranzport_fops, | |
780 | .minor_base = USB_TRANZPORT_MINOR_BASE, | |
8da3dc28 DT |
781 | }; |
782 | ||
8da3dc28 DT |
783 | /** |
784 | * usb_tranzport_probe | |
785 | * | |
786 | * Called by the usb core when a new device is connected that it thinks | |
787 | * this driver might be interested in. | |
788 | */ | |
7c68d6b7 DT |
789 | static int usb_tranzport_probe(struct usb_interface *intf, |
790 | const struct usb_device_id *id) { | |
8da3dc28 DT |
791 | struct usb_device *udev = interface_to_usbdev(intf); |
792 | struct usb_tranzport *dev = NULL; | |
793 | struct usb_host_interface *iface_desc; | |
794 | struct usb_endpoint_descriptor *endpoint; | |
795 | int i; | |
796 | int true_size; | |
797 | int retval = -ENOMEM; | |
798 | ||
9b0131cb | 799 | /* allocate memory for our device state and initialize it */ |
8da3dc28 | 800 | |
7c68d6b7 | 801 | dev = kzalloc(sizeof(*dev), GFP_KERNEL); |
8da3dc28 DT |
802 | if (dev == NULL) { |
803 | dev_err(&intf->dev, "Out of memory\n"); | |
804 | goto exit; | |
805 | } | |
3504e0c8 | 806 | mutex_init(&dev->mtx); |
8da3dc28 DT |
807 | dev->intf = intf; |
808 | init_waitqueue_head(&dev->read_wait); | |
809 | init_waitqueue_head(&dev->write_wait); | |
810 | ||
811 | iface_desc = intf->cur_altsetting; | |
812 | ||
813 | /* set up the endpoint information */ | |
814 | for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) { | |
815 | endpoint = &iface_desc->endpoint[i].desc; | |
816 | ||
817 | if (usb_endpoint_is_int_in(endpoint)) | |
818 | dev->interrupt_in_endpoint = endpoint; | |
819 | ||
820 | if (usb_endpoint_is_int_out(endpoint)) | |
821 | dev->interrupt_out_endpoint = endpoint; | |
822 | } | |
823 | if (dev->interrupt_in_endpoint == NULL) { | |
824 | dev_err(&intf->dev, "Interrupt in endpoint not found\n"); | |
825 | goto error; | |
826 | } | |
827 | if (dev->interrupt_out_endpoint == NULL) | |
7c68d6b7 DT |
828 | dev_warn(&intf->dev, |
829 | "Interrupt out endpoint not found" | |
830 | "(using control endpoint instead)\n"); | |
8da3dc28 | 831 | |
7c68d6b7 DT |
832 | dev->interrupt_in_endpoint_size = |
833 | le16_to_cpu(dev->interrupt_in_endpoint->wMaxPacketSize); | |
8da3dc28 DT |
834 | |
835 | if (dev->interrupt_in_endpoint_size != 8) | |
7c68d6b7 | 836 | dev_warn(&intf->dev, "Interrupt in endpoint size is not 8!\n"); |
8da3dc28 | 837 | |
7c68d6b7 DT |
838 | if (ring_buffer_size == 0) |
839 | ring_buffer_size = RING_BUFFER_SIZE; | |
840 | true_size = min(ring_buffer_size, RING_BUFFER_SIZE); | |
8da3dc28 | 841 | |
7c68d6b7 DT |
842 | /* FIXME - there are more usb_alloc routines for dma correctness. |
843 | Needed? */ | |
844 | ||
845 | dev->ring_buffer = | |
846 | kmalloc((true_size * sizeof(struct tranzport_cmd)) + 8, GFP_KERNEL); | |
8da3dc28 DT |
847 | |
848 | if (!dev->ring_buffer) { | |
7c68d6b7 DT |
849 | dev_err(&intf->dev, |
850 | "Couldn't allocate ring_buffer size %d\n", true_size); | |
8da3dc28 DT |
851 | goto error; |
852 | } | |
7c68d6b7 DT |
853 | dev->interrupt_in_buffer = |
854 | kmalloc(dev->interrupt_in_endpoint_size, GFP_KERNEL); | |
8da3dc28 DT |
855 | if (!dev->interrupt_in_buffer) { |
856 | dev_err(&intf->dev, "Couldn't allocate interrupt_in_buffer\n"); | |
857 | goto error; | |
858 | } | |
859 | dev->interrupt_in_urb = usb_alloc_urb(0, GFP_KERNEL); | |
860 | if (!dev->interrupt_in_urb) { | |
861 | dev_err(&intf->dev, "Couldn't allocate interrupt_in_urb\n"); | |
862 | goto error; | |
863 | } | |
7c68d6b7 DT |
864 | dev->interrupt_out_endpoint_size = |
865 | dev->interrupt_out_endpoint ? | |
866 | le16_to_cpu(dev->interrupt_out_endpoint->wMaxPacketSize) : | |
867 | udev->descriptor.bMaxPacketSize0; | |
868 | ||
869 | if (dev->interrupt_out_endpoint_size != 8) | |
870 | dev_warn(&intf->dev, | |
871 | "Interrupt out endpoint size is not 8!)\n"); | |
872 | ||
873 | dev->interrupt_out_buffer = | |
874 | kmalloc(write_buffer_size * dev->interrupt_out_endpoint_size, | |
875 | GFP_KERNEL); | |
8da3dc28 DT |
876 | if (!dev->interrupt_out_buffer) { |
877 | dev_err(&intf->dev, "Couldn't allocate interrupt_out_buffer\n"); | |
878 | goto error; | |
879 | } | |
880 | dev->interrupt_out_urb = usb_alloc_urb(0, GFP_KERNEL); | |
881 | if (!dev->interrupt_out_urb) { | |
882 | dev_err(&intf->dev, "Couldn't allocate interrupt_out_urb\n"); | |
883 | goto error; | |
884 | } | |
7c68d6b7 DT |
885 | dev->interrupt_in_interval = |
886 | min_interrupt_in_interval > | |
887 | dev->interrupt_in_endpoint->bInterval ? min_interrupt_in_interval | |
888 | : dev->interrupt_in_endpoint->bInterval; | |
889 | ||
890 | if (dev->interrupt_out_endpoint) { | |
891 | dev->interrupt_out_interval = | |
892 | min_interrupt_out_interval > | |
893 | dev->interrupt_out_endpoint->bInterval ? | |
894 | min_interrupt_out_interval : | |
895 | dev->interrupt_out_endpoint->bInterval; | |
896 | } | |
8da3dc28 DT |
897 | |
898 | /* we can register the device now, as it is ready */ | |
899 | usb_set_intfdata(intf, dev); | |
900 | ||
901 | retval = usb_register_dev(intf, &usb_tranzport_class); | |
902 | if (retval) { | |
903 | /* something prevented us from registering this driver */ | |
7c68d6b7 DT |
904 | dev_err(&intf->dev, |
905 | "Not able to get a minor for this device.\n"); | |
8da3dc28 DT |
906 | usb_set_intfdata(intf, NULL); |
907 | goto error; | |
908 | } | |
909 | ||
7c68d6b7 DT |
910 | retval = device_create_file(&intf->dev, &dev_attr_compress_wheel); |
911 | if (retval) | |
912 | goto error; | |
913 | retval = device_create_file(&intf->dev, &dev_attr_enable); | |
914 | if (retval) | |
915 | goto error; | |
916 | retval = device_create_file(&intf->dev, &dev_attr_offline); | |
917 | if (retval) | |
918 | goto error; | |
8da3dc28 DT |
919 | |
920 | /* let the user know what node this device is now attached to */ | |
7c68d6b7 DT |
921 | dev_info(&intf->dev, |
922 | "Tranzport Device #%d now attached to major %d minor %d\n", | |
923 | (intf->minor - USB_TRANZPORT_MINOR_BASE), USB_MAJOR, | |
924 | intf->minor); | |
8da3dc28 DT |
925 | |
926 | exit: | |
927 | return retval; | |
928 | ||
929 | error: | |
930 | usb_tranzport_delete(dev); | |
8da3dc28 DT |
931 | return retval; |
932 | } | |
933 | ||
934 | /** | |
935 | * usb_tranzport_disconnect | |
936 | * | |
937 | * Called by the usb core when the device is removed from the system. | |
938 | */ | |
939 | static void usb_tranzport_disconnect(struct usb_interface *intf) | |
940 | { | |
941 | struct usb_tranzport *dev; | |
942 | int minor; | |
943 | mutex_lock(&disconnect_mutex); | |
944 | dev = usb_get_intfdata(intf); | |
945 | usb_set_intfdata(intf, NULL); | |
3504e0c8 | 946 | mutex_lock(&dev->mtx); |
8da3dc28 DT |
947 | minor = intf->minor; |
948 | /* give back our minor */ | |
949 | usb_deregister_dev(intf, &usb_tranzport_class); | |
950 | ||
951 | /* if the device is not opened, then we clean up right now */ | |
952 | if (!dev->open_count) { | |
3504e0c8 | 953 | mutex_unlock(&dev->mtx); |
8da3dc28 DT |
954 | usb_tranzport_delete(dev); |
955 | } else { | |
956 | dev->intf = NULL; | |
3504e0c8 | 957 | mutex_unlock(&dev->mtx); |
8da3dc28 DT |
958 | } |
959 | ||
960 | mutex_unlock(&disconnect_mutex); | |
961 | ||
962 | dev_info(&intf->dev, "Tranzport Surface #%d now disconnected\n", | |
7c68d6b7 | 963 | (minor - USB_TRANZPORT_MINOR_BASE)); |
8da3dc28 DT |
964 | } |
965 | ||
966 | /* usb specific object needed to register this driver with the usb subsystem */ | |
967 | static struct usb_driver usb_tranzport_driver = { | |
7c68d6b7 DT |
968 | .name = "tranzport", |
969 | .probe = usb_tranzport_probe, | |
970 | .disconnect = usb_tranzport_disconnect, | |
971 | .id_table = usb_tranzport_table, | |
8da3dc28 DT |
972 | }; |
973 | ||
974 | /** | |
975 | * usb_tranzport_init | |
976 | */ | |
977 | static int __init usb_tranzport_init(void) | |
978 | { | |
979 | int retval; | |
980 | ||
981 | /* register this driver with the USB subsystem */ | |
982 | retval = usb_register(&usb_tranzport_driver); | |
983 | if (retval) | |
7c68d6b7 DT |
984 | err("usb_register failed for the " __FILE__ |
985 | " driver. Error number %d\n", retval); | |
8da3dc28 DT |
986 | return retval; |
987 | } | |
8da3dc28 DT |
988 | /** |
989 | * usb_tranzport_exit | |
990 | */ | |
7c68d6b7 | 991 | |
8da3dc28 DT |
992 | static void __exit usb_tranzport_exit(void) |
993 | { | |
994 | /* deregister this driver with the USB subsystem */ | |
995 | usb_deregister(&usb_tranzport_driver); | |
996 | } | |
997 | ||
998 | module_init(usb_tranzport_init); | |
999 | module_exit(usb_tranzport_exit); |