Click here to Skip to main content
15,885,278 members
Articles / Internet of Things / Arduino

Getting video stream from USB web-camera on Arduino Due - Part 5: Getting standard USB device descriptor

Rate me:
Please Sign up or sign in to vote.
4.75/5 (6 votes)
22 Jun 2015CPOL19 min read 23.3K   399   13  
Getting first USB device descriptors

Introduction

Part 4 is here.

In this article I'll show how to communicate with USB device and get a standard USB device descriptor out of it. Along the way I'll explain interrupt driven technique to chain function calls. At the end, received device descriptor will be shown in human readable format.

Note: the content of the article applies not only to cameras, but to all USB devices like mice, keyboards, anything you have. It should be fun to read descriptors of various devices as it gives some low level understanding of how things work.

Control transfers

Control transfers happen on a control pipe which is pipe number zero. After device is connected and reset (previous article) it is ready to accept standard requests directed to its default endpoint (number zero). A control pipe is a communication channel between host and device's endpoint number zero. Control pipe communication is called control transfer. All transfers in USB happen in stages. A control transfer consists of 2 or 3 stages, namely SETUP, DATA and HANDSHAKE. DATA stage is not always present as some transfers contain all necessary information inside SETUP stage. A stage consists of one or more transactions, in particular SETUP and HANDSHAKE stages have always one transaction but DATA state can have more. For example, control pipe maximum data size is set to 64 bytes which means that if device needs to send 100 bytes it won't fit into one transaction, thus 64 bytes go first then last 36 bytes go in second transaction. Image 1

In this article I'll be getting a descriptor that is well under 64 bytes thus DATA stage will consist of one transaction only. Let's see how control transfer looks.

Red rectangles are transactions, black rectangles inside reds are packets. Darker packets are those sent by host, white packets are sent by device. Each of those packets has a well-defined structure, basically they are one or more sets of numbers that are sent serially via wire.

Control transfer sequence discussion

Note that each stage is started by host. SETUP, IN and OUT packets define device address and endpoint address which both are zero when device was just connected and reset. DATA0 packet in SETUP stage defines what we want to do, in other words which descriptor is requested. Once device got DATA0 packet it issues ACK to indicate successful receival. If device does not send ACK back - something is wrong, host should attemt to start sending SETUP transaction again after some time.

Once SETUP transaction is completed, host waits certain amount of time to allow device to proceed request. Then host issues IN packet and waits for DATA0 packet. Note that if we had more than one IN transactions, second data packet would've been DATA1, third DATA0 and so on. After host received DATA0, it acknowledges DATA transaction. DATA0/DATA1 change is called data toggling, SAM3X handles it automatically, it knows when to start from DATA0 or DATA1 and keeps track of toggling. Two same DATA0 or DATA1 packets in a row means error, probable loss of packets.

Once DATA stage is finished, host starts HANDSHAKE stage. The rule is following: host must issue empty IN or OUT transaction in opposite direction regarding DATA stage. In our case DATA stage was IN so I issue empty (no data in DATA1 packet) OUT. Note that DATA1 packet is used here according to USB specification.

Packets format discussion

Each of above packets is split further according to USB specification. SETUP, IN and OUT have following format:

Image 2

All PIDs are specified in table 8-1 of [2, p.196]. So SETUP is 0b1101, OUT is 0b0001, etc. CRC is Cyclic Redundancy Check and is calculated by SAM3X automatically. ADDR and ENDP are device address and endpoint number which are zero in our case. DATA0/1 packets have following format:

Image 3

For our control transfer DATA is limited to 64 bytes. PID is 0b0011 for DATA0 and 0b1011 for DATA1. CRC is calculated by SAM3X automatically.

Ack packet consists of just one 8-bits field with PID 0b0010.

Note that all PIDs are 8 bits fields, first 4 bits are PID, second 4 bits are one complement of first 4 bits for error check as CRC does not cover PID fields.

Then final control transfer looks like this:

Image 4I marked yellow those blocks of data that we must explicitly populate ourselves each time with new transaction.

On this picture you can see what exactly goes through physical wire. One last thing to note here is that USB works in LSB order which means that least significant bit goes first. This is good for us as ARM processors are also use LSB order.

SETUP stage DATA0 packet

SETUP stage DATA0 packet's data (marked yellow) will have our request information, namely a standard request to get a standard USB device descriptor. Such request has a defined structure:

Image 5

Let's define standard values that will allow us to populate request structure. For that add new file USB_Specification.h to project. First field bmRequestType consists of three pieces of information that occupy different bitfields. Those are direction, request type and request recipient:

//USB request data transfer direction (bmRequestType)
#define  USB_REQ_DIR_OUT         (0<<7) //Host to device
#define  USB_REQ_DIR_IN          (1<<7) //Device to host

//USB request types (bmRequestType)
#define  USB_REQ_TYPE_STANDARD   (0<<5) //Standard request
#define  USB_REQ_TYPE_CLASS      (1<<5) //Class-specific request
#define  USB_REQ_TYPE_VENDOR     (2<<5) //Vendor-specific request

