Linux Kernel Device Drivers for AVR V-USB Devices

V-USB is a really convenient library to implement USB communication between an AVR microcontroller and any USB host enabled system.

The host side software for a V-USB device is usually handled either from a class driver, such as for HID-compliant devices, or from an userspace libusb-based application.

This post shows how to implement a Linux kernel device driver for a simple ep0-based V-USB device.

https://fabiobaltieri.com/wp-content/uploads/2012/05/vusb-kernel-intro.jpg

Why?

The first question is: why would you want to implement the driver at kernel level?

Generally, you don’t. Most non class-compliant V-USB devices are more suited to direct userspace usage. That’s simpler to implement, debug and port.

The special case is when you want to interface directly with a kernel subsystem, such as the LED framework in my example.

Generally, you want to write a Linux device driver only if you need to use the device from some kind of Linux-specific framework and you can’t fit your device in any USB class.

Still, this example shows basic API of a Linux USB device driver

Hardware

The hardware of this example is one of my avr-micro-usb keys, modified to drive an IR LED from its GPIO. The IR LED used has a normal red LED wired in reverse polarity with the IR one, so the package has been connected between two GPIOs to be able to drive it in both ways.

AVR GPIOs are capable to drive up to 40mA by specifications, so they can directly handle LED current.

That’s the board…

https://fabiobaltieri.com/wp-content/uploads/2012/05/vusb-kernel-board.jpg

…and that’s – conceptually – the schematics…

https://fabiobaltieri.com/wp-content/uploads/2012/05/vusb-kernel-led.png

USB control endpoint

The USB device is managed at not-so-low level using endpoints. The endpoint zero is called the “control endpoint”, and its requests are called “setup requests”, it’s the only one which is bidirectional and a subset of its API is common between all USB devices, as they’re used by the host OS to download device descriptors and ultimately choose the right driver and configuration.

Device vendors are free to extend standard and class requests with their own vendor requests.

In case of the V-USB stack, standard requests (type == USBRQ_TYPE_STANDARD) are handled by the stack, while all other requests are passed to the external usbFunctionSetup function, as in:

/* from usbdrv/usbdrv.h */
struct usbRequest {
    uchar       bmRequestType;
    uchar       bRequest;
    usbWord_t   wValue;
    usbWord_t   wIndex;
    usbWord_t   wLength;
};

/* usually found in main.c */
usbMsgLen_t usbFunctionSetup(uint8_t data[8])
{
        struct usbRequest *rq = (void *)data;
        ...
}

where rq→bmRequestType & USBRQ_TYPE_MASK can be USBRQ_TYPE_CLASS or USBRQ_TYPE_VENDOR.

At some point in usbFunctionSetup the code may take some action depending on bRequest, wValue and wIndex, which may be used arbitarily for USBRQ_TYPE_VENDOR requests (or not used at all), as in:

/* from requests.h */
#define CUSTOM_RQ_R_ON          3
#define CUSTOM_RQ_OFF           5

/* this only receives USBRQ_TYPE_VENDOR requests on this device */
usbMsgLen_t usbFunctionSetup(uint8_t data[8])
{
        struct usbRequest *rq = (void *)data;

        switch (rq->bRequest) {
        ...
        case CUSTOM_RQ_R_ON:
                led_r_on();
                break;
        case CUSTOM_RQ_OFF:
                led_off();
                break;
        ...
        }

        return 0;
}

Note that, through it’s not common for V-USB devices, many other USB device implementation includes bmRequestType in the switch case, thus checking the requst against other fields, such as request direction, which in this case is ignored (its in the most significant bit of bmRequestType).

Additionaly, the host may send a data payload, handled by usbFunctionRead, and may recive some data back either by pointing usbMsgPtr to it and returning the length, or with the usbFunctionWrite function.

So, to sum up, our API is just two vendor specific requests, with no other arguments or payload. That’s a simple starting point to implement our driver.

Kernel API – Driver Registration

USB kernel drivers starts by registering the usb_driver structure, which is conveniently handled for us by the module_usb_driver macro, as in:

#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/usb.h>

...

/* BIG WARNING: read the notes about shared USB-IDs if you use shared IDs */
#define AVR_LED_VENDOR_ID       0x16c0
#define AVR_LED_PRODUCT_ID      0x05dc

...

static const struct usb_device_id id_table[] = {
        { USB_DEVICE(AVR_LED_VENDOR_ID, AVR_LED_PRODUCT_ID) },
        { },
};
MODULE_DEVICE_TABLE(usb, id_table);

...

static struct usb_driver avr_led_driver = {
        .name =         "avr-micro-usb",
        .probe =        avr_led_probe,
        .disconnect =   avr_led_disconnect,
        .id_table =     id_table,
};

module_usb_driver(avr_led_driver);

From now on, each time the kernel finds a new USB device which matches provided IDs, it will call the probe function.

As we are using obdev’s shared USB IDs, we must respect obdev’s rules for ID discrimination by textual name, so that other USB devices with the same ID are not taken by our driver.

So, that’s the important code of the probe function:

#define AVR_LED_MANUFACTURER    "www.fabiobaltieri.com"
#define AVR_LED_PRODUCT         "IR-Trigger"

struct avr_led {
        struct usb_device       *udev;

        /* other instance-specific fields */
        ...
};

