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:

typedef struct
{
	void (*ConfigurationChanged)(const uint8_t Configuration);
	void (*ControlRequest)(const uint8_t bRequest, const uint8_t bmRequestType, const uint16_t wValue);
	void (*Error)();
} USB_DeviceCallbacks_t;

These callbacks are now declared in the main program and the structure is created:

void USB_Event_OnError(void);
void USB_Event_ConfigurationChanged(const uint8_t Configuration);
const USB_DeviceCallbacks_t Events_USB =
{
	.Error = USB_Event_OnError,
	.ConfigurationChanged = USB_Event_ConfigurationChanged,
};

Next the USB_Init function will be updated to save the callbacks:

extern USB_DeviceCallbacks_t _USBEvents;
void USB_Init(const USB_DeviceCallbacks_t* Events)
{
	USBController_Init(USB_MODE_DEVICE, USB_SPEED_LOW);
	_USBEvents = *Events;
	_DeviceState = USB_STATE_RESET;
}

Now the USB interrupt is adapted to call the error callback if the configuration of the control endpoint fails:

ISR(USB_GEN_vect)
{
	...
	if(USBController_CheckForInterrupt(USB_EOR_INTERRUPT))
	{
		USBController_ClearInterruptFlag(USB_EOR_INTERRUPT);
		_DeviceState = USB_STATE_RESET;
		if(Endpoint_Configure(0, ENDPOINT_TYPE_CONTROL, Endpoint_ControlSize, 0))
		{
			PORTD &= ~(0x01 << 0x04);
			PORTD |= (0x01 << 0x05);
		}
		else
		{
			if(_USBEvents.Error != NULL)
			{
				_USBEvents.Error();
			}
		}
	}
}

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:

int main(void)
{
	USB_Init(&Events_USB);
	sei();
	while(1) 
	{
	    USB_Poll();
	}
}

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.

void USB_Poll(void)
{
	if(_DeviceState == USB_STATE_UNATTACHED)
	{
		return;
	}
	uint8_t CurrEndpoint = UENUM;
	UENUM = ENDPOINT_CONTROL_ADDRESS;
	if(UEINTX & (0x01 << RXSTPI))
	{
		USBDevice_ControlRequest();
	}
	UENUM = CurrEndpoint;
}

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:

uint8_t* RequestHeader = (uint8_t*)& _ControlRequest;
for(uint8_t i = 0x00; i < sizeof(USB_SetupPacket_t); i++)
{
	*(RequestHeader++) = UEDATX;
}

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:

UEINTX &= ~(0x01 << RXSTPI);

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:

void USBDevice_ControlRequest(void)
{
	uint8_t* RequestHeader = (uint8_t*)& _ControlRequest;
	for(uint8_t i = 0x00; i < sizeof(USB_SetupPacket_t); i++)
	{
		*(RequestHeader++) = UEDATX;
	}
	UEINTX &= ~(0x01 << RXSTPI);
	switch(_ControlRequest.bRequest)
	{
		case REQUEST_SET_ADDRESS:
		{
			break;
		}
		case REQUEST_GET_DESCRIPTOR:
		{
			break;
		}
		case REQUEST_SET_CONFIGURATION:
		{
			break;	
		}
	}
}

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

