- Part One: Azio Keyboard
- Part Two: Capturing the Azio Keyboard Data
- Part Three: Reverse Engineering the Azio L70 Keyboard Protocol
- Part Four: Azio L70 Keyboard Linux Driver, The Setup
- Part Five: Azio L70 Keyboard Linux Driver, The Implementation
Introduction
In parallel with reverse engineering the keyboard protocol I began to investigate how to implement a USB driver for Linux. I assumed that someone had already written a blog post about it and I could just follow their instructions. While there are a few out there, there are not as many as you would think.
The first thing that you will find when searching for how to write a USB driver is that there are two types, kernel mode and user mode. Many USB devices can be operated in user mode. Things like cameras, dart guns, fans, etc… are all candidates. Keyboards on the other hand, not so much. Well, as long as you like to use your keyboard for things like booting, operating grub, logging in, and whatnot.
Most of the information out there points you in the direction of making a user mode driver. When you find someone asking how to implement a USB driver they are quickly steered in the direction of writing a user mode one. Which is great for them, as they are much simpler to implement but not great if you really need to implement a kernel level driver.
Linux USB Drivers
I realized that I was going to have to dig deeper and really understand how USB in general works and specifically how USB drivers work at the kernel level. Thankfully there is a terrific resource for that. Do not be put off by its age, it is still very relevant:
Programming Guide for Linux USB Device Drivers By Detlef Fliegl
Not a very inspired title but you have to love it when people get to the point.
I read the entire document. It really demystifies a lot of the aspects of USB. In particular, the most important part is section 2 where it explains the device driver framework and the data structures used.
Along with Detlef’s document, I used Matthias Vallentin’s excellent blog post on Writing a Linus Kernel Driver for an Unknown USB Device from 2007. I have to say, re-reading his article for this blog post makes me feel like I have a long way to go in terms of blogging skills. In spite of the fact that Matthias was writing a driver for a dart gun, and I a keyboard, he clearly has a deeper understanding of the underlying driver mechanics.
Some similar information can be found in the Linux Magazine article Writing an Input Module and Michael Opdenacker’s slides on Linux USB drivers.
Since I predominantly learn by example, it was time to dig into some code. This truly is the beauty of open source. I cannot imagine trying to do something like this in a closed source ecosystem.
Getting Started
Development Environment
First, I set up Kubuntu in a VirtualBox VM. I was worried that I might make a mistake with the driver and bring my whole machine down, so isolating it in a VM seemed prudent. Next I connected a second keyboard to my system. That way when I passed the Azio keyboard through to the guest OS I would still be able to interact with the host machine.
To start, I downloaded the Linux source code to my development machine. The command on Kubuntu is:
apt-get source linux
This will download the kernel source to the current working directory and apply all of Ubuntu’s patches.
You also need to make sure you have all the build tooling installed. Nowadays it comes down to a single command:
sudo apt-get build-dep linux-image-`uname -r`
I then began spelunking around the kernel source code. The drivers directory seemed like a good place to start. Indeed, I found two files in particular that were instrumental in getting my own driver implemented. The first is the generic USB keyboard driver found at drivers/hid/usbhid/usbkbd.c
and the second is the Sega Dreamcast keyboard driver found at drivers/input/keyboard/maple_keyb.c
Digging into the Existing Drivers
I found a kernel function called printk
that allows you to write messages from the driver. I littered the existing usbkbd
driver with printk statements to figure out where and what I would need to change in order to get the keyboard working. The messages are available from the dmesg
command. On Kubuntu they are also written to /var/log/dmesg
so I was able to load the driver, run
tail -f /var/log/dmesg
and watch for the debugging statements.
Compiling the Existing Driver
The real trick was compiling the little bugger. I did not want to build the entire kernel as that is very time consuming (and unnecessary). I did not even want to build all the drivers, or hid drivers. I just wanted to build the usbkbd.c driver and load it. After a lot of searching I found that you can build it with the following command:
make modules SUBDIRS=drivers/hid/usbhid
Sweet, just the one sub directory! Just load the module with:
sudo insmod drivers/hid/usbhid/usbkbd.ko
And promptly get the following error:
insmod: error inserting ‘usbkbd.ko’: -1 Invalid module format
After lots and lots and lots of searching, with a bunch of red herrings thrown in, I found that it is not really the wrong format, it is just that the version of my precompiled kernel and the version of the module were not in sync. I found the solution in the Kernel Module Programming Guide, section 2.8 Building modules for a precompiled kernel. I needed to add Ubuntu’s version suffix. In my cases I was running patch 56 with the generic kernel, so I had to add EXTRAVERSION=-56-generic
With that problem solved I could, for the first time, load a kernel module with my edits and peer into its inner workings. I began making rapid edits whereby I was unloading, compiling, and re-loading the module in rapid succession. I created a script in the root of the Linux source tree, called rebuild
, with the following contents:
#!/bin/sh make EXTRAVERSION=-56-generic modules SUBDIRS=drivers/hid/usbhid 0=~/linux-3.2.0 sudo rmmod usbkbd sudo insmod drivers/hid/usbhid/usbkbd.ko
*the 0=
just points to the root of the Linux source tree
Do not forget to chmod +x ./rebuild
to make it executable.
Understanding the Generic Driver
Generic driver, usbkbd.c, from the Linux kernel source.
The function where the magic happens is static void usb_kbd_irq(struct urb *urb)
. It is executed with every USB interrupt (see my previous post in this series for a more detailed description of USB interrupts). The urb struct
is the USB Request Block and it holds all the information about the keypress (in this case). The function first checks the status of the URB. There are several statuses upon which it simply returns. Once that gate is cleared, the actual key code handling is executed.
The driver stores the interrupt’s key codes in the URB’s context pointer. There are two byte arrays new
and old
that hold the current and previous key codes, respectively. At the beginning of function the urb->context
pointer is copied to the local variable kbd
.
struct usb_kbd *kbd = urb->context;
All of the keycodes are stored in a 256 byte array, usb_kbd_keycode
, declared earlier in the driver. Finally, input.h
includes a function to report the keycode the kernel called input_report_key
. The first argument is the keyboard device pointer, the second is the keycode and the third is either a 1 or 0 depending if the key is down or up.
The driver contains two loops that execute to determine the key codes and their states. The first loop, uses a neat little C trick that I employed too: (kbd->new[0] >> i) & 1
It takes the first byte in the keycode, bit shifts it by 0 through 7 and masks the result with 1. If the mask results in a 0 the key is up and 1 it is down, so it just reports that to the kernel. The actual keycodes in the array are offset by 224, so it adds that to i
when indexing usb_kbd_keycode
. These keys represent the modifiers keys like Alt
and Ctrl
.
for (i = 2; i < 8; i++) { if (kbd->old[i] > 3 && memscan(kbd->new + 2, kbd->old[i], 6) == kbd->new + 8) ... input_report_key(kbd->dev, usb_kbd_keycode[kbd->old[i]], 0); ... if (kbd->new[i] > 3 && memscan(kbd->old + 2, kbd->new[i], 6) == kbd->old + 8) ... input_report_key(kbd->dev, usb_kbd_keycode[kbd->old[i]], 1); ... }The last part of the function copies the incoming keycode into the
old
array for the next iteration.
memcpy(kbd->old, kbd->new, 8);Once I had a firm understanding of how the existing driver worked, I was able to implement my own. I will cover that in the next and final post in this series.
No comments:
Post a Comment