//USB recipient codes (bmRequestType)
#define  USB_REQ_RECIP_DEVICE    (0<<0) //Recipient device
#define  USB_REQ_RECIP_INTERFACE (1<<0) //Recipient interface
#define  USB_REQ_RECIP_ENDPOINT  (2<<0) //Recipient endpoint
#define  USB_REQ_RECIP_OTHER     (3<<0) //Recipient other

In this article direction will be from device to host as I want to get information out of a device. Type will be standard, later I'll use also class-specific request (UVC requests) and will never use vendor-specific requests. Recipient will be a device. Later when I dig dipper into video function I'll use interfaces and endpoints.

Values in table 9-4 of [2, p.251] are used to populate bRequest field:

C++
//Standard USB requests (bRequest)
#define  USB_REQ_GET_STATUS         0
#define  USB_REQ_CLEAR_FEATURE      1
#define  USB_REQ_SET_FEATURE        3
#define  USB_REQ_SET_ADDRESS        5
#define  USB_REQ_GET_DESCRIPTOR     6
#define  USB_REQ_SET_DESCRIPTOR     7
#define  USB_REQ_GET_CONFIGURATION  8
#define  USB_REQ_SET_CONFIGURATION  9
#define  USB_REQ_GET_INTERFACE      10
#define  USB_REQ_SET_INTERFACE      11
#define  USB_REQ_SYNCH_FRAME        12

In this article I'll use USB_REQ_GET_DESCRIPTOR to obtain standard USB device descriptor.

wValue field's value depends on what was specified in two upper fields. In this article case this field will contain descriptor type and descriptor index. Index is always zero for device descriptors and types are:

C++
#define  USB_DT_DEVICE                     1
#define  USB_DT_CONFIGURATION              2
#define  USB_DT_STRING                     3
#define  USB_DT_INTERFACE                  4
#define  USB_DT_ENDPOINT                   5
#define  USB_DT_DEVICE_QUALIFIER           6
#define  USB_DT_OTHER_SPEED_CONFIGURATION  7
#define  USB_DT_INTERFACE_POWER            8
#define  USB_DT_OTG                        9
#define  USB_DT_IAD                        0x0B
#define  USB_DT_BOS                        0x0F
#define  USB_DT_DEVICE_CAPABILITY          0x10

I will use USB_DT_DEVICE for standard USB device descriptor. This value goes into upper byte of wValue field, Index goes into lower byte.

wIndex field's value is always zero (it is used to get human readable string descriptions in various languages).

wLength specifies how many bytes to return. Standard USB device descriptor is 18 bytes.

Now as all necessary data is defined for standard request, let's define the request itself:

C++
//SETUP request
typedef struct
{
    uint8_t bmRequestType;
    uint8_t bRequest;
    uint16_t wValue;
    uint16_t wIndex;
    uint16_t wLength;
} usb_setup_req_t;

DATA stage DATA0 packet - device descriptor

We've understood SETUP stage. In the next DATA stage we'll get response from a device. Yellow block will be filled with a standard device descriptor.

Device descriptor gives general information about whole device and there is only one such type descriptor. Following table has short field explanations:

Image 6

Let's define this descriptor:

C++
//Standard USB device descriptor structure
typedef struct
{
    uint8_t bLength;
    uint8_t bDescriptorType;
    uint16_t bcdUSB;
    uint8_t bDeviceClass;
    uint8_t bDeviceSubClass;
    uint8_t bDeviceProtocol;
    uint8_t bMaxPacketSize0;
    uint16_t idVendor;
    uint16_t idProduct;
    uint16_t bcdDevice;
    uint8_t iManufacturer;
    uint8_t iProduct;
    uint8_t iSerialNumber;
    uint8_t bNumConfigurations;
} usb_dev_desc_t;

Writing HCD

At this point I've defined all necessary data structures so it's time to write some code. There are several problems to be addressed:

  • SAM3X's USB module needs extra initialization. I've only made general initialization, but for USB communication a (control) pipe must be established thus a pipe initialization is required.
  • Pipe communication will raise interrupts and because there is only one interrupt handler (one entry point), a code to determine interrupt's pipe number is needed.
  • As discussed above, a control transfer has well-defined sequence. There are stages, transactions and packets in a transfer. So the host must be aware of exactly at which point the transfer is at any time. In other words it must know what the current stage is, which transaction within the stage and which packet within the transaction. To solve this I'll create additional control structure inside UHC.c file that will have all necessary fields to manage a control transfer.
  • When USB is in HS (high speed) mode, everything should happen inside microframes that are delimited by SOF (start of frame) packets. Those are generated by SAM3X automatically when USB bus is active (not suspended). For that reason we should know when SOFs occur. This also allows us to create delays that give a connected device some time to process requests. Microframes occur every 125us which means every 8 SOFs make 1ms. I'll use this to specify delays.

