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

Initialising the USB peripheral

Initialising the USB peripheral and what registers actually matter

Updated 2020-12-04 with some stuff I wasn't sure of yesterday.

Welcome to another torturously long post. This one is only for people who want to get a very basic understanding of USB in general and how to initialise the USB peripheral and some endpoints.

OK so you want to use the PIC32MZ USB peripheral. Good call! It is high speed (not full speed or low speed like most STM32 chips that need an external high speed PHY connected to them) and actually does most of the work of getting USB to work for you. It's a genuinely good peripheral in my opinion. However, USB itself is a tricky thing to get into and understand. Today I want to cover the very, very basics of how to get the USB peripheral working and how to get your first packet from the computer.

How to set up the USB peripheral and put it into High Speed mode

First off, the good old USB naming conventions w.r.t. speed need to be cleared up:

  • Low Speed (LS) - 1.5 Megabits/s = 187.5kB/s max (awfully slow)
  • Full Speed (FS) - 12 Megabits/s = 1.5MB/s max (fine for HID devices, etc)
  • High Speed (HS) - 480 Megabits/s = 60MB/s max (what I want for my MSD [Mass Storage Device] application)
  • SuperSpeed (SS) - 5 Gigabits/s = 625MB/s max (this is USB 3, not available on any PIC device I know of)

The PIC32MZ series supports High Speed, whereas the older PIC32MX series only supported up to Full Speed. So naturally, as a PIC32MZ user I'm going to be focusing purely on High Speed operation. I am also going to be focusing on making a USB device for now (i.e. a device controlled by a host like a computer). At some point I will go into USB host mode (like reading from and writing to USB flash drives) but that's a story for another time.
If you just take any old PIC32MZ board and plug it into the computer via a USB cable, nothing will happen because the USB peripheral is off by default. To get it to start communicating with the computer we need to enable it, like this:

void USB_init(void *receive_callback)
{
    USB_RECEIVE_CALLBACK = receive_callback;

    USBCSR0bits.SOFTCONN = 0;   // Initially disable it while we go into setup

    USB_address = 0;            // We haven't been given an address yet    
    USBCSR0bits.FUNC = 0;       // Initially set the USB address to 0 until later when the host assigns us an address

    USBCSR2bits.RESETIE = 1;    // Enable the reset interrupt
    IEC4bits.USBIE = 1;         // Enable the USB interrupt
    USBCRCONbits.USBIE = 1;     // Enable USB module interrupt
    IFS4bits.USBIF = 0;         // Clear the USB interrupt flag.
    IPC33bits.USBIP = 7;        // USB Interrupt Priority 7
    IPC33bits.USBIS = 1;        // USB Interrupt Sub-Priority 1

    USBCSR0bits.HSEN = 1;       // Enable High Speed (480Mbps) USB mode
}

Line by line, as with all peripherals we turn the USB peripheral off while we configure it. This is controlled by the SOFTCONN bit field.
Secondly, initialise the USB address as 0. This I store both in an internal variable called USB_address and also in the hardware bit field USBCSR2.FUNC. Later on the computer will give us an address. Again, we are making a USB device, something that is controlled by a host. In this case, we can't assign ourselves an address. The host device, usually a computer, can have up to 127 USB devices attached to it and will handle assigning each of them an address by itself. More on this later.
Next, we set up the USB interrupt-related stuff. The USB peripheral communicates your our program via interrupts, and anything that happens will come into one giant interrupt handler. For now, I've set it to interrupt priority 7, sub-priority 1 but these numbers don't really matter. I've also enabled the USB interrupts in IEC4 and set the USB peripheral to generate interrupts as well as told it to generate an interrupt when a "USB reset" happens. More on this too, in a bit :)
Finally, turn on High Speed mode by setting USBCSR0bits.HSEN to 1. Setting it to 0 means we are going to be in low speed mode.

