Driver API

A short and complete API documentation can be found in the comments in the file usbdrv.h which ships with the driver. This document describes the most commonly used features.

Please note that you should create your own usbconfig.h from the driver's usbconfig-prototype.h and go through all options when you start a new project.

Initialization and administrative calls

Before you enable global interrupts with sei(), you should do the following:

  1. Ensure that the I/O pins used for USB are all inputs and the internal pull-up resistors for these pins are disabled. This is the default state after a reset.
  2. Call usbDeviceDisconnect(), wait several 100 milliseconds and then call usbDeviceConnect(). This enforces (re-)enumeration of the device. In theory, you don't need this, but it prevents inconsistencies between host and device after hardware or watchdog resets.
  3. Call usbInit() to initialize the driver.

Then enable interrupts with sei() and enter the main loop. The main loop is an endless loop which is expected to run at least one iteration every 50 ms. Somewhere during the main loop usbPoll() must be called to perform administrative tasks in the driver. If the driver calls functions in your code, it calls them from withing usbPoll().

Communicating through Control endpoint 0

This is the most basic functionality of USB. Messages sent to control endpoints consist of 8 bytes structured setup data (each byte has a particular meaning) and a block of arbitrary length which is either received from the device (control-in) or sent to the device (control-out). Since the length can be zero, the data block is optional.

The 8 bytes setup data are described by the C type usbRequest_t which is declared in usbdrv.h:

typedef struct usbRequest{
    uchar       bmRequestType;
    uchar       bRequest;
    usbWord_t   wValue;
    usbWord_t   wIndex;
    usbWord_t   wLength;
}usbRequest_t;

