| Commit | Line | Data |
|---|---|---|
| 1550dfd9 MD |
1 | /* |
| 2 | * $FreeBSD: src/sys/dev/usb/ums.c,v 1.64 2003/11/09 09:17:22 tanimura Exp $ | |
| 74781d8f | 3 | * $DragonFly: src/sys/dev/usbmisc/ums/ums.c,v 1.31 2008/08/14 20:55:54 hasso Exp $ |
| 1550dfd9 | 4 | */ |
| 984263bc MD |
5 | |
| 6 | /* | |
| 7 | * Copyright (c) 1998 The NetBSD Foundation, Inc. | |
| 8 | * All rights reserved. | |
| 9 | * | |
| 10 | * This code is derived from software contributed to The NetBSD Foundation | |
| 11 | * by Lennart Augustsson (lennart@augustsson.net) at | |
| 12 | * Carlstedt Research & Technology. | |
| 13 | * | |
| 14 | * Redistribution and use in source and binary forms, with or without | |
| 15 | * modification, are permitted provided that the following conditions | |
| 16 | * are met: | |
| 17 | * 1. Redistributions of source code must retain the above copyright | |
| 18 | * notice, this list of conditions and the following disclaimer. | |
| 19 | * 2. Redistributions in binary form must reproduce the above copyright | |
| 20 | * notice, this list of conditions and the following disclaimer in the | |
| 21 | * documentation and/or other materials provided with the distribution. | |
| 22 | * 3. All advertising materials mentioning features or use of this software | |
| 23 | * must display the following acknowledgement: | |
| 24 | * This product includes software developed by the NetBSD | |
| 25 | * Foundation, Inc. and its contributors. | |
| 26 | * 4. Neither the name of The NetBSD Foundation nor the names of its | |
| 27 | * contributors may be used to endorse or promote products derived | |
| 28 | * from this software without specific prior written permission. | |
| 29 | * | |
| 30 | * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS | |
| 31 | * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED | |
| 32 | * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| 33 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS | |
| 34 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| 35 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| 36 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| 37 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| 38 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| 39 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| 40 | * POSSIBILITY OF SUCH DAMAGE. | |
| 41 | */ | |
| 42 | ||
| 43 | /* | |
| 1550dfd9 | 44 | * HID spec: http://www.usb.org/developers/data/devclass/hid1_1.pdf |
| 984263bc MD |
45 | */ |
| 46 | ||
| 47 | #include <sys/param.h> | |
| 48 | #include <sys/systm.h> | |
| 49 | #include <sys/kernel.h> | |
| 50 | #include <sys/malloc.h> | |
| 51 | #include <sys/module.h> | |
| 52 | #include <sys/bus.h> | |
| 984263bc MD |
53 | #include <sys/conf.h> |
| 54 | #include <sys/tty.h> | |
| 55 | #include <sys/file.h> | |
| 56 | #include <sys/select.h> | |
| 984263bc MD |
57 | #include <sys/vnode.h> |
| 58 | #include <sys/poll.h> | |
| 96bcde04 | 59 | #include <sys/event.h> |
| 984263bc | 60 | #include <sys/sysctl.h> |
| 4e01b467 | 61 | #include <sys/thread2.h> |
| 984263bc | 62 | |
| 1f2de5d4 MD |
63 | #include <bus/usb/usb.h> |
| 64 | #include <bus/usb/usbhid.h> | |
| 984263bc | 65 | |
| 1f2de5d4 MD |
66 | #include <bus/usb/usbdi.h> |
| 67 | #include <bus/usb/usbdi_util.h> | |
| 1f2de5d4 MD |
68 | #include <bus/usb/usb_quirks.h> |
| 69 | #include <bus/usb/hid.h> | |
| 984263bc MD |
70 | |
| 71 | #include <machine/mouse.h> | |
| 72 | ||
| 73 | #ifdef USB_DEBUG | |
| fc1ef497 HT |
74 | #define DPRINTF(x) if (umsdebug) kprintf x |
| 75 | #define DPRINTFN(n,x) if (umsdebug>(n)) kprintf x | |
| 984263bc MD |
76 | int umsdebug = 0; |
| 77 | SYSCTL_NODE(_hw_usb, OID_AUTO, ums, CTLFLAG_RW, 0, "USB ums"); | |
| 78 | SYSCTL_INT(_hw_usb_ums, OID_AUTO, debug, CTLFLAG_RW, | |
| 79 | &umsdebug, 0, "ums debug level"); | |
| 80 | #else | |
| 81 | #define DPRINTF(x) | |
| 82 | #define DPRINTFN(n,x) | |
| 83 | #endif | |
| 84 | ||
| 85 | #define UMSUNIT(s) (minor(s)&0x1f) | |
| 86 | ||
| 1550dfd9 | 87 | #define MS_TO_TICKS(ms) ((ms) * hz / 1000) |
| 984263bc MD |
88 | |
| 89 | #define QUEUE_BUFSIZE 400 /* MUST be divisible by 5 _and_ 8 */ | |
| 90 | ||
| 91 | struct ums_softc { | |
| 92 | device_t sc_dev; /* base device */ | |
| 93 | usbd_interface_handle sc_iface; /* interface */ | |
| 94 | usbd_pipe_handle sc_intrpipe; /* interrupt pipe */ | |
| 95 | int sc_ep_addr; | |
| 96 | ||
| 97 | u_char *sc_ibuf; | |
| 98 | u_int8_t sc_iid; | |
| 99 | int sc_isize; | |
| 100 | struct hid_location sc_loc_x, sc_loc_y, sc_loc_z; | |
| 101 | struct hid_location *sc_loc_btn; | |
| 102 | ||
| 8c64e381 | 103 | struct callout sc_timeout; /* for spurious button ups */ |
| 984263bc MD |
104 | |
| 105 | int sc_enabled; | |
| 106 | int sc_disconnected; /* device is gone */ | |
| 107 | ||
| 108 | int flags; /* device configuration */ | |
| 109 | #define UMS_Z 0x01 /* z direction available */ | |
| 110 | #define UMS_SPUR_BUT_UP 0x02 /* spurious button up events */ | |
| 111 | int nbuttons; | |
| bc5a1c0b | 112 | #define MAX_BUTTONS 31 /* must not exceed size of sc_buttons */ |
| 984263bc MD |
113 | |
| 114 | u_char qbuf[QUEUE_BUFSIZE]; /* must be divisable by 3&4 */ | |
| 115 | u_char dummy[100]; /* XXX just for safety and for now */ | |
| 116 | int qcount, qhead, qtail; | |
| 117 | mousehw_t hw; | |
| 118 | mousemode_t mode; | |
| 119 | mousestatus_t status; | |
| 120 | ||
| 121 | int state; | |
| 122 | # define UMS_ASLEEP 0x01 /* readFromDevice is waiting */ | |
| 984263bc | 123 | struct selinfo rsel; /* process waiting in select */ |
| 984263bc MD |
124 | }; |
| 125 | ||
| 126 | #define MOUSE_FLAGS_MASK (HIO_CONST|HIO_RELATIVE) | |
| 127 | #define MOUSE_FLAGS (HIO_RELATIVE) | |
| 128 | ||
| 6ed427ca | 129 | static void ums_intr(usbd_xfer_handle xfer, |
| 984263bc MD |
130 | usbd_private_handle priv, usbd_status status); |
| 131 | ||
| 6ed427ca | 132 | static void ums_add_to_queue(struct ums_softc *sc, |
| 984263bc | 133 | int dx, int dy, int dz, int buttons); |
| 6ed427ca | 134 | static void ums_add_to_queue_timeout(void *priv); |
| 984263bc | 135 | |
| 6ed427ca HT |
136 | static int ums_enable(void *); |
| 137 | static void ums_disable(void *); | |
| 984263bc | 138 | |
| 6ed427ca HT |
139 | static d_open_t ums_open; |
| 140 | static d_close_t ums_close; | |
| 141 | static d_read_t ums_read; | |
| 142 | static d_ioctl_t ums_ioctl; | |
| 143 | static d_poll_t ums_poll; | |
| 96bcde04 SG |
144 | static d_kqfilter_t ums_kqfilter; |
| 145 | ||
| 146 | static void ums_filt_detach(struct knote *); | |
| 147 | static int ums_filt(struct knote *, long); | |
| 984263bc MD |
148 | |
| 149 | #define UMS_CDEV_MAJOR 111 | |
| 150 | ||
| 6ed427ca | 151 | static struct dev_ops ums_ops = { |
| 96bcde04 | 152 | { "ums", UMS_CDEV_MAJOR, D_KQFILTER }, |
| fef8985e MD |
153 | .d_open = ums_open, |
| 154 | .d_close = ums_close, | |
| 155 | .d_read = ums_read, | |
| 156 | .d_ioctl = ums_ioctl, | |
| 157 | .d_poll = ums_poll, | |
| 96bcde04 | 158 | .d_kqfilter = ums_kqfilter |
| 984263bc MD |
159 | }; |
| 160 | ||
| 61b69189 HT |
161 | static device_probe_t ums_match; |
| 162 | static device_attach_t ums_attach; | |
| 163 | static device_detach_t ums_detach; | |
| 164 | ||
| 165 | static devclass_t ums_devclass; | |
| 166 | ||
| 167 | static kobj_method_t ums_methods[] = { | |
| 168 | DEVMETHOD(device_probe, ums_match), | |
| 169 | DEVMETHOD(device_attach, ums_attach), | |
| 170 | DEVMETHOD(device_detach, ums_detach), | |
| 171 | {0,0} | |
| 172 | }; | |
| 173 | ||
| 174 | static driver_t ums_driver = { | |
| 175 | "ums", | |
| 176 | ums_methods, | |
| 177 | sizeof(struct ums_softc) | |
| 178 | }; | |
| 179 | ||
| 180 | MODULE_DEPEND(ums, usb, 1, 1, 1); | |
| 984263bc | 181 | |
| e785a5d9 HT |
182 | static int |
| 183 | ums_match(device_t self) | |
| 984263bc | 184 | { |
| e785a5d9 | 185 | struct usb_attach_arg *uaa = device_get_ivars(self); |
| 984263bc MD |
186 | usb_interface_descriptor_t *id; |
| 187 | int size, ret; | |
| 188 | void *desc; | |
| 189 | usbd_status err; | |
| 1550dfd9 | 190 | |
| 984263bc MD |
191 | if (!uaa->iface) |
| 192 | return (UMATCH_NONE); | |
| 193 | id = usbd_get_interface_descriptor(uaa->iface); | |
| 194 | if (!id || id->bInterfaceClass != UICLASS_HID) | |
| 195 | return (UMATCH_NONE); | |
| 196 | ||
| 1550dfd9 | 197 | err = usbd_read_report_desc(uaa->iface, &desc, &size, M_TEMP); |
| 984263bc MD |
198 | if (err) |
| 199 | return (UMATCH_NONE); | |
| 200 | ||
| 1550dfd9 | 201 | if (hid_is_collection(desc, size, |
| 984263bc MD |
202 | HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_MOUSE))) |
| 203 | ret = UMATCH_IFACECLASS; | |
| 204 | else | |
| 205 | ret = UMATCH_NONE; | |
| 206 | ||
| efda3bd0 | 207 | kfree(desc, M_TEMP); |
| 984263bc MD |
208 | return (ret); |
| 209 | } | |
| 210 | ||
| e785a5d9 HT |
211 | static int |
| 212 | ums_attach(device_t self) | |
| 984263bc | 213 | { |
| e785a5d9 HT |
214 | struct ums_softc *sc = device_get_softc(self); |
| 215 | struct usb_attach_arg *uaa = device_get_ivars(self); | |
| 984263bc | 216 | usbd_interface_handle iface = uaa->iface; |
| 984263bc MD |
217 | usb_endpoint_descriptor_t *ed; |
| 218 | int size; | |
| 219 | void *desc; | |
| 220 | usbd_status err; | |
| 984263bc MD |
221 | u_int32_t flags; |
| 222 | int i; | |
| 223 | struct hid_location loc_btn; | |
| 1550dfd9 | 224 | |
| 984263bc MD |
225 | sc->sc_disconnected = 1; |
| 226 | sc->sc_iface = iface; | |
| e785a5d9 | 227 | sc->sc_dev = self; |
| 984263bc MD |
228 | ed = usbd_interface2endpoint_descriptor(iface, 0); |
| 229 | if (!ed) { | |
| e3869ec7 | 230 | kprintf("%s: could not read endpoint descriptor\n", |
| 6ed427ca | 231 | device_get_nameunit(sc->sc_dev)); |
| e785a5d9 | 232 | return ENXIO; |
| 984263bc MD |
233 | } |
| 234 | ||
| 235 | DPRINTFN(10,("ums_attach: bLength=%d bDescriptorType=%d " | |
| 236 | "bEndpointAddress=%d-%s bmAttributes=%d wMaxPacketSize=%d" | |
| 237 | " bInterval=%d\n", | |
| 1550dfd9 | 238 | ed->bLength, ed->bDescriptorType, |
| 984263bc MD |
239 | UE_GET_ADDR(ed->bEndpointAddress), |
| 240 | UE_GET_DIR(ed->bEndpointAddress) == UE_DIR_IN ? "in":"out", | |
| 241 | UE_GET_XFERTYPE(ed->bmAttributes), | |
| 242 | UGETW(ed->wMaxPacketSize), ed->bInterval)); | |
| 243 | ||
| 244 | if (UE_GET_DIR(ed->bEndpointAddress) != UE_DIR_IN || | |
| 245 | UE_GET_XFERTYPE(ed->bmAttributes) != UE_INTERRUPT) { | |
| e3869ec7 | 246 | kprintf("%s: unexpected endpoint\n", |
| 6ed427ca | 247 | device_get_nameunit(sc->sc_dev)); |
| e785a5d9 | 248 | return ENXIO; |
| 984263bc MD |
249 | } |
| 250 | ||
| 1550dfd9 | 251 | err = usbd_read_report_desc(uaa->iface, &desc, &size, M_TEMP); |
| 984263bc | 252 | if (err) |
| e785a5d9 | 253 | return ENXIO; |
| 984263bc MD |
254 | |
| 255 | if (!hid_locate(desc, size, HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_X), | |
| 256 | hid_input, &sc->sc_loc_x, &flags)) { | |
| 6ed427ca | 257 | kprintf("%s: mouse has no X report\n", device_get_nameunit(sc->sc_dev)); |
| e785a5d9 | 258 | return ENXIO; |
| 984263bc MD |
259 | } |
| 260 | if ((flags & MOUSE_FLAGS_MASK) != MOUSE_FLAGS) { | |
| e3869ec7 | 261 | kprintf("%s: X report 0x%04x not supported\n", |
| 6ed427ca | 262 | device_get_nameunit(sc->sc_dev), flags); |
| e785a5d9 | 263 | return ENXIO; |
| 984263bc MD |
264 | } |
| 265 | ||
| 266 | if (!hid_locate(desc, size, HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_Y), | |
| 267 | hid_input, &sc->sc_loc_y, &flags)) { | |
| 6ed427ca | 268 | kprintf("%s: mouse has no Y report\n", device_get_nameunit(sc->sc_dev)); |
| e785a5d9 | 269 | return ENXIO; |
| 984263bc MD |
270 | } |
| 271 | if ((flags & MOUSE_FLAGS_MASK) != MOUSE_FLAGS) { | |
| e3869ec7 | 272 | kprintf("%s: Y report 0x%04x not supported\n", |
| 6ed427ca | 273 | device_get_nameunit(sc->sc_dev), flags); |
| e785a5d9 | 274 | return ENXIO; |
| 984263bc MD |
275 | } |
| 276 | ||
| 277 | /* try to guess the Z activator: first check Z, then WHEEL */ | |
| 278 | if (hid_locate(desc, size, HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_Z), | |
| 279 | hid_input, &sc->sc_loc_z, &flags) || | |
| 280 | hid_locate(desc, size, HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_WHEEL), | |
| 281 | hid_input, &sc->sc_loc_z, &flags)) { | |
| 282 | if ((flags & MOUSE_FLAGS_MASK) != MOUSE_FLAGS) { | |
| 283 | sc->sc_loc_z.size = 0; /* Bad Z coord, ignore it */ | |
| 284 | } else { | |
| 285 | sc->flags |= UMS_Z; | |
| 286 | } | |
| 287 | } | |
| 288 | ||
| 289 | /* figure out the number of buttons */ | |
| 290 | for (i = 1; i <= MAX_BUTTONS; i++) | |
| 291 | if (!hid_locate(desc, size, HID_USAGE2(HUP_BUTTON, i), | |
| 292 | hid_input, &loc_btn, 0)) | |
| 293 | break; | |
| 294 | sc->nbuttons = i - 1; | |
| 77652cad | 295 | sc->sc_loc_btn = kmalloc(sizeof(struct hid_location)*sc->nbuttons, |
| ad86f6b6 | 296 | M_USBDEV, M_INTWAIT); |
| 984263bc | 297 | |
| 6ed427ca | 298 | kprintf("%s: %d buttons%s\n", device_get_nameunit(sc->sc_dev), |
| 984263bc MD |
299 | sc->nbuttons, sc->flags & UMS_Z? " and Z dir." : ""); |
| 300 | ||
| 301 | for (i = 1; i <= sc->nbuttons; i++) | |
| 302 | hid_locate(desc, size, HID_USAGE2(HUP_BUTTON, i), | |
| 303 | hid_input, &sc->sc_loc_btn[i-1], 0); | |
| 304 | ||
| 305 | sc->sc_isize = hid_report_size(desc, size, hid_input, &sc->sc_iid); | |
| efda3bd0 | 306 | sc->sc_ibuf = kmalloc(sc->sc_isize, M_USB, M_INTWAIT); |
| 984263bc MD |
307 | sc->sc_ep_addr = ed->bEndpointAddress; |
| 308 | sc->sc_disconnected = 0; | |
| efda3bd0 | 309 | kfree(desc, M_TEMP); |
| 984263bc MD |
310 | |
| 311 | #ifdef USB_DEBUG | |
| 312 | DPRINTF(("ums_attach: sc=%p\n", sc)); | |
| 1550dfd9 | 313 | DPRINTF(("ums_attach: X\t%d/%d\n", |
| 984263bc | 314 | sc->sc_loc_x.pos, sc->sc_loc_x.size)); |
| 1550dfd9 | 315 | DPRINTF(("ums_attach: Y\t%d/%d\n", |
| 984263bc MD |
316 | sc->sc_loc_y.pos, sc->sc_loc_y.size)); |
| 317 | if (sc->flags & UMS_Z) | |
| 1550dfd9 | 318 | DPRINTF(("ums_attach: Z\t%d/%d\n", |
| 984263bc MD |
319 | sc->sc_loc_z.pos, sc->sc_loc_z.size)); |
| 320 | for (i = 1; i <= sc->nbuttons; i++) { | |
| 321 | DPRINTF(("ums_attach: B%d\t%d/%d\n", | |
| 322 | i, sc->sc_loc_btn[i-1].pos,sc->sc_loc_btn[i-1].size)); | |
| 323 | } | |
| 324 | DPRINTF(("ums_attach: size=%d, id=%d\n", sc->sc_isize, sc->sc_iid)); | |
| 325 | #endif | |
| 326 | ||
| 327 | if (sc->nbuttons > MOUSE_MSC_MAXBUTTON) | |
| 328 | sc->hw.buttons = MOUSE_MSC_MAXBUTTON; | |
| 329 | else | |
| 330 | sc->hw.buttons = sc->nbuttons; | |
| 331 | sc->hw.iftype = MOUSE_IF_USB; | |
| 332 | sc->hw.type = MOUSE_MOUSE; | |
| 333 | sc->hw.model = MOUSE_MODEL_GENERIC; | |
| 334 | sc->hw.hwid = 0; | |
| 335 | sc->mode.protocol = MOUSE_PROTO_MSC; | |
| 336 | sc->mode.rate = -1; | |
| 337 | sc->mode.resolution = MOUSE_RES_UNKNOWN; | |
| 338 | sc->mode.accelfactor = 0; | |
| 339 | sc->mode.level = 0; | |
| 340 | sc->mode.packetsize = MOUSE_MSC_PACKETSIZE; | |
| 341 | sc->mode.syncmask[0] = MOUSE_MSC_SYNCMASK; | |
| 342 | sc->mode.syncmask[1] = MOUSE_MSC_SYNC; | |
| 343 | ||
| 344 | sc->status.flags = 0; | |
| 345 | sc->status.button = sc->status.obutton = 0; | |
| 346 | sc->status.dx = sc->status.dy = sc->status.dz = 0; | |
| 347 | ||
| fef8985e | 348 | make_dev(&ums_ops, device_get_unit(self), |
| 3e82b46c MD |
349 | UID_ROOT, GID_OPERATOR, |
| 350 | 0644, "ums%d", device_get_unit(self)); | |
| 984263bc MD |
351 | |
| 352 | if (usbd_get_quirks(uaa->device)->uq_flags & UQ_SPUR_BUT_UP) { | |
| 353 | DPRINTF(("%s: Spurious button up events\n", | |
| 6ed427ca | 354 | device_get_nameunit(sc->sc_dev))); |
| 984263bc MD |
355 | sc->flags |= UMS_SPUR_BUT_UP; |
| 356 | } | |
| 357 | ||
| e785a5d9 | 358 | return 0; |
| 984263bc MD |
359 | } |
| 360 | ||
| 361 | ||
| 6ed427ca | 362 | static int |
| 984263bc MD |
363 | ums_detach(device_t self) |
| 364 | { | |
| 365 | struct ums_softc *sc = device_get_softc(self); | |
| 984263bc MD |
366 | |
| 367 | if (sc->sc_enabled) | |
| 368 | ums_disable(sc); | |
| 369 | ||
| 6ed427ca | 370 | DPRINTF(("%s: disconnected\n", device_get_nameunit(self))); |
| 984263bc | 371 | |
| efda3bd0 MD |
372 | kfree(sc->sc_loc_btn, M_USB); |
| 373 | kfree(sc->sc_ibuf, M_USB); | |
| 984263bc | 374 | |
| 984263bc MD |
375 | /* someone waiting for data */ |
| 376 | /* | |
| 377 | * XXX If we wakeup the process here, the device will be gone by | |
| 378 | * the time the process gets a chance to notice. *_close and friends | |
| 379 | * should be fixed to handle this case. | |
| 380 | * Or we should do a delayed detach for this. | |
| 381 | * Does this delay now force tsleep to exit with an error? | |
| 382 | */ | |
| 383 | if (sc->state & UMS_ASLEEP) { | |
| 384 | sc->state &= ~UMS_ASLEEP; | |
| 385 | wakeup(sc); | |
| 386 | } | |
| 0f538c08 | 387 | selwakeup(&sc->rsel); |
| 17336059 | 388 | KNOTE(&sc->rsel.si_note, 0); |
| 0f538c08 | 389 | |
| cd29885a | 390 | dev_ops_remove_minor(&ums_ops, /*-1, */device_get_unit(self)); |
| 984263bc MD |
391 | |
| 392 | return 0; | |
| 393 | } | |
| 394 | ||
| 395 | void | |
| c436375a SW |
396 | ums_intr(usbd_xfer_handle xfer, usbd_private_handle addr, |
| 397 | usbd_status status) | |
| 984263bc MD |
398 | { |
| 399 | struct ums_softc *sc = addr; | |
| 400 | u_char *ibuf; | |
| 401 | int dx, dy, dz; | |
| 402 | u_char buttons = 0; | |
| 403 | int i; | |
| 404 | ||
| 405 | #define UMS_BUT(i) ((i) < 3 ? (((i) + 2) % 3) : (i)) | |
| 406 | ||
| 407 | DPRINTFN(5, ("ums_intr: sc=%p status=%d\n", sc, status)); | |
| 408 | DPRINTFN(5, ("ums_intr: data = %02x %02x %02x\n", | |
| 409 | sc->sc_ibuf[0], sc->sc_ibuf[1], sc->sc_ibuf[2])); | |
| 410 | ||
| 411 | if (status == USBD_CANCELLED) | |
| 412 | return; | |
| 413 | ||
| 414 | if (status != USBD_NORMAL_COMPLETION) { | |
| 415 | DPRINTF(("ums_intr: status=%d\n", status)); | |
| 1550dfd9 MD |
416 | if (status == USBD_STALLED) |
| 417 | usbd_clear_endpoint_stall_async(sc->sc_intrpipe); | |
| 984263bc MD |
418 | return; |
| 419 | } | |
| 420 | ||
| 421 | ibuf = sc->sc_ibuf; | |
| 422 | if (sc->sc_iid) { | |
| 423 | if (*ibuf++ != sc->sc_iid) | |
| 424 | return; | |
| 425 | } | |
| 426 | ||
| 427 | dx = hid_get_data(ibuf, &sc->sc_loc_x); | |
| 428 | dy = -hid_get_data(ibuf, &sc->sc_loc_y); | |
| 429 | dz = -hid_get_data(ibuf, &sc->sc_loc_z); | |
| 430 | for (i = 0; i < sc->nbuttons; i++) | |
| 431 | if (hid_get_data(ibuf, &sc->sc_loc_btn[i])) | |
| 432 | buttons |= (1 << UMS_BUT(i)); | |
| 433 | ||
| 434 | if (dx || dy || dz || (sc->flags & UMS_Z) | |
| 435 | || buttons != sc->status.button) { | |
| 436 | DPRINTFN(5, ("ums_intr: x:%d y:%d z:%d buttons:0x%x\n", | |
| 437 | dx, dy, dz, buttons)); | |
| 438 | ||
| 439 | sc->status.button = buttons; | |
| 440 | sc->status.dx += dx; | |
| 441 | sc->status.dy += dy; | |
| 442 | sc->status.dz += dz; | |
| 443 | ||
| 444 | /* Discard data in case of full buffer */ | |
| 445 | if (sc->qcount == sizeof(sc->qbuf)) { | |
| 446 | DPRINTF(("Buffer full, discarded packet")); | |
| 447 | return; | |
| 448 | } | |
| 449 | ||
| 450 | /* | |
| 451 | * The Qtronix keyboard has a built in PS/2 port for a mouse. | |
| 452 | * The firmware once in a while posts a spurious button up | |
| 453 | * event. This event we ignore by doing a timeout for 50 msecs. | |
| 454 | * If we receive dx=dy=dz=buttons=0 before we add the event to | |
| 455 | * the queue. | |
| 456 | * In any other case we delete the timeout event. | |
| 457 | */ | |
| 458 | if (sc->flags & UMS_SPUR_BUT_UP && | |
| 459 | dx == 0 && dy == 0 && dz == 0 && buttons == 0) { | |
| 8c64e381 | 460 | callout_reset(&sc->sc_timeout, MS_TO_TICKS(50), |
| 1550dfd9 | 461 | ums_add_to_queue_timeout, (void *) sc); |
| 984263bc | 462 | } else { |
| 8c64e381 | 463 | callout_stop(&sc->sc_timeout); |
| 984263bc MD |
464 | ums_add_to_queue(sc, dx, dy, dz, buttons); |
| 465 | } | |
| 466 | } | |
| 467 | } | |
| 468 | ||
| 6ed427ca | 469 | static void |
| 984263bc MD |
470 | ums_add_to_queue_timeout(void *priv) |
| 471 | { | |
| 472 | struct ums_softc *sc = priv; | |
| 984263bc | 473 | |
| 4e01b467 | 474 | crit_enter(); |
| 984263bc | 475 | ums_add_to_queue(sc, 0, 0, 0, 0); |
| 4e01b467 | 476 | crit_exit(); |
| 984263bc MD |
477 | } |
| 478 | ||
| 6ed427ca | 479 | static void |
| 984263bc MD |
480 | ums_add_to_queue(struct ums_softc *sc, int dx, int dy, int dz, int buttons) |
| 481 | { | |
| 482 | /* Discard data in case of full buffer */ | |
| 483 | if (sc->qhead+sc->mode.packetsize > sizeof(sc->qbuf)) { | |
| 484 | DPRINTF(("Buffer full, discarded packet")); | |
| 485 | return; | |
| 486 | } | |
| 487 | ||
| 488 | if (dx > 254) dx = 254; | |
| 489 | if (dx < -256) dx = -256; | |
| 490 | if (dy > 254) dy = 254; | |
| 491 | if (dy < -256) dy = -256; | |
| 492 | if (dz > 126) dz = 126; | |
| 493 | if (dz < -128) dz = -128; | |
| 494 | ||
| 495 | sc->qbuf[sc->qhead] = sc->mode.syncmask[1]; | |
| 496 | sc->qbuf[sc->qhead] |= ~buttons & MOUSE_MSC_BUTTONS; | |
| 497 | sc->qbuf[sc->qhead+1] = dx >> 1; | |
| 498 | sc->qbuf[sc->qhead+2] = dy >> 1; | |
| 499 | sc->qbuf[sc->qhead+3] = dx - (dx >> 1); | |
| 500 | sc->qbuf[sc->qhead+4] = dy - (dy >> 1); | |
| 501 | ||
| 502 | if (sc->mode.level == 1) { | |
| 503 | sc->qbuf[sc->qhead+5] = dz >> 1; | |
| 504 | sc->qbuf[sc->qhead+6] = dz - (dz >> 1); | |
| 505 | sc->qbuf[sc->qhead+7] = ((~buttons >> 3) | |
| 506 | & MOUSE_SYS_EXTBUTTONS); | |
| 507 | } | |
| 508 | ||
| 509 | sc->qhead += sc->mode.packetsize; | |
| 510 | sc->qcount += sc->mode.packetsize; | |
| 511 | /* wrap round at end of buffer */ | |
| 512 | if (sc->qhead >= sizeof(sc->qbuf)) | |
| 513 | sc->qhead = 0; | |
| 514 | ||
| 515 | /* someone waiting for data */ | |
| 516 | if (sc->state & UMS_ASLEEP) { | |
| 517 | sc->state &= ~UMS_ASLEEP; | |
| 518 | wakeup(sc); | |
| 519 | } | |
| 0f538c08 | 520 | selwakeup(&sc->rsel); |
| 17336059 | 521 | KNOTE(&sc->rsel.si_note, 0); |
| 984263bc | 522 | } |
| c436375a | 523 | |
| 6ed427ca | 524 | static int |
| c436375a | 525 | ums_enable(void *v) |
| 984263bc MD |
526 | { |
| 527 | struct ums_softc *sc = v; | |
| 528 | ||
| 529 | usbd_status err; | |
| 530 | ||
| 531 | if (sc->sc_enabled) | |
| 532 | return EBUSY; | |
| 533 | ||
| 534 | sc->sc_enabled = 1; | |
| 535 | sc->qcount = 0; | |
| 536 | sc->qhead = sc->qtail = 0; | |
| 537 | sc->status.flags = 0; | |
| 538 | sc->status.button = sc->status.obutton = 0; | |
| 539 | sc->status.dx = sc->status.dy = sc->status.dz = 0; | |
| 540 | ||
| 8c64e381 | 541 | callout_init(&sc->sc_timeout); |
| 984263bc MD |
542 | |
| 543 | /* Set up interrupt pipe. */ | |
| 1550dfd9 MD |
544 | err = usbd_open_pipe_intr(sc->sc_iface, sc->sc_ep_addr, |
| 545 | USBD_SHORT_XFER_OK, &sc->sc_intrpipe, sc, | |
| 984263bc MD |
546 | sc->sc_ibuf, sc->sc_isize, ums_intr, |
| 547 | USBD_DEFAULT_INTERVAL); | |
| 548 | if (err) { | |
| 549 | DPRINTF(("ums_enable: usbd_open_pipe_intr failed, error=%d\n", | |
| 550 | err)); | |
| 551 | sc->sc_enabled = 0; | |
| 552 | return (EIO); | |
| 553 | } | |
| 554 | return (0); | |
| 555 | } | |
| 556 | ||
| 6ed427ca | 557 | static void |
| c436375a | 558 | ums_disable(void *priv) |
| 984263bc MD |
559 | { |
| 560 | struct ums_softc *sc = priv; | |
| 561 | ||
| 8c64e381 | 562 | callout_stop(&sc->sc_timeout); |
| 984263bc MD |
563 | |
| 564 | /* Disable interrupts. */ | |
| 565 | usbd_abort_pipe(sc->sc_intrpipe); | |
| 566 | usbd_close_pipe(sc->sc_intrpipe); | |
| 567 | ||
| 568 | sc->sc_enabled = 0; | |
| 569 | ||
| 570 | if (sc->qcount != 0) | |
| 571 | DPRINTF(("Discarded %d bytes in queue\n", sc->qcount)); | |
| 572 | } | |
| 573 | ||
| 6ed427ca | 574 | static int |
| fef8985e | 575 | ums_open(struct dev_open_args *ap) |
| 984263bc | 576 | { |
| b13267a5 | 577 | cdev_t dev = ap->a_head.a_dev; |
| 984263bc MD |
578 | struct ums_softc *sc; |
| 579 | ||
| 1e089e7e HT |
580 | sc = devclass_get_softc(ums_devclass, UMSUNIT(dev)); |
| 581 | if (sc == NULL) | |
| 582 | return (ENXIO); | |
| 984263bc MD |
583 | |
| 584 | return ums_enable(sc); | |
| 585 | } | |
| 586 | ||
| 6ed427ca | 587 | static int |
| fef8985e | 588 | ums_close(struct dev_close_args *ap) |
| 984263bc | 589 | { |
| b13267a5 | 590 | cdev_t dev = ap->a_head.a_dev; |
| 984263bc MD |
591 | struct ums_softc *sc; |
| 592 | ||
| 1e089e7e | 593 | sc = devclass_get_softc(ums_devclass, UMSUNIT(dev)); |
| 984263bc MD |
594 | |
| 595 | if (!sc) | |
| 596 | return 0; | |
| 597 | ||
| 598 | if (sc->sc_enabled) | |
| 599 | ums_disable(sc); | |
| 600 | ||
| 601 | return 0; | |
| 602 | } | |
| 603 | ||
| 6ed427ca | 604 | static int |
| fef8985e | 605 | ums_read(struct dev_read_args *ap) |
| 984263bc | 606 | { |
| b13267a5 | 607 | cdev_t dev = ap->a_head.a_dev; |
| fef8985e | 608 | struct uio *uio = ap->a_uio; |
| 984263bc | 609 | struct ums_softc *sc; |
| 984263bc MD |
610 | char buf[sizeof(sc->qbuf)]; |
| 611 | int l = 0; | |
| 612 | int error; | |
| 613 | ||
| 1e089e7e | 614 | sc = devclass_get_softc(ums_devclass, UMSUNIT(dev)); |
| 984263bc | 615 | |
| 4e01b467 | 616 | crit_enter(); |
| 984263bc | 617 | if (!sc) { |
| 4e01b467 | 618 | crit_exit(); |
| 984263bc MD |
619 | return EIO; |
| 620 | } | |
| 621 | ||
| 622 | while (sc->qcount == 0 ) { | |
| fef8985e | 623 | if (ap->a_ioflag & IO_NDELAY) { /* non-blocking I/O */ |
| 4e01b467 | 624 | crit_exit(); |
| 984263bc MD |
625 | return EWOULDBLOCK; |
| 626 | } | |
| 1550dfd9 | 627 | |
| 984263bc | 628 | sc->state |= UMS_ASLEEP; /* blocking I/O */ |
| 377d4740 | 629 | error = tsleep(sc, PCATCH, "umsrea", 0); |
| 984263bc | 630 | if (error) { |
| 4e01b467 | 631 | crit_exit(); |
| 984263bc MD |
632 | return error; |
| 633 | } else if (!sc->sc_enabled) { | |
| 4e01b467 | 634 | crit_exit(); |
| 984263bc MD |
635 | return EINTR; |
| 636 | } | |
| 637 | /* check whether the device is still there */ | |
| 638 | ||
| 639 | sc = devclass_get_softc(ums_devclass, UMSUNIT(dev)); | |
| 640 | if (!sc) { | |
| 4e01b467 | 641 | crit_exit(); |
| 984263bc MD |
642 | return EIO; |
| 643 | } | |
| 644 | } | |
| 645 | ||
| 646 | /* | |
| 4e01b467 MD |
647 | * The writer process only extends qcount and qtail. We could copy |
| 648 | * them and use the copies to do the copying out of the queue. | |
| 984263bc MD |
649 | */ |
| 650 | ||
| 651 | while ((sc->qcount > 0) && (uio->uio_resid > 0)) { | |
| 652 | l = (sc->qcount < uio->uio_resid? sc->qcount:uio->uio_resid); | |
| 653 | if (l > sizeof(buf)) | |
| 654 | l = sizeof(buf); | |
| 655 | if (l > sizeof(sc->qbuf) - sc->qtail) /* transfer till end of buf */ | |
| 656 | l = sizeof(sc->qbuf) - sc->qtail; | |
| 657 | ||
| 4e01b467 | 658 | crit_exit(); |
| 984263bc | 659 | uiomove(&sc->qbuf[sc->qtail], l, uio); |
| 4e01b467 | 660 | crit_enter(); |
| 984263bc MD |
661 | |
| 662 | if ( sc->qcount - l < 0 ) { | |
| 663 | DPRINTF(("qcount below 0, count=%d l=%d\n", sc->qcount, l)); | |
| 664 | sc->qcount = l; | |
| 665 | } | |
| 666 | sc->qcount -= l; /* remove the bytes from the buffer */ | |
| 667 | sc->qtail = (sc->qtail + l) % sizeof(sc->qbuf); | |
| 668 | } | |
| 4e01b467 | 669 | crit_exit(); |
| 984263bc MD |
670 | |
| 671 | return 0; | |
| 672 | } | |
| 673 | ||
| 6ed427ca | 674 | static int |
| fef8985e | 675 | ums_poll(struct dev_poll_args *ap) |
| 984263bc | 676 | { |
| b13267a5 | 677 | cdev_t dev = ap->a_head.a_dev; |
| 984263bc MD |
678 | struct ums_softc *sc; |
| 679 | int revents = 0; | |
| 984263bc | 680 | |
| 1e089e7e | 681 | sc = devclass_get_softc(ums_devclass, UMSUNIT(dev)); |
| 984263bc | 682 | |
| fef8985e MD |
683 | if (!sc) { |
| 684 | ap->a_events = 0; | |
| 984263bc | 685 | return 0; |
| fef8985e | 686 | } |
| 984263bc | 687 | |
| 4e01b467 | 688 | crit_enter(); |
| fef8985e | 689 | if (ap->a_events & (POLLIN | POLLRDNORM)) { |
| 984263bc | 690 | if (sc->qcount) { |
| fef8985e | 691 | revents = ap->a_events & (POLLIN | POLLRDNORM); |
| 984263bc | 692 | } else { |
| 0f538c08 | 693 | /* sc->state |= UMS_SELECT; */ |
| fef8985e | 694 | selrecord(curthread, &sc->rsel); |
| 984263bc MD |
695 | } |
| 696 | } | |
| 4e01b467 | 697 | crit_exit(); |
| fef8985e MD |
698 | ap->a_events = revents; |
| 699 | return (0); | |
| 984263bc | 700 | } |
| 1550dfd9 | 701 | |
| 96bcde04 SG |
702 | static struct filterops ums_filtops = |
| 703 | { 1, NULL, ums_filt_detach, ums_filt }; | |
| 704 | ||
| 705 | static int | |
| 706 | ums_kqfilter(struct dev_kqfilter_args *ap) | |
| 707 | { | |
| 708 | cdev_t dev = ap->a_head.a_dev; | |
| 709 | struct knote *kn = ap->a_kn; | |
| 710 | struct ums_softc *sc; | |
| 711 | struct klist *klist; | |
| 712 | ||
| 713 | ap->a_result = 0; | |
| 714 | ||
| 715 | switch (kn->kn_filter) { | |
| 716 | case EVFILT_READ: | |
| 717 | sc = devclass_get_softc(ums_devclass, UMSUNIT(dev)); | |
| 718 | kn->kn_fop = &ums_filtops; | |
| 719 | kn->kn_hook = (caddr_t)sc; | |
| 720 | break; | |
| 721 | default: | |
| 722 | ap->a_result = 1; | |
| 723 | return (0); | |
| 724 | } | |
| 725 | ||
| 726 | crit_enter(); | |
| 727 | klist = &sc->rsel.si_note; | |
| 728 | SLIST_INSERT_HEAD(klist, kn, kn_selnext); | |
| 729 | crit_exit(); | |
| 730 | ||
| 731 | return (0); | |
| 732 | } | |
| 733 | ||
| 734 | static void | |
| 735 | ums_filt_detach(struct knote *kn) | |
| 736 | { | |
| 737 | struct ums_softc *sc = (struct ums_softc *)kn->kn_hook; | |
| 738 | struct klist *klist; | |
| 739 | ||
| 740 | crit_enter(); | |
| 741 | klist = &sc->rsel.si_note; | |
| 742 | SLIST_REMOVE(klist, kn, knote, kn_selnext); | |
| 743 | crit_exit(); | |
| 744 | } | |
| 745 | ||
| 746 | static int | |
| 747 | ums_filt(struct knote *kn, long hint) | |
| 748 | { | |
| 749 | struct ums_softc *sc = (struct ums_softc *)kn->kn_hook; | |
| 750 | int ready = 0; | |
| 751 | ||
| 752 | crit_enter(); | |
| 753 | if (sc->qcount) | |
| 754 | ready = 1; | |
| 755 | crit_exit(); | |
| 756 | ||
| 757 | return (ready); | |
| 758 | } | |
| 759 | ||
| 984263bc | 760 | int |
| fef8985e | 761 | ums_ioctl(struct dev_ioctl_args *ap) |
| 984263bc | 762 | { |
| b13267a5 | 763 | cdev_t dev = ap->a_head.a_dev; |
| 984263bc MD |
764 | struct ums_softc *sc; |
| 765 | int error = 0; | |
| 984263bc MD |
766 | mousemode_t mode; |
| 767 | ||
| 1e089e7e | 768 | sc = devclass_get_softc(ums_devclass, UMSUNIT(dev)); |
| 984263bc MD |
769 | |
| 770 | if (!sc) | |
| 771 | return EIO; | |
| 772 | ||
| fef8985e | 773 | switch(ap->a_cmd) { |
| 984263bc | 774 | case MOUSE_GETHWINFO: |
| fef8985e | 775 | *(mousehw_t *)ap->a_data = sc->hw; |
| 984263bc MD |
776 | break; |
| 777 | case MOUSE_GETMODE: | |
| fef8985e | 778 | *(mousemode_t *)ap->a_data = sc->mode; |
| 984263bc MD |
779 | break; |
| 780 | case MOUSE_SETMODE: | |
| fef8985e | 781 | mode = *(mousemode_t *)ap->a_data; |
| 984263bc MD |
782 | |
| 783 | if (mode.level == -1) | |
| 784 | /* don't change the current setting */ | |
| 785 | ; | |
| 786 | else if ((mode.level < 0) || (mode.level > 1)) | |
| 787 | return (EINVAL); | |
| 788 | ||
| 4e01b467 | 789 | crit_enter(); |
| 984263bc MD |
790 | sc->mode.level = mode.level; |
| 791 | ||
| 792 | if (sc->mode.level == 0) { | |
| 793 | if (sc->nbuttons > MOUSE_MSC_MAXBUTTON) | |
| 794 | sc->hw.buttons = MOUSE_MSC_MAXBUTTON; | |
| 795 | else | |
| 796 | sc->hw.buttons = sc->nbuttons; | |
| 797 | sc->mode.protocol = MOUSE_PROTO_MSC; | |
| 798 | sc->mode.packetsize = MOUSE_MSC_PACKETSIZE; | |
| 799 | sc->mode.syncmask[0] = MOUSE_MSC_SYNCMASK; | |
| 800 | sc->mode.syncmask[1] = MOUSE_MSC_SYNC; | |
| 801 | } else if (sc->mode.level == 1) { | |
| 802 | if (sc->nbuttons > MOUSE_SYS_MAXBUTTON) | |
| 803 | sc->hw.buttons = MOUSE_SYS_MAXBUTTON; | |
| 804 | else | |
| 805 | sc->hw.buttons = sc->nbuttons; | |
| 806 | sc->mode.protocol = MOUSE_PROTO_SYSMOUSE; | |
| 807 | sc->mode.packetsize = MOUSE_SYS_PACKETSIZE; | |
| 808 | sc->mode.syncmask[0] = MOUSE_SYS_SYNCMASK; | |
| 809 | sc->mode.syncmask[1] = MOUSE_SYS_SYNC; | |
| 810 | } | |
| 811 | ||
| 812 | bzero(sc->qbuf, sizeof(sc->qbuf)); | |
| 813 | sc->qhead = sc->qtail = sc->qcount = 0; | |
| 4e01b467 | 814 | crit_exit(); |
| 984263bc MD |
815 | |
| 816 | break; | |
| 817 | case MOUSE_GETLEVEL: | |
| fef8985e | 818 | *(int *)ap->a_data = sc->mode.level; |
| 984263bc MD |
819 | break; |
| 820 | case MOUSE_SETLEVEL: | |
| fef8985e | 821 | if (*(int *)ap->a_data < 0 || *(int *)ap->a_data > 1) |
| 984263bc MD |
822 | return (EINVAL); |
| 823 | ||
| 4e01b467 | 824 | crit_enter(); |
| fef8985e | 825 | sc->mode.level = *(int *)ap->a_data; |
| 984263bc MD |
826 | |
| 827 | if (sc->mode.level == 0) { | |
| 828 | if (sc->nbuttons > MOUSE_MSC_MAXBUTTON) | |
| 829 | sc->hw.buttons = MOUSE_MSC_MAXBUTTON; | |
| 830 | else | |
| 831 | sc->hw.buttons = sc->nbuttons; | |
| 832 | sc->mode.protocol = MOUSE_PROTO_MSC; | |
| 833 | sc->mode.packetsize = MOUSE_MSC_PACKETSIZE; | |
| 834 | sc->mode.syncmask[0] = MOUSE_MSC_SYNCMASK; | |
| 835 | sc->mode.syncmask[1] = MOUSE_MSC_SYNC; | |
| 836 | } else if (sc->mode.level == 1) { | |
| 837 | if (sc->nbuttons > MOUSE_SYS_MAXBUTTON) | |
| 838 | sc->hw.buttons = MOUSE_SYS_MAXBUTTON; | |
| 839 | else | |
| 840 | sc->hw.buttons = sc->nbuttons; | |
| 841 | sc->mode.protocol = MOUSE_PROTO_SYSMOUSE; | |
| 842 | sc->mode.packetsize = MOUSE_SYS_PACKETSIZE; | |
| 843 | sc->mode.syncmask[0] = MOUSE_SYS_SYNCMASK; | |
| 844 | sc->mode.syncmask[1] = MOUSE_SYS_SYNC; | |
| 845 | } | |
| 846 | ||
| 847 | bzero(sc->qbuf, sizeof(sc->qbuf)); | |
| 848 | sc->qhead = sc->qtail = sc->qcount = 0; | |
| 4e01b467 | 849 | crit_exit(); |
| 984263bc MD |
850 | |
| 851 | break; | |
| 852 | case MOUSE_GETSTATUS: { | |
| fef8985e | 853 | mousestatus_t *status = (mousestatus_t *) ap->a_data; |
| 984263bc | 854 | |
| 4e01b467 | 855 | crit_enter(); |
| 984263bc MD |
856 | *status = sc->status; |
| 857 | sc->status.obutton = sc->status.button; | |
| 858 | sc->status.button = 0; | |
| 859 | sc->status.dx = sc->status.dy = sc->status.dz = 0; | |
| 4e01b467 | 860 | crit_exit(); |
| 984263bc MD |
861 | |
| 862 | if (status->dx || status->dy || status->dz) | |
| 863 | status->flags |= MOUSE_POSCHANGED; | |
| 864 | if (status->button != status->obutton) | |
| 865 | status->flags |= MOUSE_BUTTONSCHANGED; | |
| 866 | break; | |
| 867 | } | |
| 868 | default: | |
| 869 | error = ENOTTY; | |
| 870 | } | |
| 871 | ||
| 872 | return error; | |
| 873 | } | |
| 874 | ||
| 875 | DRIVER_MODULE(ums, uhub, ums_driver, ums_devclass, usbd_driver_load, 0); |