You may have notice that we didn't turn the USB peripheral on again. That's because after we've initialised it, we're supposed to wait until the VBUS pin is high, meaning we're connected to USB. If you're making a board, connect the VBUS pin on the PIC32MZ to the USB connector's VBUS pin (it's 5V tolerant so no worries there). In the project I uploaded yesterday, I didn't bother to check if we're connected, I assumed we were connected and powered by USB so if it didn't work that may be why.
OK, that's not actually too bad right? Pretty simple for a PIC32MZ peripheral really. Let's turn the USB peripheral on and get to the real fun.

void USB_connect()
{
    USBCSR0bits.SOFTCONN = 1;
}

From this moment on, the PIC32MZ and the computer will start communicating. From what I can see, the first thing the computer will do is send a "Reset" instruction to the device and it is at this point that you actually set up your device and get ready to roll. Inside your ISR (interrupt service routine), that'll look like this:

void __attribute__((vector(_USB_VECTOR), interrupt(ipl7srs), nomips16)) USB_handler()
{
    if(USBCSR2bits.RESETIF)
    {
        // So when this is set, the USB bus has been reset and we actually need to set up all the USB endpoints now

        USB_init_endpoints();

        USBCSR2bits.RESETIF = 0;      

    }
}

(Side note: PLEASE make sure you add SRS (not SOFT) after the ipl (interrrupt priority level) in your ISR declarations. AUTO works as well, usually, but set it to SRS to be sure. Setting it to SOFT means the PIC32MZ has to save the registers each time the ISR is called and it wastes tons of time. Do be aware you need to set the PRISS register for this to work, which I covered in an earlier post, but setting PRISS = 0x76543210 works in a pinch).
OK, so when a reset is called for, USBCSR2bits.RESETIF is set, so we catch it and then we actually set up our endpoints. But what are endpoints?

USB Endpoints

When getting into USB on the PIC32MZ, half of the battle was trying to understand what I was reading on websites. Terms like "endpoint" and "setup packet" were thrown around and it was pretty hard to understand. Perhaps I'm just not bright enough :) Let's look at the Microchip Developer site for an image

Endpoint Table

Long story short, endpoints are communications channels between your device and the host (computer or whatever that may be). An endpoint that sends data from the Host to the Device is called an OUT Endpoint. One that reads in data to the host from the device is called an IN Endpoint. This IN/OUT stuff is a bit confusing at first, but you always need to think of it from the point of view of the host, not from your PIC32MZ device. Furthermore, for High Speed and under, these endpoints are one way only (i.e. not full duplex but half duplex), though their direction can be controlled in software. So, for example, if you wanted to read a message from the host, you'd have to set your endpoint to be an OUT endpoint and then read the data. If you then wanted to send a reply, you'd have to set your endpoint to be an IN endpoint and then send the message.

There are 7 endpoints available on the PIC32MZ for us to use as we wish, numbered 1 to 7. Initially, when the device connects to the computer all communication will be done on Endpoint 0 because the computer doesn't yet known which endpoints we've chosen to use. Endpoint 0 is special and is used for what they call Control Transfers. It sends instructions from the host to the device and replies back from the device to the host.
So what happens when we plug our device in is that the host asks our device what type of device it is, what endpoints it uses and a whole bunch of other stuff. This is actually fairly involved and I won't cover it today. What I will end on today is the setup of our chosen endpoints.

Additionally, each endpoint has a mode it operates in. The valid modes are:

  • Control transfers
  • Interrupt transfers
  • Isochronous transfers
  • Bulk transfers

OK. Let's type some more! To put it fairly (perhaps too) simply:

  • Control transfers are used to send messages back and forth between the host and device about the setup/configuration/status of the device.
  • Interrupt transfers - Intended for small, infrequents bursts of data. In this mode the host is periodically checking to see if an interrupt has been raised so it guarantees delivery of data within a certain set time period.
  • Isochronous transfers - Used for time-dependent information like audio and video. It is periodic like an Interrupt transfer but it is also continuous. However, due to the need to guarantee of time and speed of transfer, it does not retry to send on error.
  • Bulk transfers - Used to transfer large blocks of non-time-dependent data in bursts. Guarantees delivery of data but not there are no time guarantees. Can use all currently available bandwidth to send data as fast as possible.

