Glider
"In het verleden behaalde resultaten bieden geen garanties voor de toekomst"
About this blog

These are the ramblings of Matthijs Kooijman, concerning the software he hacks on, hobbies he has and occasionally his personal life.

Most content on this site is licensed under the WTFPL, version 2 (details).

Questions? Praise? Blame? Feel free to contact me.

My old blog (pre-2006) is also still available.

See also my Mastodon page.

Sun Mon Tue Wed Thu Fri Sat
8
         
Powered by Blosxom &Perl onion
(With plugins: config, extensionless, hide, tagging, Markdown, macros, breadcrumbs, calendar, directorybrowse, feedback, flavourdir, include, interpolate_fancy, listplugins, menu, pagetype, preview, seemore, storynum, storytitle, writeback_recent, moreentries)
Valid XHTML 1.0 Strict & CSS
Making an old paint-mixing terminal keyboard work with Linux

Lacour paint terminal

Or: Forcing Linux to use the USB HID driver for a non-standards-compliant USB keyboard.

For an interactive art installation by the Spullenmannen, a friend asked me to have a look at an old paint mixing terminal that he wanted to use. The terminal is essentially a small computer, in a nice industrial-looking sealed casing, with a (touch?) screen, keyboard and touchpad. It was by "Lacour" and I think has been used to control paint mixing machines.

They had already gotten Linux running on the system, but could not get the keyboard to work and asked me if I could have a look.

The keyboard did work in the BIOS and grub (which also uses the BIOS), so we know it worked. Also, the BIOS seemed pretty standard, so it was unlikely that it used some very standard protocol or driver and I guessed that this was a matter of telling Linux which driver to use and/or where to find the device.

Inside the machine, it seemed the keyboard and touchpad were separate devices, controlled by some off-the-shelf microcontroller chip (probably with some custom software inside). These devices were connected to the main motherboard using a standard 10-pin expansion header intended for external USB ports, so it seemed likely that these devices were USB ports.


Closer look at the USB devices

And indeed, looking through lsusb output I noticed two unkown devices in the list:

# lsusb
Bus 002 Device 003: ID ffff:0001
Bus 002 Device 002: ID 0000:0003
(...)

These have USB vendor ids of 0x0000 and 0xffff, which I'm pretty sure are not official USB-consortium-assigned identifiers (probably invalid or reserved even), so perhaps that's why Linux is not using these properly?

Running lsusb with the --tree option allows seeing the physical port structure, but also shows which drivers are bound to which interfaces:

# lsusb --tree
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=uhci_hcd/2p, 12M
    |__ Port 1: Dev 2, If 0, Class=Human Interface Device, Driver=usbhid, 12M
    |__ Port 2: Dev 3, If 0, Class=Human Interface Device, Driver=, 12M
(...)

This shows that the keyboard (Dev 3) indeed has no driver, but the touchpad (Dev 2) is already bound to usbhid. And indeed, runnig cat /dev/input/mice and then moving over the touchpad shows that some output is being generated, so the touchpad was already working.

Looking at the detailed USB descriptors for these devices, shows that they are both advertised as supporting the HID interface (Human Interface Device), which is the default protocol for keyboards and mice nowadays:

# lsusb -d ffff:0001 -v

Bus 002 Device 003: ID ffff:0001
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass          255 Vendor Specific Class
  bDeviceSubClass         0
  bDeviceProtocol         0
  bMaxPacketSize0         8
  idVendor           0xffff 
  idProduct          0x0001 
  bcdDevice            0.01
  iManufacturer           1 Lacour Electronique
  iProduct                2 ColorKeyboard
  iSerial                 3 SE.010.H
(...)
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      1 Boot Interface Subclass
      bInterfaceProtocol      1 Keyboard
(...)

# lsusb -d 0000:00003 -v
Bus 002 Device 002: ID 0000:0003
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass            0 
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0         8
  idVendor           0x0000 
  idProduct          0x0003 
  bcdDevice            0.00
  iManufacturer           1 Lacour Electronique
  iProduct                2 Touchpad
  iSerial                 3 V2.0
(...)
    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
(...)

So, that should make it easy to get the keyboard working: Just make sure the usbhid driver is bound to it and that driver will be able to figure out what to do based on these descriptors. However, apparently something is preventing this binding from happening by default.

Looking back at the USB descriptors above, one interesting difference is that the keyboard has bDeviceClass set to "Vendor specific", whereas the touchpad has it set to 0, which means "Look at interface descriptors. So that seems the most likely reason why the keyboard is not working, since "Vendor Specific" essentially means that the device might not adhere to any of the standard USB protocols and the kernel will probably not start using this device unless it knows what kind of device it is based on the USB vendor and product id (but since those are invalid, these are unlikely to be listed in the kernel).

Binding to usbhid

So, we need to bind the keyboard to the usbhid driver. I know of two ways to do so, both through sysfs.

You can assign extra USB vid/pid pairs to a driver through the new_id sysfs file. In this case, this did not work somehow:

# echo ffff:0001 > /sys/bus/usb/drivers/usbhid/new_id
bash: echo: write error: Invalid argument

At this point, I should have stopped and looked up the right syntax used for new_id, since this was actually the right approach, but I was using the wrong syntax (see below). Instead, I tried some other stuff first.

