AVR ATtiny USB Tutorial Part 3

This is the third part of my USB tutorial for ATtiny2313 and V-USB library. In the second part we got the breadboard setup more or less covered, and now is the time for actual code! This will most likely be the longest of the three parts, so let’s get started.

Adding V-USB as a part of your project

First, we will download the latest version V-USB library from OBdev. Head to the Downloads-section and get the latest .zip – I got vusb-20120109.zip.

Unzip the archive and copy the usbdrv subfolder to your project folder (the whole folder, not just contents). Go to the subfolder and make a copy of usbconfig-prototype.h with the name usbconfig.h. Locate the #define lines for IO port and port bits and clock rate, and update them as necessary to reflect our configuration where D+ is in PD2 and D- in PD3 and clock rate is 12 MHz:

#define USB_CFG_IOPORTNAME      D
#define USB_CFG_DMINUS_BIT      3
#define USB_CFG_DPLUS_BIT       2
#define USB_CFG_CLOCK_KHZ       12000


It’s also a good idea to ensure that V-USB tells the computer that it is powered via USB (i.e. not self powered) and takes maximum of 50 mA of power (defaults in my version is USB power, 100 mA max.):

#define USB_CFG_IS_SELF_POWERED         0
#define USB_CFG_MAX_BUS_POWER           50

We will be using OBdev’s licenced vendor and device IDs, so they don’t need to be changed (note that this also means my tutorial will fall under GPL, including the schematic in part 2). But we do want to customize the vendor name and device name (note the backward slash in vendor name string used to split the #define to two lines so it fits into this blog post):

#define  USB_CFG_VENDOR_ID       0xc0, 0x16 /* = 0x16c0 */
#define  USB_CFG_DEVICE_ID       0xdc, 0x05 /* = 0x05dc */

#define USB_CFG_DEVICE_VERSION  0x00, 0x01

#define USB_CFG_VENDOR_NAME     'c', 'o', 'd', 'e', 'a', 'n', 'd', 'l', \
                                'i', 'f', 'e', '.', 'c', 'o', 'm'
#define USB_CFG_VENDOR_NAME_LEN 15

#define USB_CFG_DEVICE_NAME     'U', 'S', 'B', 'e', 'x', 'a', 'm', 'p', 'l', 'e'
#define USB_CFG_DEVICE_NAME_LEN 10

The usbconfig.h header file is well documented so you may want to read the rest of the options to get an idea what the library can can do. Now the only thing missing is the actual C file to use the library. Here is the barebones version of main.c we’ll start with:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>

#include "usbdrv.h"

#define F_CPU 12000000L
#include <util/delay.h>

USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) {
	return 0; // do nothing for now
}

int main() {
	uchar i;

    wdt_enable(WDTO_1S); // enable 1s watchdog timer

    usbInit();
	
    usbDeviceDisconnect(); // enforce re-enumeration
    for(i = 0; i<250; i++) { // wait 500 ms
        wdt_reset(); // keep the watchdog happy
        _delay_ms(2);
    }
    usbDeviceConnect();
	
    sei(); // Enable interrupts after re-enumeration
	
    while(1) {
        wdt_reset(); // keep the watchdog happy
        usbPoll();
    }
	
    return 0;
}

The code should be pretty straightforward to understand:

  • Include the usbdrv.h to access V-USB functions
  • Implement usbFunctionSetup() to handle USB requests (we’ll do that soon)
  • In the main function, set up a 1 second watchdog timer that resets the microcontroller if 1000 milliseconds pass without a call to wdt_reset()
  • Call usbInit() to initialize the V-USB library
  • Enforce USB device re-enumeration using usbDeviceDisconnect(), a 500 ms delay (while calling the watchdog reset every 2 ms) and usbDeviceConnect()
  • Enable interrupts
  • Loop forever while calling the watchdog reset and usbPoll()

The reason we are using the watchdog timer is, that our code might for some reason freeze (for example, corrupt data is read and a bug or eternal loop occurs) and the USB device would then stop responding. In this case, the wdt_reset() gets no longer called and after 1 second, the watchdog timer automatically resets our ATtiny, and the program execution starts again (like you’d just resetted the circuit yourself). While this is not absolutely necessary, it is a good practice and saves the user from unplugging and replugging the device if something strange happens.

Another thing you may wonder is why the disconnect/delay/connect -procedure is needed at all. This is because the host PC can remember the identifier assigned to a USB device even if our device resets and forgets that identifier. By enforcing re-enumeration we make sure that both the host PC and our device have the same ID used to communicate over the USB bus.