After above problems are solved there will be all necessary infrastructure to write interrupt driven transfer sequences.

Once descriptor is received I'll print it using previously written print functions. The code will be split into two groups of files: to HCD.h/.c (low level) and to USBD.h/.c (high level). High level functions will give requests and received buffer pointers to received descriptors. Low level will receive requests, go through all stages, transactions, packets until it gets (of fails) requested data and will indicate the high level about the completion of a transfer. Function pointers will be used to accomplish that.

Pipe initialization

There 10 pipes available in SAM3X's UOTGHS module, logically I'll take pipe number 0 to be a control pipe (in fact pipe 0 can only be a control pipe according to SAM3X specification [1, p.1069]). Image 7

Pipe initialization consists of pipe activation, address setup and pipe interrupts enabling.

Pipe activation is described on [1, p.1098] with nice graphical algorithm. Firstly a pipe needs to be enabled then its parameters specified. Interrupt request frequency does not matter in a control pipe as it is not an interrupt pipe. Endpoint number will always be 0, type is control pipe, token is SETUP, size is 64 (this is max. size of SAM3X's pipe 0, see [1, p.1069]. Value 3 corresponds to 64 bytes, see [1, p.1181]), number of banks will be 1 (for video stream I'll use two, one is getting data while another is being read then they switch over).

If all above parameters are specified correctly and pipe can be activated, bit CFGOK (configuration OK) of HSTPIPISR0 (host pipe & interrupt status register zero) is set to 1. If activation fails I print error message to RS-232 port and disable pipe.

As pipe is a combination of USB device address and endpoint number, a pipe's USB device address needs to be specified as well. Pipe 0 USB device address is located in register HSTADDR1 bits [0-6].

Last step is to enable pipe interrupts. In initialization function I'll enable STALL interrupt. STALL is a kind of handshake. So far we are familiar with ACK which means "everything is OK", STALL on the other hand means that request cannot be understood by device. For example if we screw something up with request fields values, we'll get STALL token instead of ACK. I'll also enable pipe error interrupt that is fired if something is wrong with pipe's data and then I'll enable global pipe 0 interrupt to make STALL and error interrupts work.

Pipe initialization is packed into one function with one parameter: USB device address. This is because device address is changed during enumeration process and thus I will have to readjust it once device address is set.

C++
uint32_t HCD_InitiatePipeZero(uint8_t Address)
{
    UOTGHS->UOTGHS_HSTPIP |= UOTGHS_HSTPIP_PEN0;                //Enable pipe 0
    
    //Pipe activation:
    //Single-bank pipe
    UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PBK_1_BANK & UOTGHS_HSTPIPCFG_PBK_Msk);
    //64 bytes pipe (3 corresponds to 64 bytes)
    UOTGHS->UOTGHS_HSTPIPCFG[0] |= ((3 << UOTGHS_HSTPIPCFG_PSIZE_Pos) & UOTGHS_HSTPIPCFG_PSIZE_Msk);
    //Pipe token is SETUP
    UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PTOKEN_SETUP & UOTGHS_HSTPIPCFG_PTOKEN_Msk);    
    //Pipe type of Pipe 0 is always CONTROL    
    UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PTYPE_CTRL & UOTGHS_HSTPIPCFG_PTYPE_Msk);    
    //Pipe endpoint number is zero        
    UOTGHS->UOTGHS_HSTPIPCFG[0] |= ((0 << UOTGHS_HSTPIPCFG_PEPNUM_Pos) & UOTGHS_HSTPIPCFG_PEPNUM_Msk);
    //Pipe bank autoswitch is off for control pipe
    UOTGHS->UOTGHS_HSTPIPCFG[0] &= ~UOTGHS_HSTPIPCFG_AUTOSW;
    //Memory allocation                                            
    UOTGHS->UOTGHS_HSTPIPCFG[0] |= UOTGHS_HSTPIPCFG_ALLOC;
    
    //Check if pipe was allocated OK
    if(0 == (UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_CFGOK))
    {
        PrintStr("Pipe 0 has not been activated.\r\n");
        UOTGHS->UOTGHS_HSTPIP &= ~UOTGHS_HSTPIP_PEN0;          //Disable pipe 0
        return 0;
    }
    
    //Pipe's USB device address
    UOTGHS->UOTGHS_HSTADDR1 = (UOTGHS->UOTGHS_HSTADDR1 & ~UOTGHS_HSTADDR1_HSTADDRP0_Msk) 
                              | (Address & UOTGHS_HSTADDR1_HSTADDRP0_Msk);
    
    //Enables stall interrupt on pipe 0
    UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_RXSTALLDES; 
    //Enables pipe 0 data bank error interrupts
    UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_PERRES;
    //Enables global pipe 0 interrupts
    UOTGHS->UOTGHS_HSTIER = UOTGHS_HSTIER_PEP_0;               
    
    PrintStr("Pipe 0 has been initialized successfully.\r\n");
    return 1;
}