The first entry is a bitmask which contains the direction of the following data block (control-in or control-out) and a context for the bRequest. The context describes the recipient of the message (device, interface or endpoint and whether it's targeted at the driver, the device class or your code). For arbitrary messages in a custom class device, the recipient should be your code (which means "vendor" in USB slang). You don't need to distinguish between device, class and endpoint since it's up to you how vendor type messages are interpreted.

The second entry (bRequest) identifies the request. If your device supports multiple functions (e.g. turn LED on, turn LED off, query input status), each function can be assigned a request number. Which numbers you use is up to you, as long as they match between the host side driver and the device.

The next two entries, wValue and wIndex only have a meaning for requests defined in the USB specification or a device class specification. For vendor requests, you can send arbitrary data.

The last entry (wLength) is the length of the data block which is sent or received.

When the host sends or receives a control message on endpoint 0 which is addressed to "vendor" or "class", the function usbFunctionSetup() is called in your code. It has one parameter: A pointer to the 8 bytes of setup data. It is your duty to check which functionality should be performed and whether more data should be received or returned to the host.

The most basic example for usbFunctionSetup() could be a device which turns an LED on or off, depending on the value of wValue sent to request number 1:

usbMsgLen_t usbFunctionSetup(uchar setupData[8])
{
    usbRequest_t *rq = (void *)setupData;   // cast to structured data for parsing
    switch(rq->bRequest){
    case 1:
        setLEDStatus(rq->wValue.bytes[0]);  // evaluate low byte only
        return 0;                           // no data block sent or received
    }
    return 0;                               // ignore all unknown requests
}

Receiving data from the device

As mentioned above, control transfers can involve a data block sent back to the host. There are two ways to implement this:

  1. If you have all the data in a static buffer in RAM, you can directly return the block from usbFunctionSetup() (see example below).
  2. If the data is generated on the fly, you can provide it in a separate function in chunks of 8 bytes. This is the only way of returning longer blocks on devices with little RAM.

The first method is easy: Provided the data is in the static variable buffer, do the following:

static uchar buffer[64];
 
usbMsgLen_t usbFunctionSetup(uchar setupData[8])
{
    usbRequest_t *rq = (void *)setupData;   // cast to structured data for parsing
    switch(rq->bRequest){
    case VENDOR_RQ_READ_BUFFER:
        usbMsgLen_t len = 64;                     // we return up to 64 bytes
        if(len > rq->wLength.word)          // if the host requests less than we have
            len = rq->wLength.word;         // return only the amount requested
        usbMsgPtr = buffer;                 // tell driver where the buffer starts
        return len;                         // tell driver how many bytes to send
    }
    return 0;                               // ignore all unknown requests
}

The return type for usbFunctionSetup and the type for a len variable should be usbMsgLen_t, which is a 16bit type if USB_CFG_LONG_TRANSFERS is defined as 1 in usbconfig.h, and a 8bit type otherwise.

In the second case the data is generated dynamically. For the following example we assume that we read it with a function getData(uchar position). This function may read a byte from an external serial memory, an external device or whatever. Since we can't initialize usbMsgPtr to a place in RAM where the data resides, we instruct the driver to ask us for data in calls to the function usbFunctionRead(). You must define USB_CFG_IMPLEMENT_FN_READ to 1 in usbconfig.h if you use this method.

static uchar  currentPosition, bytesRemaining;
 
usbMsgLen_t usbFunctionSetup(uchar setupData[8])
{
    usbRequest_t *rq = (void *)setupData;   // cast to structured data for parsing
    switch(rq->bRequest){
    case VENDOR_RQ_READ_BUFFER:
        currentPosition = 0;                // initialize position index
        bytesRemaining = rq->wLength.word;  // store the amount of data requested
        return USB_NO_MSG;                          // tell driver to use usbFunctionRead()
    }
    return 0;                               // ignore all unknown requests
}
 
uchar usbFunctionRead(uchar *data, uchar len)
{
    uchar i;
    if(len > bytesRemaining)                // len is max chunk size
        len = bytesRemaining;               // send an incomplete chunk
    bytesRemaining -= len;
    for(i = 0; i < len; i++)
        data[i] = getData(currentPosition); // copy the data to the buffer
    return len;                             // return real chunk size
}

If usbFunctionRead() returns an incomplete chunk, the transfer is terminated.

Sending data to the device

If the control transfer contains payload data (other than that sent in wValue and wIndex) sent to the device, that payload data is passed to the function usbFunctionWrite() in chunks of up to 8 bytes. You must define USB_CFG_IMPLEMENT_FN_WRITE to 1 in usbconfig.h if you use this method.

The following example uses usbFunctionWrite() to store the payload data in a static buffer:

static uchar buffer[64];
static uchar currentPosition, bytesRemaining;
 
usbMsgLen_t usbFunctionSetup(uchar setupData[8])
{
    usbRequest_t *rq = (void *)setupData;   // cast to structured data for parsing
    switch(rq->bRequest){
    case VENDOR_RQ_WRITE_BUFFER:
        currentPosition = 0;                // initialize position index
        bytesRemaining = rq->wLength.word;  // store the amount of data requested
        if(bytesRemaining > sizeof(buffer)) // limit to buffer size
            bytesRemaining = sizeof(buffer);
        return USB_NO_MSG;        // tell driver to use usbFunctionWrite()
    }
    return 0;                               // ignore all unknown requests
}
 
uchar usbFunctionWrite(uchar *data, uchar len)
{
    uchar i;
    if(len > bytesRemaining)                // if this is the last incomplete chunk
        len = bytesRemaining;               // limit to the amount we can store
    bytesRemaining -= len;
    for(i = 0; i < len; i++)
        buffer[currentPosition++] = data[i];
    return bytesRemaining == 0;             // return 1 if we have all data
}

If you want to take some action when the data block is complete, perform it before returning 1 in usbFunctionWrite().

Using Interrupt- and Bulk-In endpoints

Interrupt-In endpoints are used to send data to the host spontaneously. Bulk-In endpoints are for stream type data. Since USB is a host controlled bus, the data is not actually sent on request of the device, but the host polls for the data. You can define the poll interval for interrupt endpoints in the endpoint description. Bulk endpoints are always polled at the maximum possible rate.

In order to keep the "device sends data spontaneously" semantics, you pass interrupt and bulk data by calling usbSetInterrupt() or usbSetInterrupt3() for endpoint 1 and 3 respectively. Up to 8 bytes may be passed in one call. The driver keeps the data in a buffer until it is requested by the host.

If your application sends notifications only, you don't care whether an unsent notification is lost. In this case you simply call usbFunctionInterrupt() without any checks.

However, if no interrupt data may be lost (e.g. for stream type data), you must check whether the buffer is available before calling usbSetInterrupt(). This is done with usbInterruptIsReady() and usbInterruptIsReady3() respectively. Example:

if(usbInterruptIsReady()){               // only if previous data was sent
    uchar *p;
    uchar len = getInterruptData(&p);   // obtain chunk of max 8 bytes
    if(len > 0)                         // only send if we have data
        usbSetInterrupt(p, len);
}

Using Interrupt- and Bulk-Out endpoints

Interrupt- and Bulk-Out endpoints are used to send stream type data to the device. When the host sends a chunk of data on the endpoint, the function usbFunctionWriteOut() is called. If you use more than one interrupt- or bulk-out endpoint, the endpoint number is passed in the global variable usbRxToken. You must define USB_CFG_IMPLEMENT_FN_WRITEOUT to 1 in usbconfig.h when you use this feature.

Example:

void usbFunctionWriteOut(uchar *data, uchar len)
{
    if(usbRxToken == 2){
        processInterruptOut2Data(data, len);
    }else{
        processInterruptOut4Data(data, len);
    }
}

Intercepting standard requests

The driver handles all standard requests defined in the USB specification in a minimal way. If you are not happy with the way a request is handled, don't modify the driver. Use the macro USB_RX_USER_HOOK() in usbconfig.h instead. This macro is inserted in the handling of setup data. See the source code in usbdrv.c to see what can be modified. If you replace default functionality, end the macro with a return statement.

A simple example for such a macro is the flashing of an LED on USB traffic:

#define USB_RX_USER_HOOK(data, len)     if(usbRxToken == (uchar)USBPID_SETUP) blinkLED();

This method is used by USB2LPT to flash an LED.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License