In this section we list various features of AVR-USB and give examples how they can be used or how a particular functionality can be implemented.
Dynamic or complex descriptors
AVR-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.
AVR-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:
- You can decide to use AVR-USB's default descriptor.
- In flash memory.
- In RAM.
- 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
AVR-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.]
AVR-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.
AVR-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. If you find better ways to calibrate the oscillator, please post them here!
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. AVR-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, AVR-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.
AVR-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
AVR-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!
[If you have an example project which uses the Pin Change interrupt for USB, please link it here.]
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.
AVR-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 D-, this data line must be connected to an interrupt. The easiest way to do this is to use D- for USB interrupts (not D+ 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.
Connecting D- to a second interrupt (e.g. INT1) is another approach. An example for this method can be found in the Datenhandschuh project.