Add function declaration to top of HCD.c file, it will not be used from the outside:

C++
#include "HCD.h"
#include "UART.h"

uint32_t HCD_InitiatePipeZero(uint8_t Address);

//...

Pipe 0 interrupts

To see which pipe interrupt fired, bits PEP_0, PEP_1, ... PEP_9 of the register UOTGHS_HSTISR (host global interrupt status register) should be checked. I'll do it using if statement, also I know that I will use pipe 0 (control) and pipe 1 (streaming) only in this application, so I'll provide code for two pipes only and pipe 1 will be first to test as streaming is time critical in this low processing power (relative to video streaming task) microcontroller.

C++
uint8_t HCD_GetPipeInterruptNumber(void)
{
    if(UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_PEP_1)
        return 1;
    if(UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_PEP_0)
        return 0;

    PrintStr("Unsupported interrupt number.\r\n");
    return 100;
}

Put HCD_GetPipeInterruptNumber declaration under HCD_InitiatePipeZero declaration, this one is also not used from the outside.

Now let's review UOTGHS interrupt handler (UOTGHS_Handler). So far it distinguishes device connection, disconnection, bus power change, bus power error and SOF interrupts. Handler is already long and for better readability I'll put pipe 0 interrupts handling in separate function HCD_HandleControlPipeInterrupt and call it after pipe number 0 interrupt happened:

C++
#include "HCD.h"
#include "UART.h"

uint32_t HCD_InitiatePipeZero(uint8_t Address);
void HCD_HandleControlPipeInterrupt(void);

//...


void UOTGHS_Handler()
{
    //Manage SOF interrupt
    if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_HSOFI))
    {
        UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_HSOFIC;                 //Ack SOF
        return;
    }
    
    //Getting pipe interrupt number
    uint8_t PipeInterruptNumber = HCD_GetPipeInterruptNumber();
    if(PipeInterruptNumber == 0)
    {
        HCD_HandleControlPipeInterrupt();                             //Pipe 0 interrupt
        return;
    }

//...


void HCD_HandleControlPipeInterrupt(void)
{
    //STALL interrupt
    if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_RXSTALLDI)
    {
        UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXSTALLDIC;    //Ack STALL interrupt
        return;
    }
    
    //Pipe error interrupt
    if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_PERRI)
    {
        //Getting error
        uint32_t error = UOTGHS->UOTGHS_HSTPIPERR[0] &
                        (UOTGHS_HSTPIPERR_DATATGL | UOTGHS_HSTPIPERR_TIMEOUT
                        | UOTGHS_HSTPIPERR_PID | UOTGHS_HSTPIPERR_DATAPID);
        UOTGHS->UOTGHS_HSTPIPERR[0] = 0UL;                            //Ack all errors
        
        switch(error)
        {
            case UOTGHS_HSTPIPERR_DATATGL:
                PrintStr("UOTGHS_HSTPIPERR_DATATGL\r\n");
                break;
            case UOTGHS_HSTPIPERR_TIMEOUT:
                PrintStr("UOTGHS_HSTPIPERR_TIMEOUT\r\n");
                break;
            case UOTGHS_HSTPIPERR_DATAPID:
                PrintStr("UOTGHS_HSTPIPERR_DATAPID\r\n");
                break;
            case UOTGHS_HSTPIPERR_PID:
                PrintStr("UOTGHS_HSTPIPERR_PID\r\n");
                break;
            default:
                PrintStr("UHD_TRANS_PIDFAILURE\r\n");
        }
    }
    
    PrintStr("Uncaught Pipe 0 interrupt.\r\n");
}

Top section shows declarations that are located inside .c file. Middle section shows previously created global UOTGHS interrupt handler. I moved SOF interrupt to top as it will be the most happening interrupt and put pipe interrupt number determination and call to HCD_HandleControlPipeInterrupt right under it. The rest of the handler is not shown for clarity. Last section is the control pipe interrupt handling function. So far it handles STALL and some errors on pipe 0. I put switch statement with possible errors, however I did not try to understand what those mean. For now their happening is important, not their meaning as if everything is done the right way - there should be no error interrupts at all.

Control structure

Control structure will track control transfer stage in accordance with direction:

C++
//Definitions to store control transfer state machine
typedef enum
{
    USB_CTRL_REQ_STAGE_SETUP = 0,
    USB_CTRL_REQ_STAGE_DATA_OUT = 1,
    USB_CTRL_REQ_STAGE_DATA_IN = 2,
    USB_CTRL_REQ_STAGE_ZLP_IN = 3,
    USB_CTRL_REQ_STAGE_ZLP_OUT = 4,
} hcd_ControlRequestStageType;

SETUP, DATA_IN and ZLP_OUT are used in this article. ZLP means zero length packet, thus ZLP_IN and ZLP_OUT belong to HANDSHAKE stage. DATA_OUT and ZLP_IN will be used in the next articles. 

