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 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
(...), Arduino, AVR, BaRef, Blosxom, Book, Busy, C++, Charity, Debian, Electronics, Examination, Firefox, Flash, Framework, FreeBSD, Gnome, Hardware, Inter-Actief, IRC, JTAG, LARP, Layout, Linux, Madness, Mail, Math, MS-1013, Mutt, Nerd, Notebook, Optimization, Personal, Plugins, Protocol, QEMU, Random, Rant, Repair, S270, Sailing, Samba, Sanquin, Script, Sleep, Software, SSH, Study, Supermicro, Symbols, Tika, Travel, Trivia, USB, Windows, Work, X201, Xanthe, XBee
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.
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).
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.
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.
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.
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 :-)
Comments are closed for this story.