if(_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_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:

const void* USB_GetDescriptor(const uint16_t wValue, const uint16_t wIndex, uint16_t* Size)
{
	uint8_t DescriptorType = (wValue >> 0x08);
	uint8_t DescriptorNumber = (wValue & 0xFF);
	switch(DescriptorType)
	{
		case DESCRIPTOR_TYPE_DEVICE:
		{
			*Size = sizeof(USB_DeviceDescriptor_t);
			return &DeviceDescriptor;
		}
		case DESCRIPTOR_TYPE_CONFIGURATION:
		{
			*Size = sizeof(USB_Configuration_t);
			return &ConfigurationDescriptor;
		}
		case DESCRIPTOR_TYPE_STRING:
		{
			switch(DescriptorNumber)
			{
				case STRING_ID_LANGUAGE:
				{
					*Size = pgm_read_byte(&LANGID.bLength);
					return &LANGID;
				}
				case STRING_ID_MANUFACTURER:
				{
					*Size = pgm_read_byte(&ManufacturerString.bLength);
					return &ManufacturerString;
				}
				case STRING_ID_PRODUCT:
				{
					*Size = pgm_read_byte(&ProductString.bLength);
					return &ProductString;
				}
				case STRING_ID_SERIAL:
				{
					*Size = pgm_read_byte(&SerialString.bLength);
					return &SerialString;
				}
			}
		}
	}
	*Size = 0x00;
	return NULL;
}

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:

if(_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
{
	const void* Descriptor;
	uint16_t DescriptorSize;
	Descriptor = USB_GetDescriptor(_ControlRequest.wValue, _ControlRequest.wIndex, &DescriptorSize);
	if((DescriptorSize == 0x00) && (_USBEvents.Error != NULL))
	{
		_USBEvents.Error();
	}

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:

Endpoint_CS_State_t USB_DeviceStream_ControlIN(const void* Buffer, const uint16_t Length, const uint16_t RequestedLength)
{
	uint8_t* Buffer_Temp = (uint8_t*)Buffer;
	uint16_t Length_Temp = Length;
	uint8_t LastPacketFull = 0x00;
	if(Length > RequestedLength)
	{
		Length_Temp = RequestedLength;
	}
	while(Length_Temp)
	{
		Endpoint_CS_State_t State = USB_DeviceStream_GetControlEndpointState();
		if(State != ENDPOINT_CS_NO_ERROR)
		{
			return State;
		}
		else if(UEINTX & (0x01 << RXOUTI))
		{
			break;
		}
		if(UEINTX & (0x01 << TXINI))
		{
			while(Length_Temp && (UEBCX < Endpoint_ControlSize))
			{
				UEDATX = pgm_read_byte(Buffer_Temp++);
				Length_Temp--;
			}
			UEINTX &= ~((0x01 << TXINI) | (0x01 << FIFOCON));
		}
	}
	while(!(UEINTX & (0x01 << TXINI)));
	UEINTX &= ~((0x01 << TXINI) | (0x01 << FIFOCON));
	while(!(UEINTX & (0x01 << RXOUTI)))
	{
		Endpoint_CS_State_t State = USBStream_GetControlEndpointState();
		if(State != ENDPOINT_CS_NO_ERROR)
		{
			return State;
		}
	}
	UEINTX &= ~((0x01 << RXOUTI) | (0x01 << FIFOCON));
	return ENDPOINT_CS_NO_ERROR;
}

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:

static Endpoint_CS_State_t USB_DeviceStream_GetControlEndpointState(void)
{
	if(_DeviceState == USB_STATE_UNATTACHED)
	{
		return ENDPOINT_CS_DISCONNECT;
	}
	else if(__DeviceState == USB_STATE_SUSPEND)
	{
		return ENDPOINT_CS_SUSPEND;
	}
	else if(UEINTX & (0x01 << RXSTPI))
	{
		return ENDPOINT_CS_ABORT_FROM_HOST;
	}
	
	return ENDPOINT_CS_NO_ERROR;
}
...
Endpoint_CS_State_t State = USB_DeviceStream_GetControlEndpointState();
if(State != ENDPOINT_CS_NO_ERROR)
{
	return State;
}
else if(UEINTX & (0x01 << RXOUTI))
{
	break;
}

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:

if(UEINTX & (0x01 << TXINI))
{
	while(Length_Temp && (UEBCX < ENDPOINT_CONTROL_SIZE))
	{
		UEDATX = pgm_read_byte(Buffer_Temp++);
		Length_Temp--;
	}
	UEINTX &= ~((0x01 << TXINI) | (0x01 << FIFOCON));
	while(!(UEINTX & (0x01 << TXINI));
}

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:

UEINTX &= ~((0x01 << TXINI) | (0x01 << FIFOCON));
while(!(UEINTX & (0x01 << RXOUTI)))
{
	Endpoint_CS_State_t State = USB_DeviceStream_GetControlEndpointState();
	if(State != ENDPOINT_CS_NO_ERROR)
	{
		return State;
	}
}
UEINTX &= ~((0x01 << RXOUTI) | (0x01 << FIFOCON));

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:

case REQUEST_GET_DESCRIPTOR:
{
	if(__ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
	{
		const void* Descriptor;
		uint16_t DescriptorSize;
		Descriptor = USB_GetDescriptor(__ControlRequest.wValue, __ControlRequest.wIndex, &DescriptorSize);
		if((DescriptorSize == 0x00) && (__USBEvents.Error != NULL))
		{
			__USBEvents.Error();
		}
		USB_DeviceStream_ControlIN(Descriptor, DescriptorSize, __ControlRequest.wLength);
	}
	break;
}

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:

  1. Read the address from the message

  2. Copy the address into the UADD register, but keep ADDEN cleared

  3. Transmit a IN message with a length of 0 bytes

  4. Set ADDEN

case REQUEST_SET_ADDRESS:
{
	if(__ControlRequest.bmRequestType == (REQUEST_DIRECTION_HOST_TO_DEVICE | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
	{
		uint8_t Address = (__ControlRequest.wValue & 0x7F);
		UDADDR = (Address & 0x7F);
		Endpoint_HandleSTATUS(__ControlRequest.bmRequestType);
		UDADDR |= (0x01 << ADDEN);
		__DeviceState = USB_STATE_ADDRESSED;
	}
}

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):

void Endpoint_HandleSTATUS(const USB_RequestDirection_t Direction)
{
	if(Direction & REQUEST_DIRECTION_DEVICE_TO_HOST)
	{
		while(!(Endpoint_OUTReceived()))
		{
			if(__DeviceState == USB_STATE_UNATTACHED)
			{
				return;
			}
		}
		Endpoint_AckOUT();
	}
	else
	{
		Endpoint_FlushIN();
		while(!(Endpoint_INReady()))
		{
			if(__DeviceState == USB_STATE_UNATTACHED)
			{
				return;
			}
		}
	}
}

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.

case REQUEST_SET_CONFIGURATION:
{
	if(__ControlRequest.bmRequestType == (REQUEST_DIRECTION_HOST_TO_DEVICE | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
	{
		__Configuration = (uint8_t)__ControlRequest.wValue & 0xFF;
		if(__Configuration > 0x00)
		{
			USB_DeviceDescriptor_t* Descriptor;
			Endpoint_HandleSTATUS(__ControlRequest.bmRequestType);
			if((USB_GetDescriptor((DESCRIPTOR_TYPE_CONFIGURATION << 0x08) | (__Configuration - 0x01), __ControlRequest.wIndex, (void*)&Descriptor) == 0x00) && (__USBEvents.Error != NULL))
			{
				__USBEvents.Error();
			}
			else
			{
				__DeviceState = USB_STATE_CONFIGURED;
				if(__USBEvents.ConfigurationChanged != NULL)
				{
					__USBEvents.ConfigurationChanged(__Configuration);
				}
			}
		}
		else
		{
			__DeviceState = USB_STATE_CONFIGURED;
		}
	}
}

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:

void USBDevice_ControlRequest(void)
{
	uint8_t* RequestHeader = (uint8_t*)&__ControlRequest;
	for(uint8_t i = 0x00; i < sizeof(USB_SetupPacket_t); i++)
	{
		*(RequestHeader++) = UEDATX;
	}
	UEINTX &= ~(0x01 << RXSTPI);
	switch(__ControlRequest.bRequest)
	{
		case REQUEST_SET_ADDRESS:
		{
			if(__ControlRequest.bmRequestType == (REQUEST_DIRECTION_HOST_TO_DEVICE | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
			{
				uint8_t Address = (__ControlRequest.wValue & 0x7F);
				UDADDR = (Address & 0x7F);
				Endpoint_HandleSTATUS(__ControlRequest.bmRequestType);
				UDADDR |= (0x01 << ADDEN);
				__DeviceState = USB_STATE_ADDRESSED;
			}
			break;
		}
		case REQUEST_GET_DESCRIPTOR:
		{
			if(__ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
			{
				const void* Descriptor;
				uint16_t DescriptorSize;
				Descriptor = USB_GetDescriptor(__ControlRequest.wValue, __ControlRequest.wIndex, &DescriptorSize);
				if((DescriptorSize == 0x00) && (__USBEvents.Error != NULL))
				{
					__USBEvents.Error();
				}
				USB_DeviceStream_ControlIN(Descriptor, DescriptorSize, __ControlRequest.wLength);
			}
			break;
		}
		case REQUEST_SET_CONFIGURATION:
		{
			if(__ControlRequest.bmRequestType == (REQUEST_DIRECTION_HOST_TO_DEVICE | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
			{
				__Configuration = (uint8_t)__ControlRequest.wValue & 0xFF;
				if(__Configuration > 0x00)
				{
					USB_DeviceDescriptor_t* Descriptor;
					Endpoint_HandleSTATUS(__ControlRequest.bmRequestType);
					if((USB_GetDescriptor((DESCRIPTOR_TYPE_CONFIGURATION << 0x08) | (__Configuration - 0x01), __ControlRequest.wIndex, (void*)&Descriptor) == 0x00) && (__USBEvents.Error != NULL))
					{
						__USBEvents.Error();
					}
					else
					{
						__DeviceState = USB_STATE_CONFIGURED;
						if(__USBEvents.ConfigurationChanged != NULL)
						{
							__USBEvents.ConfigurationChanged(__Configuration);
						}
					}
				}
				else
				{
					__DeviceState = USB_STATE_CONFIGURED;
				}
			}
			break;	
		}
	}
}

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.

[Port4] 
Is Port User Connectable:         yes
Is Port Debug Capable:            no
Companion Port Number:            4
Companion Hub Symbolic Link Name: USB#VID_05E3&PID_0617#6&1f56a0e&0&1#{f18a0e88-c30c-11d0-8815-00a0c906bed8}
Protocols Supported:
 USB 1.1:                         yes
 USB 2.0:                         yes
 USB 3.0:                         no
       ---===>Device Information<===---
String Descriptor for index 2 not available while device is in low power state.
ConnectionStatus:                  
Current Config Value:              0x00  -> Device Bus Speed: Low
Device Address:                    0x1A
Open Pipes:                           0
*!*ERROR:  No open pipes!
          ===>Device Descriptor<===
bLength:                           0x12
bDescriptorType:                   0x01
bcdUSB:                          0x0011
bDeviceClass:                      0xFF  -> This is a Vendor Specific Device
bDeviceSubClass:                   0x00
bDeviceProtocol:                   0x00
bMaxPacketSize0:                   0x08 = (8) Bytes
idVendor:                        0x0123 = Vendor ID not listed with USB.org
idProduct:                       0x4567
bcdDevice:                       0x0001
iManufacturer:                     0x01
String Descriptor for index 1 not available while device is in low power state.
iProduct:                          0x02
String Descriptor for index 2 not available while device is in low power state.
iSerialNumber:                     0x03
String Descriptor for index 3 not available while device is in low power state.
bNumConfigurations:                0x01
       ---===>Full Configuration Descriptor<===---
          ===>Configuration Descriptor<===
bLength:                           0x09
bDescriptorType:                   0x02
wTotalLength:                    0x0020  -> Validated
bNumInterfaces:                    0x01
bConfigurationValue:               0x01
iConfiguration:                    0x00
bmAttributes:                      0xC0  -> Self Powered
MaxPower:                          0x32 = 100 mA
          ===>Interface Descriptor<===
bLength:                           0x09
bDescriptorType:                   0x04
bInterfaceNumber:                  0x00
bAlternateSetting:                 0x00
bNumEndpoints:                     0x02
bInterfaceClass:                   0xFF  -> Interface Class Unknown to USBView
bInterfaceSubClass:                0x00
bInterfaceProtocol:                0x00
iInterface:                        0x00
          ===>Endpoint Descriptor<===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x81  -> Direction: IN - EndpointID: 1
bmAttributes:                      0x03  -> Interrupt Transfer Type
wMaxPacketSize:                  0x0008
bInterval:                         0x0A
          ===>Endpoint Descriptor<===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x02  -> Direction: OUT - EndpointID: 2
bmAttributes:                      0x03  -> Interrupt Transfer Type
wMaxPacketSize:                  0x0008
bInterval:                         0x0A

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