Besides above enum the control structure will store SOF count to extract every 1ms, a direction information (IN or OUT), receive/send buffer pointer, total bytes received/sent, receive/send buffer index to remember position in transfers with many transactions during data stage, pause in milliseconds for delayed function calls and three function pointers: function to be called after USB device reset, function to be called at the beginning of a control transfer (after pause has elapsed) and one to be called after control transfer is completed:

C++
typedef struct
{
    uint8_t SOFCount;                           //To count every 1ms
    
    uint8_t Direction;
    hcd_ControlRequestStageType ControlRequestStage;    //Current stage of control transaction
    
    uint8_t *ptrBuffer;                         //Receive/send buffer
    uint8_t ByteCount;                          //Receive/send data size
    uint16_t Index;                             //Current position in *ptrBuffer
    
    uint16_t Pause;                             //Over how many ms to call TransferStart or ResetEnd    
    void (*ResetEnd)(void);                     //Called after reset happened
    void (*TransferStart)(void);                //Called after Pause elapsed
    void (*TransferEnd)(uint16_t);              //Called when transfer is finished
} hcd_ControlStructureType;

Both structures are specified in HCD.h file, declaration of varible of hcd_ControlStructureType type is in HCD.c file:

C++
#include "HCD.h"
#include "UART.h"

hcd_ControlStructureType hcd_ControlStructure;

//...

Please note usage of function pointers, read about them should you don't know what they are.

SOF handler

SOF handler will count delay. Once delay has elapsed TransferStart function is called if assigned. Delay is measured in milliseconds. There will also be a possibility to initiate transfer immediately bypassing SOF handler. Let's create it:

C++
void HCD_HandleSOFInterrupt(void)
{
    hcd_ControlStructure.SOFCount++;          //This happens every 0.125ms
    if(8 == hcd_ControlStructure.SOFCount)
    {
        hcd_ControlStructure.SOFCount = 0;    //This happens every 1ms
        if(0 < hcd_ControlStructure.Pause)    //If pause was specified
        {
            hcd_ControlStructure.Pause--;
            if(0 == hcd_ControlStructure.Pause)
            {
                if(0 != hcd_ControlStructure.TransferStart)
                {
                    (*hcd_ControlStructure.TransferStart)();
                    return;
                }
            }
        }
    }
}

Add declaration of this function to HCD.c file as it is not used from the outside.

USB device enumeration process after reset must start in not less than 100ms according [2, p.243]. Let's modify global USB interrupt handler to include SOF interrupt handler, minimum delay and function for enumeration start:

C++
void UOTGHS_Handler()
{
    //Manage SOF interrupt
    if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_HSOFI))
    {
        UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_HSOFIC;           //Ack SOF
        HCD_HandleSOFInterrupt();
        return;
    }
    
    //...

    
    //USB bus reset detection
    if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_RSTI))
    {
        UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_RSTIC;            //Ack reset
        PrintStr("Reset performed.\r\n");
        
        hcd_ControlStructure.Pause = 100;                       //Pause before enumeration start
        hcd_ControlStructure.TransferStart = hcd_ControlStructure.ResetEnd;
        return;
    }
    
    PrintStr("Unmanaged Interrupt.\n\r");                        //If I forgot to handle something
}

Once USB device is connected and reset, 100ms delay and transfer start function are specified (this happens every time a USB device is plugged in); SOF interrupt handler starts counting delay down, when 0 is reached TransferStart function is called. This function belongs to USBD (USB driver) and it starts enumeration process by requesting first descriptor - standard device descriptor. I'll write this function a bit later in this article.

Interrupt driven transfer

Interrupt driven transfer means that once started it triggers various interrupts that in turn carry on calling transfer functions which trigger interrupts again. This happens until transfer with all its stages, transactions and packets is completed.

A sequence starts with a call to a function that sends SETUP packet (SETUP stage, SETUP packet). SAM3X sends DATA0 packet and receives ACK from a device itself without our intervention, we just need to point it where DATA0 packet's data is. Once SETUP packet (with DATA0) is sent and ACK is recieved, SAM3X raises "Transmitted SETUP" interrupt that leads us to HCD_HandleControlPipeInterrupt function. Inside this function I determine with help of control structure that direction is IN, thus I send IN packet (DATA stage, IN packet). Once IN packet is sent, "Received IN Data" interrupt is fired that leads us again to HCD_HandleControlPipeInterrupt function. When I acknowledge it, SAM3X sends ACK packet (last gray rectangle in DATA stage). At this moment buffer data is read then I move on to HANDSHAKE stage by calling function that sends empty OUT transaction (it is empty simply because I don't fill up pipe buffer with any data). Once empty OUT packet is sent and SAM3X got ACK from device, "Transmitted OUT Data" interrupt is fired that again leads us to HCD_HandleControlPipeInterrupt function. With help of control structure I determine that this is the end of a control transfer and it's time to call TransferEnd function.