Now let’s see if we can get it all to compile. The easiest way is to use my ready-made Makefile which also contains the necessary statements for our PC side command-line client. Put the makefile to your project folder and run make main.hex too see if it works. This way, you also see the actual commands the Makefile is used to run – there is nothing special with V-USB, the main things you need to remember is to have -Iusbdrv in place for the compiler to find the .h files from usbdrv folder, and then just having the three additional object files (usbdrv.o, oddebug.o, usbdrvasm.o) put into the same .elf file along with main.o.

Responding to USB control messages

Before we head to PC side code, let’s make our device firmware respond to two basic commands sent via USB: Turning a LED on and off. USB communication is initiated from host side and so-called control messages are sent to make USB device return it’s name or any other desired function. The specification also has room for vendor-specific control messages. Let’s define two such messages:

#define USB_LED_OFF 0
#define USB_LED_ON  1

Now V-USB library automatically calls our usbFunctionSetup() method when it receives a vendor-specific control message. The parameter is a 8-byte buffer actually containing a structure called usbRequest_t that is defined in usbdrv.h. I suggest you to check that definition out. At this point we are interested in the bRequest property that will either be 0 (USB_LED_OFF) or 1 (USB_LED_ON) – we will shortly see how our command-line client on PC side will generate these control requests, so there is nothing magical happening here – we just send a request from PC side and interpret it on the other side. Here’s the modified function to turn the LED on or off:

// this gets called when custom control message is received
USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) {
    usbRequest_t *rq = (void *)data; // cast data to correct type
	
    switch(rq->bRequest) { // custom command is in the bRequest field
    case USB_LED_ON:
        PORTB |= 1; // turn LED on
        return 0;
    case USB_LED_OFF: 
        PORTB &= ~1; // turn LED off
        return 0;
    }

    return 0; // should not get here
}

Of course for this to work we need to set PB0 as output in the beginning of main() (I suggest just after the “uchar i;” line):

    DDRB = 1; // PB0 as output

Now we’re ready to deploy our code to the chip. Just call make flash and the dependencies should take care of recompiling the main code file automatically.

Windows driver hell

Now if you are doing this project on a Linux box, congratulations, you likely can just get LibUSB and compile the command-line client to talk to your new device. On Windows, however, you will need a custom device driver. Especially nasty was the fact that older example projects on V-USB site don’t work with 64-bit Windows 7 at all. For this reason, I’ll outline the basic steps for creating and installing your own driver.

Now immediately after you have flashed the ATtiny you should hear the standard Windows fanfare di-dum sound that tells a new USB device has been plugged in. Windows will look for a driver and most likely fail. This is where libusb-win32 comes to the rescue:

  1. Go to the libusb-win32 home page (google if it the above link doesn’t work)
  2. Locate the heading “Device driver installation” and follow the instructions to download the latest version. I used libusb-win32-bin-1.2.6.0.zip. Unzip to a subfolder called “libusb” in the project folder.
  3. Run the INF-wizard using the instructions, put the installer somewhere you like (I recommend a new driver-subfolder in your project folder)
  4. Easiest way to install the driver is to do it as the final step of INF-wizard, there’s a button for it
  5. Replug your device and pray for the best

I could have a separate tutorial covering my trial-and-error methods of getting the USB device driver to install and work. I hope you have better luck. In case of problems, here are some helpful tips:

  • You can use Windows Device Manager to uninstall wrong drivers for your device, or if you get and “Unknown device”, update drivers for the device
  • Select “Browse my computer for driver software” and point Windows manually for correct location. Do not mind any “drivers not certified” -alerts, to go through them
  • Sometimes Windows manages to cache the device in a certain port and no matter how many times you unplug and replug it, it gets recognized wrong. Changing USB port or switching even to a different hub (e.g. from computer case front to back) helped me in some cases
  • Restarting the machine or googling for tools to reset USB cache may help, too :)

This is what the INF-wizard (located in the bin-subfolder of libusb) looks like if you are lucky (USBexample device visible):

This is what you can also enter manually if you are not so lucky:

After clicking Next, you get to save your generated driver to a folder. It’s a good idea to use “Install now…” to avoid installing the drivers manually. My installation hanged 50 % of the time (needed to wait 5 minutes for the installer to exit), but the driver still seemed to install OK.

Command-line client

Congratulations! We are almost there! Now we only need to prepare the host-side software. For compiling it, I recommend the GNU C compiler gcc from MinGW project and the MSYS tools that you should be able to install along with it, but probably Visual C and others work just fine. MinGW installer is really hard to locate (big thumbs down for the wiki authors on usability), but currently trial and error should eventually get you here.

