Pass the first enumeration
At the end of this part, the microcontroller will be able to finish the bus enumeration on to the bus, so in the next part, we can take care of communicating with the microcontroller via USB.
Certain events that can occur while using the USB (such as a successful enumeration or an error) are important for the later application. Therefore, the first step is to define a structure that holds the callbacks for certain events:
These callbacks are now declared in the main program and the structure is created:
Next the USB_Init function will be updated to save the callbacks:
Now the USB interrupt is adapted to call the error callback if the configuration of the control endpoint fails:
The variable _USBEvents is available externally for other source files, thanks to the extern keyword. So the callbacks can be used anywhere in the driver code.
With USB, every communication host is initiated by, i.e. USB devices are not able to send data independently. The USB host must therefore periodically poll the individual bus participants for new data (this method is also called pollen) or use interrupts (not used in this example).
The query should take place in the main function of the application:
The configuration or parameterization of the USB device by the host is carried out using so-called standard requests, which are sent via SETUP packets.
The host can use these requests to query certain information (such as the descriptors) from the device or to make certain configurations on the device.
The USB_Poll function uses the state machine to check whether the microcontroller is still connected to the USB.
When the microcontroller is connected to the bus, the currently active endpoint is read from the UENUM register and saved, switched to the control endpoint and checked whether a SETUP packet has been received by querying the RXSTPI bit in the UEINTX register of the endpoint becomes. If no SETUP packet has been received, the software switches back to the previous end point and exits the function.
Otherwise, the software jumps to the USBDevice_ControlRequest function, in which the received SETUP package is read in and processed further. In this function, the received requests are evaluated. For the sake of simplicity, I limit the number of evaluated requests in this simple example to the minimum required requests:
SET_ADDRESS – Is required to set the address that the host assigns during the enumeration
GET_DESCRIPTOR – Is needed to send the different descriptors to the host
SET_CONFIGURATION – Is required to recognize the end of the enumeration
All communication takes place via the IN and OUT control end points. Data is thus read (e.g. the address that the host sent to the device) and data is written (e.g. the descriptors that the device sends to the host). The sequence of a write or read process is described in the data sheet of the USB device controller.
At the beginning of each control request, the RXSTPI bit is set by the USB controller. This signals the CPU that a new SETUP package has arrived and can be read out:
For this purpose, a pointer to the static variable _ControlRequest is created and then the individual data bytes are read from the control endpoint and written to the memory of the variable _ControlRequest.
So that the structure is filled correctly, it must be declared with the attribute __attribute__((packed)). Otherwise, the compiler may add additional bytes for a padding, which means that the data is not saved correctly when iterating over the memory area using a pointer to the structure.
After the data has been read, the RXSTPI bit must be cleared:
Deleting the RXSTPI bit signals the end of the SETUP stage and the system switches to the next stage. Depending on the type of request, the next stage is the DATA or STATUS stage. The type of request can be determined using the bRequest field of the _ControlRequest structure and queried using a switch statement:
At the beginning, the USB host sends a GET_DESCRIPTOR request to the connected device so that the host can read in the different descriptors to obtain information about the size of the control endpoint.
It makes sense to evaluate the field bmRequestType of the request, in addition to the standard requests, class-specific requests can also be used, whereby the different USB classes extend the function of a request with class-specific functions (e.g. the HID class).
In the next step, the field bmRequestType will be evaluated, which is filled with 0x80 for a standard request GET_DESCRIPTOR request. The following information results from this value:
Device to host (IN-Transfer)
Standard request
Recipient: Device
The following functions are defined for the other fields of the request:
Field
Description
wValue
Descriptor type and index
wIndex
Null oder language id (for a string descriptor)
wLength
Descriptor size
Data
Descriptor data
Now a function is needed to load the corresponding descriptor from the program memory based on the parameters wValue and wIndex. In the second step, wLength data bytes of the descriptor must be transmitted to the host. The function for loading the descriptors can e.g. look like this:
The USB_GetDescriptor function expects the values of wValue and wIndex from the host's control request as a parameter, as well as a pointer to a memory area for the length of the descriptor, and returns a pointer to the memory area of the descriptor. To do this, the type and index of the descriptor are first extracted from the parameter wValue and then the address and the size of the descriptor are determined using a switch statement.
The length of he device and configuration descriptors can be determined by using the sizeof() function and the structure of the descriptor. This does not work with the string descriptor, because sizeof() only returns the size of the pointer to the first element for an array. In this case, the size of the descriptor must therefore be read from the bLength field of the descriptor in the program memory using pgm_read_byte.
The created function is called when processing the GET_DESCRIPTOR request and the received parameters for wValue and wIndex are passed:
Now the descriptor has to be sent to the host. It should be noted that the host uses the wLength parameter to specify how many data bytes it wants to read from the descriptor. The function for sending the data looks like this:
At the beginning, the function checks whether the message to be transmitted is longer than the number of data bytes that the host requested. The data is then transferred in the subsequent while loop. First, the status of the control endpoint is checked:
If the status of the USB device changes, or the host has written a new SETUP or data packet to the OUT endpoint, the transfer stops and a corresponding error is returned.
The data transmission is then implemented in accordance with the flow diagram shown. First, the TXINI bit is used to check whether the IN endpoint has already received a new IN transaction, i.e. whether the DATA stage has started.
If a corresponding IN transaction was recognized, the endpoint is filled with data. As soon as the endpoint is filled with data, a transfer towards the host is initiated and the end point is emptied and waited for the end of the transfer:
As soon as all data has been transferred, the STATUS stage begins. The first command of the STATUS stage is always acknowledged by the USB controller with a NAK. The data sheet specifies four steps that must be processed in this case:
Set transmit ready
Wait for transmit or receive complete
If receive complete: Clear flag and exit
If transmit complete: Continue
This procedure must now be implemented in the program, whereby in this case the evaluation of Transmit Complete can be omitted:
While the software is waiting for the Receive Complete, the status of the endpoint is constantly checked and an error returned if necessary. The finished function can now be built into the processing of the GET_DESCRIPTOR request:
As soon as the host has received and evaluated the device descriptor, it defines an address for the USB device and transmits it with a SET_ADDRESS request. With this request the bit pattern for the field bmRequestType 0x00, or
Host to device (OUT-Transfer)
Standard request
Recipient: Device
The host sends the assigned device address to the connected device via the wValue field. All other fields are filled with zero or do not exist. The transmitted address must be read out by the CPU and copied into the UADD register. This is done in the following steps:
Read the address from the message
Copy the address into the UADD register, but keep ADDEN cleared
Transmit a IN message with a length of 0 bytes
Set ADDEN
The Endpoint_HandleSTATUS function takes over the STATUS stage of communication either by sending an IN packet with a data length of 0 bytes (if it was an OUT request) or by waiting for an OUT packet and including it (if it is an IN request):
So that the USB controller is not permanently stuck in the loop in the event of a disconnection from the bus, the status of the machine is also checked and the loop is exited if necessary. Finally, the status of the machine is set to USB_STATE_ADDRESSED, which means that this request has also been processed successfully.
The last necessary request is the SET_CONFIGURATION request, in which the host selects a device configuration and activates the selected configuration in the USB device. Here the bit pattern for the field bmRequestType is 0x00, or
Host to device (OUT-Transfer)
Standard request
Recipient: Device
As with the SET_ADDRESS request, this request only has the wValue field as an information carrier, which is filled with the index of the configuration selected by the host.
The host selects the configuration using the bConfigurationValue field in the configuration descriptor. This field must not be zero, since the device must then change back to the state after addressing in accordance with the specification.
As soon as the configuration index has been received, the CPU should also check whether a corresponding configuration is available. For this purpose, the corresponding configuration descriptor is read out with the help of the USB_GetDescriptor function and it is checked whether it is present.
When the configuration has been successfully completed, the device is ready for use. The ConfigurationChanged event is called. This event can be used by the application to e.g. configure additional endpoints.
The complete function USBDevice_ControlRequest now looks like this:
Now the program is copied to the microcontroller and the board is connected to the computer via USB. As soon as the program has started, the device logs on to the computer. With a look in the device manager you can check whether the registration was successful.
On a Windows PC, the USBView program can be used to display all information, such as the device configuration, the USB address, etc.
This is particularly useful for finding and recognizing errors in the enumeration.
The host has thus successfully recognized a new device and queried all descriptors and assigned a bus address. An error is currently being output because the host has not found a suitable driver.
Last updated