AVR ATtiny USB Tutorial Part 4

All right. Now that we got the basic USB code working in part 3, it’s time to wrap things up in this tutorial series. This fourth section will explain how to send data from your device to PC and also the other way around. I may later do a fifth part on how to make a USB HID device like a keyboard or mouse, so if you haven’t already, I’d recommend subscribing to the RSS feed to get updates.

Sending data from device to PC

If you look carefully at our command-line client code, you probably noticed that the control messages sent to toggle the led are of type USB_ENDPOINT_IN and we have a 256-byte buffer in place to receive any data the device sends. So far we have not received any data and the return value stored in nBytes has been zero. Let’s change that.

If you read through the documentation in usbdrv/usbdrv.h, more specifically the comments for usbFunctionSetup() we used to toggle our LED on device side, you can see that there are two ways to return data from usbFunctionSetup():

  1. Set the global pointer usbMsgPtr to a (static RAM) memory block and return the length of data from the function
  2. Define usbFunctionRead() function to do it yourself

We will be using the first method. First we define an additional message USB_DATA_OUT and a static buffer to store the data we want to send from device to PC:

#define USB_DATA_OUT 2

static uchar replyBuf[16] = "Hello, USB!";

Then we add a new case to usbFunctionSetup() to return the contents of that buffer:

    case USB_DATA_OUT: // send data to PC
        usbMsgPtr = replyBuf;
        return sizeof(replyBuf);

Whoa, that was easy! V-USB does all the rest for us.

Now the only thing needed to do is to update our command-line client to include our new “out” command. Copy the #define from above to the beginning of usbtest.c (right after the existing two #defines is a good place) and add a third if statement to the main method:

    } else if(strcmp(argv[1], "out") == 0) {
        nBytes = usb_control_msg(handle, 
            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
            USB_DATA_OUT, 0, 0, (char *)buffer, sizeof(buffer), 5000);
        printf("Got %d bytes: %sn", nBytes, buffer);
    }

Just run “make” and “make flash” to compile and update the firmware. Now try “usbtest out” – how easy was that? With 128 bytes of RAM on ATtiny and V-USB using a lot of that, you will not be able to transmit the maximum of 254 bytes this method offers (implement usbFunctionRead() to transfer longer buffers piece by piece), but even 16 characters is quite a lot! And of course you can chain multiple control requests if you want to receive more data.

Sending data from PC to device

If you only need to send a few bytes of additional data, you can use the wValue and wIndex parameters for that. Let’s make a new USB_DATA_WRITE that changes the “USB!” part of our return message to any four characters. First a new #define (add to both main.c and usbtest.c):

#define USB_DATA_WRITE 3

Then a new case to firmware side:

    case USB_DATA_WRITE: // modify reply buffer
        replyBuf[7] = rq->wValue.bytes[0];
        replyBuf[8] = rq->wValue.bytes[1];
        replyBuf[9] = rq->wIndex.bytes[0];
        replyBuf[10] = rq->wIndex.bytes[1];
        return 0;

Note that the wValue and wIndex are actually unions that allow you to either access the whole 16-bit word value with wValue.word or alternatively directly access the lower and upper bytes with wValue.bytes[0] and wValue.bytes[1], respectively (I’m not sure if the order of upper and lower bytes in the word depends on endiandness on PC side, but that’s how it works for me).

On command-line side, we add another “else if” that sends fixed letters for simplicity:

    } else if(strcmp(argv[1], "write") == 0) {
        nBytes = usb_control_msg(handle, 
            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
            USB_DATA_WRITE, 'T' + ('E' << 8), 'S' + ('T' << 8), 
            (char *)buffer, sizeof(buffer), 5000);
    }

Just compile with “make”, update firmware with “make flash” and try out the new “usbtest write” command. Note that the command itself does not print anything, you need to run “usbtest out” before and after to see how the buffer is changed. Also note that until you reset or unplug the device, the new message “Hello, TEST” will stay in the device.

Sending more data to the device

Using wValue and wIndex is fine for many applications, but if we’d like to update the whole 16-byte buffer, we’d need at least four control messages. Let’s now create our first USB_ENDPOINT_OUT message. On PC side the code is pretty simple:

#define USB_DATA_IN 4

...

    } else if(strcmp(argv[1], "in") == 0 && argc > 2) {
        nBytes = usb_control_msg(handle, 
            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, 
            USB_DATA_IN, 0, 0, argv[2], strlen(argv[2])+1, 5000);
    }

Note that the buffer size is one more than just the argument 2 length to accommodate the string-terminating NULL character. On device side we add a new variable to keep track how many bytes we are to read, and use a special return value from usbFunctionSetup() (this is all well documented in usbdrv.h):

#define USB_DATA_IN 4

static uchar dataReceived = 0, dataLength = 0; // for USB_DATA_IN

...

    case USB_DATA_IN: // receive data from PC
        dataLength = (uchar)rq->wLength.word;
        dataReceived = 0;
		
        if(dataLength > sizeof(replyBuf)) // limit to buffer size
            dataLength = sizeof(replyBuf);
			
        return USB_NO_MSG; // usbFunctionWrite will be called now

Now that the request is of type control-out (USB_ENDPOINT_OUT on PC side code) and we have returned USB_NO_MSG (with byte-size return value it’s actually 255, also the reason why data from device can be only 254 bytes tops), V-USB will call a function named usbFunctionWrite() to receive the data sent:

USB_PUBLIC uchar usbFunctionWrite(uchar *data, uchar len) {
    uchar i;
			
    for(i = 0; dataReceived < dataLength && i < len; i++, dataReceived++)
        replyBuf[dataReceived] = data[i];
		
    return (dataReceived == dataLength); // 1 if we received it all, 0 if not
}

Last thing to do is to modify usbconfig.h to tell V-USB we have provided such a function:

#define USB_CFG_IMPLEMENT_FN_WRITE      1

Now you should be able to remake and flash the project and use the new “usbtest in abcdefgh” command to replace the values in device’s 16 byte buffer. Note that if you supply a string longer than 15 characters, the terminating NULL character will not fit into buffer and “usbtest out” may print out garbage after the 16 characters. This is just a demo application so we ignore that for now. :)

Final remarks

Congratulations! You have successfully built and coded a USB device that is able to receive commands and send/receive arbitary data. Once you have understood how V-USB calls usbFunctionWrite(), it should also be pretty straightforward to make usbFunctionRead() in similar manner to send longer data to PC in multiple parts.

I have compiled the Makefile and source code files as well as relevant portions of V-USB and libusb-win32 for compiling the project, as well as the final schematic into one .zip file:

usb_tutorial_20120204.zip

Update: If you don’t for some reason like the command-line usbtest.exe, or would like to develop a graphical client, G√ľnter Meinel has created an excellent GUI version of the test program using Borland C++ Builder 3.0. You can get it with source code from here:

http://home.arcor.de/guentermeinel/download.html

I hope you have enjoyed the tutorial, and hope to see you again on this site, I’m planning to write at least a bit about 7 segment LED multiplexing, driving LCDs directly with AVR, and PicoScope USB oscilloscope in the future, and may add new subjects as I get further myself.

This is the end of “the main arc” of my V-USB tutorial, but I recommend you to read my later posts in the V-USB tutorial series, including:

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!

93 thoughts on “AVR ATtiny USB Tutorial Part 4”

  1. Hello
    One problem.
    When you get more data from the computer, change this line:

    Case USB_DATA_IN: // receive data from the computer
    datalength = (uchar) rq-> wLength.word;

    to:

    datalength = (int) rq-> wLength.word;

    max size type uchar is 255. When wLength.word is larger than 255, do not read all the data.

  2. excuseme for my bad english, but i have a question, the steps for this tutorial will avail to programming the microcontroller 2313 or only for comunications with computer

Leave a Reply

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

Time limit is exhausted. Please reload the CAPTCHA.