We already installed libusb-win32 in the previous section, and the needed library and header files are all there. Just copy (or rename) the oddly named lusb0_usb.h to usb.h in the libusb/include folder and we’re good to go.

First thing we’ll need to do is to have a little helper function to decipher USB descriptor strings used for vendor name and device name. For this and also later for communicating with our device, we will use usb_control_msg() function provided by libusb. You basically pass the device handle, direction of message (USB_ENDPOINT_IN for device to host and USB_ENDPOINT_OUT for host to device) and other information used to define the recipient and direction, and the control message request, index and value codes, as well as a buffer for input/output, buffer size and maximum timeout for the call.

For those who want to understand the control messages better, I warmly recommend the surprisingly user-friendly USB 2.0 specification. The part on control messages starts on page 248 and the first table concerning this should be Table 9-2. You can find the download link for the spec from part 1 of this tutorial. The specification is rather simple and defined constant values closely reflect the #defines in libusb so you should understand everything rather well just by comparing the spec and my example code.

If you wonder what the rest of our helper function does after calling usb_control_msg, it’s basically checking the return value and response length, and converting it from UTF16-LE to Latin1 (same as ASCII if there’s no special characters). The USB descriptor return format is strictly defined to remove any possibilities for interpretation. This is all explained in chapter 9.5 of the USB specification. So here’s the helper function (it’s basically a slightly revised version of the one used in V-USB PowerSwitch example):

/* Used to get descriptor strings for device identification */
static int usbGetDescriptorString(usb_dev_handle *dev, int index, int langid, 
                                  char *buf, int buflen) {
    char buffer[256];
    int rval, i;

    // make standard request GET_DESCRIPTOR, type string and given index 
    // (e.g. dev->iProduct)
    rval = usb_control_msg(dev, 
        USB_TYPE_STANDARD | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
        USB_REQ_GET_DESCRIPTOR, (USB_DT_STRING << 8) + index, langid, 
        buffer, sizeof(buffer), 1000);

    if(rval < 0) // error
        return rval;

    // rval should be bytes read, but buffer[0] contains the actual response size
    if((unsigned char)buffer[0] < rval)
        rval = (unsigned char)buffer[0]; // string is shorter than bytes read

    if(buffer[1] != USB_DT_STRING) // second byte is the data type
        return 0; // invalid return type

    // we're dealing with UTF-16LE here so actual chars is half of rval,
    // and index 0 doesn't count
    rval /= 2;

    /* lossy conversion to ISO Latin1 */
    for(i = 1; i < rval && i < buflen; i++) {
        if(buffer[2 * i + 1] == 0)
            buf[i-1] = buffer[2 * i];
        else
            buf[i-1] = '?'; /* outside of ISO Latin1 range */
    }
    buf[i-1] = 0;

    return i-1;
}

Now we can use this helper function when we iterate over USB devices to recognize our own device with vendor name “codeandlife.com” and device name “USBexample”. The libusb-win32 API documentation gives us a good starting point in the examples section. I won’t be hand-holding you too much on this function, the basic logic is just to loop through every USB bus and every USB device in them, open a device and ask it’s vendor and device name, return it if it matches or close and proceed to next one if it does not:

static usb_dev_handle * usbOpenDevice(int vendor, char *vendorName, 
                                      int product, char *productName) {
    struct usb_bus *bus;
    struct usb_device *dev;
    char devVendor[256], devProduct[256];

    usb_dev_handle * handle = NULL;

    usb_init();
    usb_find_busses();
    usb_find_devices();

    for(bus=usb_get_busses(); bus; bus=bus->next) {
        for(dev=bus->devices; dev; dev=dev->next) {			
            if(dev->descriptor.idVendor != vendor ||
               dev->descriptor.idProduct != product)
                continue;

            /* we need to open the device in order to query strings */
            if(!(handle = usb_open(dev))) {
                fprintf(stderr, "Warning: cannot open USB device: %sn",
                        usb_strerror());
                continue;
            }

            /* get vendor name */
            if(usbGetDescriptorString(handle, dev->descriptor.iManufacturer, 
                                      0x0409, devVendor, sizeof(devVendor)) < 0) {
                fprintf(stderr, 
                        "Warning: cannot query manufacturer for device: %sn", 
                        usb_strerror());
                usb_close(handle);
                continue;
            }

            /* get product name */
            if(usbGetDescriptorString(handle, dev->descriptor.iProduct, 
                                      0x0409, devProduct, sizeof(devVendor)) < 0) {
                fprintf(stderr, 
                        "Warning: cannot query product for device: %sn", 
                        usb_strerror());
                usb_close(handle);
                continue;
            }

            if(strcmp(devVendor, vendorName) == 0 && 
               strcmp(devProduct, productName) == 0)
                return handle;
            else
                usb_close(handle);
        }
    }

    return NULL;
}