Before I write above mentioned functions and change control pipe interrupt handler, let me show call sequence from previous paragraph graphically with proper function names:

Image 8

Sending Setup packet with data

This is the first function in the sequence. It fills up control structure and pipe 0 memory bank with data that is sent in DATA0 packet of SETUP stage, enables "Transmitted SETUP" interrupt which is called once all SETUP stage packets are sent and ACK from a device is received. Lastly, it starts transmission and exits.

C++
void HCD_SendCtrlSetupPacket(uint8_t EndpAddress, uint8_t Direction, usb_setup_req_t SetupPacketData, 
                             uint8_t *ptrBuffer, uint16_t TransferByteCount, 
                             void (*TransferEnd)(uint16_t ByteReceived))
{
    //Setting up control structure, initial stage is SETUP
    hcd_ControlStructure.ByteCount = TransferByteCount;
    hcd_ControlStructure.Direction = Direction;
    hcd_ControlStructure.ptrBuffer = ptrBuffer;
    hcd_ControlStructure.Index = 0;
    hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_SETUP;
    hcd_ControlStructure.TransferEnd = TransferEnd;
    
    //Token is SETUP
    UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk) 
                                | (UOTGHS_HSTPIPCFG_PTOKEN_SETUP & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
    
    //Getting pointer to memory bank of pipe 0
    volatile uint64_t *ptrRequestPayload = 
                           (volatile uint64_t*)&(((volatile uint64_t(*)[0x8000])UOTGHS_RAM_ADDR)[0]);
    
    //Loading it with data    
    *ptrRequestPayload = *((uint64_t*)&SetupPacketData);                            
    
    PrintStr("Sending SETUP packet + DATA0.\r\n");
    //Transmitted SETUP interrupt enable
    UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_TXSTPES;    
    //Ack that SETUP packet is in pipe and send if not frozen
    UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_FIFOCONC;
    //Send data if pipe was frozen due to error, reset or STALL    
    UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_PFREEZEC;
}

Note that pointer to the function that is called immediately after transfer completion is specified here as a parameter void (*TransferEnd)(uint16_t ByteReceived)).

Request data size is 8 bytes (64 bits) and there are 8, 16, 32 and 64-bits accesses to pipe's memory bank thus it is convenient to cast usb_setup_req_t to uint64_t to be able to copy it easily.

Interrupt (first time)

HCD_SendCtrlSetupPacket will trigger interrupt on the control pipe after SETUP packet, DATA0 packet are sent and ACK packet from a device is received. Interrupt will come to HCD_HandleControlPipeInterrupt function thus it needs to be change to be able to serve the interrupt. The interrupt will be "Transmitted SETUP interrupt" (bit UOTGHS_HSTPIPISR_TXSTPI of UOTGHS_HSTPIPISR[0] register). Code checks stage and direction and if it is SETUP and IN, it changes stage to DATA_IN and calls function that sends IN packet - HCD_SendCtrlInPacket. This starts DATA stage of the transfer.

C++
//...

//Transmitted SETUP interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_TXSTPI)                
{
    UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_PFREEZES;    //Freezes pipe 0
    UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXSTPIC;     //Ack interrupt    
      
    //Start DATA stage    
    if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_SETUP)
    {
        if(hcd_ControlStructure.Direction == USB_REQ_DIR_IN)    //Now need to send IN packet
        {
            //change to IN phase
            hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_DATA_IN;    
            HCD_SendCtrlInPacket();
            return;
        }
    }
    return;
}

//...

Sending DATA stage IN packet

This is simple function, it only sends IN token and enables "Received IN Data" interrupt.

C++
void HCD_SendCtrlInPacket(void)
{
    //Reconfigure to IN token
    UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk) 
                                | (UOTGHS_HSTPIPCFG_PTOKEN_IN & UOTGHS_HSTPIPCFG_PTOKEN_Msk);    
    //Clear Received IN Data interrupt to be able to receive it when IN data came
    UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXINIC;
    //Enable IN Received interrupt
    UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_RXINES;    
    //Free buffer
    UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_FIFOCONC;                    
    UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_PFREEZEC;
}

Note that last to rows (...FIFOCONC and ...PFREEZEC) are the same for all sendings.

Interrupt (second time)

Sending IN packet will trigger interrupt where data from a device should be received. Interrupt handler HCD_HandleControlPipeInterrupt should understand that:

C++
//...

//Received IN Data interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_RXINI)    
{
    UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXINIC;        //Ack interrupt    
      
    //Data stage: IN token has been send and DATA0/1 came in    
    if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_DATA_IN)
    {
        HCD_GetCtrlInDataPacket();                                //Getting data
        return;
    }        
    return;
}

//...   

This part of interrupt function checks if stage is DATA_IN. If yes it calls HCD_GetCtrlInDataPacket which actually received the data.

Receiving DATA stage data