For a better explanation, please see this site.

Setting up our chosen endpoints

For the next few posts, I'm going to be covering how to write a program that'll give us a Communications Device Class (aka CDC) device. Looking at the spec for a CDC device, we see we need:

  • One interrupt IN endpoint for notifications to the USB Host
  • One bulk IN and one bulk OUT endpoint for data transfer

As we need two different kinds of endpoints, we need at least two endpoints. We could use three in total but I'm going to just use two. Why? Well, for a Virtual COM port it's highly unlikely we'll be looking at simultaneous two-way communication and I also want to cover how to change the endpoint modes from IN to OUT and back again :)

OK, so to carry on from the code earlier, a USB interrupt has been raised and USBCSR2bits.RESETIF was set, meaning we now need to actually do our endpoint setup. This mostly likely didn't need to be done earlier but I really struggled getting USB to work initially and it led to me being paranoid and putting it into the USB_init() function to. Anyway, onto the code:

void USB_init_endpoints()
{
    USBE1CSR0bits.MODE = 0;     // EP1 is OUT (OUT from host = in to device = RX)
    USBE2CSR0bits.MODE = 0;     // EP2 is OUT (for now, will be IN at times)

    // Clear all interrupt flags
    USBCSR0bits.EP0IF = 0;
    USBCSR0bits.EP1TXIF = 0;
    USBCSR0bits.EP2TXIF = 0;
    USBCSR1bits.EP1RXIF = 0;
    USBCSR1bits.EP2RXIF = 0;

    // Set the maximum transfer length for each endpoint
    // Configure endpoint 0 first.
    USBE0CSR0bits.TXMAXP = 16; // Set endpoint 0 buffer size to 16 bytes (can be a maximum of 64 for EP0)

    // And next my custom endpoints
    USBE1CSR0bits.TXMAXP = 16;   // Endpoint 1 - Maximum TX payload / transaction (can be a maximum of 64 for CDC "Interrupt IN endpoint"
    USBE2CSR0bits.TXMAXP = 512;   // Endpoint 2 - Maximum TX payload / transaction (512 is the maximum, set to 512)
    USBE2CSR1bits.RXMAXP = 512;   // Endpoint 2 - Maximum RX payload / transaction (512 is the maximum, set to 512)

    // Specify which kinds of endpoint we will be using
    USBE1CSR2bits.PROTOCOL = 3; // Endpoint 1 - Interrupt mode
    USBE2CSR2bits.PROTOCOL = 2; // Endpoint 2 TX - Bulk mode
    USBE2CSR3bits.PROTOCOL = 2; // Endpoint 2 RX - Bulk mode

    // Enable DISNYET
    USBE1CSR1bits.PIDERR = 0; // Clear DISNYET to enable NYET replies
    USBE2CSR1bits.PIDERR = 0; // Clear DISNYET to enable NYET replies

    // Set up buffer location for endpoint 1
    USBCSR3bits.ENDPOINT = 1;
    USBFIFOA = 0x08;
    USBIENCSR0bits.CLRDT = 1;

    // Set up buffer locations for endpoint 2
    USBCSR3bits.ENDPOINT = 2;
    USBFIFOA = 0x000A004A;
    USBIENCSR0bits.CLRDT = 1;
    USBE2CSR3bits.TXFIFOSZ = 0x9;   // Transmit FIFO Size bits - 512 bytes
    USBE2CSR3bits.RXFIFOSZ = 0x9;   // Receive FIFO Size bits - 512 bytes

    // Set maximum size for each packet before splitting occurs
    USBOTGbits.RXFIFOSZ = 0b0110; // 0b0110 = 512 bytes
    USBOTGbits.TXFIFOSZ = 0b0110; // 0b0110 = 512 bytes

    // Set receive endpoint 2 to High Speed
    USBE2CSR3bits.SPEED = 1;

    // Disable Isochronous mode for endpoints 1 and 2
    USBE1CSR0bits.ISO = 0;      // Isochronous TX Endpoint Disable bit (Device mode).
    USBE2CSR0bits.ISO = 0;      // Isochronous TX Endpoint Disable bit (Device mode).

    // Set up endpoint interrupts
    // Initially clear all the interrupt enables (IE)
    USBCSR1 = 0;
    USBCSR2 = 0;

    USBCSR1bits.EP0IE = 1;      // Endpoint 0 interrupt enable
    USBCSR1bits.EP1TXIE = 1;    // Endpoint 1 TX interrupt enable
    USBCSR2bits.EP1RXIE = 1;    // Endpoint 1 RX interrupt enable
    USBCSR1bits.EP2TXIE = 1;    // Endpoint 2 TX interrupt enable
    USBCSR2bits.EP2RXIE = 1;    // Endpoint 2 RX interrupt enable
    USBCSR2bits.RESETIE = 1;
    USBCSR2bits.RESUMEIE = 1;
    USBCSR2bits.SOFIE = 1;

    // Set current endpoint to EP2
    USBCSR3bits.ENDPOINT = 2;    
}

