Cockpit: Structure of a basic driver

August 25, 2024

Last time, we left things off pondering if a kernelspace driver is the way to go as there already is a basic one. I no longer care what the right answer is here, only about which is the most fun way to go about this. So yes, we are building a driver. In this post we are going to look through the entire setup and basic structure of a driver, without really implementing anything useful yet. Think of it as the "Hello, world!" for drivers.

Despite this being the simplest driver we can write, there is still a lot of details to get right. First of all, system dependencies:

sudo apt-get install gcc-12 \
                     flex \
                     bison \
                     linux-headers-$(uname -r)

A basic driver

There are 2 parts to a basic driver, the driver definition, and fitting it into a Linux Module.

#include <linux/module.h>
#include <linux/init.h>
#include <linux/usb.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Giorgos Makris");
MODULE_DESCRIPTION("Driver for the WinWing UFC1");

#define VENDOR_ID 0x4098
#define PRODUCT_ID 0xbed0

static struct usb_device_id usb_table[] = {
  {USB_DEVICE(VENDOR_ID, PRODUCT_ID)},
  {},
};

MODULE_DEVICE_TABLE(usb, usb_table);

static int probe(struct usb_interface *intf, const struct usb_device_id *id) {
  printk("winwing_ufc1_devdrv - Probed\n");
  return 0;
}

static void disconnect(struct usb_interface *intf){
  printk("winwing_ufc1_devdrv - Disconnected\n");
}

static struct usb_driver driver = {
  .name = "winwing_ufc1_devdrv",
  .id_table = usb_table,
  .probe = probe,
  .disconnect = disconnect
};

static int __init ww_ufc_init(void) {
  int registered = usb_register(&driver);

  if (registered) {
    printk("winwing_ufc1_devdrv - Error: could not register driver!\n");
    return -registered;
  }
  printk("winwing_ufc1_devdrv - Initialized driver\n");
  return 0;
}

static void __exit ww_ufc_exit(void) {
  usb_deregister(&driver);
  printk("winwing_ufc1_devdrv - Unloaded driver\n");
}

module_init(ww_ufc_init);
module_exit(ww_ufc_exit);

Licenses

I am gonna start with MODULE_LICENSE, because that's the one that was the weirdest one for me. Originally I had it set to MIT. Tough luck, turns out that if you don't have the right license specified, compilation breaks.

Here are the logs when you have the wrong license:

$ make

make -C /lib/modules/6.5.0-41-generic/build M=/home/gmakris/project/winwing-ufc1-driver modules
make[1]: Entering directory '/usr/src/linux-headers-6.5.0-41-generic'
  CC [M]  /home/gmakris/project/winwing-ufc1-driver/winwing_ufc1_devdrv.o
  MODPOST /home/gmakris/project/winwing-ufc1-driver/Module.symvers
ERROR: modpost: GPL-incompatible module winwing_ufc1_devdrv.ko uses GPL-only symbol 'usb_deregister'
ERROR: modpost: GPL-incompatible module winwing_ufc1_devdrv.ko uses GPL-only symbol 'usb_register_driver'
make[3]: *** [scripts/Makefile.modpost:144: /home/gmakris/project/winwing-ufc1-driver/Module.symvers] Error 1
make[2]: *** [/usr/src/linux-headers-6.5.0-41-generic/Makefile:1991: modpost] Error 2
make[1]: *** [Makefile:234: __sub-make] Error 2
make[1]: Leaving directory '/usr/src/linux-headers-6.5.0-41-generic'
make: *** [Makefile:4: all] Error 2

usb_deregister and usb_register_driver are GPL-only symbols. Unlike the kernel, I couldn't care less about the license, so I just switched it...

Driver layout

We've looked at how to find the vendor_id and product_id in a previous post. They are important here because handing them over to the usb_table is what the kernel will use to match a driver to a connected device. There are ways to be more generic and write a driver that allows the kernel to match it to multiple devices but we don't need that, we have one device and our driver is tailored to it.

The usb_table, along with the probe and disconnect functions are what we need to define a driver. We pack all that in a struct driver and then make use of them in the module init and exit functions (ww_ufc_init and ww_ufc_exit).

Installing the driver

Linux actually does a great work at being modular and allowing you to easily plug in new modules, such as a driver. After bulding with make it is just a matter of running sudo insmod winwing_ufc1_devdrv.ko. If it fails a more descriprive error message should be in dmesg.

I found out the ugly way that uname -r can lie to you if your system has updated since it booted. In that case you will get an error like this:

$ dmesg

[  177.492936] module winwing_ufc1_devdrv: .gnu.linkonce.this_module section size must match the kernel's built struct module size at run time

If this happens then reboot and apt-get install --reinstall linux-headers-$(uname -r).

In any case, if it all works, then that's what dmesg should show:

$ dmesg

[  305.695775] usbcore: registered new interface driver winwing_ufc1_devdrv
[  305.695784] winwing_ufc1_devdrv - Initialized driver

Even though the driver is initialized at the kernel level we do not see the probe message, which means that it was not assigned to the device. There could be a couple of reasons for that but the most probable one I found is that the device itself declares that it is an HID device. So the above driver would not even be considered in this case.

Time to convert it to an HID driver and look at the difference then.

Converting to HID

#include <linux/module.h>
#include <linux/init.h>
#include <linux/hid.h>
#include <linux/hidraw.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Giorgos Makris");
MODULE_DESCRIPTION("Driver for the WinWing UFC1");

#define VENDOR_ID 0x4098
#define PRODUCT_ID 0xbed0

static struct hid_device_id device_table[] = {
  {HID_USB_DEVICE(VENDOR_ID, PRODUCT_ID)},
  {},
};

MODULE_DEVICE_TABLE(hid, device_table);

static int probe(struct hid_device *hdev, const struct hid_device_id *id) {
  printk("winwing_ufc1_devdrv: probed\n");
  return 0;
}

static int input_configured(struct hid_device *hdev, struct hid_input *hidinput) {
  return 0;
}

static int raw_event(struct hid_device *hdev, struct hid_report *report, u8 *raw_data, int size) {
  return 0;
}

static struct hid_driver ww_ufc1_driver = {
  .name = "winwing_ufc1_devdrv",
  .id_table = device_table,
  .probe = probe,
  .input_configured = input_configured,
  .raw_event = raw_event
};

module_hid_driver(ww_ufc1_driver);

Not many changes, mostly removing stuff and changing names. HID drivers are more standardized than generic USB drivers so we no longer need to have the explicit module declaration and pass it the __init and __exit functions. No more registering and deregistering, the HID module will handle that for us.

Overall seems like what I should have used in the first place, just some things you have to figure out as you go. We will take a better look at what the new functions added are for when they become necessary.

Replugging the device now shows this:

$ dmesg

[ 3451.393737] winwing_ufc1_devdrv: module verification failed: signature and/or required key missing - tainting kernel
[ 3451.445871] winwing_ufc1_devdrv - probed

Not even caring about module verification failed until it becomes a problem. Just glad that is was picked up.

Thoughts so far

It's not that I've done anything special so far, I wanted to explain the basic structure so any work that follows is easier to comprehend, both for me and others. Learned a bit about drivers and how the kernel goes about selecting the right one and it has been fun!

Resources

Here is what has helped me go through this so far:

  • Johannes 4GNU_Linux has some awesome material, definitely worth watching
  • Linux Device Drivers, I have not read through the entire thing but what I have read has been great to get my mind thinking the right way about this
  • Another WingWing driver has also been interesting to look at when doing the HID conversion

code is available here.