This function gets 8-bits access to pipe 0 memory bank, reads it byte-by-byte and copies it to control structure's buffer. ByteReceived will hold quantity of bytes returned in current transaction. Total quantity must be equal to value, specified during SETUP stage. So if ByteReceived equals to that value, it means everything is received and function must return.

Another condition to return is a Short packet. If there are many transactions during DATA stage, each DATA stage data packet must have the same size except the last one thus short packet always means last packet whether everything is OK or there are some problems with data.

Once all data is received, it specifies ZLP_OUT stage (making start of HANDSHAKE stage) and calls function that actually send OUT packet with empty DATA1 packet - HCD_SentCtrlOutEmptyPacket.

C++
void HCD_GetCtrlInDataPacket(void)
{
    uint32_t IsReceivedPacketShort = UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_SHORTPACKETI;
    uint8_t ByteReceived = UOTGHS->UOTGHS_HSTPIPISR[0] >> UOTGHS_HSTPIPISR_PBYCT_Pos;
    
    volatile uint8_t *ptrReceiveBuffer = 
                               (uint8_t *)&(((volatile uint8_t(*)[0x8000])UOTGHS_RAM_ADDR)[0]);
    
    //Copying bytes over from FIFO buffer
    while(ByteReceived)
    {
        hcd_ControlStructure.ptrBuffer[hcd_ControlStructure.Index] = *ptrReceiveBuffer++;
        hcd_ControlStructure.Index ++;
        ByteReceived--;
    }
    
    //If following is true, must move to HANDSHAKE stage
    if((hcd_ControlStructure.Index == hcd_ControlStructure.ByteCount) || IsReceivedPacketShort)
    {
        hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_ZLP_OUT;
        HCD_SentCtrlOutEmptyPacket();
        return;
    }
    
    //Next IN transaction should be initiated here.
}

Note the comment at the bottom: I'll return to this function later when all data does not fit into one transaction.

Sending HANDSHAKE OUT packet with empty DATA1 packet

It just specifies proper token (OUT) and enables "Received OUT Data" interrupt.

C++
void HCD_SentCtrlOutEmptyPacket(void)
{
    //Changing pipe token to OUT
    UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk) 
                                | (UOTGHS_HSTPIPCFG_PTOKEN_OUT & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
    //Clean OUT interrupt flag if it is set
    UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXOUTIC;
    //Enables OUT interrupt
    UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_TXOUTES;
    //Sends FIFO buffer data (which is none in this case)
    UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_FIFOCONC;
    //Enable pipe request generation
    UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_PFREEZEC;
}

Note that SAM3X understands that DATA1 should be send without our intervention, don't even need to specify it anywhere.

Interrupt (third time)

Once OUT packet and empty DATA1 packet is sent and ACK packet is received from a device, it brings us again to HCD_HandleControlPipeInterrupt function for the third time (and the last in this transfer). Here it checks the stage and if it is ZLP_OUT, the TransferEnd function is called (remember, it was specified during the call of HCD_SendCtrlSetupPacket function).

C++
//...

//Received OUT Data interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_TXOUTI)
{
    UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXOUTIC;        //Ack interrupt
      
    //HANDSHAKE stage, ACK for empty OUT is received
    if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_ZLP_OUT)
    {
        //Notifies about transaction completion
        (*hcd_ControlStructure.TransferEnd)(hcd_ControlStructure.Index);    
    }
    return;
}

//...

HCD last touch

So far HCD part is almost completed for this article and can process receiving control transfers that have return data size less of equal to pipe 0 memory bank size (64 bytes) - which means the transfers with one transaction in DATA stage.

The only thing is missing is how ResetEnd function pointer of control structure is specified. This pointer is copied to TransferStart pointer immediately after device reset is performed. I'll create a function for this:

C++
void HCD_SetEnumerationStartFunction(void (*ResetEnd)(void))
{
    hcd_ControlStructure.ResetEnd = ResetEnd;
}

and call it in main file right before call to HCD_Init. This function's declaration must be in HCD.h file as it is called from the outside.

Writing USBD

USBD (USB driver) functions will initiate transfers and get returned data. They'll work in pairs, one function is ...Begin, another is ...End - the first forms the request structure with appropriate command and the second gets results.

Getting standard device descriptor

First is ...Begin function. It forms the standard request structure: transfer is to receive data (USB_REQ_DIR_IN), this request is standard request (USB_REQ_TYPE_STANDARD) as opposed to class or vendor specific, this request is directed to a device (USB_REQ_RECIP_DEVICE) as opposed to device's interface or interface's endpoint, this request gets descriptor (USB_REQ_GET_DESCRIPTOR) and not any descriptor, but device one (USB_DT_DEVICE).

wIndex is not used in this request. wLength is 18, this is how many bytes should be returned and is the size of standard device descriptor usb_dev_desc_t.

As it is the first time it deals with HCD, a control pipe must be initialized - HCD_InitiatePipeZero.

Then HCD_SendCtrlSetupPacket is called which initiates interrupt driven control transfer described in previous section.