Oh and the 0x0409 is the language code for English, this is also found from the USB specification. Note how the vendor and device names need to be fetched using our helper function – the standard device descriptor in dev only tells the parameter values we need to use for usbGetDescriptorString() to get them (fields iManufacturer and iProduct). This is of course understandable as device descriptor is constant length but the vendor and device names have varying length.

Now that we got past all the sillyness of helper functions to scan all USB devices and return their specially formatted descriptor messages in order to get one simple device handle, the rest of the code is very straightforward. Only thing making the USB communication different from standard structure of accessing a file are the calls to usb_control_msg(), otherwise the general structure of open / do stuff / close applies:

int main(int argc, char **argv) {
    usb_dev_handle *handle = NULL;
    int nBytes = 0;
    char buffer[256];

    if(argc < 2) {
        printf("Usage:\n");
        printf("usbtext.exe on\n");
        printf("usbtext.exe off\n");
        exit(1);
    }

    handle = usbOpenDevice(0x16C0, "codeandlife.com", 0x05DC, "USBexample");

    if(handle == NULL) {
        fprintf(stderr, "Could not find USB device!n");
        exit(1);
    }

    if(strcmp(argv[1], "on") == 0) {
        nBytes = usb_control_msg(handle, 
            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
            USB_LED_ON, 0, 0, (char *)buffer, sizeof(buffer), 5000);
    } else if(strcmp(argv[1], "off") == 0) {
        nBytes = usb_control_msg(handle, 
            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
            USB_LED_OFF, 0, 0, (char *)buffer, sizeof(buffer), 5000);
    }

    if(nBytes < 0)
        fprintf(stderr, "USB error: %sn", usb_strerror());

    usb_close(handle);

    return 0;
}

Note that the second parameter for usb_control_msg now uses USB_TYPE_VENDOR to indicate we are sending a vendor-specific control message. You can also see that the request, value and index parameters (here USB_LED_ON/OFF, 0, 0) can be freely used to communicate with our code on device side.

You can grab the complete code for usbtest.c from project zip file with two #defines that match the ones in main.c and compile with make usbtest.exe (actually just “make” suffices, as usbtest.exe is one of the default targets). Now you should be able to turn the LED on and off using commands “usbtest on” and “usbtest off”. Note again that you need MinGW installed and “gcc” working in order to do this. The full compile command that “make” runs is:

gcc -I ./libusb/include -L ./libusb/lib/gcc -O -Wall usbtest.c -o usbtest.exe -lusb

Wow! That is all. I think this is enough for anyone to grasp in a single reading, so I will be splitting additional V-USB tricks into a fourth part of this tutorial that I will write later. Spending 5 hours in one setting to write this one is enough. :)

Proceed to the next part of this tutorial

Published by

Joonas Pihlajamaa

Coding since 1990 in Basic, C/C++, Perl, Java, PHP, Ruby and Python, to name a few. Also interested in math, movies, anime, and the occasional slashdot now and then. Oh, and I also have a real life, but lets not talk about it!

218 thoughts on “AVR ATtiny USB Tutorial Part 3”

  1. Hi,
    I am new to Embedded-C world.
    I have following doubts, please help me out.
    1)I don’t understand what a “MAKE” file is can you please give a link for what is it and how do we make.
    2)I am working with Atmel Studio 6 and included all the files in my project using “Solution Explorer” from V-USB folder but I still get the error undefined reference to “usbInit()” and “usbPoll”. Please help me out.
    3)Please explain what is a .s file and I am recieving the error for Khz setting. but I already set it to 12000 still get the same error.
    Thank you.

    1. Late reply… “make” is a command which reads “makefiles” which contain instructions how to compile and build code from sources. It comes with most GNU C Compiler suites (Windows mingw, installed on Linux as default), so once you have it, you can just go to command line and type “make” in the project folder, and it runs all the commands automatically for compiling. You can google for “gnu make” or “makefile” for additional information.

  2. we are doing a project for implementing usb protocols in attiny2313. we started with your tutorial we have designed the hardware but while dumping the code our device is not recognized . we tried each and everything to sort out the problem but vendor id and product id is not being found even we have entered manually.Finally, we got resource is already exist. we have completely verified our hardware but didn’t found any error in hardware.

  3. This is a great tutorial.
    I am following using UBUNTU until creating usbtest.exe … but
    when I run
    ./usbtest.exe on
    it responded:
    Could not find USB device!
    I have done plug/unplug the USB connector; had installed libusb, burn the ATtiny2313. Why this is happen?
    Help please.

  4. Nugraha, first of all check if you device is recognized – having plugged it in, launch dmesg command (in terminal). You should see something like this:
    [26291.523074] usb 1-1: new low speed USB device number 2 using uhci_hcd
    [26291.686018] usb 1-1: New USB device found, idVendor=16c0, idProduct=05dc
    [26291.686062] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
    [26291.686068] usb 1-1: Product: USB_dev_name
    [26291.686072] usb 1-1: Manufacturer: Your_vendor_name
    In place of Product and Manufacturer you should have Product and Vendor name you have set in usbconfig.h
    If device is recognized, check if Product and Manufacturer set in command-line client are the same as these in usbconfig.h

    1. Hi slawek,
      nice to meet you and thanks for your hints.
      I did the following command:

      lsusb
      … yes, now I found the device as:
      Bus 003 Device 002: ID 16c0:05dc Van Ooijen Technische Informatica shared ID for use with libusb.
      and
      dmesg
      … and the new USB is recognized:
      [ 72.400060] usb 3-1: new low-speed USB device number 2 using uhci_hcd
      [ 72.572865] usb 3-1: New USB device found, idVendor=16c0, idProduct=05dc
      [ 72.572869] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
      [ 72.572873] usb 3-1: Product: USBexample
      [ 72.572876] usb 3-1: Manufacturer: codeandlife.com

      * I reconstruct my breadboard and then everything is OKAY and I am so glad.
      Thank you.

  5. Thank you for your explanation.
    However, There are so many error in my compiler, atmel studio 6.1.
    Please tell me what you use for compiling.

    most of error like this
    error 17 variable ‘usbDescriptorDevice’ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’ C:\Users\pkang\Documents\Atmel Studio\6.1\HID_AT8\HID_AT8\usb\usbdrv.h 455 6 HID_AT8

    1. I think the compiler has been updated after this tutorial, and the old version of V-USB and this tutorial will cause warnings. Upgrading V-USB to latest version might help, but I think there might be some code changes needed to tutorial code also. Unfortunately I have not compiled this in a while, so quickest help would be through Google…

  6. If any one is getting an error about “-assembler-with-cpp” flag, change it to “-x assembler-with-cpp” in /Users/chris/Documents/Arduino/hardware/tiny/avr/platform.txt

  7. I had some trouble using the library. It says these things as error, I am new to embedded and i know this is some atmel 6.2 issue, but how do i fix it.

    Thanks
    Marcell

    Error 5 variable ‘usbDescriptorStringVendor’ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’ c:\users\marcell veszpremi\documents\atmel studio\6.2\passsowrdvusb\passsowrdvusb\usbdrv.h 479 5 passsowrdvusb
    Error 7 variable ‘usbDescriptorStringSerialNumber’ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’ c:\users\marcell veszpremi\documents\atmel studio\6.2\passsowrdvusb\passsowrdvusb\usbdrv.h 491 5 passsowrdvusb
    Error 6 variable ‘usbDescriptorStringDevice’ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’ c:\users\marcell veszpremi\documents\atmel studio\6.2\passsowrdvusb\passsowrdvusb\usbdrv.h 485 5 passsowrdvusb
    Error 4 variable ‘usbDescriptorString0′ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’ c:\users\marcell veszpremi\documents\atmel studio\6.2\passsowrdvusb\passsowrdvusb\usbdrv.h 473 6 passsowrdvusb
    Error 3 variable ‘usbDescriptorHidReport’ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’ c:\users\marcell veszpremi\documents\atmel studio\6.2\passsowrdvusb\passsowrdvusb\usbdrv.h 467 6 passsowrdvusb
    Error 1 variable ‘usbDescriptorDevice’ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’ c:\users\marcell veszpremi\documents\atmel studio\6.2\passsowrdvusb\passsowrdvusb\usbdrv.h 455 6 passsowrdvusb
    Error 2 variable ‘usbDescriptorConfiguration’ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’ c:\users\marcell veszpremi\documents\atmel studio\6.2\passsowrdvusb\passsowrdvusb\usbdrv.h 461 6 passsowrdvusb

Leave a Reply

Your email address will not be published. Required fields are marked *


+ 4 = six

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>