OK that's incredibly involved. Let's take a brief overview of what we've done.

USBE1CSR0bits.MODE = 0;     // EP1 is OUT (OUT from host = in to device = RX)
USBE2CSR0bits.MODE = 0;     // EP2 is OUT (for now, will be IN at times)

Not much to see here. Just setting the current operation mode of the EPs to be OUT (receive from host). This is entirely unnecessary in this function, we only need to set them when we get an interrupt, but I'm covering here how to change the mode.

// Clear all interrupt flags
USBCSR0bits.EP0IF = 0;
USBCSR0bits.EP1TXIF = 0;
USBCSR0bits.EP2TXIF = 0;
USBCSR1bits.EP1RXIF = 0;
USBCSR1bits.EP2RXIF = 0;

Self-explanatory, clear all the flags.

// Set the maximum transfer length for each endpoint
// Configure endpoint 0 first.
USBE0CSR0bits.TXMAXP = 16; // Set endpoint 0 buffer size to 16 bytes (can be a maximum of 64 for EP0)

// And next my custom endpoints
USBE1CSR0bits.TXMAXP = 16;   // Endpoint 1 - Maximum TX payload / transaction (can be a maximum of 64 for CDC "Interrupt IN endpoint"
USBE2CSR0bits.TXMAXP = 512;   // Endpoint 2 - Maximum TX payload / transaction (512 is the maximum, set to 512)
USBE2CSR1bits.RXMAXP = 512;   // Endpoint 2 - Maximum RX payload / transaction (512 is the maximum, set to 512)

OK, here we are getting to something more interesting. This is how many bytes can be sent in a single transaction between the host and the device.

// Specify which kinds of endpoint we will be using
USBE1CSR2bits.PROTOCOL = 3; // Endpoint 1 - Interrupt mode
USBE2CSR2bits.PROTOCOL = 2; // Endpoint 2 TX - Bulk mode
USBE2CSR3bits.PROTOCOL = 2; // Endpoint 2 RX - Bulk mode

The protocol/transfer type of each endpoint needs to be set up. As discussed earlier, we need one interrupt, one bulk IN and one bulk OUT endpoint.

BE WARNED

For setting the transfer type of IN endpoints (from device to host), you change bits in USBEnCSR2 (where n = endpoint number, in my case 2), but for OUT endpoints (from host to device) you change bits in USBEnCSR3!
A further hilarious note is that in the datasheet it's called USBIEnCSR2 not USBEnCSR2. They point to the same register but XC.h seems to name them differently from the datasheet for whatever reason. I guess because they want you to use Harmony and that means you'll never need to know it? Who knows.

// Enable DISNYET
USBE1CSR1bits.PIDERR = 0; // Clear DISNYET to enable NYET replies
USBE2CSR1bits.PIDERR = 0; // Clear DISNYET to enable NYET replies

