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.
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:
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:
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:
I 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:
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:
#define USB_REQ_DIR_OUT (0<<7) //Host to device
#define USB_REQ_DIR_IN (1<<7) //Device to host
#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
#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:
#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:
#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:
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:
Let's define this descriptor:
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]).
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.
uint32_t HCD_InitiatePipeZero(uint8_t Address)
{
UOTGHS->UOTGHS_HSTPIP |= UOTGHS_HSTPIP_PEN0;
UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PBK_1_BANK & UOTGHS_HSTPIPCFG_PBK_Msk);
UOTGHS->UOTGHS_HSTPIPCFG[0] |= ((3 << UOTGHS_HSTPIPCFG_PSIZE_Pos) & UOTGHS_HSTPIPCFG_PSIZE_Msk);
UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PTOKEN_SETUP & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
UOTGHS->UOTGHS_HSTPIPCFG[0] |= (UOTGHS_HSTPIPCFG_PTYPE_CTRL & UOTGHS_HSTPIPCFG_PTYPE_Msk);
UOTGHS->UOTGHS_HSTPIPCFG[0] |= ((0 << UOTGHS_HSTPIPCFG_PEPNUM_Pos) & UOTGHS_HSTPIPCFG_PEPNUM_Msk);
UOTGHS->UOTGHS_HSTPIPCFG[0] &= ~UOTGHS_HSTPIPCFG_AUTOSW;
UOTGHS->UOTGHS_HSTPIPCFG[0] |= UOTGHS_HSTPIPCFG_ALLOC;
if(0 == (UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_CFGOK))
{
PrintStr("Pipe 0 has not been activated.\r\n");
UOTGHS->UOTGHS_HSTPIP &= ~UOTGHS_HSTPIP_PEN0; return 0;
}
UOTGHS->UOTGHS_HSTADDR1 = (UOTGHS->UOTGHS_HSTADDR1 & ~UOTGHS_HSTADDR1_HSTADDRP0_Msk)
| (Address & UOTGHS_HSTADDR1_HSTADDRP0_Msk);
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_RXSTALLDES;
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_PERRES;
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:
#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.
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:
#include "HCD.h"
#include "UART.h"
uint32_t HCD_InitiatePipeZero(uint8_t Address);
void HCD_HandleControlPipeInterrupt(void);
void UOTGHS_Handler()
{
if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_HSOFI))
{
UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_HSOFIC; return;
}
uint8_t PipeInterruptNumber = HCD_GetPipeInterruptNumber();
if(PipeInterruptNumber == 0)
{
HCD_HandleControlPipeInterrupt(); return;
}
void HCD_HandleControlPipeInterrupt(void)
{
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_RXSTALLDI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXSTALLDIC; return;
}
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_PERRI)
{
uint32_t error = UOTGHS->UOTGHS_HSTPIPERR[0] &
(UOTGHS_HSTPIPERR_DATATGL | UOTGHS_HSTPIPERR_TIMEOUT
| UOTGHS_HSTPIPERR_PID | UOTGHS_HSTPIPERR_DATAPID);
UOTGHS->UOTGHS_HSTPIPERR[0] = 0UL;
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:
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:
typedef struct
{
uint8_t SOFCount;
uint8_t Direction;
hcd_ControlRequestStageType ControlRequestStage;
uint8_t *ptrBuffer; uint8_t ByteCount; uint16_t Index;
uint16_t Pause; void (*ResetEnd)(void); void (*TransferStart)(void); void (*TransferEnd)(uint16_t); } hcd_ControlStructureType;
Both structures are specified in HCD.h file, declaration of varible of hcd_ControlStructureType type is in HCD.c file:
#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:
void HCD_HandleSOFInterrupt(void)
{
hcd_ControlStructure.SOFCount++; if(8 == hcd_ControlStructure.SOFCount)
{
hcd_ControlStructure.SOFCount = 0; if(0 < hcd_ControlStructure.Pause) {
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:
void UOTGHS_Handler()
{
if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_HSOFI))
{
UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_HSOFIC; HCD_HandleSOFInterrupt();
return;
}
if (0 != (UOTGHS->UOTGHS_HSTISR & UOTGHS_HSTISR_RSTI))
{
UOTGHS->UOTGHS_HSTICR = UOTGHS_HSTICR_RSTIC; PrintStr("Reset performed.\r\n");
hcd_ControlStructure.Pause = 100; hcd_ControlStructure.TransferStart = hcd_ControlStructure.ResetEnd;
return;
}
PrintStr("Unmanaged Interrupt.\n\r"); }
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:
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.
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))
{
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;
UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk)
| (UOTGHS_HSTPIPCFG_PTOKEN_SETUP & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
volatile uint64_t *ptrRequestPayload =
(volatile uint64_t*)&(((volatile uint64_t(*)[0x8000])UOTGHS_RAM_ADDR)[0]);
*ptrRequestPayload = *((uint64_t*)&SetupPacketData);
PrintStr("Sending SETUP packet + DATA0.\r\n");
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_TXSTPES;
UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_FIFOCONC;
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.
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_TXSTPI)
{
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_PFREEZES; UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXSTPIC;
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_SETUP)
{
if(hcd_ControlStructure.Direction == USB_REQ_DIR_IN) {
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.
void HCD_SendCtrlInPacket(void)
{
UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk)
| (UOTGHS_HSTPIPCFG_PTOKEN_IN & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXINIC;
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_RXINES;
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:
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_RXINI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXINIC;
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_DATA_IN)
{
HCD_GetCtrlInDataPacket(); 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.
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]);
while(ByteReceived)
{
hcd_ControlStructure.ptrBuffer[hcd_ControlStructure.Index] = *ptrReceiveBuffer++;
hcd_ControlStructure.Index ++;
ByteReceived--;
}
if((hcd_ControlStructure.Index == hcd_ControlStructure.ByteCount) || IsReceivedPacketShort)
{
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_ZLP_OUT;
HCD_SentCtrlOutEmptyPacket();
return;
}
}
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.
void HCD_SentCtrlOutEmptyPacket(void)
{
UOTGHS->UOTGHS_HSTPIPCFG[0] = (UOTGHS->UOTGHS_HSTPIPCFG[0] & ~UOTGHS_HSTPIPCFG_PTOKEN_Msk)
| (UOTGHS_HSTPIPCFG_PTOKEN_OUT & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXOUTIC;
UOTGHS->UOTGHS_HSTPIPIER[0] = UOTGHS_HSTPIPIER_TXOUTES;
UOTGHS->UOTGHS_HSTPIPIDR[0] = UOTGHS_HSTPIPIDR_FIFOCONC;
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).
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_TXOUTI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXOUTIC;
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_ZLP_OUT)
{
(*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:
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.
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:
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:
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):
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:
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.