Thursday, 27 May 2010

A first device works: USB mouse

Today I managed to get the following to work: I connect a USB mouse to the host port of my BeagleBoard (via a hub, since the BeagleBoard does not support low-speed USB), and the slave USB port of my BeagleBoard to my PC. Then, I can use that mouse to control the cursor of my PC.

For that, I have a "proxy" kernel driver on the BeagleBoard, that is registered both as a USB device driver (on the EHCI side), and as a USB gadget driver (on the MUSB/OTG side).

Most USB drivers register at the interface level (that is, as a struct usb_driver), so control only a part of a device (one device could have multiple interfaces, for multiple functions). That is not what we want, so, instead, the proxy driver registers as a struct usb_device_driver, that claims the device at a lower level. Normally, there is only one of such low-level drivers, found in drivers/usb/core/generic.c. This generic driver takes care of all USB devices attached to the system, and looks for interface-level drivers for each of the interfaces of these devices.

Since the generic driver claims all new devices (and never releases them), the trick is to unbind the device from the generic driver, so that it can be claimed by our proxy driver. This is done using a command like:
echo 1-2 > /sys/bus/usb/drivers/usb/unbind
Once "unbound", our driver is able to claim it.

On the gadget side, there is nothing special: the driver is registered as a struct usb_gadget_driver. Then, we do everything we can to make the PC believe that it is connected directly to a USB mouse. For that purpose, we reply to GET_DESCRIPTOR control requests from the PC with the following device descriptor:
Bus 001 Device 005: ID 046d:c00e Logitech, Inc. M-BJ58/M-BJ69 Optical Wheel Mouse
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 0 (Defined at Interface level)
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 8
idVendor 0x046d Logitech, Inc.
idProduct 0xc00e M-BJ58/M-BJ69 Optical Wheel Mouse
bcdDevice 11.10
iManufacturer 1 Logitech
iProduct 2 USB-PS/2 Optical Mouse
iSerial 0
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 34
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 0
bmAttributes 0xa0
(Bus Powered)
Remote Wakeup
MaxPower 98mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 1 Boot Interface Subclass
bInterfaceProtocol 2 Mouse
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.10
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 52
Report Descriptors:
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0004 1x 4 bytes
bInterval 10
Device Status: 0x0000
(Bus Powered)
Currently, I have a copy of the descriptor statically defined in the driver, but of course, in the future, this descriptor will have to be read from the device itself.

Then, whenever the drivers receives a control request it does not know how to answer (for example, a class-specific HID control request), it forwards the request to the device, waits for the reply, then writes back the reply to the PC.

With that, the mouse is fully recognized by the PC, but nothing happens when you move it, because mouse events use interrupt transfers on endpoint 1. That can be fixed by listening for interrupt transfers from the device, and forwarding those to the PC.

At that point, the cursor moved, but with some huge latency: clicking was fast, but there was a delay of a few seconds when moving the mouse around... This was due to the following: the mouse is a low-speed USB device, and its endpoint descriptor asks the host to poll it for events every 10 ms (bInterval = 10). However, the MUSB device controller on the BeagleBoard appears as a high-speed device (there is probably a way to force the controller in full-speed mode, but the current driver does not seem to support that), and the definition of the polling interval, for a high-speed device, is 0.125 ms * 2^(bInterval-1). By setting bInterval to 10, the BeagleBoard device controller only got polled every 64 ms, so, when I moved the mouse, this created a lot of events, which got buffered somewhere in the BeagleBoard, until they could be released to the PC.

I fixed the problem by modifying the descriptor, and putting bInterval = 7, that is a poll every 8 ms.

I can write up some instructions about how to get this setup to work, but I guess this is not so useful if you do not have the same mouse as I have, or a similar enough mouse at least...

All the kernel modifications have been pushed in my git repository. The proxy driver code, in particular, can be found here, but the code is still far from clean and full of TODO/FIXME tags.