static int avr_led_probe(struct usb_interface *interface,
                         const struct usb_device_id *id)
{
        struct usb_device *udev = interface_to_usbdev(interface);
        struct avr_led *dev;
        int err;

        if (strcmp(udev->manufacturer, AVR_LED_MANUFACTURER))
                return -ENODEV;
        if (strcmp(udev->product, AVR_LED_PRODUCT))
                return -ENODEV;

        dev = kzalloc(sizeof(struct avr_led), GFP_KERNEL);
        if (dev == NULL) {
                dev_err(&interface->dev, "kzalloc failed\n");
                err = -ENOMEM;
                goto error;
        }

        dev->udev = usb_get_dev(udev);
        usb_set_intfdata(interface, dev);

        /* framework specific code */
        ...

        return 0:

        /* other error unwinding code */
        ...
error:
        kfree(dev);
        return err;
}

Strictly speaking, obdev rules imposes that for shared IDs, libusb must be used (see point 6 of USB-IDs-for-free.txt in the usbdrv directory), but as this code works samelessly with other libusb-based devices with the same ID on the same system, I’d say that this is ok. Otherwise, “just” use your own vendor/product ID.

The disconnect handler is called when the device is removed, and simply frees all instance datas.

static void avr_led_disconnect(struct usb_interface *interface)
{
        struct avr_led *dev;

        dev = usb_get_intfdata(interface);

        /* framework specific code */
        ...

        usb_set_intfdata(interface, NULL);
        usb_put_dev(dev->udev);

        kfree(dev->cdev.name);
        kfree(dev);
}

The important thing to keep in mind is that USB drivers are always instantiable, so that each new device gets its own intfdata, allocated at enmeration by the probe function and freed at removal by disconnect.

This data includes a pointer to its own struct usb_device structure in the udev field, which is used by other functions to address the USB device, and in other framework callbacks to get back to the complete structure using the container_of macro.

Kernel API – Control Requests

Now that our driver “owns” the USB device, you only need to know how to send vendor specific control requests to it. That’s the function prototype – just don’t be scared by the number of arguments:

/* from linux/usb.h and drivers/usb/core/message.c */

/**
 * usb_control_msg - Builds a control urb, sends it off and
 *      waits for completion
 * @dev: pointer to the usb device to send the message to
 * @pipe: endpoint "pipe" to send the message to
 * @request: USB message request value
 * @requesttype: USB message request type value
 * @value: USB message value
 * @index: USB message index value
 * @data: pointer to the data to send
 * @size: length in bytes of the data to send
 * @timeout: time in msecs to wait for the message to complete
 *      before timing out (if 0 the wait is forever)
 *
 * Context: !in_interrupt ()
 *
 * This function sends a simple control message to a specified
 * endpoint and waits for the message to complete, or timeout.
 *
 * If successful, it returns the number of bytes transferred,
 * otherwise a negative error number.
 *
 * ...
 */
int usb_control_msg(struct usb_device *dev, unsigned int pipe,
                __u8 request, __u8 requesttype,
                __u16 value, __u16 index,
                void *data, __u16 size,
                int timeout);

/* endpoint "pipe" selection */
#define usb_sndctrlpipe(dev, endpoint) ...
#define usb_rcvctrlpipe(dev, endpoint) ...

While this may look complicated, it’s practically quite simle to use, as most of the arguments are fixed, as in:

#define CUSTOM_RQ_R_ON          3

struct avr_led *dev
int err;

...

err = usb_control_msg(dev->udev,
                usb_sndctrlpipe(dev->udev, 0),
                CUSTOM_RQ_R_ON,
                USB_TYPE_VENDOR | USB_DIR_OUT,
                0, 0,
                NULL, 0,
                1000);
if (err < 0)
        dev_err(&dev->udev->dev,
                        "%s: error sending control request: %d\n",
                        __func__, err);

In the details:

  • *dev is a pointer of our struct usb_device, usually saved in our instance structure.
  • pipe identifies a specific endpoint using a usb pipe selection macro. EP number is always zero for control endpoint.
  • request is our request ID, matches with out firmware’s rq→bRequest.
  • requesttype are request flags, including direction and recipient, we use USB_TYPE_VENDOR | USB_DIR_OUT as we are sending a control-out vendor specific request.
  • value and index: used as argument for this request.
  • data and size: payload/buffer address and size for the request.
  • timeout: timeout for request execution.

Also note that USB device related printk, usually uses some function of the dev_ family, with &dev→udev→dev as the first argument, to identify the specific device in the message.

Conclusions and Further Readings

Many simple USB devices can get over with just this interface for host-device communication. More complex ones will need read/write functions (see source code for i2c-tiny-usb as an example), and additional endpoints.

Linux kernel sources are full of examples for many USB devices, if you are designing an USB vendor specific protocol you may find useful to study other driver’s code.

Also worth mentioning is Beyond Logic’s USB in a NutShell guide.

The complete source code for my examples can be found on GitHub, in the “firmware-nikon-remote” and “kernel-led” directories.


GitHub

2 Responses to Linux Kernel Device Drivers for AVR V-USB Devices

  1. cpuguy says:

    Ciao Fabio,

    do you think that your code could be modified to send MIDI messages from an AKAI LPK25 USB MIDI keyboard via an IR diode without requiring a USB host-shield etc.?

    http://blog.makezine.com/2010/11/30/usbhacking/

    I have a second device which is going to decode the IR stream and synthesize the music.

    Grazie,

    -David

    • Hello David,

      let me understand – you want to use the ir port of the usb-midi-keyboard to send a stream to an external device?

      Assuming that this is possible and you know the protocol (an extension of the usb-midi class protocol maybe?) you may probably want to do that in userspace with libusb + alsalib anyway, as that’s much simpler.

      Fabio

Leave a reply to fabiobaltieri Cancel reply