PIDERR? DISNYET? In USB host mode, this bit field is called PIDERR and in device mode it's called DISNYET (Disable NYET). It's the same field that fills two roles. The NYET (not Russian but short for No response YET) is a special kind of handshake that the device uses to tell the host that the device isn't ready for the next part of the transfer to happen. This can happen if the FIFO buffer is full or for any numbers of reasons. I now set it to 0 to enable the NYET packets.

// Set up buffer location for endpoint 1
USBCSR3bits.ENDPOINT = 1;
USBFIFOA = 0x08;
USBIENCSR0bits.CLRDT = 1;

// Set up buffer locations for endpoint 2
USBCSR3bits.ENDPOINT = 2;
USBFIFOA = 0x000A004A;
USBIENCSR0bits.CLRDT = 1;
USBE2CSR3bits.TXFIFOSZ = 0x9;   // Transmit FIFO Size bits - 512 bytes
USBE2CSR3bits.RXFIFOSZ = 0x9;   // Receive FIFO Size bits - 512 bytes

OK, we're getting to some of the real involved stuff now. Notice that we've set USBCSR3bits.ENDPOINT = 1;? That's because for some registers, like USBFIFOA, any value you put into them or read from them is valid only for the value of USBCSR3bits.ENDPOINT. So if you change ENDPOINT to 2, you'll get/set endpoint 2's information. Something to be wary of for sure!
Set the FIFO address to... 8? Not actually, no. The 8 there means 64. Due to the way the PIC32MZ's USB FIFO buffer works, the actual starting address is whatever you put in there times 8. So, in other words, start endpoint 1 TX FIFO buffer at address 64.

USBIENCSR0bits.CLRDT = 1;

According to the datasheet this "Resets the endpoint data toggle to 0". Upon further research, I've found that USB packets use DATA0 and DATA1 PIDs (Packet Identifiers) (and USB High Speed adds DATA2). In low speed and full speed transactions, when transmitting packets they toggle DATA0 and DATA1 as a form of simple error checking to know if the packet they're receiving is the one they're supposed to receive. For example, if a device receives two packets in a row with a PID of DATA0 it'll know something went wrong. DATA2 (and another one called MDATA) seems to have been added to High Speed for use with isochronous transfers and are thus beyond what I'll cover today. Thankfully this is all handled for us by the PIC32MZ's USB peripheral so we never have to worry about it.
Please see this site for more details.

USBFIFOA = 0x000A004A;

OK that looks different to endpoint 1. The first four digits are for the receive buffer's start address, which is 000A = 10 * 8 = 80. 80 because we started endpoint 1 at 64 and it's 16 bytes long, so 16 + 64 = 80. The next four digits are the transfer buffer start point. 4A, which is 74. 74 * 8 = 592 and 592 - 80 is 512, which is the length of the EP2 receive buffer. So start EP2's transfer buffer at position 592.

USBIENCSR0bits.CLRDT = 1;
USBE2CSR3bits.TXFIFOSZ = 0x9;   // Transmit FIFO Size bits - 512 bytes
USBE2CSR3bits.RXFIFOSZ = 0x9;   // Receive FIFO Size bits - 512 bytes

TXFIFOSZ and RXFIFOSZ come from the datasheet. They look like this:

TXFIFOSZ RXFIFOSZ

Very, very annoying how they only show some values while saving minimal space. If you work down, you'll see that 0x9 (0b1001) works out to 512 bytes maximum send and receive buffer size.

// Set maximum size for each packet before splitting occurs
USBOTGbits.RXFIFOSZ = 0b0110; // 0b0110 = 512 bytes
USBOTGbits.TXFIFOSZ = 0b0110; // 0b0110 = 512 bytes

Missing out these two lines caused my program to not work at all for a few days. If I sent more than, iirc, 8 characters to my code it'd crash the windows app and cause me much grief. Here we have a different table to work from, and 0110 means 512 bytes.

// Set up endpoint interrupts
// Initially clear all the interrupt enables (IE)
USBCSR1 = 0;
USBCSR2 = 0;

