USB MSD code

Code for USB MSD

OK, the mass storage device code is finally done! What a wild ride it's been. Bits and pieces of information everywhere, some of it very hard to find! It's been a good few weeks so I need a break before writing about it but I'm uploading the code for anyone who may want to use it.

Please bear in mind that on my board, the SD card is attached to SPI2 on RB3 (for SDI) and RB5 (for SDO). The Chip Select (CS) pin is attached to RA0. If you wish to use this code, you will have to modify the appropriate values in mmcpic32.h and also the PPS settings in main.c.

If anyone has any questions, please drop a comment and I'll try and help out.

Here's the code.

Tags: code, USB, MSD

USB progress update

Updates on USB code progress

Hi there folks. I've been hard at work on USB and have made great progress. I have made a USB HID Keyboard device that works and now I'm working on a USB Mass Storage Device (an SD card connected to my PIC32MZ but visible in Windows).
It's all working pretty well and when running the SPI at 50MHz (which is technically double the max frequency but whatever) I am able to get write speeds of 3MB/s and read speeds of 3.8MB/s in Windows. Even when running SPI at 25MHz I get about 2MB/s write speeds and over 2MB/s read speeds, so good progress is being made.
I need to test that this stuff is working reliably before uploading posts on my site and it's Christmas and New Year so there's a lot of family stuff going on :) As soon as I finish, I'll upload it all.

Merry Christmas and Happy New Year to those of you that celebrate it!

Tags: code, USB

Finishing the USB CDC device

Right. We're finally on the... er... final part of this set of posts on CDC. Woohoo! Today I'll cover my code for handling all the Endpoint 0 transactions and cover how to send and receive data on the bulk endpoints.

Handling the Endpoint 0 transactions for a CDC device

OK. Let's paste this code in:

void USB_EP0_handle_transaction()
{
    int cnt;
    unsigned char temp_data[7];
    USB_LINE_CODING new_line_coding;

    // We're not going to bother with whether bmRequestType is IN or OUT for the most part
    switch (USB_transaction.bmRequest)
    {
        case 0xC:
        {
            USBE0CSR0bits.STALL = 1;
            break;

        }
        case 0x0: 
        {
            if (USB_transaction.bmRequestType == 0x80) // Get status
                USB_queue_EP0(USB_device_description, 0, 0);
            if (USB_transaction.bmRequestType == 0x00) // Select function
                USB_queue_EP0(USB_device_description, 0, 0);
            break;            
        }

        case 0x5: // Set USB address
        {
            USBE0CSR0bits.RXRDYC = 1;
            USB_address = EP[0].rx_buffer[2];
            USB_set_address = 1;
            break;
        }

        case 0x6: // Get descriptor
        {
            switch (USB_transaction.wValue >> 8)
            {
                case 0x1: // Get device descriptor
                {
                    USB_queue_EP0(USB_device_description, sizeof(USB_device_description), USB_transaction.wLength);
                    break;
                }

                case 0x2: // Get configuration descriptor
                {
                    USB_queue_EP0(USB_config_descriptor, sizeof(USB_config_descriptor), USB_transaction.wLength);
                    break;
                }

                case 0x3: // Get string descriptors
                {
                    switch (USB_transaction.wValue & 0xFF)
                    {
                        case 0x0: // String 0 - Language ID
                        {
                            USB_queue_EP0(string0, sizeof(string0), USB_transaction.wLength);
                            break;
                        }
                        case 0x1: // String 1 - Vendor
                        {
                            USB_queue_EP0(string1, sizeof(string1), USB_transaction.wLength);
                            break;
                        }
                        case 0x2: // String 2 - Product name
                        {
                            USB_queue_EP0(string2, sizeof(string2), USB_transaction.wLength);
                            break;
                        }
                        case 0x3: // String 3 - Serial number
                        {
                            USB_queue_EP0(string3, sizeof(string3), USB_transaction.wLength);
                            break;
                        }
                        case 0x4: // String 4 - Configuration
                        {
                            USB_queue_EP0(string4, sizeof(string4), USB_transaction.wLength);
                            break;
                        }
                        case 0x5: // String 5 - Interface
                        {
                            USB_queue_EP0(string5, sizeof(string5), USB_transaction.wLength);
                            break;
                        }
                        default: break;
                    }
                }
                default: 
                {
                    break;
                }
            }
            break;
        }

        case 0x9: // Set interface
        {
            // Zegads we have enumeration!
            break;
        }

        case 0x20:
        {
            if (USB_transaction.bmRequestType == 0x21)
            {
                // Set line coding - host asks us to set our line coding to what it specifies
                // Read (wLength) bytes from USB Endpoint 0
                USB_receive_EP0(USB_transaction.wLength);

                USB_line_coding.baud = ((int)EP[0].rx_buffer[3] << 24) | ((int)EP[0].rx_buffer[2] << 16) | ((int)EP[0].rx_buffer[1] << 8) | EP[0].rx_buffer[0];
                USB_line_coding.parity_bits = EP[0].rx_buffer[4];
                USB_line_coding.stop_bits = EP[0].rx_buffer[5];
                USB_line_coding.data_bits = EP[0].rx_buffer[6];
            }
            break;

        }

        case 0x21:
        {
            // Get line coding - send current line coding to the host
            temp_data[0] = USB_line_coding.baud & 0xFF;
            temp_data[1] = (USB_line_coding.baud >> 8) & 0xFF;
            temp_data[2] = (USB_line_coding.baud >> 16) & 0xFF;
            temp_data[3] = USB_line_coding.baud >> 24;
            temp_data[4] = USB_line_coding.stop_bits;
            temp_data[5] = USB_line_coding.parity_bits;
            temp_data[6] = USB_line_coding.data_bits;
            USB_queue_EP0(temp_data, sizeof(temp_data), USB_transaction.wLength);
            break;
        }

        case 0x22:
        {
            if (USB_transaction.bmRequestType == 0x21)
            {
                 // Set line control state
                USB_line_control_state = USB_transaction.wValue >> 8;

                if (USB_line_control_state != 0)
                {
                    // We're connected now
                }
            }
            break;
        }

        default: 
        {
            USBE0CSR0bits.STALL = 1;
            break;
        }
    }   
}

The comments are fairly self explanatory I think? Nothing fancy there but there are two things I want to mention:

case 0x5: // Set USB address
{
    USBE0CSR0bits.RXRDYC = 1;
    USB_address = EP[0].rx_buffer[2];
    USB_set_address = 1;
    break;
}

As I mentioned a few posts ago, because we are making a device we cannot assign ourselves an address. We have to wait for the host to assign one to us. The value will range from 0 to 127 and will probably differe depending on what USB devices the host already has attached to it. When it sends us an address, we need to put the value into USBCSR0bits.FUNC. I do this in my USB ISR, like this:

if (USB_set_address) // Do we need the set the USB address?
{
    USBCSR0bits.FUNC = USB_address & 0x7F;
    USB_set_address = 0;
}

Secondly, you'll notice this:

USBE0CSR0bits.STALL = 1;

From what I can tell, this will tell the host that the endpoint has stalled either due to an error or because it did not recognise the request. I don't believe this is ever called in my code, I've put it in there because I like to have something in the default case of my switch statements.

OK, so that's all well and good but once your device is enumerated, i.e. recognised and set up by the host, any data it sends will be on the bulk data endpoints. For my code, my bulk data IN endpoint and bulk data OUT endpoint are both endpoint 2. Let's take a look at that in my USB ISR:

if(USBE2CSR1bits.RXPKTRDY)
{
    EP[2].rx_num_bytes = USB_receive_EP2();
    if (USB_RECEIVE_CALLBACK) USB_RECEIVE_CALLBACK(EP[2].rx_buffer, EP[2].rx_num_bytes);       
    USBCSR1bits.EP2RXIF = 0;
}  

This is slightly different from endpoint 0. This time we don't have RXRDYC we have direct access to RXPKTRDY. More on that later. Let's take a look at the USBreceive_EP2()function:

int USB_receive_EP2()
{
    unsigned char *FIFO_buffer;
    int cnt;
    int bytes_received;

    USBE2CSR0bits.MODE = 0;     // EP2 is RX

    bytes_received = USBE2CSR2bits.RXCNT;

    FIFO_buffer = (unsigned char *)&USBFIFO2;

    for(cnt = 0; cnt < bytes_received; cnt++)
    {
        EP[2].rx_buffer[cnt] = *(FIFO_buffer + (cnt & 3)); // Store the received bytes in array ep0data[].
    }

    USBE2CSR1bits.RXPKTRDY = 0;

    return bytes_received;
}

So the flow here is very similar to endpoint 0 except the one line:

USBE2CSR0bits.MODE = 0;     // EP2 is RX

Because I'm using endpoint 2 in a dual role as both a sending and a receiving endpoint, every time I wish to send or receive on it I must set the MODE to the correct value. You can do this just before reading to or writing from the FIFO buffer. 0 means receiving mode and 1 means sending mode.

USBE2CSR1bits.RXPKTRDY = 0;

This is what we were doing indirectly on endpoint 0 by setting RXRDYC to 1, it's just that we now have access to the register directly. We have received the packet, so need to clear the flag.

OK and what's this then?

if (USB_RECEIVE_CALLBACK) USB_RECEIVE_CALLBACK(EP[2].rx_buffer, EP[2].rx_num_bytes);

In my code I've made it possible to have a callback that gets called whenever data is received on endpoint 2. It can be found in the file main.c that comes with the code. In it, I echo back the contents that were received back to the host. I do this by calling USB_send_EP2() so let's have a quick look at that:

void USB_send_EP2(volatile unsigned char *buffer, int size)
{
    int cnt;
    unsigned char *FIFO_buffer;

    USBE2CSR0bits.MODE = 1;     // EP2 is TX

    FIFO_buffer = (unsigned char *)&USBFIFO2;

    for (cnt = 0; cnt < size; cnt++)
    {
        *FIFO_buffer = buffer[cnt]; // Send the bytes
    }

    USBE2CSR0bits.TXPKTRDY = 1;
}

Unlike endpoint 0, we have a maximum packet size of 512 bytes now. As the maximum receive size is also 512 bytes and the PIC32MZ is cutting off packets into 512 byte chunks (thanks to settings in the USBOTG register) I should never receive more than 512 bytes at a time so my code will never end up sending more than 512 bytes at a time. If that were the case, I'd have to handle splitting the data to send up into 512 byte packets, just like I did for the 64 byte long packets on endpoint 0.

Please note that just because you don't receive more than 512 bytes at a time, that doesn't mean the host is limited to 512 characters at once. I pasted the 40,000 characters of one of my random code files into a terminal and my PIC32MZ received it and echoed it back just fine. The host and the PIC32MZ USB peripheral just gave it to me in 512 byte chunks is all.

And with this, the incredibly long series of posts on making a USB CDC device on PIC32MZ are finally done! Next up, an HID device to astound, amaze and annoy!

As always, here's the code!

Tags: code, USB, CDC

USB descriptors and how to send and receive data on endpoint 0

Sending and receiving packets on Endpoint 0

Hello again. Last time we go to the point where we'd set up the USB peripheral, gotten a "Reset" command from the host and had set up our endpoints on the PIC32MZ side. Today I'll be briefly covering descriptors and how to send and receive data on endpoint 0 between the device and the host.

So let's take a look at the next step I have in my USB ISR:

    // Endpoint 0 Interrupt Handler
    if(USBCSR0bits.EP0IF == 1)
    {        
        if(USBE0CSR0bits.RXRDY)
        {
            USB_receive_EP0(USBE0CSR2bits.RXCNT);

            USB_transaction.bmRequestType = EP[0].rx_buffer[0];
            USB_transaction.bmRequest = EP[0].rx_buffer[1];
            USB_transaction.wValue = (int)(EP[0].rx_buffer[3] << 8) | EP[0].rx_buffer[2];
            USB_transaction.wIndex = (int)(EP[0].rx_buffer[5] << 8) | EP[0].rx_buffer[4];
            USB_transaction.wLength = (int)(EP[0].rx_buffer[7] << 8) | EP[0].rx_buffer[6];

            USB_EP0_handle_transaction();

            if (USB_transaction.wLength == 0) USBE0CSR0bits.DATAEND = 1; // End of Data Control bit (Device mode) 
        }

        if (USBE0CSR0bits.SETEND) 
        {
            USBE0CSR0bits.SETENDC = 1;
        }

        USBCSR0bits.EP0IF = 0;  // Clear the USB EndPoint 0 Interrupt Flag.
    }

OK so it's not immediately obvious what's happening there, but broadly the steps are:

  • If the EndPoint 0 Interrupt Flag EP0IF is set in USBCSR0 that means something has occurred on endpoint 0 that we need to handle.
  • If the RXRDY flag is set in USBE0CSR0, that means we have received some data and need to read it out the FIFO.
  • We read the data and then process it to see what it being asked of us.
  • We send a reply, if needed.
  • We clear EP0IF in USBCSR0.

The number of bytes received is stored in USBE0CSR2bits.RXCNT. So let's take a look at how we read from the USB FIFO, because it has a nasty trap in it.

void USB_receive_EP0(int length)
{
    unsigned char *FIFO_buffer;
    int cnt;

    EP[0].rx_num_bytes = USBE0CSR2bits.RXCNT; // Endpoint 0 - Received Bytes Count

    FIFO_buffer = (unsigned char *)&USBFIFO0;

    for(cnt = 0; cnt < length; cnt++)
    {
        EP[0].rx_buffer[cnt] = *(FIFO_buffer + (cnt & 3)); // Store the received bytes in EP[0].rx_buffer[]
    }

    USBE0CSR0bits.RXRDYC = 1;
}

OK so it's fairly simple except for one thing:

    FIFO_buffer = (unsigned char *)&USBFIFO0;

    for(cnt = 0; cnt < length; cnt++)
    {
        EP[0].rx_buffer[cnt] = *(FIFO_buffer + (cnt & 3)); // Store the received bytes in EP[0].rx_buffer[]
    }

Why aren't we just reading from USBFIFO0 directly? Well, two reasons. First, any time you read from the USB FIFO buffers it is a 32-bit read. This means that if you just go:

    data = USBFIFO0;

It will read 4 bytes from USBFIFO0 and clear them out of the FIFO, which is not at all what we want. So we have to access the FIFO indirectly, as an 8-bit byte, like this:

FIFO_buffer = (unsigned char *)&USBFIFO0;

We could read this 32-bits at a time, it'd seemingly be faster. But just like the other peripherals the 4 bytes you'd end up reading would be in the reverse order to what we wanted because the PIC32MZ uses Little Endian byte ordering, which means the byte order would be, for example, 3, 2, 1, 0, 7, 6, 5, 4 instead of the 0, 1, 2, 3, 4, 5, 6, 7 we expect. This means we'd have to do extra processing to get everything back in the order we want. Additionally, the length of all transactions isn't always divisible by 4 meaning yet more logic would be needed. And then finally, on the troubled PIC32MZ EC series, there is this nasty errata:

PIC32MZ EC series USB FIFO errata

So, for now at least, I'm avoiding 32-bits. So we read it like this:

EP[0].rx_buffer[cnt] = *(FIFO_buffer + (cnt & 3)); // Store the received bytes in EP[0].rx_buffer[]

So what's going on there? We have an address pointed to by *FIFO_buffer. This address is the first byte of a 4-byte number. So to read all 4 bytes, we need to also read *FIFO_buffer + 1, *FIFO_buffer + 2 and *FIFO_buffer + 3. The moment we read from *FIFO_buffer + 3, the 4 bytes we just read will be cleared out of the FIFO buffer and a new value will appear in *USBFIFOA. So we need to start reading the first byte again (which is again *FIFO_buffer).

Next, let's take a look at this line:

USBE0CSR0bits.RXRDYC = 1;

What does it do? Let's play a game. Open up the PIC32MZ EF datasheet and search for "RXRDYC". I'll wait... Yeah, it's not there is it? And it isn't in DS60001326 either. But why? Let's follow the declaration in MPLAB X and see where it goes:

PIC32MZ USBE0CSR0 in XC.h

And let's compare that to the datasheet:

PIC32MZ USBE0CSR0 in the datasheet

And before you ask yes, USBE0CSR0 and USBIE0CSR0 are referring to the same register. I don't know for sure what went wrong here but I guess it's just more proof that Microchip hates you and wants you to use Harmony. So anyway, the bit RXRDYC aka SVCRPR need to be set when we receive a packet. This in turn clear the bit field RXPKTRDY, which signifies that we got the packet. What?

  • Setting RXRDYC clears RXPKTRDY (which we cannot clear directly for endpoint 0)
  • Clearing RXPKTRDY means we received the packet successfully

OK finally, we have received a packet from the host. Now what do we do with it? Well, these so-called "setup packets" look like this:

PIC32MZ USB setup packet

The important parts about this for us are:

  • Request type's bit 7 will be set for a read request and cleared for a write request.
  • Request is exactly what information the host wants from us.
  • wValue is a parameter of request type (for specific requests within a certain type of request).
  • wIndex is another parameter of request type.
  • wLength is the maximum length of the reply that it expects.

That wLength can be pretty tricky. More on it later when we send descriptors.

In my code, I've retrieved these values like this:

USB_transaction.bmRequestType = EP[0].rx_buffer[0];
USB_transaction.bmRequest = EP[0].rx_buffer[1];
USB_transaction.wValue = (int)(EP[0].rx_buffer[3] << 8) | EP[0].rx_buffer[2];
USB_transaction.wIndex = (int)(EP[0].rx_buffer[5] << 8) | EP[0].rx_buffer[4];
USB_transaction.wLength = (int)(EP[0].rx_buffer[7] << 8) | EP[0].rx_buffer[6];

(Note: USB_transaction is just a struct I made myself to keep track of the current transaction)

OK so we have gotten our first packet. For my CDC example code, the first packet I get is:

80 06 00 01 00 00 40 00 (all numbers in hex)

Break that down we get:

  • Request Type is 0x80 so it's an IN transmission, it wants us to send it something
  • Request is 0x06, which is listed as "Get Descriptor"
  • wValue is 0x0001, which means "Get Device Descriptor"
  • wIndex is 0x0000, so nothing to see there
  • wLength is 0x0040, meaning it wants a maximum of 64 bytes in return

OK so it wants our device descriptor. A descriptor is a set format for providing information about our device to the host computer. For my CDC example, my device descriptor looks like this:

unsigned char USB_device_description[] = {
/* Descriptor Length                        */ 18, /* Always 18 or 0x12 */
/* DescriptorType: DEVICE                   */ 0x01,
/* bcdUSB (ver 2.0)                         */ 0x00,0x02,
/* bDeviceClass                             */ 0x02,
/* bDeviceSubClass                          */ 0x00,
/* bDeviceProtocol                          */ 0x00,
/* bMaxPacketSize0                          */ 0x40, /* Always 64 or 0x40 for High Speed USB */
/* idVendor                                 */ 0xD8,0x04, /* e.g. - 0x04D8 - Microchip VID */
/* idProduct                                */ 0x0A,0x00, /* e.g. - 0x000A */
/* bcdDevice                                */ 0x00,0x02, /* e.g. - 02.00 */
/* iManufacturer                            */ 0x01,
/* iProduct                                 */ 0x02,
/* iSerialNumber                            */ 0x03, 
/* bNumConfigurations                       */ 0x01
};

Each of these USB descriptors has a length byte in the front which is the total length of the descriptor. This is a pretty set format and if you change the wrong things you'll find the host (Windows or whatever) fails to recognise your device. But there are some things you can change here, namely:

/* bcdUSB (ver 2.0)                         */ 0x00,0x02,

This 0x0002 (which is actually going to be interpreted by the host as 0x0200) means our device supports USB specification version 2.00 (which is where High Speed USB came in).

/* idVendor                                 */ 0xD8,0x04, /* e.g. - 0x04D8 - Microchip VID */
/* idProduct                                */ 0x0A,0x00, /* e.g. - 0x000A */
/* bcdDevice - Device release number        */ 0x00,0x02, /* e.g. - 02.00 */

I've "borrowed" Microchip's USB vendor and product IDs because we are allowed to do so when testing our products using the PIC32. If you're going to release a commercial product you will naturally have to register with the USB Implementers Forum (USB-IF) and get your own codes.

/* iManufacturer                            */ 0x01,
/* iProduct                                 */ 0x02,
/* iSerialNumber                            */ 0x03, 

These numbers are the index values of strings associated with manufacturer name, product name and product serial number. We will use these later. If you do not plan to provide these later you can safely set these to 0 and the host will not ask you for them later on.

OK, but how do we send this off to the host? Let's take a look:

// Send EP[0].tx_num_bytes from EP[0].tx_buffer on endpoint 0
void USB_send_EP0()
{
    int cnt;
    unsigned char *FIFO_buffer;

    FIFO_buffer = (unsigned char *)&USBFIFO0;

    if (USB_EP0_Wait_TXRDY()) return;

    cnt = 0;

    while (cnt < EP[0].tx_num_bytes)
    {
        *FIFO_buffer = EP[0].tx_buffer[cnt]; // Send the bytes
        cnt++;
        // Have we sent 64 bytes?
        if ((cnt > 0) && (cnt % 64 == 0))
        {
            // Set TXRDY and wait for it to be cleared before sending any more bytes
            USBE0CSR0bits.TXRDY = 1;            
            if (USB_EP0_Wait_TXRDY()) return;
        }
    }

    if (cnt % 64 !=0 ) USBE0CSR0bits.TXRDY = 1;            
}

So again, because we are sending a byte at a time, we need to get a byte sized pointer to USBFIFO0. There are, however, a few differences with sending and receiving. First off:

if (USB_EP0_Wait_TXRDY()) return;

We have to wait until the USB peripheral is ready to send a packet. The code of this function looks like this:

int USB_EP0_Wait_TXRDY()
{
    int timeout;

    timeout = 0;

    while (USBE0CSR0bits.TXRDY)
    {
        timeout++;
        if (timeout > USB_EP0_WAIT_TIMEOUT) return 1;
    };

    return 0;
}

When USBE0CSR0bits.TXRDY is set it means there is a packet in the FIFO buffer ready to send. Basically we need to wait until USBE0CSR0bits.TXRDY is clear before we're able to send another packet. Something I copied from MisterHemi's code is the timeout which will save us from being stuck in an infinite loop if the USB gets disconnected or something else goes wrong. In the datasheet, TXRDY is called TXPKTRDY so I don't know why they changed it. Amusingly (?), for all endpoints other than 0 it is still called TXPKTRDY. Go figure!

Furthermore, let's take a look at this section of the code:

if ((cnt > 0) && (cnt % 64 == 0))
{
    // Set TXRDY and wait for it to be cleared before sending any more bytes
    USBE0CSR0bits.TXRDY = 1;            
    if (USB_EP0_Wait_TXRDY()) return;
}

The maximum size of a packet for endpoint 0 is 64 bytes (and I set it as much in the initialisation). But some data we need to send is more than 64 bytes long, so how do we do this? Well, we put 64 bytes into the FIFO buffer, set TXRDY to 1 to let the hardware know we have a packet it has to send, and then wait for TXRDY to be cleared. We repeat this every 64 byte chunk until we are finished. At the end we once again need to set TXRDY to tell the USB peripheral to send the data we put into the FIFO buffer, but only if we're not on another 64 byte boundary :)

OK, so that's the Device Descriptor sent, next it is going to ask us for...

The Configuration Descriptor

This is where the host is going to ask us for specifics about our USB device. What kind of device it is, how many endpoints it has, how many configurations it has, how much power it uses if bus powered (powered by the USB port) or if it is self-powered. It is pretty long and again, there's not much here you can freely change. This is what it looks like for my CDC device:

unsigned char USB_config_descriptor[] = {
// Referenced from: https://gist.github.com/tai/acd59b125a007ad47767
/*********************************************************************
 Configuration Descriptors 
 *********************************************************************/
/*  bLength (Descriptor Length)             */ 9,    // How long is this initial descriptor?
/*  bDescriptorType: CONFIG                 */ 0x02  // What kind of descriptor is this? 0x02 = configuration descriptor
/*  wTotalLength                            */ 0x43,0x00, // What is the total length of this packet? (0x0043 = 67 bytes)
/*  bNumInterfaces                          */ 2,    // How many interfaces does this device have? 2
/*  bConfigurationValue                     */ 1,    // How many configurations does this device have?
/*  iConfiguration                          */ 0,    // String offset for the name of this configuration, I've elected to not provide a name so set this to 0
/*  bmAttributes                            */ 0x80, // bit 6 set = bus powered = 0x80, 0xC0 is for self powered. See important note below!
/*  bMaxPower                               */ 0x32, // Value x 2mA, set to 0 for self powered, was 0x32
/*********************************************************************
  Interface 0 - Communications Class
 *********************************************************************/                                               
/* bLength                                  */ 0x09,
/* bDescriptorType: INTERFACE               */ 0x04,
/* bInterfaceNumber                         */ 0x00, // So as you can see, the interface numbers are zero-based
/* bAlternateSetting                        */ 0x00, // It has no alternate configurations
/* bNumEndpoints: 1 endpoint(s)             */ 0x01, // We have one endpoint for this interface
/* bInterfaceClass: CDC                     */ 0x02, // It is a CDC device
/* bInterfaceSubclass:Abstract Control Model*/ 0x02, // Using the Abstract Control Model (ACM)
/* bInterfaceProtocol:AT Commands V.25ter   */ 0x01, // Using AT Commands v.25ter
/* iInterface                               */ 0x00, // String offset = 0 again means no string
/*********************************************************************
 Header Functional Descriptor
 *********************************************************************/                                               
/* bLength                                  */ 0x05,
/* bDescriptorType: CS_INTERFACE            */ 0x24,
/* bDescriptorSubtype: HEADER FD            */ 0x00,
/* bcdADC                                   */ 0x20,0x01,
/*********************************************************************
 Abstract Control Model Functional Descriptor
 *********************************************************************/                                               
/* bLength                                  */ 0x04,
/* bDescriptorType: CS_INTERFACE            */ 0x24,
/* bDescriptorSubtype: ACM-FD               */ 0x02,
/* bmCapabilities                           */ 0x02,
/*********************************************************************
 Union Functional Descriptor
 *********************************************************************/                                               
/* bLength                                  */ 0x05,
/* bDescriptorType: CS_INTERFACE            */ 0x24,
/* bDescriptorSubtype: Union FD             */ 0x06,
/* bControlInterface                        */ 0x00,
/* bSubordinateInterface                    */ 0x01,
/*********************************************************************
 Call Management Functional Descriptor
 *********************************************************************/
/* bLength                                  */ 0x05,
/* bDescriptorType: CS_INTERFACE            */ 0x24,
/* bDescriptorSubtype: CM-FD                */ 0X01,
/* bmCapabilities                           */ 0x00,
/* bDataInterface                           */ 0x01,
/*********************************************************************
 Interrupt IN Endpoint - 1 - IN means **to** the host **from** the device
 *********************************************************************/                                               
/* bLength                                  */ 0x07,
/* bDescriptorType: ENDPOINT                */ 0x05,
/* bEndpointAddress: IN Endpoint 1          */ 0x81, // This is very important. 0x81 means endpoint 0x1 with an 0x80 ORed to it. Just writing 0x1 here would make it an OUT endpoint and you'd likely get a descriptor error from the host.  
/* bmAttributes: INTERRUPT                  */ 0x03, // CDC needs one interrupt mode IN endpoint
/* max packet size (LSB)                    */ 0x10, 
/* max packet size (MSB)                    */ 0x00, // The maximum size of this endpoint was set to 16 bytes in the endpoint initialisation, so we set it to 0x0010, which is 16
/* polling interval                         */ 0x2,  // A brief polling interval to ensure rapid checking
/*********************************************************************
 Interface 1 - Data Class Interface - The second interface of two
 *********************************************************************/                                               
/* bLength                                  */ 0x09,
/* bDescriptorType: INTERFACE               */ 0x04,
/* interface index                          */ 0x01,
/* alt setting index                        */ 0x00,
/* bNumEndpoints: 2 endpoint(s)             */ 0x02, // This interface has two BULK mode endpoints attached to it
/* bInterfaceClass: CDC-Data                */ 0x0A,
/* bInterfaceSubclass: unused               */ 0x00,
/* bInterfaceProtocol: None                 */ 0x00,
/* iInterface                               */ 0x00,
/*********************************************************************
 Endpoint 2 (Bulk OUT)
 *********************************************************************/
/* bLength                                  */ 0x07,
/* bDescriptorType: ENDPOINT                */ 0x05,
/* bEndpointAddress: OUT Endpoint 2         */ 0x02, // Again, 0x02 means endpoint 2 sending from host to device (OUT from host)
/* bmAttributes: BULK                       */ 0x02, // Bulk mode protocol
/* max packet size (LSB)                    */ 0x00,
/* max packet size (MSB)                    */ 0x02, // 0x0200 = 512 bytes maximum packet size
/* bInterval: None for BULK                 */ 0x00, // No interval for bulk mode endpoints
/*********************************************************************
 Endpoint 2 (Bulk IN)
 *********************************************************************/                                               
/* bLength                                  */ 0x07,
/* bDescriptorType: ENDPOINT                */ 0x05,
/* bEndpointAddress: OUT Endpoint 2         */ 0x82, // 0x82 means endpoint 2 but sending from device to host (IN to host)
/* bmAttributes: BULK                       */ 0x02, // Bulk mode protocol
/* max packet size (LSB)                    */ 0x00,
/* max packet size (MSB)                    */ 0x02, // 0x0200 = 512 bytes maximum packet size
/* bInterval: None for BULK                 */ 0x00 // No interval for bulk mode endpoints
};

OK, so a pretty long array that you just need to send to the device to set it up on the host. Let's take a look at just a few of the things you need to be aware of:

/*  bmAttributes                            */ 0x80, /* bit 6 set = bus powered = 0x80, 0xC0 is for self powered */
/*  bMaxPower                               */ 0x32, /* Value x 2mA, set to 0 for self powered, was 0x32 */

For bmAttributes, a value of 0x80 means it is bus-powered and 0xC0 means it is self-powered. There is another feature of USB called remote wake-up, which allows the host to wake up your device. Pretty cool, but unfortunately:

PIC32MZ USB No Remote Wakeup

Great. Another win for Microchip. The next value, bMaxPower sets how many milliamps the device is allowed to draw, maximum, from the USB port, but divided by 2. As my device uses up to 100mA, I set this value to 0x32, which is 50 (50 x 2 = 100mA). As you can see, the maximum device a USB port can thus provide is 255 x 2 = 510mA (but the spec limits this to 500mA).

I've added comments to part of this but honestly, there's not much you can tweak and change here without breaking it, besides stuff relating to strings and endpoints.

Let's take a look at something else you may want to send to the host, namely the strings defining your device:

#define STR_DESC(l) (((((l))<<1) & 0xFF) | 0x0300)
// String descriptors. All have a common format: [byte] string_length, [byte] data_type (3 = string), UTF-16 encoded string, hence all the 0's between each 8-byte character

// Language description
unsigned char string0[] =  { 4, 0x03, 0x09, 0x04}; // 0x0409 = English

// Vendor description
unsigned short string1[] = { STR_DESC(11),
                            'A','i','d','a','n',' ','M','o','c','k','e'};   

// Product description
unsigned short string2[] = { STR_DESC(12),
                            'U','S','B',' ','C','D','C',' ','T','e','s','t'};

// Serial number description
unsigned short string3[] = { STR_DESC(7),
                            '1','2','3','-','3','2','1'};

// Configuration description
unsigned short string4[] = { STR_DESC(8),
                            'U','L','T','R','A',' ','S','E'};
// Interface description 
unsigned short string5[] = { STR_DESC(7),
                            'C','D','C',' ','A','C','M'};

Hmmm... OK so that may not be quite what you were expecting. All the strings are encoded in UTF-16, meaning each character takes up two bytes. The first byte is the string length (so a max of 255 again) followed by descriptor type 0x3 for string. I've combined this using a macro found on The Thirteenth Floor:

#define STR_DESC(l) (((((l))<<1) & 0xFF) | 0x0300)

So basically, if you say STR_DESC(0x8) it'll spit out 0x310.

OK! So that's how you send the configuration descriptor. There's not much left but that's enough for this time I think.

Next time I'll finish off the CDC example. Take care!

Tags: code, USB