Features by Example

In this section we list various features of V-USB and give examples how they can be used or how a particular functionality can be implemented.

Dynamic or complex descriptors

V-USB comes with a built-in set of USB descriptors. These descriptors are sufficient for custom class devices using only endpoint 0 and optionally interrupt endpoint 1 and for HID class devices. If you want to do more complex things, you must provide your own descriptors.

V-USB has a very versatile interface how descriptors can be provided to the driver. It is even possible to decide which set of descriptors are used at run-time, e.g. depending on a jumper setting. [If you want to write a section about USB descriptors, please do so and link it here!]

Descriptors can be provided in one of four ways:

  1. You can decide to use V-USB's default descriptor (with eventually resides in flash memory).
  2. In flash memory.
  3. In RAM.
  4. By means of a function which returns the descriptor.

This can be configured for each descriptor in usbconfig.h by defining USB_CFG_DESCR_PROPS_*.

Custom descriptor in flash memory

To replace the default configuration descriptor (for example, you can do this with any other descriptor), declare it in your main code:

PROGMEM char usbDescriptorConfiguration[DESCRIPTOR_SIZE] = {
    9,                  // length of descriptor in bytes
    USBDESCR_CONFIG,    // descriptor type
    ...
};

And in usbconfig.h define
#define DESCRIPTOR_SIZE 45   // use correct size here
#define USB_CFG_DESCR_PROPS_CONFIGURATION USB_PROP_LENGTH(DESCRIPTOR_SIZE)

You must know the descriptor size in bytes and define it in usbconfig.h.

Custom descriptor in RAM

This is similar to example above:

char usbDescriptorConfiguration[DESCRIPTOR_SIZE]; // is initialized at runtime

And in usbconfig.h define
#define DESCRIPTOR_SIZE 45   // use correct size here
#define USB_CFG_DESCR_PROPS_CONFIGURATION (USB_PROP_IS_RAM | USB_PROP_LENGTH(DESCRIPTOR_SIZE))

The descriptor size must be known at compile time.

Custom descriptor provided by function

This method offers the most flexibility. In usbconfig.h define

#define USB_CFG_DESCR_PROPS_CONFIGURATION USB_PROP_IS_DYNAMIC

In your main code implement the function usbFunctionDescriptor(). This function is similar to usbFunctionSetup(). All methods available in usbFunctionSetup() to return data can also be used here to return the descriptor data. You can set usbMsgPtr and return the number of bytes or you can delegate to usbFunctionRead() by returning -1. An example for this can be found in the firmware of AVR-Doper in main.c.

Clocking the AVR from the RC oscillator with auto-calibration

V-USB requires a precise clock because it synchronizes to the host's data stream at the beginning of each packet and then samples the bits in constant intervals. The longest data packet for low speed USB is 11 bytes. Since we don't need the CRC, we read 9 bytes at maximum. Including stuffed bits, that's a maximum of 84 bits. Bit sampling must not drift more than 1/4 bit during these 84 bits, resulting in a requirement of 0.3% clock precision.

Calibrating the AVR's internal RC oscillator to 12 MHz with 0.3% precision is out of the specified range.
[It may be practically possible, though. If you experiment with the RC oscillator calibrated to 12 MHz, please post your results here.]
I have checked this, without luck, as expected. Some host controllers can detect such a USB device but will get out of communication later. Most host controllers don't even detect such devices. henni

V-USB includes a 16.5 MHz module which has a built-in Phase Locked Loop and inserts leap bits to stay in sync with the sender. This module tolerates slightly more than 1% deviation from the nominal clock. This precision is within the specified range of Atmel's RC oscillators. However, the clock rate of 16.5 MHz is out of range for most AVRs. Only some devices have an additional clock doubler built-in which can double the RC oscillator clock. This option is called "High Frequency PLL Clock" and can be selected with the CKSEL fuse bits. AVRs known to have this option are the ATTiny25, ATTiny45, ATTiny85, ATTiny26 (not recommended for new designs), ATtiny261, ATtiny461, ATtiny861.

Regardless of the actual CPU clock, the RC oscillator must be calibrated to a precise source. This source can be the USB frame clock of 1 millisecond. After a USB reset, low speed devices are supplied with the frame clock for several milliseconds before any communication starts. We can use this interval for calibration.

V-USB has a hook where you can call a user supplied function when the USB reset state ends, see the USB_RESET_HOOK() in usbconfig-prototype.h. This is the ideal point to insert a calibration function. The following example is taken from EasyLogger:

In usbconfig.h add

#ifndef __ASSEMBLER__
extern void usbEventResetReady(void);
#endif
#define USB_RESET_HOOK(isReset)         if(!isReset){usbEventResetReady();}
#define USB_CFG_HAVE_MEASURE_FRAME_LENGTH   1

And in your main code:
static void calibrateOscillator(void)
{
uchar       step = 128;
uchar       trialValue = 0, optimumValue;
int         x, optimumDev, targetValue = (unsigned)(1499 * (double)F_CPU / 10.5e6 + 0.5);
 
    /* do a binary search: */
    do{
        OSCCAL = trialValue + step;
        x = usbMeasureFrameLength();    // proportional to current real frequency
        if(x < targetValue)             // frequency still too low
            trialValue += step;
        step >>= 1;
    }while(step > 0);
    /* We have a precision of +/- 1 for optimum OSCCAL here */
    /* now do a neighborhood search for optimum value */
    optimumValue = trialValue;
    optimumDev = x; // this is certainly far away from optimum
    for(OSCCAL = trialValue - 1; OSCCAL <= trialValue + 1; OSCCAL++){
        x = usbMeasureFrameLength() - targetValue;
        if(x < 0)
            x = -x;
        if(x < optimumDev){
            optimumDev = x;
            optimumValue = OSCCAL;
        }
    }
    OSCCAL = optimumValue;
}
 
void    usbEventResetReady(void)
{
    cli();  // usbMeasureFrameLength() counts CPU cycles, so disable interrupts.
    calibrateOscillator();
    sei();
    eeprom_write_byte(0, OSCCAL);   // store the calibrated value in EEPROM
}

This code first does a binary search and then a neighborhood search to obtain the optimum OSCCAL value. Please note that the binary search may set a broad range of OSCCAL values which may exceed the maximum operating frequency for low supply voltages.

The FunkUsb sample uses another approach for synchronizing. As a long-term working device with changing chip temperature (the antenna is best placed beneath a window), it does synchronization permanently, using a timer channel for SOF frequency measuring. A permanently-running 8-bit timer without overflow interrupt is sufficient and can be used for PWM generation too. Some changes to AVRUSB core were necessary. Of course, DATA– must be used as interrupt input.

Re-Enumeration

USB devices are addressed by a 7 bit USB address which is assigned when the device is plugged in during the Enumeration Phase. The device starts with address 0 until the host assigns a new address to it.

Enumeration is only performed when the device is connected to the bus. If the device has a CPU reset, it's memory is usually cleared and the assigned address is lost. Since the host does not know that the device had a reset, it still addresses the device under it's old address, but the device won't answer.

It is therefore useful to simulate a device disconnect in the device initialization code. This ensures that host and device agree on the same address. You should therefore insert the following code in your initialization (best before usbInit() because the USB interrupt must be disabled during disconnect state):

usbDeviceDisconnect();
uchar i = 0;
while(--i){         // fake USB disconnect for > 250 ms
    wdt_reset();    // if watchdog is active, reset it
    _delay_ms(1);   // library call -- has limited range
}
usbDeviceConnect();

The host detects whether a device is connected or not by the pull-up resistor on D-. The clean approach is therefore to make the pull-up switchable. V-USB uses this method if you have defined the pin where the pull-up resistor is connected in usbconfig.h (macros USB_CFG_PULLUP_IOPORTNAME and USB_CFG_PULLUP_BIT).

If these macros are not defined and the pull-up resistor is hard-wired, V-USB simulates the disconnect by pulling D- to ground. This is the same voltage level as seen without the pull-up.

Emulating an existing device

Sometimes you want to copy the interface of an existing device. For example, if you make a boot loader, you may want to fake an existing programmer to the host so that your programming software can use the boot loader without changes. In this case you must copy the USB descriptors including all endpoint descriptors from the device you want to emulate. You must also use the same endpoint numbers and the same endpoint types as the original device.

V-USB uses endpoint numbers 1 and 3 for interrupt- or bulk-in endpoints and any number (except 0, of course) for interrupt- or bulk-out endpoints. Since this may not match the device you want to emulate, you can configure these numbers in usbconfig.h. What is referenced as endpoint 3 can be configured to any number with the macro USB_CFG_EP3_NUMBER. All other in-type endpoints are interpreted as endpoint 1. All out endpoints can be identified individually anyway.

For an example how Atmel's USB driven programmers can be emulated, see the AVRminiProg project.

Other interrupts than INT0 for USB

V-USB uses hardware interrupt INT0 by default because it must have the highest priority among all used interrupts. You can configure it to use another interrupt if you make sure that higher level interrupts are never enabled.

There are a couple of defines associated with the interrupt in usbconfig.h. The defaults are

#define USB_INTR_CFG            MCUCR    // register where interrupt features are configured
#define USB_INTR_CFG_SET        ((1<<ISC00) | (1<<ISC01))   // feature bits to set
#define USB_INTR_CFG_CLR        0       // feature bits to clear
#define USB_INTR_ENABLE         GIMSK   // register where interrupt enable bit resides
#define USB_INTR_ENABLE_BIT     INT0    // bit number in above register
#define USB_INTR_PENDING        GIFR    // register where interrupt pending bit resides
#define USB_INTR_PENDING_BIT    INTF0   // bit number in above register
#define USB_INTR_VECTOR         SIG_INTERRUPT0  // interrupt vector

When you change the interrupt feature, enable and pending registers and bits, don't forget to also update the interrupt vector!

The AVRUSB core works without change using interrupts by DATA+ or DATA–, and with low-level triggering at DATA– too. The latter enables usage of an INT pin in deep standby mode of AVR controller. Furthermore, code that resets the IRQ state isn't necessary inside AVRUSB core.

In rare cases, the INT inputs are not free for AVRUSB, and someone must use some pin-change interrupts available at ATtiny and the newer ATmegaX8 family.
A working implementation that (mis)uses ICP (input capture) for USB DATA– interrupting is USB2LPT. The INT pins are not usable in this example because this device needs a true 8-bit port where all pins are toggling the same time when requested.

Implementing suspend mode

The USB standard defines a suspend mode. When the host computer goes into sleep mode, it requests that all USB devices go into a low power suspend mode. Suspend mode is signaled to the devices by the absence of any USB activity.

V-USB does not implement suspend mode by itself. This is the task of the main application. However, it offers hooks to check for USB activity. Since the only USB activity seen may be the frame pulse on DATA–, this data line must be connected to an interrupt. The easiest way to do this is to use DATA– for USB interrupts (not DATA+ as usual). Then define USB_COUNT_SOF to 1 in usbconfig.h and watch the global variable usbSofCount in your main loop. If it stops incrementing, you should put the device into suspend mode and wait for activity on the USB.

An example for this type of suspend mode implementation can be found in USB2LPT. This example uses the watchdog as timer to detect missing SOFs = USB standby. For USB wakeup detecting (SE0 = both DATA lines low for some 10 ms), the example uses either the watchdog for ATmega8 too, or the asynchronous pin-change interrupt available for the ATmega48. Both methods have advantages and disadvantages, especially in 5 V designs.

Using DATA– as main interrupt (INT0 or INT1) is the usual approach. An example for this method can be found in the Datenhandschuh project.

Basic tutorial

You can find a V-USB tutorial explaining a basic setup of BootloadHID + Vendor type requests sent to custom HID class device.

After-the-fact CRC checking

For some/all configurations V-USB doesn't fully check the USB CRC of data received from the host. This prevents it from giving the host immediate notification so it can be transparently re-sent. However, your code can still verify the received data's CRC and take some kind of action if it's corrupt. You might avoid acting on it, and perhaps notify the host program of this error in a custom way (not available if you're implementing a standard device where errors are assumed to be reported in the normal USB way).

This code detects errors in requests sent to your code (but not ones that V-USB handles internally):

uchar usbFunctionSetup( uchar data [8] )
{
    if ( usbCrc16( data, 8 + 2 ) != 0x4FFE )
    {
        // CRC error
    }
    ...
}

To report the error, you might return 0 for a zero-length reply if the host software was expecting a non-zero-length reply.

This detects a CRC error for data passed to your write function:

uchar usbFunctionWrite( uchar* data, uchar len )
{
    if ( usbCrc16( data, len + 2 ) != 0x4FFE )
    {
        // CRC error
    }
    ...
}

Here you might report the error by returning 0xFF, which V-USB interprets as an error and gives some kind of host notification on.

The above checks the CRC by relying on the property that the checksum of the data with its checksum appended is always 0x4FFE (if the checksum is correct). This is due to the way the appended checksum cancels out the current checksum and makes the entire checksum act like it was on two zero bytes and nothing more.

The checksum can be verified in slightly less time (though more generated code) by checking it in the more conventional way:

unsigned crc = usbCrc16( data, len );
if ( (crc & 0xFF) != data [len] || (crc >> 8 & 0xFF) != data [len+1] )
{
    // CRC error
}

This might save 20-80 cycles, at a cost of tens of bytes of code.

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