The second way to bind a driver is to specify a specific device, identified by its sysfs identifier:

# echo 2-2:1.0 > /sys/bus/usb/drivers/usbhid/bind
bash: echo: write error: No such device

The device identifier used here (2-2:1.0) is directory name below /sys/bus/usb/devices and is, I think, built like <bus>-<port>:1.<interface> (where 1 might the configuration?). You can find this info in the lsusb --tree output:

/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=uhci_hcd/2p, 12M
|__ Port 2: Dev 3, If 0, Class=Human Interface Device, Driver=, 12M

I knew that the syntax I used for the device id was correct, since I could use it to unbind and rebind the usbhid module from the touchpad. I suspect that there is some probe mechanism in the usbhid driver that runs after you bind the driver which tests the device to see if it is compatible, and that mechanism rejects it.

How does the kernel handle this?

As I usually do when I cannot get something to work, I dive into the source code. I knew that Linux device/driver association usually works with a driver-specific matching table (that tells the underlying subsystem, such as the usb subsystem in this case, which devices can be handled by a driver) or probe function (which is a bit of driver-specific code that can be called by the kernel to probe whether a device is compatible with a driver). There is also configuration based on Device Tree, but AFAIK this is only used in embedded platforms, not on x86.

Looking at the usbhid_probe() and usb_kbd_probe() functions, I did not see any conditions that would not be fulfilled by this particular USB device.

The match table for usbhid also only matches the interface class and not the device class. The same goes for the module.alias file, which I read might also be involved (though I am not sure how):

# cat /lib/modules/*/modules.alias|grep usbhid
alias usb:v*p*d*dc*dsc*dp*ic03isc*ip*in* usbhid

So, the failing check must be at a lower level, probably in the usb subsystem.

Digging a bit further, I found the usb_match_one_id_intf() function, which is the core of matching USB drivers to USB device interfaces. And indeed, it says:

/* The interface class, subclass, protocol and number should never be
 * checked for a match if the device class is Vendor Specific,
 * unless the match record specifies the Vendor ID. */

So, the entry in the usbhid table is being ignored since it matches only the interface, while the device class is "Vendor Specific". But how to fix this?

A little but upwards in the call stack, is a bit of code that matches a driver to an usb device or interface. This has two sources: The static table from the driver source code, and a dynamic table that can be filled with (hey, we know this part!) the new_id file in sysfs. So that suggests that if we can get an entry into this dynamic table, that matches the vendor id, it should work even with a "Vendor Specific" device class.

Back to new_id

Looking further at how this dynamic table is filled, I found the code that handles writes to new_id, and it parses it input like this:

fields = sscanf(buf, "%x %x %x %x %x", &idVendor, &idProduct, &bInterfaceClass, &refVendor, &refProduct);

In other words, it expects space separated values, rather than just a colon separated vidpid pair. Reading on in the code shows that only the first two (vid/pid) are required, the rest is optional. Trying that actually works right away:

# echo ffff 0001 > /sys/bus/usb/drivers/usbhid/new_id
# dmesg
(...)
[ 5011.088134] input: Lacour Electronique ColorKeyboard as /devices/pci0000:00/0000:00:1d.1/usb2/2-2/2-2:1.0/0003:FFFF:0001.0006/input/input16
[ 5011.150265] hid-generic 0003:FFFF:0001.0006: input,hidraw3: USB HID v1.11 Keyboard [Lacour Electronique ColorKeyboard] on usb-0000:00:1d.1-2/input0

After this, I found I can now use the unbind file to unbind the usbhid driver again, and bind to rebind it. So it seems that using bind indeed still goes through the probe/match code, which previously failed but with the entry in the dynamic table, works.

Making this persistent

So nice that it works, but this dynamic table will be lost on a reboot. How to make it persistent? I can just drop this particular line into the /etc/rc.local startup script, but that does not feel so elegant (it will probably work, since it only needs the usbhid module to be loaded and should work even when the USB device is not known/enumerated yet).

However, as suggested by this post, you can also use udev to run this command at the moment the USB devices is "added" (i.e. enumerated by the kernel). To do so, simply drop a file in /etc/udev/rules.d:

$ cat /etc/udev/rules.d/99-keyboard.rules
# Integrated USB keyboard has invalid USB VIDPID and also has bDeviceClass=255,
# causing the hid driver to ignore it. This writes to sysfs to let the usbhid
# driver match the device on USB VIDPID, which overrides the bDeviceClass ignore.
# See also:
# https://unix.stackexchange.com/a/165845
# https://github.com/torvalds/linux/blob/bf3bd966dfd7d9582f50e9bd08b15922197cd277/drivers/usb/core/driver.c#L647-L656
# https://github.com/torvalds/linux/blob/3039fadf2bfdc104dc963820c305778c7c1a6229/drivers/hid/usbhid/hid-core.c#L1619-L1623
ACTION=="add", ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="0001", RUN+="/bin/sh -c 'echo ffff 0001 > /sys/bus/usb/drivers/usbhid/new_id'"

And with that, the keyboard works automatically at startup. Nice :-)

 
0 comments -:- permalink -:- 18:52
Copyright by Matthijs Kooijman - most content WTFPL