USBCSR1bits.EP0IE = 1;      // Endpoint 0 interrupt enable
USBCSR1bits.EP1TXIE = 1;    // Endpoint 1 TX interrupt enable
USBCSR2bits.EP1RXIE = 1;    // Endpoint 1 RX interrupt enable
USBCSR1bits.EP2TXIE = 1;    // Endpoint 2 TX interrupt enable
USBCSR2bits.EP2RXIE = 1;    // Endpoint 2 RX interrupt enable
USBCSR2bits.RESETIE = 1;
USBCSR2bits.RESUMEIE = 1;
USBCSR2bits.SOFIE = 1;

// Set current endpoint to EP2
USBCSR3bits.ENDPOINT = 2;    

Not much to explain here. Just setting interrupt enables so that when these interrupts occurs I get notified of them in my ISR.

Phew, what an incredibly long post. Next time, CDC.

Tags: code, tl;dr

USB

USB on the PIC32MZ

UPDATE: ERROR IN CODE FIXED

In the USB_device_description, change the line called /* bDeviceClass */ from 0x00 to 0x02 otherwise enumeration won't complete

Hello again all, he said to no one. Great news! I've finally gotten the USB port on the PIC32MZ working without Harmony. It's been a multi-year on-again-off-again project but this time I managed to get it done. Well, it works on all my development boards anyway.
What I've gotten to work so far is USB CDC (virtual COM port), which means I no longer have to use a USB-UART chip on my PIC32MZ development boards. From here I want to move on to getting HID working and then the ultimate goal of MSD. But for now, I'm uploading the CDC code and hopefully if anyone reads this they can see if it works on their boards :)

The documentation is severely lacking on USB-related stuff and to give credit where credit is due, here are all the sites I referenced when making my code:
* Microchip's own OTG datasheet
* Microchip's AN1166 on USB CDC
* The massively long USB.org CDC spec 1.2
* MARKDINGST.BLOGSPOT.COM that helped me understand the get/set line coding better
* WWW.XMOS.AI that helped me understand what the various endpoints were
* AIJU.DE that really helped explain some of the intricacies of the flags I needed to set
* WWW.BEYONDLOGIC.ORG for their simple explanataion of the USB setup packets
* WWW.SILABS.COM for a better explanation of USB-CDC

And then the big ones. Actual examples of USB-CDC for the PIC32MZ. I couldn't get either of these to work on my dev board but I used them as skeletons to base my own code off of (with a fair bit of copy and pasting!). Thanks to both of these people without whom I'd never have had any hope of getting this done:

What you will notice missing there is MPLAB Harmony, because it continues to be a bloated, slow, useless mess that is so cryptic that it is useless and provides no hints of how to do anything yourself. The Harmony approach of one library for all devices may be great for corporations who don't care about how anything works but it is atrocious for a hobbyist like me trying to understand how to use the PIC32MZ series.

Anyway, I will go into a brief explanation of USB and CDC in a future post but for now I just wanted to share the code.

Please note that while I have started splitting up the code into a few different files the work isn't done yet. I also have a lot more to learn about USB so some of the naming or things I'm doing may well be wrong :) Point is, it works on my board and I think having the code out there is better than not having it out there. As always, YMMV!

Here's the code. I hope it works on your boards and, if not, leave a comment and I'll try and help out.

Tags: code

A quick update!

An update on my site

Hi there everyone and no one. I haven't updated this site for quite some time (10 months or so). Certainly the COVID-19 pandemic has changed the way we do work and changed the world but apart from that I really just started this site to share the knowledge of PIC32MZ that I had gained through years of trial and error. I was sick of seeing useless "read the manual" and "work it out for yourself" posts by arrogant and selfish people on the microchip forums and annoyed by the tons of errors I had found in the datasheets. But now that I've written about all the stuff I wanted to share I really don't know what more to write about :)

I'm keeping the site up and I do hope it can provide a useful reference to the few people that need it. I'll also be uploading some of the schematics of stuff I've been working on in the next week or two.

Works and life have kept me busy and so I've been putting off replying to posts ("I'll get to that tomorrow") but I'll get to them today and hopefully stop being so lazy :)

Apologies if I miss out on replying to anyone's older questions the notification system here sucks!

Tags: rambling