C++
void USBD_GetDeviceDescriptorBegin(void)
{
    usb_setup_req_t ControlRequest;
    
    ControlRequest.bmRequestType = USB_REQ_DIR_IN | USB_REQ_TYPE_STANDARD | USB_REQ_RECIP_DEVICE;
    ControlRequest.bRequest = USB_REQ_GET_DESCRIPTOR;
    ControlRequest.wValue = (USB_DT_DEVICE << 8);
    ControlRequest.wIndex = 0;
    ControlRequest.wLength = 18;
    
    if(HCD_InitiatePipeZero(0))
    {
        HCD_SendCtrlSetupPacket(0, USB_REQ_DIR_IN, ControlRequest, Buffer, ControlRequest.wLength, 
                                                                         USBD_GetDeviceDescriptorEnd);
    }    
}

Buffer is specified in USBD.h file:

uint8_t Buffer[1024];

Once HCD processed the transfer, it calls USBD_GetDeviceDescriptorEnd function:

C++
void USBD_GetDeviceDescriptorEnd(uint16_t ByteReceived)
{
    PrintStr("Standard device descriptor size is ");
    PrintDEC(ByteReceived);
    PrintStr(".\r\n");
    
    PrintDeviceDescriptor(Buffer);
}

Returned data is in Buffer, I pass its pointer to printing function PrintDeviceDescriptor which is:

C++
void PrintDeviceDescriptor(uint8_t* ptrBuffer)
{
    usb_dev_desc_t dev_desc;
    memcpy(&dev_desc, ptrBuffer, 18);    
    
    PrintStr("\r\n------Standard Device Descriptor------\r\n");
    PrintStr("bLength: \t\t");
    PrintDEC(dev_desc.bLength);
    PrintStr("\r\n");
    PrintStr("bDescriptorType: \t");
    PrintDEC(dev_desc.bDescriptorType);
    PrintStr("\r\n");
    PrintStr("bcdUSB: \t\t");
    PrintHEX16(dev_desc.bcdUSB);
    PrintStr("\r\n");
    PrintStr("bDeviceClass: \t");
    PrintHEX((uint8_t*)(&dev_desc.bDeviceClass), 1);
    PrintStr("\r\n");
    PrintStr("bDeviceSubClass: \t");
    PrintDEC(dev_desc.bDeviceSubClass);
    PrintStr("\r\n");
    PrintStr("bDeviceProtocol: \t");
    PrintDEC(dev_desc.bDeviceProtocol);
    PrintStr("\r\n");
    PrintStr("bMazPacketSize0: \t");
    PrintDEC(dev_desc.bMaxPacketSize0);
    PrintStr("\r\n");
    PrintStr("idVendor: \t\t");
    PrintHEX16(dev_desc.idVendor);
    PrintStr("\r\n");
    PrintStr("idProduct: \t\t");
    PrintHEX16(dev_desc.idProduct);
    PrintStr("\r\n");
    PrintStr("bcdDevice: \t\t");
    PrintHEX16(dev_desc.bcdDevice);
    PrintStr("\r\n");
    PrintStr("iManufacturer: \t");
    PrintDEC(dev_desc.iManufacturer);
    PrintStr("\r\n");
    PrintStr("iProduct: \t\t");
    PrintDEC(dev_desc.iProduct);
    PrintStr("\r\n");
    PrintStr("iSerialNumber: \t");
    PrintDEC(dev_desc.iSerialNumber);
    PrintStr("\r\n");
    PrintStr("bNumConfigurations:\t");
    PrintDEC(dev_desc.bNumConfigurations);
    PrintStr("\r\n-------------------------------------\r\n");
}

Let's build the code, upload it and look at the output (your values should be a bit different):

Image 9

My camera complies with USB 2.0 (bcbUSB), its class is Multi-interface Function (bDeviceClass, bDeviceSubClass and bDeviceProtocol have combination 0xEF, 2, 1) which means that my camera has several (as opposed to one) video interfaces that are grouped with help of Interface Association Descriptor (which I'll discuss in the next article). I can communicate with control endpoint using packets not bigger than 64 bytes (bMaxPacketSize0). idVendor and idProduct are assigned by USB forum (USB legal body name) when you pay them money, those numbers are unique and can be seen in Device manager together with device version (bcdDevice) - equipment identifier property: 

Image 10

iManufacturer, iProduct and iSerialNumber are indexes to read string (human readable) descriptions which I do not do. And the most important is the number of configurations (bNumConfigurations) that is 1, which means that my camera has only one configuration with index 0 (zero) and I'll use 0 (zero) as a parameter in the next article to read a configuration descriptors.

Conclusion

This article contains the minimum basics of USB communication in Arduino Due. One transfer is described in full details using interrupt driven technique. Next article with focus on getting all the rest of descriptors and building some kind of a device map.

Source code is here.

Part 6 is here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Ukraine Ukraine
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --