Programming the USB module of PIC32MZ without Harmony (3) – Setup and Configuration as USB device

Let’s start coding…

Wait…. I need to list the procedures below how the USB device is setup through the enumeration process:

  1. USB initialization:
  2. Detect USB bus reset (through _USB_VECTOR) and initialize endpoints.
  3. Detect endpoint 0 interrupt (through _USB_VECTOR) and communicate with USB host regarding:
    • Receiving USB address and setup.
    • Sending USB description of the device.

Now let break down each item with the codes:

1. USB initialization:

This is very high-level but basic configuration that needs to be up during the PIC’s start-up phase:

void initUSB(void)
{
    USBCSR0bits.SOFTCONN = 0;   // Initially disable USB while we go into setup

    /* 
     * USB host hasn't given any address yet, assign 0 for now 
     * until later when the host assigns us an address.
     */
    USB_address = 0;
    USBCSR0bits.FUNC = 0;

    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

}

This section can also include more USB setup such as USB DMA, which we’ll cover in my later posts. As the first attempt, I would like to make sure the PIC32MZ can be enumerated correctly.

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

The above code will enable the USB D+/D- lines and make the USB device active. This code can be part of the initUSB() function. Upon the execution of USBCSR0bits.SOFTCONN = 1, you will hear the USB connection sound, and the host (usually it’s a PC) will first start with resetting the USB bus. Since we’ve already enabled the USB ISR, the reset action will be captured immediately on the device as follows:

2. Capture USB reset and initialize the endpoints.

All the USB events will be recorded in their associated flags among USBCSR1/2/3. All the triggers of those events will be recorded through the _USB_VECTOR. Once the ISR is called, we will need to first distinguish which event is triggered:

uint8_t USB_set_address = 0;

void __attribute__((vector(_USB_VECTOR), interrupt(ipl7srs), no_fpu, nomips16)) handleUSB()
{
    unsigned char *FIFO_data;
    
    uint32_t CSR0 = USBCSR0;
    
    _Bool EP0IF = (CSR0 & (1<<16)) ? 1 : 0;
    _Bool EP3TXIF = (CSR0 & (1<<19)) ? 1 : 0;
    
    uint32_t CSR1 = USBCSR1;
    
    _Bool EP1RXIF = (CSR1 & (1 << 1)) ? 1 : 0;
    _Bool EP2RXIF = (CSR1 & (1 << 2)) ? 1 : 0;
    
    uint32_t CSR2 = USBCSR2;
    
    _Bool RESETIF = (CSR2 & (1 << 18)) ? 1 : 0;

    
    if(RESETIF)
    {
        /*
         * When this is set, the USB bus has been reset and 
         * we need to set up all the USB endpoints.
        */
        initUSBEndpoints();
        USBCSR2bits.RESETIF = 0;      
    }
    
    /* Endpoint 0 Interrupt Handler */
    if(EP0IF)
    {
        if (USB_set_address) // Set the USB address if not
        {
            USBCSR0bits.FUNC = USB_address & 0x7F;
            USB_set_address = 0;
        }
        
        if(USBE0CSR0bits.RXRDY)
        {
            receiveUSBEP0(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];
            
            processUSBEP0Transaction();
            
            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.
    }
    IFS4bits.USBIF = 0;
}

3. Detect endpoint 0 event

I have verified through the actual enumeration process that the “if(RESETIF)” branch is firstly called upon connection. So the PIC will firstly setup all endpoints as the first step. Once endpoints are set, the host will start sending data down to the endpoint 0, which includes information such as address and request types, the endpoint 0 data reception is handled by the function “receiveUSBEP0()”.

void receiveUSBEP0(int length)
{
    unsigned char *FIFO_buffer;
    
    // Store number of bytes received
    EP[0].rx_num_bytes = USBE0CSR2bits.RXCNT;
                
    // Get 8-bit pointer to USB FIFO for endpoint 0
    FIFO_buffer = (unsigned char *)&USBFIFO0;
    
    for(cnt = 0; cnt < length; cnt++)
    {
        // Read in one byte at a time
        EP[0].rx_buffer[cnt] = *(FIFO_buffer + (cnt & 3));
    }
        
    USBE0CSR0bits.RXRDYC = 1;
}

Based on the types of requests, the endpoint 0 will respond to the host using the function “processUSBEP0Transaction()”.

void processUSBEP0Transaction(void)
{
    switch (USB_transaction.bmRequest)
    {
        case 0xC:
        {
            USBE0CSR0bits.STALL = 1;
            break;
            
        }
        case 0x0: 
        {
            if (USB_transaction.bmRequestType == 0x80) // Get status
                queueUSBEP0(USB_device_description, 0, 0);
            if (USB_transaction.bmRequestType == 0x00) // Select function
                queueUSBEP0(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
                {
                    queueUSBEP0(USB_device_description, sizeof(USB_device_description), USB_transaction.wLength);
                    break;
                }
                case 0x2: // Get configuration descriptor
                {
                    queueUSBEP0(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
                        {
                            queueUSBEP0(string0, sizeof(string0), USB_transaction.wLength);
                            break;
                        }
                        case 0x1: // String 1 - Vendor
                        {
                            queueUSBEP0(string1, sizeof(string1), USB_transaction.wLength);
                            break;
                        }
                        case 0x2: // String 2 - Product name
                        {
                            queueUSBEP0(string2, sizeof(string2), USB_transaction.wLength);
                            break;
                        }
                        case 0x3: // String 3 - Serial number
                        {
                            queueUSBEP0(string3, sizeof(string3), USB_transaction.wLength);
                            break;
                        }
                        case 0x4: // String 4 - Configuration
                        {
                            queueUSBEP0(string4, sizeof(string4), USB_transaction.wLength);
                            break;
                        }
                        case 0x5: // String 5 - Interface
                        {
                            queueUSBEP0(string5, sizeof(string5), USB_transaction.wLength);
                            break;
                        }
                        default: break;
                    }
                }
                default: 
                {
                    break;
                }
            }
            break;
        }
        case 0x9: // Set interface
        {
            break;
        }
        default: 
        {
            USBE0CSR0bits.STALL = 1;
            break;
        }
    }   
}

The above endpoint 0 response code works for the “custom (0xFF)” interface class and subclass. Pre-defined class, such as “CDC (0x0A)”, requires very specific responses in order for the device to setup properly.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.